Compare commits
65 Commits
master
...
07492b5b57
| Author | SHA1 | Date | |
|---|---|---|---|
| 07492b5b57 | |||
| 14913bcd3d | |||
| 041650c287 | |||
| a63ffbc731 | |||
| 9d2f1cc5bd | |||
| f72f9feb9a | |||
| 417737f904 | |||
| a06c2ebdcf | |||
| e174ecda42 | |||
| 5246a905bb | |||
| d00d84b69c | |||
| 4dafa8c596 | |||
| 53584b8730 | |||
| ce40468b77 | |||
| 4b4fab3c33 | |||
| db2fab5e7d | |||
| 42be0a5919 | |||
| 17400fa6ff | |||
| deb14d2c94 | |||
| 65c5b1029b | |||
| a1fbb7c21d | |||
| d076ac8fef | |||
| c82e4afc4d | |||
| ac72fdc4a6 | |||
| b2e050c467 | |||
| 914d7dd9d1 | |||
| 21bf8f79e2 | |||
| 38feff4369 | |||
| 404529e8a4 | |||
| 3db18858c3 | |||
| 72a9576abe | |||
| 462c2c7dfe | |||
| ef8bfeaf84 | |||
| ba6be037ac | |||
| 5ca1c7f570 | |||
| cd8e477534 | |||
| c439e9741e | |||
| 0a5c70e49f | |||
| 19f2c9efe2 | |||
| 230c74fd9b | |||
| a2c19e2e49 | |||
| 9f9a4b38b8 | |||
| 524356cf8d | |||
| a2993212ca | |||
| fba2e5fc94 | |||
| cf68a93b45 | |||
| 3000268a0e | |||
| 196c5be67a | |||
| 33bad193b4 | |||
| d5277802f7 | |||
| 28e6cf50d1 | |||
| 42cb5071c2 | |||
| 23a798a63a | |||
| 5dd84c6b39 | |||
| d0ae20911b | |||
| b6d06dd96d | |||
| 09b3ed44ba | |||
| 603abe63cb | |||
| 1c0e6533ae | |||
| 00aa614cfd | |||
| 4905d10bc0 | |||
| b4e8ccb77f | |||
| 2a82ee4d5c | |||
| 7b213e7456 | |||
| cfc261878a |
@@ -1,6 +1,4 @@
|
||||
skip_list:
|
||||
- run-once
|
||||
- var-naming[no-role-prefix] # user-facing API dicts (cis, system, hypervisor) are intentionally not role-prefixed
|
||||
- args[module] # false positives from variable-based module_defaults (_proxmox_auth, _vmware_auth)
|
||||
exclude_paths:
|
||||
- roles/global_defaults/
|
||||
|
||||
526
README.md
526
README.md
@@ -1,8 +1,8 @@
|
||||
# Ansible Bootstrap
|
||||
|
||||
Automated Linux system bootstrap using the Arch Linux ISO as a universal installer. Deploys any supported distribution on virtual or physical targets via Infrastructure-as-Code.
|
||||
An Ansible playbook for automating Linux system bootstrap in an Infrastructure-as-Code manner. It uses the Arch Linux ISO as a foundational tool to provide an efficient and systematic method for the automatic deployment of a variety of Linux distributions on designated target systems, ensuring a standardized setup across different platforms.
|
||||
|
||||
Non-Arch targets require the appropriate package manager available from the ISO environment (e.g. `dnf` for RHEL-family). Set `system.features.chroot.tool` if `arch-chroot` is unavailable.
|
||||
Most roles are adaptable for use with systems beyond Arch Linux, requiring only that the target system can install the necessary package manager (e.g. `dnf` for RHEL-based systems). A replacement for the `arch-chroot` command may also be required; set `system.features.chroot.tool` accordingly.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -13,33 +13,34 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
- 4.1 [Core Variables](#41-core-variables)
|
||||
- 4.2 [`system` Dictionary](#42-system-dictionary)
|
||||
- 4.3 [`hypervisor` Dictionary](#43-hypervisor-dictionary)
|
||||
- 4.4 [`cis` Dictionary](#44-cis-dictionary)
|
||||
- 4.5 [VMware Guest Operations](#45-vmware-guest-operations)
|
||||
- 4.6 [Multi-Disk Schema](#46-multi-disk-schema)
|
||||
- 4.7 [Advanced Partitioning Overrides](#47-advanced-partitioning-overrides)
|
||||
- 4.8 [Cleanup Defaults](#48-cleanup-defaults)
|
||||
5. [Execution Pipeline](#5-execution-pipeline)
|
||||
6. [Usage](#6-usage)
|
||||
7. [Security](#7-security)
|
||||
- 4.4 [VMware Guest Operations](#44-vmware-guest-operations)
|
||||
- 4.5 [Multi-Disk Schema](#45-multi-disk-schema)
|
||||
- 4.6 [Advanced Partitioning Overrides](#46-advanced-partitioning-overrides)
|
||||
5. [How to Use the Playbook](#5-how-to-use-the-playbook)
|
||||
- 5.1 [Prerequisites](#51-prerequisites)
|
||||
- 5.2 [Running the Playbook](#52-running-the-playbook)
|
||||
- 5.3 [Example Usage](#53-example-usage)
|
||||
6. [Security](#6-security)
|
||||
7. [Operational Notes](#7-operational-notes)
|
||||
8. [Safety](#8-safety)
|
||||
|
||||
## 1. Supported Platforms
|
||||
|
||||
### Distributions
|
||||
|
||||
| `system.os` | Distribution | `system.version` |
|
||||
| ------------ | ------------------------ | ------------------------------------- |
|
||||
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
|
||||
| `alpine` | Alpine Linux | latest (rolling) |
|
||||
| `archlinux` | Arch Linux | latest (rolling) |
|
||||
| `debian` | Debian | `10`-`13`, `unstable` |
|
||||
| `fedora` | Fedora | `38`-`45` |
|
||||
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
|
||||
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
|
||||
| `rocky` | Rocky Linux | `8`, `9`, `10` |
|
||||
| `ubuntu` | Ubuntu (latest non-LTS) | optional (e.g. `24.04`) |
|
||||
| `ubuntu-lts` | Ubuntu LTS | optional (e.g. `24.04`) |
|
||||
| `void` | Void Linux | latest (rolling) |
|
||||
| `system.os` | Distribution | `system.version` |
|
||||
| ------------ | ------------------------ | ------------------------------- |
|
||||
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
|
||||
| `alpine` | Alpine Linux | latest (rolling) |
|
||||
| `archlinux` | Arch Linux | latest (rolling) |
|
||||
| `debian` | Debian | `10`, `11`, `12`, `13`, `unstable` |
|
||||
| `fedora` | Fedora | `40`, `41`, `42`, `43` |
|
||||
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
|
||||
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
|
||||
| `rocky` | Rocky Linux | `8`, `9`, `10` |
|
||||
| `ubuntu` | Ubuntu | latest |
|
||||
| `ubuntu-lts` | Ubuntu LTS | latest |
|
||||
| `void` | Void Linux | latest (rolling) |
|
||||
|
||||
### Hypervisors
|
||||
|
||||
@@ -54,28 +55,28 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
## 2. Compatibility Notes
|
||||
|
||||
- `rhel_iso` is required for `system.os: rhel`.
|
||||
- RHEL installs should use `ext4` or `xfs` (not `btrfs`).
|
||||
- `custom_iso: true` skips ArchISO validation; your installer must provide required tooling.
|
||||
- On non-Arch installers, set `system.features.chroot.tool` explicitly.
|
||||
- RHEL installs should use `system.filesystem: ext4` or `system.filesystem: xfs` (not `btrfs`).
|
||||
- For RHEL 8 specifically, prefer `ext4` over `xfs` if you hit installer/filesystem edge cases.
|
||||
- `custom_iso: true` skips ArchISO validation and pacman preparation; your installer image must already provide required tooling.
|
||||
- On non-Arch installers, set `system.features.chroot.tool` (`arch-chroot`, `chroot`, or `systemd-nspawn`) explicitly as needed.
|
||||
|
||||
## 3. Configuration Model
|
||||
|
||||
Two dict-based variables drive the entire configuration:
|
||||
The project uses two dict-based variables:
|
||||
|
||||
- **`system`** -- host, network, users, disk layout, encryption, and feature toggles
|
||||
- **`hypervisor`** -- virtualization backend credentials and targeting
|
||||
- `system` for host/runtime/install configuration
|
||||
- `hypervisor` for virtualization backend configuration
|
||||
|
||||
An optional third dict **`cis`** overrides CIS hardening parameters when `system.features.cis.enabled: true`.
|
||||
|
||||
All three are standard Ansible variables. Place them in `group_vars/`, `host_vars/`, or inline inventory. With `hash_behaviour = merge`, dictionaries merge across scopes, so shared values go in group vars and host-specific overrides go per-host.
|
||||
These are normal Ansible variables and belong in host/group vars. You can define them in inventory host entries, `group_vars/*`, or `host_vars/*`. Dictionary variables are merged across scopes (`group_vars` -> `host_vars`) by project config (`hash_behaviour = merge`), so you can set shared values like `system.filesystem` once in group vars and override only host-specific keys per host.
|
||||
|
||||
### Variable Placement
|
||||
|
||||
| Location | Scope | Typical use |
|
||||
| ------------------------ | ----------- | -------------------------------------------------------------- |
|
||||
| `group_vars/all.yml` | All hosts | Shared `hypervisor`, `system.filesystem`, `boot_iso` |
|
||||
| `group_vars/<group>.yml` | Group | Environment-specific defaults |
|
||||
| `host_vars/<host>.yml` | Single host | Host-specific overrides (`system.network.ip`, `system.id`, etc.) |
|
||||
| Location | Scope | Typical use |
|
||||
| -------------------------- | ----------- | ------------------------------------------------------------ |
|
||||
| `group_vars/all.yml` | All hosts | Shared defaults like `hypervisor`, `system.filesystem`, `boot_iso` |
|
||||
| `group_vars/<group>.yml` | Group | Environment or role-specific defaults |
|
||||
| `host_vars/<host>.yml` | Single host | Host-specific overrides |
|
||||
| Inventory inline host vars | Single host | Inline definitions for quick setup |
|
||||
|
||||
### Example Inventory
|
||||
|
||||
@@ -89,9 +90,8 @@ all:
|
||||
type: proxmox
|
||||
url: pve01.example.com
|
||||
username: root@pam
|
||||
password: !vault |
|
||||
$ANSIBLE_VAULT...
|
||||
node: pve01
|
||||
password: CHANGE_ME
|
||||
host: pve01
|
||||
storage: local-lvm
|
||||
|
||||
children:
|
||||
@@ -107,6 +107,7 @@ all:
|
||||
id: 101
|
||||
cpus: 2
|
||||
memory: 4096
|
||||
balloon: 0
|
||||
network:
|
||||
bridge: vmbr0
|
||||
ip: 10.0.0.10
|
||||
@@ -123,24 +124,19 @@ all:
|
||||
fstype: xfs
|
||||
users:
|
||||
- name: ops
|
||||
password: !vault |
|
||||
$ANSIBLE_VAULT...
|
||||
password: CHANGE_ME
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
sudo: true
|
||||
root:
|
||||
password: !vault |
|
||||
$ANSIBLE_VAULT...
|
||||
password: CHANGE_ME
|
||||
luks:
|
||||
enabled: true
|
||||
passphrase: !vault |
|
||||
$ANSIBLE_VAULT...
|
||||
passphrase: CHANGE_ME
|
||||
auto: true
|
||||
method: tpm2
|
||||
tpm2:
|
||||
pcrs: "7"
|
||||
features:
|
||||
cis:
|
||||
enabled: true
|
||||
firewall:
|
||||
enabled: true
|
||||
backend: firewalld
|
||||
@@ -151,308 +147,270 @@ all:
|
||||
|
||||
### 4.1 Core Variables
|
||||
|
||||
Top-level variables outside `system`/`hypervisor`/`cis`.
|
||||
These top-level variables sit outside the `system`/`hypervisor` dictionaries.
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
| ---------------- | ------ | -------------------------- | ---------------------------------------------------- |
|
||||
| `boot_iso` | string | -- | Boot ISO path (required for virtual installs) |
|
||||
| `rhel_iso` | string | -- | RHEL ISO path (required when `system.os: rhel`) |
|
||||
| `custom_iso` | bool | `false` | Skip ArchISO validation and pacman setup |
|
||||
| `thirdparty_tasks` | string | `dropins/preparation.yml` | Drop-in task file included during environment setup |
|
||||
| Variable | Type | Description |
|
||||
| ----------------------------------- | ------ | ------------------------------------------------ |
|
||||
| `boot_iso` | string | Path to the boot ISO image (required for virtual installs). |
|
||||
| `rhel_iso` | string | Path to the RHEL ISO (required when `system.os: rhel`). |
|
||||
| `custom_iso` | bool | Skip ArchISO validation and pacman setup. Default `false`. |
|
||||
| `thirdparty_tasks` | string | Drop-in task file included during environment setup. Default `dropins/preparation.yml`. |
|
||||
|
||||
### 4.2 `system` Dictionary
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------ | ---------- | ------------------ | ------------------------------------------------------ |
|
||||
| `type` | string | `virtual` | `virtual` or `physical` |
|
||||
| `os` | string | -- | Target distribution (see [table](#distributions)) |
|
||||
| `version` | string | -- | Version selector for versioned distros |
|
||||
| `filesystem` | string | -- | `btrfs`, `ext4`, or `xfs` |
|
||||
| `name` | string | inventory hostname | Final hostname |
|
||||
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
|
||||
| `locale` | string | `en_US.UTF-8` | System locale |
|
||||
| `keymap` | string | `us` | Console keymap |
|
||||
| `id` | int/string | -- | VMID (required for Proxmox) |
|
||||
| `cpus` | int | `0` | vCPU count (required for virtual) |
|
||||
| `memory` | int | `0` | Memory in MiB (required for virtual) |
|
||||
| `balloon` | int | `0` | Balloon memory in MiB (Proxmox) |
|
||||
| `path` | string | -- | Hypervisor folder/path |
|
||||
| `packages` | list | `[]` | Additional packages installed post-reboot |
|
||||
| `network` | dict | see below | Network configuration |
|
||||
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#46-multi-disk-schema)) |
|
||||
| `users` | list | `[]` | User accounts |
|
||||
| `root` | dict | see below | Root account settings |
|
||||
| `luks` | dict | see below | Encryption settings |
|
||||
| `features` | dict | see below | Feature toggles |
|
||||
Top-level host install/runtime settings. Use these keys under `system`.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------ | ---------- | -------------------- | ---------------------------------------- |
|
||||
| `type` | string | `virtual` | `virtual` or `physical` |
|
||||
| `os` | string | empty | Target distribution (see [table](#distributions)) |
|
||||
| `version` | string | empty | Version selector for distro families |
|
||||
| `filesystem` | string | empty | `btrfs`, `ext4`, or `xfs` |
|
||||
| `name` | string | inventory hostname | Final hostname |
|
||||
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
|
||||
| `locale` | string | `en_US.UTF-8` | System locale |
|
||||
| `keymap` | string | `us` | Console keymap (`vconsole.conf`) |
|
||||
| `id` | int/string | empty | VMID (required for Proxmox) |
|
||||
| `cpus` | int | `0` | vCPU count |
|
||||
| `memory` | int | `0` | Memory in MiB |
|
||||
| `balloon` | int | `0` | Balloon memory in MiB |
|
||||
| `path` | string | empty | Hypervisor folder/path (libvirt/vmware) |
|
||||
| `packages` | list | `[]` | Additional packages installed post-reboot |
|
||||
| `network` | dict | see below | Network configuration |
|
||||
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#45-multi-disk-schema)) |
|
||||
| `users` | list | `[]` | User accounts (see below) |
|
||||
| `root` | dict | see below | Root account settings |
|
||||
| `luks` | dict | see below | Encryption settings |
|
||||
| `features` | dict | see below | Feature toggles |
|
||||
|
||||
#### `system.network`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------------- | ---------- | ------- | ---------------------------------------------- |
|
||||
| `bridge` | string | -- | Hypervisor network/bridge name |
|
||||
| `vlan` | string/int | -- | VLAN tag |
|
||||
| `ip` | string | -- | Static IP (omit for DHCP) |
|
||||
| `prefix` | int | -- | CIDR prefix (1-32, required with `ip`) |
|
||||
| `gateway` | string | -- | Default gateway |
|
||||
| `dns.servers` | list | `[]` | DNS resolvers (must be a YAML list) |
|
||||
| `dns.search` | list | `[]` | Search domains (must be a YAML list) |
|
||||
| `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) |
|
||||
| Key | Type | Default | Description |
|
||||
| -------------- | ---------- | ------- | ---------------------------------------------------- |
|
||||
| `bridge` | string | empty | Hypervisor network/bridge name |
|
||||
| `vlan` | string/int | empty | VLAN tag |
|
||||
| `ip` | string | empty | Static IP (omit for DHCP) |
|
||||
| `prefix` | int | empty | CIDR prefix for static IP |
|
||||
| `gateway` | string | empty | Default gateway (static only) |
|
||||
| `dns.servers` | list | `[]` | DNS resolvers (must be a YAML list) |
|
||||
| `dns.search` | list | `[]` | Search domains (must be a YAML list) |
|
||||
| `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) |
|
||||
|
||||
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry list. When `interfaces` is set, it takes precedence. Each entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`.
|
||||
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry `interfaces[]` list. When `interfaces` is set, it takes precedence and the flat fields are back-populated from `interfaces[0]` for backward compatibility. Each `interfaces[]` entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`.
|
||||
|
||||
#### `system.users`
|
||||
|
||||
Dict keyed by username. At least one user must have a `password` (used for SSH access during bootstrap). Users without a password get locked accounts (key-only auth).
|
||||
A list of user account dictionaries. Credentials for the first user are prompted interactively by default via `vars_prompt` in `main.yml`, but can be supplied via inventory, vars files, or `-e` for non-interactive runs.
|
||||
|
||||
```yaml
|
||||
system:
|
||||
users:
|
||||
svcansible:
|
||||
password: "vault_lookup"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
appuser:
|
||||
sudo: "ALL=(ALL) NOPASSWD: ALL"
|
||||
keys:
|
||||
- "ssh-ed25519 BBBB..."
|
||||
```
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ----------- | ------- | -------------------------------------------------- |
|
||||
| *(dict key)* | string | -- | Username (required) |
|
||||
| `password` | string | -- | User password (required for at least one user) |
|
||||
| `keys` | list | `[]` | SSH public keys |
|
||||
| `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string |
|
||||
|
||||
Users must be defined in inventory. The dict format enables additive merging across inventory layers with `hash_behaviour=merge`.
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ------ | ------- | -------------------------------------------- |
|
||||
| `name` | string | empty | Username created on target (required) |
|
||||
| `password` | string | empty | User password (also used for sudo) |
|
||||
| `keys` | list | `[]` | SSH public keys for `authorized_keys` |
|
||||
| `sudo` | string | empty | Custom sudoers rule (optional, per-user) |
|
||||
|
||||
#### `system.root`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ------ | ------- | ------------- |
|
||||
| `password` | string | -- | Root password |
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ------ | ------- | -------------- |
|
||||
| `password` | string | empty | Root password |
|
||||
|
||||
#### `system.luks`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------ | ------ | ------------------ | ------------------------------------------ |
|
||||
| `enabled` | bool | `false` | Enable encrypted root |
|
||||
| `passphrase` | string | -- | Passphrase for format/open/enroll |
|
||||
| `mapper` | string | `SYSTEM_DECRYPTED` | Mapper name under `/dev/mapper` |
|
||||
| `auto` | bool | `true` | Auto-unlock toggle |
|
||||
| `method` | string | `tpm2` | Auto-unlock backend: `tpm2` or `keyfile` |
|
||||
| `keysize` | int | `64` | Keyfile size in bytes |
|
||||
| `options` | string | `discard,tries=3` | Additional crypttab options |
|
||||
| `type` | string | `luks2` | LUKS format type |
|
||||
| `cipher` | string | `aes-xts-plain64` | Cipher |
|
||||
| `hash` | string | `sha512` | Hash algorithm |
|
||||
| `iter` | int | `4000` | PBKDF iteration time (ms) |
|
||||
| `bits` | int | `512` | Key size (bits) |
|
||||
| `pbkdf` | string | `argon2id` | PBKDF algorithm |
|
||||
| `urandom` | bool | `true` | Use urandom during key generation |
|
||||
| `verify` | bool | `true` | Verify passphrase during format |
|
||||
LUKS container, unlock, and initramfs-related settings.
|
||||
|
||||
| Key | Type | Default | Allowed | Description |
|
||||
| ------------ | ------ | ------------------ | -------------------------- | ------------------------------------------ |
|
||||
| `enabled` | bool | `false` | `true`/`false` | Enable encrypted root workflow |
|
||||
| `passphrase` | string | empty | any string | Passphrase used for format/open/enroll |
|
||||
| `mapper` | string | `SYSTEM_DECRYPTED` | mapper name | Mapper name under `/dev/mapper` |
|
||||
| `auto` | bool | `true` | `true`/`false` | Auto-unlock behavior toggle |
|
||||
| `method` | string | `tpm2` | `tpm2`, `keyfile` | Auto-unlock backend when `auto=true` |
|
||||
| `keysize` | int | `64` | positive int | Keyfile size (bytes) for keyfile mode |
|
||||
| `options` | string | `discard,tries=3` | crypttab opts | Additional crypttab/kernel options |
|
||||
| `type` | string | `luks2` | cryptsetup type | LUKS format type |
|
||||
| `cipher` | string | `aes-xts-plain64` | cipher name | Cryptsetup cipher |
|
||||
| `hash` | string | `sha512` | hash name | Cryptsetup hash |
|
||||
| `iter` | int | `4000` | positive int | PBKDF iteration time (ms) |
|
||||
| `bits` | int | `512` | positive int | Key size (bits) |
|
||||
| `pbkdf` | string | `argon2id` | pbkdf name | PBKDF algorithm |
|
||||
| `urandom` | bool | `true` | `true`/`false` | Use urandom during key generation |
|
||||
| `verify` | bool | `true` | `true`/`false` | Verify passphrase during format |
|
||||
|
||||
#### `system.luks.tpm2`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------- | ------------- | ------- | ---------------------------------------------- |
|
||||
| `device` | string | `auto` | TPM2 device selector |
|
||||
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`); empty = no PCR binding |
|
||||
TPM2-specific policy settings used when `system.luks.method: tpm2`.
|
||||
|
||||
**TPM2 auto-unlock:** Uses `systemd-cryptenroll` on all distros. The user-set passphrase
|
||||
remains as a backup unlock method. TPM2 enrollment runs in the chroot during bootstrap;
|
||||
if it fails (e.g. no TPM2 hardware), the system boots with passphrase-only unlock and
|
||||
TPM2 can be enrolled post-deployment via `systemd-cryptenroll --tpm2-device=auto <device>`.
|
||||
|
||||
On Debian/Ubuntu, TPM2 auto-unlock requires dracut (initramfs-tools does not support `tpm2-device`).
|
||||
The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `features.initramfs.generator`.
|
||||
| Key | Type | Default | Allowed | Description |
|
||||
| ------ | ----------- | ------- | --------------------- | --------------------------------------------------- |
|
||||
| `device` | string | `auto` | `auto` or device path | TPM2 device selector |
|
||||
| `pcrs` | string/list | empty | PCR expression | PCR binding policy (e.g. `"7"` or `"0+7"`) |
|
||||
|
||||
#### `system.features`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------ | ------ | -------------- | ------------------------------------ |
|
||||
| `cis.enabled` | bool | `false` | Enable CIS hardening (see [4.4](#44-cis-dictionary)) |
|
||||
| `selinux.enabled` | bool | `true` | SELinux management |
|
||||
| `firewall.enabled` | bool | `true` | Firewall setup |
|
||||
| `firewall.backend` | string | `firewalld` | `firewalld` or `ufw` |
|
||||
| `firewall.toolkit` | string | `nftables` | `nftables` or `iptables` |
|
||||
| `ssh.enabled` | bool | `true` | SSH service/package management |
|
||||
| `zstd.enabled` | bool | `true` | zstd-related tuning |
|
||||
| `swap.enabled` | bool | `true` | Swap setup |
|
||||
| `banner.motd` | bool | `false` | MOTD banner |
|
||||
| `banner.sudo` | bool | `true` | Sudo banner |
|
||||
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` |
|
||||
| `initramfs.generator` | string | auto-detected | Override initramfs generator (see below) |
|
||||
| `desktop.*` | dict | see below | Desktop environment settings (see [4.2.5](#425-systemfeaturesdesktop)) |
|
||||
Feature toggles for optional system configuration.
|
||||
|
||||
**Initramfs generator auto-detection:** RedHat → dracut, Arch → mkinitcpio, Debian/Ubuntu → initramfs-tools.
|
||||
Override with `dracut`, `mkinitcpio`, or `initramfs-tools`. When LUKS TPM2 auto-unlock is enabled and the
|
||||
native generator does not support `tpm2-device`, the generator is automatically upgraded to dracut.
|
||||
On distros with older dracut (no `tpm2-tss` module), clevis is used as a fallback for TPM2 binding.
|
||||
|
||||
#### 4.2.5 `system.features.desktop`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ----------------- | ------ | -------------- | ----------------------------------------- |
|
||||
| `enabled` | bool | `false` | Install desktop environment |
|
||||
| `environment` | string | -- | `gnome`, `kde`, `xfce`, `sway`, `hyprland`, `cinnamon`, `mate`, `lxqt`, `budgie` |
|
||||
| `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, `lightdm`, `ly`, `greetd` |
|
||||
|
||||
When `enabled: true`, the bootstrap installs the desktop environment packages, enables the display manager
|
||||
and bluetooth services, and sets the systemd default target to `graphical.target`.
|
||||
|
||||
Display manager auto-detection: gnome→gdm, kde→sddm, xfce→lightdm, sway→greetd, hyprland→ly.
|
||||
| Key | Type | Default | Allowed | Description |
|
||||
| ------------------ | ------ | -------------- | ------------------------------------------ | ---------------------------------- |
|
||||
| `cis.enabled` | bool | `false` | `true`/`false` | Enable CIS hardening role |
|
||||
| `selinux.enabled` | bool | `true` | `true`/`false` | SELinux management |
|
||||
| `firewall.enabled` | bool | `true` | `true`/`false` | Enable firewall role actions |
|
||||
| `firewall.backend` | string | `firewalld` | `firewalld`, `ufw` | Firewall service backend |
|
||||
| `firewall.toolkit` | string | `nftables` | `nftables`, `iptables` | Packet filtering toolkit |
|
||||
| `ssh.enabled` | bool | `true` | `true`/`false` | SSH service/package management |
|
||||
| `zstd.enabled` | bool | `true` | `true`/`false` | zstd related tuning |
|
||||
| `swap.enabled` | bool | `true` | `true`/`false` | Swap setup toggle |
|
||||
| `banner.motd` | bool | `false` | `true`/`false` | MOTD banner management |
|
||||
| `banner.sudo` | bool | `true` | `true`/`false` | Sudo banner management |
|
||||
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, `systemd-nspawn` | Chroot wrapper command |
|
||||
|
||||
### 4.3 `hypervisor` Dictionary
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------ | ------ | ------- | ---------------------------------------------------- |
|
||||
| `type` | string | -- | `libvirt`, `proxmox`, `vmware`, `xen`, or `none` |
|
||||
| `url` | string | -- | API host (Proxmox/VMware) |
|
||||
| `username` | string | -- | API username |
|
||||
| `password` | string | -- | API password |
|
||||
| `node` | string | -- | Target compute node (Proxmox node / VMware ESXi host; mutually exclusive with `cluster` on VMware) |
|
||||
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
|
||||
| `datacenter` | string | -- | VMware datacenter |
|
||||
| `cluster` | string | -- | VMware cluster |
|
||||
| `certs` | bool | `true` | TLS certificate validation (VMware) |
|
||||
| `ssh` | bool | `false` | Enable SSH on guest and switch connection (VMware) |
|
||||
| Key | Type | Description |
|
||||
| ------------ | ------ | -------------------------------------------------------- |
|
||||
| `type` | string | `libvirt`, `proxmox`, `vmware`, `xen`, or `none` |
|
||||
| `url` | string | Proxmox/VMware API host |
|
||||
| `username` | string | API username |
|
||||
| `password` | string | API password |
|
||||
| `host` | string | Proxmox node name |
|
||||
| `storage` | string | Proxmox/VMware storage identifier |
|
||||
| `datacenter` | string | VMware datacenter |
|
||||
| `cluster` | string | VMware cluster |
|
||||
| `certs` | bool | TLS certificate validation for VMware |
|
||||
| `ssh` | bool | VMware: enable SSH on guest and switch connection to SSH |
|
||||
|
||||
### 4.4 `cis` Dictionary
|
||||
### 4.4 VMware Guest Operations
|
||||
|
||||
When `system.features.cis.enabled: true`, the CIS role applies hardening. All values have sensible defaults; override specific keys via the `cis` dict.
|
||||
When `hypervisor.type: vmware` uses the `vmware_tools` connection, these Ansible connection variables are required.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------------------- | ------ | ------- | ------------------------------------------------ |
|
||||
| `modules_blacklist` | list | see below | Kernel modules to blacklist via modprobe |
|
||||
| `sysctl` | dict | see below | Sysctl key/value pairs written to `10-cis.conf` |
|
||||
| `sshd_options` | list | see below | SSHD options applied via lineinfile |
|
||||
| `pwquality_minlen` | int | `14` | Minimum password length |
|
||||
| `tmout` | int | `900` | Shell timeout (seconds) |
|
||||
| `umask` | string | `077` | Default umask in bashrc |
|
||||
| `umask_profile` | string | `027` | Default umask in /etc/profile |
|
||||
| `faillock_deny` | int | `5` | Failed login attempts before lockout |
|
||||
| `faillock_unlock_time` | int | `900` | Lockout duration (seconds) |
|
||||
| `password_remember` | int | `5` | Password history depth |
|
||||
| Variable | Description |
|
||||
| ------------------------------- | -------------------------------------------------- |
|
||||
| `ansible_vmware_tools_user` | Guest OS username for guest operations |
|
||||
| `ansible_vmware_tools_password` | Guest OS password for guest operations |
|
||||
| `ansible_vmware_guest_path` | VM inventory path (`/datacenter/vm/folder/name`) |
|
||||
| `ansible_vmware_host` | vCenter/ESXi hostname |
|
||||
| `ansible_vmware_user` | vCenter/ESXi API username |
|
||||
| `ansible_vmware_password` | vCenter/ESXi API password |
|
||||
| `ansible_vmware_validate_certs` | Enable/disable TLS certificate validation |
|
||||
|
||||
**Default modules blacklist:** `freevxfs`, `jffs2`, `hfs`, `hfsplus`, `cramfs`, `udf`, `usb-storage`, `dccp`, `sctp`, `rds`, `tipc`, `firewire-core`, `firewire-sbp2`, `thunderbolt`. `squashfs` is added automatically except on Ubuntu (snap dependency).
|
||||
### 4.5 Multi-Disk Schema
|
||||
|
||||
**Default sysctl settings** include: `kernel.yama.ptrace_scope=2`, `kernel.kptr_restrict=2`, `kernel.perf_event_paranoid=3`, `kernel.unprivileged_bpf_disabled=1`, IPv4/IPv6 hardening, ARP protection, and IPv6 disabled by default. Override individual keys:
|
||||
`system.disks[0]` is always the OS disk. Additional entries define data disks.
|
||||
|
||||
```yaml
|
||||
cis:
|
||||
sysctl:
|
||||
net.ipv6.conf.all.disable_ipv6: 0 # re-enable IPv6
|
||||
net.ipv4.ip_forward: 1 # enable for routers/containers
|
||||
```
|
||||
| Key | Type | Description |
|
||||
| ------------- | ------ | ---------------------------------------------------- |
|
||||
| `size` | number | Disk size in GB (required for virtual installs) |
|
||||
| `device` | string | Explicit block device (required for physical data disks) |
|
||||
| `mount.path` | string | Mount point (for additional disks) |
|
||||
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
||||
| `mount.label` | string | Optional filesystem label |
|
||||
| `mount.opts` | string | Mount options (default: `defaults`) |
|
||||
|
||||
**Default SSHD options** enforce: `PermitRootLogin no`, `PasswordAuthentication no`, `X11Forwarding no`, `AllowTcpForwarding no`, `MaxAuthTries 4`, and post-quantum KEX (mlkem768x25519-sha256 on OpenSSH 9.9+). Override per-option:
|
||||
|
||||
```yaml
|
||||
cis:
|
||||
sshd_options:
|
||||
- { option: X11Forwarding, value: "yes" }
|
||||
- { option: AllowTcpForwarding, value: "yes" }
|
||||
```
|
||||
|
||||
Note: providing `sshd_options` replaces the entire list. Copy the defaults from `roles/cis/defaults/main.yml` and modify as needed.
|
||||
|
||||
### 4.5 VMware Guest Operations
|
||||
|
||||
When `hypervisor.type: vmware` uses the `vmware_tools` connection:
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------- | -------------------------------------------- |
|
||||
| `ansible_vmware_tools_user` | Guest OS username |
|
||||
| `ansible_vmware_tools_password` | Guest OS password |
|
||||
| `ansible_vmware_guest_path` | VM inventory path |
|
||||
| `ansible_vmware_host` | vCenter/ESXi hostname |
|
||||
| `ansible_vmware_user` | vCenter/ESXi API username |
|
||||
| `ansible_vmware_password` | vCenter/ESXi API password |
|
||||
| `ansible_vmware_validate_certs` | TLS certificate validation |
|
||||
|
||||
### 4.6 Multi-Disk Schema
|
||||
|
||||
`system.disks[0]` is the OS disk (no `mount.path`). Additional entries define data disks.
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------- | ------ | ------------------------------------------------------ |
|
||||
| `size` | number | Disk size in GB (required for virtual) |
|
||||
| `device` | string | Block device path (required for physical data disks) |
|
||||
| `partition` | string | Partition device path (required for physical data disks) |
|
||||
| `mount.path` | string | Mount point (additional disks only) |
|
||||
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
||||
| `mount.label` | string | Filesystem label |
|
||||
| `mount.opts` | string | Mount options (default: `defaults`) |
|
||||
Virtual install example:
|
||||
|
||||
```yaml
|
||||
system:
|
||||
disks:
|
||||
- size: 80 # OS disk
|
||||
- size: 200 # Data disk
|
||||
- size: 80
|
||||
- size: 200
|
||||
mount:
|
||||
path: /data
|
||||
fstype: xfs
|
||||
label: DATA
|
||||
opts: defaults,noatime
|
||||
- size: 300
|
||||
mount:
|
||||
path: /backup
|
||||
fstype: ext4
|
||||
```
|
||||
|
||||
### 4.7 Advanced Partitioning Overrides
|
||||
Physical install example (device paths required):
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------ | ------------ | ---------------------------------------- |
|
||||
| `partitioning_efi_size_mib` | `512` | EFI system partition size in MiB |
|
||||
| `partitioning_boot_size_mib` | `1024` | Separate `/boot` size in MiB |
|
||||
| `partitioning_separate_boot` | auto-derived | Force a separate `/boot` partition |
|
||||
| `partitioning_boot_fs_fstype` | auto-derived | Filesystem for `/boot` |
|
||||
| `partitioning_use_full_disk` | `true` | Use remaining VG space for root LV |
|
||||
```yaml
|
||||
system:
|
||||
type: physical
|
||||
disks:
|
||||
- device: /dev/sda
|
||||
size: 120
|
||||
- device: /dev/sdb
|
||||
size: 500
|
||||
mount:
|
||||
path: /data
|
||||
fstype: ext4
|
||||
```
|
||||
|
||||
**Swap sizing:** RAM >= 16GB gets swap = RAM/2. RAM < 16GB gets swap = max(RAM_GB, 2GB). Further capped to prevent over-allocation on small disks.
|
||||
### 4.6 Advanced Partitioning Overrides
|
||||
|
||||
**LVM layout** (when not using btrfs): root, swap, and when CIS is enabled: `/home` (2-20GB, 10% of disk), `/var` (2GB), `/var/log` (2GB), `/var/log/audit` (1.5GB).
|
||||
Use these only when you need to override the default partition layout logic.
|
||||
|
||||
### 4.8 Cleanup Defaults
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------ | ------------------------------------------------- | ------------ |
|
||||
| `partitioning_efi_size_mib` | EFI system partition size in MiB | `512` |
|
||||
| `partitioning_boot_size_mib` | Separate `/boot` size in MiB (when used) | `1024` |
|
||||
| `partitioning_separate_boot` | Force a separate `/boot` partition | auto-derived |
|
||||
| `partitioning_boot_fs_fstype` | Filesystem for `/boot` when separate | auto-derived |
|
||||
| `partitioning_use_full_disk` | Consume remaining VG space for root LV | `true` |
|
||||
|
||||
Post-install verification and recovery settings.
|
||||
## 5. How to Use the Playbook
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------- | ------- | ----------------------------------------------------- |
|
||||
| `cleanup_verify_boot` | `true` | Check VM accessibility after reboot |
|
||||
| `cleanup_boot_timeout` | `300` | Timeout in seconds for boot verification |
|
||||
| `cleanup_remove_on_failure` | `true` | Auto-remove VMs that fail to boot (created this run only) |
|
||||
### 5.1 Prerequisites
|
||||
|
||||
## 5. Execution Pipeline
|
||||
- Ansible installed on the control machine.
|
||||
- Inventory file with target systems defined and variables configured.
|
||||
- Disposable/non-production targets (the playbook enforces production-safety checks).
|
||||
|
||||
Roles execute in this order:
|
||||
### 5.2 Running the Playbook
|
||||
|
||||
1. **global_defaults** -- normalize inputs, validate, set OS flags
|
||||
2. **system_check** -- detect installer environment, verify live/non-prod target
|
||||
3. **virtualization** -- create VM (if virtual), attach disks, cloud-init
|
||||
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman
|
||||
5. **partitioning** -- create partitions, LVM, LUKS, mount filesystems
|
||||
6. **bootstrap** -- install base system and packages (OS-specific)
|
||||
7. **configuration** -- users, fstab, locales, bootloader, encryption enrollment, networking
|
||||
8. **cis** -- CIS hardening (when `system.features.cis.enabled: true`)
|
||||
9. **cleanup** -- unmount, shutdown installer, remove media, verify boot
|
||||
|
||||
## 6. Usage
|
||||
Execute the playbook using `ansible-playbook`, ensuring that all necessary variables are defined either in the inventory, in a vars file, or passed via `-e`. Credentials (`root_password`, `user_name`, `user_password`, `user_public_key`) are prompted interactively unless supplied through inventory or extra vars.
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.yml main.yml
|
||||
ansible-playbook -i inventory.yml main.yml -e @vars.yml
|
||||
ansible-playbook -i inventory.yml main.yml -e @vars_example.yml
|
||||
```
|
||||
|
||||
All credentials (`system.users`, `system.root.password`) must be defined in inventory or passed via `-e`.
|
||||
### 5.3 Example Usage
|
||||
|
||||
Example inventory files are included:
|
||||
Use the bundled example files as starting points for new inventories:
|
||||
|
||||
- `inventory_example.yml` -- Proxmox virtual setup
|
||||
- `inventory_libvirt_example.yml` -- libvirt virtual setup
|
||||
- `inventory_baremetal_example.yml` -- bare-metal physical setup
|
||||
- `vars_example.yml` -- shared variable overrides
|
||||
- `vars_baremetal_example.yml` -- bare-metal variable overrides
|
||||
|
||||
## 7. Security
|
||||
```bash
|
||||
# Proxmox example
|
||||
ansible-playbook -i inventory_example.yml main.yml
|
||||
|
||||
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, user passwords in `system.users`, `system.root.password`).
|
||||
# libvirt example
|
||||
ansible-playbook -i inventory_libvirt_example.yml main.yml
|
||||
|
||||
# Custom inventory with separate vars file
|
||||
ansible-playbook -i inventory.yml main.yml -e @vars_example.yml
|
||||
```
|
||||
|
||||
## 6. Security
|
||||
|
||||
To protect sensitive information such as passwords, API keys, and other confidential variables (e.g. `hypervisor.password`, `system.luks.passphrase`), **use Ansible Vault** instead of plaintext inventory files.
|
||||
|
||||
## 7. Operational Notes
|
||||
|
||||
- For virtual installs, `system.cpus`, `system.memory`, and `system.disks[0].size` are required and validated.
|
||||
- For physical installs, sizing is derived from the detected install drive; set installer access (`ansible_user`/`ansible_password`) when the installer environment differs from the prompted user credentials.
|
||||
- `system.network.dns.servers` and `system.network.dns.search` must be YAML lists.
|
||||
- `hypervisor.type` selects backend-specific provisioning and cleanup behavior.
|
||||
- Guest tools are selected automatically by hypervisor: `qemu-guest-agent` for `libvirt`/`proxmox`, `open-vm-tools` for `vmware`.
|
||||
- With `system.luks.method: tpm2` on virtual installs, the virtualization role enables a TPM2 device where supported (libvirt/proxmox/vmware).
|
||||
- With LUKS enabled on non-Arch targets, provisioning uses an ESP (512 MiB), a separate `/boot` (1 GiB), and the encrypted root; adjust sizes via `partitioning_efi_size_mib` and `partitioning_boot_size_mib` if needed.
|
||||
- For VMware, `hypervisor.ssh: true` enables SSH on the guest and switches the connection to SSH for the remaining tasks.
|
||||
- Molecule is scaffolded with a delegated driver and a no-op converge for lint-only validation.
|
||||
|
||||
## 8. Safety
|
||||
|
||||
The playbook aborts on non-live/production targets. It refuses to touch pre-existing VMs and only cleans up VMs created in the current run.
|
||||
This playbook intentionally aborts if it detects a non-live/production target. It also refuses to touch pre-existing VMs and only cleans up VMs created in the current run.
|
||||
|
||||
Always run lint after changes:
|
||||
|
||||
```bash
|
||||
ansible-lint
|
||||
```
|
||||
|
||||
@@ -3,6 +3,3 @@ hash_behaviour = merge
|
||||
interpreter_python = auto_silent
|
||||
deprecation_warnings = False
|
||||
host_key_checking = False
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=30 -o ServerAliveCountMax=10
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
---
|
||||
collections:
|
||||
- name: ansible.posix
|
||||
version: "2.1.0"
|
||||
- name: community.general
|
||||
version: "12.3.0"
|
||||
- name: community.libvirt
|
||||
version: "2.0.0"
|
||||
- name: community.crypto
|
||||
version: "3.1.0"
|
||||
- name: community.proxmox
|
||||
version: "1.5.0"
|
||||
- name: community.vmware
|
||||
version: "6.2.0"
|
||||
- name: vmware.vmware
|
||||
version: "2.7.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ all:
|
||||
url: "pve01.example.com"
|
||||
username: "root@pam"
|
||||
password: "CHANGE_ME"
|
||||
node: "pve01"
|
||||
host: "pve01"
|
||||
storage: "local-lvm"
|
||||
boot_iso: "local:iso/archlinux-x86_64.iso"
|
||||
children:
|
||||
|
||||
195
main.yml
195
main.yml
@@ -1,20 +1,96 @@
|
||||
---
|
||||
# Bootstrap pipeline — role execution order:
|
||||
# 1. global_defaults — normalize + validate system/hypervisor/disk input
|
||||
# 2. system_check — pre-flight hardware/environment safety checks
|
||||
# 3. virtualization — create VM on hypervisor (libvirt/proxmox/vmware/xen)
|
||||
# 4. environment — detect live ISO, configure installer network, install tools
|
||||
# 5. partitioning — partition disk, create FS, LUKS, LVM, mount everything
|
||||
# 6. bootstrap — debootstrap/pacstrap/dnf install the target OS into /mnt
|
||||
# 7. configuration — users, network, encryption, fstab, bootloader, services
|
||||
# 8. cis — CIS hardening (optional, per system.features.cis.enabled)
|
||||
# 9. cleanup — unmount, remove cloud-init artifacts, reboot/shutdown
|
||||
- name: Create and configure VMs
|
||||
hosts: "{{ bootstrap_target | default('all') }}"
|
||||
strategy: free # noqa: run-once[play]
|
||||
gather_facts: false
|
||||
become: true
|
||||
vars_prompt:
|
||||
- name: user_name
|
||||
prompt: |
|
||||
What is your username?
|
||||
private: false
|
||||
|
||||
- name: user_public_key
|
||||
prompt: |
|
||||
What is your ssh key?
|
||||
private: false
|
||||
|
||||
- name: user_password
|
||||
prompt: |
|
||||
What is your password?
|
||||
confirm: true
|
||||
|
||||
- name: root_password
|
||||
prompt: |
|
||||
What is your root password?
|
||||
confirm: true
|
||||
pre_tasks:
|
||||
- name: Apply prompted authentication values to system input
|
||||
vars:
|
||||
system_input: "{{ system | default({}) }}"
|
||||
system_users_input: "{{ system_input.users | default([]) }}"
|
||||
system_first_user: >-
|
||||
{{
|
||||
system_users_input[0]
|
||||
if (system_users_input is iterable and system_users_input is not string
|
||||
and system_users_input is not mapping and system_users_input | length > 0)
|
||||
else {}
|
||||
}}
|
||||
system_root_input: "{{ (system_input.root | default({})) if (system_input.root is mapping) else {} }}"
|
||||
prompt_user_name: "{{ user_name | default(system_user_name | default(''), true) | string }}"
|
||||
prompt_user_key: "{{ user_public_key | default(user_key | default(system_user_key | default(''), true), true) | string | trim }}"
|
||||
prompt_user_password: "{{ user_password | default(system_user_password | default(''), true) | string }}"
|
||||
prompt_root_password: "{{ root_password | default(system_root_password | default(''), true) | string }}"
|
||||
resolved_user:
|
||||
name: >-
|
||||
{{
|
||||
system_first_user.name | string
|
||||
if (system_first_user.name | default('') | string | length) > 0
|
||||
else prompt_user_name
|
||||
}}
|
||||
keys: >-
|
||||
{{
|
||||
system_first_user['keys']
|
||||
if (system_first_user['keys'] is defined
|
||||
and system_first_user['keys'] is iterable
|
||||
and system_first_user['keys'] is not string
|
||||
and system_first_user['keys'] | length > 0)
|
||||
else (
|
||||
[prompt_user_key]
|
||||
if (prompt_user_key | length > 0)
|
||||
else []
|
||||
)
|
||||
}}
|
||||
password: >-
|
||||
{{
|
||||
system_first_user.password | string
|
||||
if (system_first_user.password | default('') | string | length) > 0
|
||||
else prompt_user_password
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
system: >-
|
||||
{{
|
||||
system_input
|
||||
| combine(
|
||||
{
|
||||
'users': (
|
||||
[resolved_user]
|
||||
+ (system_users_input[1:]
|
||||
if (system_users_input is sequence
|
||||
and system_users_input is not string
|
||||
and system_users_input | length > 1)
|
||||
else [])
|
||||
),
|
||||
'root': {
|
||||
'password': (
|
||||
(system_root_input.password | default('') | string | length) > 0
|
||||
) | ternary(system_root_input.password | string, prompt_root_password)
|
||||
}
|
||||
},
|
||||
recursive=True
|
||||
)
|
||||
}}
|
||||
|
||||
- name: Load global defaults
|
||||
ansible.builtin.import_role:
|
||||
name: global_defaults
|
||||
@@ -23,79 +99,32 @@
|
||||
ansible.builtin.import_role:
|
||||
name: system_check
|
||||
|
||||
tasks:
|
||||
- name: Bootstrap pipeline
|
||||
block:
|
||||
- name: Record that no pre-existing VM was found
|
||||
ansible.builtin.set_fact:
|
||||
_vm_absent_before_bootstrap: true
|
||||
roles:
|
||||
- role: virtualization
|
||||
when: system_cfg.type == "virtual"
|
||||
become: false
|
||||
vars:
|
||||
ansible_connection: local
|
||||
|
||||
- name: Create virtual machine
|
||||
when: system_cfg.type == "virtual"
|
||||
ansible.builtin.include_role:
|
||||
name: virtualization
|
||||
public: true
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
- role: environment
|
||||
vars:
|
||||
ansible_connection: "{{ 'vmware_tools' if hypervisor_type == 'vmware' else 'ssh' }}"
|
||||
|
||||
- name: Configure environment
|
||||
ansible.builtin.include_role:
|
||||
name: environment
|
||||
public: true
|
||||
- role: partitioning
|
||||
vars:
|
||||
partitioning_boot_partition_suffix: 1
|
||||
partitioning_main_partition_suffix: 2
|
||||
|
||||
- name: Partition disks
|
||||
ansible.builtin.include_role:
|
||||
name: partitioning
|
||||
public: true
|
||||
vars:
|
||||
partitioning_boot_partition_suffix: 1
|
||||
partitioning_main_partition_suffix: 2
|
||||
- role: bootstrap
|
||||
|
||||
- name: Install base system
|
||||
ansible.builtin.include_role:
|
||||
name: bootstrap
|
||||
public: true
|
||||
- role: configuration
|
||||
|
||||
- name: Apply system configuration
|
||||
ansible.builtin.include_role:
|
||||
name: configuration
|
||||
public: true
|
||||
- role: cis
|
||||
when: system_cfg.features.cis.enabled | bool
|
||||
|
||||
- name: Apply CIS hardening
|
||||
when: system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.include_role:
|
||||
name: cis
|
||||
public: true
|
||||
|
||||
- name: Clean up and finalize
|
||||
when: system_cfg.type in ["virtual", "physical"]
|
||||
ansible.builtin.include_role:
|
||||
name: cleanup
|
||||
public: true
|
||||
|
||||
rescue:
|
||||
- name: Delete VM on bootstrap failure
|
||||
when:
|
||||
- _vm_absent_before_bootstrap | default(false) | bool
|
||||
- virtualization_vm_created_in_run | default(false) | bool
|
||||
- system_cfg.type == "virtual"
|
||||
ansible.builtin.include_role:
|
||||
name: virtualization
|
||||
tasks_from: delete
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
tags:
|
||||
- rescue_cleanup
|
||||
|
||||
- name: Fail host after bootstrap rescue
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Bootstrap failed for {{ hostname }}.
|
||||
{{ 'VM was deleted to allow clean retry.'
|
||||
if (virtualization_vm_created_in_run | default(false))
|
||||
else 'VM was not created in this run (kept).' }}
|
||||
- role: cleanup
|
||||
when: system_cfg.type in ["virtual", "physical"]
|
||||
become: false
|
||||
|
||||
post_tasks:
|
||||
- name: Set post-reboot connection flags
|
||||
@@ -118,16 +147,10 @@
|
||||
- name: Set final SSH credentials for post-reboot tasks
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
no_log: true
|
||||
vars:
|
||||
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: ssh
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: "{{ _primary.key }}"
|
||||
ansible_password: "{{ _primary.value.password }}"
|
||||
ansible_become_password: "{{ _primary.value.password }}"
|
||||
ansible_user: "{{ system_cfg.users[0].name }}"
|
||||
ansible_password: "{{ system_cfg.users[0].password }}"
|
||||
ansible_become_password: "{{ system_cfg.users[0].password }}"
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
# OS → task file mapping for bootstrap dispatch.
|
||||
# Each key matches a supported `os` value; value is the task file to include.
|
||||
bootstrap_os_task_map:
|
||||
almalinux: _dnf_family.yml
|
||||
alpine: alpine.yml
|
||||
archlinux: archlinux.yml
|
||||
debian: debian.yml
|
||||
fedora: _dnf_family.yml
|
||||
opensuse: opensuse.yml
|
||||
rocky: _dnf_family.yml
|
||||
rhel: rhel.yml
|
||||
ubuntu: ubuntu.yml
|
||||
ubuntu-lts: ubuntu.yml
|
||||
void: void.yml
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
- name: Load desktop package definitions
|
||||
ansible.builtin.include_vars:
|
||||
file: desktop.yml
|
||||
|
||||
- name: Resolve desktop packages
|
||||
vars:
|
||||
_de: "{{ system_cfg.features.desktop.environment }}"
|
||||
_family_pkgs: "{{ bootstrap_desktop_packages[os_family] | default({}) }}"
|
||||
_de_config: "{{ _family_pkgs[_de] | default({}) }}"
|
||||
ansible.builtin.set_fact:
|
||||
_desktop_groups: "{{ _de_config.groups | default([]) }}"
|
||||
_desktop_packages: "{{ _de_config.packages | default([]) }}"
|
||||
|
||||
- name: Validate desktop environment is supported
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (_desktop_groups | length > 0) or (_desktop_packages | length > 0)
|
||||
fail_msg: >-
|
||||
Desktop environment '{{ system_cfg.features.desktop.environment }}'
|
||||
is not defined for os_family '{{ os_family }}'.
|
||||
Supported: {{ (bootstrap_desktop_packages[os_family] | default({})).keys() | join(', ') }}
|
||||
quiet: true
|
||||
|
||||
- name: Install desktop package groups
|
||||
when: _desktop_groups | length > 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }}
|
||||
--setopt=install_weak_deps=False group install -y {{ _desktop_groups | join(' ') }}
|
||||
register: _desktop_group_result
|
||||
changed_when: _desktop_group_result.rc == 0
|
||||
|
||||
- name: Install desktop packages
|
||||
when: _desktop_packages | length > 0
|
||||
vars:
|
||||
_install_commands:
|
||||
RedHat: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }}
|
||||
--setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }}
|
||||
Debian: >-
|
||||
{{ chroot_command }} apt install -y {{ _desktop_packages | join(' ') }}
|
||||
Archlinux: >-
|
||||
pacstrap /mnt {{ _desktop_packages | join(' ') }}
|
||||
Suse: >-
|
||||
{{ chroot_command }} zypper install -y {{ _desktop_packages | join(' ') }}
|
||||
ansible.builtin.command: "{{ _install_commands[os_family] }}"
|
||||
register: _desktop_pkg_result
|
||||
changed_when: _desktop_pkg_result.rc == 0
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
- name: "Bootstrap {{ os | capitalize }}"
|
||||
vars:
|
||||
_dnf_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_dnf_repos: "{{ _dnf_config.repos | map('regex_replace', '^', '--repo=') | join(' ') }}"
|
||||
_dnf_groups: "{{ _dnf_config.base | join(' ') }}"
|
||||
_dnf_extra: >-
|
||||
{{
|
||||
((_dnf_config.extra | default([])) + (_dnf_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: "Install base system for {{ os | capitalize }}"
|
||||
ansible.builtin.command: >-
|
||||
dnf --releasever={{ os_version }} --best {{ _dnf_repos }}
|
||||
--installroot=/mnt --setopt=install_weak_deps=False
|
||||
groupinstall -y {{ _dnf_groups }}
|
||||
register: bootstrap_dnf_base_result
|
||||
changed_when: bootstrap_dnf_base_result.rc == 0
|
||||
failed_when:
|
||||
- bootstrap_dnf_base_result.rc != 0
|
||||
- "'scriptlet' not in bootstrap_dnf_base_result.stderr"
|
||||
|
||||
- name: Ensure chroot has DNS resolution
|
||||
ansible.builtin.file:
|
||||
src: /run/NetworkManager/resolv.conf
|
||||
dest: /mnt/etc/resolv.conf
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Install extra packages
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
|
||||
install -y {{ _dnf_extra }}
|
||||
register: bootstrap_dnf_extra_result
|
||||
changed_when: bootstrap_dnf_extra_result.rc == 0
|
||||
|
||||
- name: Detect installed kernel package name
|
||||
ansible.builtin.command: "{{ chroot_command }} rpm -q kernel-core"
|
||||
register: bootstrap_dnf_kernel_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Reinstall kernel package
|
||||
vars:
|
||||
_kernel_pkg: "{{ 'kernel-core' if bootstrap_dnf_kernel_check.rc == 0 else 'kernel' }}"
|
||||
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y {{ _kernel_pkg }}"
|
||||
register: bootstrap_dnf_kernel_result
|
||||
changed_when: bootstrap_dnf_kernel_result.rc == 0
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
# Resolve the OS-specific variable namespace and task file for the bootstrap role.
|
||||
- name: Validate OS is supported for bootstrap
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- os is defined
|
||||
- os in bootstrap_os_task_map
|
||||
fail_msg: >-
|
||||
Unsupported OS '{{ os | default("undefined") }}' for bootstrap.
|
||||
Supported: {{ bootstrap_os_task_map | dict2items | map(attribute='key') | join(', ') }}
|
||||
quiet: true
|
||||
29
roles/bootstrap/tasks/almalinux.yml
Normal file
29
roles/bootstrap/tasks/almalinux.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Bootstrap AlmaLinux
|
||||
vars:
|
||||
bootstrap_almalinux_extra: >-
|
||||
{{
|
||||
lookup('vars', bootstrap_var_key)
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install AlmaLinux base system
|
||||
ansible.builtin.command: >-
|
||||
dnf --releasever={{ os_version }} --best --repo=baseos --repo=appstream
|
||||
--installroot=/mnt --setopt=install_weak_deps=False
|
||||
groupinstall -y core
|
||||
register: bootstrap_almalinux_base_result
|
||||
changed_when: bootstrap_almalinux_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
|
||||
install -y {{ bootstrap_almalinux_extra }}
|
||||
register: bootstrap_almalinux_extra_result
|
||||
changed_when: bootstrap_almalinux_extra_result.rc == 0
|
||||
|
||||
- name: Reinstall kernel core
|
||||
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core"
|
||||
register: bootstrap_almalinux_kernel_result
|
||||
changed_when: bootstrap_almalinux_kernel_result.rc == 0
|
||||
@@ -1,25 +1,21 @@
|
||||
---
|
||||
- name: Bootstrap Alpine Linux
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_base_packages: "{{ _config.base | join(' ') }}"
|
||||
_extra_packages: >-
|
||||
bootstrap_alpine_packages: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install Alpine Linux base
|
||||
- name: Install Alpine Linux packages
|
||||
ansible.builtin.command: >
|
||||
apk --root /mnt --no-cache add {{ _base_packages }}
|
||||
apk --root /mnt --no-cache add alpine-base
|
||||
register: bootstrap_alpine_bootstrap_result
|
||||
changed_when: bootstrap_alpine_bootstrap_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
when: _extra_packages | trim | length > 0
|
||||
when: bootstrap_alpine_packages | length > 0
|
||||
ansible.builtin.command: >
|
||||
apk --root /mnt add {{ _extra_packages }}
|
||||
apk --root /mnt add {{ bootstrap_alpine_packages }}
|
||||
register: bootstrap_alpine_extra_result
|
||||
changed_when: bootstrap_alpine_extra_result.rc == 0
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
---
|
||||
- name: Bootstrap ArchLinux
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
bootstrap_archlinux_packages: >-
|
||||
{{
|
||||
((_config.base | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
lookup('vars', bootstrap_var_key)
|
||||
}}
|
||||
ansible.builtin.command: >-
|
||||
pacstrap /mnt {{ bootstrap_archlinux_packages | join(' ') }}
|
||||
pacstrap /mnt {{ bootstrap_archlinux_packages | reject('equalto', '') | join(' ') }} --asexplicit
|
||||
register: bootstrap_result
|
||||
changed_when: bootstrap_result.rc == 0
|
||||
|
||||
@@ -10,58 +10,53 @@
|
||||
else 'sid' if (os_version | string) == 'unstable'
|
||||
else 'trixie'
|
||||
}}
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
bootstrap_debian_base_csv: "{{ (['ca-certificates'] + _config.base) | unique | join(',') }}"
|
||||
bootstrap_debian_package_config: >-
|
||||
{{
|
||||
lookup('vars', bootstrap_var_key)
|
||||
}}
|
||||
bootstrap_debian_base_packages: >-
|
||||
{{
|
||||
bootstrap_debian_package_config.base
|
||||
| default([])
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
bootstrap_debian_extra_packages: >-
|
||||
{{
|
||||
bootstrap_debian_package_config.extra
|
||||
| default([])
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
bootstrap_debian_base_csv: "{{ bootstrap_debian_base_packages | join(',') }}"
|
||||
bootstrap_debian_extra_args: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
bootstrap_debian_extra_packages
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Validate Debian package configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _config is mapping
|
||||
- _config.base is sequence
|
||||
- _config.extra is sequence
|
||||
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
|
||||
- bootstrap_debian_package_config is mapping
|
||||
- bootstrap_debian_package_config.base is defined
|
||||
- bootstrap_debian_package_config.base is sequence
|
||||
- bootstrap_debian_package_config.base is not string
|
||||
- bootstrap_debian_package_config.extra is defined
|
||||
- bootstrap_debian_package_config.extra is sequence
|
||||
- bootstrap_debian_package_config.extra is not string
|
||||
fail_msg: "bootstrap package definition for {{ bootstrap_var_key }} must be a mapping with base/extra lists."
|
||||
quiet: true
|
||||
|
||||
- name: Install Debian base system
|
||||
ansible.builtin.command: >-
|
||||
debootstrap --include={{ bootstrap_debian_base_csv }}
|
||||
{{ bootstrap_debian_release }} /mnt {{ system_cfg.mirror }}
|
||||
{{ bootstrap_debian_release }} /mnt http://deb.debian.org/debian/
|
||||
register: bootstrap_debian_base_result
|
||||
changed_when: bootstrap_debian_base_result.rc == 0
|
||||
|
||||
- name: Write bootstrap sources.list
|
||||
ansible.builtin.template:
|
||||
src: debian.sources.list.j2
|
||||
dest: /mnt/etc/apt/sources.list
|
||||
mode: "0644"
|
||||
|
||||
- name: Configure apt performance tuning
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/apt/apt.conf.d/99performance
|
||||
content: |
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
mode: "0644"
|
||||
|
||||
- name: Update package lists
|
||||
ansible.builtin.command: "{{ chroot_command }} apt update"
|
||||
register: bootstrap_debian_update_result
|
||||
changed_when: bootstrap_debian_update_result.rc == 0
|
||||
|
||||
- name: Upgrade all packages to latest versions
|
||||
ansible.builtin.command: "{{ chroot_command }} apt full-upgrade -y"
|
||||
register: bootstrap_debian_upgrade_result
|
||||
changed_when: "'0 upgraded' not in bootstrap_debian_upgrade_result.stdout"
|
||||
|
||||
- name: Install extra packages
|
||||
when: bootstrap_debian_extra_args | trim | length > 0
|
||||
when: bootstrap_debian_extra_packages | length > 0
|
||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"
|
||||
register: bootstrap_debian_extra_result
|
||||
changed_when: bootstrap_debian_extra_result.rc == 0
|
||||
|
||||
29
roles/bootstrap/tasks/fedora.yml
Normal file
29
roles/bootstrap/tasks/fedora.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Bootstrap Fedora
|
||||
vars:
|
||||
bootstrap_fedora_extra: >-
|
||||
{{
|
||||
lookup('vars', bootstrap_var_key)
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install Fedora base system
|
||||
ansible.builtin.command: >-
|
||||
dnf --releasever={{ os_version }} --best --repo=fedora --repo=fedora-updates
|
||||
--installroot=/mnt --setopt=install_weak_deps=False
|
||||
groupinstall -y critical-path-base core
|
||||
register: bootstrap_fedora_base_result
|
||||
changed_when: bootstrap_fedora_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
|
||||
install -y {{ bootstrap_fedora_extra }}
|
||||
register: bootstrap_fedora_extra_result
|
||||
changed_when: bootstrap_fedora_extra_result.rc == 0
|
||||
|
||||
- name: Reinstall kernel core
|
||||
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core"
|
||||
register: bootstrap_fedora_kernel_result
|
||||
changed_when: bootstrap_fedora_kernel_result.rc == 0
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
- name: Validate bootstrap input
|
||||
ansible.builtin.import_tasks: _validate.yml
|
||||
|
||||
- name: Create API filesystem mountpoints in installroot
|
||||
when: os_family == 'RedHat'
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.file:
|
||||
path: "/mnt/{{ item }}"
|
||||
state: directory
|
||||
@@ -14,7 +11,7 @@
|
||||
- sys
|
||||
|
||||
- name: Mount API filesystems into installroot
|
||||
when: os_family == 'RedHat'
|
||||
when: is_rhel | bool
|
||||
ansible.posix.mount:
|
||||
src: "{{ item.src }}"
|
||||
path: "/mnt/{{ item.path }}"
|
||||
@@ -31,13 +28,21 @@
|
||||
|
||||
- name: Run OS-specific bootstrap process
|
||||
vars:
|
||||
bootstrap_os_task_map:
|
||||
almalinux: almalinux.yml
|
||||
alpine: alpine.yml
|
||||
archlinux: archlinux.yml
|
||||
debian: debian.yml
|
||||
fedora: fedora.yml
|
||||
opensuse: opensuse.yml
|
||||
rocky: rocky.yml
|
||||
rhel: rhel.yml
|
||||
ubuntu: ubuntu.yml
|
||||
ubuntu-lts: ubuntu.yml
|
||||
void: void.yml
|
||||
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
|
||||
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
|
||||
|
||||
- name: Install desktop environment packages
|
||||
when: system_cfg.features.desktop.enabled | bool
|
||||
ansible.builtin.include_tasks: _desktop.yml
|
||||
|
||||
- name: Ensure chroot uses live environment DNS
|
||||
ansible.builtin.file:
|
||||
src: /run/NetworkManager/resolv.conf
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
---
|
||||
- name: Bootstrap openSUSE
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_base_patterns: "{{ _config.base | join(' ') }}"
|
||||
_extra_packages: >-
|
||||
bootstrap_opensuse_packages: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install openSUSE base patterns
|
||||
- name: Install openSUSE base packages
|
||||
ansible.builtin.command: >
|
||||
zypper --root /mnt --non-interactive install -t pattern {{ _base_patterns }}
|
||||
zypper --root /mnt --non-interactive install -t pattern patterns-base-base
|
||||
register: bootstrap_opensuse_base_result
|
||||
changed_when: bootstrap_opensuse_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
when: _extra_packages | trim | length > 0
|
||||
- name: Install openSUSE extra packages
|
||||
when: bootstrap_opensuse_packages | length > 0
|
||||
ansible.builtin.command: >
|
||||
zypper --root /mnt --non-interactive install {{ _extra_packages }}
|
||||
zypper --root /mnt --non-interactive install {{ bootstrap_opensuse_packages }}
|
||||
register: bootstrap_opensuse_extra_result
|
||||
changed_when: bootstrap_opensuse_extra_result.rc == 0
|
||||
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
---
|
||||
- name: Bootstrap RHEL System
|
||||
vars:
|
||||
_rhel_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_rhel_repos: "{{ _rhel_config.repos | map('regex_replace', '^', '--repo=') | join(' ') }}"
|
||||
_rhel_groups: "{{ _rhel_config.base | join(' ') }}"
|
||||
_rhel_extra: >-
|
||||
{{
|
||||
((_rhel_config.extra | default([])) + (_rhel_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install base packages in chroot environment
|
||||
ansible.builtin.command: >-
|
||||
dnf --releasever={{ os_version_major }} --best {{ _rhel_repos }}
|
||||
dnf --releasever={{ os_version_major }} --repo=rhel{{ os_version_major }}-baseos
|
||||
--installroot=/mnt
|
||||
--setopt=install_weak_deps=False --setopt=optional_metadata_types=filelists
|
||||
groupinstall -y {{ _rhel_groups }}
|
||||
groupinstall -y core base standard
|
||||
register: bootstrap_result
|
||||
changed_when: bootstrap_result.rc == 0
|
||||
failed_when:
|
||||
@@ -50,8 +40,15 @@
|
||||
remote_src: true
|
||||
|
||||
- name: Install additional packages in chroot
|
||||
vars:
|
||||
bootstrap_rhel_extra: >-
|
||||
{{
|
||||
lookup('vars', bootstrap_var_key)
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }} --best
|
||||
--setopt=install_weak_deps=False install -y {{ _rhel_extra }}
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }}
|
||||
--setopt=install_weak_deps=False install -y {{ bootstrap_rhel_extra }}
|
||||
register: bootstrap_result
|
||||
changed_when: bootstrap_result.rc == 0
|
||||
|
||||
29
roles/bootstrap/tasks/rocky.yml
Normal file
29
roles/bootstrap/tasks/rocky.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Bootstrap Rocky Linux
|
||||
vars:
|
||||
bootstrap_rocky_extra: >-
|
||||
{{
|
||||
lookup('vars', bootstrap_var_key)
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install Rocky Linux base system
|
||||
ansible.builtin.command: >-
|
||||
dnf --releasever={{ os_version }} --best --repo=baseos --repo=appstream
|
||||
--installroot=/mnt --setopt=install_weak_deps=False
|
||||
groupinstall -y core
|
||||
register: bootstrap_rocky_base_result
|
||||
changed_when: bootstrap_rocky_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
|
||||
install -y {{ bootstrap_rocky_extra }}
|
||||
register: bootstrap_rocky_extra_result
|
||||
changed_when: bootstrap_rocky_extra_result.rc == 0
|
||||
|
||||
- name: Reinstall kernel core
|
||||
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core"
|
||||
register: bootstrap_rocky_kernel_result
|
||||
changed_when: bootstrap_rocky_kernel_result.rc == 0
|
||||
@@ -3,25 +3,41 @@
|
||||
vars:
|
||||
# ubuntu = latest non-LTS, ubuntu-lts = latest LTS
|
||||
bootstrap_ubuntu_release_map:
|
||||
ubuntu: questing
|
||||
ubuntu: plucky
|
||||
ubuntu-lts: noble
|
||||
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('noble') }}"
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
bootstrap_ubuntu_base_csv: "{{ (['ca-certificates'] + _config.base) | unique | join(',') }}"
|
||||
bootstrap_ubuntu_extra_args: >-
|
||||
bootstrap_ubuntu_package_config: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
lookup('vars', bootstrap_var_key)
|
||||
}}
|
||||
bootstrap_ubuntu_base_packages: >-
|
||||
{{
|
||||
bootstrap_ubuntu_package_config.base
|
||||
| default([])
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
bootstrap_ubuntu_extra_packages: >-
|
||||
{{
|
||||
bootstrap_ubuntu_package_config.extra
|
||||
| default([])
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
bootstrap_ubuntu_base_csv: "{{ bootstrap_ubuntu_base_packages | join(',') }}"
|
||||
bootstrap_ubuntu_extra: "{{ bootstrap_ubuntu_extra_packages | join(' ') }}"
|
||||
block:
|
||||
- name: Validate Ubuntu package configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _config is mapping
|
||||
- _config.base is sequence
|
||||
- _config.extra is sequence
|
||||
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
|
||||
- bootstrap_ubuntu_package_config is mapping
|
||||
- bootstrap_ubuntu_package_config.base is defined
|
||||
- bootstrap_ubuntu_package_config.base is sequence
|
||||
- bootstrap_ubuntu_package_config.base is not string
|
||||
- bootstrap_ubuntu_package_config.extra is defined
|
||||
- bootstrap_ubuntu_package_config.extra is sequence
|
||||
- bootstrap_ubuntu_package_config.extra is not string
|
||||
fail_msg: "bootstrap package definition for {{ bootstrap_var_key }} must be a mapping with base/extra lists."
|
||||
quiet: true
|
||||
|
||||
- name: Install Ubuntu base system
|
||||
@@ -30,37 +46,22 @@
|
||||
--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
--include={{ bootstrap_ubuntu_base_csv }}
|
||||
{{ bootstrap_ubuntu_release }} /mnt
|
||||
{{ system_cfg.mirror }}
|
||||
http://archive.ubuntu.com/ubuntu/
|
||||
register: bootstrap_ubuntu_base_result
|
||||
changed_when: bootstrap_ubuntu_base_result.rc == 0
|
||||
|
||||
- name: Write bootstrap sources.list
|
||||
ansible.builtin.template:
|
||||
src: ubuntu.sources.list.j2
|
||||
dest: /mnt/etc/apt/sources.list
|
||||
mode: "0644"
|
||||
|
||||
- name: Configure apt performance tuning
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/apt/apt.conf.d/99performance
|
||||
content: |
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
mode: "0644"
|
||||
- name: Enable universe repository
|
||||
ansible.builtin.command: "{{ chroot_command }} sed -i '1s|$| universe|' /etc/apt/sources.list"
|
||||
register: bootstrap_ubuntu_repo_result
|
||||
changed_when: bootstrap_ubuntu_repo_result.rc == 0
|
||||
|
||||
- name: Update package lists
|
||||
ansible.builtin.command: "{{ chroot_command }} apt update"
|
||||
register: bootstrap_ubuntu_update_result
|
||||
changed_when: bootstrap_ubuntu_update_result.rc == 0
|
||||
|
||||
- name: Upgrade all packages to latest versions
|
||||
ansible.builtin.command: "{{ chroot_command }} apt full-upgrade -y"
|
||||
register: bootstrap_ubuntu_upgrade_result
|
||||
changed_when: "'0 upgraded' not in bootstrap_ubuntu_upgrade_result.stdout"
|
||||
|
||||
- name: Install extra packages
|
||||
when: bootstrap_ubuntu_extra_args | trim | length > 0
|
||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"
|
||||
when: bootstrap_ubuntu_extra_packages | length > 0
|
||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra }}"
|
||||
register: bootstrap_ubuntu_extra_result
|
||||
changed_when: bootstrap_ubuntu_extra_result.rc == 0
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
---
|
||||
- name: Bootstrap Void Linux
|
||||
vars:
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
_base_packages: "{{ _config.base | join(' ') }}"
|
||||
_extra_packages: >-
|
||||
bootstrap_void_packages: >-
|
||||
{{
|
||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
||||
| reject('equalto', '')
|
||||
| join(' ')
|
||||
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
|
||||
}}
|
||||
block:
|
||||
- name: Install Void Linux base
|
||||
- name: Install Void Linux base packages
|
||||
ansible.builtin.command: >
|
||||
xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current {{ _base_packages }}
|
||||
xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current void-repo-nonfree base-system
|
||||
register: bootstrap_void_base_result
|
||||
changed_when: bootstrap_void_base_result.rc == 0
|
||||
|
||||
- name: Install extra packages
|
||||
when: _extra_packages | trim | length > 0
|
||||
when: bootstrap_void_packages | length > 0
|
||||
ansible.builtin.command: >
|
||||
xbps-install -Su -r /mnt {{ _extra_packages }}
|
||||
xbps-install -Su -r /mnt {{ bootstrap_void_packages }}
|
||||
register: bootstrap_void_extra_result
|
||||
changed_when: bootstrap_void_extra_result.rc == 0
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = bootstrap_debian_release %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
{% if release != 'sid' %}
|
||||
|
||||
deb https://security.debian.org/debian-security {{ release }}-security {{ components }}
|
||||
deb-src https://security.debian.org/debian-security {{ release }}-security {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-updates {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-updates {{ components }}
|
||||
{% endif %}
|
||||
@@ -1,16 +0,0 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = bootstrap_ubuntu_release %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main restricted universe multiverse' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-updates {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-updates {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-security {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-security {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-backports {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-backports {{ components }}
|
||||
@@ -1,149 +0,0 @@
|
||||
---
|
||||
# Per-family desktop environment package definitions.
|
||||
# Keyed by os_family -> environment -> groups (dnf groupinstall) / packages.
|
||||
# Kept intentionally minimal: base DE + essential tools, no full suites.
|
||||
bootstrap_desktop_packages:
|
||||
RedHat:
|
||||
gnome:
|
||||
groups:
|
||||
- workstation-product-environment
|
||||
packages: []
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- plasma-systemmonitor
|
||||
- sddm
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
- kscreen
|
||||
- kde-gtk-config
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
xfce:
|
||||
groups:
|
||||
- xfce-desktop-environment
|
||||
packages:
|
||||
- lightdm
|
||||
Debian:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- gnome-core
|
||||
- gdm3
|
||||
- gnome-tweaks
|
||||
- xdg-user-dirs
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- sddm
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
- kscreen
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
xfce:
|
||||
groups: []
|
||||
packages:
|
||||
- xfce4
|
||||
- xfce4-goodies
|
||||
- lightdm
|
||||
- xdg-user-dirs
|
||||
Archlinux:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- gnome
|
||||
- gdm
|
||||
- xdg-user-dirs
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- sddm
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
- kscreen
|
||||
- kde-gtk-config
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
xfce:
|
||||
groups: []
|
||||
packages:
|
||||
- xfce4
|
||||
- xfce4-goodies
|
||||
- lightdm
|
||||
- xdg-user-dirs
|
||||
sway:
|
||||
groups: []
|
||||
packages:
|
||||
- sway
|
||||
- waybar
|
||||
- foot
|
||||
- wofi
|
||||
- greetd
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-wlr
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
hyprland:
|
||||
groups: []
|
||||
packages:
|
||||
- hyprland
|
||||
- kitty
|
||||
- wofi
|
||||
- waybar
|
||||
- ly
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-hyprland
|
||||
- polkit-kde-agent
|
||||
- qt5-wayland
|
||||
- qt6-wayland
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
Suse:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- patterns-gnome-gnome_basic
|
||||
- gdm
|
||||
- xdg-user-dirs
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- patterns-kde-kde_plasma
|
||||
- sddm
|
||||
- xdg-user-dirs
|
||||
|
||||
# Display manager auto-detection from desktop environment.
|
||||
bootstrap_desktop_dm_map:
|
||||
gnome: gdm
|
||||
kde: sddm
|
||||
xfce: lightdm
|
||||
sway: greetd
|
||||
hyprland: ly@tty2
|
||||
cinnamon: lightdm
|
||||
mate: lightdm
|
||||
lxqt: sddm
|
||||
budgie: gdm
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# Feature-gated packages shared across all distros.
|
||||
# Arch has special nftables handling and composes this differently.
|
||||
# Common feature-gated packages. Built as a clean list (no empty strings).
|
||||
# Arch overrides nftables → iptables-nft; SSH package names vary per distro.
|
||||
bootstrap_common_conditional: >-
|
||||
{{
|
||||
(
|
||||
@@ -14,387 +14,157 @@ bootstrap_common_conditional: >-
|
||||
)
|
||||
}}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-OS package definitions: base (rootfs/group install), extra (post-base),
|
||||
# conditional (feature/version-gated, appended by task files).
|
||||
# DNF-based distros also carry repos (dnf --repo) and use base as group names.
|
||||
# ---------------------------------------------------------------------------
|
||||
bootstrap_rhel_base: >-
|
||||
{{
|
||||
['bind-utils', 'dhcp-client', 'efibootmgr',
|
||||
'glibc-langpack-de', 'glibc-langpack-en', 'lrzsz',
|
||||
'lvm2', 'mtr', 'ncurses-term', 'nfs-utils',
|
||||
'policycoreutils-python-utils', 'shim', 'tmux', 'vim', 'zstd']
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_rhel:
|
||||
repos:
|
||||
- "rhel{{ os_version_major }}-baseos"
|
||||
base:
|
||||
- core
|
||||
- base
|
||||
- standard
|
||||
extra:
|
||||
- bind-utils
|
||||
- efibootmgr
|
||||
- glibc-langpack-de
|
||||
- glibc-langpack-en
|
||||
- grub2
|
||||
- lrzsz
|
||||
- lvm2
|
||||
- mtr
|
||||
- ncurses-term
|
||||
- nfs-utils
|
||||
- policycoreutils-python-utils
|
||||
- shim
|
||||
- tmux
|
||||
- vim
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['grub2-efi-x64'] if os_version_major | default('') == '8' else ['grub2-efi'])
|
||||
+ (['grub2-tools-extra'] if os_version_major | default('') in ['8', '9'] else [])
|
||||
+ (['dhcp-client'] if (os_version_major | default('9') | int) < 10 else [])
|
||||
+ (['python39'] if os_version_major | default('') == '8' else ['python'])
|
||||
+ (['kernel'] if os_version_major | default('') == '10' else [])
|
||||
+ (['zram-generator'] if os_version_major | default('') in ['9', '10'] else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
bootstrap_rhel_versioned: >-
|
||||
{{
|
||||
['grub2']
|
||||
+ (['grub2-efi-x64'] if os_version_major | default('') == '8' else ['grub2-efi'])
|
||||
+ (['grub2-tools-extra'] if os_version_major | default('') in ['8', '9'] else [])
|
||||
+ (['python39'] if os_version_major | default('') == '8' else ['python'])
|
||||
+ (['kernel'] if os_version_major | default('') == '10' else [])
|
||||
+ (['zram-generator'] if os_version_major | default('') in ['9', '10'] else [])
|
||||
}}
|
||||
|
||||
bootstrap_almalinux:
|
||||
repos:
|
||||
- baseos
|
||||
- appstream
|
||||
base:
|
||||
- core
|
||||
extra:
|
||||
- bind-utils
|
||||
- efibootmgr
|
||||
- glibc-langpack-de
|
||||
- glibc-langpack-en
|
||||
- grub2
|
||||
- grub2-efi
|
||||
- kernel
|
||||
- lrzsz
|
||||
- lvm2
|
||||
- mtr
|
||||
- nc
|
||||
- ncurses-term
|
||||
- nfs-utils
|
||||
- nfsv4-client-utils
|
||||
- policycoreutils-python-utils
|
||||
- ppp
|
||||
- python3
|
||||
- shim
|
||||
- tmux
|
||||
- vim
|
||||
- zram-generator
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['dbus-daemon'] if (os_version_major | default('10') | int) >= 9 else [])
|
||||
+ (['dhcp-client'] if (os_version_major | default('10') | int) < 10 else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
bootstrap_rhel: "{{ bootstrap_rhel_base + bootstrap_rhel_versioned }}"
|
||||
|
||||
bootstrap_rocky:
|
||||
repos:
|
||||
- baseos
|
||||
- appstream
|
||||
base:
|
||||
- core
|
||||
extra:
|
||||
- bind-utils
|
||||
- efibootmgr
|
||||
- glibc-langpack-de
|
||||
- glibc-langpack-en
|
||||
- grub2
|
||||
- grub2-efi
|
||||
- kernel
|
||||
- lrzsz
|
||||
- lvm2
|
||||
- mtr
|
||||
- nc
|
||||
- ncurses-term
|
||||
- nfs-utils
|
||||
- nfsv4-client-utils
|
||||
- policycoreutils-python-utils
|
||||
- ppp
|
||||
- python3
|
||||
- shim
|
||||
- telnet
|
||||
- tmux
|
||||
- util-linux-core
|
||||
- vim
|
||||
- wget
|
||||
- zram-generator
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['dhcp-client'] if (os_version_major | default('9') | int) < 10 else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
bootstrap_almalinux: >-
|
||||
{{
|
||||
bootstrap_rhel_base
|
||||
+ ['grub2', 'grub2-efi', 'dbus-daemon', 'lrzsz',
|
||||
'nfsv4-client-utils', 'nc', 'ppp', 'python3', 'zram-generator']
|
||||
}}
|
||||
|
||||
bootstrap_fedora:
|
||||
repos:
|
||||
- fedora
|
||||
- fedora-updates
|
||||
base:
|
||||
- critical-path-base
|
||||
- core
|
||||
extra:
|
||||
- bat
|
||||
- bind-utils
|
||||
- btrfs-progs
|
||||
- cronie
|
||||
- dhcp-client
|
||||
- duf
|
||||
- efibootmgr
|
||||
- entr
|
||||
- fish
|
||||
- fzf
|
||||
- glibc-langpack-de
|
||||
- glibc-langpack-en
|
||||
- grub2
|
||||
- grub2-efi
|
||||
- htop
|
||||
- iperf3
|
||||
- logrotate
|
||||
- lrzsz
|
||||
- lvm2
|
||||
- nc
|
||||
- nfs-utils
|
||||
- nfsv4-client-utils
|
||||
- polkit
|
||||
- ppp
|
||||
- python3
|
||||
- ripgrep
|
||||
- shim
|
||||
- tmux
|
||||
- vim-default-editor
|
||||
- wget
|
||||
- zoxide
|
||||
- zram-generator
|
||||
- zstd
|
||||
conditional: "{{ bootstrap_common_conditional }}"
|
||||
bootstrap_rocky: >-
|
||||
{{
|
||||
bootstrap_rhel_base
|
||||
+ ['grub2', 'grub2-efi', 'nfsv4-client-utils', 'nc', 'ppp',
|
||||
'python3', 'telnet', 'util-linux-core', 'wget', 'zram-generator']
|
||||
}}
|
||||
|
||||
bootstrap_fedora: >-
|
||||
{{
|
||||
['bat', 'bind-utils', 'btrfs-progs', 'cronie', 'dhcp-client',
|
||||
'duf', 'efibootmgr', 'entr', 'fish', 'fzf',
|
||||
'glibc-langpack-de', 'glibc-langpack-en', 'grub2', 'grub2-efi',
|
||||
'htop', 'iperf3', 'logrotate', 'lrzsz', 'lvm2',
|
||||
'nc', 'nfs-utils', 'nfsv4-client-utils', 'polkit', 'ppp',
|
||||
'python3', 'ripgrep', 'shim', 'tmux', 'vim-default-editor',
|
||||
'wget', 'zoxide', 'zram-generator', 'zstd']
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_debian_base_common: >-
|
||||
{{
|
||||
['btrfs-progs', 'cron', 'gnupg', 'grub-efi', 'grub-efi-amd64-signed',
|
||||
'grub2-common', 'locales', 'logrotate', 'lvm2', 'python3', 'xfsprogs']
|
||||
+ (['cryptsetup-initramfs'] if system_cfg.luks.enabled | bool else [])
|
||||
+ (['openssh-server'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
}}
|
||||
|
||||
bootstrap_debian_extra_common:
|
||||
- apparmor-utils
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- entr
|
||||
- fish
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- tcpd
|
||||
- vim
|
||||
- wget
|
||||
- zstd
|
||||
|
||||
bootstrap_debian_extra_versioned: >-
|
||||
{{
|
||||
['linux-image-amd64']
|
||||
+ (['duf'] if (os_version | string) not in ['10', '11'] else [])
|
||||
+ (['fastfetch'] if (os_version | string) in ['13', 'unstable'] else [])
|
||||
+ (['neofetch'] if (os_version | string) == '12' else [])
|
||||
+ (['software-properties-common'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
+ (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else [])
|
||||
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
}}
|
||||
|
||||
bootstrap_debian:
|
||||
base:
|
||||
- btrfs-progs
|
||||
- cron
|
||||
- cryptsetup-initramfs
|
||||
- gnupg
|
||||
- grub-efi
|
||||
- grub-efi-amd64-signed
|
||||
- grub2-common
|
||||
- locales
|
||||
- logrotate
|
||||
- lvm2
|
||||
- openssh-server
|
||||
- python3
|
||||
- xfsprogs
|
||||
extra:
|
||||
- apparmor-utils
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- entr
|
||||
- fish
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- linux-image-amd64
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- needrestart
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- tcpd
|
||||
- vim
|
||||
- wget
|
||||
- zstd
|
||||
conditional: >-
|
||||
base: "{{ bootstrap_debian_base_common }}"
|
||||
extra: >-
|
||||
{{
|
||||
(['duf'] if (os_version | string) not in ['10', '11'] else [])
|
||||
+ (['fastfetch'] if (os_version | string) in ['13', 'unstable'] else [])
|
||||
+ (['neofetch'] if (os_version | string) == '12' else [])
|
||||
+ (['software-properties-common'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
+ (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else [])
|
||||
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
bootstrap_debian_extra_common
|
||||
+ bootstrap_debian_extra_versioned
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_ubuntu:
|
||||
base:
|
||||
- btrfs-progs
|
||||
- cron
|
||||
- cryptsetup-initramfs
|
||||
- gnupg
|
||||
- grub-efi
|
||||
- grub-efi-amd64-signed
|
||||
- grub2-common
|
||||
- initramfs-tools
|
||||
- linux-image-generic
|
||||
- locales
|
||||
- logrotate
|
||||
- lvm2
|
||||
- openssh-server
|
||||
- python3
|
||||
- xfsprogs
|
||||
extra:
|
||||
- apparmor-utils
|
||||
- bash-completion
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- dnsutils
|
||||
- duf
|
||||
- entr
|
||||
- eza
|
||||
- fdupes
|
||||
- fio
|
||||
- fish
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- ncurses-term
|
||||
- needrestart
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- software-properties-common
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- systemd-zram-generator
|
||||
- tcpd
|
||||
- traceroute
|
||||
- util-linux-extra
|
||||
- vim
|
||||
- wget
|
||||
- yq
|
||||
- zoxide
|
||||
- zstd
|
||||
conditional: >-
|
||||
extra: >-
|
||||
{{
|
||||
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
|
||||
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
bootstrap_debian_base_common
|
||||
+ bootstrap_debian_extra_common
|
||||
+ ['bash-completion', 'dnsutils', 'duf', 'eza', 'fdupes', 'fio',
|
||||
'ncurses-term', 'software-properties-common', 'systemd-zram-generator',
|
||||
'tldr', 'traceroute', 'util-linux-extra', 'yq', 'zoxide']
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_archlinux:
|
||||
base:
|
||||
- base
|
||||
- btrfs-progs
|
||||
- cronie
|
||||
- dhcpcd
|
||||
- efibootmgr
|
||||
- fastfetch
|
||||
- fish
|
||||
- fzf
|
||||
- grub
|
||||
- htop
|
||||
- libpwquality
|
||||
- linux
|
||||
- logrotate
|
||||
- lrzsz
|
||||
- lsof
|
||||
- lvm2
|
||||
- ncdu
|
||||
- networkmanager
|
||||
- nfs-utils
|
||||
- ppp
|
||||
- python
|
||||
- reflector
|
||||
- rsync
|
||||
- sudo
|
||||
- tldr
|
||||
- tmux
|
||||
- vim
|
||||
- zram-generator
|
||||
extra: []
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['iptables-nft'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (['sbctl'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
|
||||
}}
|
||||
bootstrap_archlinux: >-
|
||||
{{
|
||||
['base', 'btrfs-progs', 'cronie', 'dhcpcd', 'efibootmgr', 'fastfetch',
|
||||
'fish', 'fzf', 'grub', 'htop', 'libpwquality', 'linux', 'logrotate',
|
||||
'lrzsz', 'lsof', 'lvm2', 'ncdu', 'networkmanager', 'nfs-utils',
|
||||
'ppp', 'python', 'reflector',
|
||||
'rsync', 'sudo', 'tldr', 'tmux', 'vim', 'zram-generator']
|
||||
+ (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['iptables-nft'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
|
||||
}}
|
||||
|
||||
bootstrap_alpine:
|
||||
base:
|
||||
- alpine-base
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- e2fsprogs
|
||||
- linux-lts
|
||||
- logrotate
|
||||
- lvm2
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- util-linux
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
bootstrap_alpine: >-
|
||||
{{
|
||||
['alpine-base', 'btrfs-progs', 'chrony', 'curl', 'e2fsprogs',
|
||||
'linux-lts', 'logrotate', 'lvm2', 'python3', 'rsync', 'sudo',
|
||||
'util-linux', 'vim', 'xfsprogs']
|
||||
+ (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_opensuse:
|
||||
base:
|
||||
- patterns-base-base
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- e2fsprogs
|
||||
- glibc-locale
|
||||
- kernel-default
|
||||
- logrotate
|
||||
- lvm2
|
||||
- NetworkManager
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
bootstrap_opensuse: >-
|
||||
{{
|
||||
['btrfs-progs', 'chrony', 'curl', 'e2fsprogs',
|
||||
'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_void:
|
||||
base:
|
||||
- base-system
|
||||
- void-repo-nonfree
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- dhcpcd
|
||||
- e2fsprogs
|
||||
- logrotate
|
||||
- lvm2
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
bootstrap_void: >-
|
||||
{{
|
||||
['btrfs-progs', 'chrony', 'curl', 'dhcpcd', 'e2fsprogs',
|
||||
'logrotate', 'lvm2', 'python3', 'rsync', 'sudo',
|
||||
'vim', 'xfsprogs']
|
||||
+ (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -1,91 +1,8 @@
|
||||
---
|
||||
# User-facing API: override via top-level `cis` dict in inventory.
|
||||
# Merged with these defaults in _normalize.yml → cis_cfg.
|
||||
cis_defaults:
|
||||
modules_blacklist:
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- cramfs
|
||||
- udf
|
||||
- usb-storage
|
||||
- dccp
|
||||
- sctp
|
||||
- rds
|
||||
- tipc
|
||||
- firewire-core
|
||||
- firewire-sbp2
|
||||
- thunderbolt
|
||||
sysctl:
|
||||
fs.suid_dumpable: 0
|
||||
kernel.dmesg_restrict: 1
|
||||
kernel.kptr_restrict: 2
|
||||
kernel.perf_event_paranoid: 3
|
||||
kernel.unprivileged_bpf_disabled: 1
|
||||
kernel.yama.ptrace_scope: 2
|
||||
kernel.randomize_va_space: 2
|
||||
net.ipv4.ip_forward: 0
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
net.ipv4.conf.all.log_martians: 1
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.all.secure_redirects: 0
|
||||
net.ipv4.conf.all.send_redirects: 0
|
||||
net.ipv4.conf.all.accept_redirects: 0
|
||||
net.ipv4.conf.all.accept_source_route: 0
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
net.ipv4.conf.default.log_martians: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
net.ipv4.conf.default.secure_redirects: 0
|
||||
net.ipv4.conf.default.send_redirects: 0
|
||||
net.ipv4.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.all.accept_redirects: 0
|
||||
net.ipv6.conf.all.disable_ipv6: 1
|
||||
net.ipv6.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.default.disable_ipv6: 1
|
||||
net.ipv6.conf.lo.disable_ipv6: 1
|
||||
sshd_options:
|
||||
- { option: LogLevel, value: VERBOSE }
|
||||
- { option: LoginGraceTime, value: "60" }
|
||||
- { option: PermitRootLogin, value: "no" }
|
||||
- { option: StrictModes, value: "yes" }
|
||||
- { option: MaxAuthTries, value: "4" }
|
||||
- { option: MaxSessions, value: "10" }
|
||||
- { option: MaxStartups, value: "10:30:60" }
|
||||
- { option: PubkeyAuthentication, value: "yes" }
|
||||
- { option: HostbasedAuthentication, value: "no" }
|
||||
- { option: IgnoreRhosts, value: "yes" }
|
||||
- { option: PasswordAuthentication, value: "no" }
|
||||
- { option: PermitEmptyPasswords, value: "no" }
|
||||
- { option: KerberosAuthentication, value: "no" }
|
||||
- { option: GSSAPIAuthentication, value: "no" }
|
||||
- { option: AllowAgentForwarding, value: "no" }
|
||||
- { option: AllowTcpForwarding, value: "no" }
|
||||
- { option: KbdInteractiveAuthentication, value: "no" }
|
||||
- { option: GatewayPorts, value: "no" }
|
||||
- { option: X11Forwarding, value: "no" }
|
||||
- { option: PermitUserEnvironment, value: "no" }
|
||||
- { option: ClientAliveInterval, value: "300" }
|
||||
- { option: ClientAliveCountMax, value: "1" }
|
||||
- { option: PermitTunnel, value: "no" }
|
||||
- { option: Banner, value: /etc/issue.net }
|
||||
pwquality_minlen: 14
|
||||
tmout: 900
|
||||
umask: "077"
|
||||
umask_profile: "027"
|
||||
faillock_deny: 5
|
||||
faillock_unlock_time: 900
|
||||
password_remember: 5
|
||||
|
||||
# Platform-specific binary names for CIS permission targets
|
||||
cis_fusermount_binary: "{{ 'fusermount3' if is_rhel | default(false) | bool else 'fusermount' }}"
|
||||
cis_write_binary: "{{ 'write' if is_rhel | default(false) | bool else 'wall' }}"
|
||||
|
||||
cis: {}
|
||||
|
||||
cis_permission_targets:
|
||||
- { path: "/mnt/etc/ssh/sshd_config", mode: "0600" }
|
||||
- { path: "/mnt/etc/cron.hourly", mode: "0700" }
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
- name: Normalize CIS input
|
||||
ansible.builtin.set_fact:
|
||||
cis_enabled: "{{ cis is defined and (cis is mapping or cis | bool) }}"
|
||||
cis_input: "{{ cis if cis is mapping else {} }}"
|
||||
|
||||
- name: Normalize CIS configuration
|
||||
when: cis_enabled and cis_cfg is not defined
|
||||
ansible.builtin.set_fact:
|
||||
cis_cfg: "{{ cis_defaults | combine(cis_input, recursive=True) }}"
|
||||
@@ -3,7 +3,7 @@
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/mnt/etc/profile"
|
||||
regexp: "^(\\s*)umask\\s+\\d+"
|
||||
line: "umask {{ cis_cfg.umask_profile }}"
|
||||
line: "umask 027"
|
||||
|
||||
# Non-RHEL/non-Debian distros: loop evaluates to [] (intentional skip)
|
||||
- name: Prevent Login to Accounts With Empty Password
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
# Fedora ships its own crypto-policies preset and update-crypto-policies
|
||||
# behaves differently; applying DEFAULT:NO-SHA1 can break package signing.
|
||||
- name: Configure System Cryptography Policy
|
||||
when: os in (os_family_rhel | difference(['fedora']))
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1"
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
---
|
||||
- name: Normalize CIS configuration
|
||||
ansible.builtin.import_tasks: _normalize.yml
|
||||
|
||||
- name: Apply CIS hardening
|
||||
when: cis_enabled
|
||||
block:
|
||||
- name: Include CIS hardening tasks
|
||||
ansible.builtin.include_tasks: "{{ cis_task }}"
|
||||
loop:
|
||||
- modules.yml
|
||||
- sysctl.yml
|
||||
- auth.yml
|
||||
- crypto.yml
|
||||
- files.yml
|
||||
- security_lines.yml
|
||||
- permissions.yml
|
||||
- sshd.yml
|
||||
loop_control:
|
||||
loop_var: cis_task
|
||||
- name: Include CIS hardening tasks
|
||||
ansible.builtin.include_tasks: "{{ cis_task }}"
|
||||
loop:
|
||||
- modules.yml
|
||||
- sysctl.yml
|
||||
- auth.yml
|
||||
- crypto.yml
|
||||
- files.yml
|
||||
- security_lines.yml
|
||||
- permissions.yml
|
||||
- sshd.yml
|
||||
loop_control:
|
||||
loop_var: cis_task
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
---
|
||||
- name: Disable Kernel Modules
|
||||
vars:
|
||||
# Ubuntu uses squashfs for snap packages — blacklisting it breaks snap entirely
|
||||
cis_modules_squashfs: "{{ [] if os in ['ubuntu', 'ubuntu-lts'] else ['squashfs'] }}"
|
||||
cis_modules_all: "{{ cis_cfg.modules_blacklist + cis_modules_squashfs }}"
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/modprobe.d/cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
# CIS LVL 3 Restrictions
|
||||
{% for mod in cis_modules_all %}
|
||||
install {{ mod }}{{ ' ' * (16 - mod | length) }}/bin/false
|
||||
{% endfor %}
|
||||
install freevxfs /bin/false
|
||||
install jffs2 /bin/false
|
||||
install hfs /bin/false
|
||||
install hfsplus /bin/false
|
||||
install cramfs /bin/false
|
||||
# Note: disabling squashfs breaks snap (Ubuntu). Remove for snap-dependent hosts.
|
||||
install squashfs /bin/false
|
||||
install udf /bin/false
|
||||
install usb-storage /bin/false
|
||||
install dccp /bin/false
|
||||
install sctp /bin/false
|
||||
install rds /bin/false
|
||||
install tipc /bin/false
|
||||
|
||||
- name: Remove old USB rules file
|
||||
ansible.builtin.file:
|
||||
|
||||
@@ -2,30 +2,19 @@
|
||||
- name: Add Security related lines into config files
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.content }}"
|
||||
loop:
|
||||
- { path: /mnt/etc/security/limits.conf, regexp: '^\*\s+hard\s+core\s+', content: "* hard core 0" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*minlen\s*=', content: "minlen = {{ cis_cfg.pwquality_minlen }}" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*dcredit\s*=', content: dcredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ucredit\s*=', content: ucredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ocredit\s*=', content: ocredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*lcredit\s*=', content: lcredit = -1 }
|
||||
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*umask\s+\d+'
|
||||
content: "umask {{ cis_cfg.umask }}"
|
||||
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*(export\s+)?TMOUT='
|
||||
content: "export TMOUT={{ cis_cfg.tmout }}"
|
||||
- path: '/mnt/{{ "usr/lib/systemd/journald.conf" if is_rhel | bool else "etc/systemd/journald.conf" }}'
|
||||
regexp: '^\s*#?\s*Storage='
|
||||
content: Storage=persistent
|
||||
- path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+logfile='
|
||||
content: Defaults logfile="/var/log/sudo.log"
|
||||
- path: /mnt/etc/pam.d/su
|
||||
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
|
||||
content: auth required pam_wheel.so
|
||||
- { path: /mnt/etc/security/limits.conf, content: "* hard core 0" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, content: minlen = 14 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, content: dcredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, content: ucredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, content: ocredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, content: lcredit = -1 }
|
||||
- { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: umask 077 }
|
||||
- { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: export TMOUT=900 }
|
||||
- { 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/pam.d/su, content: auth required pam_wheel.so }
|
||||
- path: >-
|
||||
/mnt/etc/{{
|
||||
"pam.d/common-auth"
|
||||
@@ -34,9 +23,8 @@
|
||||
if os == "fedora"
|
||||
else "pam.d/system-auth"
|
||||
}}
|
||||
regexp: '^\s*auth\s+required\s+pam_faillock\.so'
|
||||
content: >-
|
||||
auth required pam_faillock.so onerr=fail audit silent deny={{ cis_cfg.faillock_deny }} unlock_time={{ cis_cfg.faillock_unlock_time }}
|
||||
auth required pam_faillock.so onerr=fail audit silent deny=5 unlock_time=900
|
||||
- path: >-
|
||||
/mnt/etc/{{
|
||||
"pam.d/common-account"
|
||||
@@ -45,7 +33,6 @@
|
||||
if os == "fedora"
|
||||
else "pam.d/system-auth"
|
||||
}}
|
||||
regexp: '^\s*account\s+required\s+pam_faillock\.so'
|
||||
content: account required pam_faillock.so
|
||||
- path: >-
|
||||
/mnt/etc/pam.d/{{
|
||||
@@ -53,10 +40,9 @@
|
||||
if is_debian | bool
|
||||
else "passwd"
|
||||
}}
|
||||
regexp: '^\s*password\s+\[success=1.*\]\s+pam_unix\.so'
|
||||
content: >-
|
||||
password [success=1 default=ignore] pam_unix.so obscure sha512 remember={{ cis_cfg.password_remember }}
|
||||
- { path: /mnt/etc/hosts.deny, regexp: '^ALL:\s*ALL', content: "ALL: ALL" }
|
||||
- { path: /mnt/etc/hosts.allow, regexp: '^sshd:\s*ALL', content: "sshd: ALL" }
|
||||
password [success=1 default=ignore] pam_unix.so obscure sha512 remember=5
|
||||
- { path: /mnt/etc/hosts.deny, content: "ALL: ALL" }
|
||||
- { path: /mnt/etc/hosts.allow, content: "sshd: ALL" }
|
||||
loop_control:
|
||||
label: "{{ item.content }}"
|
||||
|
||||
@@ -4,7 +4,31 @@
|
||||
path: /mnt/etc/ssh/sshd_config
|
||||
regexp: ^\s*#?{{ item.option }}\s+.*$
|
||||
line: "{{ item.option }} {{ item.value }}"
|
||||
loop: "{{ cis_cfg.sshd_options }}"
|
||||
loop:
|
||||
- { option: LogLevel, value: VERBOSE }
|
||||
- { option: LoginGraceTime, value: "60" }
|
||||
- { option: PermitRootLogin, value: "no" }
|
||||
- { option: StrictModes, value: "yes" }
|
||||
- { option: MaxAuthTries, value: "4" }
|
||||
- { option: MaxSessions, value: "10" }
|
||||
- { option: MaxStartups, value: "10:30:60" }
|
||||
- { option: PubkeyAuthentication, value: "yes" }
|
||||
- { option: HostbasedAuthentication, value: "no" }
|
||||
- { option: IgnoreRhosts, value: "yes" }
|
||||
- { option: PasswordAuthentication, value: "no" }
|
||||
- { option: PermitEmptyPasswords, value: "no" }
|
||||
- { option: KerberosAuthentication, value: "no" }
|
||||
- { option: GSSAPIAuthentication, value: "no" }
|
||||
- { option: AllowAgentForwarding, value: "no" }
|
||||
- { option: AllowTcpForwarding, value: "no" }
|
||||
- { option: KbdInteractiveAuthentication, value: "no" }
|
||||
- { option: GatewayPorts, value: "no" }
|
||||
- { option: X11Forwarding, value: "no" }
|
||||
- { option: PermitUserEnvironment, value: "no" }
|
||||
- { option: ClientAliveInterval, value: "300" }
|
||||
- { option: ClientAliveCountMax, value: "1" }
|
||||
- { option: PermitTunnel, value: "no" }
|
||||
- { option: Banner, value: /etc/issue.net }
|
||||
loop_control:
|
||||
label: "{{ item.option }}"
|
||||
|
||||
@@ -38,3 +62,7 @@
|
||||
###########################
|
||||
AllowStreamLocalForwarding no
|
||||
PermitUserRC no
|
||||
AllowUsers *
|
||||
AllowGroups *
|
||||
DenyUsers nobody
|
||||
DenyGroups nobody
|
||||
|
||||
@@ -5,6 +5,30 @@
|
||||
mode: "0644"
|
||||
content: |
|
||||
## CIS Sysctl configurations
|
||||
{% for key, value in cis_cfg.sysctl | dictsort %}
|
||||
{{ key }}={{ value }}
|
||||
{% endfor %}
|
||||
fs.suid_dumpable=0
|
||||
kernel.dmesg_restrict=1
|
||||
kernel.yama.ptrace_scope=1
|
||||
kernel.randomize_va_space=2
|
||||
# Network
|
||||
# Disable forwarding; override in inventory for routers/containers
|
||||
net.ipv4.ip_forward=0
|
||||
net.ipv4.tcp_syncookies=1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts=1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses=1
|
||||
net.ipv4.conf.all.log_martians=1
|
||||
net.ipv4.conf.all.rp_filter=1
|
||||
net.ipv4.conf.all.secure_redirects=0
|
||||
net.ipv4.conf.all.send_redirects=0
|
||||
net.ipv4.conf.all.accept_redirects=0
|
||||
net.ipv4.conf.all.accept_source_route=0
|
||||
net.ipv4.conf.default.log_martians=1
|
||||
net.ipv4.conf.default.rp_filter=1
|
||||
net.ipv4.conf.default.secure_redirects=0
|
||||
net.ipv4.conf.default.send_redirects=0
|
||||
net.ipv4.conf.default.accept_redirects=0
|
||||
net.ipv6.conf.all.accept_redirects=0
|
||||
# Disable IPv6; override in inventory if IPv6 is needed
|
||||
net.ipv6.conf.all.disable_ipv6=1
|
||||
net.ipv6.conf.default.accept_redirects=0
|
||||
net.ipv6.conf.default.disable_ipv6=1
|
||||
net.ipv6.conf.lo.disable_ipv6=1
|
||||
|
||||
@@ -3,3 +3,13 @@
|
||||
cleanup_verify_boot: true
|
||||
cleanup_boot_timeout: 300
|
||||
cleanup_remove_on_failure: true
|
||||
|
||||
# Libvirt paths
|
||||
cleanup_libvirt_image_dir: >-
|
||||
{{
|
||||
system_cfg.path
|
||||
if system_cfg is defined and (system_cfg.path | string | length) > 0
|
||||
else '/var/lib/libvirt/images'
|
||||
}}
|
||||
cleanup_libvirt_cloudinit_path: >-
|
||||
{{ [cleanup_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }}
|
||||
|
||||
@@ -14,6 +14,19 @@
|
||||
- name: Initialize cleaned VM XML
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Remove boot ISO device from VM XML (target match)
|
||||
community.general.xml:
|
||||
xmlstring: "{{ cleanup_libvirt_domain_xml }}"
|
||||
xpath: "/domain/devices/disk[target/@dev='sda']"
|
||||
state: absent
|
||||
register: cleanup_libvirt_xml_strip_boot
|
||||
|
||||
- name: Update cleaned VM XML after removing boot ISO
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Remove boot ISO device from VM XML (source match)
|
||||
when: boot_iso is defined and boot_iso | length > 0
|
||||
@@ -27,17 +40,19 @@
|
||||
when: boot_iso is defined and boot_iso | length > 0
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot_source.xmlstring }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Remove boot ISO device from VM XML (target fallback)
|
||||
- name: Remove cloud-init ISO device from VM XML (target match)
|
||||
community.general.xml:
|
||||
xmlstring: "{{ cleanup_libvirt_domain_xml }}"
|
||||
xpath: "/domain/devices/disk[target/@dev='sda']"
|
||||
xpath: "/domain/devices/disk[target/@dev='sdb']"
|
||||
state: absent
|
||||
register: cleanup_libvirt_xml_strip_boot
|
||||
register: cleanup_libvirt_xml_strip_cloudinit
|
||||
|
||||
- name: Update cleaned VM XML after removing boot ISO
|
||||
- name: Update cleaned VM XML after removing cloud-init ISO
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Remove cloud-init ISO device from VM XML (source match)
|
||||
community.general.xml:
|
||||
@@ -49,17 +64,7 @@
|
||||
- name: Update cleaned VM XML after removing cloud-init ISO source match
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit_source.xmlstring }}"
|
||||
|
||||
- name: Remove cloud-init ISO device from VM XML (target fallback)
|
||||
community.general.xml:
|
||||
xmlstring: "{{ cleanup_libvirt_domain_xml }}"
|
||||
xpath: "/domain/devices/disk[target/@dev='sdb']"
|
||||
state: absent
|
||||
register: cleanup_libvirt_xml_strip_cloudinit
|
||||
|
||||
- name: Update cleaned VM XML after removing cloud-init ISO
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Strip XML declaration for libvirt define
|
||||
ansible.builtin.set_fact:
|
||||
@@ -71,12 +76,7 @@
|
||||
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
|
||||
| trim
|
||||
}}
|
||||
|
||||
- name: Ensure boot device is set to hard disk in VM XML
|
||||
when: "'<boot ' not in cleanup_libvirt_domain_xml_clean"
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_libvirt_domain_xml_clean: >-
|
||||
{{ cleanup_libvirt_domain_xml_clean | regex_replace('(</type>)', '\1\n <boot dev="hd"/>') }}
|
||||
changed_when: false
|
||||
|
||||
- name: Update VM definition without installer media
|
||||
community.libvirt.virt:
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
- name: Remove cloud-init disk
|
||||
ansible.builtin.file:
|
||||
path: "{{ virtualization_libvirt_cloudinit_path }}"
|
||||
path: "{{ cleanup_libvirt_cloudinit_path }}"
|
||||
state: absent
|
||||
|
||||
- name: Ensure VM is powered off before restart
|
||||
@@ -94,35 +94,6 @@
|
||||
state: destroyed
|
||||
failed_when: false
|
||||
|
||||
- name: Enroll Secure Boot keys in VM NVRAM
|
||||
when:
|
||||
- system_cfg.features.secure_boot.enabled | default(false) | bool
|
||||
- os != 'archlinux'
|
||||
block:
|
||||
- name: Find VM NVRAM file path
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
virsh -c {{ libvirt_uri | default('qemu:///system') }} dumpxml {{ hostname }}
|
||||
| grep -oP '<nvram[^>]*>\K[^<]+'
|
||||
executable: /bin/bash
|
||||
register: _sb_nvram_path
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Enroll Secure Boot keys via virt-fw-vars
|
||||
when: _sb_nvram_path.stdout | default('') | length > 0
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- virt-fw-vars
|
||||
- --inplace
|
||||
- "{{ _sb_nvram_path.stdout | trim }}"
|
||||
- --enroll-redhat
|
||||
- --secure-boot
|
||||
register: _sb_enroll_result
|
||||
changed_when: _sb_enroll_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Start the VM
|
||||
community.libvirt.virt:
|
||||
name: "{{ hostname }}"
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
module_defaults:
|
||||
community.proxmox.proxmox_disk: "{{ _proxmox_auth }}"
|
||||
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
|
||||
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:
|
||||
- name: Cleanup Setup Disks
|
||||
community.proxmox.proxmox_disk:
|
||||
@@ -25,4 +32,3 @@
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: restarted
|
||||
no_log: true
|
||||
|
||||
@@ -88,7 +88,11 @@
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
module_defaults:
|
||||
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
|
||||
community.proxmox.proxmox_kvm:
|
||||
api_host: "{{ hypervisor_cfg.url }}"
|
||||
api_user: "{{ hypervisor_cfg.username }}"
|
||||
api_password: "{{ hypervisor_cfg.password }}"
|
||||
node: "{{ hypervisor_cfg.host }}"
|
||||
no_log: true
|
||||
block:
|
||||
- name: Stop Proxmox VM
|
||||
@@ -109,7 +113,11 @@
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
module_defaults:
|
||||
community.vmware.vmware_guest: "{{ _vmware_auth }}"
|
||||
community.vmware.vmware_guest:
|
||||
hostname: "{{ hypervisor_cfg.url }}"
|
||||
username: "{{ hypervisor_cfg.username }}"
|
||||
password: "{{ hypervisor_cfg.password }}"
|
||||
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
|
||||
no_log: true
|
||||
block:
|
||||
- name: Power off VMware VM
|
||||
|
||||
@@ -4,8 +4,18 @@
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
module_defaults:
|
||||
community.vmware.vmware_guest: "{{ _vmware_auth }}"
|
||||
vmware.vmware.vm_powerstate: "{{ _vmware_auth }}"
|
||||
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:
|
||||
- name: Remove CD-ROM from VM in vCenter
|
||||
|
||||
@@ -7,11 +7,34 @@
|
||||
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
|
||||
block:
|
||||
- name: Ensure Xen disk definitions exist
|
||||
ansible.builtin.include_tasks: ../../virtualization/tasks/_xen_disks.yml
|
||||
when: virtualization_xen_disks is not defined
|
||||
ansible.builtin.set_fact:
|
||||
cleanup_xen_disks: "{{ cleanup_xen_disks | default([]) + [cleanup_xen_disk_cfg] }}"
|
||||
vars:
|
||||
device_letter_map: "abcdefghijklmnopqrstuvwxyz"
|
||||
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
|
||||
cleanup_xen_disk_cfg: >-
|
||||
{{
|
||||
{
|
||||
'path': (
|
||||
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
|
||||
if ansible_loop.index0 == 0
|
||||
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
|
||||
),
|
||||
'target': 'xvd' ~ device_letter,
|
||||
'size': (item.size | float)
|
||||
}
|
||||
}}
|
||||
loop: "{{ system_cfg.disks }}"
|
||||
loop_control:
|
||||
label: "{{ item | to_json }}"
|
||||
extended: true
|
||||
changed_when: false
|
||||
|
||||
- name: Render Xen VM configuration without installer media
|
||||
vars:
|
||||
xen_installer_media_enabled: false
|
||||
virtualization_xen_disks: "{{ virtualization_xen_disks | default(cleanup_xen_disks | default([])) }}"
|
||||
ansible.builtin.template:
|
||||
src: xen.cfg.j2
|
||||
dest: /tmp/xen-{{ hostname }}.cfg
|
||||
@@ -35,8 +58,3 @@
|
||||
- /tmp/xen-{{ hostname }}.cfg
|
||||
register: cleanup_xen_start_result
|
||||
changed_when: cleanup_xen_start_result.rc == 0
|
||||
|
||||
- name: Remove temporary Xen configuration file
|
||||
ansible.builtin.file:
|
||||
path: /tmp/xen-{{ hostname }}.cfg
|
||||
state: absent
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
# Network configuration dispatch — maps OS name to the task file
|
||||
# that writes network config. Default (NetworkManager) applies to
|
||||
# all OSes not explicitly listed.
|
||||
configuration_network_task_map:
|
||||
alpine: network_alpine.yml
|
||||
void: network_void.yml
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
# Shared task: update BLS (Boot Loader Specification) entries with kernel cmdline.
|
||||
# Expects variable: _bls_cmdline (the kernel command line string)
|
||||
- name: Find BLS entries
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/boot/loader/entries
|
||||
patterns: "*.conf"
|
||||
register: _bls_entries
|
||||
changed_when: false
|
||||
|
||||
- name: Update BLS options
|
||||
when: _bls_entries.files | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "^options "
|
||||
line: "options {{ _bls_cmdline }}"
|
||||
loop: "{{ _bls_entries.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
# Resolve platform-specific configuration for the target OS family.
|
||||
# Sets _configuration_platform from configuration_platform_config[os_family].
|
||||
- name: Resolve platform-specific configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- os_family is defined
|
||||
- os_family in configuration_platform_config
|
||||
fail_msg: >-
|
||||
Unsupported os_family '{{ os_family | default("undefined") }}'.
|
||||
Extend configuration_platform_config in vars/main.yml.
|
||||
quiet: true
|
||||
|
||||
- name: Set platform configuration
|
||||
ansible.builtin.set_fact:
|
||||
_configuration_platform: "{{ configuration_platform_config[os_family] }}"
|
||||
|
||||
- name: Override EFI loader to shim for Secure Boot
|
||||
when:
|
||||
- system_cfg.features.secure_boot.enabled | bool
|
||||
- _configuration_platform.efi_loader != 'shimx64.efi'
|
||||
- os != 'archlinux'
|
||||
ansible.builtin.set_fact:
|
||||
_configuration_platform: >-
|
||||
{{ _configuration_platform | combine({'efi_loader': 'shimx64.efi'}) }}
|
||||
@@ -6,10 +6,11 @@
|
||||
"redhat" if os == "rhel"
|
||||
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
|
||||
}}
|
||||
_efi_loader: "{{ _configuration_platform.efi_loader }}"
|
||||
_efi_loader: >-
|
||||
{{ "shimx64.efi" if is_rhel | bool else "grubx64.efi" }}
|
||||
block:
|
||||
- name: Install GRUB EFI binary
|
||||
when: _configuration_platform.grub_install
|
||||
when: not (is_rhel | bool)
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/sbin/grub-install --target=x86_64-efi
|
||||
--efi-directory={{ partitioning_efi_mountpoint }}
|
||||
@@ -34,16 +35,6 @@
|
||||
register: configuration_efi_entry_result
|
||||
changed_when: configuration_efi_entry_result.rc == 0
|
||||
|
||||
- name: Set installed OS as first EFI boot entry
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
efibootmgr | grep -i '{{ _efi_vendor }}' | grep -oP 'Boot\K[0-9A-F]+' | head -1
|
||||
| xargs -I{} efibootmgr -o {}
|
||||
executable: /bin/bash
|
||||
register: _efi_bootorder_result
|
||||
changed_when: _efi_bootorder_result.rc == 0
|
||||
|
||||
- name: Ensure lvm2 for non btrfs filesystems
|
||||
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
|
||||
ansible.builtin.lineinfile:
|
||||
@@ -53,67 +44,33 @@
|
||||
backrefs: true
|
||||
|
||||
- name: Regenerate initramfs
|
||||
when: _configuration_platform.initramfs_cmd | length > 0
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ _configuration_platform.initramfs_cmd }}"
|
||||
when: os not in ["alpine", "void"]
|
||||
vars:
|
||||
configuration_initramfs_cmd: >-
|
||||
{{
|
||||
'/usr/sbin/mkinitcpio -P'
|
||||
if os == "archlinux"
|
||||
else (
|
||||
'/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin '
|
||||
+ '/usr/sbin/update-initramfs -u -k all'
|
||||
if is_debian | bool
|
||||
else '/usr/bin/dracut --regenerate-all --force'
|
||||
)
|
||||
}}
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ configuration_initramfs_cmd }}"
|
||||
register: configuration_initramfs_result
|
||||
changed_when: configuration_initramfs_result.rc == 0
|
||||
|
||||
- name: Generate grub config (RedHat)
|
||||
when: os_family == 'RedHat'
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/sbin/{{ _configuration_platform.grub_mkconfig_prefix }}
|
||||
-o /boot/grub2/grub.cfg
|
||||
register: configuration_grub_result
|
||||
changed_when: configuration_grub_result.rc == 0
|
||||
|
||||
- name: Fix btrfs BLS boot variable in grub config
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.filesystem == 'btrfs'
|
||||
ansible.builtin.replace:
|
||||
path: /mnt/boot/grub2/grub.cfg
|
||||
regexp: 'search --no-floppy --fs-uuid --set=boot \S+'
|
||||
replace: 'set boot=$root'
|
||||
|
||||
- name: Create EFI grub.cfg wrapper for RedHat
|
||||
when: os_family == 'RedHat'
|
||||
- name: Generate grub config
|
||||
vars:
|
||||
_grub2_path: >-
|
||||
configuration_grub_cfg_cmd: >-
|
||||
{{
|
||||
'/grub2'
|
||||
if (partitioning_separate_boot | bool)
|
||||
else ('/@/boot/grub2' if system_cfg.filesystem == 'btrfs' else '/boot/grub2')
|
||||
'/usr/sbin/grub2-mkconfig -o '
|
||||
+ partitioning_efi_mountpoint
|
||||
+ '/EFI/' + _efi_vendor + '/grub.cfg'
|
||||
if is_rhel | bool
|
||||
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
|
||||
}}
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
uuid=$(grep -m1 'search.*--set=root' /mnt/boot/grub2/grub.cfg | grep -oP '[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}')
|
||||
cat > /mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grub.cfg <<GRUBEOF
|
||||
search --no-floppy --fs-uuid --set=dev $uuid
|
||||
set prefix=(\$dev){{ _grub2_path }}
|
||||
export \$prefix
|
||||
configfile \$prefix/grub.cfg
|
||||
GRUBEOF
|
||||
executable: /bin/bash
|
||||
register: _grub_wrapper_result
|
||||
changed_when: _grub_wrapper_result.rc == 0
|
||||
|
||||
- name: Generate grub config (non-RedHat)
|
||||
when: os_family != 'RedHat'
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg"
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"
|
||||
register: configuration_grub_result
|
||||
changed_when: configuration_grub_result.rc == 0
|
||||
|
||||
- name: Rebuild GRUB as standalone EFI for Secure Boot
|
||||
when:
|
||||
- system_cfg.features.secure_boot.enabled | default(false) | bool
|
||||
- os == 'archlinux'
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} grub-mkstandalone
|
||||
-d /usr/lib/grub/x86_64-efi
|
||||
-O x86_64-efi
|
||||
--disable-shim-lock
|
||||
-o {{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grubx64.efi
|
||||
boot/grub/grub.cfg=/boot/grub/grub.cfg
|
||||
register: _grub_standalone_result
|
||||
changed_when: _grub_standalone_result.rc == 0
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
---
|
||||
- name: Configure disk encryption
|
||||
when: system_cfg.luks.enabled | bool
|
||||
no_log: true
|
||||
vars:
|
||||
configuration_luks_passphrase: >-
|
||||
{{ system_cfg.luks.passphrase | string }}
|
||||
block:
|
||||
- name: Set LUKS configuration facts
|
||||
vars:
|
||||
_raw_pcrs: >-
|
||||
luks_tpm2_pcrs: >-
|
||||
{{
|
||||
(
|
||||
system_cfg.luks.tpm2.pcrs
|
||||
@@ -20,17 +19,6 @@
|
||||
| regex_replace('\\s+', '')
|
||||
| regex_replace('^\\+|\\+$', '')
|
||||
}}
|
||||
_sb_pcr7_safe: >-
|
||||
{{
|
||||
system_cfg.features.secure_boot.enabled | bool
|
||||
and system_cfg.type | default('virtual') != 'virtual'
|
||||
}}
|
||||
luks_tpm2_pcrs: >-
|
||||
{{
|
||||
_raw_pcrs
|
||||
if _raw_pcrs | length > 0
|
||||
else ('7' if (_sb_pcr7_safe | bool) else '')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_luks_mapper_name: "{{ system_cfg.luks.mapper }}"
|
||||
configuration_luks_uuid: "{{ partitioning_luks_uuid | default('') }}"
|
||||
@@ -47,12 +35,7 @@
|
||||
configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}"
|
||||
configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}"
|
||||
configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key"
|
||||
configuration_luks_tpm2_token_lib: >-
|
||||
{{
|
||||
'/usr/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
|
||||
if os_family == 'Debian'
|
||||
else '/usr/lib64/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
|
||||
}}
|
||||
changed_when: false
|
||||
|
||||
- name: Validate LUKS UUID is available
|
||||
ansible.builtin.assert:
|
||||
@@ -68,13 +51,8 @@
|
||||
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
|
||||
no_log: true
|
||||
|
||||
- name: Detect TPM2 unlock method
|
||||
ansible.builtin.include_tasks: encryption/initramfs_detect.yml
|
||||
|
||||
- name: Enroll TPM2 via systemd-cryptenroll
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('systemd-cryptenroll') == 'systemd-cryptenroll'
|
||||
- name: Enroll TPM2 for LUKS
|
||||
when: configuration_luks_auto_method == 'tpm2'
|
||||
ansible.builtin.include_tasks: encryption/tpm2.yml
|
||||
|
||||
- name: Configure LUKS keyfile auto-decrypt
|
||||
@@ -100,7 +78,7 @@
|
||||
}}
|
||||
luks_tpm2_option_list: >-
|
||||
{{
|
||||
(configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll')
|
||||
(configuration_luks_auto_method == 'tpm2')
|
||||
| ternary(
|
||||
['tpm2-device=' + configuration_luks_tpm2_device]
|
||||
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||
@@ -144,16 +122,231 @@
|
||||
path: /mnt{{ configuration_luks_keyfile_path }}
|
||||
state: absent
|
||||
|
||||
- name: Configure initramfs for LUKS
|
||||
ansible.builtin.include_tasks: encryption/initramfs.yml
|
||||
- name: Write crypttab entry
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/crypttab
|
||||
regexp: "^{{ configuration_luks_mapper_name }}\\s"
|
||||
line: >-
|
||||
{{ configuration_luks_mapper_name }} UUID={{ configuration_luks_uuid }}
|
||||
{{ configuration_luks_crypttab_keyfile }} {{ configuration_luks_crypttab_options }}
|
||||
create: true
|
||||
mode: "0600"
|
||||
|
||||
- name: Configure crypttab
|
||||
ansible.builtin.include_tasks: encryption/crypttab.yml
|
||||
- name: Ensure keyfile pattern for initramfs-tools
|
||||
when:
|
||||
- is_debian | bool
|
||||
- configuration_luks_keyfile_in_use
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/cryptsetup-initramfs/conf-hook
|
||||
regexp: "^KEYFILE_PATTERN="
|
||||
line: "KEYFILE_PATTERN=/etc/cryptsetup-keys.d/*.key"
|
||||
create: true
|
||||
mode: "0644"
|
||||
|
||||
- name: Configure mkinitcpio hooks for LUKS
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/mkinitcpio.conf
|
||||
regexp: "^HOOKS="
|
||||
line: >-
|
||||
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole
|
||||
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
|
||||
|
||||
- name: Read mkinitcpio configuration
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/mkinitcpio.conf
|
||||
register: configuration_mkinitcpio_slurp
|
||||
|
||||
- name: Build mkinitcpio FILES list
|
||||
when: os == 'archlinux'
|
||||
vars:
|
||||
mkinitcpio_files_list: >-
|
||||
{{
|
||||
(
|
||||
configuration_mkinitcpio_slurp.content | b64decode
|
||||
| regex_findall('^FILES=\\(([^)]*)\\)', multiline=True)
|
||||
| default([])
|
||||
| first
|
||||
| default('')
|
||||
).split()
|
||||
}}
|
||||
mkinitcpio_files_list_new: >-
|
||||
{{
|
||||
(
|
||||
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
|
||||
if configuration_luks_keyfile_in_use
|
||||
else (
|
||||
mkinitcpio_files_list
|
||||
| reject('equalto', configuration_luks_keyfile_path)
|
||||
| list
|
||||
)
|
||||
)
|
||||
| unique
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
|
||||
|
||||
- name: Configure mkinitcpio FILES list
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/mkinitcpio.conf
|
||||
regexp: "^FILES="
|
||||
line: >-
|
||||
FILES=({{
|
||||
configuration_mkinitcpio_files_list_new | join(' ')
|
||||
}})
|
||||
|
||||
- name: Ensure dracut config directory exists
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/dracut.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure dracut for LUKS
|
||||
when: _initramfs_generator | default('') == 'dracut'
|
||||
ansible.builtin.include_tasks: encryption/dracut.yml
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/dracut.conf.d/crypt.conf
|
||||
content: |
|
||||
add_dracutmodules+=" crypt "
|
||||
{% if configuration_luks_keyfile_in_use %}
|
||||
install_items+=" {{ configuration_luks_keyfile_path }} "
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Configure GRUB for LUKS
|
||||
when: _initramfs_generator | default('') != 'dracut' or os_family != 'RedHat'
|
||||
ansible.builtin.include_tasks: encryption/grub.yml
|
||||
- name: Read kernel cmdline defaults
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/kernel/cmdline
|
||||
register: configuration_kernel_cmdline_slurp
|
||||
|
||||
- name: Build kernel cmdline with LUKS args
|
||||
when: is_rhel | bool
|
||||
vars:
|
||||
kernel_cmdline_current: >-
|
||||
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }}
|
||||
kernel_cmdline_list: >-
|
||||
{{
|
||||
kernel_cmdline_current.split()
|
||||
if kernel_cmdline_current | length > 0 else []
|
||||
}}
|
||||
kernel_cmdline_filtered: >-
|
||||
{{
|
||||
kernel_cmdline_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
}}
|
||||
kernel_cmdline_new: >-
|
||||
{{
|
||||
(kernel_cmdline_filtered + configuration_luks_kernel_args.split())
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Write kernel cmdline with LUKS args
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/kernel/cmdline
|
||||
mode: "0644"
|
||||
content: "{{ configuration_kernel_cmdline_new }}\n"
|
||||
|
||||
- name: Find BLS entries for encryption kernel cmdline
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/boot/loader/entries
|
||||
patterns: "*.conf"
|
||||
register: configuration_kernel_bls_entries
|
||||
changed_when: false
|
||||
|
||||
- name: Update BLS options with LUKS args
|
||||
when:
|
||||
- is_rhel | bool
|
||||
- configuration_kernel_bls_entries.files | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "^options "
|
||||
line: "options {{ configuration_kernel_cmdline_new }}"
|
||||
loop: "{{ configuration_kernel_bls_entries.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Read grub defaults
|
||||
when: not is_rhel | bool
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/default/grub
|
||||
register: configuration_grub_slurp
|
||||
|
||||
- name: Build grub command lines with LUKS args
|
||||
when: not is_rhel | bool
|
||||
vars:
|
||||
grub_content: "{{ configuration_grub_slurp.content | b64decode }}"
|
||||
grub_cmdline_linux: >-
|
||||
{{
|
||||
grub_content
|
||||
| regex_findall('^GRUB_CMDLINE_LINUX=\"(.*)\"', multiline=True)
|
||||
| default([])
|
||||
| first
|
||||
| default('')
|
||||
}}
|
||||
grub_cmdline_default: >-
|
||||
{{
|
||||
grub_content
|
||||
| regex_findall('^GRUB_CMDLINE_LINUX_DEFAULT=\"(.*)\"', multiline=True)
|
||||
| default([])
|
||||
| first
|
||||
| default('')
|
||||
}}
|
||||
grub_cmdline_linux_list: >-
|
||||
{{
|
||||
grub_cmdline_linux.split()
|
||||
if grub_cmdline_linux | length > 0 else []
|
||||
}}
|
||||
grub_cmdline_default_list: >-
|
||||
{{
|
||||
grub_cmdline_default.split()
|
||||
if grub_cmdline_default | length > 0 else []
|
||||
}}
|
||||
luks_kernel_args_list: "{{ configuration_luks_kernel_args.split() }}"
|
||||
grub_cmdline_linux_new: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
grub_cmdline_linux_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
)
|
||||
+ luks_kernel_args_list
|
||||
)
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
grub_cmdline_default_new: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
grub_cmdline_default_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
)
|
||||
+ luks_kernel_args_list
|
||||
)
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_grub_content: "{{ grub_content }}"
|
||||
configuration_grub_cmdline_linux: "{{ grub_cmdline_linux }}"
|
||||
configuration_grub_cmdline_default: "{{ grub_cmdline_default }}"
|
||||
configuration_grub_cmdline_linux_new: "{{ grub_cmdline_linux_new }}"
|
||||
configuration_grub_cmdline_default_new: "{{ grub_cmdline_default_new }}"
|
||||
|
||||
- name: Update GRUB_CMDLINE_LINUX_DEFAULT for LUKS
|
||||
when: not is_rhel | bool
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/default/grub
|
||||
regexp: "^GRUB_CMDLINE_LINUX_DEFAULT="
|
||||
line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ configuration_grub_cmdline_default_new }}"'
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
- name: Write crypttab entry
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/crypttab
|
||||
regexp: "^{{ configuration_luks_mapper_name }}\\s"
|
||||
line: >-
|
||||
{{ configuration_luks_mapper_name }} UUID={{ configuration_luks_uuid }}
|
||||
{{ configuration_luks_crypttab_keyfile }} {{ configuration_luks_crypttab_options }}
|
||||
create: true
|
||||
mode: "0600"
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
- name: Ensure dracut config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/dracut.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Configure dracut for LUKS
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/dracut.conf.d/crypt.conf
|
||||
content: |
|
||||
add_dracutmodules+=" crypt systemd "
|
||||
{% if configuration_luks_keyfile_in_use | default(false) %}
|
||||
install_items+=" {{ configuration_luks_keyfile_path }} "
|
||||
{% endif %}
|
||||
{% if configuration_luks_auto_method == 'tpm2' %}
|
||||
install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} "
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
# --- Kernel cmdline: write rd.luks.* args for dracut ---
|
||||
- name: Ensure kernel cmdline directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/kernel
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Read existing kernel cmdline
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/kernel/cmdline
|
||||
register: _kernel_cmdline_slurp
|
||||
failed_when: false
|
||||
|
||||
- name: Build kernel cmdline with LUKS args
|
||||
vars:
|
||||
_cmdline_current: >-
|
||||
{{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }}
|
||||
_cmdline_list: >-
|
||||
{{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }}
|
||||
_cmdline_filtered: >-
|
||||
{{
|
||||
_cmdline_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
}}
|
||||
_cmdline_new: >-
|
||||
{{
|
||||
(_cmdline_filtered + configuration_luks_kernel_args.split())
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_dracut_kernel_cmdline: "{{ _cmdline_new }}"
|
||||
|
||||
- name: Write kernel cmdline with LUKS args
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/kernel/cmdline
|
||||
mode: "0644"
|
||||
content: "{{ _dracut_kernel_cmdline }}\n"
|
||||
|
||||
# --- BLS entries: RedHat-specific ---
|
||||
- name: Update BLS entries with LUKS kernel cmdline
|
||||
when: os_family == 'RedHat'
|
||||
vars:
|
||||
_bls_cmdline: "{{ _dracut_kernel_cmdline }}"
|
||||
ansible.builtin.include_tasks: ../_bls_update.yml
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
- name: Read grub defaults
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/default/grub
|
||||
register: configuration_grub_slurp
|
||||
|
||||
- name: Build grub command lines with LUKS args
|
||||
vars:
|
||||
grub_content: "{{ configuration_grub_slurp.content | b64decode }}"
|
||||
grub_cmdline_linux: >-
|
||||
{{
|
||||
grub_content
|
||||
| regex_findall('^GRUB_CMDLINE_LINUX=\"(.*)\"', multiline=True)
|
||||
| default([])
|
||||
| first
|
||||
| default('')
|
||||
}}
|
||||
grub_cmdline_default: >-
|
||||
{{
|
||||
grub_content
|
||||
| regex_findall('^GRUB_CMDLINE_LINUX_DEFAULT=\"(.*)\"', multiline=True)
|
||||
| default([])
|
||||
| first
|
||||
| default('')
|
||||
}}
|
||||
grub_cmdline_linux_list: >-
|
||||
{{
|
||||
grub_cmdline_linux.split()
|
||||
if grub_cmdline_linux | length > 0 else []
|
||||
}}
|
||||
grub_cmdline_default_list: >-
|
||||
{{
|
||||
grub_cmdline_default.split()
|
||||
if grub_cmdline_default | length > 0 else []
|
||||
}}
|
||||
luks_kernel_args_list: "{{ configuration_luks_kernel_args.split() }}"
|
||||
grub_cmdline_linux_new: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
grub_cmdline_linux_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
)
|
||||
+ luks_kernel_args_list
|
||||
)
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
grub_cmdline_default_new: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
grub_cmdline_default_list
|
||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||
| list
|
||||
)
|
||||
+ luks_kernel_args_list
|
||||
)
|
||||
| unique
|
||||
| join(' ')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_grub_content: "{{ grub_content }}"
|
||||
configuration_grub_cmdline_linux: "{{ grub_cmdline_linux }}"
|
||||
configuration_grub_cmdline_default: "{{ grub_cmdline_default }}"
|
||||
configuration_grub_cmdline_linux_new: "{{ grub_cmdline_linux_new }}"
|
||||
configuration_grub_cmdline_default_new: "{{ grub_cmdline_default_new }}"
|
||||
|
||||
- name: Update GRUB_CMDLINE_LINUX_DEFAULT for LUKS
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/default/grub
|
||||
regexp: "^GRUB_CMDLINE_LINUX_DEFAULT="
|
||||
line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ configuration_grub_cmdline_default_new }}"'
|
||||
@@ -1,152 +0,0 @@
|
||||
---
|
||||
# Initramfs configuration for LUKS auto-unlock.
|
||||
# Runs AFTER Build LUKS parameters (so configuration_luks_keyfile_in_use is set).
|
||||
# _initramfs_generator and _tpm2_method are set by initramfs_detect.yml.
|
||||
|
||||
# --- clevis: install and bind TPM2 ---
|
||||
- name: Install clevis in target system
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} apt install -y clevis clevis-luks clevis-tpm2 clevis-initramfs tpm2-tools
|
||||
register: _clevis_install_result
|
||||
changed_when: _clevis_install_result.rc == 0
|
||||
|
||||
- name: Install clevis on installer for LUKS binding
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
community.general.pacman:
|
||||
name:
|
||||
- clevis
|
||||
- tpm2-tools
|
||||
state: present
|
||||
retries: 3
|
||||
delay: 5
|
||||
|
||||
- name: Create clevis passphrase file
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/root/.luks-enroll-key
|
||||
content: "{{ configuration_luks_passphrase }}"
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Ensure TPM device accessible for clevis
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.shell: >-
|
||||
ls /mnt/dev/tpmrm0 2>/dev/null
|
||||
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Bind LUKS to TPM2 via clevis
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
vars:
|
||||
_clevis_config: >-
|
||||
{{
|
||||
'{"pcr_ids":"' + configuration_luks_tpm2_pcrs + '"}'
|
||||
if configuration_luks_tpm2_pcrs | length > 0
|
||||
else '{}'
|
||||
}}
|
||||
ansible.builtin.command: >-
|
||||
clevis luks bind -f -k /mnt/root/.luks-enroll-key
|
||||
-d {{ configuration_luks_device }} tpm2 '{{ _clevis_config }}'
|
||||
register: _clevis_bind_result
|
||||
changed_when: _clevis_bind_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
# Initramfs regeneration is handled by the bootloader task which runs after
|
||||
# encryption configuration. Clevis hooks are included automatically by
|
||||
# update-initramfs when clevis-initramfs is installed.
|
||||
|
||||
- name: Remove clevis passphrase file
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.file:
|
||||
path: /mnt/root/.luks-enroll-key
|
||||
state: absent
|
||||
|
||||
- name: Report clevis binding result
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ 'Clevis TPM2 binding succeeded' if (_clevis_bind_result.rc | default(1)) == 0
|
||||
else 'Clevis TPM2 binding failed: ' + (_clevis_bind_result.stderr | default('unknown')) + '. System will require passphrase at boot.' }}
|
||||
|
||||
# --- initramfs-tools: keyfile support (non-TPM2) ---
|
||||
- name: Configure initramfs-tools keyfile pattern
|
||||
when:
|
||||
- _initramfs_generator | default('') == 'initramfs-tools'
|
||||
- configuration_luks_keyfile_in_use | default(false) | bool
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/cryptsetup-initramfs/conf-hook
|
||||
regexp: "^KEYFILE_PATTERN="
|
||||
line: "KEYFILE_PATTERN=/etc/cryptsetup-keys.d/*.key"
|
||||
create: true
|
||||
mode: "0644"
|
||||
|
||||
# --- mkinitcpio: systemd + sd-encrypt hooks ---
|
||||
- name: Configure mkinitcpio hooks for LUKS
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/mkinitcpio.conf
|
||||
regexp: "^HOOKS="
|
||||
line: >-
|
||||
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole
|
||||
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
|
||||
|
||||
- name: Read mkinitcpio configuration
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
ansible.builtin.slurp:
|
||||
src: /mnt/etc/mkinitcpio.conf
|
||||
register: configuration_mkinitcpio_slurp
|
||||
|
||||
- name: Build mkinitcpio FILES list
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
vars:
|
||||
mkinitcpio_files_list: >-
|
||||
{{
|
||||
(
|
||||
configuration_mkinitcpio_slurp.content | b64decode
|
||||
| regex_findall('^FILES=\\(([^)]*)\\)', multiline=True)
|
||||
| default([])
|
||||
| first
|
||||
| default('')
|
||||
).split()
|
||||
}}
|
||||
mkinitcpio_files_list_new: >-
|
||||
{{
|
||||
(
|
||||
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
|
||||
if (configuration_luks_keyfile_in_use | default(false))
|
||||
else (
|
||||
mkinitcpio_files_list
|
||||
| reject('equalto', configuration_luks_keyfile_path)
|
||||
| list
|
||||
)
|
||||
)
|
||||
| unique
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
|
||||
|
||||
- name: Configure mkinitcpio FILES list
|
||||
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/mkinitcpio.conf
|
||||
regexp: "^FILES="
|
||||
line: >-
|
||||
FILES=({{
|
||||
configuration_mkinitcpio_files_list_new | join(' ')
|
||||
}})
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
# Resolve initramfs generator and TPM2 unlock method.
|
||||
# Sets _initramfs_generator and _tpm2_method facts.
|
||||
#
|
||||
# Generator detection: derived from the platform's initramfs_cmd
|
||||
# (dracut → dracut, mkinitcpio → mkinitcpio, else → initramfs-tools)
|
||||
# TPM2 method: systemd-cryptenroll when generator supports tpm2-device,
|
||||
# clevis fallback otherwise. Non-native dracut installed automatically.
|
||||
|
||||
- name: Resolve initramfs generator
|
||||
vars:
|
||||
_user_generator: "{{ system_cfg.features.initramfs.generator | default('') }}"
|
||||
_native_generator: >-
|
||||
{{
|
||||
'dracut' if _configuration_platform.initramfs_cmd is search('dracut')
|
||||
else ('mkinitcpio' if _configuration_platform.initramfs_cmd is search('mkinitcpio')
|
||||
else 'initramfs-tools')
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_initramfs_generator: >-
|
||||
{{ _user_generator if _user_generator | length > 0 else _native_generator }}
|
||||
_initramfs_native_generator: "{{ _native_generator }}"
|
||||
|
||||
# --- Install non-native dracut if overridden or needed ---
|
||||
- name: Install dracut in chroot when not native
|
||||
when:
|
||||
- _initramfs_generator == 'dracut'
|
||||
- _initramfs_native_generator != 'dracut'
|
||||
ansible.builtin.shell: >-
|
||||
{{ chroot_command }} sh -c '
|
||||
command -v apt >/dev/null 2>&1 && apt install -y dracut ||
|
||||
command -v pacman >/dev/null 2>&1 && pacman -S --noconfirm dracut ||
|
||||
command -v dnf >/dev/null 2>&1 && dnf install -y dracut
|
||||
'
|
||||
register: _dracut_install_result
|
||||
changed_when: _dracut_install_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Override initramfs command to dracut
|
||||
when:
|
||||
- _initramfs_generator == 'dracut'
|
||||
- _initramfs_native_generator != 'dracut'
|
||||
vars:
|
||||
# Generate dracut initramfs with output name matching what GRUB expects:
|
||||
# mkinitcpio native: /boot/initramfs-linux.img (Arch convention)
|
||||
# initramfs-tools native: /boot/initrd.img-<kver> (Debian convention)
|
||||
_dracut_cmd: >-
|
||||
{{
|
||||
'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initramfs-linux.img $kver; done"'
|
||||
if _initramfs_native_generator == 'mkinitcpio'
|
||||
else 'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"'
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_configuration_platform: >-
|
||||
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
|
||||
|
||||
# --- TPM2 method detection ---
|
||||
- name: Probe dracut for TPM2 module support
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _initramfs_generator != 'mkinitcpio'
|
||||
ansible.builtin.command: "{{ chroot_command }} dracut --list-modules"
|
||||
register: _dracut_modules_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resolve TPM2 unlock method
|
||||
when: configuration_luks_auto_method == 'tpm2'
|
||||
vars:
|
||||
# mkinitcpio sd-encrypt supports tpm2-device natively
|
||||
# dracut with tpm2-tss module supports tpm2-device natively
|
||||
# everything else needs clevis
|
||||
_supports_tpm2_native: >-
|
||||
{{
|
||||
_initramfs_generator == 'mkinitcpio'
|
||||
or ('tpm2-tss' in (_dracut_modules_check.stdout | default('')))
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
_tpm2_method: "{{ 'systemd-cryptenroll' if _supports_tpm2_native | bool else 'clevis' }}"
|
||||
|
||||
# --- Auto-upgrade to dracut when tpm2-tss available but generator isn't dracut ---
|
||||
- name: Switch to dracut for TPM2 support
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method == 'systemd-cryptenroll'
|
||||
- _initramfs_generator not in ['dracut', 'mkinitcpio']
|
||||
vars:
|
||||
_dracut_cmd: >-
|
||||
bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"
|
||||
ansible.builtin.set_fact:
|
||||
_initramfs_generator: dracut
|
||||
_configuration_platform: >-
|
||||
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
|
||||
|
||||
- name: Report TPM2 configuration
|
||||
when: configuration_luks_auto_method == 'tpm2'
|
||||
ansible.builtin.debug:
|
||||
msg: "TPM2 unlock: {{ _tpm2_method | default('none') }} | initramfs: {{ _initramfs_generator }}"
|
||||
@@ -86,6 +86,7 @@
|
||||
device: "{{ configuration_luks_device }}"
|
||||
passphrase: "{{ configuration_luks_passphrase }}"
|
||||
new_keyfile: "/mnt{{ configuration_luks_keyfile_path }}"
|
||||
register: configuration_luks_addkey_retry
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
---
|
||||
# TPM2 enrollment via systemd-cryptenroll.
|
||||
# Works with dracut and mkinitcpio (sd-encrypt). The user-set passphrase
|
||||
# remains as a backup unlock method — no auto-generated keyfiles.
|
||||
- name: Enroll TPM2 for LUKS
|
||||
block:
|
||||
# Tempfile in chroot /tmp — accessible by both chroot and host commands
|
||||
- name: Create temporary passphrase file for TPM2 enrollment
|
||||
ansible.builtin.tempfile:
|
||||
path: /mnt/root
|
||||
path: /mnt/tmp
|
||||
prefix: luks-passphrase-
|
||||
state: file
|
||||
register: _tpm2_passphrase_tempfile
|
||||
register: configuration_luks_tpm2_passphrase_tempfile
|
||||
|
||||
- name: Write passphrase into temporary file
|
||||
- name: Write passphrase into temporary file for TPM2 enrollment
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ _tpm2_passphrase_tempfile.path }}"
|
||||
dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
|
||||
content: "{{ configuration_luks_passphrase }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Ensure TPM device is accessible in chroot
|
||||
ansible.builtin.shell: >-
|
||||
ls /mnt/dev/tpmrm0 2>/dev/null
|
||||
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Enroll TPM2 token via systemd-cryptenroll
|
||||
- name: Enroll TPM2 token
|
||||
vars:
|
||||
_enroll_args: >-
|
||||
configuration_luks_enroll_args: >-
|
||||
{{
|
||||
[
|
||||
'/usr/bin/systemd-cryptenroll',
|
||||
@@ -37,28 +28,69 @@
|
||||
'--tpm2-with-pin=false',
|
||||
'--wipe-slot=tpm2',
|
||||
'--unlock-key-file=' + (
|
||||
_tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '')
|
||||
configuration_luks_tpm2_passphrase_tempfile.path
|
||||
| regex_replace('^/mnt', '')
|
||||
)
|
||||
]
|
||||
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||
if configuration_luks_tpm2_pcrs | length > 0 else [])
|
||||
+ [configuration_luks_device]
|
||||
}}
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}"
|
||||
register: _tpm2_enroll_result
|
||||
changed_when: _tpm2_enroll_result.rc == 0
|
||||
configuration_luks_enroll_chroot_cmd: >-
|
||||
{{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }}
|
||||
ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}"
|
||||
register: configuration_luks_tpm2_enroll_chroot
|
||||
changed_when: configuration_luks_tpm2_enroll_chroot.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Retry TPM2 enrollment in installer environment
|
||||
when:
|
||||
- (configuration_luks_tpm2_enroll_chroot.rc | default(1)) != 0
|
||||
vars:
|
||||
configuration_luks_enroll_args: >-
|
||||
{{
|
||||
[
|
||||
'/usr/bin/systemd-cryptenroll',
|
||||
'--tpm2-device=' + configuration_luks_tpm2_device,
|
||||
'--tpm2-with-pin=false',
|
||||
'--wipe-slot=tpm2',
|
||||
'--unlock-key-file=' + configuration_luks_tpm2_passphrase_tempfile.path
|
||||
]
|
||||
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||
if configuration_luks_tpm2_pcrs | length > 0 else [])
|
||||
+ [configuration_luks_device]
|
||||
}}
|
||||
ansible.builtin.command:
|
||||
argv: "{{ configuration_luks_enroll_args }}"
|
||||
register: configuration_luks_tpm2_enroll_host
|
||||
changed_when: configuration_luks_tpm2_enroll_host.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Validate TPM2 enrollment succeeded
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(configuration_luks_tpm2_enroll_chroot.rc | default(1)) == 0
|
||||
or (configuration_luks_tpm2_enroll_host.rc | default(1)) == 0
|
||||
fail_msg: >-
|
||||
TPM2 enrollment failed.
|
||||
chroot rc={{ configuration_luks_tpm2_enroll_chroot.rc | default('n/a') }},
|
||||
host rc={{ configuration_luks_tpm2_enroll_host.rc | default('n/a') }},
|
||||
chroot stderr={{ configuration_luks_tpm2_enroll_chroot.stderr | default('') }},
|
||||
host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }}
|
||||
rescue:
|
||||
- name: TPM2 enrollment failed
|
||||
- name: Warn about TPM2 enrollment failure
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
TPM2 enrollment failed: {{ _tpm2_enroll_result.stderr | default('unknown') }}.
|
||||
The system will require the passphrase for LUKS unlock on boot.
|
||||
TPM2 can be enrolled post-deployment via: systemd-cryptenroll --tpm2-device=auto {{ configuration_luks_device }}
|
||||
TPM2 enrollment failed — falling back to keyfile auto-decrypt.
|
||||
The system will use a keyfile instead of TPM2 for automatic LUKS unlock.
|
||||
|
||||
- name: Fallback to keyfile auto-decrypt
|
||||
ansible.builtin.set_fact:
|
||||
configuration_luks_auto_method: keyfile
|
||||
always:
|
||||
- name: Remove temporary passphrase file
|
||||
when: _tpm2_passphrase_tempfile.path is defined
|
||||
- name: Remove TPM2 enrollment passphrase file
|
||||
when: configuration_luks_tpm2_passphrase_tempfile.path is defined
|
||||
ansible.builtin.file:
|
||||
path: "{{ _tpm2_passphrase_tempfile.path }}"
|
||||
path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
|
||||
state: absent
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
- name: Append vim configurations to vimrc
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ '/mnt/etc/vim/vimrc' if os_family == 'Debian' else '/mnt/etc/vimrc' }}"
|
||||
path: "{{ '/mnt/etc/vim/vimrc' if is_debian | bool else '/mnt/etc/vimrc' }}"
|
||||
block: |
|
||||
set encoding=utf-8
|
||||
set number
|
||||
@@ -9,11 +9,9 @@
|
||||
set smartindent
|
||||
set mouse=a
|
||||
insertafter: EOF
|
||||
marker: "\" {mark} CUSTOM VIM CONFIG"
|
||||
marker: "# {mark} CUSTOM VIM CONFIG"
|
||||
failed_when: false
|
||||
|
||||
# Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster
|
||||
# for zram. Override post-bootstrap via the linux role or sysctl if needed.
|
||||
- name: Add memory tuning parameters
|
||||
ansible.builtin.blockinfile:
|
||||
path: /mnt/etc/sysctl.d/90-memory.conf
|
||||
@@ -43,7 +41,7 @@
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy Custom Shell config
|
||||
ansible.builtin.copy:
|
||||
src: custom.sh
|
||||
ansible.builtin.template:
|
||||
src: custom.sh.j2
|
||||
dest: /mnt/etc/profile.d/custom.sh
|
||||
mode: "0644"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
- name: Configure grub defaults
|
||||
when: os_family != 'RedHat'
|
||||
when: not is_rhel | bool
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /mnt/etc/default/grub
|
||||
regexp: "{{ item.regexp }}"
|
||||
@@ -14,7 +14,7 @@
|
||||
label: "{{ item.line }}"
|
||||
|
||||
- name: Ensure grub defaults file exists for RHEL-based systems
|
||||
when: os_family == 'RedHat'
|
||||
when: is_rhel | bool
|
||||
block:
|
||||
- name: Build RHEL kernel command line defaults
|
||||
vars:
|
||||
@@ -62,6 +62,7 @@
|
||||
ansible.builtin.set_fact:
|
||||
configuration_grub_cmdline_linux_base: "{{ grub_cmdline_linux_base }}"
|
||||
configuration_kernel_cmdline_base: "{{ grub_kernel_cmdline_base }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Check if grub defaults file exists
|
||||
ansible.builtin.stat:
|
||||
@@ -96,10 +97,22 @@
|
||||
mode: "0644"
|
||||
content: "{{ configuration_kernel_cmdline_base }}\n"
|
||||
|
||||
- name: Update BLS entries with kernel cmdline defaults
|
||||
vars:
|
||||
_bls_cmdline: "{{ configuration_kernel_cmdline_base }}"
|
||||
ansible.builtin.include_tasks: _bls_update.yml
|
||||
- name: Find BLS entries for GRUB configuration
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/boot/loader/entries
|
||||
patterns: "*.conf"
|
||||
register: configuration_grub_bls_entries
|
||||
changed_when: false
|
||||
|
||||
- name: Update BLS options with kernel cmdline defaults
|
||||
when: configuration_grub_bls_entries.files | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "^options "
|
||||
line: "options {{ configuration_kernel_cmdline_base }}"
|
||||
loop: "{{ configuration_grub_bls_entries.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Enable GRUB cryptodisk for encrypted /boot
|
||||
when: partitioning_grub_enable_cryptodisk | bool
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- name: Setup locales
|
||||
block:
|
||||
- name: Configure locale.gen
|
||||
when: _configuration_platform.locale_gen
|
||||
when: not is_rhel | bool
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /mnt/etc/locale.gen
|
||||
regexp: "{{ item.regex }}"
|
||||
@@ -25,7 +25,7 @@
|
||||
label: "{{ item.line }}"
|
||||
|
||||
- name: Generate locales
|
||||
when: _configuration_platform.locale_gen
|
||||
when: not is_rhel | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/locale-gen"
|
||||
register: configuration_locale_result
|
||||
changed_when: configuration_locale_result.rc == 0
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
---
|
||||
- name: Resolve platform configuration
|
||||
ansible.builtin.import_tasks: _resolve_platform.yml
|
||||
|
||||
- name: Include configuration tasks
|
||||
when: configuration_task.when | default(true)
|
||||
ansible.builtin.include_tasks: "{{ configuration_task.file }}"
|
||||
loop:
|
||||
- file: repositories.yml
|
||||
when: "{{ os_family == 'Debian' }}"
|
||||
- file: banner.yml
|
||||
- file: fstab.yml
|
||||
- file: locales.yml
|
||||
@@ -17,14 +12,12 @@
|
||||
- file: encryption.yml
|
||||
when: "{{ system_cfg.luks.enabled | bool }}"
|
||||
- file: bootloader.yml
|
||||
- file: secure_boot.yml
|
||||
when: "{{ system_cfg.features.secure_boot.enabled | bool }}"
|
||||
- file: extras.yml
|
||||
- file: network.yml
|
||||
- file: users.yml
|
||||
- file: sudo.yml
|
||||
- file: selinux.yml
|
||||
when: "{{ os_family == 'RedHat' }}"
|
||||
when: "{{ is_rhel | bool }}"
|
||||
loop_control:
|
||||
loop_var: configuration_task
|
||||
label: "{{ configuration_task.file }}"
|
||||
|
||||
@@ -29,10 +29,9 @@
|
||||
- configuration_detected_interfaces | length > 0
|
||||
fail_msg: Failed to detect any network interfaces.
|
||||
|
||||
- name: Set DNS configuration facts
|
||||
ansible.builtin.set_fact:
|
||||
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
|
||||
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
|
||||
|
||||
- name: Configure networking
|
||||
vars:
|
||||
configuration_network_task_map:
|
||||
alpine: network_alpine.yml
|
||||
void: network_void.yml
|
||||
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
- 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"
|
||||
@@ -23,14 +25,13 @@
|
||||
{% endfor %}
|
||||
|
||||
- name: Set Alpine DNS resolvers
|
||||
when: configuration_dns_list | length > 0 or configuration_dns_search | length > 0
|
||||
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: |
|
||||
{% if configuration_dns_search | length > 0 %}
|
||||
search {{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% for resolver in configuration_dns_list %}
|
||||
nameserver {{ resolver }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
- name: Copy NetworkManager keyfile per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
configuration_iface_name: "{{ item.name | default(configuration_detected_interfaces[idx] | default('')) }}"
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
- name: Write dhcpcd configuration
|
||||
vars:
|
||||
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/dhcpcd.conf
|
||||
mode: "0644"
|
||||
@@ -18,9 +20,6 @@
|
||||
{% if loop.index0 == 0 and configuration_dns_list | length > 0 %}
|
||||
static domain_name_servers={{ configuration_dns_list | join(' ') }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_search | length > 0 %}
|
||||
static domain_search={{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
- name: Write final sources.list
|
||||
vars:
|
||||
_debian_release_map:
|
||||
"10": buster
|
||||
"11": bullseye
|
||||
"12": bookworm
|
||||
"13": trixie
|
||||
unstable: sid
|
||||
_ubuntu_release_map:
|
||||
ubuntu: questing
|
||||
ubuntu-lts: noble
|
||||
ansible.builtin.template:
|
||||
src: "{{ os | replace('-lts', '') }}.sources.list.j2"
|
||||
dest: /mnt/etc/apt/sources.list
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure apt performance configuration persists
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/apt/apt.conf.d/99performance
|
||||
content: |
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
mode: "0644"
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: Configure shim-based Secure Boot
|
||||
when: os != 'archlinux'
|
||||
ansible.builtin.include_tasks: secure_boot/shim.yml
|
||||
|
||||
- name: Configure sbctl Secure Boot
|
||||
when: os == 'archlinux'
|
||||
ansible.builtin.include_tasks: secure_boot/sbctl.yml
|
||||
@@ -1,115 +0,0 @@
|
||||
---
|
||||
- name: Configure sbctl Secure Boot
|
||||
block:
|
||||
- name: Create Secure Boot signing keys
|
||||
ansible.builtin.command: "{{ chroot_command }} sbctl create-keys"
|
||||
register: _sbctl_create_keys
|
||||
changed_when: _sbctl_create_keys.rc == 0
|
||||
failed_when:
|
||||
- _sbctl_create_keys.rc != 0
|
||||
- "'already exists' not in (_sbctl_create_keys.stderr | default(''))"
|
||||
|
||||
- name: Enroll Secure Boot keys in firmware
|
||||
ansible.builtin.command: "{{ chroot_command }} sbctl enroll-keys --microsoft"
|
||||
register: _sbctl_enroll
|
||||
changed_when: _sbctl_enroll.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Install first-boot enrollment service if chroot enrollment failed
|
||||
when: _sbctl_enroll.rc | default(1) != 0
|
||||
block:
|
||||
- name: Create first-boot sbctl enrollment service
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/system/sbctl-enroll.service
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Enroll Secure Boot keys via sbctl
|
||||
ConditionPathExists=!/var/lib/sbctl/.enrolled
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/sbctl enroll-keys --microsoft
|
||||
ExecStartPost=/usr/bin/touch /var/lib/sbctl/.enrolled
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
- name: Enable first-boot enrollment service
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable sbctl-enroll.service"
|
||||
register: _sbctl_service_enable
|
||||
changed_when: _sbctl_service_enable.rc == 0
|
||||
|
||||
- name: Find kernel images to sign
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/boot
|
||||
patterns: "vmlinuz-*"
|
||||
file_type: file
|
||||
register: _sbctl_kernel_images
|
||||
|
||||
- name: Sign kernel images
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} sbctl sign -s {{ item.path | regex_replace('^/mnt', '') }}
|
||||
loop: "{{ _sbctl_kernel_images.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
register: _sbctl_sign_kernel
|
||||
changed_when: _sbctl_sign_kernel.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Sign GRUB EFI binary
|
||||
vars:
|
||||
_grub_efi_path: "{{ partitioning_efi_mountpoint }}/EFI/archlinux/grubx64.efi"
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} sbctl sign -s {{ _grub_efi_path }}
|
||||
register: _sbctl_sign_grub
|
||||
changed_when: _sbctl_sign_grub.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Ensure pacman hooks directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/pacman.d/hooks
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Install sbctl auto-signing pacman hook
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/pacman.d/hooks/99-sbctl-sign.hook
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Trigger]
|
||||
Operation = Install
|
||||
Operation = Upgrade
|
||||
Type = Path
|
||||
Target = boot/vmlinuz-*
|
||||
Target = usr/lib/modules/*/vmlinuz
|
||||
|
||||
[Action]
|
||||
Description = Signing kernel images for Secure Boot...
|
||||
When = PostTransaction
|
||||
Exec = /usr/bin/sbctl sign-all
|
||||
Depends = sbctl
|
||||
|
||||
- name: Verify sbctl signing status
|
||||
ansible.builtin.command: "{{ chroot_command }} sbctl verify"
|
||||
register: _sbctl_verify
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Report sbctl Secure Boot status
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Secure Boot (sbctl):
|
||||
Enrollment={{ 'done' if (_sbctl_enroll.rc | default(1)) == 0 else 'deferred to first boot' }}.
|
||||
{{ _sbctl_verify.stdout | default('Verify not available') }}
|
||||
|
||||
rescue:
|
||||
- name: Secure Boot setup failed
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
sbctl Secure Boot setup failed.
|
||||
On VMs make sure the OVMF firmware is in Setup Mode (fresh NVRAM).
|
||||
On bare metal enter the firmware setup and switch to Setup Mode first.
|
||||
To recover manually: sbctl create-keys && sbctl enroll-keys --microsoft && sbctl sign-all
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
- name: Configure shim-based Secure Boot
|
||||
vars:
|
||||
_efi_vendor: >-
|
||||
{{
|
||||
"redhat" if os == "rhel"
|
||||
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
|
||||
}}
|
||||
block:
|
||||
- name: Find shim binary in target system
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
{{ chroot_command }} find /usr/lib/shim /boot/efi/EFI
|
||||
\( -name 'shimx64.efi.signed.latest' -o -name 'shimx64.efi.dualsigned'
|
||||
-o -name 'shimx64.efi.signed' -o -name 'shimx64.efi' \)
|
||||
-type f | sort -r | head -1
|
||||
executable: /bin/bash
|
||||
register: _shim_find_result
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Copy shim to EFI vendor directory
|
||||
when:
|
||||
- _shim_find_result.stdout | default('') | length > 0
|
||||
- _configuration_platform.grub_install | bool
|
||||
ansible.builtin.command: >-
|
||||
cp /mnt{{ _shim_find_result.stdout_lines | first }}
|
||||
/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi
|
||||
register: _shim_copy_result
|
||||
changed_when: _shim_copy_result.rc == 0
|
||||
|
||||
- name: Verify shim is present
|
||||
ansible.builtin.stat:
|
||||
path: "/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi"
|
||||
register: _shim_stat
|
||||
|
||||
- name: Report Secure Boot status
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Secure Boot (shim): {{
|
||||
'shimx64.efi installed at ' ~ partitioning_efi_mountpoint ~ '/EFI/' ~ _efi_vendor
|
||||
if (_shim_stat.stat.exists | default(false))
|
||||
else 'shimx64.efi not found, shim package may handle placement on first boot'
|
||||
}}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
- name: Fix SELinux
|
||||
when: os_family == 'RedHat'
|
||||
when: is_rhel | bool
|
||||
block:
|
||||
- name: Fix SELinux by pre-labeling the filesystem before first boot
|
||||
when: os in ['almalinux', 'rocky', 'rhel'] and system_cfg.features.selinux.enabled | bool
|
||||
@@ -11,8 +11,6 @@
|
||||
register: configuration_setfiles_result
|
||||
changed_when: configuration_setfiles_result.rc == 0
|
||||
|
||||
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
|
||||
# is left permissive and expected to relabel on first boot.
|
||||
- name: Disable SELinux
|
||||
when: os == "fedora" or not system_cfg.features.selinux.enabled | bool
|
||||
ansible.builtin.lineinfile:
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
---
|
||||
- name: Enable systemd services
|
||||
when: _configuration_platform.init_system == 'systemd'
|
||||
when: os not in ['alpine', 'void']
|
||||
vars:
|
||||
_desktop_dm: >-
|
||||
{{
|
||||
system_cfg.features.desktop.display_manager
|
||||
if (system_cfg.features.desktop.display_manager | length > 0)
|
||||
else (configuration_desktop_dm_map[system_cfg.features.desktop.environment] | default(''))
|
||||
}}
|
||||
configuration_systemd_services: >-
|
||||
{{
|
||||
['NetworkManager']
|
||||
+ (['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ ([('ssh' if is_debian | bool else 'sshd')] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
|
||||
+ ([_desktop_dm] if system_cfg.features.desktop.enabled | bool and _desktop_dm | length > 0 else [])
|
||||
+ (['bluetooth'] if system_cfg.features.desktop.enabled | bool else [])
|
||||
}}
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
|
||||
loop: "{{ configuration_systemd_services }}"
|
||||
register: configuration_enable_service_result
|
||||
changed_when: configuration_enable_service_result.rc == 0
|
||||
|
||||
- name: Activate UFW firewall
|
||||
when:
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} ufw --force enable"
|
||||
register: _ufw_enable_result
|
||||
changed_when: _ufw_enable_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Set default systemd target to graphical
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl set-default graphical.target"
|
||||
register: _desktop_target_result
|
||||
changed_when: _desktop_target_result.rc == 0
|
||||
|
||||
- name: Enable OpenRC services
|
||||
when: _configuration_platform.init_system == 'openrc'
|
||||
when: os == 'alpine'
|
||||
vars:
|
||||
configuration_openrc_services: >-
|
||||
{{
|
||||
@@ -73,7 +48,7 @@
|
||||
when: item.stat.exists
|
||||
|
||||
- name: Enable runit services
|
||||
when: _configuration_platform.init_system == 'runit'
|
||||
when: os == 'void'
|
||||
vars:
|
||||
configuration_runit_services: >-
|
||||
{{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
# Bootstrap-only: permissive SSH for initial Ansible access.
|
||||
# Post-bootstrap hardening (key-only, no root login) is handled by the linux role.
|
||||
- name: Ensure SSH password authentication is enabled
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/ssh/sshd_config
|
||||
|
||||
@@ -9,21 +9,18 @@
|
||||
|
||||
- name: Give sudo access to wheel group
|
||||
ansible.builtin.copy:
|
||||
content: "{{ _configuration_platform.sudo_group }} ALL=(ALL) ALL\n"
|
||||
content: "{{ '%sudo ALL=(ALL) ALL\n' if is_debian | bool else '%wheel ALL=(ALL) ALL\n' }}"
|
||||
dest: /mnt/etc/sudoers.d/01-wheel
|
||||
mode: "0440"
|
||||
validate: /usr/sbin/visudo --check --file=%s
|
||||
|
||||
- name: Deploy per-user sudoers rules
|
||||
when: item.value.sudo is defined and (item.value.sudo | string | length > 0)
|
||||
vars:
|
||||
configuration_sudoers_rule: >-
|
||||
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
||||
when: item.sudo is defined and (item.sudo | string | length) > 0
|
||||
ansible.builtin.copy:
|
||||
content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
|
||||
dest: "/mnt/etc/sudoers.d/{{ item.key }}"
|
||||
content: "{{ item.name }} {{ item.sudo }}\n"
|
||||
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
|
||||
mode: "0440"
|
||||
validate: /usr/sbin/visudo --check --file=%s
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
loop: "{{ system_cfg.users }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
label: "{{ item.name }}"
|
||||
|
||||
@@ -1,68 +1,62 @@
|
||||
---
|
||||
- name: Set root password
|
||||
when: (system_cfg.root.password | default('') | string | length) > 0
|
||||
ansible.builtin.shell: >-
|
||||
set -o pipefail &&
|
||||
echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} /usr/sbin/chpasswd -e
|
||||
echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} chpasswd -e
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: configuration_root_result
|
||||
changed_when: configuration_root_result.rc == 0
|
||||
no_log: true
|
||||
|
||||
- name: Lock root account when no password is set
|
||||
when: (system_cfg.root.password | default('') | string | length) == 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/bin/passwd -l root
|
||||
register: configuration_root_lock_result
|
||||
changed_when: configuration_root_lock_result.rc == 0
|
||||
|
||||
- name: Set root shell
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell }} root
|
||||
{{ 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
|
||||
vars:
|
||||
configuration_user_group: "{{ _configuration_platform.user_group }}"
|
||||
configuration_user_group: >-
|
||||
{{ "sudo" if is_debian | bool else "wheel" }}
|
||||
# UID starts at 1000; safe for fresh installs only
|
||||
configuration_useradd_cmd: >-
|
||||
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
|
||||
--uid {{ 1000 + _idx }}
|
||||
--groups {{ configuration_user_group }} {{ item.key }}
|
||||
{{ ('--password ' ~ (item.value.password | password_hash('sha512'))) if (item.value.password | default('') | string | length > 0) else '' }}
|
||||
--shell {{ item.value.shell | default('/bin/bash') }}
|
||||
--uid {{ 1000 + ansible_loop.index0 }}
|
||||
--groups {{ configuration_user_group }} {{ item.name }}
|
||||
--password {{ item.password | password_hash('sha512') }} --shell {{ item.shell | default('/bin/bash') }}
|
||||
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
loop: "{{ system_cfg.users }}"
|
||||
loop_control:
|
||||
index_var: _idx
|
||||
label: "{{ item.key }}"
|
||||
extended: true
|
||||
label: "{{ item.name }}"
|
||||
register: configuration_user_result
|
||||
changed_when: configuration_user_result.rc == 0
|
||||
no_log: true
|
||||
|
||||
- name: Ensure .ssh directory exists
|
||||
when: (item.value['keys'] | default([]) | length) > 0
|
||||
when: item['keys'] | default([]) | length > 0
|
||||
ansible.builtin.file:
|
||||
path: "/mnt/home/{{ item.key }}/.ssh"
|
||||
path: "/mnt/home/{{ item.name }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ 1000 + _idx }}"
|
||||
group: "{{ 1000 + _idx }}"
|
||||
owner: "{{ 1000 + ansible_loop.index0 }}"
|
||||
group: "{{ 1000 + ansible_loop.index0 }}"
|
||||
mode: "0700"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
loop: "{{ system_cfg.users }}"
|
||||
loop_control:
|
||||
index_var: _idx
|
||||
label: "{{ item.key }}"
|
||||
extended: true
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Deploy SSH authorized_keys
|
||||
when: (item.value['keys'] | default([]) | length) > 0
|
||||
ansible.builtin.copy:
|
||||
content: "{{ item.value['keys'] | join('\n') }}\n"
|
||||
dest: "/mnt/home/{{ item.key }}/.ssh/authorized_keys"
|
||||
owner: "{{ 1000 + _idx }}"
|
||||
group: "{{ 1000 + _idx }}"
|
||||
- name: Add SSH public keys to authorized_keys
|
||||
vars:
|
||||
_uid: "{{ 1000 + (system_cfg.users | map(attribute='name') | list).index(item.0.name) }}"
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/mnt/home/{{ item.0.name }}/.ssh/authorized_keys"
|
||||
line: "{{ item.1 }}"
|
||||
owner: "{{ _uid }}"
|
||||
group: "{{ _uid }}"
|
||||
mode: "0600"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
create: true
|
||||
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
|
||||
loop_control:
|
||||
index_var: _idx
|
||||
label: "{{ item.key }}"
|
||||
label: "{{ item.0.name }}: {{ item.1[:40] }}..."
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = _debian_release_map[os_version | string] | default('trixie') %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
{% if release != 'sid' %}
|
||||
|
||||
deb https://security.debian.org/debian-security {{ release }}-security {{ components }}
|
||||
deb-src https://security.debian.org/debian-security {{ release }}-security {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-updates {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-updates {{ components }}
|
||||
{% endif %}
|
||||
@@ -2,15 +2,12 @@
|
||||
id=LAN-{{ idx }}
|
||||
uuid={{ configuration_net_uuid }}
|
||||
type=ethernet
|
||||
autoconnect-priority=10
|
||||
{% if configuration_iface_name | length > 0 %}
|
||||
interface-name={{ configuration_iface_name }}
|
||||
{% endif %}
|
||||
|
||||
[ipv4]
|
||||
{% set iface = configuration_iface %}
|
||||
{% set dns_list = configuration_dns_list %}
|
||||
{% set search_list = configuration_dns_search %}
|
||||
{% set dns_list = system_cfg.network.dns.servers | default([]) %}
|
||||
{% set search_list = system_cfg.network.dns.search | default([]) %}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }}
|
||||
method=manual
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = _ubuntu_release_map[os] | default('noble') %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main restricted universe multiverse' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-updates {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-updates {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-security {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-security {{ components }}
|
||||
|
||||
deb {{ mirror }} {{ release }}-backports {{ components }}
|
||||
deb-src {{ mirror }} {{ release }}-backports {{ components }}
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
# Platform-specific configuration values keyed by os_family.
|
||||
# Consumed as _configuration_platform in tasks via:
|
||||
# configuration_platform_config[os_family]
|
||||
configuration_platform_config:
|
||||
RedHat:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: shimx64.efi
|
||||
grub_install: false
|
||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
||||
grub_mkconfig_prefix: grub2-mkconfig
|
||||
locale_gen: false
|
||||
init_system: systemd
|
||||
Debian:
|
||||
user_group: sudo
|
||||
sudo_group: "%sudo"
|
||||
ssh_service: ssh
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: >-
|
||||
/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
/usr/sbin/update-initramfs -u -k all
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
Archlinux:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: "/usr/sbin/mkinitcpio -P"
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
Suse:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
Alpine:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: ""
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: false
|
||||
init_system: openrc
|
||||
Void:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: ""
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: false
|
||||
init_system: runit
|
||||
|
||||
# Display manager auto-detection from desktop environment name.
|
||||
configuration_desktop_dm_map:
|
||||
gnome: gdm
|
||||
kde: sddm
|
||||
xfce: lightdm
|
||||
sway: greetd
|
||||
hyprland: ly@tty2
|
||||
cinnamon: lightdm
|
||||
mate: lightdm
|
||||
lxqt: sddm
|
||||
budgie: gdm
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
# Connection and timing
|
||||
environment_wait_timeout: 180
|
||||
environment_wait_delay: 5
|
||||
|
||||
# Pacman installer settings
|
||||
environment_parallel_downloads: 20
|
||||
environment_pacman_lock_timeout: 120
|
||||
environment_pacman_retries: 4
|
||||
environment_pacman_retry_delay: 15
|
||||
@@ -1,102 +0,0 @@
|
||||
---
|
||||
- name: Select primary Network Interface
|
||||
when: hypervisor_type == "vmware"
|
||||
ansible.builtin.set_fact:
|
||||
environment_interface_name: >-
|
||||
{{
|
||||
(
|
||||
(ansible_facts.interfaces | default(ansible_facts['ansible_interfaces'] | default([])))
|
||||
| reject('equalto', 'lo')
|
||||
| list
|
||||
| first
|
||||
)
|
||||
| default('')
|
||||
}}
|
||||
|
||||
- name: Bring up network interface
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- environment_interface_name | default('') | length > 0
|
||||
ansible.builtin.command: "ip link set {{ environment_interface_name }} up"
|
||||
register: environment_link_result
|
||||
changed_when: environment_link_result.rc == 0
|
||||
|
||||
- name: Set IP-Address
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
|
||||
ansible.builtin.command: >-
|
||||
ip addr replace {{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }}
|
||||
dev {{ environment_interface_name }}
|
||||
register: environment_ip_result
|
||||
changed_when: environment_ip_result.rc == 0
|
||||
|
||||
- name: Set Default Gateway
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length > 0
|
||||
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
|
||||
ansible.builtin.command: "ip route replace default via {{ system_cfg.network.gateway }}"
|
||||
register: environment_gateway_result
|
||||
changed_when: environment_gateway_result.rc == 0
|
||||
|
||||
- name: Configure DNS resolvers
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.network.dns.servers | default([]) | length > 0
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/resolv.conf
|
||||
content: |
|
||||
{% for server in system_cfg.network.dns.servers %}
|
||||
nameserver {{ server }}
|
||||
{% endfor %}
|
||||
{% if system_cfg.network.dns.search | default([]) | length > 0 %}
|
||||
search {{ system_cfg.network.dns.search | join(' ') }}
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Synchronize clock via NTP
|
||||
ansible.builtin.command: timedatectl set-ntp true
|
||||
register: environment_ntp_result
|
||||
changed_when: environment_ntp_result.rc == 0
|
||||
|
||||
- name: Configure SSH for root login
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- hypervisor_cfg.ssh | default(false) | bool
|
||||
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
|
||||
block:
|
||||
- name: Allow login
|
||||
ansible.builtin.replace:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "{{ item.regexp }}"
|
||||
replace: "{{ item.replace }}"
|
||||
loop:
|
||||
- regexp: "^#?PermitEmptyPasswords.*"
|
||||
replace: "PermitEmptyPasswords yes"
|
||||
- regexp: "^#?PermitRootLogin.*"
|
||||
replace: "PermitRootLogin yes"
|
||||
loop_control:
|
||||
label: "{{ item.replace }}"
|
||||
|
||||
- name: Reload SSH service to apply changes
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: reloaded
|
||||
|
||||
- name: Switch to SSH connection
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: ssh
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: root
|
||||
ansible_password: ""
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
|
||||
- name: Reset connection for SSH switchover
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify SSH connectivity
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 30
|
||||
delay: 2
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
- name: Wait for connection
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: "{{ environment_wait_timeout }}"
|
||||
delay: "{{ environment_wait_delay }}"
|
||||
|
||||
- name: Gather facts
|
||||
ansible.builtin.setup:
|
||||
|
||||
- name: Check for live environment markers
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop:
|
||||
- /run/archiso
|
||||
- /run/live
|
||||
- /run/initramfs
|
||||
- /run/initramfs/live
|
||||
register: environment_live_marker_stat
|
||||
changed_when: false
|
||||
|
||||
- name: Determine root filesystem type
|
||||
ansible.builtin.set_fact:
|
||||
environment_root_fstype: >-
|
||||
{{
|
||||
ansible_mounts
|
||||
| selectattr('mount', 'equalto', '/')
|
||||
| map(attribute='fstype')
|
||||
| list
|
||||
| first
|
||||
| default('')
|
||||
| lower
|
||||
}}
|
||||
environment_archiso_present: >-
|
||||
{{
|
||||
(
|
||||
environment_live_marker_stat.results
|
||||
| selectattr('item', 'equalto', '/run/archiso')
|
||||
| selectattr('stat.exists')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
}}
|
||||
|
||||
- name: Identify live environment indicators
|
||||
ansible.builtin.set_fact:
|
||||
environment_is_live_environment: >-
|
||||
{{
|
||||
(
|
||||
environment_live_marker_stat.results
|
||||
| selectattr('stat.exists')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
or environment_root_fstype in ['overlay', 'overlayfs', 'squashfs', 'aufs']
|
||||
or (ansible_hostname | default('') | lower is search('live'))
|
||||
}}
|
||||
|
||||
- name: Abort if target is not a live environment
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- environment_is_live_environment | bool
|
||||
fail_msg: |
|
||||
PRODUCTION SYSTEM DETECTED - ABORTING
|
||||
|
||||
The target system does not appear to be a live installer environment.
|
||||
This playbook must run from a live ISO to avoid wiping production data.
|
||||
|
||||
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
|
||||
quiet: true
|
||||
|
||||
- name: Harden sshd for Ansible automation
|
||||
ansible.builtin.blockinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
marker: "# {mark} BOOTSTRAP ANSIBLE SETTINGS"
|
||||
block: |
|
||||
PerSourcePenalties no
|
||||
MaxStartups 50:30:100
|
||||
ClientAliveInterval 30
|
||||
ClientAliveCountMax 10
|
||||
register: _sshd_config_result
|
||||
|
||||
- name: Restart sshd immediately if config was changed
|
||||
when: _sshd_config_result is changed
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
|
||||
- name: Abort if the host is not booted from the Arch install media
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
- not environment_archiso_present | bool
|
||||
ansible.builtin.fail:
|
||||
msg: This host is not booted from the Arch install media!
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
- name: Speed-up Bootstrap process
|
||||
when: not (custom_iso | bool)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pacman.conf
|
||||
regexp: ^#ParallelDownloads =
|
||||
line: "ParallelDownloads = {{ environment_parallel_downloads }}"
|
||||
|
||||
- name: Wait for pacman lock to be released
|
||||
when: not (custom_iso | bool)
|
||||
ansible.builtin.wait_for:
|
||||
path: /var/lib/pacman/db.lck
|
||||
state: absent
|
||||
timeout: "{{ environment_pacman_lock_timeout }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Setup Pacman
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
- item.os is not defined or os in item.os
|
||||
community.general.pacman:
|
||||
update_cache: true
|
||||
force: true
|
||||
name: "{{ item.name }}"
|
||||
state: latest
|
||||
loop:
|
||||
- { name: glibc }
|
||||
- { name: lua, os: [almalinux, fedora, rhel, rocky] }
|
||||
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
|
||||
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
|
||||
- { name: debian-archive-keyring, os: [debian] }
|
||||
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
retries: "{{ environment_pacman_retries }}"
|
||||
delay: "{{ environment_pacman_retry_delay }}"
|
||||
|
||||
- name: Prepare /iso mount and repository for RHEL-based systems
|
||||
when: os == "rhel"
|
||||
block:
|
||||
- name: Create /iso directory
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/install/redhat/dvd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Detect RHEL ISO device
|
||||
ansible.builtin.command: lsblk -rno NAME,TYPE
|
||||
register: environment_lsblk_result
|
||||
changed_when: false
|
||||
|
||||
- name: Select RHEL ISO device
|
||||
vars:
|
||||
_rom_devices: >-
|
||||
{{
|
||||
environment_lsblk_result.stdout_lines
|
||||
| map('split', ' ')
|
||||
| selectattr('1', 'equalto', 'rom')
|
||||
| map('first')
|
||||
| map('regex_replace', '^', '/dev/')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
environment_rhel_iso_device: >-
|
||||
{{
|
||||
_rom_devices[-1]
|
||||
if _rom_devices | length > 1
|
||||
else (_rom_devices[0] | default('/dev/sr1'))
|
||||
}}
|
||||
|
||||
- name: Mount RHEL ISO
|
||||
ansible.posix.mount:
|
||||
src: "{{ environment_rhel_iso_device }}"
|
||||
path: /usr/local/install/redhat/dvd
|
||||
fstype: iso9660
|
||||
opts: "ro,loop"
|
||||
state: mounted
|
||||
|
||||
# Security note: RPM Sequoia signature policy is relaxed to allow
|
||||
# bootstrapping RHEL-family distros from the Arch ISO, where the
|
||||
# host rpm/dnf does not trust target distro GPG keys. Package
|
||||
# integrity is verified by the target system's own rpm after reboot.
|
||||
- name: Create RPM macros directory
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.file:
|
||||
path: /etc/rpm
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Relax RPM Sequoia signature policy for RHEL bootstrap
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/rpm/macros
|
||||
content: "%_pkgverify_level none\n"
|
||||
mode: "0644"
|
||||
|
||||
- name: Configure RHEL Repos for installation
|
||||
when: is_rhel | bool
|
||||
block:
|
||||
- name: Create directories for repository files and RPM GPG keys
|
||||
ansible.builtin.file:
|
||||
path: /etc/yum.repos.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Create RHEL repository file
|
||||
ansible.builtin.template:
|
||||
src: "{{ os }}.repo.j2"
|
||||
dest: /etc/yum.repos.d/{{ os }}.repo
|
||||
mode: "0644"
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
- name: Check for third-party preparation tasks
|
||||
run_once: true
|
||||
become: false
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_connection: local
|
||||
block:
|
||||
- name: Resolve third-party preparation task path
|
||||
ansible.builtin.set_fact:
|
||||
environment_thirdparty_tasks_path: >-
|
||||
{{
|
||||
thirdparty_tasks
|
||||
if thirdparty_tasks | regex_search('^/')
|
||||
else playbook_dir + '/' + thirdparty_tasks
|
||||
}}
|
||||
|
||||
- name: Stat third-party preparation tasks
|
||||
ansible.builtin.stat:
|
||||
path: "{{ environment_thirdparty_tasks_path }}"
|
||||
register: environment_thirdparty_tasks_stat
|
||||
|
||||
- name: Run third-party preparation tasks
|
||||
when:
|
||||
- thirdparty_tasks | length > 0
|
||||
- environment_thirdparty_tasks_stat.stat.exists
|
||||
ansible.builtin.include_tasks: "{{ environment_thirdparty_tasks_path }}"
|
||||
@@ -1,15 +1,279 @@
|
||||
---
|
||||
- name: Configure work environment
|
||||
become: "{{ (hypervisor_type | default('none')) != 'vmware' }}"
|
||||
become: "{{ hypervisor_type != 'vmware' }}"
|
||||
block:
|
||||
- name: Detect and validate live environment
|
||||
ansible.builtin.include_tasks: _detect_live.yml
|
||||
- name: Wait for connection
|
||||
ansible.builtin.wait_for_connection:
|
||||
timeout: 180
|
||||
delay: 5
|
||||
|
||||
- name: Configure network and connectivity
|
||||
ansible.builtin.include_tasks: _configure_network.yml
|
||||
- name: Gather facts
|
||||
ansible.builtin.setup:
|
||||
|
||||
- name: Check for live environment markers
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop:
|
||||
- /run/archiso
|
||||
- /run/live
|
||||
- /run/initramfs
|
||||
- /run/initramfs/live
|
||||
register: environment_live_marker_stat
|
||||
changed_when: false
|
||||
|
||||
- name: Determine root filesystem type
|
||||
ansible.builtin.set_fact:
|
||||
environment_root_fstype: >-
|
||||
{{
|
||||
ansible_mounts
|
||||
| selectattr('mount', 'equalto', '/')
|
||||
| map(attribute='fstype')
|
||||
| list
|
||||
| first
|
||||
| default('')
|
||||
| lower
|
||||
}}
|
||||
environment_archiso_present: >-
|
||||
{{
|
||||
(
|
||||
environment_live_marker_stat.results
|
||||
| selectattr('item', 'equalto', '/run/archiso')
|
||||
| selectattr('stat.exists')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
}}
|
||||
changed_when: false
|
||||
|
||||
- name: Identify live environment indicators
|
||||
ansible.builtin.set_fact:
|
||||
environment_is_live_environment: >-
|
||||
{{
|
||||
(
|
||||
environment_live_marker_stat.results
|
||||
| selectattr('stat.exists')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
or environment_root_fstype in ['overlay', 'overlayfs', 'squashfs', 'aufs']
|
||||
or (ansible_hostname | default('') | lower is search('live'))
|
||||
}}
|
||||
changed_when: false
|
||||
|
||||
- name: Abort if target is not a live environment
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- environment_is_live_environment | bool
|
||||
fail_msg: |
|
||||
PRODUCTION SYSTEM DETECTED - ABORTING
|
||||
|
||||
The target system does not appear to be a live installer environment.
|
||||
This playbook must run from a live ISO to avoid wiping production data.
|
||||
|
||||
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
|
||||
quiet: true
|
||||
|
||||
- name: Abort if the host is not booted from the Arch install media
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
- not environment_archiso_present | bool
|
||||
ansible.builtin.fail:
|
||||
msg: This host is not booted from the Arch install media!
|
||||
|
||||
- name: Select primary Network Interface
|
||||
when: hypervisor_type == "vmware"
|
||||
ansible.builtin.set_fact:
|
||||
environment_interface_name: >-
|
||||
{{
|
||||
(
|
||||
(ansible_facts.interfaces | default(ansible_facts['ansible_interfaces'] | default([])))
|
||||
| reject('equalto', 'lo')
|
||||
| list
|
||||
| first
|
||||
)
|
||||
| default('')
|
||||
}}
|
||||
changed_when: false
|
||||
|
||||
- name: Set IP-Address
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
|
||||
ansible.builtin.command: >-
|
||||
ip addr replace {{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }}
|
||||
dev {{ environment_interface_name }}
|
||||
register: environment_ip_result
|
||||
changed_when: environment_ip_result.rc == 0
|
||||
|
||||
- name: Set Default Gateway
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length > 0
|
||||
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
|
||||
ansible.builtin.command: "ip route replace default via {{ system_cfg.network.gateway }}"
|
||||
register: environment_gateway_result
|
||||
changed_when: environment_gateway_result.rc == 0
|
||||
|
||||
- name: Synchronize clock via NTP
|
||||
ansible.builtin.command: timedatectl set-ntp true
|
||||
register: environment_ntp_result
|
||||
changed_when: environment_ntp_result.rc == 0
|
||||
|
||||
- name: Configure SSH for root login
|
||||
when: hypervisor_type == "vmware" and hypervisor_cfg.ssh | bool
|
||||
block:
|
||||
- name: Allow login
|
||||
ansible.builtin.replace:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "{{ item.regexp }}"
|
||||
replace: "{{ item.replace }}"
|
||||
loop:
|
||||
- regexp: "^#?PermitEmptyPasswords.*"
|
||||
replace: "PermitEmptyPasswords yes"
|
||||
- regexp: "^#?PermitRootLogin.*"
|
||||
replace: "PermitRootLogin yes"
|
||||
loop_control:
|
||||
label: "{{ item.replace }}"
|
||||
|
||||
- name: Reload SSH service to apply changes
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: reloaded
|
||||
|
||||
- name: Set SSH connection for VMware
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: ssh
|
||||
ansible_user: root
|
||||
|
||||
- name: Prepare installer environment
|
||||
ansible.builtin.include_tasks: _prepare_installer.yml
|
||||
block:
|
||||
- name: Speed-up Bootstrap process
|
||||
when: not (custom_iso | bool)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pacman.conf
|
||||
regexp: ^#ParallelDownloads =
|
||||
line: ParallelDownloads = 20
|
||||
|
||||
- name: Wait for pacman lock to be released
|
||||
when: not (custom_iso | bool)
|
||||
ansible.builtin.wait_for:
|
||||
path: /var/lib/pacman/db.lck
|
||||
state: absent
|
||||
timeout: 120
|
||||
changed_when: false
|
||||
|
||||
- name: Setup Pacman
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
- item.os is not defined or os in item.os
|
||||
community.general.pacman:
|
||||
update_cache: true
|
||||
force: true
|
||||
name: "{{ item.name }}"
|
||||
state: latest
|
||||
loop:
|
||||
- { name: glibc }
|
||||
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
|
||||
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
|
||||
- { name: debian-archive-keyring, os: [debian] }
|
||||
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
retries: 4
|
||||
delay: 15
|
||||
|
||||
- name: Prepare /iso mount and repository for RHEL-based systems
|
||||
when: os == "rhel"
|
||||
block:
|
||||
- name: Create /iso directory
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/install/redhat/dvd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Detect RHEL ISO device
|
||||
ansible.builtin.command: lsblk -rno NAME,TYPE
|
||||
register: environment_lsblk_result
|
||||
changed_when: false
|
||||
|
||||
- name: Select RHEL ISO device
|
||||
vars:
|
||||
_rom_devices: >-
|
||||
{{
|
||||
environment_lsblk_result.stdout_lines
|
||||
| map('split', ' ')
|
||||
| selectattr('1', 'equalto', 'rom')
|
||||
| map('first')
|
||||
| map('regex_replace', '^', '/dev/')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
environment_rhel_iso_device: >-
|
||||
{{
|
||||
_rom_devices[-1]
|
||||
if _rom_devices | length > 1
|
||||
else (_rom_devices[0] | default('/dev/sr1'))
|
||||
}}
|
||||
|
||||
- name: Mount RHEL ISO
|
||||
ansible.posix.mount:
|
||||
src: "{{ environment_rhel_iso_device }}"
|
||||
path: /usr/local/install/redhat/dvd
|
||||
fstype: iso9660
|
||||
opts: "ro,loop"
|
||||
state: mounted
|
||||
|
||||
# Security note: RPM Sequoia signature policy is relaxed to allow
|
||||
# bootstrapping RHEL-family distros from the Arch ISO, where the
|
||||
# host rpm/dnf does not trust target distro GPG keys. Package
|
||||
# integrity is verified by the target system's own rpm after reboot.
|
||||
- name: Relax RPM Sequoia signature policy for RHEL bootstrap
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/rpm/macros
|
||||
content: "%_pkgverify_level none\n"
|
||||
mode: "0644"
|
||||
|
||||
- name: Configure RHEL Repos for installation
|
||||
when: is_rhel | bool
|
||||
block:
|
||||
- name: Create directories for repository files and RPM GPG keys
|
||||
ansible.builtin.file:
|
||||
path: /etc/yum.repos.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Create RHEL repository file
|
||||
ansible.builtin.template:
|
||||
src: "{{ os }}.repo.j2"
|
||||
dest: /etc/yum.repos.d/{{ os }}.repo
|
||||
mode: "0644"
|
||||
|
||||
- name: Check for third-party preparation tasks
|
||||
run_once: true
|
||||
become: false
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_connection: local
|
||||
block:
|
||||
- name: Resolve third-party preparation task path
|
||||
ansible.builtin.set_fact:
|
||||
environment_thirdparty_tasks_path: >-
|
||||
{{
|
||||
thirdparty_tasks
|
||||
if thirdparty_tasks | regex_search('^/')
|
||||
else playbook_dir + '/' + thirdparty_tasks
|
||||
}}
|
||||
changed_when: false
|
||||
|
||||
- name: Stat third-party preparation tasks
|
||||
ansible.builtin.stat:
|
||||
path: "{{ environment_thirdparty_tasks_path }}"
|
||||
register: environment_thirdparty_tasks_stat
|
||||
changed_when: false
|
||||
|
||||
- name: Run third-party preparation tasks
|
||||
ansible.builtin.include_tasks: _thirdparty.yml
|
||||
when:
|
||||
- thirdparty_tasks | length > 0
|
||||
- environment_thirdparty_tasks_stat.stat.exists
|
||||
ansible.builtin.include_tasks: "{{ environment_thirdparty_tasks_path }}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[appstream]
|
||||
[alma-appstream]
|
||||
name=AlmaLinux $releasever - AppStream
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/AppStream/$basearch/os/
|
||||
@@ -9,7 +9,7 @@ gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
enabled_metadata=1
|
||||
|
||||
[baseos]
|
||||
[alma-baseos]
|
||||
name=AlmaLinux $releasever - BaseOS
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/BaseOS/$basearch/os/
|
||||
@@ -20,7 +20,7 @@ gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
enabled_metadata=1
|
||||
|
||||
[extras]
|
||||
[alma-extras]
|
||||
name=AlmaLinux $releasever - Extras
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/extras
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/extras/$basearch/os/
|
||||
@@ -31,7 +31,7 @@ gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
enabled_metadata=0
|
||||
|
||||
[highavailability]
|
||||
[alma-highavailability]
|
||||
name=AlmaLinux $releasever - HighAvailability
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/highavailability
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/HighAvailability/$basearch/os/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[baseos]
|
||||
[rocky-baseos]
|
||||
name=Rocky Linux $releasever - BaseOS
|
||||
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=BaseOS-$releasever
|
||||
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/BaseOS/$basearch/os/
|
||||
@@ -8,14 +8,3 @@ countme=1
|
||||
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
|
||||
metadata_expire=86400
|
||||
enabled_metadata=1
|
||||
|
||||
[appstream]
|
||||
name=Rocky Linux $releasever - AppStream
|
||||
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=AppStream-$releasever
|
||||
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/AppStream/$basearch/os/
|
||||
gpgcheck=1
|
||||
enabled=1
|
||||
countme=1
|
||||
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
|
||||
metadata_expire=86400
|
||||
enabled_metadata=1
|
||||
|
||||
@@ -9,22 +9,6 @@ os_family_debian:
|
||||
- debian
|
||||
- ubuntu
|
||||
- ubuntu-lts
|
||||
|
||||
# OS → family mapping — aligns with the main project's ansible_os_family pattern.
|
||||
# Enables platform_config dict lookups per role instead of inline when: is_rhel chains.
|
||||
os_family_map:
|
||||
almalinux: RedHat
|
||||
alpine: Alpine
|
||||
archlinux: Archlinux
|
||||
debian: Debian
|
||||
fedora: RedHat
|
||||
opensuse: Suse
|
||||
rhel: RedHat
|
||||
rocky: RedHat
|
||||
ubuntu: Debian
|
||||
ubuntu-lts: Debian
|
||||
void: Void
|
||||
|
||||
os_supported:
|
||||
- almalinux
|
||||
- alpine
|
||||
@@ -46,15 +30,13 @@ hypervisor_defaults:
|
||||
url: ""
|
||||
username: ""
|
||||
password: ""
|
||||
node: ""
|
||||
host: ""
|
||||
storage: ""
|
||||
datacenter: ""
|
||||
cluster: ""
|
||||
folder: ""
|
||||
certs: false
|
||||
ssh: false
|
||||
|
||||
physical_default_os: "archlinux"
|
||||
custom_iso: false
|
||||
thirdparty_tasks: "dropins/preparation.yml"
|
||||
|
||||
@@ -62,7 +44,7 @@ system_defaults:
|
||||
type: "virtual" # virtual|physical
|
||||
os: ""
|
||||
version: ""
|
||||
filesystem: "ext4"
|
||||
filesystem: ""
|
||||
name: ""
|
||||
id: ""
|
||||
cpus: 0
|
||||
@@ -82,13 +64,11 @@ system_defaults:
|
||||
timezone: "Europe/Vienna"
|
||||
locale: "en_US.UTF-8"
|
||||
keymap: "us"
|
||||
mirror: ""
|
||||
packages: []
|
||||
disks: []
|
||||
users: {}
|
||||
users: []
|
||||
root:
|
||||
password: ""
|
||||
shell: "/bin/bash"
|
||||
luks:
|
||||
enabled: false
|
||||
passphrase: ""
|
||||
@@ -129,57 +109,8 @@ system_defaults:
|
||||
rhel_repo:
|
||||
source: "iso" # iso|satellite|none — how RHEL systems get packages post-install
|
||||
url: "" # Satellite/custom repo URL when source=satellite
|
||||
aur:
|
||||
enabled: false
|
||||
helper: "yay" # yay|paru
|
||||
user: "_aur_builder"
|
||||
chroot:
|
||||
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn
|
||||
initramfs:
|
||||
generator: "" # auto-detected; override: dracut|mkinitcpio|initramfs-tools
|
||||
desktop:
|
||||
enabled: false
|
||||
environment: "" # gnome|kde|xfce|sway|hyprland|cinnamon|mate|lxqt|budgie
|
||||
display_manager: "" # auto from environment when empty; override: gdm|sddm|lightdm|greetd
|
||||
secure_boot:
|
||||
enabled: false
|
||||
method: "" # arch only: sbctl (default) or uki; ignored for other distros
|
||||
|
||||
# Per-hypervisor required fields — drives data-driven validation.
|
||||
# All virtual types additionally require network bridge or interfaces.
|
||||
hypervisor_required_fields:
|
||||
proxmox:
|
||||
hypervisor: [url, username, password, node, storage]
|
||||
system: [id]
|
||||
vmware:
|
||||
hypervisor: [url, username, password, datacenter, storage]
|
||||
system: []
|
||||
xen:
|
||||
hypervisor: []
|
||||
system: []
|
||||
libvirt:
|
||||
hypervisor: []
|
||||
system: []
|
||||
|
||||
# Hypervisor-to-disk device prefix mapping for virtual machines.
|
||||
# Physical installs must set system.disks[].device explicitly.
|
||||
hypervisor_disk_device_map:
|
||||
libvirt: "/dev/vd"
|
||||
xen: "/dev/xvd"
|
||||
proxmox: "/dev/sd"
|
||||
vmware: "/dev/sd"
|
||||
|
||||
# Mountpoints managed by the partitioning role — forbidden for extra disks.
|
||||
reserved_mounts:
|
||||
- /boot
|
||||
- /boot/efi
|
||||
- /home
|
||||
- /var
|
||||
- /var/log
|
||||
- /var/log/audit
|
||||
|
||||
# Drive letter sequence for disk device naming (max 26 disks).
|
||||
disk_letter_map: "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
system_disk_defaults:
|
||||
size: 0
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
- name: Normalize system disks input
|
||||
vars:
|
||||
system_disks: "{{ system_cfg.disks | default([]) }}"
|
||||
system_disk_letter_map: "{{ disk_letter_map }}"
|
||||
system_disk_letter_map: "abcdefghijklmnopqrstuvwxyz"
|
||||
system_disk_device_prefix: >-
|
||||
{{
|
||||
hypervisor_disk_device_map.get(hypervisor_type, '')
|
||||
{'libvirt': '/dev/vd', 'xen': '/dev/xvd', 'proxmox': '/dev/sd', 'vmware': '/dev/sd'}.get(hypervisor_type, '')
|
||||
if system_cfg.type == 'virtual'
|
||||
else ''
|
||||
}}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- name: Build normalized system configuration
|
||||
vars:
|
||||
system_raw: "{{ system_defaults | combine(system, recursive=True) }}"
|
||||
system_type: "{{ 'virtual' if (system_raw.type | string | lower) in ['vm', 'virtual'] else (system_raw.type | string | lower) }}"
|
||||
system_type: "{{ system_raw.type | string | lower }}"
|
||||
system_os_input: "{{ system_raw.os | default('') | string | lower }}"
|
||||
system_name: >-
|
||||
{{
|
||||
@@ -10,28 +10,17 @@
|
||||
if (system_raw.name | default('') | string | trim | length) > 0
|
||||
else inventory_hostname
|
||||
}}
|
||||
_mirror_defaults:
|
||||
debian: "https://deb.debian.org/debian/"
|
||||
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
||||
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg:
|
||||
# --- Identity & platform ---
|
||||
type: "{{ system_type }}"
|
||||
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
|
||||
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 }}"
|
||||
# --- VM sizing (ignored for physical) ---
|
||||
cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}"
|
||||
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
|
||||
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
|
||||
# --- Network ---
|
||||
# Flat fields (bridge, ip, etc.) and interfaces[] are mutually exclusive.
|
||||
# When interfaces[] is set, flat fields are populated from the first
|
||||
# interface in the "Populate primary network fields" task below.
|
||||
# When only flat fields are set, a synthetic interfaces[] entry is built.
|
||||
network:
|
||||
bridge: "{{ system_raw.network.bridge | default('') | string }}"
|
||||
vlan: "{{ system_raw.network.vlan | default('') | string }}"
|
||||
@@ -67,22 +56,10 @@
|
||||
else []
|
||||
)
|
||||
}}
|
||||
# --- Locale & environment ---
|
||||
timezone: "{{ system_raw.timezone | string }}"
|
||||
locale: "{{ system_raw.locale | string }}"
|
||||
keymap: "{{ system_raw.keymap | string }}"
|
||||
mirror: >-
|
||||
{{
|
||||
system_raw.mirror | string | trim
|
||||
if (system_raw.mirror | default('') | string | trim | length) > 0
|
||||
else _mirror_defaults[system_raw.os | default('') | string | lower] | default('')
|
||||
}}
|
||||
path: >-
|
||||
{{
|
||||
(system_raw.path | default('') | string)
|
||||
if (system_raw.path | default('') | string | length > 0)
|
||||
else (hypervisor_cfg.folder | default('') | string)
|
||||
}}
|
||||
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: >-
|
||||
{{
|
||||
(
|
||||
@@ -94,13 +71,10 @@
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
# --- Storage & accounts ---
|
||||
disks: "{{ system_raw.disks | default([]) }}"
|
||||
users: "{{ system_raw.users | default({}) }}"
|
||||
users: "{{ system_raw.users | default([]) }}"
|
||||
root:
|
||||
password: "{{ system_raw.root.password | string }}"
|
||||
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
|
||||
# --- LUKS disk encryption ---
|
||||
luks:
|
||||
enabled: "{{ system_raw.luks.enabled | bool }}"
|
||||
passphrase: "{{ system_raw.luks.passphrase | string }}"
|
||||
@@ -120,7 +94,6 @@
|
||||
pbkdf: "{{ system_raw.luks.pbkdf | string }}"
|
||||
urandom: "{{ system_raw.luks.urandom | bool }}"
|
||||
verify: "{{ system_raw.luks.verify | bool }}"
|
||||
# --- Feature flags ---
|
||||
features:
|
||||
cis:
|
||||
enabled: "{{ system_raw.features.cis.enabled | bool }}"
|
||||
@@ -144,16 +117,27 @@
|
||||
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
|
||||
chroot:
|
||||
tool: "{{ system_raw.features.chroot.tool | string }}"
|
||||
initramfs:
|
||||
generator: "{{ system_raw.features.initramfs.generator | default('') | string | lower }}"
|
||||
desktop:
|
||||
enabled: "{{ system_raw.features.desktop.enabled | bool }}"
|
||||
environment: "{{ system_raw.features.desktop.environment | default('') | string | lower }}"
|
||||
display_manager: "{{ system_raw.features.desktop.display_manager | default('') | string | lower }}"
|
||||
secure_boot:
|
||||
enabled: "{{ system_raw.features.secure_boot.enabled | bool }}"
|
||||
method: "{{ system_raw.features.secure_boot.method | default('') | string | lower }}"
|
||||
hostname: "{{ system_name }}"
|
||||
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
|
||||
os: "{{ 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)
|
||||
}}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
that:
|
||||
- system is mapping
|
||||
- system.network is not defined or system.network is mapping
|
||||
- system.users is not defined or system.users is mapping
|
||||
- system.users is not defined or (system.users is iterable and system.users is not string and system.users is not mapping)
|
||||
- system.root is not defined or system.root is mapping
|
||||
- system.luks is not defined or system.luks is mapping
|
||||
- system.features is not defined or system.features is mapping
|
||||
fail_msg: "system and its nested keys (network, root, luks, features, users) must be dictionaries."
|
||||
fail_msg: "system and its nested keys (network, root, luks, features) must be dictionaries; system.users must be a list."
|
||||
quiet: true
|
||||
|
||||
- name: Validate DNS lists (not strings)
|
||||
@@ -25,17 +25,17 @@
|
||||
quiet: true
|
||||
|
||||
- name: Validate system.users entries
|
||||
when: system.users is defined and system.users is mapping and system.users | length > 0
|
||||
when: system.users is defined and system.users | length > 0
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.key | string | length > 0
|
||||
- item.value['keys'] is not defined or (item.value['keys'] is iterable and item.value['keys'] is not string)
|
||||
fail_msg: "Each system.users entry must be a dict keyed by username; 'keys' must be a list."
|
||||
- item is mapping
|
||||
- item.name is defined and (item.name | string | length) > 0
|
||||
- item['keys'] is not defined or (item['keys'] is iterable and item['keys'] is not string)
|
||||
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
|
||||
quiet: true
|
||||
loop: "{{ system.users | dict2items }}"
|
||||
loop: "{{ system.users }}"
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
label: "{{ item.name | default('(unnamed)') }}"
|
||||
|
||||
- name: Validate system features input types
|
||||
when: system.features is defined
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
---
|
||||
- name: Ensure hypervisor input is a dictionary
|
||||
ansible.builtin.set_fact:
|
||||
hypervisor: "{{ hypervisor | default({}) }}"
|
||||
|
||||
- name: Validate hypervisor input
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hypervisor is mapping
|
||||
- hypervisor.type is defined
|
||||
- hypervisor.type | string | length > 0
|
||||
fail_msg: "hypervisor must be a dictionary and hypervisor.type must be set (e.g. libvirt|proxmox|vmware|xen|none)."
|
||||
quiet: true
|
||||
|
||||
- name: Normalize hypervisor configuration
|
||||
when: hypervisor_cfg is not defined
|
||||
block:
|
||||
- name: Ensure hypervisor input is a dictionary
|
||||
ansible.builtin.set_fact:
|
||||
hypervisor: "{{ hypervisor | default({}) }}"
|
||||
|
||||
- name: Validate hypervisor input
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hypervisor is mapping
|
||||
- hypervisor.type is defined
|
||||
- hypervisor.type | string | length > 0
|
||||
fail_msg: "hypervisor must be a dictionary and hypervisor.type must be set (e.g. libvirt|proxmox|vmware|xen|none)."
|
||||
quiet: true
|
||||
|
||||
- name: Merge hypervisor defaults with input
|
||||
vars:
|
||||
merged: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}"
|
||||
ansible.builtin.set_fact:
|
||||
hypervisor_cfg: "{{ merged }}"
|
||||
hypervisor_type: "{{ merged.type | string | lower }}"
|
||||
no_log: true
|
||||
vars:
|
||||
merged: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}"
|
||||
ansible.builtin.set_fact:
|
||||
hypervisor_cfg: "{{ merged }}"
|
||||
hypervisor_type: "{{ merged.type | string | lower }}"
|
||||
no_log: true
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
# Centralized normalization — all input dicts (system, hypervisor, disks)
|
||||
# are normalized here into system_cfg, hypervisor_cfg, etc.
|
||||
# Downstream roles consume these computed facts directly and do NOT need
|
||||
# per-role _normalize.yml (except CIS, which has its own input dict).
|
||||
- name: Global defaults loaded
|
||||
ansible.builtin.debug:
|
||||
msg: Global defaults loaded.
|
||||
@@ -10,41 +6,9 @@
|
||||
- name: Normalize hypervisor inputs
|
||||
ansible.builtin.include_tasks: hypervisor.yml
|
||||
|
||||
- name: Set VMware module auth defaults
|
||||
when: hypervisor_type == 'vmware'
|
||||
ansible.builtin.set_fact:
|
||||
_vmware_auth:
|
||||
hostname: "{{ hypervisor_cfg.url }}"
|
||||
username: "{{ hypervisor_cfg.username }}"
|
||||
password: "{{ hypervisor_cfg.password }}"
|
||||
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
|
||||
datacenter: "{{ hypervisor_cfg.datacenter }}"
|
||||
no_log: true
|
||||
|
||||
- name: Set Proxmox module auth defaults
|
||||
when: hypervisor_type == 'proxmox'
|
||||
ansible.builtin.set_fact:
|
||||
_proxmox_auth:
|
||||
api_host: "{{ hypervisor_cfg.url }}"
|
||||
api_user: "{{ hypervisor_cfg.username }}"
|
||||
api_password: "{{ hypervisor_cfg.password }}"
|
||||
_proxmox_auth_node:
|
||||
api_host: "{{ hypervisor_cfg.url }}"
|
||||
api_user: "{{ hypervisor_cfg.username }}"
|
||||
api_password: "{{ hypervisor_cfg.password }}"
|
||||
node: "{{ hypervisor_cfg.node }}"
|
||||
no_log: true
|
||||
|
||||
- name: Normalize system inputs
|
||||
ansible.builtin.include_tasks: system.yml
|
||||
|
||||
- name: Inherit folder from hypervisor when system path is empty
|
||||
when:
|
||||
- system_cfg.path | default('') | string | length == 0
|
||||
- hypervisor_cfg.folder | default('') | string | length > 0
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: "{{ system_cfg | combine({'path': hypervisor_cfg.folder | string}, recursive=True) }}"
|
||||
|
||||
- name: Validate variables
|
||||
ansible.builtin.include_tasks: validation.yml
|
||||
|
||||
@@ -52,7 +16,6 @@
|
||||
ansible.builtin.set_fact:
|
||||
is_rhel: "{{ os in os_family_rhel }}"
|
||||
is_debian: "{{ os in os_family_debian }}"
|
||||
os_family: "{{ os_family_map[os] | default('Unknown') }}"
|
||||
|
||||
- name: Normalize OS version for keying
|
||||
when:
|
||||
@@ -81,14 +44,10 @@
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
- hypervisor_type != "vmware"
|
||||
vars:
|
||||
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||
ansible.builtin.set_fact:
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: "{{ _primary.key }}"
|
||||
ansible_password: "{{ _primary.value.password }}"
|
||||
ansible_become_password: "{{ _primary.value.password }}"
|
||||
ansible_user: "{{ system_cfg.users[0].name }}"
|
||||
ansible_password: "{{ system_cfg.users[0].password }}"
|
||||
ansible_become_password: "{{ system_cfg.users[0].password }}"
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
no_log: true
|
||||
|
||||
@@ -96,12 +55,3 @@
|
||||
when: hypervisor_type == "vmware"
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: vmware_tools
|
||||
ansible_host: "{{ hypervisor_cfg.url }}"
|
||||
ansible_port: 443
|
||||
ansible_user: root
|
||||
ansible_password: ""
|
||||
ansible_vmware_user: "{{ hypervisor_cfg.username }}"
|
||||
ansible_vmware_password: "{{ hypervisor_cfg.password }}"
|
||||
ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}"
|
||||
ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}"
|
||||
no_log: true
|
||||
|
||||
@@ -1,107 +1,9 @@
|
||||
---
|
||||
# Two code paths:
|
||||
# 1. Fresh run (system_cfg undefined): normalize from raw `system` input.
|
||||
# 2. Pre-computed (system_cfg already set, e.g. from main project's deploy_iac):
|
||||
# merge with bootstrap system_defaults to fill missing fields (luks, features,
|
||||
# etc.) that bootstrap expects but the main project doesn't set, then derive
|
||||
# convenience facts (hostname, os, os_version).
|
||||
- name: Normalize system and disk configuration
|
||||
when: system_cfg is not defined
|
||||
block:
|
||||
- name: Validate raw system input types
|
||||
ansible.builtin.include_tasks: _validate_input.yml
|
||||
- name: Validate raw system input types
|
||||
ansible.builtin.include_tasks: _validate_input.yml
|
||||
|
||||
- name: Normalize system configuration
|
||||
ansible.builtin.include_tasks: _normalize_system.yml
|
||||
- name: Normalize system configuration
|
||||
ansible.builtin.include_tasks: _normalize_system.yml
|
||||
|
||||
- name: Normalize disk configuration
|
||||
ansible.builtin.include_tasks: _normalize_disks.yml
|
||||
|
||||
- name: Populate primary network fields from first interface
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- system_cfg.network.interfaces | default([]) | length > 0
|
||||
- system_cfg.network.ip | default('') | string | length == 0
|
||||
vars:
|
||||
_primary: "{{ system_cfg.network.interfaces[0] }}"
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: >-
|
||||
{{
|
||||
system_cfg | combine({
|
||||
'network': system_cfg.network | combine({
|
||||
'bridge': _primary.bridge | default(''),
|
||||
'vlan': _primary.vlan | default(''),
|
||||
'ip': _primary.ip | default(''),
|
||||
'prefix': _primary.prefix | default(''),
|
||||
'gateway': _primary.gateway | default('')
|
||||
})
|
||||
}, recursive=True)
|
||||
}}
|
||||
|
||||
- name: Check if pre-computed system_cfg needs enrichment
|
||||
when: system_cfg is defined
|
||||
ansible.builtin.set_fact:
|
||||
_bootstrap_needs_enrichment: "{{ hostname is not defined }}"
|
||||
|
||||
- name: Merge pre-computed system_cfg with bootstrap system_defaults
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- _bootstrap_needs_enrichment | default(false) | bool
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: "{{ system_defaults | combine(system | default({}), recursive=True) | combine(system_cfg, recursive=True) }}"
|
||||
|
||||
- name: Apply mirror default for pre-computed system_cfg
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- _bootstrap_needs_enrichment | default(false) | bool
|
||||
- system_cfg.mirror | default('') | string | trim | length == 0
|
||||
vars:
|
||||
# Same as _normalize_system.yml — kept in sync manually.
|
||||
_mirror_defaults:
|
||||
debian: "https://deb.debian.org/debian/"
|
||||
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
||||
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: >-
|
||||
{{
|
||||
system_cfg | combine({
|
||||
'mirror': _mirror_defaults[system_cfg.os | default('') | string | lower] | default('')
|
||||
}, recursive=True)
|
||||
}}
|
||||
|
||||
- name: Populate primary network fields from first interface (pre-computed)
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- _bootstrap_needs_enrichment | default(false) | bool
|
||||
- system_cfg.network.interfaces | default([]) | length > 0
|
||||
- system_cfg.network.bridge | default('') | string | length == 0
|
||||
vars:
|
||||
_primary: "{{ system_cfg.network.interfaces[0] }}"
|
||||
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: Derive convenience facts from pre-computed system_cfg
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- _bootstrap_needs_enrichment | default(false) | bool
|
||||
ansible.builtin.set_fact:
|
||||
hostname: "{{ system_cfg.name | default(inventory_hostname) }}"
|
||||
os: "{{ system_cfg.os | default('') }}"
|
||||
os_version: "{{ system_cfg.version | default('') | string }}"
|
||||
|
||||
- name: Normalize disk configuration (pre-computed system_cfg)
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- install_drive is not defined
|
||||
- name: Normalize disk configuration
|
||||
ansible.builtin.include_tasks: _normalize_disks.yml
|
||||
|
||||
@@ -123,22 +123,15 @@
|
||||
or (
|
||||
os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"]
|
||||
) or (
|
||||
os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 43
|
||||
os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 45
|
||||
) or (
|
||||
os in ["rocky", "almalinux"]
|
||||
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
|
||||
) or (
|
||||
os == "rhel"
|
||||
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
|
||||
) or (
|
||||
os == "ubuntu"
|
||||
and (os_version | string) is match("^(2[0-9])\\.04$")
|
||||
) or (
|
||||
os == "ubuntu-lts"
|
||||
and (os_version | string) is match("^(2[0-9])\\.04$")
|
||||
) or (
|
||||
os in ["ubuntu", "ubuntu-lts"]
|
||||
and (os_version | default('') | string | length) == 0
|
||||
) or (
|
||||
os in ["alpine", "archlinux", "opensuse", "void"]
|
||||
)
|
||||
@@ -152,60 +145,70 @@
|
||||
fail_msg: "rhel_iso is required when os=rhel."
|
||||
quiet: true
|
||||
|
||||
- name: Validate hypervisor-specific required fields
|
||||
- name: Validate Proxmox hypervisor inputs
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
- hypervisor_type in hypervisor_required_fields
|
||||
- hypervisor_type == "proxmox"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (hypervisor_cfg[item] | default('') | string | length) > 0
|
||||
fail_msg: "Missing required {{ hypervisor_type }} field: hypervisor.{{ item }}"
|
||||
- hypervisor_cfg.url | string | length > 0
|
||||
- hypervisor_cfg.username | string | length > 0
|
||||
- hypervisor_cfg.password | string | length > 0
|
||||
- hypervisor_cfg.host | string | length > 0
|
||||
- hypervisor_cfg.storage | string | length > 0
|
||||
- system_cfg.id | string | length > 0
|
||||
- >-
|
||||
(system_cfg.network.bridge | default('') | string | length > 0)
|
||||
or (system_cfg.network.interfaces | default([]) | length > 0)
|
||||
fail_msg: >-
|
||||
Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage),
|
||||
system.id, and system.network.bridge (or system.network.interfaces[]).
|
||||
quiet: true
|
||||
loop: "{{ hypervisor_required_fields[hypervisor_type].hypervisor | default([]) }}"
|
||||
loop_control:
|
||||
label: "hypervisor.{{ item }}"
|
||||
no_log: true
|
||||
|
||||
- name: Validate VMware placement (cluster or node required, mutually exclusive)
|
||||
- name: Validate VMware hypervisor inputs
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
- hypervisor_type == "vmware"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hypervisor_cfg.url | string | length > 0
|
||||
- hypervisor_cfg.username | string | length > 0
|
||||
- hypervisor_cfg.password | string | length > 0
|
||||
- hypervisor_cfg.datacenter | string | length > 0
|
||||
- hypervisor_cfg.cluster | string | length > 0
|
||||
- hypervisor_cfg.storage | string | length > 0
|
||||
- >-
|
||||
(hypervisor_cfg.cluster | default('') | string | length > 0)
|
||||
or (hypervisor_cfg.node | default('') | string | length > 0)
|
||||
- >-
|
||||
(hypervisor_cfg.cluster | default('') | string | length == 0)
|
||||
or (hypervisor_cfg.node | default('') | string | length == 0)
|
||||
(system_cfg.network.bridge | default('') | string | length > 0)
|
||||
or (system_cfg.network.interfaces | default([]) | length > 0)
|
||||
fail_msg: >-
|
||||
VMware requires either hypervisor.cluster or hypervisor.node (mutually exclusive).
|
||||
cluster targets a vSphere cluster; node targets a specific ESXi host.
|
||||
Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage)
|
||||
and system.network.bridge (or system.network.interfaces[]).
|
||||
quiet: true
|
||||
no_log: true
|
||||
|
||||
- name: Validate hypervisor-specific required system fields
|
||||
- name: Validate Xen hypervisor inputs
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
- hypervisor_type in hypervisor_required_fields
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (system_cfg[item] | default('') | string | length) > 0
|
||||
fail_msg: "Missing required {{ hypervisor_type }} field: system.{{ item }}"
|
||||
quiet: true
|
||||
loop: "{{ hypervisor_required_fields[hypervisor_type].system | default([]) }}"
|
||||
loop_control:
|
||||
label: "system.{{ item }}"
|
||||
|
||||
- name: Validate virtual machine network requirement
|
||||
when: system_cfg.type == "virtual"
|
||||
- hypervisor_type == "xen"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(system_cfg.network.bridge | default('') | string | length > 0)
|
||||
or (system_cfg.network.interfaces | default([]) | length > 0)
|
||||
fail_msg: >-
|
||||
Missing required {{ hypervisor_type }} network configuration.
|
||||
Define system.network.bridge (or system.network.interfaces[]).
|
||||
fail_msg: "Missing required Xen inputs. Define system.network.bridge (or system.network.interfaces[])."
|
||||
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
|
||||
@@ -223,7 +226,6 @@
|
||||
- system_cfg.features.firewall.toolkit is defined
|
||||
- system_cfg.features.firewall.toolkit in ["iptables", "nftables"]
|
||||
- system_cfg.features.firewall.enabled is defined
|
||||
- system_cfg.features.ssh.enabled is defined
|
||||
- system_cfg.features.banner.motd is defined
|
||||
- system_cfg.features.banner.sudo is defined
|
||||
- system_cfg.luks.enabled is defined
|
||||
@@ -241,8 +243,6 @@
|
||||
- system_cfg.disks is defined and (system_cfg.disks | length) > 0
|
||||
- (system_cfg.disks[0].size | float) > 0
|
||||
- (system_cfg.disks[0].size | float) >= 20
|
||||
# Btrfs minimum disk: swap_size + 5.5 GiB overhead (subvolumes + metadata).
|
||||
# Swap sizing: memory < 16 GiB → max(memory_GiB, 2); memory >= 16 GiB → memory/2.
|
||||
- >-
|
||||
system_cfg.filesystem != "btrfs"
|
||||
or (
|
||||
@@ -252,7 +252,7 @@
|
||||
(system_cfg.memory | float / 1024 >= 16.0)
|
||||
| ternary(
|
||||
(system_cfg.memory | float / 2048),
|
||||
[system_cfg.memory | float / 1024, 2.0] | max
|
||||
[system_cfg.memory | float / 1024, 4.0] | max
|
||||
)
|
||||
)
|
||||
+ 5.5
|
||||
@@ -261,16 +261,13 @@
|
||||
fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size."
|
||||
quiet: true
|
||||
|
||||
- name: Validate at least one user with a password is defined
|
||||
vars:
|
||||
_pw_users: "{{ system_cfg.users | dict2items | selectattr('value.password', 'defined') | list }}"
|
||||
- name: Validate at least one user is defined
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.users | default({}) | length > 0
|
||||
- _pw_users | length > 0
|
||||
- _pw_users[0].key | string | length > 0
|
||||
- _pw_users[0].value.password | string | length > 0
|
||||
fail_msg: "At least one user with a password must be defined in system.users."
|
||||
- system_cfg.users | default([]) | length > 0
|
||||
- system_cfg.users[0].name is defined and (system_cfg.users[0].name | string | length) > 0
|
||||
- system_cfg.users[0].password is defined and (system_cfg.users[0].password | string | length) > 0
|
||||
fail_msg: "At least one user with a name and password must be defined in system.users[]."
|
||||
quiet: true
|
||||
no_log: true
|
||||
|
||||
@@ -313,8 +310,8 @@
|
||||
system_disk_mounts: >-
|
||||
{{
|
||||
(system_cfg.disks | default([]))
|
||||
| map(attribute='mount', default={})
|
||||
| map(attribute='path', default='')
|
||||
| map(attribute='mount')
|
||||
| map(attribute='path')
|
||||
| map('string')
|
||||
| map('trim')
|
||||
| reject('equalto', '')
|
||||
@@ -329,11 +326,15 @@
|
||||
- name: Validate disk mount definitions
|
||||
when: system_cfg.disks is defined
|
||||
vars:
|
||||
all_reserved_mounts: >-
|
||||
{{
|
||||
reserved_mounts
|
||||
+ (['/var/cache/pacman/pkg'] if os == 'archlinux' else [])
|
||||
}}
|
||||
reserved_mounts:
|
||||
- /boot
|
||||
- /boot/efi
|
||||
- /home
|
||||
- /swap
|
||||
- /var
|
||||
- /var/cache/pacman/pkg
|
||||
- /var/log
|
||||
- /var/log/audit
|
||||
disk_mount: "{{ (item.mount.path | default('') | string) | trim }}"
|
||||
disk_fstype: "{{ (item.mount.fstype | default('') | string) | trim }}"
|
||||
disk_device: "{{ (item.device | default('') | string) | trim }}"
|
||||
@@ -342,7 +343,7 @@
|
||||
that:
|
||||
- disk_mount == "" or disk_mount.startswith("/")
|
||||
- disk_mount == "" or disk_mount != "/"
|
||||
- disk_mount == "" or disk_mount not in all_reserved_mounts
|
||||
- disk_mount == "" or disk_mount not in reserved_mounts
|
||||
- disk_mount == "" or disk_fstype in ["btrfs", "ext4", "xfs"]
|
||||
- disk_mount == "" or system_cfg.type == "virtual" or (disk_device | length) > 0
|
||||
- disk_mount == "" or system_cfg.type != "virtual" or (disk_size | float) > 0
|
||||
@@ -358,8 +359,7 @@
|
||||
that:
|
||||
- system_cfg.network.prefix is defined
|
||||
- (system_cfg.network.prefix | int) > 0
|
||||
- (system_cfg.network.prefix | int) <= 32
|
||||
fail_msg: "system.network.prefix must be between 1 and 32 when system.network.ip is set."
|
||||
fail_msg: "system.network.prefix is required when system.network.ip is set."
|
||||
quiet: true
|
||||
|
||||
- name: Validate network interfaces entries
|
||||
@@ -368,48 +368,8 @@
|
||||
that:
|
||||
- item is mapping
|
||||
- item.bridge is defined and (item.bridge | string | length) > 0
|
||||
- >-
|
||||
(item.ip | default('') | string | length) == 0
|
||||
or (item.prefix | default('') | string | length) > 0
|
||||
fail_msg: "Each system.network.interfaces[] entry must have a 'bridge' key and 'prefix' when 'ip' is set."
|
||||
fail_msg: "Each system.network.interfaces[] entry must be a dict with at least a 'bridge' key."
|
||||
quiet: true
|
||||
loop: "{{ system_cfg.network.interfaces }}"
|
||||
loop_control:
|
||||
label: "{{ item | to_json }}"
|
||||
|
||||
- name: Validate hostname format
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hostname is regex("^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$")
|
||||
fail_msg: "hostname '{{ hostname }}' contains invalid characters. Use only alphanumeric, hyphens, dots, and underscores."
|
||||
quiet: true
|
||||
|
||||
- name: Validate IP address format
|
||||
when: system_cfg.network.ip is defined and (system_cfg.network.ip | string | length) > 0
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.network.ip is regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}$")
|
||||
fail_msg: "system.network.ip '{{ system_cfg.network.ip }}' is not a valid IPv4 address."
|
||||
quiet: true
|
||||
|
||||
- name: Validate DNS server format
|
||||
when:
|
||||
- system_cfg.network.dns.servers is defined
|
||||
- system_cfg.network.dns.servers | length > 0
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item is regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}$")
|
||||
fail_msg: "DNS server '{{ item }}' is not a valid IPv4 address."
|
||||
quiet: true
|
||||
loop: "{{ system_cfg.network.dns.servers }}"
|
||||
|
||||
- name: Validate LUKS method
|
||||
when: system_cfg.luks.enabled | bool
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.luks.method in ["tpm2", "keyfile"]
|
||||
- >-
|
||||
(system_cfg.luks.passphrase | string | length) > 0
|
||||
fail_msg: "system.luks.method must be 'tpm2' or 'keyfile', and luks.passphrase must be set when LUKS is enabled."
|
||||
quiet: true
|
||||
no_log: true
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
---
|
||||
partitioning_btrfs_compress_opt: "{{ 'compress=zstd:15' if system_cfg.features.zstd.enabled | bool else '' }}"
|
||||
# Partition separator: 'p' for NVMe/mmcblk (device path ends in digit), empty for SCSI/virtio.
|
||||
# Examples: /dev/sda → /dev/sda1, /dev/nvme0n1 → /dev/nvme0n1p1
|
||||
partitioning_part_sep: "{{ 'p' if (install_drive | default('') | regex_search('\\d$')) else '' }}"
|
||||
partitioning_boot_partition_suffix: 1
|
||||
partitioning_main_partition_suffix: 2
|
||||
partitioning_efi_size_mib: 512
|
||||
@@ -116,12 +113,12 @@ partitioning_grub_enable_cryptodisk: >-
|
||||
and not (partitioning_separate_boot | bool)
|
||||
and (partitioning_efi_mountpoint == '/boot/efi')
|
||||
}}
|
||||
partitioning_luks_device: "{{ install_drive ~ partitioning_part_sep ~ (partitioning_root_partition_suffix | string) }}"
|
||||
partitioning_luks_device: "{{ install_drive ~ (partitioning_root_partition_suffix | string) }}"
|
||||
partitioning_root_device: >-
|
||||
{{
|
||||
'/dev/mapper/' + system_cfg.luks.mapper
|
||||
if (system_cfg.luks.enabled | bool)
|
||||
else install_drive ~ partitioning_part_sep ~ (partitioning_root_partition_suffix | string)
|
||||
else install_drive ~ (partitioning_root_partition_suffix | string)
|
||||
}}
|
||||
partitioning_disk_size_gb: >-
|
||||
{{
|
||||
@@ -155,6 +152,6 @@ partitioning_swap_size_gb: >-
|
||||
((partitioning_memory_mb / 1024) >= 16.0)
|
||||
| ternary(
|
||||
(partitioning_memory_mb / 2048) | int,
|
||||
[partitioning_memory_mb / 1024, 2.0] | max | int
|
||||
[partitioning_memory_mb / 1024, 4.0] | max | int
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
---
|
||||
- name: Create filesystems
|
||||
block:
|
||||
- name: Create FAT32 filesystem in boot partition
|
||||
community.general.filesystem:
|
||||
dev: "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_partition_suffix }}"
|
||||
fstype: vfat
|
||||
opts: -F32 -n BOOT
|
||||
force: true
|
||||
|
||||
- name: Create filesystem for /boot partition
|
||||
when: partitioning_separate_boot | bool
|
||||
community.general.filesystem:
|
||||
dev: "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
|
||||
fstype: "{{ partitioning_boot_fs_fstype }}"
|
||||
opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if (is_rhel | bool and partitioning_boot_fs_fstype == 'xfs') else omit }}"
|
||||
force: true
|
||||
|
||||
- name: Remove unsupported ext4 features from /boot
|
||||
when:
|
||||
- partitioning_separate_boot | bool
|
||||
- partitioning_boot_fs_fstype == 'ext4'
|
||||
- os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11')
|
||||
ansible.builtin.command: >-
|
||||
tune2fs -O "^orphan_file,^metadata_csum_seed"
|
||||
"{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Create swap filesystem
|
||||
when:
|
||||
- system_cfg.filesystem != 'btrfs'
|
||||
- system_cfg.features.swap.enabled | bool
|
||||
community.general.filesystem:
|
||||
fstype: swap
|
||||
dev: /dev/{{ partitioning_vg_name }}/swap
|
||||
|
||||
- name: Create filesystem
|
||||
ansible.builtin.include_tasks: "{{ system_cfg.filesystem }}.yml"
|
||||
|
||||
- name: Get UUID for boot filesystem
|
||||
ansible.builtin.command: blkid -s UUID -o value '{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_partition_suffix }}'
|
||||
register: partitioning_boot_uuid
|
||||
changed_when: false
|
||||
failed_when: partitioning_boot_uuid.rc != 0 or (partitioning_boot_uuid.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for /boot filesystem
|
||||
when: partitioning_separate_boot | bool
|
||||
ansible.builtin.command: >-
|
||||
blkid -s UUID -o value '{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}'
|
||||
register: partitioning_boot_fs_uuid
|
||||
changed_when: false
|
||||
failed_when: partitioning_boot_fs_uuid.rc != 0 or (partitioning_boot_fs_uuid.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for main filesystem
|
||||
ansible.builtin.command: blkid -s UUID -o value '{{ partitioning_root_device }}'
|
||||
register: partitioning_main_uuid
|
||||
changed_when: false
|
||||
failed_when: partitioning_main_uuid.rc != 0 or (partitioning_main_uuid.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for LVM root filesystem
|
||||
when: system_cfg.filesystem != 'btrfs'
|
||||
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/root
|
||||
register: partitioning_uuid_root_result
|
||||
changed_when: false
|
||||
failed_when: partitioning_uuid_root_result.rc != 0 or (partitioning_uuid_root_result.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for LVM swap filesystem
|
||||
when:
|
||||
- system_cfg.filesystem != 'btrfs'
|
||||
- system_cfg.features.swap.enabled | bool
|
||||
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/swap
|
||||
register: partitioning_uuid_swap_result
|
||||
changed_when: false
|
||||
failed_when: partitioning_uuid_swap_result.rc != 0 or (partitioning_uuid_swap_result.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for LVM home filesystem
|
||||
when:
|
||||
- system_cfg.filesystem != 'btrfs'
|
||||
- system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/home
|
||||
register: partitioning_uuid_home_result
|
||||
changed_when: false
|
||||
failed_when: partitioning_uuid_home_result.rc != 0 or (partitioning_uuid_home_result.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for LVM var filesystem
|
||||
when:
|
||||
- system_cfg.filesystem != 'btrfs'
|
||||
- system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var
|
||||
register: partitioning_uuid_var_result
|
||||
changed_when: false
|
||||
failed_when: partitioning_uuid_var_result.rc != 0 or (partitioning_uuid_var_result.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for LVM var_log filesystem
|
||||
when:
|
||||
- system_cfg.filesystem != 'btrfs'
|
||||
- system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var_log
|
||||
register: partitioning_uuid_var_log_result
|
||||
changed_when: false
|
||||
failed_when: partitioning_uuid_var_log_result.rc != 0 or (partitioning_uuid_var_log_result.stdout | trim | length) == 0
|
||||
|
||||
- name: Get UUID for LVM var_log_audit filesystem
|
||||
when:
|
||||
- system_cfg.filesystem != 'btrfs'
|
||||
- system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var_log_audit
|
||||
register: partitioning_uuid_var_log_audit_result
|
||||
changed_when: false
|
||||
failed_when: partitioning_uuid_var_log_audit_result.rc != 0 or (partitioning_uuid_var_log_audit_result.stdout | trim | length) == 0
|
||||
|
||||
- name: Assign UUIDs to Variables
|
||||
when: system_cfg.filesystem != 'btrfs'
|
||||
ansible.builtin.set_fact:
|
||||
partitioning_uuid_root: "{{ partitioning_uuid_root_result.stdout_lines | default([]) }}"
|
||||
partitioning_uuid_swap: >-
|
||||
{{
|
||||
partitioning_uuid_swap_result.stdout_lines | default([])
|
||||
if system_cfg.features.swap.enabled | bool
|
||||
else []
|
||||
}}
|
||||
partitioning_uuid_home: >-
|
||||
{{
|
||||
partitioning_uuid_home_result.stdout_lines | default([])
|
||||
if system_cfg.features.cis.enabled | bool
|
||||
else []
|
||||
}}
|
||||
partitioning_uuid_var: >-
|
||||
{{
|
||||
partitioning_uuid_var_result.stdout_lines | default([])
|
||||
if system_cfg.features.cis.enabled | bool
|
||||
else []
|
||||
}}
|
||||
partitioning_uuid_var_log: >-
|
||||
{{
|
||||
partitioning_uuid_var_log_result.stdout_lines | default([])
|
||||
if system_cfg.features.cis.enabled | bool
|
||||
else []
|
||||
}}
|
||||
partitioning_uuid_var_log_audit: >-
|
||||
{{
|
||||
partitioning_uuid_var_log_audit_result.stdout_lines | default([])
|
||||
if system_cfg.features.cis.enabled | bool
|
||||
else []
|
||||
}}
|
||||
@@ -1,192 +0,0 @@
|
||||
---
|
||||
# LVM Sizing Algorithm
|
||||
# ====================
|
||||
# Sizes are computed from disk_size_gb, memory_mb, and feature flags.
|
||||
#
|
||||
# Swap sizing:
|
||||
# - RAM >= 16 GB → swap = RAM/2 (in GB)
|
||||
# - RAM < 16 GB → swap = max(RAM_GB, 2)
|
||||
# - Capped to: min(target, 4 + max(disk - overhead, 0))
|
||||
# - Further capped to: max available after subtracting reserved + CIS + extent reserve + 4 GB buffer
|
||||
#
|
||||
# Root sizing:
|
||||
# - Full-disk mode (default): disk - reserved - swap - extent_reserve - (CIS volumes if enabled)
|
||||
# - Partial mode: tiered — <4 GB available → 4 GB, 4-12 GB → all available, >12 GB → 40% of disk
|
||||
#
|
||||
# CIS volumes (only when CIS enabled):
|
||||
# - /home: max(min(home_raw, home_max), home_min) where home_raw = (disk - overhead) * 10%
|
||||
# - /var: 2 GB, /var/log: 2 GB, /var/log/audit: 1.5 GB
|
||||
#
|
||||
# Extent reserve: 10 extents * 4 MiB = ~0.04 GB (prevents VG overflow)
|
||||
|
||||
- name: Create LVM logical volumes
|
||||
when: system_cfg.filesystem != 'btrfs'
|
||||
block:
|
||||
- name: Create LVM volume group
|
||||
community.general.lvg:
|
||||
vg: "{{ partitioning_vg_name }}"
|
||||
pvs: "{{ partitioning_root_device }}"
|
||||
|
||||
- name: Create LVM logical volumes
|
||||
when:
|
||||
- system_cfg.features.cis.enabled | bool or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']
|
||||
- system_cfg.features.swap.enabled | bool or item.lv != 'swap'
|
||||
vars:
|
||||
partitioning_lvm_extent_reserve_count: 10
|
||||
partitioning_lvm_extent_size_mib: 4
|
||||
partitioning_lvm_extent_reserve_gb: >-
|
||||
{{
|
||||
(
|
||||
(partitioning_lvm_extent_reserve_count | float)
|
||||
* (partitioning_lvm_extent_size_mib | float)
|
||||
/ 1024
|
||||
) | round(2, 'ceil')
|
||||
}}
|
||||
partitioning_lvm_swap_target_gb: >-
|
||||
{{
|
||||
(
|
||||
((partitioning_memory_mb | float / 1024) >= 16.0)
|
||||
| ternary(
|
||||
(partitioning_memory_mb | float / 2048),
|
||||
[(partitioning_memory_mb | float / 1024), 2] | max | float
|
||||
)
|
||||
)
|
||||
if system_cfg.features.swap.enabled | bool
|
||||
else 0
|
||||
}}
|
||||
partitioning_lvm_swap_cap_gb: >-
|
||||
{{
|
||||
(
|
||||
4
|
||||
+ [
|
||||
(partitioning_disk_size_gb | float) - (partitioning_disk_overhead_gb | float),
|
||||
0
|
||||
] | max
|
||||
)
|
||||
if system_cfg.features.swap.enabled | bool
|
||||
else 0
|
||||
}}
|
||||
partitioning_lvm_swap_target_limited_gb: >-
|
||||
{{
|
||||
(
|
||||
[
|
||||
partitioning_lvm_swap_target_gb,
|
||||
partitioning_lvm_swap_cap_gb
|
||||
] | min
|
||||
)
|
||||
if system_cfg.features.swap.enabled | bool
|
||||
else 0
|
||||
}}
|
||||
partitioning_lvm_swap_max_gb: >-
|
||||
{{
|
||||
(
|
||||
[
|
||||
(
|
||||
(partitioning_disk_size_gb | float)
|
||||
- (partitioning_reserved_gb | float)
|
||||
- (system_cfg.features.cis.enabled | bool | ternary(partitioning_cis_reserved_gb | float, 0))
|
||||
- partitioning_lvm_extent_reserve_gb
|
||||
- 4
|
||||
),
|
||||
0
|
||||
] | max
|
||||
)
|
||||
if system_cfg.features.swap.enabled | bool
|
||||
else 0
|
||||
}}
|
||||
partitioning_lvm_available_gb: >-
|
||||
{{
|
||||
(
|
||||
(partitioning_disk_size_gb | float)
|
||||
- (partitioning_reserved_gb | float)
|
||||
- (system_cfg.features.cis.enabled | bool | ternary(partitioning_cis_reserved_gb | float, 0))
|
||||
- partitioning_lvm_extent_reserve_gb
|
||||
- partitioning_lvm_swap_target_limited_gb
|
||||
) | float
|
||||
}}
|
||||
partitioning_lvm_home_raw_gb: >-
|
||||
{{
|
||||
((partitioning_disk_size_gb | float) - (partitioning_disk_overhead_gb | float))
|
||||
* (partitioning_home_allocation_pct | float)
|
||||
}}
|
||||
partitioning_lvm_home_gb: >-
|
||||
{{
|
||||
[
|
||||
[(partitioning_lvm_home_raw_gb | float), (partitioning_home_min_gb | float)] | max,
|
||||
(partitioning_home_max_gb | float)
|
||||
] | min
|
||||
}}
|
||||
partitioning_lvm_root_default_gb: >-
|
||||
{{
|
||||
[
|
||||
(
|
||||
((partitioning_lvm_available_gb | float) < 4)
|
||||
| ternary(
|
||||
4,
|
||||
(
|
||||
((partitioning_lvm_available_gb | float) > 12)
|
||||
| ternary(
|
||||
((partitioning_disk_size_gb | float) * 0.4)
|
||||
| round(0, 'ceil'),
|
||||
partitioning_lvm_available_gb
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
4
|
||||
] | max
|
||||
}}
|
||||
partitioning_lvm_swap_gb: >-
|
||||
{{
|
||||
(
|
||||
[
|
||||
partitioning_lvm_swap_target_limited_gb,
|
||||
partitioning_lvm_swap_max_gb
|
||||
] | min | round(2, 'floor')
|
||||
)
|
||||
if system_cfg.features.swap.enabled | bool
|
||||
else 0
|
||||
}}
|
||||
partitioning_lvm_root_full_gb: >-
|
||||
{{
|
||||
[
|
||||
(
|
||||
(partitioning_disk_size_gb | float)
|
||||
- (partitioning_reserved_gb | float)
|
||||
- (partitioning_lvm_swap_gb | float)
|
||||
- partitioning_lvm_extent_reserve_gb
|
||||
- (
|
||||
(partitioning_lvm_home_gb | float)
|
||||
+ (partitioning_lvm_var_gb | float)
|
||||
+ (partitioning_lvm_var_log_gb | float)
|
||||
+ (partitioning_lvm_var_log_audit_gb | float)
|
||||
if system_cfg.features.cis.enabled | bool
|
||||
else 0
|
||||
)
|
||||
),
|
||||
4
|
||||
] | max | round(2, 'floor')
|
||||
}}
|
||||
partitioning_lvm_root_gb: >-
|
||||
{{
|
||||
partitioning_lvm_root_full_gb
|
||||
if partitioning_use_full_disk | bool
|
||||
else partitioning_lvm_root_default_gb
|
||||
}}
|
||||
community.general.lvol:
|
||||
vg: "{{ partitioning_vg_name }}"
|
||||
lv: "{{ item.lv }}"
|
||||
size: "{{ item.size }}"
|
||||
state: present
|
||||
loop:
|
||||
- lv: root
|
||||
size: "{{ partitioning_lvm_root_gb | string + 'G' }}"
|
||||
- lv: swap
|
||||
size: "{{ partitioning_lvm_swap_gb | string + 'G' }}"
|
||||
- lv: home
|
||||
size: "{{ partitioning_lvm_home_gb | string + 'G' }}"
|
||||
- { lv: var, size: "{{ partitioning_lvm_var_gb }}G" }
|
||||
- { lv: var_log, size: "{{ partitioning_lvm_var_log_gb }}G" }
|
||||
- { lv: var_log_audit, size: "{{ partitioning_lvm_var_log_audit_gb }}G" }
|
||||
loop_control:
|
||||
label: "{{ item.lv }}"
|
||||
@@ -1,116 +0,0 @@
|
||||
---
|
||||
- name: Partition install drive
|
||||
block:
|
||||
- name: Prepare partitions
|
||||
block:
|
||||
- name: Disable swap
|
||||
ansible.builtin.command: swapoff -a
|
||||
register: partitioning_swapoff_result
|
||||
changed_when: partitioning_swapoff_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Find mounts under /mnt
|
||||
ansible.builtin.command: findmnt -R /mnt -n -o TARGET
|
||||
register: partitioning_mounted_paths
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Unmount /mnt mounts
|
||||
when: partitioning_mounted_paths.stdout_lines | length > 0
|
||||
ansible.posix.mount:
|
||||
path: "{{ item }}"
|
||||
state: unmounted
|
||||
loop: "{{ partitioning_mounted_paths.stdout_lines | reverse }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
failed_when: false
|
||||
|
||||
- name: Remove LVM volume group
|
||||
community.general.lvg:
|
||||
vg: "{{ partitioning_vg_name }}"
|
||||
state: absent
|
||||
force: true
|
||||
failed_when: false
|
||||
|
||||
- name: Close LUKS mapper
|
||||
when: system_cfg.luks.enabled | bool
|
||||
community.crypto.luks_device:
|
||||
name: "{{ system_cfg.luks.mapper }}"
|
||||
state: closed
|
||||
failed_when: false
|
||||
|
||||
- name: Remove LUKS mapper device
|
||||
when: system_cfg.luks.enabled | bool
|
||||
ansible.builtin.command: >-
|
||||
dmsetup remove --force --retry {{ system_cfg.luks.mapper }}
|
||||
register: partitioning_dmsetup_remove
|
||||
changed_when: partitioning_dmsetup_remove.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Remove LUKS signatures
|
||||
when: system_cfg.luks.enabled | bool
|
||||
community.crypto.luks_device:
|
||||
device: "{{ partitioning_luks_device }}"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Wipe filesystem signatures
|
||||
ansible.builtin.shell: >-
|
||||
find /dev -wholename "{{ install_drive }}*" -exec wipefs --force --all {} \;
|
||||
register: partitioning_wipefs_result
|
||||
changed_when: partitioning_wipefs_result.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Refresh kernel partition table
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
- "partprobe {{ install_drive }}"
|
||||
- "udevadm settle"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Define partitions
|
||||
block:
|
||||
- name: Create partition layout
|
||||
community.general.parted:
|
||||
device: "{{ install_drive }}"
|
||||
label: gpt
|
||||
number: "{{ item.number }}"
|
||||
part_end: "{{ item.part_end | default(omit) }}"
|
||||
part_start: "{{ item.part_start | default(omit) }}"
|
||||
name: "{{ item.name }}"
|
||||
flags: "{{ item.flags | default(omit) }}"
|
||||
state: present
|
||||
loop: "{{ partitioning_layout }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
rescue:
|
||||
- name: Refresh kernel partition table after failure
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
- "partprobe {{ install_drive }}"
|
||||
- "udevadm settle"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Retry partition layout
|
||||
community.general.parted:
|
||||
device: "{{ install_drive }}"
|
||||
label: gpt
|
||||
number: "{{ item.number }}"
|
||||
part_end: "{{ item.part_end | default(omit) }}"
|
||||
part_start: "{{ item.part_start | default(omit) }}"
|
||||
name: "{{ item.name }}"
|
||||
flags: "{{ item.flags | default(omit) }}"
|
||||
state: present
|
||||
loop: "{{ partitioning_layout }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Settle partition table
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
- "partprobe {{ install_drive }}"
|
||||
- "udevadm settle"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
- name: Detect system memory for swap sizing
|
||||
when:
|
||||
- system_cfg.features.swap.enabled | bool
|
||||
- partitioning_vm_memory is not defined or (partitioning_vm_memory | float) <= 0
|
||||
- (system_cfg.memory | default(0) | float) <= 0
|
||||
block:
|
||||
- name: Read system memory
|
||||
ansible.builtin.command: awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo
|
||||
register: partitioning_memtotal_mb
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Set partitioning vm memory default
|
||||
ansible.builtin.set_fact:
|
||||
partitioning_vm_memory: "{{ (partitioning_memtotal_mb.stdout | default('4096') | int) | float }}"
|
||||
|
||||
- name: Set partitioning vm_size for physical installs
|
||||
when:
|
||||
- system_cfg.type == "physical"
|
||||
- partitioning_vm_size is not defined or (partitioning_vm_size | float) <= 0
|
||||
- install_drive | length > 0
|
||||
block:
|
||||
- name: Detect install drive size
|
||||
ansible.builtin.command: "lsblk -b -dn -o SIZE {{ install_drive }}"
|
||||
register: partitioning_disk_size_bytes
|
||||
changed_when: false
|
||||
|
||||
- name: Set partitioning vm_size from install drive size
|
||||
when:
|
||||
- partitioning_disk_size_bytes.stdout is defined
|
||||
- (partitioning_disk_size_bytes.stdout | trim | length) > 0
|
||||
ansible.builtin.set_fact:
|
||||
partitioning_vm_size: >-
|
||||
{{
|
||||
(partitioning_disk_size_bytes.stdout | trim | int / 1024 / 1024 / 1024)
|
||||
| round(2, 'floor')
|
||||
}}
|
||||
@@ -1,130 +0,0 @@
|
||||
---
|
||||
- name: Mount filesystems
|
||||
block:
|
||||
# CIS mode: mount all paths (separate partitions for /home, /var, etc.)
|
||||
# Non-CIS btrfs: only mount subvolume paths (/home, /var/log, /var/cache/pacman/pkg)
|
||||
# Non-CIS LVM: skip CIS-only paths (/home, /var, /var/log, /var/log/audit, /var/cache/pacman/pkg)
|
||||
- name: Mount filesystems and subvolumes
|
||||
when:
|
||||
- >-
|
||||
system_cfg.features.cis.enabled | bool or (
|
||||
not (system_cfg.features.cis.enabled | bool) and (
|
||||
(system_cfg.filesystem == 'btrfs' and item.path in ['/home', '/var/log']
|
||||
+ (['/var/cache/pacman/pkg'] if os == 'archlinux' else []))
|
||||
or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg'])
|
||||
)
|
||||
)
|
||||
- >-
|
||||
not (item.path in ['/swap', '/var/cache/pacman/pkg'] and (system_cfg.filesystem != 'btrfs' or os != 'archlinux'))
|
||||
- system_cfg.features.swap.enabled | bool or item.path != '/swap'
|
||||
ansible.posix.mount:
|
||||
path: /mnt{{ item.path }}
|
||||
src: "{{ 'UUID=' + (partitioning_main_uuid.stdout if system_cfg.filesystem == 'btrfs' else item.uuid) }}"
|
||||
fstype: "{{ system_cfg.filesystem }}"
|
||||
opts: "{{ item.opts }}"
|
||||
state: mounted
|
||||
loop:
|
||||
# ssd: no-op on kernels 5.15+ (btrfs auto-detects); kept for older kernel compat
|
||||
- path: ""
|
||||
uuid: "{{ partitioning_uuid_root[0] | default(omit) }}"
|
||||
opts: >-
|
||||
{{
|
||||
'defaults'
|
||||
if system_cfg.filesystem != 'btrfs'
|
||||
else [
|
||||
'rw', 'relatime', partitioning_btrfs_compress_opt, 'ssd', 'space_cache=v2',
|
||||
'discard=async', 'subvol=@'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
- path: /swap
|
||||
opts: >-
|
||||
{{
|
||||
[
|
||||
'rw', 'nosuid', 'nodev', 'relatime', partitioning_btrfs_compress_opt, 'ssd',
|
||||
'space_cache=v2', 'discard=async', 'subvol=@swap'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
- path: /home
|
||||
uuid: "{{ partitioning_uuid_home[0] | default(omit) }}"
|
||||
opts: >-
|
||||
{{
|
||||
'defaults,nosuid,nodev'
|
||||
if system_cfg.filesystem != 'btrfs'
|
||||
else [
|
||||
'rw', 'nosuid', 'nodev', 'relatime', partitioning_btrfs_compress_opt, 'ssd',
|
||||
'space_cache=v2', 'discard=async', 'subvol=@home'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
- path: /var
|
||||
uuid: "{{ partitioning_uuid_var[0] | default(omit) }}"
|
||||
opts: >-
|
||||
{{
|
||||
'defaults,nosuid,nodev'
|
||||
if system_cfg.filesystem != 'btrfs'
|
||||
else [
|
||||
'rw', 'nosuid', 'nodev', 'relatime', partitioning_btrfs_compress_opt, 'ssd',
|
||||
'space_cache=v2', 'discard=async', 'subvol=@var'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
- path: /var/log
|
||||
uuid: "{{ partitioning_uuid_var_log[0] | default(omit) }}"
|
||||
opts: >-
|
||||
{{
|
||||
'defaults,nosuid,nodev,noexec'
|
||||
if system_cfg.filesystem != 'btrfs'
|
||||
else [
|
||||
'rw', 'nosuid', 'nodev', 'noexec', 'relatime', partitioning_btrfs_compress_opt,
|
||||
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@var_log'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
- path: /var/cache/pacman/pkg
|
||||
uuid: "{{ partitioning_uuid_root | default([]) | first | default(omit) }}"
|
||||
opts: >-
|
||||
{{
|
||||
'defaults,nosuid,nodev,noexec'
|
||||
if system_cfg.filesystem != 'btrfs'
|
||||
else [
|
||||
'rw', 'nosuid', 'nodev', 'noexec', 'relatime', partitioning_btrfs_compress_opt,
|
||||
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@pkg'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
- path: /var/log/audit
|
||||
uuid: "{{ partitioning_uuid_var_log_audit[0] | default(omit) }}"
|
||||
opts: >-
|
||||
{{
|
||||
'defaults,nosuid,nodev,noexec'
|
||||
if system_cfg.filesystem != 'btrfs'
|
||||
else [
|
||||
'rw', 'nosuid', 'nodev', 'noexec', 'relatime', partitioning_btrfs_compress_opt,
|
||||
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@var_log_audit'
|
||||
] | reject('equalto', '') | join(',')
|
||||
}}
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Mount /boot filesystem
|
||||
when: partitioning_separate_boot | bool
|
||||
ansible.posix.mount:
|
||||
path: /mnt/boot
|
||||
src: "UUID={{ partitioning_boot_fs_uuid.stdout }}"
|
||||
fstype: "{{ partitioning_boot_fs_fstype }}"
|
||||
opts: defaults
|
||||
state: mounted
|
||||
|
||||
- name: Mount boot filesystem
|
||||
ansible.posix.mount:
|
||||
path: "/mnt{{ partitioning_efi_mountpoint }}"
|
||||
src: UUID={{ partitioning_boot_uuid.stdout }}
|
||||
fstype: vfat
|
||||
state: mounted
|
||||
|
||||
- name: Activate swap
|
||||
when: system_cfg.features.swap.enabled | bool
|
||||
vars:
|
||||
partitioning_swap_cmd: >-
|
||||
{{ 'swapon /mnt/swap/swapfile' if system_cfg.filesystem == 'btrfs' else 'swapon -U ' + partitioning_uuid_swap[0] }}
|
||||
ansible.builtin.command: "{{ partitioning_swap_cmd }}"
|
||||
register: partitioning_swap_activate_result
|
||||
# swapon returns 255 if swap is already active
|
||||
failed_when: partitioning_swap_activate_result.rc not in [0, 255]
|
||||
changed_when: partitioning_swap_activate_result.rc == 0
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
- name: Configure LUKS encryption
|
||||
when: system_cfg.luks.enabled | bool
|
||||
block:
|
||||
- name: Validate LUKS passphrase
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (system_cfg.luks.passphrase | string | length) > 0
|
||||
fail_msg: system.luks.passphrase must be set when LUKS is enabled.
|
||||
no_log: true
|
||||
|
||||
- name: Ensure LUKS container exists
|
||||
community.crypto.luks_device:
|
||||
device: "{{ partitioning_luks_device }}"
|
||||
state: present
|
||||
type: "{{ system_cfg.luks.type }}"
|
||||
cipher: "{{ system_cfg.luks.cipher }}"
|
||||
hash: "{{ system_cfg.luks.hash }}"
|
||||
keysize: "{{ system_cfg.luks.bits }}"
|
||||
pbkdf:
|
||||
algorithm: "{{ system_cfg.luks.pbkdf }}"
|
||||
iteration_time: "{{ (system_cfg.luks.iter | float) / 1000 }}"
|
||||
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
||||
no_log: true
|
||||
|
||||
- name: Force-close LUKS mapper
|
||||
community.crypto.luks_device:
|
||||
name: "{{ system_cfg.luks.mapper }}"
|
||||
state: closed
|
||||
failed_when: false
|
||||
|
||||
- name: Force-remove LUKS mapper device
|
||||
ansible.builtin.command: >-
|
||||
dmsetup remove --force --retry {{ system_cfg.luks.mapper }}
|
||||
register: partitioning_dmsetup_remove_after_format
|
||||
changed_when: partitioning_dmsetup_remove_after_format.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Settle udev after removing LUKS mapper
|
||||
ansible.builtin.command: udevadm settle
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Ensure LUKS mapper is opened
|
||||
block:
|
||||
- name: Open LUKS device
|
||||
community.crypto.luks_device:
|
||||
device: "{{ partitioning_luks_device }}"
|
||||
state: opened
|
||||
name: "{{ system_cfg.luks.mapper }}"
|
||||
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
||||
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
|
||||
no_log: true
|
||||
rescue:
|
||||
- name: Force-close stale LUKS mapper
|
||||
community.crypto.luks_device:
|
||||
name: "{{ system_cfg.luks.mapper }}"
|
||||
state: closed
|
||||
failed_when: false
|
||||
|
||||
- name: Force-remove stale LUKS mapper device
|
||||
ansible.builtin.command: >-
|
||||
dmsetup remove --force --retry {{ system_cfg.luks.mapper }}
|
||||
register: partitioning_dmsetup_remove_retry
|
||||
changed_when: partitioning_dmsetup_remove_retry.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- name: Settle udev after removing stale LUKS mapper
|
||||
ansible.builtin.command: udevadm settle
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Retry opening LUKS device
|
||||
community.crypto.luks_device:
|
||||
device: "{{ partitioning_luks_device }}"
|
||||
state: opened
|
||||
name: "{{ system_cfg.luks.mapper }}"
|
||||
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
||||
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
|
||||
no_log: true
|
||||
|
||||
- name: Get LUKS UUID
|
||||
ansible.builtin.command: "cryptsetup luksUUID {{ partitioning_luks_device }}"
|
||||
register: partitioning_luks_uuid_result
|
||||
changed_when: false
|
||||
|
||||
- name: Store LUKS UUID
|
||||
ansible.builtin.set_fact:
|
||||
partitioning_luks_uuid: "{{ partitioning_luks_uuid_result.stdout | trim }}"
|
||||
@@ -37,13 +37,12 @@
|
||||
- name: Enable quotas on Btrfs filesystem
|
||||
ansible.builtin.command: btrfs quota enable /mnt
|
||||
register: partitioning_btrfs_quota_result
|
||||
changed_when: partitioning_btrfs_quota_result.rc == 0
|
||||
changed_when: false
|
||||
|
||||
- name: Make root subvolumes
|
||||
when:
|
||||
- system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit']
|
||||
- system_cfg.features.cis.enabled or item.subvol not in ['var_log_audit']
|
||||
- system_cfg.features.swap.enabled | bool or item.subvol != 'swap'
|
||||
- item.os is not defined or os in item.os
|
||||
ansible.builtin.command: btrfs su cr /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
|
||||
args:
|
||||
creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
|
||||
@@ -52,28 +51,22 @@
|
||||
- { subvol: swap }
|
||||
- { subvol: home }
|
||||
- { subvol: var }
|
||||
- { subvol: pkg, os: [archlinux] }
|
||||
- { subvol: pkg }
|
||||
- { subvol: var_log }
|
||||
- { subvol: var_log_audit }
|
||||
loop_control:
|
||||
label: "{{ item.subvol }}"
|
||||
|
||||
- name: Set default btrfs subvolume to @
|
||||
ansible.builtin.shell: >-
|
||||
btrfs subvolume list /mnt | awk '/ path @$/ {print $2}'
|
||||
| xargs -I{} btrfs subvolume set-default {} /mnt
|
||||
register: partitioning_btrfs_default_result
|
||||
changed_when: partitioning_btrfs_default_result.rc == 0
|
||||
register: partitioning_btrfs_subvol_result
|
||||
|
||||
- name: Set quotas for subvolumes
|
||||
when: system_cfg.features.cis.enabled | bool
|
||||
when: system_cfg.features.cis.enabled
|
||||
ansible.builtin.command: btrfs qgroup limit {{ item.quota }} /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
|
||||
loop:
|
||||
- { subvol: home, quota: "{{ partitioning_btrfs_home_quota }}" }
|
||||
loop_control:
|
||||
label: "{{ item.subvol }}"
|
||||
register: partitioning_btrfs_qgroup_result
|
||||
changed_when: partitioning_btrfs_qgroup_result.rc == 0
|
||||
changed_when: false
|
||||
|
||||
- name: Create a Btrfs swap file
|
||||
when: system_cfg.features.swap.enabled | bool
|
||||
@@ -81,6 +74,7 @@
|
||||
btrfs filesystem mkswapfile --size {{ partitioning_swap_size_gb }}g --uuid clear /mnt/@swap/swapfile
|
||||
args:
|
||||
creates: /mnt/@swap/swapfile
|
||||
register: partitioning_btrfs_swap_result
|
||||
|
||||
- name: Unmount Partition
|
||||
ansible.posix.mount:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
- name: Create and format ext4 logical volumes
|
||||
when: system_cfg.features.cis.enabled | bool 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:
|
||||
dev: /dev/{{ partitioning_vg_name }}/{{ item.lv }}
|
||||
fstype: ext4
|
||||
@@ -17,7 +17,7 @@
|
||||
- name: Remove Unsupported features for older Systems
|
||||
when: >
|
||||
(os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11'))
|
||||
and (system_cfg.features.cis.enabled | bool 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/{{ partitioning_vg_name }}/{{ item.lv }}"
|
||||
loop:
|
||||
- { lv: root }
|
||||
|
||||
@@ -48,14 +48,6 @@
|
||||
ansible.builtin.command: udevadm settle
|
||||
changed_when: false
|
||||
|
||||
- name: Wipe existing filesystem signatures on additional disk partitions
|
||||
when: partitioning_extra_disks | length > 0
|
||||
ansible.builtin.command: "wipefs --force --all {{ item.partition }}"
|
||||
changed_when: true
|
||||
loop: "{{ partitioning_extra_disks }}"
|
||||
loop_control:
|
||||
label: "{{ item.partition }}"
|
||||
|
||||
- name: Create filesystems on additional disks
|
||||
when: partitioning_extra_disks | length > 0
|
||||
vars:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user