Compare commits
52 Commits
master
...
37df881daa
| Author | SHA1 | Date | |
|---|---|---|---|
| 37df881daa | |||
| 55b21eae5d | |||
| b1e938b7f0 | |||
| c843f5289b | |||
| 9757ed3785 | |||
| 876e90ce2b | |||
| 7c44cb1ff0 | |||
| 5d0630a386 | |||
| 3eaf918a53 | |||
| 382e82ff85 | |||
| db7dc53bd7 | |||
| 7d45f25a7e | |||
| 3880b8f41e | |||
| dc3c4a901f | |||
| 6bfaa0aa2b | |||
| 5dc0424acb | |||
| c12332a853 | |||
| 5ace4577a2 | |||
| c0ff01ef40 | |||
| 57417514e3 | |||
| 0928588c1f | |||
| 6d622f2db4 | |||
| b11d65a6f3 | |||
| 3623fc292c | |||
| dfca7ec94b | |||
| e8be84bf49 | |||
| 322cc0b1ce | |||
| 4b38754f8b | |||
| a6bc7ffe04 | |||
| c529e71ebc | |||
| cb46de2b6d | |||
| 9169117b25 | |||
| 6c94c519fb | |||
| efd96a42b8 | |||
| 68661c3cca | |||
| 1db20c7ac0 | |||
| 7b155b427b | |||
| ca8721e98f | |||
| cdb2559d8f | |||
| 443f6623df | |||
| 6cf418fe00 | |||
| 47ec5fe621 | |||
| 240f945cce | |||
| 663a04556f | |||
| 6febd1acf1 | |||
| 008187860c | |||
| cd1be6b5e1 | |||
| 15be6149fd | |||
| ca29ad200d | |||
| 8079099cee | |||
| 9e79185b07 | |||
| b88bf2860f |
@@ -1,5 +1,6 @@
|
|||||||
skip_list:
|
skip_list:
|
||||||
- run-once
|
- run-once
|
||||||
- var-naming[no-role-prefix] # user-facing API dicts (cis, system, hypervisor) are intentionally not role-prefixed
|
- 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:
|
exclude_paths:
|
||||||
- roles/global_defaults/
|
- roles/global_defaults/
|
||||||
|
|||||||
177
README.md
177
README.md
@@ -30,16 +30,13 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
|||||||
| `system.os` | Distribution | `system.version` |
|
| `system.os` | Distribution | `system.version` |
|
||||||
| ------------ | ------------------------ | ------------------------------------- |
|
| ------------ | ------------------------ | ------------------------------------- |
|
||||||
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
|
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
|
||||||
| `alpine` | Alpine Linux | latest (rolling) |
|
|
||||||
| `archlinux` | Arch Linux | latest (rolling) |
|
| `archlinux` | Arch Linux | latest (rolling) |
|
||||||
| `debian` | Debian | `10`-`13`, `unstable` |
|
| `debian` | Debian | `10`-`13`, `unstable` |
|
||||||
| `fedora` | Fedora | `38`-`45` |
|
| `fedora` | Fedora | `38`-`45` |
|
||||||
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
|
|
||||||
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
|
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
|
||||||
| `rocky` | Rocky Linux | `8`, `9`, `10` |
|
| `rocky` | Rocky Linux | `8`, `9`, `10` |
|
||||||
| `ubuntu` | Ubuntu (latest non-LTS) | optional (e.g. `24.04`) |
|
| `ubuntu` | Ubuntu (latest non-LTS) | optional (e.g. `24.04`) |
|
||||||
| `ubuntu-lts` | Ubuntu LTS | optional (e.g. `24.04`) |
|
| `ubuntu-lts` | Ubuntu LTS | optional (e.g. `24.04`) |
|
||||||
| `void` | Void Linux | latest (rolling) |
|
|
||||||
|
|
||||||
### Hypervisors
|
### Hypervisors
|
||||||
|
|
||||||
@@ -122,7 +119,7 @@ all:
|
|||||||
path: /data
|
path: /data
|
||||||
fstype: xfs
|
fstype: xfs
|
||||||
users:
|
users:
|
||||||
- name: ops
|
ops:
|
||||||
password: !vault |
|
password: !vault |
|
||||||
$ANSIBLE_VAULT...
|
$ANSIBLE_VAULT...
|
||||||
keys:
|
keys:
|
||||||
@@ -167,7 +164,7 @@ Top-level variables outside `system`/`hypervisor`/`cis`.
|
|||||||
| `type` | string | `virtual` | `virtual` or `physical` |
|
| `type` | string | `virtual` | `virtual` or `physical` |
|
||||||
| `os` | string | -- | Target distribution (see [table](#distributions)) |
|
| `os` | string | -- | Target distribution (see [table](#distributions)) |
|
||||||
| `version` | string | -- | Version selector for versioned distros |
|
| `version` | string | -- | Version selector for versioned distros |
|
||||||
| `filesystem` | string | -- | `btrfs`, `ext4`, or `xfs` |
|
| `filesystem` | string | `ext4` | `btrfs`, `ext4`, or `xfs` |
|
||||||
| `name` | string | inventory hostname | Final hostname |
|
| `name` | string | inventory hostname | Final hostname |
|
||||||
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
|
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
|
||||||
| `locale` | string | `en_US.UTF-8` | System locale |
|
| `locale` | string | `en_US.UTF-8` | System locale |
|
||||||
@@ -176,11 +173,12 @@ Top-level variables outside `system`/`hypervisor`/`cis`.
|
|||||||
| `cpus` | int | `0` | vCPU count (required for virtual) |
|
| `cpus` | int | `0` | vCPU count (required for virtual) |
|
||||||
| `memory` | int | `0` | Memory in MiB (required for virtual) |
|
| `memory` | int | `0` | Memory in MiB (required for virtual) |
|
||||||
| `balloon` | int | `0` | Balloon memory in MiB (Proxmox) |
|
| `balloon` | int | `0` | Balloon memory in MiB (Proxmox) |
|
||||||
| `path` | string | -- | Hypervisor folder/path |
|
| `path` | string | -- | Hypervisor folder/path (falls back to `hypervisor.folder`) |
|
||||||
|
| `mirror` | string | per-distro default | Override package mirror (Debian/Ubuntu) |
|
||||||
| `packages` | list | `[]` | Additional packages installed post-reboot |
|
| `packages` | list | `[]` | Additional packages installed post-reboot |
|
||||||
| `network` | dict | see below | Network configuration |
|
| `network` | dict | see below | Network configuration |
|
||||||
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#46-multi-disk-schema)) |
|
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#46-multi-disk-schema)) |
|
||||||
| `users` | list | `[]` | User accounts |
|
| `users` | dict | `{}` | User accounts (keyed by username) |
|
||||||
| `root` | dict | see below | Root account settings |
|
| `root` | dict | see below | Root account settings |
|
||||||
| `luks` | dict | see below | Encryption settings |
|
| `luks` | dict | see below | Encryption settings |
|
||||||
| `features` | dict | see below | Feature toggles |
|
| `features` | dict | see below | Feature toggles |
|
||||||
@@ -202,20 +200,36 @@ When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`
|
|||||||
|
|
||||||
#### `system.users`
|
#### `system.users`
|
||||||
|
|
||||||
|
Dict keyed by username. At least one user must have a `password` (used for SSH access during bootstrap). Users without a password get locked accounts (key-only auth).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
system:
|
||||||
|
users:
|
||||||
|
svcansible:
|
||||||
|
password: "vault_lookup"
|
||||||
|
keys:
|
||||||
|
- "ssh-ed25519 AAAA..."
|
||||||
|
appuser:
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD: ALL"
|
||||||
|
keys:
|
||||||
|
- "ssh-ed25519 BBBB..."
|
||||||
|
```
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
| Key | Type | Default | Description |
|
||||||
| ---------- | ----------- | ------- | -------------------------------------------------- |
|
| ---------- | ----------- | ------- | -------------------------------------------------- |
|
||||||
| `name` | string | -- | Username (required) |
|
| *(dict key)* | string | -- | Username (required) |
|
||||||
| `password` | string | -- | User password (required for first user) |
|
| `password` | string | -- | User password (required for at least one user) |
|
||||||
| `keys` | list | `[]` | SSH public keys |
|
| `keys` | list | `[]` | SSH public keys |
|
||||||
| `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string |
|
| `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string |
|
||||||
|
|
||||||
The first user's credentials are prompted interactively via `vars_prompt` unless supplied in inventory or `-e`.
|
Users must be defined in inventory. The dict format enables additive merging across inventory layers with `hash_behaviour=merge`.
|
||||||
|
|
||||||
#### `system.root`
|
#### `system.root`
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
| Key | Type | Default | Description |
|
||||||
| ---------- | ------ | ------- | ------------- |
|
| ---------- | ------ | ----------- | ------------- |
|
||||||
| `password` | string | -- | Root password |
|
| `password` | string | -- | Root password |
|
||||||
|
| `shell` | string | `/bin/bash` | Login shell |
|
||||||
|
|
||||||
#### `system.luks`
|
#### `system.luks`
|
||||||
|
|
||||||
@@ -234,15 +248,21 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
|
|||||||
| `iter` | int | `4000` | PBKDF iteration time (ms) |
|
| `iter` | int | `4000` | PBKDF iteration time (ms) |
|
||||||
| `bits` | int | `512` | Key size (bits) |
|
| `bits` | int | `512` | Key size (bits) |
|
||||||
| `pbkdf` | string | `argon2id` | PBKDF algorithm |
|
| `pbkdf` | string | `argon2id` | PBKDF algorithm |
|
||||||
| `urandom` | bool | `true` | Use urandom during key generation |
|
|
||||||
| `verify` | bool | `true` | Verify passphrase during format |
|
|
||||||
|
|
||||||
#### `system.luks.tpm2`
|
#### `system.luks.tpm2`
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
| Key | Type | Default | Description |
|
||||||
| -------- | ------------- | ------- | ---------------------------------------------- |
|
| -------- | ------------- | ------- | ---------------------------------------------- |
|
||||||
| `device` | string | `auto` | TPM2 device selector |
|
| `device` | string | `auto` | TPM2 device selector |
|
||||||
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`) |
|
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`); empty = no PCR binding |
|
||||||
|
|
||||||
|
**TPM2 auto-unlock:** Uses `systemd-cryptenroll` on all distros. The user-set passphrase
|
||||||
|
remains as a backup unlock method. TPM2 enrollment runs in the chroot during bootstrap;
|
||||||
|
if it fails (e.g. no TPM2 hardware), the system boots with passphrase-only unlock and
|
||||||
|
TPM2 can be enrolled post-deployment via `systemd-cryptenroll --tpm2-device=auto <device>`.
|
||||||
|
|
||||||
|
On Debian/Ubuntu, TPM2 auto-unlock requires dracut (initramfs-tools does not support `tpm2-device`).
|
||||||
|
The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `features.initramfs.generator`.
|
||||||
|
|
||||||
#### `system.features`
|
#### `system.features`
|
||||||
|
|
||||||
@@ -259,6 +279,119 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
|
|||||||
| `banner.motd` | bool | `false` | MOTD banner |
|
| `banner.motd` | bool | `false` | MOTD banner |
|
||||||
| `banner.sudo` | bool | `true` | Sudo banner |
|
| `banner.sudo` | bool | `true` | Sudo banner |
|
||||||
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` |
|
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` |
|
||||||
|
| `initramfs.generator` | string | auto-detected | Override initramfs generator (see below) |
|
||||||
|
| `rhel_repo.source` | string | `iso` | RHEL post-install repo source: `iso`, `satellite`, or `none` |
|
||||||
|
| `rhel_repo.url` | string | -- | Satellite/custom repo URL when `source: satellite` |
|
||||||
|
| `secure_boot.enabled` | bool | `false` | Enable Secure Boot (Arch via sbctl, others via shim) |
|
||||||
|
| `secure_boot.method` | string | -- | Arch only: `sbctl` (default) or `uki` |
|
||||||
|
| `desktop.*` | dict | see below | Desktop environment settings (see [4.2.5](#425-systemfeaturesdesktop)) |
|
||||||
|
| `firmware.*` | dict | see below | Vendor firmware blobs and CPU microcode (see [4.2.6](#426-systemfeaturesfirmware)) |
|
||||||
|
| `gpu.*` | dict | see below | Mesa/Vulkan and per-vendor GPU userspace (see [4.2.7](#427-systemfeaturesgpu)) |
|
||||||
|
| `peripherals.*` | dict | see below | Fingerprint readers, webcams, DisplayLink (see [4.2.8](#428-systemfeaturesperipherals)) |
|
||||||
|
| `hardware.*` | dict | see below | Hardware-detection profile override (see [4.2.9](#429-systemfeatureshardware)) |
|
||||||
|
|
||||||
|
**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`, `sway`, or `hyprland` |
|
||||||
|
| `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, or `greetd` |
|
||||||
|
| `autologin` | bool \| string | `false` | `false` to disable, or a username from `system.users` to auto-login that user |
|
||||||
|
| `session` | string | auto-from-environment | Session to autologin into; overrides the per-environment default (sddm `.desktop` basename / greetd command) |
|
||||||
|
| `groups` | list | `[]` | Opt-in package groups installed on top of the base set (keys of `desktop_package_groups`, e.g. `dev`) |
|
||||||
|
|
||||||
|
All desktop environments are Wayland-only. `sway` and `hyprland` are available on Arch only;
|
||||||
|
`gnome` and `kde` are available on all three families.
|
||||||
|
|
||||||
|
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 to gdm, kde to sddm, sway to greetd, hyprland to greetd.
|
||||||
|
|
||||||
|
When `autologin` names a user, the matching display manager is configured to log that user in without a
|
||||||
|
password prompt. `session` is resolved automatically per environment when left empty (gdm picks its default,
|
||||||
|
sddm uses `plasma.desktop` for kde, greetd runs the compositor command for sway/hyprland), so it only needs
|
||||||
|
setting to override that choice.
|
||||||
|
|
||||||
|
#### 4.2.6 `system.features.firmware`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| ----------- | --------------- | ------- | ----------------------------------------------------------------- |
|
||||||
|
| `enabled` | bool \| `auto` | `auto` | Install vendor firmware blobs. `auto` = on for `physical`, off for `virtual` |
|
||||||
|
| `microcode` | bool \| `auto` | `auto` | Install CPU microcode. `auto` follows `firmware.enabled` |
|
||||||
|
|
||||||
|
Defaults are designed so a baremetal install picks up firmware automatically with no inventory entry needed,
|
||||||
|
while VMs skip it (the hypervisor handles those). The environment role detects CPU/GPU/wireless vendors from
|
||||||
|
the live host (via `lscpu` and `lspci`) and the bootstrap role installs only the matching firmware packages.
|
||||||
|
On Arch, this uses the vendor splits (`linux-firmware-amdgpu`, `linux-firmware-realtek`, etc.) so the install
|
||||||
|
stays minimal. On Debian, it uses the equivalent `firmware-*` packages. Distros without firmware splits fall
|
||||||
|
back to a single meta package.
|
||||||
|
|
||||||
|
#### 4.2.7 `system.features.gpu`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --------------- | ------ | ------- | ---------------------------------------------------- |
|
||||||
|
| `enabled` | bool | `false` | Install Mesa, Vulkan, and per-GPU userspace |
|
||||||
|
| `nvidia_driver` | string | `auto` | One of `auto`, `open`, `proprietary`, `nouveau` |
|
||||||
|
|
||||||
|
Pair with `desktop.enabled: true` for a working desktop. The package set is determined by the same hardware
|
||||||
|
profile as `firmware`. The `nvidia_driver: auto` default picks **`open`** (`nvidia-open` kernel modules) for
|
||||||
|
Turing or newer GPUs, falls back to **`proprietary`** for older cards on distros that ship the proprietary
|
||||||
|
driver, and falls back to **`nouveau`** elsewhere. Force a specific flavor by setting the value explicitly.
|
||||||
|
|
||||||
|
Proprietary and open Nvidia drivers on Fedora require RPMFusion non-free, which the bootstrap enables
|
||||||
|
automatically when needed. Debian uses `nvidia-driver` from the `non-free` component (already enabled in the
|
||||||
|
managed `sources.list`). Ubuntu uses `restricted`. Arch ships both `nvidia-open-dkms` and `nvidia-dkms` in
|
||||||
|
the `extra` repository - no third-party setup required.
|
||||||
|
|
||||||
|
#### 4.2.8 `system.features.peripherals`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| ------------- | --------------- | ------- | ---------------------------------------------------------- |
|
||||||
|
| `enabled` | bool \| `auto` | `auto` | Master switch. `auto` follows `desktop.enabled` |
|
||||||
|
| `fingerprint` | bool \| `auto` | `auto` | `fprintd`/`libfprint`. `auto` = install when reader detected |
|
||||||
|
| `webcam` | bool \| `auto` | `auto` | `v4l-utils` and userspace tooling. `auto` follows `enabled` |
|
||||||
|
| `displaylink` | bool | `false` | DisplayLink dock support (explicit opt-in; see notes) |
|
||||||
|
|
||||||
|
Fingerprint detection scans `lsusb` for known reader vendor IDs (Synaptics, Validity, Goodix, Elan, Egis,
|
||||||
|
Broadcom, AuthenTec, Upek, Futronic). When `fingerprint: auto` and a reader is present, `fprintd` and the
|
||||||
|
PAM helper are installed. PAM enrollment must be done post-install (`fprintd-enroll`).
|
||||||
|
|
||||||
|
DisplayLink ships proprietary userspace that distros do not package consistently. The bootstrap installs the
|
||||||
|
in-tree `evdi-dkms` kernel module on Debian/Ubuntu and the `evdi` module on Fedora, but the userspace blob
|
||||||
|
must still be installed manually from DisplayLink's site after first boot. Arch users typically use AUR
|
||||||
|
(`displaylink`); this is not wired into the bootstrap.
|
||||||
|
|
||||||
|
#### 4.2.9 `system.features.hardware`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
| --------- | ---- | ------- | -------------------------------------------------------------------- |
|
||||||
|
| `profile` | dict | `{}` | Hardware-detection override; empty means autodetect from live host |
|
||||||
|
|
||||||
|
When empty, hardware is detected at the start of the bootstrap. When set, detection is skipped and the
|
||||||
|
supplied profile drives package selection - this is the **golden-image** flow: bake an image with a fixed
|
||||||
|
profile, snapshot it, and reuse the same profile on every deploy of that hardware class.
|
||||||
|
|
||||||
|
Profile shape:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
system:
|
||||||
|
features:
|
||||||
|
hardware:
|
||||||
|
profile:
|
||||||
|
cpu: intel # intel | amd
|
||||||
|
gpus: [intel, nvidia] # any of: intel, amd, nvidia
|
||||||
|
nvidia_supports_open: true # set false to force proprietary/nouveau
|
||||||
|
wireless: [intel] # any of: intel, amd, atheros, broadcom,
|
||||||
|
# mediatek, marvell, realtek, qcom, cirrus
|
||||||
|
fingerprint: false # set true to force fprintd install
|
||||||
|
```
|
||||||
|
|
||||||
### 4.3 `hypervisor` Dictionary
|
### 4.3 `hypervisor` Dictionary
|
||||||
|
|
||||||
@@ -272,7 +405,7 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
|
|||||||
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
|
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
|
||||||
| `datacenter` | string | -- | VMware datacenter |
|
| `datacenter` | string | -- | VMware datacenter |
|
||||||
| `cluster` | string | -- | VMware cluster |
|
| `cluster` | string | -- | VMware cluster |
|
||||||
| `certs` | bool | `true` | TLS certificate validation (VMware) |
|
| `certs` | bool | `false` | TLS certificate validation (VMware) |
|
||||||
| `ssh` | bool | `false` | Enable SSH on guest and switch connection (VMware) |
|
| `ssh` | bool | `false` | Enable SSH on guest and switch connection (VMware) |
|
||||||
|
|
||||||
### 4.4 `cis` Dictionary
|
### 4.4 `cis` Dictionary
|
||||||
@@ -336,7 +469,7 @@ When `hypervisor.type: vmware` uses the `vmware_tools` connection:
|
|||||||
| ------------- | ------ | ------------------------------------------------------ |
|
| ------------- | ------ | ------------------------------------------------------ |
|
||||||
| `size` | number | Disk size in GB (required for virtual) |
|
| `size` | number | Disk size in GB (required for virtual) |
|
||||||
| `device` | string | Block device path (required for physical data disks) |
|
| `device` | string | Block device path (required for physical data disks) |
|
||||||
| `partition` | string | Partition device path (required for physical data disks) |
|
| `partition` | string | Derived from `device` during normalization (not user input) |
|
||||||
| `mount.path` | string | Mount point (additional disks only) |
|
| `mount.path` | string | Mount point (additional disks only) |
|
||||||
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
||||||
| `mount.label` | string | Filesystem label |
|
| `mount.label` | string | Filesystem label |
|
||||||
@@ -384,9 +517,9 @@ Roles execute in this order:
|
|||||||
1. **global_defaults** -- normalize inputs, validate, set OS flags
|
1. **global_defaults** -- normalize inputs, validate, set OS flags
|
||||||
2. **system_check** -- detect installer environment, verify live/non-prod target
|
2. **system_check** -- detect installer environment, verify live/non-prod target
|
||||||
3. **virtualization** -- create VM (if virtual), attach disks, cloud-init
|
3. **virtualization** -- create VM (if virtual), attach disks, cloud-init
|
||||||
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman
|
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman, detect hardware
|
||||||
5. **partitioning** -- create partitions, LVM, LUKS, mount filesystems
|
5. **partitioning** -- create partitions, LVM, LUKS, mount filesystems
|
||||||
6. **bootstrap** -- install base system and packages (OS-specific)
|
6. **bootstrap** -- install base system, packages, and vendor-matched hardware bits
|
||||||
7. **configuration** -- users, fstab, locales, bootloader, encryption enrollment, networking
|
7. **configuration** -- users, fstab, locales, bootloader, encryption enrollment, networking
|
||||||
8. **cis** -- CIS hardening (when `system.features.cis.enabled: true`)
|
8. **cis** -- CIS hardening (when `system.features.cis.enabled: true`)
|
||||||
9. **cleanup** -- unmount, shutdown installer, remove media, verify boot
|
9. **cleanup** -- unmount, shutdown installer, remove media, verify boot
|
||||||
@@ -398,7 +531,7 @@ 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.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Credentials for the first user and root are prompted interactively via `vars_prompt` unless already set in inventory or passed via `-e`.
|
All credentials (`system.users`, `system.root.password`) must be defined in inventory or passed via `-e`.
|
||||||
|
|
||||||
Example inventory files are included:
|
Example inventory files are included:
|
||||||
|
|
||||||
@@ -408,7 +541,7 @@ Example inventory files are included:
|
|||||||
|
|
||||||
## 7. Security
|
## 7. Security
|
||||||
|
|
||||||
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, `system.users[].password`, `system.root.password`).
|
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, user passwords in `system.users`, `system.root.password`).
|
||||||
|
|
||||||
## 8. Safety
|
## 8. Safety
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ hash_behaviour = merge
|
|||||||
interpreter_python = auto_silent
|
interpreter_python = auto_silent
|
||||||
deprecation_warnings = False
|
deprecation_warnings = False
|
||||||
host_key_checking = False
|
host_key_checking = False
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=30 -o ServerAliveCountMax=10
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ all:
|
|||||||
baremetal01.example.com:
|
baremetal01.example.com:
|
||||||
ansible_host: 10.0.0.162
|
ansible_host: 10.0.0.162
|
||||||
ansible_user: root
|
ansible_user: root
|
||||||
ansible_password: "1234"
|
ansible_password: "CHANGE_ME"
|
||||||
ansible_become_password: "1234"
|
ansible_become_password: "CHANGE_ME"
|
||||||
|
# Required for physical installs: confirms the operator accepts that
|
||||||
|
# install_drive will be wiped. system_check refuses to run without it.
|
||||||
|
physical_install_confirmed: true
|
||||||
system:
|
system:
|
||||||
type: "physical"
|
type: "physical"
|
||||||
os: "archlinux"
|
os: "archlinux"
|
||||||
@@ -18,3 +21,10 @@ all:
|
|||||||
disks:
|
disks:
|
||||||
- device: "/dev/sda"
|
- device: "/dev/sda"
|
||||||
size: 120
|
size: 120
|
||||||
|
users:
|
||||||
|
admin:
|
||||||
|
password: "CHANGE_ME"
|
||||||
|
keys:
|
||||||
|
- "ssh-ed25519 AAAA..."
|
||||||
|
root:
|
||||||
|
password: "CHANGE_ME"
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ all:
|
|||||||
label: DATA
|
label: DATA
|
||||||
opts: defaults
|
opts: defaults
|
||||||
users:
|
users:
|
||||||
- name: "ops"
|
ops:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
@@ -100,7 +100,7 @@ all:
|
|||||||
path: /srv/data
|
path: /srv/data
|
||||||
fstype: ext4
|
fstype: ext4
|
||||||
users:
|
users:
|
||||||
- name: "dbadmin"
|
dbadmin:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ all:
|
|||||||
url: "localhost"
|
url: "localhost"
|
||||||
username: ""
|
username: ""
|
||||||
password: ""
|
password: ""
|
||||||
host: ""
|
|
||||||
storage: "default"
|
storage: "default"
|
||||||
boot_iso: "/var/lib/libvirt/images/archlinux-x86_64.iso"
|
boot_iso: "/var/lib/libvirt/images/archlinux-x86_64.iso"
|
||||||
children:
|
children:
|
||||||
@@ -40,7 +39,7 @@ all:
|
|||||||
path: /var/www
|
path: /var/www
|
||||||
fstype: xfs
|
fstype: xfs
|
||||||
users:
|
users:
|
||||||
- name: "web"
|
web:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
@@ -82,7 +81,7 @@ all:
|
|||||||
path: /data
|
path: /data
|
||||||
fstype: ext4
|
fstype: ext4
|
||||||
users:
|
users:
|
||||||
- name: "db"
|
db:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
@@ -123,7 +122,7 @@ all:
|
|||||||
path: /data
|
path: /data
|
||||||
fstype: btrfs
|
fstype: btrfs
|
||||||
users:
|
users:
|
||||||
- name: "compute"
|
compute:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
|
|||||||
120
main.yml
120
main.yml
@@ -1,107 +1,20 @@
|
|||||||
---
|
---
|
||||||
# Bootstrap pipeline — role execution order:
|
# Bootstrap pipeline - role execution order:
|
||||||
# 1. global_defaults — normalize + validate system/hypervisor/disk input
|
# 1. global_defaults - normalize + validate system/hypervisor/disk input
|
||||||
# 2. system_check — pre-flight hardware/environment safety checks
|
# 2. system_check - pre-flight hardware/environment safety checks
|
||||||
# 3. virtualization — create VM on hypervisor (libvirt/proxmox/vmware/xen)
|
# 3. virtualization - create VM on hypervisor (libvirt/proxmox/vmware/xen)
|
||||||
# 4. environment — detect live ISO, configure installer network, install tools
|
# 4. environment - detect live ISO, configure installer network, install tools, detect hardware
|
||||||
# 5. partitioning — partition disk, create FS, LUKS, LVM, mount everything
|
# 5. partitioning - partition disk, create FS, LUKS, LVM, mount everything
|
||||||
# 6. bootstrap — debootstrap/pacstrap/dnf install the target OS into /mnt
|
# 6. bootstrap - debootstrap/pacstrap/dnf install the target OS into /mnt
|
||||||
# 7. configuration — users, network, encryption, fstab, bootloader, services
|
# 7. configuration - users, network, encryption, fstab, bootloader, services
|
||||||
# 8. cis — CIS hardening (optional, per system.features.cis.enabled)
|
# 8. cis - CIS hardening (optional, per system.features.cis.enabled)
|
||||||
# 9. cleanup — unmount, remove cloud-init artifacts, reboot/shutdown
|
# 9. cleanup - unmount, remove cloud-init artifacts, reboot/shutdown
|
||||||
- name: Create and configure VMs
|
- name: Create and configure VMs
|
||||||
hosts: "{{ bootstrap_target | default('all') }}"
|
hosts: "{{ bootstrap_target | default('all') }}"
|
||||||
strategy: free # noqa: run-once[play]
|
strategy: free # noqa: run-once[play]
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
become: true
|
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:
|
pre_tasks:
|
||||||
- name: Apply prompted authentication values to system input
|
|
||||||
no_log: true
|
|
||||||
vars:
|
|
||||||
system_input: "{{ system | default({}) }}"
|
|
||||||
system_users_input: "{{ system_input.users | default([]) }}"
|
|
||||||
system_first_user: >-
|
|
||||||
{{
|
|
||||||
system_users_input[0]
|
|
||||||
if (system_users_input is iterable and system_users_input is not string
|
|
||||||
and system_users_input is not mapping and system_users_input | length > 0)
|
|
||||||
else {}
|
|
||||||
}}
|
|
||||||
system_root_input: "{{ (system_input.root | default({})) if (system_input.root is mapping) else {} }}"
|
|
||||||
prompt_user_name: "{{ user_name | default(system_user_name | default(''), true) | string }}"
|
|
||||||
prompt_user_key: "{{ user_public_key | default(user_key | default(system_user_key | default(''), true), true) | string | trim }}"
|
|
||||||
prompt_user_password: "{{ user_password | default(system_user_password | default(''), true) | string }}"
|
|
||||||
prompt_root_password: "{{ root_password | default(system_root_password | default(''), true) | string }}"
|
|
||||||
resolved_user:
|
|
||||||
name: >-
|
|
||||||
{{
|
|
||||||
system_first_user.name | string
|
|
||||||
if (system_first_user.name | default('') | string | length) > 0
|
|
||||||
else prompt_user_name
|
|
||||||
}}
|
|
||||||
keys: >-
|
|
||||||
{{
|
|
||||||
system_first_user['keys']
|
|
||||||
if (system_first_user['keys'] is defined
|
|
||||||
and system_first_user['keys'] is iterable
|
|
||||||
and system_first_user['keys'] is not string
|
|
||||||
and system_first_user['keys'] | length > 0)
|
|
||||||
else (
|
|
||||||
[prompt_user_key]
|
|
||||||
if (prompt_user_key | length > 0)
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
password: >-
|
|
||||||
{{
|
|
||||||
system_first_user.password | string
|
|
||||||
if (system_first_user.password | default('') | string | length) > 0
|
|
||||||
else prompt_user_password
|
|
||||||
}}
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
system: >-
|
|
||||||
{{
|
|
||||||
system_input
|
|
||||||
| combine(
|
|
||||||
{
|
|
||||||
'users': (
|
|
||||||
[resolved_user]
|
|
||||||
+ (system_users_input[1:]
|
|
||||||
if (system_users_input is sequence
|
|
||||||
and system_users_input is not string
|
|
||||||
and system_users_input | length > 1)
|
|
||||||
else [])
|
|
||||||
),
|
|
||||||
'root': {
|
|
||||||
'password': (
|
|
||||||
(system_root_input.password | default('') | string | length) > 0
|
|
||||||
) | ternary(system_root_input.password | string, prompt_root_password)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
recursive=True
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
|
|
||||||
- name: Load global defaults
|
- name: Load global defaults
|
||||||
ansible.builtin.import_role:
|
ansible.builtin.import_role:
|
||||||
name: global_defaults
|
name: global_defaults
|
||||||
@@ -160,8 +73,6 @@
|
|||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: cleanup
|
name: cleanup
|
||||||
public: true
|
public: true
|
||||||
vars:
|
|
||||||
ansible_become: false
|
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- name: Delete VM on bootstrap failure
|
- name: Delete VM on bootstrap failure
|
||||||
@@ -208,10 +119,15 @@
|
|||||||
when:
|
when:
|
||||||
- post_reboot_can_connect | bool
|
- post_reboot_can_connect | bool
|
||||||
no_log: true
|
no_log: true
|
||||||
|
vars:
|
||||||
|
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
ansible_user: "{{ system_cfg.users[0].name }}"
|
ansible_connection: ssh
|
||||||
ansible_password: "{{ system_cfg.users[0].password }}"
|
ansible_host: "{{ system_cfg.network.ip }}"
|
||||||
ansible_become_password: "{{ system_cfg.users[0].password }}"
|
ansible_port: 22
|
||||||
|
ansible_user: "{{ _primary.key }}"
|
||||||
|
ansible_password: "{{ _primary.value.password }}"
|
||||||
|
ansible_become_password: "{{ _primary.value.password }}"
|
||||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||||
ansible_python_interpreter: /usr/bin/python3
|
ansible_python_interpreter: /usr/bin/python3
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
---
|
---
|
||||||
# OS → task file mapping for bootstrap dispatch.
|
# OS -> task file mapping for bootstrap dispatch.
|
||||||
# Each key matches a supported `os` value; value is the task file to include.
|
# Each key matches a supported `os` value; value is the task file to include.
|
||||||
bootstrap_os_task_map:
|
bootstrap_os_task_map:
|
||||||
almalinux: _dnf_family.yml
|
almalinux: _dnf_family.yml
|
||||||
alpine: alpine.yml
|
|
||||||
archlinux: archlinux.yml
|
archlinux: archlinux.yml
|
||||||
debian: debian.yml
|
debian: debian.yml
|
||||||
fedora: _dnf_family.yml
|
fedora: _dnf_family.yml
|
||||||
opensuse: opensuse.yml
|
|
||||||
rocky: _dnf_family.yml
|
rocky: _dnf_family.yml
|
||||||
rhel: rhel.yml
|
rhel: rhel.yml
|
||||||
ubuntu: ubuntu.yml
|
ubuntu: ubuntu.yml
|
||||||
ubuntu-lts: ubuntu.yml
|
ubuntu-lts: ubuntu.yml
|
||||||
void: void.yml
|
|
||||||
|
|||||||
69
roles/bootstrap/tasks/_desktop.yml
Normal file
69
roles/bootstrap/tasks/_desktop.yml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
- 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({}) }}"
|
||||||
|
_base: "{{ bootstrap_desktop_base_packages[os_family] | default([]) }}"
|
||||||
|
_requested_groups: "{{ system_cfg.features.desktop.groups | default([]) }}"
|
||||||
|
_group_pkgs: >-
|
||||||
|
{{
|
||||||
|
_requested_groups
|
||||||
|
| select('in', desktop_package_groups)
|
||||||
|
| map('extract', desktop_package_groups)
|
||||||
|
| map(attribute=os_family, default=[])
|
||||||
|
| list
|
||||||
|
| sum(start=[])
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
# GNOME ships under different dnf environment groups: Fedora uses
|
||||||
|
# workstation-product-environment, enterprise RHEL/Rocky/Alma use
|
||||||
|
# graphical-server-environment ("Server with GUI").
|
||||||
|
_desktop_groups: >-
|
||||||
|
{{ ['graphical-server-environment']
|
||||||
|
if (_de == 'gnome' and os_family == 'RedHat' and os != 'fedora')
|
||||||
|
else (_de_config.groups | default([])) }}
|
||||||
|
_desktop_packages: >-
|
||||||
|
{{
|
||||||
|
((_de_config.packages | default([])) + _base + _group_pkgs)
|
||||||
|
| reject('equalto', '')
|
||||||
|
| unique
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Validate desktop environment is supported
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- system_cfg.features.desktop.environment in (bootstrap_desktop_packages[os_family] | default({}))
|
||||||
|
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_major }}
|
||||||
|
--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_major }}
|
||||||
|
--setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }}
|
||||||
|
Debian: >-
|
||||||
|
{{ chroot_command }} apt install -y --install-recommends {{ _desktop_packages | join(' ') }}
|
||||||
|
Archlinux: >-
|
||||||
|
pacstrap /mnt {{ _desktop_packages | join(' ') }}
|
||||||
|
ansible.builtin.command: "{{ _install_commands[os_family] }}"
|
||||||
|
register: _desktop_pkg_result
|
||||||
|
changed_when: _desktop_pkg_result.rc == 0
|
||||||
@@ -13,11 +13,14 @@
|
|||||||
block:
|
block:
|
||||||
- name: "Install base system for {{ os | capitalize }}"
|
- name: "Install base system for {{ os | capitalize }}"
|
||||||
ansible.builtin.command: >-
|
ansible.builtin.command: >-
|
||||||
dnf --releasever={{ os_version }} --best {{ _dnf_repos }}
|
dnf --releasever={{ os_version_major }} --best {{ _dnf_repos }}
|
||||||
--installroot=/mnt --setopt=install_weak_deps=False
|
--installroot=/mnt --setopt=install_weak_deps=False
|
||||||
groupinstall -y {{ _dnf_groups }}
|
groupinstall -y {{ _dnf_groups }}
|
||||||
register: bootstrap_dnf_base_result
|
register: bootstrap_dnf_base_result
|
||||||
changed_when: bootstrap_dnf_base_result.rc == 0
|
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
|
- name: Ensure chroot has DNS resolution
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
|
|
||||||
- name: Install extra packages
|
- name: Install extra packages
|
||||||
ansible.builtin.command: >-
|
ansible.builtin.command: >-
|
||||||
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
|
{{ chroot_command }} dnf --releasever={{ os_version_major }} --setopt=install_weak_deps=False
|
||||||
install -y {{ _dnf_extra }}
|
install -y {{ _dnf_extra }}
|
||||||
register: bootstrap_dnf_extra_result
|
register: bootstrap_dnf_extra_result
|
||||||
changed_when: bootstrap_dnf_extra_result.rc == 0
|
changed_when: bootstrap_dnf_extra_result.rc == 0
|
||||||
|
|||||||
181
roles/bootstrap/tasks/_hardware.yml
Normal file
181
roles/bootstrap/tasks/_hardware.yml
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
# Installs vendor-matched microcode/firmware/GPU/peripheral packages into /mnt
|
||||||
|
# based on hardware_profile_active set by environment/_detect_hardware.yml.
|
||||||
|
- name: Load hardware package definitions
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
file: hardware.yml
|
||||||
|
|
||||||
|
- name: Validate hardware support for current os_family
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- os_family in bootstrap_hardware_packages
|
||||||
|
- hardware_profile_active is defined
|
||||||
|
fail_msg: >-
|
||||||
|
Hardware feature requested but no package map for os_family
|
||||||
|
'{{ os_family }}'. Extend roles/bootstrap/vars/hardware.yml.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
# nvidia_driver: auto -> open (Turing+) -> proprietary (older, if family ships it)
|
||||||
|
# -> nouveau (fallback). Explicit value falls back to nouveau when
|
||||||
|
# the family lacks packages for it.
|
||||||
|
- name: Resolve Nvidia driver flavor
|
||||||
|
vars:
|
||||||
|
_family: "{{ bootstrap_hardware_packages[os_family] }}"
|
||||||
|
_user_driver: "{{ system_cfg.features.gpu.nvidia_driver | default('auto') }}"
|
||||||
|
_has_nvidia: "{{ 'nvidia' in (hardware_profile_active.gpus | default([])) }}"
|
||||||
|
_supports_open: "{{ hardware_profile_active.nvidia_supports_open | default(true) | bool }}"
|
||||||
|
_open_pkgs: "{{ _family.gpu_nvidia.open | default([]) }}"
|
||||||
|
_prop_pkgs: "{{ _family.gpu_nvidia.proprietary | default([]) }}"
|
||||||
|
_auto_choice: >-
|
||||||
|
{{
|
||||||
|
('open' if _supports_open and _open_pkgs | length > 0
|
||||||
|
else ('proprietary' if _prop_pkgs | length > 0
|
||||||
|
else 'nouveau'))
|
||||||
|
}}
|
||||||
|
_user_choice: >-
|
||||||
|
{{
|
||||||
|
_auto_choice if _user_driver == 'auto'
|
||||||
|
else (_user_driver
|
||||||
|
if (_family.gpu_nvidia[_user_driver] | default([]) | length > 0)
|
||||||
|
else 'nouveau')
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_nvidia_driver_resolved: "{{ _user_choice if _has_nvidia else 'nouveau' }}"
|
||||||
|
|
||||||
|
# Fedora's akmod-nvidia* packages live in RPMFusion non-free, which is not
|
||||||
|
# enabled out of the box; install the release RPM before the package step.
|
||||||
|
- name: Enable RPMFusion non-free for Fedora Nvidia install
|
||||||
|
when:
|
||||||
|
- os_family == 'RedHat'
|
||||||
|
- os == 'fedora'
|
||||||
|
- system_cfg.features.gpu.enabled | bool
|
||||||
|
- _nvidia_driver_resolved in ['open', 'proprietary']
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
{{ chroot_command }} dnf install -y
|
||||||
|
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{{ os_version_major }}.noarch.rpm
|
||||||
|
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{{ os_version_major }}.noarch.rpm
|
||||||
|
register: _rpmfusion_result
|
||||||
|
changed_when: _rpmfusion_result.rc == 0
|
||||||
|
|
||||||
|
- name: Resolve hardware package set
|
||||||
|
vars:
|
||||||
|
_family: "{{ bootstrap_hardware_packages[os_family] }}"
|
||||||
|
_cpu: "{{ hardware_profile_active.cpu | default('') | string }}"
|
||||||
|
_gpus: "{{ hardware_profile_active.gpus | default([]) | list }}"
|
||||||
|
_wifi: "{{ hardware_profile_active.wireless | default([]) | list }}"
|
||||||
|
_fp_detected: "{{ hardware_profile_active.fingerprint | default(false) | bool }}"
|
||||||
|
_firmware_on: "{{ system_cfg.features.firmware.enabled | bool }}"
|
||||||
|
_microcode_on: "{{ _firmware_on and (system_cfg.features.firmware.microcode | bool) }}"
|
||||||
|
_gpu_on: "{{ system_cfg.features.gpu.enabled | bool }}"
|
||||||
|
_peripherals_on: "{{ system_cfg.features.peripherals.enabled | bool }}"
|
||||||
|
_webcam_pref: "{{ system_cfg.features.peripherals.webcam | default('auto') }}"
|
||||||
|
_fp_pref: "{{ system_cfg.features.peripherals.fingerprint | default('auto') }}"
|
||||||
|
_dl_on: "{{ system_cfg.features.peripherals.displaylink | bool }}"
|
||||||
|
_webcam_on: >-
|
||||||
|
{{
|
||||||
|
_peripherals_on
|
||||||
|
and (_webcam_pref == 'true' or (_webcam_pref == 'auto' and _peripherals_on))
|
||||||
|
}}
|
||||||
|
_fp_on: >-
|
||||||
|
{{
|
||||||
|
_peripherals_on
|
||||||
|
and (_fp_pref == 'true' or (_fp_pref == 'auto' and _fp_detected))
|
||||||
|
}}
|
||||||
|
# Union of GPU/wireless/CPU vendors; CPU vendor is included so Intel-CPU
|
||||||
|
# systems pull i915/iwlwifi firmware via the same vendor split.
|
||||||
|
_cpu_vendor_list: "{{ ([_cpu] if (_cpu | length > 0) else []) | list }}"
|
||||||
|
_firmware_vendors: >-
|
||||||
|
{{
|
||||||
|
(_firmware_on | ternary(
|
||||||
|
(_gpus + _wifi + _cpu_vendor_list)
|
||||||
|
| reject('equalto', '') | unique | list,
|
||||||
|
[]
|
||||||
|
))
|
||||||
|
}}
|
||||||
|
_microcode_pkgs: >-
|
||||||
|
{{
|
||||||
|
((_microcode_on and _cpu | length > 0) | ternary(
|
||||||
|
_family.cpu_microcode[_cpu] | default([]),
|
||||||
|
[]
|
||||||
|
)) | list
|
||||||
|
}}
|
||||||
|
_firmware_pkgs: >-
|
||||||
|
{{
|
||||||
|
(_firmware_on | ternary(
|
||||||
|
(_family.firmware_base | default([]) | list)
|
||||||
|
+ (_firmware_vendors
|
||||||
|
| map('extract', _family.firmware | default({}))
|
||||||
|
| select('truthy')
|
||||||
|
| list
|
||||||
|
| sum(start=[])),
|
||||||
|
[]
|
||||||
|
)) | list
|
||||||
|
}}
|
||||||
|
_gpu_base_pkgs: "{{ (_gpu_on | ternary(_family.gpu_base | default([]), [])) | list }}"
|
||||||
|
_gpu_vendor_pkgs: >-
|
||||||
|
{{
|
||||||
|
(_gpu_on | ternary(
|
||||||
|
(_gpus | reject('equalto', 'nvidia') | list)
|
||||||
|
| map('extract', _family.gpu | default({}))
|
||||||
|
| select('truthy')
|
||||||
|
| list
|
||||||
|
| sum(start=[]),
|
||||||
|
[]
|
||||||
|
)) | list
|
||||||
|
}}
|
||||||
|
_gpu_nvidia_pkgs: >-
|
||||||
|
{{
|
||||||
|
((_gpu_on and ('nvidia' in _gpus)) | ternary(
|
||||||
|
_family.gpu_nvidia[_nvidia_driver_resolved] | default([]),
|
||||||
|
[]
|
||||||
|
)) | list
|
||||||
|
}}
|
||||||
|
_peripherals_base_pkgs: >-
|
||||||
|
{{
|
||||||
|
(_webcam_on | ternary(_family.peripherals_base | default([]), [])) | list
|
||||||
|
}}
|
||||||
|
_peripherals_fingerprint_pkgs: >-
|
||||||
|
{{
|
||||||
|
(_fp_on | ternary(_family.peripherals_fingerprint | default([]), [])) | list
|
||||||
|
}}
|
||||||
|
_peripherals_displaylink_pkgs: >-
|
||||||
|
{{
|
||||||
|
(_dl_on | ternary(_family.peripherals_displaylink | default([]), [])) | list
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_hardware_packages: >-
|
||||||
|
{{
|
||||||
|
(_microcode_pkgs + _firmware_pkgs
|
||||||
|
+ _gpu_base_pkgs + _gpu_vendor_pkgs + _gpu_nvidia_pkgs
|
||||||
|
+ _peripherals_base_pkgs + _peripherals_fingerprint_pkgs
|
||||||
|
+ _peripherals_displaylink_pkgs)
|
||||||
|
| reject('equalto', '')
|
||||||
|
| unique
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Report hardware package selection
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
Hardware install ({{ os_family }}):
|
||||||
|
cpu={{ hardware_profile_active.cpu | default('-') }},
|
||||||
|
gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }},
|
||||||
|
nvidia_driver={{ _nvidia_driver_resolved }},
|
||||||
|
wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }},
|
||||||
|
fingerprint={{ hardware_profile_active.fingerprint | default(false) }}
|
||||||
|
-> {{ _hardware_packages | length }} package(s)
|
||||||
|
|
||||||
|
- name: Install hardware packages
|
||||||
|
when: _hardware_packages | length > 0
|
||||||
|
vars:
|
||||||
|
_install_commands:
|
||||||
|
RedHat: >-
|
||||||
|
{{ chroot_command }} dnf --releasever={{ os_version_major }}
|
||||||
|
--setopt=install_weak_deps=False install -y {{ _hardware_packages | join(' ') }}
|
||||||
|
Debian: >-
|
||||||
|
{{ chroot_command }} apt install -y {{ _hardware_packages | join(' ') }}
|
||||||
|
Archlinux: >-
|
||||||
|
pacstrap /mnt {{ _hardware_packages | join(' ') }}
|
||||||
|
ansible.builtin.command: "{{ _install_commands[os_family] }}"
|
||||||
|
register: _hardware_install_result
|
||||||
|
changed_when: _hardware_install_result.rc == 0
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Bootstrap Alpine Linux
|
|
||||||
vars:
|
|
||||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
|
||||||
_base_packages: "{{ _config.base | join(' ') }}"
|
|
||||||
_extra_packages: >-
|
|
||||||
{{
|
|
||||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
|
||||||
| reject('equalto', '')
|
|
||||||
| join(' ')
|
|
||||||
}}
|
|
||||||
block:
|
|
||||||
- name: Install Alpine Linux base
|
|
||||||
ansible.builtin.command: >
|
|
||||||
apk --root /mnt --no-cache add {{ _base_packages }}
|
|
||||||
register: bootstrap_alpine_bootstrap_result
|
|
||||||
changed_when: bootstrap_alpine_bootstrap_result.rc == 0
|
|
||||||
|
|
||||||
- name: Install extra packages
|
|
||||||
when: _extra_packages | trim | length > 0
|
|
||||||
ansible.builtin.command: >
|
|
||||||
apk --root /mnt add {{ _extra_packages }}
|
|
||||||
register: bootstrap_alpine_extra_result
|
|
||||||
changed_when: bootstrap_alpine_extra_result.rc == 0
|
|
||||||
|
|
||||||
- name: Install bootloader
|
|
||||||
ansible.builtin.command: >
|
|
||||||
apk --root /mnt add grub grub-efi efibootmgr
|
|
||||||
register: bootstrap_alpine_bootloader_result
|
|
||||||
changed_when: bootstrap_alpine_bootloader_result.rc == 0
|
|
||||||
@@ -55,13 +55,21 @@
|
|||||||
register: bootstrap_debian_update_result
|
register: bootstrap_debian_update_result
|
||||||
changed_when: bootstrap_debian_update_result.rc == 0
|
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
|
- name: Install extra packages
|
||||||
when: bootstrap_debian_extra_args | trim | length > 0
|
when: bootstrap_debian_extra_args | trim | length > 0
|
||||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"
|
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"
|
||||||
register: bootstrap_debian_extra_result
|
register: bootstrap_debian_extra_result
|
||||||
changed_when: bootstrap_debian_extra_result.rc == 0
|
changed_when: bootstrap_debian_extra_result.rc == 0
|
||||||
|
|
||||||
|
# Printing (libcups2) and mDNS (libavahi*) are needed by a desktop session,
|
||||||
|
# so keep them when a desktop is requested.
|
||||||
- name: Remove unnecessary packages
|
- name: Remove unnecessary packages
|
||||||
|
when: not (system_cfg.features.desktop.enabled | bool)
|
||||||
ansible.builtin.command: "{{ chroot_command }} apt remove -y libcups2 libavahi-common3 libavahi-common-data"
|
ansible.builtin.command: "{{ chroot_command }} apt remove -y libcups2 libavahi-common3 libavahi-common-data"
|
||||||
register: bootstrap_debian_remove_result
|
register: bootstrap_debian_remove_result
|
||||||
changed_when: bootstrap_debian_remove_result.rc == 0
|
changed_when: bootstrap_debian_remove_result.rc == 0
|
||||||
|
|||||||
@@ -34,6 +34,17 @@
|
|||||||
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
|
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
|
||||||
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
|
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
|
||||||
|
|
||||||
|
- name: Install hardware-matched firmware/microcode/GPU/peripheral packages
|
||||||
|
when: >-
|
||||||
|
(system_cfg.features.firmware.enabled | bool)
|
||||||
|
or (system_cfg.features.gpu.enabled | bool)
|
||||||
|
or (system_cfg.features.peripherals.enabled | bool)
|
||||||
|
ansible.builtin.include_tasks: _hardware.yml
|
||||||
|
|
||||||
|
- 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
|
- name: Ensure chroot uses live environment DNS
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
src: /run/NetworkManager/resolv.conf
|
src: /run/NetworkManager/resolv.conf
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Bootstrap openSUSE
|
|
||||||
vars:
|
|
||||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
|
||||||
_base_patterns: "{{ _config.base | join(' ') }}"
|
|
||||||
_extra_packages: >-
|
|
||||||
{{
|
|
||||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
|
||||||
| reject('equalto', '')
|
|
||||||
| join(' ')
|
|
||||||
}}
|
|
||||||
block:
|
|
||||||
- name: Install openSUSE base patterns
|
|
||||||
ansible.builtin.command: >
|
|
||||||
zypper --root /mnt --non-interactive install -t pattern {{ _base_patterns }}
|
|
||||||
register: bootstrap_opensuse_base_result
|
|
||||||
changed_when: bootstrap_opensuse_base_result.rc == 0
|
|
||||||
|
|
||||||
- name: Install extra packages
|
|
||||||
when: _extra_packages | trim | length > 0
|
|
||||||
ansible.builtin.command: >
|
|
||||||
zypper --root /mnt --non-interactive install {{ _extra_packages }}
|
|
||||||
register: bootstrap_opensuse_extra_result
|
|
||||||
changed_when: bootstrap_opensuse_extra_result.rc == 0
|
|
||||||
|
|
||||||
- name: Install bootloader
|
|
||||||
ansible.builtin.command: >
|
|
||||||
zypper --root /mnt --non-interactive install grub2 grub2-efi efibootmgr
|
|
||||||
register: bootstrap_opensuse_bootloader_result
|
|
||||||
changed_when: bootstrap_opensuse_bootloader_result.rc == 0
|
|
||||||
@@ -54,6 +54,11 @@
|
|||||||
register: bootstrap_ubuntu_update_result
|
register: bootstrap_ubuntu_update_result
|
||||||
changed_when: bootstrap_ubuntu_update_result.rc == 0
|
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
|
- name: Install extra packages
|
||||||
when: bootstrap_ubuntu_extra_args | trim | length > 0
|
when: bootstrap_ubuntu_extra_args | trim | length > 0
|
||||||
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"
|
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Bootstrap Void Linux
|
|
||||||
vars:
|
|
||||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
|
||||||
_base_packages: "{{ _config.base | join(' ') }}"
|
|
||||||
_extra_packages: >-
|
|
||||||
{{
|
|
||||||
((_config.extra | default([])) + (_config.conditional | default([])))
|
|
||||||
| reject('equalto', '')
|
|
||||||
| join(' ')
|
|
||||||
}}
|
|
||||||
block:
|
|
||||||
- name: Install Void Linux base
|
|
||||||
ansible.builtin.command: >
|
|
||||||
xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current {{ _base_packages }}
|
|
||||||
register: bootstrap_void_base_result
|
|
||||||
changed_when: bootstrap_void_base_result.rc == 0
|
|
||||||
|
|
||||||
- name: Install extra packages
|
|
||||||
when: _extra_packages | trim | length > 0
|
|
||||||
ansible.builtin.command: >
|
|
||||||
xbps-install -Su -r /mnt {{ _extra_packages }}
|
|
||||||
register: bootstrap_void_extra_result
|
|
||||||
changed_when: bootstrap_void_extra_result.rc == 0
|
|
||||||
|
|
||||||
- name: Install bootloader
|
|
||||||
ansible.builtin.command: >
|
|
||||||
xbps-install -Sy -r /mnt grub-x86_64-efi efibootmgr
|
|
||||||
register: bootstrap_void_bootloader_result
|
|
||||||
changed_when: bootstrap_void_bootloader_result.rc == 0
|
|
||||||
171
roles/bootstrap/vars/desktop.yml
Normal file
171
roles/bootstrap/vars/desktop.yml
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
---
|
||||||
|
# Per-family desktop environment package definitions.
|
||||||
|
# Keyed by os_family -> environment -> groups (dnf group install) / packages.
|
||||||
|
# Wayland only: gnome, kde, sway, hyprland. No X11/xorg-server, no X11-only DEs.
|
||||||
|
# DE sets carry the session bits + the DE-specific xdg portal backend; the
|
||||||
|
# shared base below (fonts/audio/portal core/power/viewer apps) is layered on
|
||||||
|
# top for every DE via bootstrap_desktop_base_packages.
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
sway:
|
||||||
|
groups: []
|
||||||
|
packages:
|
||||||
|
- sway
|
||||||
|
- waybar
|
||||||
|
- foot
|
||||||
|
- wofi
|
||||||
|
- nautilus
|
||||||
|
- greetd
|
||||||
|
- greetd-tuigreet
|
||||||
|
- xdg-user-dirs
|
||||||
|
- xdg-desktop-portal-wlr
|
||||||
|
- polkit-gnome
|
||||||
|
- bluez
|
||||||
|
hyprland:
|
||||||
|
groups: []
|
||||||
|
packages:
|
||||||
|
- hyprland
|
||||||
|
- kitty
|
||||||
|
- wofi
|
||||||
|
- waybar
|
||||||
|
- nautilus
|
||||||
|
- greetd
|
||||||
|
- greetd-tuigreet
|
||||||
|
- xdg-user-dirs
|
||||||
|
- xdg-desktop-portal-hyprland
|
||||||
|
- polkit-kde-agent
|
||||||
|
- qt5-wayland
|
||||||
|
- qt6-wayland
|
||||||
|
- bluez
|
||||||
|
|
||||||
|
# Shared desktop base, installed for EVERY DE whenever desktop.enabled.
|
||||||
|
# Fonts (noto + emoji + one nerd font), audio stack (pipewire + wireplumber +
|
||||||
|
# pipewire-pulse), xdg portal core, power-profiles-daemon, and viewer-only base
|
||||||
|
# apps (browser, PDF/image/video viewers). DE metas (gnome/plasma) bundle their
|
||||||
|
# own file manager + settings, so no file manager is added here - the wlroots
|
||||||
|
# DE sets above carry their own (nautilus).
|
||||||
|
bootstrap_desktop_base_packages:
|
||||||
|
RedHat:
|
||||||
|
- google-noto-sans-fonts
|
||||||
|
- google-noto-emoji-fonts
|
||||||
|
- fira-code-fonts
|
||||||
|
- pipewire
|
||||||
|
- wireplumber
|
||||||
|
- pipewire-pulseaudio
|
||||||
|
- xdg-desktop-portal
|
||||||
|
- power-profiles-daemon
|
||||||
|
- firefox
|
||||||
|
- evince
|
||||||
|
- eog
|
||||||
|
- mpv
|
||||||
|
Debian:
|
||||||
|
- fonts-noto
|
||||||
|
- fonts-noto-color-emoji
|
||||||
|
- fonts-firacode
|
||||||
|
- pipewire
|
||||||
|
- wireplumber
|
||||||
|
- pipewire-pulse
|
||||||
|
- xdg-desktop-portal
|
||||||
|
- power-profiles-daemon
|
||||||
|
- firefox-esr
|
||||||
|
- evince
|
||||||
|
- eog
|
||||||
|
- mpv
|
||||||
|
Archlinux:
|
||||||
|
- noto-fonts
|
||||||
|
- noto-fonts-emoji
|
||||||
|
- ttf-nerd-fonts-symbols
|
||||||
|
- pipewire
|
||||||
|
- wireplumber
|
||||||
|
- pipewire-pulse
|
||||||
|
- xdg-desktop-portal
|
||||||
|
- power-profiles-daemon
|
||||||
|
- firefox
|
||||||
|
- evince
|
||||||
|
- loupe
|
||||||
|
- mpv
|
||||||
|
|
||||||
|
# Opt-in package groups, selected per host via features.desktop.groups (a list
|
||||||
|
# of group names). _desktop.yml installs the union of the requested groups'
|
||||||
|
# packages. Empty selection by default.
|
||||||
|
desktop_package_groups:
|
||||||
|
dev:
|
||||||
|
RedHat:
|
||||||
|
- git
|
||||||
|
- "@development-tools"
|
||||||
|
- neovim
|
||||||
|
- python3-pip
|
||||||
|
Debian:
|
||||||
|
- git
|
||||||
|
- build-essential
|
||||||
|
- neovim
|
||||||
|
- python3-pip
|
||||||
|
Archlinux:
|
||||||
|
- git
|
||||||
|
- base-devel
|
||||||
|
- neovim
|
||||||
|
- python-pip
|
||||||
108
roles/bootstrap/vars/hardware.yml
Normal file
108
roles/bootstrap/vars/hardware.yml
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
# Hardware-aware package definitions keyed by os_family. Schema:
|
||||||
|
# cpu_microcode[intel|amd] CPU vendor microcode
|
||||||
|
# firmware_base unconditional firmware packages
|
||||||
|
# firmware[<vendor>] vendor-split firmware (intel|amd|nvidia|
|
||||||
|
# atheros|broadcom|mediatek|marvell|realtek|
|
||||||
|
# qcom|cirrus|other)
|
||||||
|
# gpu_base mesa + vulkan loader
|
||||||
|
# gpu[intel|amd] per-GPU userspace
|
||||||
|
# gpu_nvidia[open|proprietary|nouveau] nvidia driver flavor
|
||||||
|
# peripherals_base webcam/scanner stack
|
||||||
|
# peripherals_fingerprint fprintd + libfprint
|
||||||
|
# peripherals_displaylink evdi kernel module + DisplayLink helpers
|
||||||
|
# Only packages matching detected hardware are installed; families without
|
||||||
|
# vendor splits collapse to a single firmware meta package.
|
||||||
|
bootstrap_hardware_packages:
|
||||||
|
Archlinux:
|
||||||
|
cpu_microcode:
|
||||||
|
intel: [intel-ucode]
|
||||||
|
amd: [amd-ucode]
|
||||||
|
firmware_base: []
|
||||||
|
firmware:
|
||||||
|
intel: [linux-firmware-other] # iwlwifi + i915 firmware live here
|
||||||
|
amd: [linux-firmware-amdgpu]
|
||||||
|
nvidia: [linux-firmware-nvidia]
|
||||||
|
atheros: [linux-firmware-atheros]
|
||||||
|
broadcom: [linux-firmware-broadcom]
|
||||||
|
mediatek: [linux-firmware-mediatek]
|
||||||
|
marvell: [linux-firmware-marvell]
|
||||||
|
realtek: [linux-firmware-realtek]
|
||||||
|
qcom: [linux-firmware-qcom]
|
||||||
|
cirrus: [linux-firmware-cirrus]
|
||||||
|
other: [linux-firmware-other]
|
||||||
|
gpu_base: [mesa, vulkan-icd-loader]
|
||||||
|
gpu:
|
||||||
|
intel: [vulkan-intel, intel-media-driver]
|
||||||
|
amd: [vulkan-radeon, libva-mesa-driver]
|
||||||
|
gpu_nvidia:
|
||||||
|
open: [nvidia-open-dkms, nvidia-utils]
|
||||||
|
proprietary: [nvidia-dkms, nvidia-utils]
|
||||||
|
# Wayland-only: kernel nouveau module + mesa/gbm drive the display; no Xorg DDX.
|
||||||
|
nouveau: [vulkan-nouveau]
|
||||||
|
peripherals_base: [v4l-utils]
|
||||||
|
peripherals_fingerprint: [fprintd, libfprint]
|
||||||
|
peripherals_displaylink: [] # AUR only; user must wire in AUR helper
|
||||||
|
|
||||||
|
Debian:
|
||||||
|
cpu_microcode:
|
||||||
|
intel: [intel-microcode]
|
||||||
|
amd: [amd64-microcode]
|
||||||
|
firmware_base: [firmware-linux-free]
|
||||||
|
firmware:
|
||||||
|
intel: [firmware-iwlwifi, firmware-misc-nonfree]
|
||||||
|
amd: [firmware-amd-graphics, firmware-misc-nonfree]
|
||||||
|
nvidia: [firmware-misc-nonfree]
|
||||||
|
atheros: [firmware-atheros]
|
||||||
|
broadcom: [firmware-brcm80211]
|
||||||
|
mediatek: [firmware-misc-nonfree]
|
||||||
|
marvell: [firmware-misc-nonfree]
|
||||||
|
realtek: [firmware-realtek]
|
||||||
|
qcom: [firmware-misc-nonfree]
|
||||||
|
cirrus: [firmware-misc-nonfree]
|
||||||
|
other: [firmware-misc-nonfree]
|
||||||
|
gpu_base: [mesa-vulkan-drivers, libgl1-mesa-dri]
|
||||||
|
gpu:
|
||||||
|
intel: [intel-media-va-driver, i965-va-driver]
|
||||||
|
amd: [libva-glx2, mesa-va-drivers]
|
||||||
|
gpu_nvidia:
|
||||||
|
# Debian trixie+ ships nvidia-open-kernel-dkms; older releases only have
|
||||||
|
# the proprietary nvidia-driver. Both come from the non-free component.
|
||||||
|
open: [nvidia-open-kernel-dkms, nvidia-driver, nvidia-vulkan-icd]
|
||||||
|
proprietary: [nvidia-driver, nvidia-vulkan-icd]
|
||||||
|
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
|
||||||
|
nouveau: []
|
||||||
|
peripherals_base: [v4l-utils]
|
||||||
|
peripherals_fingerprint: [fprintd, libpam-fprintd]
|
||||||
|
peripherals_displaylink: [evdi-dkms] # userspace driver still needs vendor .run
|
||||||
|
|
||||||
|
RedHat:
|
||||||
|
cpu_microcode:
|
||||||
|
intel: [microcode_ctl]
|
||||||
|
amd: [microcode_ctl]
|
||||||
|
firmware_base: [linux-firmware]
|
||||||
|
firmware:
|
||||||
|
intel: []
|
||||||
|
amd: []
|
||||||
|
nvidia: []
|
||||||
|
atheros: []
|
||||||
|
broadcom: []
|
||||||
|
mediatek: []
|
||||||
|
marvell: []
|
||||||
|
realtek: []
|
||||||
|
qcom: []
|
||||||
|
cirrus: []
|
||||||
|
other: []
|
||||||
|
gpu_base: [mesa-dri-drivers, mesa-vulkan-drivers, vulkan-loader]
|
||||||
|
gpu:
|
||||||
|
intel: [intel-media-driver, libva-intel-driver]
|
||||||
|
amd: [mesa-va-drivers]
|
||||||
|
gpu_nvidia:
|
||||||
|
# akmod packages from RPMFusion non-free; repo enabled by _hardware.yml.
|
||||||
|
open: [akmod-nvidia-open, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda]
|
||||||
|
proprietary: [akmod-nvidia, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda]
|
||||||
|
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
|
||||||
|
nouveau: []
|
||||||
|
peripherals_base: [v4l-utils]
|
||||||
|
peripherals_fingerprint: [fprintd, fprintd-pam]
|
||||||
|
peripherals_displaylink: [evdi] # COPR-supplied; repo enablement deferred
|
||||||
@@ -201,6 +201,7 @@ bootstrap_debian:
|
|||||||
- lrzsz
|
- lrzsz
|
||||||
- mtr
|
- mtr
|
||||||
- ncdu
|
- ncdu
|
||||||
|
- needrestart
|
||||||
- net-tools
|
- net-tools
|
||||||
- network-manager
|
- network-manager
|
||||||
- python-is-python3
|
- python-is-python3
|
||||||
@@ -221,6 +222,7 @@ bootstrap_debian:
|
|||||||
+ (['software-properties-common'] if (os_version | string) not in ['13', 'unstable'] 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 [])
|
+ (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else [])
|
||||||
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
|
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||||
|
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||||
+ bootstrap_common_conditional
|
+ bootstrap_common_conditional
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -262,6 +264,7 @@ bootstrap_ubuntu:
|
|||||||
- mtr
|
- mtr
|
||||||
- ncdu
|
- ncdu
|
||||||
- ncurses-term
|
- ncurses-term
|
||||||
|
- needrestart
|
||||||
- net-tools
|
- net-tools
|
||||||
- network-manager
|
- network-manager
|
||||||
- python-is-python3
|
- python-is-python3
|
||||||
@@ -283,6 +286,7 @@ bootstrap_ubuntu:
|
|||||||
conditional: >-
|
conditional: >-
|
||||||
{{
|
{{
|
||||||
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
|
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
|
||||||
|
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||||
+ bootstrap_common_conditional
|
+ bootstrap_common_conditional
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -321,75 +325,6 @@ bootstrap_archlinux:
|
|||||||
{{
|
{{
|
||||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||||
+ (['iptables-nft'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.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_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_opensuse:
|
|
||||||
base:
|
|
||||||
- patterns-base-base
|
|
||||||
extra:
|
|
||||||
- btrfs-progs
|
|
||||||
- chrony
|
|
||||||
- curl
|
|
||||||
- e2fsprogs
|
|
||||||
- glibc-locale
|
|
||||||
- kernel-default
|
|
||||||
- logrotate
|
|
||||||
- lvm2
|
|
||||||
- NetworkManager
|
|
||||||
- python3
|
|
||||||
- rsync
|
|
||||||
- sudo
|
|
||||||
- vim
|
|
||||||
- xfsprogs
|
|
||||||
conditional: >-
|
|
||||||
{{
|
|
||||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
|
||||||
+ bootstrap_common_conditional
|
|
||||||
}}
|
|
||||||
|
|
||||||
bootstrap_void:
|
|
||||||
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
|
|
||||||
}}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
# User-facing API: override via top-level `cis` dict in inventory.
|
# User-facing API: override via top-level `cis` dict in inventory.
|
||||||
# Merged with these defaults in _normalize.yml → cis_cfg.
|
# Merged with these defaults in _normalize.yml -> cis_cfg.
|
||||||
cis_defaults:
|
cis_defaults:
|
||||||
modules_blacklist:
|
modules_blacklist:
|
||||||
- freevxfs
|
- freevxfs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
- name: Disable Kernel Modules
|
- name: Disable Kernel Modules
|
||||||
vars:
|
vars:
|
||||||
# Ubuntu uses squashfs for snap packages — blacklisting it breaks snap entirely
|
# Ubuntu uses squashfs for snap packages - blacklisting it breaks snap entirely
|
||||||
cis_modules_squashfs: "{{ [] if os in ['ubuntu', 'ubuntu-lts'] else ['squashfs'] }}"
|
cis_modules_squashfs: "{{ [] if os in ['ubuntu', 'ubuntu-lts'] else ['squashfs'] }}"
|
||||||
cis_modules_all: "{{ cis_cfg.modules_blacklist + cis_modules_squashfs }}"
|
cis_modules_all: "{{ cis_cfg.modules_blacklist + cis_modules_squashfs }}"
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
- name: Initialize cleaned VM XML
|
- name: Initialize cleaned VM XML
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}"
|
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}"
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Remove boot ISO device from VM XML (source match)
|
- name: Remove boot ISO device from VM XML (source match)
|
||||||
when: boot_iso is defined and boot_iso | length > 0
|
when: boot_iso is defined and boot_iso | length > 0
|
||||||
@@ -28,7 +27,6 @@
|
|||||||
when: boot_iso is defined and boot_iso | length > 0
|
when: boot_iso is defined and boot_iso | length > 0
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot_source.xmlstring }}"
|
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 boot ISO device from VM XML (target fallback)
|
||||||
community.general.xml:
|
community.general.xml:
|
||||||
@@ -40,7 +38,6 @@
|
|||||||
- name: Update cleaned VM XML after removing boot ISO
|
- name: Update cleaned VM XML after removing boot ISO
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
|
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Remove cloud-init ISO device from VM XML (source match)
|
- name: Remove cloud-init ISO device from VM XML (source match)
|
||||||
community.general.xml:
|
community.general.xml:
|
||||||
@@ -52,7 +49,6 @@
|
|||||||
- name: Update cleaned VM XML after removing cloud-init ISO source match
|
- name: Update cleaned VM XML after removing cloud-init ISO source match
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit_source.xmlstring }}"
|
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit_source.xmlstring }}"
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Remove cloud-init ISO device from VM XML (target fallback)
|
- name: Remove cloud-init ISO device from VM XML (target fallback)
|
||||||
community.general.xml:
|
community.general.xml:
|
||||||
@@ -64,7 +60,6 @@
|
|||||||
- name: Update cleaned VM XML after removing cloud-init ISO
|
- name: Update cleaned VM XML after removing cloud-init ISO
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
|
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Strip XML declaration for libvirt define
|
- name: Strip XML declaration for libvirt define
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
@@ -76,7 +71,12 @@
|
|||||||
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
|
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
|
||||||
| trim
|
| trim
|
||||||
}}
|
}}
|
||||||
changed_when: false
|
|
||||||
|
- name: Ensure boot device is set to hard disk in VM XML
|
||||||
|
when: "'<boot ' not in cleanup_libvirt_domain_xml_clean"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cleanup_libvirt_domain_xml_clean: >-
|
||||||
|
{{ cleanup_libvirt_domain_xml_clean | regex_replace('(</type>)', '\1\n <boot dev="hd"/>') }}
|
||||||
|
|
||||||
- name: Update VM definition without installer media
|
- name: Update VM definition without installer media
|
||||||
community.libvirt.virt:
|
community.libvirt.virt:
|
||||||
@@ -94,6 +94,35 @@
|
|||||||
state: destroyed
|
state: destroyed
|
||||||
failed_when: false
|
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
|
- name: Start the VM
|
||||||
community.libvirt.virt:
|
community.libvirt.virt:
|
||||||
name: "{{ hostname }}"
|
name: "{{ hostname }}"
|
||||||
|
|||||||
@@ -25,3 +25,4 @@
|
|||||||
community.proxmox.proxmox_kvm:
|
community.proxmox.proxmox_kvm:
|
||||||
vmid: "{{ system_cfg.id }}"
|
vmid: "{{ system_cfg.id }}"
|
||||||
state: restarted
|
state: restarted
|
||||||
|
no_log: true
|
||||||
|
|||||||
@@ -7,34 +7,11 @@
|
|||||||
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
|
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
|
||||||
block:
|
block:
|
||||||
- name: Ensure Xen disk definitions exist
|
- name: Ensure Xen disk definitions exist
|
||||||
when: virtualization_xen_disks is not defined
|
ansible.builtin.include_tasks: ../../virtualization/tasks/_xen_disks.yml
|
||||||
ansible.builtin.set_fact:
|
|
||||||
cleanup_xen_disks: "{{ cleanup_xen_disks | default([]) + [cleanup_xen_disk_cfg] }}"
|
|
||||||
vars:
|
|
||||||
device_letter_map: "{{ disk_letter_map }}"
|
|
||||||
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
|
|
||||||
cleanup_xen_disk_cfg: >-
|
|
||||||
{{
|
|
||||||
{
|
|
||||||
'path': (
|
|
||||||
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
|
|
||||||
if ansible_loop.index0 == 0
|
|
||||||
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
|
|
||||||
),
|
|
||||||
'target': 'xvd' ~ device_letter,
|
|
||||||
'size': (item.size | float)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
loop: "{{ system_cfg.disks }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item | to_json }}"
|
|
||||||
extended: true
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Render Xen VM configuration without installer media
|
- name: Render Xen VM configuration without installer media
|
||||||
vars:
|
vars:
|
||||||
xen_installer_media_enabled: false
|
xen_installer_media_enabled: false
|
||||||
virtualization_xen_disks: "{{ virtualization_xen_disks | default(cleanup_xen_disks | default([])) }}"
|
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: xen.cfg.j2
|
src: xen.cfg.j2
|
||||||
dest: /tmp/xen-{{ hostname }}.cfg
|
dest: /tmp/xen-{{ hostname }}.cfg
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
---
|
---
|
||||||
# Network configuration dispatch — maps OS name to the task file
|
# Network configuration dispatch - maps OS name to the task file
|
||||||
# that writes network config. Default (NetworkManager) applies to
|
# that writes network config. Default (NetworkManager) applies to
|
||||||
# all OSes not explicitly listed.
|
# all OSes not explicitly listed.
|
||||||
configuration_network_task_map:
|
configuration_network_task_map: {}
|
||||||
alpine: network_alpine.yml
|
|
||||||
void: network_void.yml
|
|
||||||
|
|||||||
@@ -14,3 +14,12 @@
|
|||||||
- name: Set platform configuration
|
- name: Set platform configuration
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_configuration_platform: "{{ configuration_platform_config[os_family] }}"
|
_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'}) }}
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
register: configuration_efi_entry_result
|
register: configuration_efi_entry_result
|
||||||
changed_when: configuration_efi_entry_result.rc == 0
|
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
|
- name: Ensure lvm2 for non btrfs filesystems
|
||||||
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
|
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
@@ -48,16 +58,62 @@
|
|||||||
register: configuration_initramfs_result
|
register: configuration_initramfs_result
|
||||||
changed_when: configuration_initramfs_result.rc == 0
|
changed_when: configuration_initramfs_result.rc == 0
|
||||||
|
|
||||||
- name: Generate grub config
|
- name: Generate grub config (RedHat)
|
||||||
vars:
|
when: os_family == 'RedHat'
|
||||||
configuration_grub_cfg_cmd: >-
|
ansible.builtin.command: >-
|
||||||
{{
|
{{ chroot_command }} /usr/sbin/{{ _configuration_platform.grub_mkconfig_prefix }}
|
||||||
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o '
|
-o /boot/grub2/grub.cfg
|
||||||
+ partitioning_efi_mountpoint
|
|
||||||
+ '/EFI/' + _efi_vendor + '/grub.cfg'
|
|
||||||
if os_family == 'RedHat'
|
|
||||||
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
|
|
||||||
}}
|
|
||||||
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"
|
|
||||||
register: configuration_grub_result
|
register: configuration_grub_result
|
||||||
changed_when: configuration_grub_result.rc == 0
|
changed_when: configuration_grub_result.rc == 0
|
||||||
|
|
||||||
|
- name: Fix btrfs BLS boot variable in grub config
|
||||||
|
when:
|
||||||
|
- os_family == 'RedHat'
|
||||||
|
- system_cfg.filesystem == 'btrfs'
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: /mnt/boot/grub2/grub.cfg
|
||||||
|
regexp: 'search --no-floppy --fs-uuid --set=boot \S+'
|
||||||
|
replace: 'set boot=$root'
|
||||||
|
|
||||||
|
- name: Create EFI grub.cfg wrapper for RedHat
|
||||||
|
when: os_family == 'RedHat'
|
||||||
|
vars:
|
||||||
|
_grub2_path: >-
|
||||||
|
{{
|
||||||
|
'/grub2'
|
||||||
|
if (partitioning_separate_boot | bool)
|
||||||
|
else ('/@/boot/grub2' if system_cfg.filesystem == 'btrfs' else '/boot/grub2')
|
||||||
|
}}
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
set -o pipefail
|
||||||
|
uuid=$(grep -m1 'search.*--set=root' /mnt/boot/grub2/grub.cfg | grep -oP '[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}')
|
||||||
|
cat > /mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grub.cfg <<GRUBEOF
|
||||||
|
search --no-floppy --fs-uuid --set=dev $uuid
|
||||||
|
set prefix=(\$dev){{ _grub2_path }}
|
||||||
|
export \$prefix
|
||||||
|
configfile \$prefix/grub.cfg
|
||||||
|
GRUBEOF
|
||||||
|
executable: /bin/bash
|
||||||
|
register: _grub_wrapper_result
|
||||||
|
changed_when: _grub_wrapper_result.rc == 0
|
||||||
|
|
||||||
|
- name: Generate grub config (non-RedHat)
|
||||||
|
when: os_family != 'RedHat'
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg"
|
||||||
|
register: configuration_grub_result
|
||||||
|
changed_when: configuration_grub_result.rc == 0
|
||||||
|
|
||||||
|
- name: Rebuild GRUB as standalone EFI for Secure Boot
|
||||||
|
when:
|
||||||
|
- system_cfg.features.secure_boot.enabled | default(false) | bool
|
||||||
|
- os == 'archlinux'
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
{{ chroot_command }} grub-mkstandalone
|
||||||
|
-d /usr/lib/grub/x86_64-efi
|
||||||
|
-O x86_64-efi
|
||||||
|
--disable-shim-lock
|
||||||
|
-o {{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grubx64.efi
|
||||||
|
boot/grub/grub.cfg=/boot/grub/grub.cfg
|
||||||
|
register: _grub_standalone_result
|
||||||
|
changed_when: _grub_standalone_result.rc == 0
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
block:
|
block:
|
||||||
- name: Set LUKS configuration facts
|
- name: Set LUKS configuration facts
|
||||||
vars:
|
vars:
|
||||||
luks_tpm2_pcrs: >-
|
_raw_pcrs: >-
|
||||||
{{
|
{{
|
||||||
(
|
(
|
||||||
system_cfg.luks.tpm2.pcrs
|
system_cfg.luks.tpm2.pcrs
|
||||||
@@ -20,6 +20,17 @@
|
|||||||
| regex_replace('\\s+', '')
|
| regex_replace('\\s+', '')
|
||||||
| regex_replace('^\\+|\\+$', '')
|
| 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:
|
ansible.builtin.set_fact:
|
||||||
configuration_luks_mapper_name: "{{ system_cfg.luks.mapper }}"
|
configuration_luks_mapper_name: "{{ system_cfg.luks.mapper }}"
|
||||||
configuration_luks_uuid: "{{ partitioning_luks_uuid | default('') }}"
|
configuration_luks_uuid: "{{ partitioning_luks_uuid | default('') }}"
|
||||||
@@ -36,6 +47,12 @@
|
|||||||
configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}"
|
configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}"
|
||||||
configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}"
|
configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}"
|
||||||
configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key"
|
configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key"
|
||||||
|
configuration_luks_tpm2_token_lib: >-
|
||||||
|
{{
|
||||||
|
'/usr/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
|
||||||
|
if os_family == 'Debian'
|
||||||
|
else '/usr/lib64/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
|
||||||
|
}}
|
||||||
|
|
||||||
- name: Validate LUKS UUID is available
|
- name: Validate LUKS UUID is available
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
@@ -51,8 +68,13 @@
|
|||||||
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
|
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Enroll TPM2 for LUKS
|
- name: Detect TPM2 unlock method
|
||||||
when: configuration_luks_auto_method == 'tpm2'
|
ansible.builtin.include_tasks: encryption/initramfs_detect.yml
|
||||||
|
|
||||||
|
- name: Enroll TPM2 via systemd-cryptenroll
|
||||||
|
when:
|
||||||
|
- configuration_luks_auto_method == 'tpm2'
|
||||||
|
- _tpm2_method | default('systemd-cryptenroll') == 'systemd-cryptenroll'
|
||||||
ansible.builtin.include_tasks: encryption/tpm2.yml
|
ansible.builtin.include_tasks: encryption/tpm2.yml
|
||||||
|
|
||||||
- name: Configure LUKS keyfile auto-decrypt
|
- name: Configure LUKS keyfile auto-decrypt
|
||||||
@@ -78,7 +100,7 @@
|
|||||||
}}
|
}}
|
||||||
luks_tpm2_option_list: >-
|
luks_tpm2_option_list: >-
|
||||||
{{
|
{{
|
||||||
(configuration_luks_auto_method == 'tpm2')
|
(configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll')
|
||||||
| ternary(
|
| ternary(
|
||||||
['tpm2-device=' + configuration_luks_tpm2_device]
|
['tpm2-device=' + configuration_luks_tpm2_device]
|
||||||
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||||
@@ -122,16 +144,16 @@
|
|||||||
path: /mnt{{ configuration_luks_keyfile_path }}
|
path: /mnt{{ configuration_luks_keyfile_path }}
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
|
- name: Configure initramfs for LUKS
|
||||||
|
ansible.builtin.include_tasks: encryption/initramfs.yml
|
||||||
|
|
||||||
- name: Configure crypttab
|
- name: Configure crypttab
|
||||||
ansible.builtin.include_tasks: encryption/crypttab.yml
|
ansible.builtin.include_tasks: encryption/crypttab.yml
|
||||||
|
|
||||||
- name: Configure initramfs
|
- name: Configure dracut for LUKS
|
||||||
ansible.builtin.include_tasks: encryption/initramfs.yml
|
when: _initramfs_generator | default('') == 'dracut'
|
||||||
|
|
||||||
- name: Configure dracut
|
|
||||||
when: os_family == 'RedHat'
|
|
||||||
ansible.builtin.include_tasks: encryption/dracut.yml
|
ansible.builtin.include_tasks: encryption/dracut.yml
|
||||||
|
|
||||||
- name: Configure GRUB for LUKS
|
- name: Configure GRUB for LUKS
|
||||||
when: not os_family == 'RedHat'
|
when: _initramfs_generator | default('') != 'dracut'
|
||||||
ansible.builtin.include_tasks: encryption/grub.yml
|
ansible.builtin.include_tasks: encryption/grub.yml
|
||||||
|
|||||||
@@ -9,48 +9,58 @@
|
|||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: /mnt/etc/dracut.conf.d/crypt.conf
|
dest: /mnt/etc/dracut.conf.d/crypt.conf
|
||||||
content: |
|
content: |
|
||||||
add_dracutmodules+=" crypt "
|
add_dracutmodules+=" crypt systemd "
|
||||||
{% if configuration_luks_keyfile_in_use %}
|
{% if configuration_luks_keyfile_in_use | default(false) %}
|
||||||
install_items+=" {{ configuration_luks_keyfile_path }} "
|
install_items+=" {{ configuration_luks_keyfile_path }} "
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if configuration_luks_auto_method == 'tpm2' %}
|
||||||
|
install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} "
|
||||||
|
{% endif %}
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
- name: Read kernel cmdline defaults
|
# --- Kernel cmdline: write rd.luks.* args for dracut ---
|
||||||
|
- name: Ensure kernel cmdline directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /mnt/etc/kernel
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Read existing kernel cmdline
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: /mnt/etc/kernel/cmdline
|
src: /mnt/etc/kernel/cmdline
|
||||||
register: configuration_kernel_cmdline_slurp
|
register: _kernel_cmdline_slurp
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
- name: Build kernel cmdline with LUKS args
|
- name: Build kernel cmdline with LUKS args
|
||||||
vars:
|
vars:
|
||||||
kernel_cmdline_current: >-
|
_cmdline_current: >-
|
||||||
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }}
|
{{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }}
|
||||||
kernel_cmdline_list: >-
|
_cmdline_list: >-
|
||||||
|
{{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }}
|
||||||
|
_cmdline_filtered: >-
|
||||||
{{
|
{{
|
||||||
kernel_cmdline_current.split()
|
_cmdline_list
|
||||||
if kernel_cmdline_current | length > 0 else []
|
|
||||||
}}
|
|
||||||
kernel_cmdline_filtered: >-
|
|
||||||
{{
|
|
||||||
kernel_cmdline_list
|
|
||||||
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
|
||||||
| list
|
| list
|
||||||
}}
|
}}
|
||||||
kernel_cmdline_new: >-
|
_cmdline_new: >-
|
||||||
{{
|
{{
|
||||||
(kernel_cmdline_filtered + configuration_luks_kernel_args.split())
|
(_cmdline_filtered + configuration_luks_kernel_args.split())
|
||||||
| unique
|
| unique
|
||||||
| join(' ')
|
| join(' ')
|
||||||
}}
|
}}
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}"
|
_dracut_kernel_cmdline: "{{ _cmdline_new }}"
|
||||||
|
|
||||||
- name: Write kernel cmdline with LUKS args
|
- name: Write kernel cmdline with LUKS args
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: /mnt/etc/kernel/cmdline
|
dest: /mnt/etc/kernel/cmdline
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
content: "{{ configuration_kernel_cmdline_new }}\n"
|
content: "{{ _dracut_kernel_cmdline }}\n"
|
||||||
|
|
||||||
|
# --- BLS entries: RedHat-specific ---
|
||||||
- name: Update BLS entries with LUKS kernel cmdline
|
- name: Update BLS entries with LUKS kernel cmdline
|
||||||
|
when: os_family == 'RedHat'
|
||||||
vars:
|
vars:
|
||||||
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}"
|
_bls_cmdline: "{{ _dracut_kernel_cmdline }}"
|
||||||
ansible.builtin.include_tasks: ../_bls_update.yml
|
ansible.builtin.include_tasks: ../_bls_update.yml
|
||||||
|
|||||||
@@ -1,8 +1,104 @@
|
|||||||
---
|
---
|
||||||
- name: Ensure keyfile pattern for initramfs-tools
|
# Initramfs configuration for LUKS auto-unlock.
|
||||||
|
# Runs AFTER Build LUKS parameters (so configuration_luks_keyfile_in_use is set).
|
||||||
|
# _initramfs_generator and _tpm2_method are set by initramfs_detect.yml.
|
||||||
|
|
||||||
|
# --- clevis: install and bind TPM2 ---
|
||||||
|
- name: Install clevis in target system
|
||||||
when:
|
when:
|
||||||
- os_family == 'Debian'
|
- configuration_luks_auto_method == 'tpm2'
|
||||||
- configuration_luks_keyfile_in_use
|
- _tpm2_method | default('') == 'clevis'
|
||||||
|
vars:
|
||||||
|
_clevis_install_cmd:
|
||||||
|
Debian: >-
|
||||||
|
{{ chroot_command }} apt install -y
|
||||||
|
clevis clevis-luks clevis-tpm2 clevis-initramfs tpm2-tools
|
||||||
|
RedHat: >-
|
||||||
|
{{ chroot_command }} dnf install -y
|
||||||
|
clevis clevis-luks clevis-systemd tpm2-tools
|
||||||
|
Archlinux: >-
|
||||||
|
{{ chroot_command }} pacman -S --noconfirm --needed
|
||||||
|
clevis tpm2-tools
|
||||||
|
ansible.builtin.command: "{{ _clevis_install_cmd[os_family] }}"
|
||||||
|
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:
|
ansible.builtin.lineinfile:
|
||||||
path: /mnt/etc/cryptsetup-initramfs/conf-hook
|
path: /mnt/etc/cryptsetup-initramfs/conf-hook
|
||||||
regexp: "^KEYFILE_PATTERN="
|
regexp: "^KEYFILE_PATTERN="
|
||||||
@@ -10,8 +106,9 @@
|
|||||||
create: true
|
create: true
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
|
# --- mkinitcpio: systemd + sd-encrypt hooks ---
|
||||||
- name: Configure mkinitcpio hooks for LUKS
|
- name: Configure mkinitcpio hooks for LUKS
|
||||||
when: os == 'archlinux'
|
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /mnt/etc/mkinitcpio.conf
|
path: /mnt/etc/mkinitcpio.conf
|
||||||
regexp: "^HOOKS="
|
regexp: "^HOOKS="
|
||||||
@@ -20,13 +117,13 @@
|
|||||||
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
|
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
|
||||||
|
|
||||||
- name: Read mkinitcpio configuration
|
- name: Read mkinitcpio configuration
|
||||||
when: os == 'archlinux'
|
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: /mnt/etc/mkinitcpio.conf
|
src: /mnt/etc/mkinitcpio.conf
|
||||||
register: configuration_mkinitcpio_slurp
|
register: configuration_mkinitcpio_slurp
|
||||||
|
|
||||||
- name: Build mkinitcpio FILES list
|
- name: Build mkinitcpio FILES list
|
||||||
when: os == 'archlinux'
|
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||||
vars:
|
vars:
|
||||||
mkinitcpio_files_list: >-
|
mkinitcpio_files_list: >-
|
||||||
{{
|
{{
|
||||||
@@ -42,7 +139,7 @@
|
|||||||
{{
|
{{
|
||||||
(
|
(
|
||||||
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
|
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
|
||||||
if configuration_luks_keyfile_in_use
|
if (configuration_luks_keyfile_in_use | default(false))
|
||||||
else (
|
else (
|
||||||
mkinitcpio_files_list
|
mkinitcpio_files_list
|
||||||
| reject('equalto', configuration_luks_keyfile_path)
|
| reject('equalto', configuration_luks_keyfile_path)
|
||||||
@@ -55,7 +152,7 @@
|
|||||||
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
|
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
|
||||||
|
|
||||||
- name: Configure mkinitcpio FILES list
|
- name: Configure mkinitcpio FILES list
|
||||||
when: os == 'archlinux'
|
when: _initramfs_generator | default('') == 'mkinitcpio'
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /mnt/etc/mkinitcpio.conf
|
path: /mnt/etc/mkinitcpio.conf
|
||||||
regexp: "^FILES="
|
regexp: "^FILES="
|
||||||
|
|||||||
98
roles/configuration/tasks/encryption/initramfs_detect.yml
Normal file
98
roles/configuration/tasks/encryption/initramfs_detect.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
# Resolve initramfs generator and TPM2 unlock method.
|
||||||
|
# Sets _initramfs_generator and _tpm2_method facts.
|
||||||
|
#
|
||||||
|
# Generator detection: derived from the platform's initramfs_cmd
|
||||||
|
# (dracut -> dracut, mkinitcpio -> mkinitcpio, else -> initramfs-tools)
|
||||||
|
# TPM2 method: systemd-cryptenroll when generator supports tpm2-device,
|
||||||
|
# clevis fallback otherwise. Non-native dracut installed automatically.
|
||||||
|
|
||||||
|
- name: Resolve initramfs generator
|
||||||
|
vars:
|
||||||
|
_user_generator: "{{ system_cfg.features.initramfs.generator | default('') }}"
|
||||||
|
_native_generator: >-
|
||||||
|
{{
|
||||||
|
'dracut' if _configuration_platform.initramfs_cmd is search('dracut')
|
||||||
|
else ('mkinitcpio' if _configuration_platform.initramfs_cmd is search('mkinitcpio')
|
||||||
|
else 'initramfs-tools')
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_initramfs_generator: >-
|
||||||
|
{{ _user_generator if _user_generator | length > 0 else _native_generator }}
|
||||||
|
_initramfs_native_generator: "{{ _native_generator }}"
|
||||||
|
|
||||||
|
# --- Install non-native dracut if overridden or needed ---
|
||||||
|
- name: Install dracut in chroot when not native
|
||||||
|
when:
|
||||||
|
- _initramfs_generator == 'dracut'
|
||||||
|
- _initramfs_native_generator != 'dracut'
|
||||||
|
ansible.builtin.shell: >-
|
||||||
|
{{ chroot_command }} sh -c '
|
||||||
|
command -v apt >/dev/null 2>&1 && apt install -y dracut ||
|
||||||
|
command -v pacman >/dev/null 2>&1 && pacman -S --noconfirm dracut ||
|
||||||
|
command -v dnf >/dev/null 2>&1 && dnf install -y dracut
|
||||||
|
'
|
||||||
|
register: _dracut_install_result
|
||||||
|
changed_when: _dracut_install_result.rc == 0
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Override initramfs command to dracut
|
||||||
|
when:
|
||||||
|
- _initramfs_generator == 'dracut'
|
||||||
|
- _initramfs_native_generator != 'dracut'
|
||||||
|
vars:
|
||||||
|
# Generate dracut initramfs with output name matching what GRUB expects:
|
||||||
|
# mkinitcpio native: /boot/initramfs-linux.img (Arch convention)
|
||||||
|
# initramfs-tools native: /boot/initrd.img-<kver> (Debian convention)
|
||||||
|
_dracut_cmd: >-
|
||||||
|
{{
|
||||||
|
'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initramfs-linux.img $kver; done"'
|
||||||
|
if _initramfs_native_generator == 'mkinitcpio'
|
||||||
|
else 'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"'
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_configuration_platform: >-
|
||||||
|
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
|
||||||
|
|
||||||
|
# --- TPM2 method detection ---
|
||||||
|
- name: Probe dracut for TPM2 module support
|
||||||
|
when:
|
||||||
|
- configuration_luks_auto_method == 'tpm2'
|
||||||
|
- _initramfs_generator != 'mkinitcpio'
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} dracut --list-modules"
|
||||||
|
register: _dracut_modules_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Resolve TPM2 unlock method
|
||||||
|
when: configuration_luks_auto_method == 'tpm2'
|
||||||
|
vars:
|
||||||
|
# mkinitcpio sd-encrypt supports tpm2-device natively
|
||||||
|
# dracut with tpm2-tss module supports tpm2-device natively
|
||||||
|
# everything else needs clevis
|
||||||
|
_supports_tpm2_native: >-
|
||||||
|
{{
|
||||||
|
_initramfs_generator == 'mkinitcpio'
|
||||||
|
or ('tpm2-tss' in (_dracut_modules_check.stdout | default('')))
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_tpm2_method: "{{ 'systemd-cryptenroll' if _supports_tpm2_native | bool else 'clevis' }}"
|
||||||
|
|
||||||
|
# --- Auto-upgrade to dracut when tpm2-tss available but generator isn't dracut ---
|
||||||
|
- name: Switch to dracut for TPM2 support
|
||||||
|
when:
|
||||||
|
- configuration_luks_auto_method == 'tpm2'
|
||||||
|
- _tpm2_method == 'systemd-cryptenroll'
|
||||||
|
- _initramfs_generator not in ['dracut', 'mkinitcpio']
|
||||||
|
vars:
|
||||||
|
_dracut_cmd: >-
|
||||||
|
bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_initramfs_generator: dracut
|
||||||
|
_configuration_platform: >-
|
||||||
|
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
|
||||||
|
|
||||||
|
- name: Report TPM2 configuration
|
||||||
|
when: configuration_luks_auto_method == 'tpm2'
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "TPM2 unlock: {{ _tpm2_method | default('none') }} | initramfs: {{ _initramfs_generator }}"
|
||||||
@@ -86,7 +86,6 @@
|
|||||||
device: "{{ configuration_luks_device }}"
|
device: "{{ configuration_luks_device }}"
|
||||||
passphrase: "{{ configuration_luks_passphrase }}"
|
passphrase: "{{ configuration_luks_passphrase }}"
|
||||||
new_keyfile: "/mnt{{ configuration_luks_keyfile_path }}"
|
new_keyfile: "/mnt{{ configuration_luks_keyfile_path }}"
|
||||||
register: configuration_luks_addkey_retry
|
|
||||||
failed_when: false
|
failed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
@@ -108,7 +107,7 @@
|
|||||||
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
|
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: >-
|
msg: >-
|
||||||
LUKS keyfile enrollment failed — falling back to manual unlock at boot.
|
LUKS keyfile enrollment failed - falling back to manual unlock at boot.
|
||||||
The system will prompt for the LUKS passphrase during startup.
|
The system will prompt for the LUKS passphrase during startup.
|
||||||
|
|
||||||
- name: Fallback to manual LUKS unlock if keyfile enrollment failed
|
- name: Fallback to manual LUKS unlock if keyfile enrollment failed
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
---
|
---
|
||||||
|
# TPM2 enrollment via systemd-cryptenroll.
|
||||||
|
# Works with dracut and mkinitcpio (sd-encrypt). The user-set passphrase
|
||||||
|
# remains as a backup unlock method - no auto-generated keyfiles.
|
||||||
- name: Enroll TPM2 for LUKS
|
- name: Enroll TPM2 for LUKS
|
||||||
block:
|
block:
|
||||||
# Tempfile in chroot /tmp — accessible by both chroot and host commands
|
|
||||||
- name: Create temporary passphrase file for TPM2 enrollment
|
- name: Create temporary passphrase file for TPM2 enrollment
|
||||||
ansible.builtin.tempfile:
|
ansible.builtin.tempfile:
|
||||||
path: /mnt/tmp
|
path: /mnt/root
|
||||||
prefix: luks-passphrase-
|
prefix: luks-passphrase-
|
||||||
state: file
|
state: file
|
||||||
register: configuration_luks_tpm2_passphrase_tempfile
|
register: _tpm2_passphrase_tempfile
|
||||||
|
|
||||||
- name: Write passphrase into temporary file for TPM2 enrollment
|
- name: Write passphrase into temporary file
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
|
dest: "{{ _tpm2_passphrase_tempfile.path }}"
|
||||||
content: "{{ configuration_luks_passphrase }}"
|
content: "{{ configuration_luks_passphrase }}"
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0600"
|
mode: "0600"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Enroll TPM2 token
|
- name: Ensure TPM device is accessible in chroot
|
||||||
|
ansible.builtin.shell: >-
|
||||||
|
ls /mnt/dev/tpmrm0 2>/dev/null
|
||||||
|
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Enroll TPM2 token via systemd-cryptenroll
|
||||||
vars:
|
vars:
|
||||||
configuration_luks_enroll_args: >-
|
_enroll_args: >-
|
||||||
{{
|
{{
|
||||||
[
|
[
|
||||||
'/usr/bin/systemd-cryptenroll',
|
'/usr/bin/systemd-cryptenroll',
|
||||||
@@ -28,70 +37,28 @@
|
|||||||
'--tpm2-with-pin=false',
|
'--tpm2-with-pin=false',
|
||||||
'--wipe-slot=tpm2',
|
'--wipe-slot=tpm2',
|
||||||
'--unlock-key-file=' + (
|
'--unlock-key-file=' + (
|
||||||
configuration_luks_tpm2_passphrase_tempfile.path
|
_tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '')
|
||||||
| regex_replace('^/mnt', '')
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
|
||||||
if configuration_luks_tpm2_pcrs | length > 0 else [])
|
if configuration_luks_tpm2_pcrs | length > 0 else [])
|
||||||
+ [configuration_luks_device]
|
+ [configuration_luks_device]
|
||||||
}}
|
}}
|
||||||
configuration_luks_enroll_chroot_cmd: >-
|
ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}"
|
||||||
{{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }}
|
register: _tpm2_enroll_result
|
||||||
ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}"
|
changed_when: _tpm2_enroll_result.rc == 0
|
||||||
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:
|
rescue:
|
||||||
- name: Warn about TPM2 enrollment failure
|
- name: TPM2 enrollment failed
|
||||||
ansible.builtin.fail:
|
ansible.builtin.debug:
|
||||||
msg: >-
|
msg: >-
|
||||||
WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt.
|
TPM2 enrollment failed: {{ _tpm2_enroll_result.stderr | default('unknown') }}.
|
||||||
The system will use a keyfile instead of TPM2 for automatic LUKS unlock.
|
The system will require the passphrase for LUKS unlock on boot.
|
||||||
ignore_errors: true
|
TPM2 can be enrolled post-deployment via: systemd-cryptenroll --tpm2-device=auto {{ configuration_luks_device }}
|
||||||
|
|
||||||
- name: Fallback to keyfile auto-decrypt
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
configuration_luks_auto_method: keyfile
|
|
||||||
always:
|
always:
|
||||||
- name: Remove TPM2 enrollment passphrase file
|
- name: Remove temporary passphrase file
|
||||||
when: configuration_luks_tpm2_passphrase_tempfile.path is defined
|
when: _tpm2_passphrase_tempfile.path is defined
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
|
path: "{{ _tpm2_passphrase_tempfile.path }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
set smartindent
|
set smartindent
|
||||||
set mouse=a
|
set mouse=a
|
||||||
insertafter: EOF
|
insertafter: EOF
|
||||||
marker: "# {mark} CUSTOM VIM CONFIG"
|
marker: "\" {mark} CUSTOM VIM CONFIG"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
# Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster
|
# Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
- name: Create zram config
|
- name: Create zram config
|
||||||
when:
|
when:
|
||||||
- (os != "debian" or (os_version | string) != "11") and os != "rhel"
|
- (os != "debian" or (os_version | string) != "11") and os != "rhel"
|
||||||
- os not in ["alpine", "void"]
|
|
||||||
- system_cfg.features.swap.enabled | bool
|
- system_cfg.features.swap.enabled | bool
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: /mnt/etc/systemd/zram-generator.conf
|
dest: /mnt/etc/systemd/zram-generator.conf
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
- file: encryption.yml
|
- file: encryption.yml
|
||||||
when: "{{ system_cfg.luks.enabled | bool }}"
|
when: "{{ system_cfg.luks.enabled | bool }}"
|
||||||
- file: bootloader.yml
|
- file: bootloader.yml
|
||||||
|
- file: secure_boot.yml
|
||||||
|
when: "{{ system_cfg.features.secure_boot.enabled | bool }}"
|
||||||
- file: extras.yml
|
- file: extras.yml
|
||||||
- file: network.yml
|
- file: network.yml
|
||||||
- file: users.yml
|
- file: users.yml
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Write Alpine network interfaces
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /mnt/etc/network/interfaces
|
|
||||||
mode: "0644"
|
|
||||||
content: |
|
|
||||||
auto lo
|
|
||||||
iface lo inet loopback
|
|
||||||
{% for iface in system_cfg.network.interfaces %}
|
|
||||||
{% set inv_name = iface.name | default('') | string %}
|
|
||||||
{% set det_name = configuration_detected_interfaces[loop.index0] | default('eth' ~ loop.index0) %}
|
|
||||||
{% set iface_name = inv_name if inv_name | length > 0 else det_name %}
|
|
||||||
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
|
|
||||||
|
|
||||||
auto {{ iface_name }}
|
|
||||||
iface {{ iface_name }} inet {{ 'static' if has_static else 'dhcp' }}
|
|
||||||
{% if has_static %}
|
|
||||||
address {{ iface.ip }}/{{ iface.prefix }}
|
|
||||||
{% if iface.gateway | default('') | string | length %}
|
|
||||||
gateway {{ iface.gateway }}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
- name: Set Alpine DNS resolvers
|
|
||||||
when: configuration_dns_list | length > 0 or configuration_dns_search | length > 0
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /mnt/etc/resolv.conf
|
|
||||||
mode: "0644"
|
|
||||||
content: |
|
|
||||||
{% if configuration_dns_search | length > 0 %}
|
|
||||||
search {{ configuration_dns_search | join(' ') }}
|
|
||||||
{% endif %}
|
|
||||||
{% for resolver in configuration_dns_list %}
|
|
||||||
nameserver {{ resolver }}
|
|
||||||
{% endfor %}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Write dhcpcd configuration
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /mnt/etc/dhcpcd.conf
|
|
||||||
mode: "0644"
|
|
||||||
content: |
|
|
||||||
{% for iface in system_cfg.network.interfaces %}
|
|
||||||
{% set inv_name = iface.name | default('') | string %}
|
|
||||||
{% set det_name = configuration_detected_interfaces[loop.index0] | default('eth' ~ loop.index0) %}
|
|
||||||
{% set iface_name = inv_name if inv_name | length > 0 else det_name %}
|
|
||||||
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
|
|
||||||
{% if has_static %}
|
|
||||||
interface {{ iface_name }}
|
|
||||||
static ip_address={{ iface.ip }}/{{ iface.prefix }}
|
|
||||||
{% if iface.gateway | default('') | string | length %}
|
|
||||||
static routers={{ iface.gateway }}
|
|
||||||
{% endif %}
|
|
||||||
{% if loop.index0 == 0 and configuration_dns_list | length > 0 %}
|
|
||||||
static domain_name_servers={{ configuration_dns_list | join(' ') }}
|
|
||||||
{% endif %}
|
|
||||||
{% if loop.index0 == 0 and configuration_dns_search | length > 0 %}
|
|
||||||
static domain_search={{ configuration_dns_search | join(' ') }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
19
roles/configuration/tasks/secure_boot.yml
Normal file
19
roles/configuration/tasks/secure_boot.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
- name: Validate Secure Boot is supported on this OS
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- os in ['archlinux', 'debian', 'ubuntu', 'ubuntu-lts',
|
||||||
|
'rhel', 'rocky', 'almalinux', 'fedora']
|
||||||
|
fail_msg: >-
|
||||||
|
Secure Boot is not supported on {{ os }} in this bootstrap. Supported:
|
||||||
|
Arch (sbctl) and Debian/Ubuntu/RHEL/Rocky/Alma/Fedora (shim). Disable
|
||||||
|
system.features.secure_boot.enabled or pick a supported OS.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- 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
|
||||||
115
roles/configuration/tasks/secure_boot/sbctl.yml
Normal file
115
roles/configuration/tasks/secure_boot/sbctl.yml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
- name: Configure sbctl Secure Boot
|
||||||
|
block:
|
||||||
|
- name: Create Secure Boot signing keys
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} sbctl create-keys"
|
||||||
|
register: _sbctl_create_keys
|
||||||
|
changed_when: _sbctl_create_keys.rc == 0
|
||||||
|
failed_when:
|
||||||
|
- _sbctl_create_keys.rc != 0
|
||||||
|
- "'already exists' not in (_sbctl_create_keys.stderr | default(''))"
|
||||||
|
|
||||||
|
- name: Enroll Secure Boot keys in firmware
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} sbctl enroll-keys --microsoft"
|
||||||
|
register: _sbctl_enroll
|
||||||
|
changed_when: _sbctl_enroll.rc == 0
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Install first-boot enrollment service if chroot enrollment failed
|
||||||
|
when: _sbctl_enroll.rc | default(1) != 0
|
||||||
|
block:
|
||||||
|
- name: Create first-boot sbctl enrollment service
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /mnt/etc/systemd/system/sbctl-enroll.service
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Enroll Secure Boot keys via sbctl
|
||||||
|
ConditionPathExists=!/var/lib/sbctl/.enrolled
|
||||||
|
After=local-fs.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/sbctl enroll-keys --microsoft
|
||||||
|
ExecStartPost=/usr/bin/touch /var/lib/sbctl/.enrolled
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
- name: Enable first-boot enrollment service
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} systemctl enable sbctl-enroll.service"
|
||||||
|
register: _sbctl_service_enable
|
||||||
|
changed_when: _sbctl_service_enable.rc == 0
|
||||||
|
|
||||||
|
- name: Find kernel images to sign
|
||||||
|
ansible.builtin.find:
|
||||||
|
paths: /mnt/boot
|
||||||
|
patterns: "vmlinuz-*"
|
||||||
|
file_type: file
|
||||||
|
register: _sbctl_kernel_images
|
||||||
|
|
||||||
|
- name: Sign kernel images
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
{{ chroot_command }} sbctl sign -s {{ item.path | regex_replace('^/mnt', '') }}
|
||||||
|
loop: "{{ _sbctl_kernel_images.files }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.path | basename }}"
|
||||||
|
register: _sbctl_sign_kernel
|
||||||
|
changed_when: _sbctl_sign_kernel.rc == 0
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Sign GRUB EFI binary
|
||||||
|
vars:
|
||||||
|
_grub_efi_path: "{{ partitioning_efi_mountpoint }}/EFI/archlinux/grubx64.efi"
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
{{ chroot_command }} sbctl sign -s {{ _grub_efi_path }}
|
||||||
|
register: _sbctl_sign_grub
|
||||||
|
changed_when: _sbctl_sign_grub.rc == 0
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Ensure pacman hooks directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /mnt/etc/pacman.d/hooks
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Install sbctl auto-signing pacman hook
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /mnt/etc/pacman.d/hooks/99-sbctl-sign.hook
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
[Trigger]
|
||||||
|
Operation = Install
|
||||||
|
Operation = Upgrade
|
||||||
|
Type = Path
|
||||||
|
Target = boot/vmlinuz-*
|
||||||
|
Target = usr/lib/modules/*/vmlinuz
|
||||||
|
|
||||||
|
[Action]
|
||||||
|
Description = Signing kernel images for Secure Boot...
|
||||||
|
When = PostTransaction
|
||||||
|
Exec = /usr/bin/sbctl sign-all
|
||||||
|
Depends = sbctl
|
||||||
|
|
||||||
|
- name: Verify sbctl signing status
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} sbctl verify"
|
||||||
|
register: _sbctl_verify
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Report sbctl Secure Boot status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
Secure Boot (sbctl):
|
||||||
|
Enrollment={{ 'done' if (_sbctl_enroll.rc | default(1)) == 0 else 'deferred to first boot' }}.
|
||||||
|
{{ _sbctl_verify.stdout | default('Verify not available') }}
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: Secure Boot setup failed
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
sbctl Secure Boot setup failed.
|
||||||
|
On VMs make sure the OVMF firmware is in Setup Mode (fresh NVRAM).
|
||||||
|
On bare metal enter the firmware setup and switch to Setup Mode first.
|
||||||
|
To recover manually: sbctl create-keys && sbctl enroll-keys --microsoft && sbctl sign-all
|
||||||
45
roles/configuration/tasks/secure_boot/shim.yml
Normal file
45
roles/configuration/tasks/secure_boot/shim.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
- name: Configure shim-based Secure Boot
|
||||||
|
vars:
|
||||||
|
_efi_vendor: >-
|
||||||
|
{{
|
||||||
|
"redhat" if os == "rhel"
|
||||||
|
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
|
||||||
|
}}
|
||||||
|
block:
|
||||||
|
- name: Find shim binary in target system
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: >-
|
||||||
|
set -o pipefail &&
|
||||||
|
{{ chroot_command }} find /usr/lib/shim /boot/efi/EFI
|
||||||
|
\( -name 'shimx64.efi.signed.latest' -o -name 'shimx64.efi.dualsigned'
|
||||||
|
-o -name 'shimx64.efi.signed' -o -name 'shimx64.efi' \)
|
||||||
|
-type f | sort -r | head -1
|
||||||
|
executable: /bin/bash
|
||||||
|
register: _shim_find_result
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Copy shim to EFI vendor directory
|
||||||
|
when:
|
||||||
|
- _shim_find_result.stdout | default('') | length > 0
|
||||||
|
- _configuration_platform.grub_install | bool
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
cp /mnt{{ _shim_find_result.stdout_lines | first }}
|
||||||
|
/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi
|
||||||
|
register: _shim_copy_result
|
||||||
|
changed_when: _shim_copy_result.rc == 0
|
||||||
|
|
||||||
|
- name: Verify shim is present
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi"
|
||||||
|
register: _shim_stat
|
||||||
|
|
||||||
|
- name: Report Secure Boot status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
Secure Boot (shim): {{
|
||||||
|
'shimx64.efi installed at ' ~ partitioning_efi_mountpoint ~ '/EFI/' ~ _efi_vendor
|
||||||
|
if (_shim_stat.stat.exists | default(false))
|
||||||
|
else 'shimx64.efi not found, shim package may handle placement on first boot'
|
||||||
|
}}
|
||||||
@@ -1,4 +1,31 @@
|
|||||||
---
|
---
|
||||||
|
- name: Resolve desktop facts
|
||||||
|
when: system_cfg.features.desktop.enabled | bool
|
||||||
|
vars:
|
||||||
|
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_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(''))
|
||||||
|
}}
|
||||||
|
_desktop_session: "{{ system_cfg.features.desktop.session | default('') }}"
|
||||||
|
# Explicit session wins, else the per-environment command. Single source of
|
||||||
|
# truth for the greetd assert, the config gate, and the template.
|
||||||
|
_greetd_session: >-
|
||||||
|
{{
|
||||||
|
system_cfg.features.desktop.session
|
||||||
|
if (system_cfg.features.desktop.session | default('') | length > 0)
|
||||||
|
else (configuration_desktop_session_cmd_map[system_cfg.features.desktop.environment] | default(''))
|
||||||
|
}}
|
||||||
|
_desktop_autologin_user: >-
|
||||||
|
{{
|
||||||
|
_autologin
|
||||||
|
if (_autologin | string | lower not in ['', 'false'] and _autologin in system_cfg.users)
|
||||||
|
else ''
|
||||||
|
}}
|
||||||
|
|
||||||
- name: Enable systemd services
|
- name: Enable systemd services
|
||||||
when: _configuration_platform.init_system == 'systemd'
|
when: _configuration_platform.init_system == 'systemd'
|
||||||
vars:
|
vars:
|
||||||
@@ -9,72 +36,151 @@
|
|||||||
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' 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 [])
|
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
|
||||||
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
|
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
|
||||||
|
+ (['bluetooth'] if system_cfg.features.desktop.enabled | bool else [])
|
||||||
}}
|
}}
|
||||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
|
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
|
||||||
loop: "{{ configuration_systemd_services }}"
|
loop: "{{ configuration_systemd_services }}"
|
||||||
register: configuration_enable_service_result
|
register: configuration_enable_service_result
|
||||||
changed_when: configuration_enable_service_result.rc == 0
|
changed_when: configuration_enable_service_result.rc == 0
|
||||||
|
failed_when: >-
|
||||||
|
configuration_enable_service_result.rc != 0
|
||||||
|
and 'No such file or directory' not in (configuration_enable_service_result.stderr | default(''))
|
||||||
|
and 'does not exist' not in (configuration_enable_service_result.stderr | default(''))
|
||||||
|
|
||||||
- name: Enable OpenRC services
|
- name: Enable display manager for selected desktop
|
||||||
when: _configuration_platform.init_system == 'openrc'
|
when:
|
||||||
vars:
|
- _configuration_platform.init_system == 'systemd'
|
||||||
configuration_openrc_services: >-
|
- system_cfg.features.desktop.enabled | bool
|
||||||
{{
|
- _desktop_dm | length > 0
|
||||||
['networking']
|
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ _desktop_dm }}"
|
||||||
+ (['sshd'] if system_cfg.features.ssh.enabled | bool else [])
|
register: configuration_enable_dm_result
|
||||||
+ ([system_cfg.features.firewall.backend] if system_cfg.features.firewall.enabled | bool else [])
|
changed_when: configuration_enable_dm_result.rc == 0
|
||||||
}}
|
# Unlike optional services above, a missing/unenabled DM is fatal: chroot
|
||||||
|
# systemctl can exit 0 while only warning on stderr, so check both.
|
||||||
|
failed_when: >-
|
||||||
|
configuration_enable_dm_result.rc != 0
|
||||||
|
or 'No such file or directory' in (configuration_enable_dm_result.stderr | default(''))
|
||||||
|
or 'does not exist' in (configuration_enable_dm_result.stderr | default(''))
|
||||||
|
|
||||||
|
- 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 PipeWire user services globally
|
||||||
|
when:
|
||||||
|
- _configuration_platform.init_system == 'systemd'
|
||||||
|
- system_cfg.features.desktop.enabled | bool
|
||||||
|
ansible.builtin.command: "{{ chroot_command }} systemctl --global enable {{ item }}"
|
||||||
|
loop: "{{ configuration_desktop_audio_units }}"
|
||||||
|
register: _desktop_audio_result
|
||||||
|
changed_when: _desktop_audio_result.rc == 0
|
||||||
|
failed_when: >-
|
||||||
|
_desktop_audio_result.rc != 0
|
||||||
|
and 'No such file or directory' not in (_desktop_audio_result.stderr | default(''))
|
||||||
|
and 'does not exist' not in (_desktop_audio_result.stderr | default(''))
|
||||||
|
|
||||||
|
- name: Assert greetd has a real session command to launch
|
||||||
|
when:
|
||||||
|
- system_cfg.features.desktop.enabled | bool
|
||||||
|
- _desktop_dm == 'greetd'
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- _greetd_session | length > 0
|
||||||
|
- not (_greetd_session | trim | regex_search('\\.desktop$'))
|
||||||
|
fail_msg: >-
|
||||||
|
greetd needs an executable session command, but the resolved command for desktop
|
||||||
|
environment '{{ system_cfg.features.desktop.environment }}' is
|
||||||
|
'{{ _greetd_session }}'. greetd suits wlroots compositors (sway, hyprland) that
|
||||||
|
launch from a plain command; kde/gnome ship a '.desktop' session and should use
|
||||||
|
their own display manager (sddm, gdm). Set features.desktop.session to an
|
||||||
|
executable, or pick a different display manager.
|
||||||
|
|
||||||
|
- name: Generate greetd configuration
|
||||||
|
when:
|
||||||
|
- _configuration_platform.init_system == 'systemd'
|
||||||
|
- system_cfg.features.desktop.enabled | bool
|
||||||
|
- _desktop_dm == 'greetd'
|
||||||
|
- _greetd_session | length > 0
|
||||||
block:
|
block:
|
||||||
- name: Ensure OpenRC runlevel directory exists
|
- name: Ensure greetd config directory exists
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /mnt/etc/runlevels/default
|
path: /mnt/etc/greetd
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Check OpenRC init scripts
|
- name: Write greetd config.toml
|
||||||
ansible.builtin.stat:
|
ansible.builtin.template:
|
||||||
path: "/mnt/etc/init.d/{{ item }}"
|
src: greetd-config.toml.j2
|
||||||
loop: "{{ configuration_openrc_services }}"
|
dest: /mnt/etc/greetd/config.toml
|
||||||
register: configuration_openrc_service_stats
|
mode: "0644"
|
||||||
|
|
||||||
- name: Enable OpenRC services
|
- name: Configure GDM autologin
|
||||||
ansible.builtin.file:
|
when:
|
||||||
src: "/mnt/etc/init.d/{{ item.item }}"
|
- _configuration_platform.init_system == 'systemd'
|
||||||
dest: "/mnt/etc/runlevels/default/{{ item.item }}"
|
- system_cfg.features.desktop.enabled | bool
|
||||||
state: link
|
- _desktop_dm == 'gdm'
|
||||||
loop: "{{ configuration_openrc_service_stats.results }}"
|
- _desktop_autologin_user | length > 0
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
when: item.stat.exists
|
|
||||||
|
|
||||||
- name: Enable runit services
|
|
||||||
when: _configuration_platform.init_system == 'runit'
|
|
||||||
vars:
|
vars:
|
||||||
configuration_runit_services: >-
|
# Debian's gdm3 reads /etc/gdm3/daemon.conf; RedHat/Arch GDM read
|
||||||
{{
|
# /etc/gdm/custom.conf. The keys are identical, only the path differs.
|
||||||
['dhcpcd']
|
_gdm_dir: "/mnt/etc/{{ 'gdm3' if os_family == 'Debian' else 'gdm' }}"
|
||||||
+ (['sshd'] if system_cfg.features.ssh.enabled | bool else [])
|
_gdm_conf: "{{ 'daemon.conf' if os_family == 'Debian' else 'custom.conf' }}"
|
||||||
+ ([system_cfg.features.firewall.backend] if system_cfg.features.firewall.enabled | bool else [])
|
|
||||||
}}
|
|
||||||
block:
|
block:
|
||||||
- name: Ensure runit service directory exists
|
- name: Ensure GDM config directory exists
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /mnt/var/service
|
path: "{{ _gdm_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Check runit service definitions
|
- name: Write GDM autologin config
|
||||||
ansible.builtin.stat:
|
ansible.builtin.template:
|
||||||
path: "/mnt/etc/sv/{{ item }}"
|
src: gdm-custom.conf.j2
|
||||||
loop: "{{ configuration_runit_services }}"
|
dest: "{{ _gdm_dir }}/{{ _gdm_conf }}"
|
||||||
register: configuration_runit_service_stats
|
mode: "0644"
|
||||||
|
|
||||||
- name: Enable runit services
|
- name: Configure SDDM autologin
|
||||||
|
when:
|
||||||
|
- _configuration_platform.init_system == 'systemd'
|
||||||
|
- system_cfg.features.desktop.enabled | bool
|
||||||
|
- _desktop_dm == 'sddm'
|
||||||
|
- _desktop_autologin_user | length > 0
|
||||||
|
block:
|
||||||
|
- name: Ensure SDDM config directory exists
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
src: "/mnt/etc/sv/{{ item.item }}"
|
path: /mnt/etc/sddm.conf.d
|
||||||
dest: "/mnt/var/service/{{ item.item }}"
|
state: directory
|
||||||
state: link
|
mode: "0755"
|
||||||
loop: "{{ configuration_runit_service_stats.results }}"
|
|
||||||
loop_control:
|
# Plasma 6 ships the Wayland session as plasma.desktop; Plasma 5 ships it as
|
||||||
label: "{{ item.item }}"
|
# plasmawayland.desktop (plasma.desktop is the X11 session there). Pick the
|
||||||
when: item.stat.exists
|
# installed Wayland session so autologin never lands on X11.
|
||||||
|
- name: Discover installed KDE Wayland sessions
|
||||||
|
ansible.builtin.find:
|
||||||
|
paths: /mnt/usr/share/wayland-sessions
|
||||||
|
patterns: "plasma.desktop,plasmawayland.desktop"
|
||||||
|
register: _kde_wayland_sessions
|
||||||
|
|
||||||
|
- name: Resolve the KDE Wayland session file
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_sddm_session: >-
|
||||||
|
{%- set names = _kde_wayland_sessions.files | map(attribute='path') | map('basename') | list -%}
|
||||||
|
{{ 'plasma.desktop' if 'plasma.desktop' in names else (names | first | default('')) }}
|
||||||
|
|
||||||
|
- name: Write SDDM autologin drop-in
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sddm-autologin.conf.j2
|
||||||
|
dest: /mnt/etc/sddm.conf.d/10-autologin.conf
|
||||||
|
mode: "0644"
|
||||||
|
|||||||
@@ -15,15 +15,16 @@
|
|||||||
validate: /usr/sbin/visudo --check --file=%s
|
validate: /usr/sbin/visudo --check --file=%s
|
||||||
|
|
||||||
- name: Deploy per-user sudoers rules
|
- name: Deploy per-user sudoers rules
|
||||||
when: item.sudo | default(false)
|
# Jinja truthiness: bool true / a rule string => deploy; false / '' / unset => skip.
|
||||||
|
when: item.value.sudo | default(false)
|
||||||
vars:
|
vars:
|
||||||
configuration_sudoers_rule: >-
|
configuration_sudoers_rule: >-
|
||||||
{{ item.sudo if item.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
content: "{{ item.name }} {{ configuration_sudoers_rule }}\n"
|
content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
|
||||||
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
|
dest: "/mnt/etc/sudoers.d/{{ item.key }}"
|
||||||
mode: "0440"
|
mode: "0440"
|
||||||
validate: /usr/sbin/visudo --check --file=%s
|
validate: /usr/sbin/visudo --check --file=%s
|
||||||
loop: "{{ system_cfg.users }}"
|
loop: "{{ system_cfg.users | dict2items }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.name }}"
|
label: "{{ item.key }}"
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
---
|
---
|
||||||
- name: Set root password
|
- name: Set root password
|
||||||
|
when: (system_cfg.root.password | default('') | string | length) > 0
|
||||||
ansible.builtin.shell: >-
|
ansible.builtin.shell: >-
|
||||||
set -o pipefail &&
|
set -o pipefail &&
|
||||||
echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} /usr/sbin/chpasswd -e
|
echo 'root:{{ system_cfg.root.password if (system_cfg.root.password | string)[:1] == "$" else system_cfg.root.password | password_hash("sha512") }}'
|
||||||
|
| {{ chroot_command }} /usr/sbin/chpasswd -e
|
||||||
args:
|
args:
|
||||||
executable: /bin/bash
|
executable: /bin/bash
|
||||||
register: configuration_root_result
|
register: configuration_root_result
|
||||||
changed_when: configuration_root_result.rc == 0
|
changed_when: configuration_root_result.rc == 0
|
||||||
no_log: true
|
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
|
- name: Set root shell
|
||||||
ansible.builtin.command: >-
|
ansible.builtin.command: >-
|
||||||
{{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell }} root
|
{{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell }} root
|
||||||
@@ -18,44 +27,47 @@
|
|||||||
- name: Create user accounts
|
- name: Create user accounts
|
||||||
vars:
|
vars:
|
||||||
configuration_user_group: "{{ _configuration_platform.user_group }}"
|
configuration_user_group: "{{ _configuration_platform.user_group }}"
|
||||||
# UID starts at 1000; safe for fresh installs only
|
# plaintext is hashed; a pre-computed crypt hash ($6$/$y$/...) passes through.
|
||||||
|
configuration_user_pw: >-
|
||||||
|
{{ item.value.password if (item.value.password | string)[:1] == '$'
|
||||||
|
else item.value.password | password_hash('sha512') }}
|
||||||
configuration_useradd_cmd: >-
|
configuration_useradd_cmd: >-
|
||||||
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
|
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
|
||||||
--uid {{ 1000 + ansible_loop.index0 }}
|
--uid {{ 1000 + _idx }}
|
||||||
--groups {{ configuration_user_group }} {{ item.name }}
|
--groups {{ configuration_user_group }} {{ item.key }}
|
||||||
--password {{ item.password | password_hash('sha512') }} --shell {{ item.shell | default('/bin/bash') }}
|
{{ ('--password ' ~ configuration_user_pw) if (item.value.password | default('') | string | length > 0) else '' }}
|
||||||
|
--shell {{ item.value.shell | default('/bin/bash') }}
|
||||||
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
|
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
|
||||||
loop: "{{ system_cfg.users }}"
|
loop: "{{ system_cfg.users | dict2items }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
extended: true
|
index_var: _idx
|
||||||
label: "{{ item.name }}"
|
label: "{{ item.key }}"
|
||||||
register: configuration_user_result
|
register: configuration_user_result
|
||||||
changed_when: configuration_user_result.rc == 0
|
changed_when: configuration_user_result.rc == 0
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Ensure .ssh directory exists
|
- name: Ensure .ssh directory exists
|
||||||
when: item['keys'] | default([]) | length > 0
|
when: ('keys' in item.value) and (item.value['keys'] | length) > 0
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "/mnt/home/{{ item.name }}/.ssh"
|
path: "/mnt/home/{{ item.key }}/.ssh"
|
||||||
state: directory
|
state: directory
|
||||||
owner: "{{ 1000 + ansible_loop.index0 }}"
|
owner: "{{ 1000 + _idx }}"
|
||||||
group: "{{ 1000 + ansible_loop.index0 }}"
|
group: "{{ 1000 + _idx }}"
|
||||||
mode: "0700"
|
mode: "0700"
|
||||||
loop: "{{ system_cfg.users }}"
|
loop: "{{ system_cfg.users | dict2items }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
extended: true
|
index_var: _idx
|
||||||
label: "{{ item.name }}"
|
label: "{{ item.key }}"
|
||||||
|
|
||||||
- name: Add SSH public keys to authorized_keys
|
- name: Deploy SSH authorized_keys
|
||||||
vars:
|
when: ('keys' in item.value) and (item.value['keys'] | length) > 0
|
||||||
configuration_uid: "{{ 1000 + (system_cfg.users | map(attribute='name') | list).index(item.0.name) }}"
|
ansible.builtin.copy:
|
||||||
ansible.builtin.lineinfile:
|
content: "{{ item.value['keys'] | join('\n') }}\n"
|
||||||
path: "/mnt/home/{{ item.0.name }}/.ssh/authorized_keys"
|
dest: "/mnt/home/{{ item.key }}/.ssh/authorized_keys"
|
||||||
line: "{{ item.1 }}"
|
owner: "{{ 1000 + _idx }}"
|
||||||
owner: "{{ configuration_uid }}"
|
group: "{{ 1000 + _idx }}"
|
||||||
group: "{{ configuration_uid }}"
|
|
||||||
mode: "0600"
|
mode: "0600"
|
||||||
create: true
|
loop: "{{ system_cfg.users | dict2items }}"
|
||||||
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
|
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.0.name }}: {{ item.1[:40] }}..."
|
index_var: _idx
|
||||||
|
label: "{{ item.key }}"
|
||||||
|
|||||||
4
roles/configuration/templates/gdm-custom.conf.j2
Normal file
4
roles/configuration/templates/gdm-custom.conf.j2
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[daemon]
|
||||||
|
WaylandEnable=true
|
||||||
|
AutomaticLoginEnable=true
|
||||||
|
AutomaticLogin={{ _desktop_autologin_user }}
|
||||||
12
roles/configuration/templates/greetd-config.toml.j2
Normal file
12
roles/configuration/templates/greetd-config.toml.j2
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[terminal]
|
||||||
|
vt = 1
|
||||||
|
|
||||||
|
[default_session]
|
||||||
|
command = "tuigreet --time --remember --cmd {{ _greetd_session }}"
|
||||||
|
user = "greeter"
|
||||||
|
{% if _desktop_autologin_user | length > 0 %}
|
||||||
|
|
||||||
|
[initial_session]
|
||||||
|
command = "{{ _greetd_session }}"
|
||||||
|
user = "{{ _desktop_autologin_user }}"
|
||||||
|
{% endif %}
|
||||||
6
roles/configuration/templates/sddm-autologin.conf.j2
Normal file
6
roles/configuration/templates/sddm-autologin.conf.j2
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{% set _session = _desktop_session if (_desktop_session | length > 0) else _sddm_session %}
|
||||||
|
[Autologin]
|
||||||
|
User={{ _desktop_autologin_user }}
|
||||||
|
{% if _session | length > 0 %}
|
||||||
|
Session={{ _session }}
|
||||||
|
{% endif %}
|
||||||
@@ -35,33 +35,24 @@ configuration_platform_config:
|
|||||||
grub_mkconfig_prefix: grub-mkconfig
|
grub_mkconfig_prefix: grub-mkconfig
|
||||||
locale_gen: true
|
locale_gen: true
|
||||||
init_system: systemd
|
init_system: systemd
|
||||||
Suse:
|
|
||||||
user_group: wheel
|
# Display manager auto-detection from desktop environment name.
|
||||||
sudo_group: "%wheel"
|
configuration_desktop_dm_map:
|
||||||
ssh_service: sshd
|
gnome: gdm
|
||||||
efi_loader: grubx64.efi
|
kde: sddm
|
||||||
grub_install: true
|
sway: greetd
|
||||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
hyprland: greetd
|
||||||
grub_mkconfig_prefix: grub-mkconfig
|
|
||||||
locale_gen: true
|
# Per-environment session command for greetd-launched compositors (sway/hyprland):
|
||||||
init_system: systemd
|
# the executable greetd starts. kde/gnome use a display manager (sddm/gdm) whose
|
||||||
Alpine:
|
# Wayland session is resolved separately, so they are not in this map.
|
||||||
user_group: wheel
|
configuration_desktop_session_cmd_map:
|
||||||
sudo_group: "%wheel"
|
sway: sway
|
||||||
ssh_service: sshd
|
hyprland: Hyprland
|
||||||
efi_loader: grubx64.efi
|
|
||||||
grub_install: true
|
# PipeWire user units enabled globally when a desktop is installed.
|
||||||
initramfs_cmd: ""
|
# pipewire/pipewire-pulse are socket-activated; wireplumber ships no socket.
|
||||||
grub_mkconfig_prefix: grub-mkconfig
|
configuration_desktop_audio_units:
|
||||||
locale_gen: false
|
- pipewire.socket
|
||||||
init_system: openrc
|
- pipewire-pulse.socket
|
||||||
Void:
|
- wireplumber.service
|
||||||
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
|
|
||||||
|
|||||||
@@ -8,3 +8,36 @@ environment_parallel_downloads: 20
|
|||||||
environment_pacman_lock_timeout: 120
|
environment_pacman_lock_timeout: 120
|
||||||
environment_pacman_retries: 4
|
environment_pacman_retries: 4
|
||||||
environment_pacman_retry_delay: 15
|
environment_pacman_retry_delay: 15
|
||||||
|
|
||||||
|
# PCI vendor IDs -> vendor codes used by hardware detection.
|
||||||
|
# Only vendors that drive distinct firmware/driver packages are mapped.
|
||||||
|
environment_pci_vendor_map:
|
||||||
|
"8086": intel
|
||||||
|
"1002": amd
|
||||||
|
"1022": amd
|
||||||
|
"10de": nvidia
|
||||||
|
"14e4": broadcom
|
||||||
|
"10ec": realtek
|
||||||
|
"168c": atheros
|
||||||
|
"0cf3": atheros
|
||||||
|
"168d": atheros
|
||||||
|
"14c3": mediatek
|
||||||
|
"11ab": marvell
|
||||||
|
"1b4b": marvell
|
||||||
|
"17cb": qcom
|
||||||
|
"105b": qcom
|
||||||
|
"1cf3": cirrus
|
||||||
|
"13d7": cirrus
|
||||||
|
|
||||||
|
# USB vendor IDs of fingerprint readers supported by libfprint / fprintd.
|
||||||
|
# Lowercase, four-digit hex; matched against `lsusb` output.
|
||||||
|
environment_fingerprint_vendor_ids:
|
||||||
|
- "06cb" # Synaptics (modern ThinkPad/Dell)
|
||||||
|
- "138a" # Validity Sensors (older ThinkPad)
|
||||||
|
- "1c7a" # LighTuning / Egis
|
||||||
|
- "27c6" # Goodix
|
||||||
|
- "04f3" # Elan
|
||||||
|
- "0a5c" # Broadcom
|
||||||
|
- "08ff" # AuthenTec (legacy)
|
||||||
|
- "147e" # Upek (legacy)
|
||||||
|
- "1491" # Futronic
|
||||||
|
|||||||
5
roles/environment/handlers/main.yml
Normal file
5
roles/environment/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: Restart sshd
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
@@ -87,9 +87,10 @@
|
|||||||
- name: Switch to SSH connection
|
- name: Switch to SSH connection
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
ansible_connection: ssh
|
ansible_connection: ssh
|
||||||
|
ansible_host: "{{ system_cfg.network.ip }}"
|
||||||
|
ansible_port: 22
|
||||||
ansible_user: root
|
ansible_user: root
|
||||||
ansible_password: ""
|
ansible_password: ""
|
||||||
ansible_host: "{{ system_cfg.network.ip }}"
|
|
||||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||||
|
|
||||||
- name: Reset connection for SSH switchover
|
- name: Reset connection for SSH switchover
|
||||||
|
|||||||
149
roles/environment/tasks/_detect_hardware.yml
Normal file
149
roles/environment/tasks/_detect_hardware.yml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
---
|
||||||
|
# Hardware detection on the live installer host.
|
||||||
|
#
|
||||||
|
# Resolves system_cfg.features.hardware.profile when not explicitly set, so
|
||||||
|
# downstream bootstrap can install vendor-matched microcode/firmware/GPU/
|
||||||
|
# peripheral packages. When the user supplies an override profile, detection
|
||||||
|
# is skipped (golden-image flow: bake an image with a fixed profile).
|
||||||
|
#
|
||||||
|
# Output fact: hardware_profile_active = {
|
||||||
|
# cpu: 'intel'|'amd'|'',
|
||||||
|
# gpus: list of 'intel'|'amd'|'nvidia',
|
||||||
|
# nvidia_supports_open: bool, # true when all detected Nvidia GPUs are
|
||||||
|
# # Turing or newer (device id >= 0x1e00)
|
||||||
|
# wireless: list of vendor codes ('intel'|'realtek'|'atheros'|...),
|
||||||
|
# fingerprint: bool, # USB fingerprint reader detected
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Skipped entirely when neither firmware/gpu/peripherals features are enabled.
|
||||||
|
|
||||||
|
- name: Resolve hardware detection requirement
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_hardware_detection_needed: >-
|
||||||
|
{{
|
||||||
|
(system_cfg.features.firmware.enabled | bool)
|
||||||
|
or (system_cfg.features.gpu.enabled | bool)
|
||||||
|
or (system_cfg.features.peripherals.enabled | bool)
|
||||||
|
}}
|
||||||
|
_hardware_profile_override: "{{ system_cfg.features.hardware.profile | default({}) }}"
|
||||||
|
|
||||||
|
- name: Use supplied hardware profile (override)
|
||||||
|
when:
|
||||||
|
- _hardware_detection_needed | bool
|
||||||
|
- _hardware_profile_override | length > 0
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
hardware_profile_active:
|
||||||
|
cpu: "{{ _hardware_profile_override.cpu | default('') | string | lower }}"
|
||||||
|
gpus: "{{ _hardware_profile_override.gpus | default([]) | map('lower') | list }}"
|
||||||
|
nvidia_supports_open: "{{ _hardware_profile_override.nvidia_supports_open | default(true) | bool }}"
|
||||||
|
wireless: "{{ _hardware_profile_override.wireless | default([]) | map('lower') | list }}"
|
||||||
|
fingerprint: "{{ _hardware_profile_override.fingerprint | default(false) | bool }}"
|
||||||
|
|
||||||
|
- name: Detect hardware from live host
|
||||||
|
when:
|
||||||
|
- _hardware_detection_needed | bool
|
||||||
|
- _hardware_profile_override | length == 0
|
||||||
|
block:
|
||||||
|
- name: Read CPU vendor
|
||||||
|
ansible.builtin.command: lscpu
|
||||||
|
register: _hardware_lscpu
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Read PCI device list
|
||||||
|
ansible.builtin.command: lspci -nn
|
||||||
|
register: _hardware_lspci
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Read USB device list
|
||||||
|
ansible.builtin.command: lsusb
|
||||||
|
register: _hardware_lsusb
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Resolve detected hardware profile
|
||||||
|
vars:
|
||||||
|
_vendor_keys: "{{ environment_pci_vendor_map.keys() | list }}"
|
||||||
|
_cpu_vendor_raw: >-
|
||||||
|
{{
|
||||||
|
_hardware_lscpu.stdout
|
||||||
|
| regex_search('(?im)^Vendor ID:\\s*(\\S+)', '\\1')
|
||||||
|
| default([''], true)
|
||||||
|
| first
|
||||||
|
}}
|
||||||
|
_cpu_vendor: >-
|
||||||
|
{{
|
||||||
|
'intel' if _cpu_vendor_raw == 'GenuineIntel'
|
||||||
|
else ('amd' if _cpu_vendor_raw == 'AuthenticAMD' else '')
|
||||||
|
}}
|
||||||
|
# PCI classes: 0300 = VGA, 0302 = 3D, 0280 = wireless network controller.
|
||||||
|
_gpu_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[(0300|0302)\\]:') | list }}"
|
||||||
|
_gpu_pairs: >-
|
||||||
|
{{
|
||||||
|
_gpu_lines
|
||||||
|
| map('regex_search', '\\[([0-9a-f]{4}):([0-9a-f]{4})\\]', '\\1', '\\2')
|
||||||
|
| select('truthy')
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
_gpu_vendor_ids: "{{ _gpu_pairs | map('first') | select('in', _vendor_keys) | list }}"
|
||||||
|
_gpu_vendors: "{{ _gpu_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
|
||||||
|
_nvidia_device_ids: >-
|
||||||
|
{{
|
||||||
|
_gpu_pairs
|
||||||
|
| selectattr('0', 'equalto', '10de')
|
||||||
|
| map(attribute=1)
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
_nvidia_min_id: >-
|
||||||
|
{{
|
||||||
|
(_nvidia_device_ids | map('int', base=16) | list | min)
|
||||||
|
if _nvidia_device_ids | length > 0 else 0
|
||||||
|
}}
|
||||||
|
# 0x1e00 = 7680 = first Turing device id; Turing+ supports nvidia-open.
|
||||||
|
_nvidia_supports_open: "{{ _nvidia_device_ids | length > 0 and (_nvidia_min_id | int) >= 7680 }}"
|
||||||
|
_wifi_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0280\\]:') | list }}"
|
||||||
|
_wifi_vendor_ids: >-
|
||||||
|
{{
|
||||||
|
_wifi_lines
|
||||||
|
| map('regex_search', '\\[([0-9a-f]{4}):[0-9a-f]{4}\\]', '\\1')
|
||||||
|
| select('truthy')
|
||||||
|
| map('first')
|
||||||
|
| select('in', _vendor_keys)
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
_wifi_vendors: "{{ _wifi_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
|
||||||
|
_fingerprint_present: >-
|
||||||
|
{{
|
||||||
|
(_hardware_lsusb.stdout | default(''))
|
||||||
|
| regex_search(
|
||||||
|
'(?i)ID (' ~ (environment_fingerprint_vendor_ids | join('|')) ~ '):'
|
||||||
|
)
|
||||||
|
is not none
|
||||||
|
}}
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
hardware_profile_active:
|
||||||
|
cpu: "{{ _cpu_vendor }}"
|
||||||
|
gpus: "{{ _gpu_vendors }}"
|
||||||
|
nvidia_supports_open: "{{ _nvidia_supports_open | bool }}"
|
||||||
|
wireless: "{{ _wifi_vendors }}"
|
||||||
|
fingerprint: "{{ _fingerprint_present | bool }}"
|
||||||
|
|
||||||
|
- name: Initialize empty hardware profile when detection skipped
|
||||||
|
when: not (_hardware_detection_needed | bool)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
hardware_profile_active:
|
||||||
|
cpu: ""
|
||||||
|
gpus: []
|
||||||
|
nvidia_supports_open: true
|
||||||
|
wireless: []
|
||||||
|
fingerprint: false
|
||||||
|
|
||||||
|
- name: Report active hardware profile
|
||||||
|
when: _hardware_detection_needed | bool
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
Hardware profile {{ 'override' if _hardware_profile_override | length > 0 else 'detected' }}:
|
||||||
|
cpu={{ hardware_profile_active.cpu | default('-') }},
|
||||||
|
gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }}
|
||||||
|
{{ '(open-supported)' if hardware_profile_active.nvidia_supports_open | bool else '(legacy)' }},
|
||||||
|
wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }},
|
||||||
|
fingerprint={{ hardware_profile_active.fingerprint | default(false) }}
|
||||||
@@ -68,6 +68,20 @@
|
|||||||
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
|
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
|
||||||
quiet: true
|
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
|
||||||
|
notify: Restart sshd
|
||||||
|
|
||||||
|
- name: Apply pending sshd restart before continuing
|
||||||
|
ansible.builtin.meta: flush_handlers
|
||||||
|
|
||||||
- name: Abort if the host is not booted from the Arch install media
|
- name: Abort if the host is not booted from the Arch install media
|
||||||
when:
|
when:
|
||||||
- not (custom_iso | bool)
|
- not (custom_iso | bool)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
state: latest
|
state: latest
|
||||||
loop:
|
loop:
|
||||||
- { name: glibc }
|
- { name: glibc }
|
||||||
|
- { name: lua, os: [almalinux, fedora, rhel, rocky] }
|
||||||
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
|
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
|
||||||
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
|
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
|
||||||
- { name: debian-archive-keyring, os: [debian] }
|
- { name: debian-archive-keyring, os: [debian] }
|
||||||
|
|||||||
@@ -11,5 +11,8 @@
|
|||||||
- name: Prepare installer environment
|
- name: Prepare installer environment
|
||||||
ansible.builtin.include_tasks: _prepare_installer.yml
|
ansible.builtin.include_tasks: _prepare_installer.yml
|
||||||
|
|
||||||
|
- name: Detect hardware for firmware/GPU package selection
|
||||||
|
ansible.builtin.include_tasks: _detect_hardware.yml
|
||||||
|
|
||||||
- name: Run third-party preparation tasks
|
- name: Run third-party preparation tasks
|
||||||
ansible.builtin.include_tasks: _thirdparty.yml
|
ansible.builtin.include_tasks: _thirdparty.yml
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
# OS family lists — single source of truth for platform detection and validation
|
# OS family lists - single source of truth for platform detection and validation
|
||||||
os_family_rhel:
|
os_family_rhel:
|
||||||
- almalinux
|
- almalinux
|
||||||
- fedora
|
- fedora
|
||||||
@@ -10,33 +10,27 @@ os_family_debian:
|
|||||||
- ubuntu
|
- ubuntu
|
||||||
- ubuntu-lts
|
- ubuntu-lts
|
||||||
|
|
||||||
# OS → family mapping — aligns with the main project's ansible_os_family pattern.
|
# 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.
|
# Enables platform_config dict lookups per role instead of inline when: is_rhel chains.
|
||||||
os_family_map:
|
os_family_map:
|
||||||
almalinux: RedHat
|
almalinux: RedHat
|
||||||
alpine: Alpine
|
|
||||||
archlinux: Archlinux
|
archlinux: Archlinux
|
||||||
debian: Debian
|
debian: Debian
|
||||||
fedora: RedHat
|
fedora: RedHat
|
||||||
opensuse: Suse
|
|
||||||
rhel: RedHat
|
rhel: RedHat
|
||||||
rocky: RedHat
|
rocky: RedHat
|
||||||
ubuntu: Debian
|
ubuntu: Debian
|
||||||
ubuntu-lts: Debian
|
ubuntu-lts: Debian
|
||||||
void: Void
|
|
||||||
|
|
||||||
os_supported:
|
os_supported:
|
||||||
- almalinux
|
- almalinux
|
||||||
- alpine
|
|
||||||
- archlinux
|
- archlinux
|
||||||
- debian
|
- debian
|
||||||
- fedora
|
- fedora
|
||||||
- opensuse
|
|
||||||
- rhel
|
- rhel
|
||||||
- rocky
|
- rocky
|
||||||
- ubuntu
|
- ubuntu
|
||||||
- ubuntu-lts
|
- ubuntu-lts
|
||||||
- void
|
|
||||||
|
|
||||||
# User input. Normalized into hypervisor_cfg + hypervisor_type.
|
# User input. Normalized into hypervisor_cfg + hypervisor_type.
|
||||||
hypervisor:
|
hypervisor:
|
||||||
@@ -85,7 +79,7 @@ system_defaults:
|
|||||||
mirror: ""
|
mirror: ""
|
||||||
packages: []
|
packages: []
|
||||||
disks: []
|
disks: []
|
||||||
users: []
|
users: {}
|
||||||
root:
|
root:
|
||||||
password: ""
|
password: ""
|
||||||
shell: "/bin/bash"
|
shell: "/bin/bash"
|
||||||
@@ -106,8 +100,6 @@ system_defaults:
|
|||||||
iter: 4000
|
iter: 4000
|
||||||
bits: 512
|
bits: 512
|
||||||
pbkdf: "argon2id"
|
pbkdf: "argon2id"
|
||||||
urandom: true
|
|
||||||
verify: true
|
|
||||||
features:
|
features:
|
||||||
cis:
|
cis:
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -127,12 +119,37 @@ system_defaults:
|
|||||||
motd: false
|
motd: false
|
||||||
sudo: true
|
sudo: true
|
||||||
rhel_repo:
|
rhel_repo:
|
||||||
source: "iso" # iso|satellite|none — how RHEL systems get packages post-install
|
source: "iso" # iso|satellite|none - how RHEL systems get packages post-install
|
||||||
url: "" # Satellite/custom repo URL when source=satellite
|
url: "" # Satellite/custom repo URL when source=satellite
|
||||||
chroot:
|
chroot:
|
||||||
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn
|
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn
|
||||||
|
initramfs:
|
||||||
|
generator: "" # auto-detected; override: dracut|mkinitcpio|initramfs-tools
|
||||||
|
desktop:
|
||||||
|
enabled: false
|
||||||
|
environment: "" # gnome|kde|sway|hyprland
|
||||||
|
display_manager: "" # auto from environment when empty; override: gdm|sddm|greetd
|
||||||
|
autologin: false # false | username from system.users
|
||||||
|
session: "" # session name/command for the autologin user
|
||||||
|
groups: [] # opt-in package groups (keys of desktop_package_groups)
|
||||||
|
secure_boot:
|
||||||
|
enabled: false
|
||||||
|
method: "" # arch only: sbctl (default) or uki; ignored for other distros
|
||||||
|
firmware:
|
||||||
|
enabled: "auto" # auto = on for physical, off for virtual
|
||||||
|
microcode: "auto"
|
||||||
|
gpu:
|
||||||
|
enabled: "auto" # auto = follows desktop.enabled
|
||||||
|
nvidia_driver: "auto" # auto | open | proprietary | nouveau
|
||||||
|
peripherals:
|
||||||
|
enabled: "auto" # auto = follows desktop.enabled
|
||||||
|
fingerprint: "auto"
|
||||||
|
webcam: "auto"
|
||||||
|
displaylink: false
|
||||||
|
hardware:
|
||||||
|
profile: {} # empty = autodetect; set to override (golden image)
|
||||||
|
|
||||||
# Per-hypervisor required fields — drives data-driven validation.
|
# Per-hypervisor required fields - drives data-driven validation.
|
||||||
# All virtual types additionally require network bridge or interfaces.
|
# All virtual types additionally require network bridge or interfaces.
|
||||||
hypervisor_required_fields:
|
hypervisor_required_fields:
|
||||||
proxmox:
|
proxmox:
|
||||||
@@ -156,7 +173,7 @@ hypervisor_disk_device_map:
|
|||||||
proxmox: "/dev/sd"
|
proxmox: "/dev/sd"
|
||||||
vmware: "/dev/sd"
|
vmware: "/dev/sd"
|
||||||
|
|
||||||
# Mountpoints managed by the partitioning role — forbidden for extra disks.
|
# Mountpoints managed by the partitioning role - forbidden for extra disks.
|
||||||
reserved_mounts:
|
reserved_mounts:
|
||||||
- /boot
|
- /boot
|
||||||
- /boot/efi
|
- /boot/efi
|
||||||
|
|||||||
@@ -28,21 +28,41 @@
|
|||||||
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
|
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
|
||||||
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
|
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
|
||||||
# --- Network ---
|
# --- Network ---
|
||||||
# Flat fields (bridge, ip, etc.) and interfaces[] are mutually exclusive.
|
# Flat fields (bridge, ip, etc.) and interfaces[] express the same primary NIC.
|
||||||
# When interfaces[] is set, flat fields are populated from the first
|
# When only flat fields are set, a synthetic interfaces[] entry is built below.
|
||||||
# interface in the "Populate primary network fields" task below.
|
# When interfaces[] is set, the flat ip/prefix/gateway are backfilled from
|
||||||
# When only flat fields are set, a synthetic interfaces[] entry is built.
|
# interfaces[0] so consumers reading the flat fields (e.g. the post-reboot
|
||||||
|
# reconnect block) still work.
|
||||||
network:
|
network:
|
||||||
bridge: "{{ system_raw.network.bridge | default('') | string }}"
|
bridge: >-
|
||||||
|
{{
|
||||||
|
(system_raw.network.bridge | default('') | string)
|
||||||
|
if (system_raw.network.bridge | default('') | string | length) > 0
|
||||||
|
else (system_raw.network.interfaces[0].bridge | default('') | string
|
||||||
|
if (system_raw.network.interfaces | default([]) | length) > 0 else '')
|
||||||
|
}}
|
||||||
vlan: "{{ system_raw.network.vlan | default('') | string }}"
|
vlan: "{{ system_raw.network.vlan | default('') | string }}"
|
||||||
ip: "{{ system_raw.network.ip | default('') | string }}"
|
ip: >-
|
||||||
|
{{
|
||||||
|
(system_raw.network.ip | default('') | string)
|
||||||
|
if (system_raw.network.ip | default('') | string | length) > 0
|
||||||
|
else (system_raw.network.interfaces[0].ip | default('') | string
|
||||||
|
if (system_raw.network.interfaces | default([]) | length) > 0 else '')
|
||||||
|
}}
|
||||||
prefix: >-
|
prefix: >-
|
||||||
{{
|
{{
|
||||||
(system_raw.network.prefix | int | string)
|
(system_raw.network.prefix | int | string)
|
||||||
if (system_raw.network.prefix | default('') | string | length) > 0
|
if (system_raw.network.prefix | default('') | string | length) > 0
|
||||||
else ''
|
else (system_raw.network.interfaces[0].prefix | default('') | string
|
||||||
|
if (system_raw.network.interfaces | default([]) | length) > 0 else '')
|
||||||
|
}}
|
||||||
|
gateway: >-
|
||||||
|
{{
|
||||||
|
(system_raw.network.gateway | default('') | string)
|
||||||
|
if (system_raw.network.gateway | default('') | string | length) > 0
|
||||||
|
else (system_raw.network.interfaces[0].gateway | default('') | string
|
||||||
|
if (system_raw.network.interfaces | default([]) | length) > 0 else '')
|
||||||
}}
|
}}
|
||||||
gateway: "{{ system_raw.network.gateway | default('') | string }}"
|
|
||||||
dns:
|
dns:
|
||||||
servers: "{{ system_raw.network.dns.servers | default([]) }}"
|
servers: "{{ system_raw.network.dns.servers | default([]) }}"
|
||||||
search: "{{ system_raw.network.dns.search | default([]) }}"
|
search: "{{ system_raw.network.dns.search | default([]) }}"
|
||||||
@@ -96,7 +116,7 @@
|
|||||||
}}
|
}}
|
||||||
# --- Storage & accounts ---
|
# --- Storage & accounts ---
|
||||||
disks: "{{ system_raw.disks | default([]) }}"
|
disks: "{{ system_raw.disks | default([]) }}"
|
||||||
users: "{{ system_raw.users | default([]) }}"
|
users: "{{ system_raw.users | default({}) }}"
|
||||||
root:
|
root:
|
||||||
password: "{{ system_raw.root.password | string }}"
|
password: "{{ system_raw.root.password | string }}"
|
||||||
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
|
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
|
||||||
@@ -118,8 +138,6 @@
|
|||||||
iter: "{{ system_raw.luks.iter | int }}"
|
iter: "{{ system_raw.luks.iter | int }}"
|
||||||
bits: "{{ system_raw.luks.bits | int }}"
|
bits: "{{ system_raw.luks.bits | int }}"
|
||||||
pbkdf: "{{ system_raw.luks.pbkdf | string }}"
|
pbkdf: "{{ system_raw.luks.pbkdf | string }}"
|
||||||
urandom: "{{ system_raw.luks.urandom | bool }}"
|
|
||||||
verify: "{{ system_raw.luks.verify | bool }}"
|
|
||||||
# --- Feature flags ---
|
# --- Feature flags ---
|
||||||
features:
|
features:
|
||||||
cis:
|
cis:
|
||||||
@@ -144,27 +162,68 @@
|
|||||||
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
|
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
|
||||||
chroot:
|
chroot:
|
||||||
tool: "{{ system_raw.features.chroot.tool | string }}"
|
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 }}"
|
||||||
|
autologin: "{{ system_raw.features.desktop.autologin | default(false) }}"
|
||||||
|
session: "{{ system_raw.features.desktop.session | default('') | string }}"
|
||||||
|
groups: "{{ system_raw.features.desktop.groups | default([]) }}"
|
||||||
|
secure_boot:
|
||||||
|
enabled: "{{ system_raw.features.secure_boot.enabled | bool }}"
|
||||||
|
method: "{{ system_raw.features.secure_boot.method | default('') | string | lower }}"
|
||||||
|
firmware:
|
||||||
|
enabled: >-
|
||||||
|
{{
|
||||||
|
(system_type == 'physical')
|
||||||
|
if (system_raw.features.firmware.enabled | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.firmware.enabled | bool)
|
||||||
|
}}
|
||||||
|
microcode: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
(system_type == 'physical')
|
||||||
|
if (system_raw.features.firmware.enabled | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.firmware.enabled | bool)
|
||||||
|
)
|
||||||
|
if (system_raw.features.firmware.microcode | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.firmware.microcode | bool)
|
||||||
|
}}
|
||||||
|
gpu:
|
||||||
|
enabled: >-
|
||||||
|
{{
|
||||||
|
(system_raw.features.desktop.enabled | bool)
|
||||||
|
if (system_raw.features.gpu.enabled | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.gpu.enabled | bool)
|
||||||
|
}}
|
||||||
|
nvidia_driver: "{{ system_raw.features.gpu.nvidia_driver | default('auto') | string | lower }}"
|
||||||
|
peripherals:
|
||||||
|
enabled: >-
|
||||||
|
{{
|
||||||
|
(system_raw.features.desktop.enabled | bool)
|
||||||
|
if (system_raw.features.peripherals.enabled | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.peripherals.enabled | bool)
|
||||||
|
}}
|
||||||
|
# fingerprint/webcam stay tri-state ('auto'|'true'|'false') because the
|
||||||
|
# 'auto' branch is resolved at install time using detection results.
|
||||||
|
fingerprint: >-
|
||||||
|
{{
|
||||||
|
'auto'
|
||||||
|
if (system_raw.features.peripherals.fingerprint | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.peripherals.fingerprint | bool | string | lower)
|
||||||
|
}}
|
||||||
|
webcam: >-
|
||||||
|
{{
|
||||||
|
'auto'
|
||||||
|
if (system_raw.features.peripherals.webcam | string | lower) == 'auto'
|
||||||
|
else (system_raw.features.peripherals.webcam | bool | string | lower)
|
||||||
|
}}
|
||||||
|
displaylink: "{{ system_raw.features.peripherals.displaylink | bool }}"
|
||||||
|
hardware:
|
||||||
|
profile: "{{ system_raw.features.hardware.profile | default({}) }}"
|
||||||
hostname: "{{ system_name }}"
|
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 (physical_default_os if system_type == 'physical' else '') }}"
|
||||||
os_version: "{{ system_raw.version | default('') | string }}"
|
os_version: "{{ system_raw.version | default('') | string }}"
|
||||||
no_log: true
|
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:
|
that:
|
||||||
- system is mapping
|
- system is mapping
|
||||||
- system.network is not defined or system.network is mapping
|
- system.network is not defined or system.network is mapping
|
||||||
- system.users is not defined or (system.users is iterable and system.users is not string and system.users is not mapping)
|
- system.users is not defined or system.users is mapping
|
||||||
- system.root is not defined or system.root is mapping
|
- system.root is not defined or system.root is mapping
|
||||||
- system.luks is not defined or system.luks is mapping
|
- system.luks is not defined or system.luks is mapping
|
||||||
- system.features is not defined or system.features is mapping
|
- system.features is not defined or system.features is mapping
|
||||||
fail_msg: "system and its nested keys (network, root, luks, features) must be dictionaries; system.users must be a list."
|
fail_msg: "system and its nested keys (network, root, luks, features, users) must be dictionaries."
|
||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
- name: Validate DNS lists (not strings)
|
- name: Validate DNS lists (not strings)
|
||||||
@@ -25,17 +25,17 @@
|
|||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
- name: Validate system.users entries
|
- name: Validate system.users entries
|
||||||
when: system.users is defined and system.users | length > 0
|
when: system.users is defined and system.users is mapping and system.users | length > 0
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- item is mapping
|
- item.value is mapping
|
||||||
- item.name is defined and (item.name | string | length) > 0
|
- item.key | string | length > 0
|
||||||
- item['keys'] is not defined or (item['keys'] is iterable and item['keys'] is not string)
|
- ('keys' not in item.value) or (item.value['keys'] is iterable and item.value['keys'] is not string)
|
||||||
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
|
fail_msg: "Each system.users entry must be a dict keyed by username; 'keys' must be a list."
|
||||||
quiet: true
|
quiet: true
|
||||||
loop: "{{ system.users }}"
|
loop: "{{ system.users | dict2items }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.name | default('(unnamed)') }}"
|
label: "{{ item.key }}"
|
||||||
|
|
||||||
- name: Validate system features input types
|
- name: Validate system features input types
|
||||||
when: system.features is defined
|
when: system.features is defined
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
# Centralized normalization — all input dicts (system, hypervisor, disks)
|
# Centralized normalization - all input dicts (system, hypervisor, disks)
|
||||||
# are normalized here into system_cfg, hypervisor_cfg, etc.
|
# are normalized here into system_cfg, hypervisor_cfg, etc.
|
||||||
# Downstream roles consume these computed facts directly and do NOT need
|
# Downstream roles consume these computed facts directly and do NOT need
|
||||||
# per-role _normalize.yml (except CIS, which has its own input dict).
|
# per-role _normalize.yml (except CIS, which has its own input dict).
|
||||||
@@ -81,10 +81,14 @@
|
|||||||
when:
|
when:
|
||||||
- system_cfg.type == "virtual"
|
- system_cfg.type == "virtual"
|
||||||
- hypervisor_type != "vmware"
|
- hypervisor_type != "vmware"
|
||||||
|
vars:
|
||||||
|
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
ansible_user: "{{ system_cfg.users[0].name }}"
|
ansible_host: "{{ system_cfg.network.ip }}"
|
||||||
ansible_password: "{{ system_cfg.users[0].password }}"
|
ansible_port: 22
|
||||||
ansible_become_password: "{{ system_cfg.users[0].password }}"
|
ansible_user: "{{ _primary.key }}"
|
||||||
|
ansible_password: "{{ _primary.value.password }}"
|
||||||
|
ansible_become_password: "{{ _primary.value.password }}"
|
||||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
@@ -92,12 +96,12 @@
|
|||||||
when: hypervisor_type == "vmware"
|
when: hypervisor_type == "vmware"
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
ansible_connection: vmware_tools
|
ansible_connection: vmware_tools
|
||||||
ansible_vmware_host: "{{ hypervisor_cfg.url }}"
|
ansible_host: "{{ hypervisor_cfg.url }}"
|
||||||
ansible_vmware_port: 443
|
ansible_port: 443
|
||||||
|
ansible_user: root
|
||||||
|
ansible_password: ""
|
||||||
ansible_vmware_user: "{{ hypervisor_cfg.username }}"
|
ansible_vmware_user: "{{ hypervisor_cfg.username }}"
|
||||||
ansible_vmware_password: "{{ hypervisor_cfg.password }}"
|
ansible_vmware_password: "{{ hypervisor_cfg.password }}"
|
||||||
ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}"
|
ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}"
|
||||||
ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}"
|
ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}"
|
||||||
ansible_vmware_tools_user: root
|
|
||||||
ansible_vmware_tools_password: "{{ system_cfg.root.password }}"
|
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|||||||
@@ -17,6 +17,27 @@
|
|||||||
- name: Normalize disk configuration
|
- name: Normalize disk configuration
|
||||||
ansible.builtin.include_tasks: _normalize_disks.yml
|
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
|
- name: Check if pre-computed system_cfg needs enrichment
|
||||||
when: system_cfg is defined
|
when: system_cfg is defined
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
@@ -35,7 +56,7 @@
|
|||||||
- _bootstrap_needs_enrichment | default(false) | bool
|
- _bootstrap_needs_enrichment | default(false) | bool
|
||||||
- system_cfg.mirror | default('') | string | trim | length == 0
|
- system_cfg.mirror | default('') | string | trim | length == 0
|
||||||
vars:
|
vars:
|
||||||
# Same as _normalize_system.yml — kept in sync manually.
|
# Same as _normalize_system.yml - kept in sync manually.
|
||||||
_mirror_defaults:
|
_mirror_defaults:
|
||||||
debian: "https://deb.debian.org/debian/"
|
debian: "https://deb.debian.org/debian/"
|
||||||
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
or (
|
or (
|
||||||
os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"]
|
os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"]
|
||||||
) or (
|
) or (
|
||||||
os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 43
|
os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 45
|
||||||
) or (
|
) or (
|
||||||
os in ["rocky", "almalinux"]
|
os in ["rocky", "almalinux"]
|
||||||
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
|
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
os in ["ubuntu", "ubuntu-lts"]
|
os in ["ubuntu", "ubuntu-lts"]
|
||||||
and (os_version | default('') | string | length) == 0
|
and (os_version | default('') | string | length) == 0
|
||||||
) or (
|
) or (
|
||||||
os in ["alpine", "archlinux", "opensuse", "void"]
|
os == "archlinux"
|
||||||
)
|
)
|
||||||
fail_msg: "Invalid os/version specified. Please check README.md for supported values."
|
fail_msg: "Invalid os/version specified. Please check README.md for supported values."
|
||||||
quiet: true
|
quiet: true
|
||||||
@@ -232,6 +232,69 @@
|
|||||||
fail_msg: Invalid feature flags were specified, please check your inventory/vars.
|
fail_msg: Invalid feature flags were specified, please check your inventory/vars.
|
||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
|
- name: Validate hardware feature flags
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- system_cfg.features.firmware.enabled is defined
|
||||||
|
- system_cfg.features.firmware.microcode is defined
|
||||||
|
- system_cfg.features.gpu.enabled is defined
|
||||||
|
- system_cfg.features.gpu.nvidia_driver in ["auto", "open", "proprietary", "nouveau"]
|
||||||
|
- system_cfg.features.peripherals.enabled is defined
|
||||||
|
- system_cfg.features.peripherals.fingerprint in ["auto", "true", "false"]
|
||||||
|
- system_cfg.features.peripherals.webcam in ["auto", "true", "false"]
|
||||||
|
- system_cfg.features.peripherals.displaylink is defined
|
||||||
|
- system_cfg.features.hardware.profile is mapping
|
||||||
|
fail_msg: >-
|
||||||
|
Invalid hardware feature flags. firmware.enabled/microcode,
|
||||||
|
peripherals.enabled and peripherals.displaylink must be bool (or 'auto'
|
||||||
|
sentinel for firmware); gpu.nvidia_driver in
|
||||||
|
[auto|open|proprietary|nouveau]; peripherals.fingerprint and
|
||||||
|
peripherals.webcam in [auto|true|false]; hardware.profile must be a dict.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Validate desktop environment
|
||||||
|
when: system_cfg.features.desktop.enabled | bool
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- system_cfg.features.desktop.environment in ["gnome", "kde", "sway", "hyprland"]
|
||||||
|
- >-
|
||||||
|
system_cfg.features.desktop.environment not in ["sway", "hyprland"]
|
||||||
|
or os_family_map[os] | default('') == "Archlinux"
|
||||||
|
- >-
|
||||||
|
system_cfg.features.desktop.display_manager | default('') | length == 0
|
||||||
|
or system_cfg.features.desktop.display_manager in ["gdm", "sddm", "greetd"]
|
||||||
|
- >-
|
||||||
|
system_cfg.features.desktop.display_manager | default('') != "greetd"
|
||||||
|
or system_cfg.features.desktop.environment in ["sway", "hyprland"]
|
||||||
|
- >-
|
||||||
|
system_cfg.features.desktop.environment != "gnome"
|
||||||
|
or system_cfg.features.desktop.display_manager | default('') in ["", "gdm"]
|
||||||
|
- >-
|
||||||
|
system_cfg.features.desktop.environment != "kde"
|
||||||
|
or system_cfg.features.desktop.display_manager | default('') in ["", "sddm"]
|
||||||
|
fail_msg: >-
|
||||||
|
Invalid desktop config: environment '{{ system_cfg.features.desktop.environment }}'
|
||||||
|
for os_family '{{ os_family_map[os] | default('Unknown') }}',
|
||||||
|
display_manager '{{ system_cfg.features.desktop.display_manager | default('') }}'.
|
||||||
|
gnome and kde are available on all families; sway and hyprland are Archlinux only.
|
||||||
|
display_manager must be empty (auto) or match the environment's native DM:
|
||||||
|
gnome->gdm, kde->sddm, sway/hyprland->greetd. Only that DM's package is
|
||||||
|
installed, so a mismatched override fails at enable time.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Validate desktop autologin
|
||||||
|
when: system_cfg.features.desktop.enabled | bool
|
||||||
|
vars:
|
||||||
|
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- _autologin is boolean and not _autologin or (_autologin is string and _autologin | length > 0 and _autologin in system_cfg.users)
|
||||||
|
fail_msg: >-
|
||||||
|
desktop.autologin must be false or a username string present in
|
||||||
|
system.users; got '{{ _autologin }}'. Bool true is not accepted - the
|
||||||
|
resolver matches the value against system.users by name.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
- name: Validate virtual system sizing
|
- name: Validate virtual system sizing
|
||||||
when: system_cfg.type == "virtual"
|
when: system_cfg.type == "virtual"
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
@@ -242,7 +305,7 @@
|
|||||||
- (system_cfg.disks[0].size | float) > 0
|
- (system_cfg.disks[0].size | float) > 0
|
||||||
- (system_cfg.disks[0].size | float) >= 20
|
- (system_cfg.disks[0].size | float) >= 20
|
||||||
# Btrfs minimum disk: swap_size + 5.5 GiB overhead (subvolumes + metadata).
|
# 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.
|
# Swap sizing: memory < 16 GiB -> max(memory_GiB, 2); memory >= 16 GiB -> memory/2.
|
||||||
- >-
|
- >-
|
||||||
system_cfg.filesystem != "btrfs"
|
system_cfg.filesystem != "btrfs"
|
||||||
or (
|
or (
|
||||||
@@ -261,13 +324,16 @@
|
|||||||
fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size."
|
fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size."
|
||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
- name: Validate at least one user is defined
|
- name: Validate at least one user with a password is defined
|
||||||
|
vars:
|
||||||
|
_pw_users: "{{ system_cfg.users | dict2items | selectattr('value.password', 'defined') | list }}"
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- system_cfg.users | default([]) | length > 0
|
- system_cfg.users | default({}) | length > 0
|
||||||
- system_cfg.users[0].name is defined and (system_cfg.users[0].name | string | length) > 0
|
- _pw_users | length > 0
|
||||||
- system_cfg.users[0].password is defined and (system_cfg.users[0].password | string | length) > 0
|
- _pw_users[0].key | string | length > 0
|
||||||
fail_msg: "At least one user with a name and password must be defined in system.users[]."
|
- _pw_users[0].value.password | string | length > 0
|
||||||
|
fail_msg: "At least one user with a password must be defined in system.users."
|
||||||
quiet: true
|
quiet: true
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
partitioning_btrfs_compress_opt: "{{ 'compress=zstd:15' if system_cfg.features.zstd.enabled | bool else '' }}"
|
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.
|
# Partition separator: 'p' for NVMe/mmcblk (device path ends in digit), empty for SCSI/virtio.
|
||||||
# Examples: /dev/sda → /dev/sda1, /dev/nvme0n1 → /dev/nvme0n1p1
|
# Examples: /dev/sda -> /dev/sda1, /dev/nvme0n1 -> /dev/nvme0n1p1
|
||||||
partitioning_part_sep: "{{ 'p' if (install_drive | default('') | regex_search('\\d$')) else '' }}"
|
partitioning_part_sep: "{{ 'p' if (install_drive | default('') | regex_search('\\d$')) else '' }}"
|
||||||
partitioning_boot_partition_suffix: 1
|
partitioning_boot_partition_suffix: 1
|
||||||
partitioning_main_partition_suffix: 2
|
partitioning_main_partition_suffix: 2
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
ansible.builtin.command: >-
|
ansible.builtin.command: >-
|
||||||
tune2fs -O "^orphan_file,^metadata_csum_seed"
|
tune2fs -O "^orphan_file,^metadata_csum_seed"
|
||||||
"{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
|
"{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
|
||||||
register: partitioning_boot_ext4_tune_result
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Create swap filesystem
|
- name: Create swap filesystem
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
# Sizes are computed from disk_size_gb, memory_mb, and feature flags.
|
# Sizes are computed from disk_size_gb, memory_mb, and feature flags.
|
||||||
#
|
#
|
||||||
# Swap sizing:
|
# Swap sizing:
|
||||||
# - RAM >= 16 GB → swap = RAM/2 (in GB)
|
# - RAM >= 16 GB -> swap = RAM/2 (in GB)
|
||||||
# - RAM < 16 GB → swap = max(RAM_GB, 2)
|
# - RAM < 16 GB -> swap = max(RAM_GB, 2)
|
||||||
# - Capped to: min(target, 4 + max(disk - overhead, 0))
|
# - Capped to: min(target, 4 + max(disk - overhead, 0))
|
||||||
# - Further capped to: max available after subtracting reserved + CIS + extent reserve + 4 GB buffer
|
# - Further capped to: max available after subtracting reserved + CIS + extent reserve + 4 GB buffer
|
||||||
#
|
#
|
||||||
# Root sizing:
|
# Root sizing:
|
||||||
# - Full-disk mode (default): disk - reserved - swap - extent_reserve - (CIS volumes if enabled)
|
# - 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
|
# - Partial mode: tiered - <4 GB available -> 4 GB, 4-12 GB -> all available, >12 GB -> 40% of disk
|
||||||
#
|
#
|
||||||
# CIS volumes (only when CIS enabled):
|
# CIS volumes (only when CIS enabled):
|
||||||
# - /home: max(min(home_raw, home_max), home_min) where home_raw = (disk - overhead) * 10%
|
# - /home: max(min(home_raw, home_max), home_min) where home_raw = (disk - overhead) * 10%
|
||||||
|
|||||||
@@ -65,9 +65,7 @@
|
|||||||
ansible.builtin.command: "{{ item }}"
|
ansible.builtin.command: "{{ item }}"
|
||||||
loop:
|
loop:
|
||||||
- "partprobe {{ install_drive }}"
|
- "partprobe {{ install_drive }}"
|
||||||
- "blockdev --rereadpt {{ install_drive }}"
|
|
||||||
- "udevadm settle"
|
- "udevadm settle"
|
||||||
register: partitioning_partprobe_result
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
@@ -91,9 +89,7 @@
|
|||||||
ansible.builtin.command: "{{ item }}"
|
ansible.builtin.command: "{{ item }}"
|
||||||
loop:
|
loop:
|
||||||
- "partprobe {{ install_drive }}"
|
- "partprobe {{ install_drive }}"
|
||||||
- "blockdev --rereadpt {{ install_drive }}"
|
|
||||||
- "udevadm settle"
|
- "udevadm settle"
|
||||||
register: partitioning_partprobe_retry
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
@@ -116,6 +112,5 @@
|
|||||||
loop:
|
loop:
|
||||||
- "partprobe {{ install_drive }}"
|
- "partprobe {{ install_drive }}"
|
||||||
- "udevadm settle"
|
- "udevadm settle"
|
||||||
register: partitioning_partprobe_settle
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|||||||
@@ -9,12 +9,13 @@
|
|||||||
- >-
|
- >-
|
||||||
system_cfg.features.cis.enabled | bool or (
|
system_cfg.features.cis.enabled | bool or (
|
||||||
not (system_cfg.features.cis.enabled | bool) and (
|
not (system_cfg.features.cis.enabled | bool) and (
|
||||||
(system_cfg.filesystem == 'btrfs' and item.path in ['/home', '/var/log', '/var/cache/pacman/pkg'])
|
(system_cfg.filesystem == 'btrfs' and item.path in ['/home', '/var/log']
|
||||||
|
+ (['/var/cache/pacman/pkg'] if os == 'archlinux' else []))
|
||||||
or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg'])
|
or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg'])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
- >-
|
- >-
|
||||||
not (item.path in ['/swap', '/var/cache/pacman/pkg'] and system_cfg.filesystem != 'btrfs')
|
not (item.path in ['/swap', '/var/cache/pacman/pkg'] and (system_cfg.filesystem != 'btrfs' or os != 'archlinux'))
|
||||||
- system_cfg.features.swap.enabled | bool or item.path != '/swap'
|
- system_cfg.features.swap.enabled | bool or item.path != '/swap'
|
||||||
ansible.posix.mount:
|
ansible.posix.mount:
|
||||||
path: /mnt{{ item.path }}
|
path: /mnt{{ item.path }}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
algorithm: "{{ system_cfg.luks.pbkdf }}"
|
algorithm: "{{ system_cfg.luks.pbkdf }}"
|
||||||
iteration_time: "{{ (system_cfg.luks.iter | float) / 1000 }}"
|
iteration_time: "{{ (system_cfg.luks.iter | float) / 1000 }}"
|
||||||
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
||||||
register: partitioning_luks_format_result
|
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Force-close LUKS mapper
|
- name: Force-close LUKS mapper
|
||||||
@@ -51,7 +50,6 @@
|
|||||||
name: "{{ system_cfg.luks.mapper }}"
|
name: "{{ system_cfg.luks.mapper }}"
|
||||||
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
||||||
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
|
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
|
||||||
register: partitioning_luks_open_result
|
|
||||||
no_log: true
|
no_log: true
|
||||||
rescue:
|
rescue:
|
||||||
- name: Force-close stale LUKS mapper
|
- name: Force-close stale LUKS mapper
|
||||||
@@ -79,7 +77,6 @@
|
|||||||
name: "{{ system_cfg.luks.mapper }}"
|
name: "{{ system_cfg.luks.mapper }}"
|
||||||
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
passphrase: "{{ system_cfg.luks.passphrase | string }}"
|
||||||
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
|
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
|
||||||
register: partitioning_luks_open_retry
|
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Get LUKS UUID
|
- name: Get LUKS UUID
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
when:
|
when:
|
||||||
- system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit']
|
- system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit']
|
||||||
- system_cfg.features.swap.enabled | bool or item.subvol != 'swap'
|
- 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 }}
|
ansible.builtin.command: btrfs su cr /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
|
||||||
args:
|
args:
|
||||||
creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
|
creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
|
||||||
@@ -51,12 +52,21 @@
|
|||||||
- { subvol: swap }
|
- { subvol: swap }
|
||||||
- { subvol: home }
|
- { subvol: home }
|
||||||
- { subvol: var }
|
- { subvol: var }
|
||||||
- { subvol: pkg }
|
- { subvol: pkg, os: [archlinux] }
|
||||||
- { subvol: var_log }
|
- { subvol: var_log }
|
||||||
- { subvol: var_log_audit }
|
- { subvol: var_log_audit }
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.subvol }}"
|
label: "{{ item.subvol }}"
|
||||||
register: partitioning_btrfs_subvol_result
|
|
||||||
|
- name: Set default btrfs subvolume to @
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: >-
|
||||||
|
set -o pipefail &&
|
||||||
|
btrfs subvolume list /mnt | awk '/ path @$/ {print $2}'
|
||||||
|
| xargs -I{} btrfs subvolume set-default {} /mnt
|
||||||
|
executable: /bin/bash
|
||||||
|
register: partitioning_btrfs_default_result
|
||||||
|
changed_when: partitioning_btrfs_default_result.rc == 0
|
||||||
|
|
||||||
- name: Set quotas for subvolumes
|
- name: Set quotas for subvolumes
|
||||||
when: system_cfg.features.cis.enabled | bool
|
when: system_cfg.features.cis.enabled | bool
|
||||||
@@ -74,7 +84,6 @@
|
|||||||
btrfs filesystem mkswapfile --size {{ partitioning_swap_size_gb }}g --uuid clear /mnt/@swap/swapfile
|
btrfs filesystem mkswapfile --size {{ partitioning_swap_size_gb }}g --uuid clear /mnt/@swap/swapfile
|
||||||
args:
|
args:
|
||||||
creates: /mnt/@swap/swapfile
|
creates: /mnt/@swap/swapfile
|
||||||
register: partitioning_btrfs_swap_result
|
|
||||||
|
|
||||||
- name: Unmount Partition
|
- name: Unmount Partition
|
||||||
ansible.posix.mount:
|
ansible.posix.mount:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
# Cloud-init support matrix:
|
# Cloud-init support matrix:
|
||||||
# libvirt — cloud-init ISO attached as CDROM (user-data + network-config)
|
# libvirt - cloud-init ISO attached as CDROM (user-data + network-config)
|
||||||
# proxmox — cloud-init via Proxmox API (cicustom, ciuser, cipassword, etc.)
|
# proxmox - cloud-init via Proxmox API (cicustom, ciuser, cipassword, etc.)
|
||||||
# vmware — no cloud-init; configuration is applied post-install via chroot
|
# vmware - no cloud-init; configuration is applied post-install via chroot
|
||||||
# xen — no cloud-init; configuration is applied post-install via chroot
|
# xen - no cloud-init; configuration is applied post-install via chroot
|
||||||
virtualization_libvirt_image_dir: >-
|
virtualization_libvirt_image_dir: >-
|
||||||
{{
|
{{
|
||||||
system_cfg.path
|
system_cfg.path
|
||||||
@@ -17,15 +17,25 @@ virtualization_libvirt_cloudinit_path: >-
|
|||||||
virtualization_xen_disk_path: /var/lib/xen/images
|
virtualization_xen_disk_path: /var/lib/xen/images
|
||||||
|
|
||||||
virtualization_libvirt_machine_type: q35
|
virtualization_libvirt_machine_type: q35
|
||||||
virtualization_libvirt_ovmf_code: /usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd
|
# Secboot OVMF firmware candidates, ordered Arch, Debian/Ubuntu, Fedora/RHEL.
|
||||||
virtualization_libvirt_ovmf_vars: /usr/share/edk2/x64/OVMF_VARS.4m.fd
|
# libvirt.yml resolves these to the first file present on the controller.
|
||||||
|
virtualization_libvirt_ovmf_code_candidates:
|
||||||
|
- /usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd
|
||||||
|
- /usr/share/OVMF/OVMF_CODE_4M.secboot.fd
|
||||||
|
- /usr/share/edk2/ovmf/OVMF_CODE.secboot.fd
|
||||||
|
- /usr/share/OVMF/OVMF_CODE.secboot.fd
|
||||||
|
virtualization_libvirt_ovmf_vars_candidates:
|
||||||
|
- /usr/share/edk2/x64/OVMF_VARS.4m.fd
|
||||||
|
- /usr/share/OVMF/OVMF_VARS_4M.fd
|
||||||
|
- /usr/share/edk2/ovmf/OVMF_VARS.fd
|
||||||
|
- /usr/share/OVMF/OVMF_VARS.fd
|
||||||
|
|
||||||
virtualization_tpm2_enabled: >-
|
virtualization_tpm2_enabled: >-
|
||||||
{{
|
{{
|
||||||
(system_cfg.luks.enabled | bool)
|
(
|
||||||
and (system_cfg.luks.auto | bool)
|
(system_cfg.luks.enabled | bool)
|
||||||
and (
|
and (system_cfg.luks.auto | bool)
|
||||||
(system_cfg.luks.method | lower)
|
and (system_cfg.luks.method | lower == 'tpm2')
|
||||||
== 'tpm2'
|
|
||||||
)
|
)
|
||||||
|
or (system_cfg.features.secure_boot.enabled | default(false) | bool)
|
||||||
}}
|
}}
|
||||||
|
|||||||
26
roles/virtualization/tasks/_xen_disks.yml
Normal file
26
roles/virtualization/tasks/_xen_disks.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
- name: Build Xen disk definitions
|
||||||
|
when: virtualization_xen_disks is not defined
|
||||||
|
block:
|
||||||
|
- name: Compute Xen disk configuration
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
virtualization_xen_disks: "{{ virtualization_xen_disks | default([]) + [_xen_disk_cfg] }}"
|
||||||
|
vars:
|
||||||
|
device_letter_map: "{{ disk_letter_map }}"
|
||||||
|
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
|
||||||
|
_xen_disk_cfg: >-
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
'path': (
|
||||||
|
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
|
||||||
|
if ansible_loop.index0 == 0
|
||||||
|
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
|
||||||
|
),
|
||||||
|
'target': 'xvd' ~ device_letter,
|
||||||
|
'size': (item.size | float)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loop: "{{ system_cfg.disks }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item | to_json }}"
|
||||||
|
extended: true
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Undefine libvirt VM
|
- name: Undefine libvirt VM
|
||||||
community.libvirt.virt:
|
ansible.builtin.command:
|
||||||
name: "{{ hostname }}"
|
cmd: "virsh -c {{ libvirt_uri | default('qemu:///system') }} undefine {{ hostname }} --nvram"
|
||||||
command: undefine
|
register: _libvirt_undefine_result
|
||||||
uri: "{{ libvirt_uri | default('qemu:///system') }}"
|
changed_when: _libvirt_undefine_result.rc == 0
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove libvirt disk images
|
- name: Remove libvirt disk images
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
- xl
|
- xl
|
||||||
- destroy
|
- destroy
|
||||||
- "{{ hostname }}"
|
- "{{ hostname }}"
|
||||||
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove Xen VM config
|
- name: Remove Xen VM config
|
||||||
|
|||||||
@@ -70,6 +70,19 @@
|
|||||||
- /tmp/cloud-user-data-{{ hostname }}.yml
|
- /tmp/cloud-user-data-{{ hostname }}.yml
|
||||||
- /tmp/cloud-network-config-{{ hostname }}.yml
|
- /tmp/cloud-network-config-{{ hostname }}.yml
|
||||||
|
|
||||||
|
# Resolve OVMF firmware to the first candidate present on the controller
|
||||||
|
# unless the user pinned an explicit path. first_found needs the localhost
|
||||||
|
# delegation since the candidates live on the libvirt host, not the target.
|
||||||
|
- name: Resolve OVMF firmware paths
|
||||||
|
delegate_to: localhost
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
virtualization_libvirt_ovmf_code: >-
|
||||||
|
{{ virtualization_libvirt_ovmf_code if virtualization_libvirt_ovmf_code | default('', true) | length > 0
|
||||||
|
else lookup('ansible.builtin.first_found', virtualization_libvirt_ovmf_code_candidates) }}
|
||||||
|
virtualization_libvirt_ovmf_vars: >-
|
||||||
|
{{ virtualization_libvirt_ovmf_vars if virtualization_libvirt_ovmf_vars | default('', true) | length > 0
|
||||||
|
else lookup('ansible.builtin.first_found', virtualization_libvirt_ovmf_vars_candidates) }}
|
||||||
|
|
||||||
# uri defaults to qemu:///system (local libvirtd)
|
# uri defaults to qemu:///system (local libvirtd)
|
||||||
- name: Create VM using libvirt
|
- name: Create VM using libvirt
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{{ out }}
|
{{ out }}
|
||||||
community.proxmox.proxmox_kvm:
|
community.proxmox.proxmox_kvm:
|
||||||
ciuser: "{{ system_cfg.users[0].name }}"
|
ciuser: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).key }}"
|
||||||
cipassword: "{{ system_cfg.users[0].password }}"
|
cipassword: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).value.password }}"
|
||||||
ciupgrade: false
|
ciupgrade: false
|
||||||
vmid: "{{ system_cfg.id }}"
|
vmid: "{{ system_cfg.id }}"
|
||||||
name: "{{ hostname }}"
|
name: "{{ hostname }}"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
esxi_hostname: "{{ hypervisor_cfg.node if (hypervisor_cfg.node | default('') | length > 0) else omit }}"
|
esxi_hostname: "{{ hypervisor_cfg.node if (hypervisor_cfg.node | default('') | length > 0) else omit }}"
|
||||||
folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
|
folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
|
||||||
name: "{{ hostname }}"
|
name: "{{ hostname }}"
|
||||||
# Generic guest ID — VMware auto-detects OS post-install
|
# Generic guest ID - VMware auto-detects OS post-install
|
||||||
guest_id: otherLinux64Guest
|
guest_id: otherLinux64Guest
|
||||||
annotation: |
|
annotation: |
|
||||||
{{ note if note is defined else '' }}
|
{{ note if note is defined else '' }}
|
||||||
|
|||||||
@@ -2,28 +2,7 @@
|
|||||||
- name: Deploy VM on Xen
|
- name: Deploy VM on Xen
|
||||||
block:
|
block:
|
||||||
- name: Build disk definitions
|
- name: Build disk definitions
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.include_tasks: _xen_disks.yml
|
||||||
virtualization_xen_disks: "{{ virtualization_xen_disks | default([]) + [virtualization_xen_disk_cfg] }}"
|
|
||||||
vars:
|
|
||||||
device_letter_map: "{{ disk_letter_map }}"
|
|
||||||
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
|
|
||||||
virtualization_xen_disk_cfg: >-
|
|
||||||
{{
|
|
||||||
{
|
|
||||||
'path': (
|
|
||||||
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
|
|
||||||
if ansible_loop.index0 == 0
|
|
||||||
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
|
|
||||||
),
|
|
||||||
'target': 'xvd' ~ device_letter,
|
|
||||||
'size': (item.size | float)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
loop: "{{ system_cfg.disks }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item | to_json }}"
|
|
||||||
extended: true
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Create VM disks for Xen
|
- name: Create VM disks for Xen
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|||||||
@@ -4,17 +4,25 @@ ssh_pwauth: true
|
|||||||
package_update: false
|
package_update: false
|
||||||
package_upgrade: false
|
package_upgrade: false
|
||||||
users:
|
users:
|
||||||
{% for user in system_cfg.users %}
|
{% for username, attrs in system_cfg.users.items() %}
|
||||||
- name: "{{ user.name }}"
|
- name: "{{ username }}"
|
||||||
primary_group: "{{ user.name }}"
|
primary_group: "{{ username }}"
|
||||||
groups: users
|
groups: users
|
||||||
|
{% set _sudo = attrs.sudo | default(false) %}
|
||||||
|
{% if _sudo is string %}
|
||||||
|
sudo: "{{ _sudo }}"
|
||||||
|
{% elif _sudo | bool %}
|
||||||
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
passwd: "{{ user.password | password_hash('sha512') }}"
|
{% endif %}
|
||||||
|
{% if attrs.password | default('') | length > 0 %}
|
||||||
|
passwd: "{{ attrs.password | password_hash('sha512') }}"
|
||||||
lock_passwd: false
|
lock_passwd: false
|
||||||
{% set ssh_keys = user['keys'] | default([]) %}
|
{% else %}
|
||||||
{% if ssh_keys | length > 0 %}
|
lock_passwd: true
|
||||||
|
{% endif %}
|
||||||
|
{% if 'keys' in attrs and attrs['keys'] is iterable and attrs['keys'] is not string and attrs['keys'] | length > 0 %}
|
||||||
ssh_authorized_keys:
|
ssh_authorized_keys:
|
||||||
{% for key in ssh_keys %}
|
{% for key in attrs['keys'] %}
|
||||||
- "{{ key }}"
|
- "{{ key }}"
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ system:
|
|||||||
path: /data
|
path: /data
|
||||||
fstype: ext4
|
fstype: ext4
|
||||||
users:
|
users:
|
||||||
- name: "admin"
|
admin:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
@@ -61,3 +61,17 @@ system:
|
|||||||
sudo: true
|
sudo: true
|
||||||
chroot:
|
chroot:
|
||||||
tool: "arch-chroot"
|
tool: "arch-chroot"
|
||||||
|
desktop:
|
||||||
|
enabled: false
|
||||||
|
environment: "" # gnome|kde|sway|hyprland (sway/hyprland are Archlinux only)
|
||||||
|
autologin: false # false | a username from system.users (not bool true)
|
||||||
|
# firmware/microcode default to "auto" - on for physical, off for virtual.
|
||||||
|
# gpu and peripherals.* default to "auto" tied to desktop.enabled.
|
||||||
|
# Override only when you want non-default behavior.
|
||||||
|
gpu:
|
||||||
|
enabled: "auto" # auto = follows desktop.enabled; set true to force
|
||||||
|
nvidia_driver: "auto" # auto = open|proprietary|nouveau by GPU generation
|
||||||
|
peripherals:
|
||||||
|
displaylink: false # explicit opt-in for DisplayLink docks
|
||||||
|
hardware:
|
||||||
|
profile: {} # autodetect; or set to bake a golden-image profile
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ system:
|
|||||||
label: DATA
|
label: DATA
|
||||||
opts: defaults
|
opts: defaults
|
||||||
users:
|
users:
|
||||||
- name: "ops"
|
ops:
|
||||||
password: "CHANGE_ME"
|
password: "CHANGE_ME"
|
||||||
keys:
|
keys:
|
||||||
- "ssh-ed25519 AAAA..."
|
- "ssh-ed25519 AAAA..."
|
||||||
@@ -79,8 +79,6 @@ system:
|
|||||||
iter: 4000
|
iter: 4000
|
||||||
bits: 512
|
bits: 512
|
||||||
pbkdf: "argon2id"
|
pbkdf: "argon2id"
|
||||||
urandom: true
|
|
||||||
verify: true
|
|
||||||
packages:
|
packages:
|
||||||
- jq
|
- jq
|
||||||
- tmux
|
- tmux
|
||||||
|
|||||||
Reference in New Issue
Block a user