Compare commits
34 Commits
6bfaa0aa2b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 41ccf2a5b9 | |||
| 8f9cfe3b2f | |||
| 6a75237197 | |||
| b04aad12fb | |||
| 4fff9f8d80 | |||
| 7f12a0f3d8 | |||
| ceb2237bbb | |||
| 477c8379c4 | |||
| 579c499c02 | |||
| 89e366d0f0 | |||
| 6fe843355e | |||
| 441876fab9 | |||
| 00acd4d200 | |||
| d922efd2e4 | |||
| 939c5c741f | |||
| 2c35409519 | |||
| d2a19cfd5c | |||
| 44f5adc682 | |||
| 0185797af9 | |||
| e0ecf628cd | |||
| 37df881daa | |||
| 55b21eae5d | |||
| b1e938b7f0 | |||
| c843f5289b | |||
| 9757ed3785 | |||
| 876e90ce2b | |||
| 7c44cb1ff0 | |||
| 5d0630a386 | |||
| 3eaf918a53 | |||
| 382e82ff85 | |||
| db7dc53bd7 | |||
| 7d45f25a7e | |||
| 3880b8f41e | |||
| dc3c4a901f |
271
README.md
271
README.md
@@ -13,7 +13,7 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
- 4.1 [Core Variables](#41-core-variables)
|
||||
- 4.2 [`system` Dictionary](#42-system-dictionary)
|
||||
- 4.3 [`hypervisor` Dictionary](#43-hypervisor-dictionary)
|
||||
- 4.4 [`cis` Dictionary](#44-cis-dictionary)
|
||||
- 4.4 [CIS Hardening](#44-cis-hardening)
|
||||
- 4.5 [VMware Guest Operations](#45-vmware-guest-operations)
|
||||
- 4.6 [Multi-Disk Schema](#46-multi-disk-schema)
|
||||
- 4.7 [Advanced Partitioning Overrides](#47-advanced-partitioning-overrides)
|
||||
@@ -29,17 +29,14 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
|
||||
| `system.os` | Distribution | `system.version` |
|
||||
| ------------ | ------------------------ | ------------------------------------- |
|
||||
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
|
||||
| `alpine` | Alpine Linux | latest (rolling) |
|
||||
| `almalinux` | AlmaLinux | `9`, `10` |
|
||||
| `archlinux` | Arch Linux | latest (rolling) |
|
||||
| `debian` | Debian | `10`-`13`, `unstable` |
|
||||
| `fedora` | Fedora | `38`-`45` |
|
||||
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
|
||||
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
|
||||
| `rocky` | Rocky Linux | `8`, `9`, `10` |
|
||||
| `ubuntu` | Ubuntu (latest non-LTS) | optional (e.g. `24.04`) |
|
||||
| `ubuntu-lts` | Ubuntu LTS | optional (e.g. `24.04`) |
|
||||
| `void` | Void Linux | latest (rolling) |
|
||||
| `debian` | Debian | `12`, `13`, `unstable` |
|
||||
| `fedora` | Fedora | `43`, `44` |
|
||||
| `rhel` | Red Hat Enterprise Linux | `9`, `10` |
|
||||
| `rocky` | Rocky Linux | `9`, `10` |
|
||||
| `ubuntu` | Ubuntu (latest non-LTS) | optional (tracks 25.10 `questing`) |
|
||||
| `ubuntu-lts` | Ubuntu LTS | optional (tracks 26.04 `resolute`) |
|
||||
|
||||
### Hypervisors
|
||||
|
||||
@@ -62,12 +59,10 @@ Non-Arch targets require the appropriate package manager available from the ISO
|
||||
|
||||
Two dict-based variables drive the entire configuration:
|
||||
|
||||
- **`system`** -- host, network, users, disk layout, encryption, and feature toggles
|
||||
- **`system`** -- host, network, users, disk layout, encryption, and feature toggles (including CIS hardening under `system.features.cis`)
|
||||
- **`hypervisor`** -- virtualization backend credentials and targeting
|
||||
|
||||
An optional third dict **`cis`** overrides CIS hardening parameters when `system.features.cis.enabled: true`.
|
||||
|
||||
All three are standard Ansible variables. Place them in `group_vars/`, `host_vars/`, or inline inventory. With `hash_behaviour = merge`, dictionaries merge across scopes, so shared values go in group vars and host-specific overrides go per-host.
|
||||
Both are standard Ansible variables. Place them in `group_vars/`, `host_vars/`, or inline inventory. With `hash_behaviour = merge`, dictionaries merge across scopes, so shared values go in group vars and host-specific overrides go per-host.
|
||||
|
||||
### Variable Placement
|
||||
|
||||
@@ -122,7 +117,7 @@ all:
|
||||
path: /data
|
||||
fstype: xfs
|
||||
users:
|
||||
- name: ops
|
||||
ops:
|
||||
password: !vault |
|
||||
$ANSIBLE_VAULT...
|
||||
keys:
|
||||
@@ -151,7 +146,7 @@ all:
|
||||
|
||||
### 4.1 Core Variables
|
||||
|
||||
Top-level variables outside `system`/`hypervisor`/`cis`.
|
||||
Top-level variables outside `system`/`hypervisor`.
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
| ---------------- | ------ | -------------------------- | ---------------------------------------------------- |
|
||||
@@ -167,7 +162,7 @@ Top-level variables outside `system`/`hypervisor`/`cis`.
|
||||
| `type` | string | `virtual` | `virtual` or `physical` |
|
||||
| `os` | string | -- | Target distribution (see [table](#distributions)) |
|
||||
| `version` | string | -- | Version selector for versioned distros |
|
||||
| `filesystem` | string | -- | `btrfs`, `ext4`, or `xfs` |
|
||||
| `filesystem` | string | `ext4` | `btrfs`, `ext4`, or `xfs` |
|
||||
| `name` | string | inventory hostname | Final hostname |
|
||||
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
|
||||
| `locale` | string | `en_US.UTF-8` | System locale |
|
||||
@@ -176,15 +171,35 @@ Top-level variables outside `system`/`hypervisor`/`cis`.
|
||||
| `cpus` | int | `0` | vCPU count (required for virtual) |
|
||||
| `memory` | int | `0` | Memory in MiB (required for virtual) |
|
||||
| `balloon` | int | `0` | Balloon memory in MiB (Proxmox) |
|
||||
| `path` | string | -- | Hypervisor folder/path |
|
||||
| `path` | string | -- | Hypervisor folder/path (falls back to `hypervisor.folder`) |
|
||||
| `content` | dict | see below | Package content source (mirror/DVD/Satellite, family-resolved) |
|
||||
| `packages` | list | `[]` | Additional packages installed post-reboot |
|
||||
| `network` | dict | see below | Network configuration |
|
||||
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#46-multi-disk-schema)) |
|
||||
| `users` | list | `[]` | User accounts |
|
||||
| `users` | dict | `{}` | User accounts (keyed by username) |
|
||||
| `root` | dict | see below | Root account settings |
|
||||
| `luks` | dict | see below | Encryption settings |
|
||||
| `features` | dict | see below | Feature toggles |
|
||||
|
||||
#### `system.content`
|
||||
|
||||
Uniform package content source, family-resolved. `source: ''` defaults to `dvd` on EL and `mirror` on Debian/Ubuntu/Arch. Satellite values come from inventory/vault only, never committed code.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------------------------- | ------ | -------------- | ----------------------------------------------------------------- |
|
||||
| `source` | string | family default | `dvd`, `mirror`, `satellite`, or `none` |
|
||||
| `url` | string | family default | Mirror URL / EL `.repo` baseurl |
|
||||
| `proxy` | string | -- | `http://host:port` content proxy (dnf/apt/pacman) |
|
||||
| `gpgcheck` | bool | `true` | Repository GPG checking |
|
||||
| `satellite.host` | string | -- | EL Katello/Satellite hostname |
|
||||
| `satellite.ip` | string | -- | Optional `/etc/hosts` entry when DNS does not resolve the host |
|
||||
| `satellite.org` | string | -- | Organization label |
|
||||
| `satellite.activation_key` | string | -- | Activation key |
|
||||
| `satellite.ca_url` | string | derived | Katello CA RPM URL (default `https://<host>/pub/katello-ca-consumer-latest.noarch.rpm`) |
|
||||
| `satellite.service_level` | string | -- | syspurpose service level |
|
||||
| `satellite.environment` | string | -- | Lifecycle environment |
|
||||
| `satellite.install` | bool | `false` | `false`: base from DVD/mirror then register; `true`: install from Satellite |
|
||||
|
||||
#### `system.network`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
@@ -229,8 +244,9 @@ Users must be defined in inventory. The dict format enables additive merging acr
|
||||
#### `system.root`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ---------- | ------ | ------- | ------------- |
|
||||
| ---------- | ------ | ----------- | ------------- |
|
||||
| `password` | string | -- | Root password |
|
||||
| `shell` | string | `/bin/bash` | Login shell |
|
||||
|
||||
#### `system.luks`
|
||||
|
||||
@@ -249,8 +265,6 @@ Users must be defined in inventory. The dict format enables additive merging acr
|
||||
| `iter` | int | `4000` | PBKDF iteration time (ms) |
|
||||
| `bits` | int | `512` | Key size (bits) |
|
||||
| `pbkdf` | string | `argon2id` | PBKDF algorithm |
|
||||
| `urandom` | bool | `true` | Use urandom during key generation |
|
||||
| `verify` | bool | `true` | Verify passphrase during format |
|
||||
|
||||
#### `system.luks.tpm2`
|
||||
|
||||
@@ -271,7 +285,10 @@ The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------ | ------ | -------------- | ------------------------------------ |
|
||||
| `cis.enabled` | bool | `false` | Enable CIS hardening (see [4.4](#44-cis-dictionary)) |
|
||||
| `cis.enabled` | bool | `false` | Enable CIS hardening (see [4.4](#44-cis-hardening)) |
|
||||
| `cis.profile` | string | `default` | CIS profile: `default`, `l1`, or `l2` (see [4.4](#44-cis-hardening)) |
|
||||
| `cis.rules` | dict | `{}` | Per-rule CIS overrides |
|
||||
| `cis.params` | dict | `{}` | CIS parameter overrides |
|
||||
| `selinux.enabled` | bool | `true` | SELinux management |
|
||||
| `firewall.enabled` | bool | `true` | Firewall setup |
|
||||
| `firewall.backend` | string | `firewalld` | `firewalld` or `ufw` |
|
||||
@@ -283,9 +300,15 @@ The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `
|
||||
| `banner.sudo` | bool | `true` | Sudo banner |
|
||||
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` |
|
||||
| `initramfs.generator` | string | auto-detected | Override initramfs generator (see below) |
|
||||
| `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, camera, audio, bluetooth, 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.
|
||||
**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.
|
||||
@@ -295,13 +318,147 @@ On distros with older dracut (no `tpm2-tss` module), clevis is used as a fallbac
|
||||
| Key | Type | Default | Description |
|
||||
| ----------------- | ------ | -------------- | ----------------------------------------- |
|
||||
| `enabled` | bool | `false` | Install desktop environment |
|
||||
| `environment` | string | -- | `gnome`, `kde`, `xfce`, `sway`, `hyprland`, `cinnamon`, `mate`, `lxqt`, `budgie` |
|
||||
| `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, `lightdm`, `ly`, `greetd` |
|
||||
| `environment` | string | `""` | `gnome`, `kde`, `sway`, or `hyprland` |
|
||||
| `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, `plasma-login-manager`, `greetd`, or `ly` |
|
||||
| `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. On enterprise Linux
|
||||
(almalinux/rocky/rhel) the base desktop installs browser, PDF and image viewers but no
|
||||
video player - none is packaged in the EL base repositories, and no third-party repo is
|
||||
pulled in; add one from rpmfusion/flatpak if you need it.
|
||||
|
||||
When `enabled: true`, the bootstrap installs the desktop environment packages, enables the display manager
|
||||
and bluetooth services, and sets the systemd default target to `graphical.target`.
|
||||
|
||||
Display manager auto-detection: gnome→gdm, kde→sddm, xfce→lightdm, sway→greetd, hyprland→ly.
|
||||
Display manager auto-detection: gnome to gdm; kde to plasma-login-manager on Arch and
|
||||
Fedora 44+ (Plasma 6.6), else sddm; sway and hyprland to greetd.
|
||||
|
||||
`ly` is an explicit-only override (never auto-selected), available on Arch only,
|
||||
and is desktop-agnostic - it can front any environment. It runs on `tty2` with
|
||||
`getty@tty2` masked, and its autologin is written to `/etc/ly/config.ini`; set `session`
|
||||
to the target session's `.desktop` basename (sway and hyprland resolve automatically).
|
||||
|
||||
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.
|
||||
|
||||
> **Known limitation - Nvidia on Enterprise Linux (AlmaLinux/Rocky/RHEL):** the EL `akmod-nvidia*`
|
||||
> packages live in RPMFusion non-free, and the bootstrap only enables RPMFusion automatically on
|
||||
> **Fedora**, not on EL. So Nvidia on a bare EL desktop is best-effort: enable RPMFusion (or supply the
|
||||
> driver repo) out of band, or it falls back to `nouveau`. EL desktops are not a primary target.
|
||||
|
||||
#### 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 |
|
||||
| `camera` | bool \| `auto` | `auto` | `v4l-utils` for UVC webcams. `auto` = install when a UVC/IPU6 camera is detected (IPU6 out-of-tree stack is logged, not auto-installed) |
|
||||
| `audio` | bool \| `auto` | `auto` | SOF firmware + ALSA UCM. `auto` = install when an audio device is detected |
|
||||
| `bluetooth` | bool \| `auto` | `auto` | `bluez`. `auto` = install when a Bluetooth controller is detected |
|
||||
| `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 | `{}` | Full override: non-empty SKIPS detection (golden image); empty = autodetect |
|
||||
| group fields | mixed | -- | `cpu`/`gpus`/`wireless`/`audio`/`camera`/`fingerprint`/`bluetooth`/`packages`/`disable`/`kernel_params` MERGE over autodetect (see below) |
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
The same keys (minus `profile`) can also be set **directly under `hardware`** as a
|
||||
declarative **hardware group** that MERGES over auto-detection (auto-detect = base; the
|
||||
group supplements/overrides it). Unlike `profile`, which skips detection entirely, the
|
||||
group keeps detection running and layers on top - use it to pin everything a known device
|
||||
needs so nothing is ever under-set.
|
||||
|
||||
| Key | Type | Merge semantics |
|
||||
| ------------------------- | ---- | -------------------------------------------------------- |
|
||||
| `cpu` | str | pin the CPU vendor (overrides detection when non-empty) |
|
||||
| `gpus`/`wireless`/`audio` | list | union with the detected vendor codes |
|
||||
| `camera` | dict | `{uvc, ipu6}` booleans OR'd with detection |
|
||||
| `fingerprint`/`bluetooth` | bool | OR'd with detection (force-on) |
|
||||
| `packages` | dict | per-`os_family` extra packages, added to the install set (deduped; empty entries dropped) |
|
||||
| `disable` | list | feature/vendor names force-off, applied last |
|
||||
| `kernel_params` | list | extra kernel cmdline params, appended to the bootloader |
|
||||
|
||||
Example - a laptop with an Intel IPU6 camera (out-of-tree stack) and a Cirrus amp, pinned
|
||||
in a group's `group_vars`:
|
||||
|
||||
```yaml
|
||||
system:
|
||||
features:
|
||||
hardware:
|
||||
bluetooth: true # force-on if detection misses the combo card
|
||||
camera:
|
||||
ipu6: true # force the IPU6 path
|
||||
packages: # out-of-tree/AUR bits detection must not auto-install
|
||||
Archlinux: [intel-ipu6-dkms, v4l2-relayd, linux-firmware-cirrus]
|
||||
disable: [displaylink] # never pull DisplayLink on this device
|
||||
kernel_params: ["i915.enable_psr=0"]
|
||||
```
|
||||
|
||||
### 4.3 `hypervisor` Dictionary
|
||||
|
||||
@@ -315,47 +472,53 @@ Display manager auto-detection: gnome→gdm, kde→sddm, xfce→lightdm, sway→
|
||||
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
|
||||
| `datacenter` | string | -- | VMware datacenter |
|
||||
| `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) |
|
||||
|
||||
### 4.4 `cis` Dictionary
|
||||
### 4.4 CIS Hardening
|
||||
|
||||
When `system.features.cis.enabled: true`, the CIS role applies hardening. All values have sensible defaults; override specific keys via the `cis` dict.
|
||||
When `system.features.cis.enabled: true`, the CIS role applies hardening. The behaviour is driven by three keys under `system.features.cis`:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------------------- | ------ | ------- | ------------------------------------------------ |
|
||||
| `modules_blacklist` | list | see below | Kernel modules to blacklist via modprobe |
|
||||
| `sysctl` | dict | see below | Sysctl key/value pairs written to `10-cis.conf` |
|
||||
| `sshd_options` | list | see below | SSHD options applied via lineinfile |
|
||||
| `pwquality_minlen` | int | `14` | Minimum password length |
|
||||
| `tmout` | int | `900` | Shell timeout (seconds) |
|
||||
| `umask` | string | `077` | Default umask in bashrc |
|
||||
| `umask_profile` | string | `027` | Default umask in /etc/profile |
|
||||
| `faillock_deny` | int | `5` | Failed login attempts before lockout |
|
||||
| `faillock_unlock_time` | int | `900` | Lockout duration (seconds) |
|
||||
| `password_remember` | int | `5` | Password history depth |
|
||||
| --------- | ------ | ----------- | ----------------------------------------------------------------- |
|
||||
| `enabled` | bool | `false` | Apply CIS hardening at all |
|
||||
| `profile` | string | `default` | `default` (house baseline), `l1` (clean CIS Level 1), or `l2` |
|
||||
| `rules` | dict | `{}` | Per-rule on/off overrides on top of the profile |
|
||||
| `params` | dict | `{}` | Parameter overrides (deep-merged; list values replace wholesale) |
|
||||
|
||||
**Default modules blacklist:** `freevxfs`, `jffs2`, `hfs`, `hfsplus`, `cramfs`, `udf`, `usb-storage`, `dccp`, `sctp`, `rds`, `tipc`, `firewire-core`, `firewire-sbp2`, `thunderbolt`. `squashfs` is added automatically except on Ubuntu (snap dependency).
|
||||
**Profiles.** `default` is the established house baseline (CIS Level 1 plus the USB lockdown, full module blacklist, and IPv6-disable extras, minus the usability-hostile controls). `l1` is a clean CIS Level 1: it drops the L2 extras and adds password aging, AIDE, and warning banners. `l2` is `l1` plus auditd and the L2 extras.
|
||||
|
||||
**Default sysctl settings** include: `kernel.yama.ptrace_scope=2`, `kernel.kptr_restrict=2`, `kernel.perf_event_paranoid=3`, `kernel.unprivileged_bpf_disabled=1`, IPv4/IPv6 hardening, ARP protection, and IPv6 disabled by default. Override individual keys:
|
||||
**Per-rule overrides.** Toggle an individual rule without changing profile, e.g. keep the default profile but allow USB and IPv6 on a desktop:
|
||||
|
||||
```yaml
|
||||
system:
|
||||
features:
|
||||
cis:
|
||||
sysctl:
|
||||
net.ipv6.conf.all.disable_ipv6: 0 # re-enable IPv6
|
||||
net.ipv4.ip_forward: 1 # enable for routers/containers
|
||||
enabled: true
|
||||
rules:
|
||||
usb_lockdown: false
|
||||
ipv6_disable: false
|
||||
```
|
||||
|
||||
**Default SSHD options** enforce: `PermitRootLogin no`, `PasswordAuthentication no`, `X11Forwarding no`, `AllowTcpForwarding no`, `MaxAuthTries 4`, and post-quantum KEX (mlkem768x25519-sha256 on OpenSSH 9.9+). Override per-option:
|
||||
Rule keys: `module_blacklist`, `usb_lockdown`, `sysctl_hardening`, `ipv6_disable`, `umask_default`, `empty_password_login`, `pwquality`, `core_dumps`, `shell_timeout`, `journald_persistent`, `sudo_logfile`, `su_restriction`, `faillock`, `password_history`, `tcp_wrappers`, `crypto_policy`, `mask_services`, `cron_at_access`, `file_permissions`, `sshd_hardening`, `password_expiry`, `aide`, `warning_banners`, `auditd`, and the opt-in `grub_password` (set `rules.grub_password: true` with `params.grub_password_hash`).
|
||||
|
||||
**Parameters.** Override baseline values under `params` (full list in `roles/cis/vars/main.yml`):
|
||||
|
||||
```yaml
|
||||
system:
|
||||
features:
|
||||
cis:
|
||||
sshd_options:
|
||||
enabled: true
|
||||
profile: l1
|
||||
params:
|
||||
pwquality_minlen: 16
|
||||
sysctl: # dict: deep-merged over the profile's set
|
||||
net.ipv4.ip_forward: 1
|
||||
sshd_options: # list: REPLACES the entire default list
|
||||
- {option: X11Forwarding, value: "yes"}
|
||||
- { option: AllowTcpForwarding, value: "yes" }
|
||||
```
|
||||
|
||||
Note: providing `sshd_options` replaces the entire list. Copy the defaults from `roles/cis/defaults/main.yml` and modify as needed.
|
||||
Common params: `modules_blacklist` (list), `sysctl` (dict), `sshd_options` (list), `pwquality_minlen` (14), `tmout` (900), `umask` (077), `umask_profile` (027), `faillock_deny` (5), `faillock_unlock_time` (900), `password_remember` (5), `pass_max_days` (365), `aide_cron_hour`/`aide_cron_minute`, `banner_text`, `grub_password_hash`.
|
||||
|
||||
### 4.5 VMware Guest Operations
|
||||
|
||||
@@ -379,7 +542,7 @@ When `hypervisor.type: vmware` uses the `vmware_tools` connection:
|
||||
| ------------- | ------ | ------------------------------------------------------ |
|
||||
| `size` | number | Disk size in GB (required for virtual) |
|
||||
| `device` | string | Block device path (required for physical data disks) |
|
||||
| `partition` | string | Partition device path (required for physical data disks) |
|
||||
| `partition` | string | Derived from `device` during normalization (not user input) |
|
||||
| `mount.path` | string | Mount point (additional disks only) |
|
||||
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
||||
| `mount.label` | string | Filesystem label |
|
||||
@@ -427,9 +590,9 @@ Roles execute in this order:
|
||||
1. **global_defaults** -- normalize inputs, validate, set OS flags
|
||||
2. **system_check** -- detect installer environment, verify live/non-prod target
|
||||
3. **virtualization** -- create VM (if virtual), attach disks, cloud-init
|
||||
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman
|
||||
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman, detect hardware
|
||||
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
|
||||
8. **cis** -- CIS hardening (when `system.features.cis.enabled: true`)
|
||||
9. **cleanup** -- unmount, shutdown installer, remove media, verify boot
|
||||
|
||||
@@ -9,8 +9,11 @@ all:
|
||||
baremetal01.example.com:
|
||||
ansible_host: 10.0.0.162
|
||||
ansible_user: root
|
||||
ansible_password: "1234"
|
||||
ansible_become_password: "1234"
|
||||
ansible_password: "CHANGE_ME"
|
||||
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:
|
||||
type: "physical"
|
||||
os: "archlinux"
|
||||
@@ -18,3 +21,10 @@ all:
|
||||
disks:
|
||||
- device: "/dev/sda"
|
||||
size: 120
|
||||
users:
|
||||
admin:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
root:
|
||||
password: "CHANGE_ME"
|
||||
|
||||
@@ -43,7 +43,7 @@ all:
|
||||
label: DATA
|
||||
opts: defaults
|
||||
users:
|
||||
- name: "ops"
|
||||
ops:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
@@ -100,7 +100,7 @@ all:
|
||||
path: /srv/data
|
||||
fstype: ext4
|
||||
users:
|
||||
- name: "dbadmin"
|
||||
dbadmin:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
|
||||
@@ -6,7 +6,6 @@ all:
|
||||
url: "localhost"
|
||||
username: ""
|
||||
password: ""
|
||||
host: ""
|
||||
storage: "default"
|
||||
boot_iso: "/var/lib/libvirt/images/archlinux-x86_64.iso"
|
||||
children:
|
||||
@@ -40,7 +39,7 @@ all:
|
||||
path: /var/www
|
||||
fstype: xfs
|
||||
users:
|
||||
- name: "web"
|
||||
web:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
@@ -82,7 +81,7 @@ all:
|
||||
path: /data
|
||||
fstype: ext4
|
||||
users:
|
||||
- name: "db"
|
||||
db:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
@@ -123,7 +122,7 @@ all:
|
||||
path: /data
|
||||
fstype: btrfs
|
||||
users:
|
||||
- name: "compute"
|
||||
compute:
|
||||
password: "CHANGE_ME"
|
||||
keys:
|
||||
- "ssh-ed25519 AAAA..."
|
||||
|
||||
59
main.yml
59
main.yml
@@ -1,14 +1,4 @@
|
||||
---
|
||||
# Bootstrap pipeline — role execution order:
|
||||
# 1. global_defaults — normalize + validate system/hypervisor/disk input
|
||||
# 2. system_check — pre-flight hardware/environment safety checks
|
||||
# 3. virtualization — create VM on hypervisor (libvirt/proxmox/vmware/xen)
|
||||
# 4. environment — detect live ISO, configure installer network, install tools
|
||||
# 5. partitioning — partition disk, create FS, LUKS, LVM, mount everything
|
||||
# 6. bootstrap — debootstrap/pacstrap/dnf install the target OS into /mnt
|
||||
# 7. configuration — users, network, encryption, fstab, bootloader, services
|
||||
# 8. cis — CIS hardening (optional, per system.features.cis.enabled)
|
||||
# 9. cleanup — unmount, remove cloud-init artifacts, reboot/shutdown
|
||||
- name: Create and configure VMs
|
||||
hosts: "{{ bootstrap_target | default('all') }}"
|
||||
strategy: free # noqa: run-once[play]
|
||||
@@ -62,6 +52,12 @@
|
||||
name: configuration
|
||||
public: true
|
||||
|
||||
# Past this point the OS is installed and configured; a CIS hardening or
|
||||
# cleanup failure must not delete an otherwise-good VM.
|
||||
- name: Mark base system complete
|
||||
ansible.builtin.set_fact:
|
||||
_bootstrap_base_complete: true
|
||||
|
||||
- name: Apply CIS hardening
|
||||
when: system_cfg.features.cis.enabled | bool
|
||||
ansible.builtin.include_role:
|
||||
@@ -75,11 +71,16 @@
|
||||
public: true
|
||||
|
||||
rescue:
|
||||
- name: Decide whether to delete the half-built VM
|
||||
ansible.builtin.set_fact:
|
||||
_delete_vm_on_rescue: >-
|
||||
{{ _vm_absent_before_bootstrap | default(false) | bool
|
||||
and virtualization_vm_created_in_run | default(false) | bool
|
||||
and system_cfg.type == "virtual"
|
||||
and not (_bootstrap_base_complete | default(false) | bool) }}
|
||||
|
||||
- name: Delete VM on bootstrap failure
|
||||
when:
|
||||
- _vm_absent_before_bootstrap | default(false) | bool
|
||||
- virtualization_vm_created_in_run | default(false) | bool
|
||||
- system_cfg.type == "virtual"
|
||||
when: _delete_vm_on_rescue | bool
|
||||
ansible.builtin.include_role:
|
||||
name: virtualization
|
||||
tasks_from: delete
|
||||
@@ -93,9 +94,8 @@
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Bootstrap failed for {{ hostname }}.
|
||||
{{ 'VM was deleted to allow clean retry.'
|
||||
if (virtualization_vm_created_in_run | default(false))
|
||||
else 'VM was not created in this run (kept).' }}
|
||||
{{ 'VM was deleted to allow clean retry.' if (_delete_vm_on_rescue | bool)
|
||||
else 'VM kept (base system installed or not created this run).' }}
|
||||
|
||||
post_tasks:
|
||||
- name: Set post-reboot connection flags
|
||||
@@ -131,6 +131,15 @@
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
- name: Wait for the rebooted host to accept SSH
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
ansible.builtin.wait_for_connection:
|
||||
delay: 5
|
||||
sleep: 5
|
||||
# 600s: a selinux-enabled first boot relabels the filesystem and reboots once more.
|
||||
timeout: 600
|
||||
|
||||
- name: Re-gather facts for target OS after reboot
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
@@ -140,6 +149,22 @@
|
||||
- min
|
||||
- pkg_mgr
|
||||
|
||||
- name: Register with the Satellite content source
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
- system_cfg.content.source == 'satellite'
|
||||
- system_cfg.os | lower in os_family_rhel
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/roles/configuration/tasks/satellite_register.yml"
|
||||
|
||||
- name: Activate the firewall on the rebooted host
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/roles/configuration/tasks/firewall.yml"
|
||||
vars:
|
||||
firewall_phase: postreboot
|
||||
|
||||
- name: Install post-reboot packages
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
|
||||
@@ -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.
|
||||
bootstrap_os_task_map:
|
||||
almalinux: _dnf_family.yml
|
||||
alpine: alpine.yml
|
||||
archlinux: archlinux.yml
|
||||
debian: debian.yml
|
||||
fedora: _dnf_family.yml
|
||||
opensuse: opensuse.yml
|
||||
rocky: _dnf_family.yml
|
||||
rhel: rhel.yml
|
||||
ubuntu: ubuntu.yml
|
||||
ubuntu-lts: ubuntu.yml
|
||||
void: void.yml
|
||||
|
||||
@@ -8,14 +8,33 @@
|
||||
_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([]) }}"
|
||||
_dm: "{{ system_cfg.features.desktop.display_manager | default('') }}"
|
||||
_dm_override_pkg: "{{ (bootstrap_dm_override_packages[_dm] | default({}))[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:
|
||||
_desktop_groups: "{{ _de_config.groups | default([]) }}"
|
||||
_desktop_packages: "{{ _de_config.packages | default([]) }}"
|
||||
_desktop_packages: >-
|
||||
{{
|
||||
((_de_config.packages | default([])) + _base + _group_pkgs + [_dm_override_pkg])
|
||||
| reject('equalto', '')
|
||||
| unique
|
||||
| list
|
||||
}}
|
||||
|
||||
- name: Validate desktop environment is supported
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (_desktop_groups | length > 0) or (_desktop_packages | length > 0)
|
||||
- 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 }}'.
|
||||
@@ -25,7 +44,7 @@
|
||||
- name: Install desktop package groups
|
||||
when: _desktop_groups | length > 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }}
|
||||
{{ 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
|
||||
@@ -35,14 +54,13 @@
|
||||
vars:
|
||||
_install_commands:
|
||||
RedHat: >-
|
||||
{{ chroot_command }} dnf --releasever={{ os_version }}
|
||||
{{ chroot_command }} dnf --releasever={{ os_version_major }}
|
||||
--setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }}
|
||||
Debian: >-
|
||||
{{ chroot_command }} apt install -y {{ _desktop_packages | join(' ') }}
|
||||
{{ chroot_command }} env DEBIAN_FRONTEND=noninteractive
|
||||
apt install -y --install-recommends {{ _desktop_packages | join(' ') }}
|
||||
Archlinux: >-
|
||||
pacstrap /mnt {{ _desktop_packages | join(' ') }}
|
||||
Suse: >-
|
||||
{{ chroot_command }} zypper install -y {{ _desktop_packages | join(' ') }}
|
||||
ansible.builtin.command: "{{ _install_commands[os_family] }}"
|
||||
register: _desktop_pkg_result
|
||||
changed_when: _desktop_pkg_result.rc == 0
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
block:
|
||||
- name: "Install base system for {{ os | capitalize }}"
|
||||
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
|
||||
groupinstall -y {{ _dnf_groups }}
|
||||
register: bootstrap_dnf_base_result
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
- name: Install extra packages
|
||||
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 }}
|
||||
register: bootstrap_dnf_extra_result
|
||||
changed_when: bootstrap_dnf_extra_result.rc == 0
|
||||
|
||||
94
roles/bootstrap/tasks/_hardware.yml
Normal file
94
roles/bootstrap/tasks/_hardware.yml
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
- 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([]) | difference(_hardware_profile_disable | 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
|
||||
ansible.builtin.include_tasks: _resolve_hardware_packages.yml
|
||||
|
||||
- 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: Note Intel IPU6 camera (out-of-tree stack)
|
||||
when: hardware_profile_active.camera.ipu6 | default(false) | bool
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Intel IPU6 MIPI camera detected. Its driver stack (intel-ipu6 firmware,
|
||||
DKMS module, v4l2-relayd, libcamera) is out-of-tree/AUR and is NOT auto-
|
||||
installed. Pin the packages in a hardware group via
|
||||
system.features.hardware.packages[{{ os_family }}].
|
||||
|
||||
- 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
|
||||
125
roles/bootstrap/tasks/_resolve_hardware_packages.yml
Normal file
125
roles/bootstrap/tasks/_resolve_hardware_packages.yml
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
# Split out of _hardware.yml so fixtures can seed the inputs and assert the
|
||||
# resolved _hardware_packages list with no chroot/install.
|
||||
- name: Resolve hardware package set
|
||||
vars:
|
||||
_family: "{{ bootstrap_hardware_packages[os_family] }}"
|
||||
_disable: "{{ _hardware_profile_disable | default([]) | list }}"
|
||||
_profile_packages: "{{ (_hardware_profile_packages | default({}))[os_family] | default([]) | list }}"
|
||||
_cpu: "{{ hardware_profile_active.cpu | default('') | string }}"
|
||||
_gpus: "{{ hardware_profile_active.gpus | default([]) | difference(_disable) | list }}"
|
||||
_wifi: "{{ hardware_profile_active.wireless | default([]) | difference(_disable) | list }}"
|
||||
_fp_detected: "{{ hardware_profile_active.fingerprint | default(false) | bool }}"
|
||||
_audio: "{{ hardware_profile_active.audio | default([]) | difference(_disable) | list }}"
|
||||
_bt_detected: "{{ hardware_profile_active.bluetooth | 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 }}"
|
||||
_camera_pref: "{{ system_cfg.features.peripherals.camera | default('auto') }}"
|
||||
_camera_uvc: "{{ hardware_profile_active.camera.uvc | default(false) | bool }}"
|
||||
_camera_ipu6: "{{ hardware_profile_active.camera.ipu6 | default(false) | bool }}"
|
||||
_fp_pref: "{{ system_cfg.features.peripherals.fingerprint | default('auto') }}"
|
||||
_audio_pref: "{{ system_cfg.features.peripherals.audio | default('auto') }}"
|
||||
_bt_pref: "{{ system_cfg.features.peripherals.bluetooth | default('auto') }}"
|
||||
_dl_on: "{{ (system_cfg.features.peripherals.displaylink | bool) and ('displaylink' not in _disable) }}"
|
||||
_camera_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('camera' not in _disable)
|
||||
and (_camera_pref == 'true' or (_camera_pref == 'auto' and (_camera_uvc or _camera_ipu6)))
|
||||
}}
|
||||
_fp_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('fingerprint' not in _disable)
|
||||
and (_fp_pref == 'true' or (_fp_pref == 'auto' and _fp_detected))
|
||||
}}
|
||||
_audio_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('audio' not in _disable)
|
||||
and (_audio_pref == 'true' or (_audio_pref == 'auto' and (_audio | length > 0)))
|
||||
}}
|
||||
_bt_on: >-
|
||||
{{
|
||||
_peripherals_on
|
||||
and ('bluetooth' not in _disable)
|
||||
and (_bt_pref == 'true' or (_bt_pref == 'auto' and _bt_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 and _cpu not in _disable) else []) | list }}"
|
||||
_firmware_vendors: >-
|
||||
{{
|
||||
(_firmware_on | ternary(
|
||||
(_gpus + _wifi + _cpu_vendor_list)
|
||||
| reject('equalto', '') | unique | list,
|
||||
[]
|
||||
))
|
||||
}}
|
||||
_microcode_pkgs: >-
|
||||
{{
|
||||
((_microcode_on and _cpu | length > 0 and _cpu not in _disable) | 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
|
||||
}}
|
||||
_camera_base_pkgs: >-
|
||||
{{
|
||||
(_camera_on | ternary(_family.camera_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
|
||||
}}
|
||||
_audio_base_pkgs: "{{ (_audio_on | ternary(_family.audio_base | default([]), [])) | list }}"
|
||||
_bluetooth_base_pkgs: "{{ (_bt_on | ternary(_family.bluetooth_base | default([]), [])) | list }}"
|
||||
ansible.builtin.set_fact:
|
||||
_hardware_packages: >-
|
||||
{{
|
||||
(_microcode_pkgs + _firmware_pkgs
|
||||
+ _gpu_base_pkgs + _gpu_vendor_pkgs + _gpu_nvidia_pkgs
|
||||
+ _audio_base_pkgs + _bluetooth_base_pkgs
|
||||
+ _camera_base_pkgs + _peripherals_fingerprint_pkgs
|
||||
+ _peripherals_displaylink_pkgs
|
||||
+ _profile_packages)
|
||||
| reject('equalto', '')
|
||||
| unique
|
||||
| list
|
||||
}}
|
||||
@@ -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
|
||||
@@ -8,7 +8,44 @@
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
block:
|
||||
- name: Notify that mirror mode falls back to the public mirrorlist
|
||||
when:
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length == 0
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
content.source is 'mirror' but content.url is empty: keeping the live
|
||||
ISO public mirrorlist (refreshed by reflector). Set content.url to pin
|
||||
a specific mirror.
|
||||
|
||||
- name: Point pacman at the content mirror
|
||||
when: system_cfg.content.url | length > 0
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/pacman.d/mirrorlist
|
||||
content: "Server = {{ system_cfg.content.url }}/$repo/os/$arch\n"
|
||||
mode: "0644"
|
||||
|
||||
- name: Refresh Arch keyring in the live environment
|
||||
ansible.builtin.command: pacman -Sy --noconfirm archlinux-keyring
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
register: bootstrap_arch_keyring
|
||||
changed_when: bootstrap_arch_keyring.rc == 0
|
||||
|
||||
- name: Install Arch base system
|
||||
ansible.builtin.command: >-
|
||||
pacstrap /mnt {{ bootstrap_archlinux_packages | join(' ') }}
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
register: bootstrap_result
|
||||
changed_when: bootstrap_result.rc == 0
|
||||
|
||||
- name: Persist the content mirror in the installed system
|
||||
when: system_cfg.content.url | length > 0
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/pacman.d/mirrorlist
|
||||
content: "Server = {{ system_cfg.content.url }}/$repo/os/$arch\n"
|
||||
mode: "0644"
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
vars:
|
||||
bootstrap_debian_release: >-
|
||||
{{
|
||||
'buster' if (os_version | string) == '10'
|
||||
else 'bullseye' if (os_version | string) == '11'
|
||||
else 'bookworm' if (os_version | string) == '12'
|
||||
'bookworm' if (os_version | string) == '12'
|
||||
else 'trixie' if (os_version | string) == '13'
|
||||
else 'sid' if (os_version | string) == 'unstable'
|
||||
else 'trixie'
|
||||
@@ -28,10 +26,27 @@
|
||||
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
|
||||
quiet: true
|
||||
|
||||
- name: Check for a debootstrap script for the target release
|
||||
ansible.builtin.stat:
|
||||
path: "/usr/share/debootstrap/scripts/{{ bootstrap_debian_release }}"
|
||||
register: bootstrap_debian_script
|
||||
|
||||
- name: Symlink a missing debootstrap script to the sid base
|
||||
ansible.builtin.file:
|
||||
src: sid
|
||||
dest: "/usr/share/debootstrap/scripts/{{ bootstrap_debian_release }}"
|
||||
state: link
|
||||
when: not bootstrap_debian_script.stat.exists
|
||||
|
||||
- name: Install Debian base system
|
||||
ansible.builtin.command: >-
|
||||
debootstrap --include={{ bootstrap_debian_base_csv }}
|
||||
{{ bootstrap_debian_release }} /mnt {{ system_cfg.mirror }}
|
||||
debootstrap --keyring=/usr/share/keyrings/debian-archive-keyring.gpg
|
||||
--include={{ bootstrap_debian_base_csv }}
|
||||
{{ bootstrap_debian_release }} /mnt
|
||||
{{ system_cfg.content.url }}
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
register: bootstrap_debian_base_result
|
||||
changed_when: bootstrap_debian_base_result.rc == 0
|
||||
|
||||
@@ -48,6 +63,10 @@
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
Acquire::http::Proxy "{{ system_cfg.content.proxy }}";
|
||||
Acquire::https::Proxy "{{ system_cfg.content.proxy }}";
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Update package lists
|
||||
@@ -66,7 +85,10 @@
|
||||
register: bootstrap_debian_extra_result
|
||||
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
|
||||
when: not (system_cfg.features.desktop.enabled | bool)
|
||||
ansible.builtin.command: "{{ chroot_command }} apt remove -y libcups2 libavahi-common3 libavahi-common-data"
|
||||
register: bootstrap_debian_remove_result
|
||||
changed_when: bootstrap_debian_remove_result.rc == 0
|
||||
|
||||
@@ -29,11 +29,42 @@
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
# Installers write their cache inside the installroot; redirect it off the 2 GiB CIS /var LV.
|
||||
- name: Create bootstrap package-cache directory
|
||||
ansible.builtin.file:
|
||||
path: /mnt/.bootstrap-cache
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Redirect package cache off the CIS /var LV
|
||||
ansible.posix.mount:
|
||||
src: /mnt/.bootstrap-cache
|
||||
path: /mnt/var/cache
|
||||
fstype: none
|
||||
opts: bind
|
||||
state: ephemeral
|
||||
|
||||
- name: Run OS-specific bootstrap process
|
||||
vars:
|
||||
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
|
||||
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
|
||||
|
||||
# dnf --installroot never runs anaconda, so no authselect profile is selected and
|
||||
# /etc/pam.d/system-auth is missing, leaving the system unable to authenticate.
|
||||
# local is the right profile: local-auth only, no pam_sss.so, still CIS-capable.
|
||||
- name: Select default authselect profile for the PAM stack
|
||||
when: is_authselect | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} authselect select local --force"
|
||||
register: bootstrap_authselect_result
|
||||
changed_when: bootstrap_authselect_result.rc == 0
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
@@ -24,12 +24,14 @@
|
||||
- "'grub2-common' not in (bootstrap_result.stderr | default(''))"
|
||||
|
||||
- name: Ensure chroot RHEL DVD directory exists
|
||||
when: system_cfg.content.source != 'mirror'
|
||||
ansible.builtin.file:
|
||||
path: /mnt/usr/local/install/redhat/dvd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Bind mount RHEL DVD into chroot
|
||||
when: system_cfg.content.source != 'mirror'
|
||||
ansible.posix.mount:
|
||||
src: /usr/local/install/redhat/dvd
|
||||
path: /mnt/usr/local/install/redhat/dvd
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# ubuntu = latest non-LTS, ubuntu-lts = latest LTS
|
||||
bootstrap_ubuntu_release_map:
|
||||
ubuntu: questing
|
||||
ubuntu-lts: noble
|
||||
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('noble') }}"
|
||||
ubuntu-lts: resolute
|
||||
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('resolute') }}"
|
||||
_config: "{{ lookup('vars', bootstrap_var_key) }}"
|
||||
bootstrap_ubuntu_base_csv: "{{ (['ca-certificates'] + _config.base) | unique | join(',') }}"
|
||||
bootstrap_ubuntu_extra_args: >-
|
||||
@@ -24,13 +24,28 @@
|
||||
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
|
||||
quiet: true
|
||||
|
||||
- name: Check for a debootstrap script for the target release
|
||||
ansible.builtin.stat:
|
||||
path: "/usr/share/debootstrap/scripts/{{ bootstrap_ubuntu_release }}"
|
||||
register: bootstrap_ubuntu_script
|
||||
|
||||
- name: Symlink a missing debootstrap script to the ubuntu base
|
||||
ansible.builtin.file:
|
||||
src: gutsy
|
||||
dest: "/usr/share/debootstrap/scripts/{{ bootstrap_ubuntu_release }}"
|
||||
state: link
|
||||
when: not bootstrap_ubuntu_script.stat.exists
|
||||
|
||||
- name: Install Ubuntu base system
|
||||
ansible.builtin.command: >-
|
||||
debootstrap
|
||||
--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
--include={{ bootstrap_ubuntu_base_csv }}
|
||||
{{ bootstrap_ubuntu_release }} /mnt
|
||||
{{ system_cfg.mirror }}
|
||||
{{ system_cfg.content.url }}
|
||||
environment:
|
||||
http_proxy: "{{ system_cfg.content.proxy }}"
|
||||
https_proxy: "{{ system_cfg.content.proxy }}"
|
||||
register: bootstrap_ubuntu_base_result
|
||||
changed_when: bootstrap_ubuntu_base_result.rc == 0
|
||||
|
||||
@@ -47,6 +62,10 @@
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
Acquire::http::Proxy "{{ system_cfg.content.proxy }}";
|
||||
Acquire::https::Proxy "{{ system_cfg.content.proxy }}";
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Update package lists
|
||||
|
||||
@@ -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
|
||||
@@ -1,7 +1,7 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = bootstrap_debian_release %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
|
||||
{% set mirror = system_cfg.content.url | default('http://deb.debian.org/debian', true) %}
|
||||
{% set components = 'main contrib non-free non-free-firmware' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = bootstrap_ubuntu_release %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set mirror = system_cfg.content.url %}
|
||||
{% set components = 'main restricted universe multiverse' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
---
|
||||
# Per-family desktop environment package definitions.
|
||||
# Keyed by os_family -> environment -> groups (dnf groupinstall) / packages.
|
||||
# Kept intentionally minimal: base DE + essential tools, no full suites.
|
||||
# Wayland only: gnome, kde, sway, hyprland. No X11/xorg-server, no X11-only DEs.
|
||||
|
||||
# plasma-login-manager on Arch/Fedora44+ (Plasma 6.6), else sddm.
|
||||
bootstrap_kde_login_manager: >-
|
||||
{{
|
||||
'plasma-login-manager'
|
||||
if (os == 'archlinux' or (os == 'fedora' and (os_version | int) >= 44))
|
||||
else 'sddm'
|
||||
}}
|
||||
|
||||
# Native DMs ride in each DE's package set; only explicit non-native overrides
|
||||
# need a package here. ly is Arch-only (validation rejects it elsewhere first).
|
||||
bootstrap_dm_override_packages:
|
||||
ly:
|
||||
Archlinux: ly
|
||||
|
||||
# EL = non-fedora RedHat.
|
||||
bootstrap_os_is_el: "{{ os in ['almalinux', 'rocky', 'rhel'] }}"
|
||||
bootstrap_os_is_el10: "{{ bootstrap_os_is_el | bool and (os_version | default('0') | int) >= 10 }}"
|
||||
# EL10 renames (evince->papers, eog->loupe, ppd->tuned-ppd); fira-code + mpv absent on EL.
|
||||
bootstrap_desktop_browser: "{{ 'firefox-esr' if os == 'debian' else 'firefox' }}"
|
||||
bootstrap_desktop_pdf: "{{ 'papers' if bootstrap_os_is_el10 | bool else 'evince' }}"
|
||||
bootstrap_desktop_image: "{{ 'loupe' if bootstrap_os_is_el10 | bool else 'eog' }}"
|
||||
bootstrap_desktop_power: "{{ 'tuned-ppd' if bootstrap_os_is_el10 | bool else 'power-profiles-daemon' }}"
|
||||
bootstrap_desktop_redhat_codefont: "{{ '' if bootstrap_os_is_el | bool else 'fira-code-fonts' }}"
|
||||
bootstrap_desktop_redhat_video: "{{ '' if bootstrap_os_is_el | bool else 'mpv' }}"
|
||||
|
||||
bootstrap_desktop_packages:
|
||||
RedHat:
|
||||
gnome:
|
||||
groups:
|
||||
- workstation-product-environment
|
||||
packages: []
|
||||
groups: []
|
||||
packages:
|
||||
- gnome-shell
|
||||
- gnome-control-center
|
||||
- nautilus
|
||||
- gnome-session
|
||||
- gdm
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
@@ -15,7 +43,7 @@ bootstrap_desktop_packages:
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- plasma-systemmonitor
|
||||
- sddm
|
||||
- "{{ bootstrap_kde_login_manager }}"
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
@@ -24,13 +52,6 @@ bootstrap_desktop_packages:
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
xfce:
|
||||
groups:
|
||||
- xfce-desktop-environment
|
||||
packages:
|
||||
- lightdm
|
||||
Debian:
|
||||
gnome:
|
||||
groups: []
|
||||
@@ -45,7 +66,7 @@ bootstrap_desktop_packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- sddm
|
||||
- "{{ bootstrap_kde_login_manager }}"
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
@@ -53,15 +74,6 @@ bootstrap_desktop_packages:
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
xfce:
|
||||
groups: []
|
||||
packages:
|
||||
- xfce4
|
||||
- xfce4-goodies
|
||||
- lightdm
|
||||
- xdg-user-dirs
|
||||
Archlinux:
|
||||
gnome:
|
||||
groups: []
|
||||
@@ -75,7 +87,7 @@ bootstrap_desktop_packages:
|
||||
- plasma-desktop
|
||||
- plasma-nm
|
||||
- plasma-pa
|
||||
- sddm
|
||||
- "{{ bootstrap_kde_login_manager }}"
|
||||
- konsole
|
||||
- dolphin
|
||||
- kate
|
||||
@@ -84,15 +96,6 @@ bootstrap_desktop_packages:
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-kde
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
xfce:
|
||||
groups: []
|
||||
packages:
|
||||
- xfce4
|
||||
- xfce4-goodies
|
||||
- lightdm
|
||||
- xdg-user-dirs
|
||||
sway:
|
||||
groups: []
|
||||
packages:
|
||||
@@ -100,12 +103,13 @@ bootstrap_desktop_packages:
|
||||
- waybar
|
||||
- foot
|
||||
- wofi
|
||||
- nautilus
|
||||
- greetd
|
||||
- greetd-tuigreet
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-wlr
|
||||
- polkit-gnome
|
||||
- bluez
|
||||
- pipewire
|
||||
- wireplumber
|
||||
hyprland:
|
||||
groups: []
|
||||
packages:
|
||||
@@ -113,37 +117,78 @@ bootstrap_desktop_packages:
|
||||
- kitty
|
||||
- wofi
|
||||
- waybar
|
||||
- ly
|
||||
- nautilus
|
||||
- greetd
|
||||
- greetd-tuigreet
|
||||
- xdg-user-dirs
|
||||
- xdg-desktop-portal-hyprland
|
||||
- polkit-kde-agent
|
||||
- qt5-wayland
|
||||
- qt6-wayland
|
||||
- bluez
|
||||
|
||||
# Installed for EVERY DE whenever desktop.enabled. No file manager here: DE metas
|
||||
# bundle their own and the wlroots sets above carry nautilus.
|
||||
bootstrap_desktop_base_packages:
|
||||
RedHat:
|
||||
- google-noto-sans-fonts
|
||||
- google-noto-emoji-fonts
|
||||
- "{{ bootstrap_desktop_redhat_codefont }}"
|
||||
- pipewire
|
||||
- wireplumber
|
||||
Suse:
|
||||
gnome:
|
||||
groups: []
|
||||
packages:
|
||||
- patterns-gnome-gnome_basic
|
||||
- gdm
|
||||
- xdg-user-dirs
|
||||
kde:
|
||||
groups: []
|
||||
packages:
|
||||
- patterns-kde-kde_plasma
|
||||
- sddm
|
||||
- xdg-user-dirs
|
||||
- pipewire-pulseaudio
|
||||
- xdg-desktop-portal
|
||||
- "{{ bootstrap_desktop_power }}"
|
||||
- bluez
|
||||
- firefox
|
||||
- "{{ bootstrap_desktop_pdf }}"
|
||||
- "{{ bootstrap_desktop_image }}"
|
||||
- "{{ bootstrap_desktop_redhat_video }}"
|
||||
Debian:
|
||||
- fonts-noto
|
||||
- fonts-noto-color-emoji
|
||||
- fonts-firacode
|
||||
- pipewire
|
||||
- wireplumber
|
||||
- pipewire-pulse
|
||||
- xdg-desktop-portal
|
||||
- power-profiles-daemon
|
||||
- bluez
|
||||
- "{{ bootstrap_desktop_browser }}"
|
||||
- evince
|
||||
- eog
|
||||
- mpv
|
||||
Archlinux:
|
||||
- noto-fonts
|
||||
- noto-fonts-emoji
|
||||
- ttf-nerd-fonts-symbols
|
||||
- pipewire
|
||||
- wireplumber
|
||||
- pipewire-pulse
|
||||
- xdg-desktop-portal
|
||||
- power-profiles-daemon
|
||||
- bluez
|
||||
- firefox
|
||||
- evince
|
||||
- loupe
|
||||
- mpv
|
||||
|
||||
# Display manager auto-detection from desktop environment.
|
||||
bootstrap_desktop_dm_map:
|
||||
gnome: gdm
|
||||
kde: sddm
|
||||
xfce: lightdm
|
||||
sway: greetd
|
||||
hyprland: ly@tty2
|
||||
cinnamon: lightdm
|
||||
mate: lightdm
|
||||
lxqt: sddm
|
||||
budgie: gdm
|
||||
# Opt-in groups selected per host via features.desktop.groups; the union of the
|
||||
# requested groups' packages is installed. 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
|
||||
|
||||
103
roles/bootstrap/vars/hardware.yml
Normal file
103
roles/bootstrap/vars/hardware.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
# Hardware-aware package definitions keyed by os_family, consumed by
|
||||
# _resolve_hardware_packages.yml. Only packages matching detected hardware are
|
||||
# installed; families without vendor splits collapse to one 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]
|
||||
camera_base: [v4l-utils]
|
||||
peripherals_fingerprint: [fprintd, libfprint]
|
||||
peripherals_displaylink: [] # AUR only; user must wire in AUR helper
|
||||
audio_base: [sof-firmware, alsa-ucm-conf]
|
||||
bluetooth_base: [bluez, bluez-utils]
|
||||
|
||||
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: []
|
||||
camera_base: [v4l-utils]
|
||||
peripherals_fingerprint: [fprintd, libpam-fprintd]
|
||||
peripherals_displaylink: [evdi-dkms] # userspace driver still needs vendor .run
|
||||
audio_base: [firmware-sof-signed, alsa-ucm-conf]
|
||||
bluetooth_base: [bluez]
|
||||
|
||||
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: []
|
||||
camera_base: [v4l-utils]
|
||||
peripherals_fingerprint: [fprintd, fprintd-pam]
|
||||
peripherals_displaylink: [evdi] # COPR-supplied; repo enablement deferred
|
||||
audio_base: [alsa-sof-firmware, alsa-ucm]
|
||||
bluetooth_base: [bluez]
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# Feature-gated packages shared across all distros.
|
||||
# Arch has special nftables handling and composes this differently.
|
||||
# Feature-gated packages shared across all distros. Arch strips nftables from
|
||||
# this and composes it differently.
|
||||
bootstrap_common_conditional: >-
|
||||
{{
|
||||
(
|
||||
@@ -11,18 +11,37 @@ bootstrap_common_conditional: >-
|
||||
+ (['cryptsetup', 'tpm2-tools'] if system_cfg.luks.enabled | bool else [])
|
||||
+ (['qemu-guest-agent'] if hypervisor_type in ['libvirt', 'proxmox'] else [])
|
||||
+ (['open-vm-tools'] if hypervisor_type == 'vmware' else [])
|
||||
+ (['cloud-init'] if system_cfg.features.cloud_init | bool else [])
|
||||
)
|
||||
}}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-OS package definitions: base (rootfs/group install), extra (post-base),
|
||||
# conditional (feature/version-gated, appended by task files).
|
||||
# DNF-based distros also carry repos (dnf --repo) and use base as group names.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Native-installer parity backfill: anaconda and the d-i "standard" task leave
|
||||
# these, but install_weak_deps=False / Recommends-off minimal installs drop them.
|
||||
bootstrap_el_runtime:
|
||||
- NetworkManager
|
||||
- authselect
|
||||
- authselect-libs
|
||||
- chrony
|
||||
- crypto-policies
|
||||
- crypto-policies-scripts
|
||||
- dbus
|
||||
- polkit
|
||||
|
||||
bootstrap_deb_runtime:
|
||||
- apparmor-utils
|
||||
- chrony
|
||||
- libpam-pwquality
|
||||
- needrestart
|
||||
- network-manager
|
||||
- sudo
|
||||
|
||||
# Per-OS package definitions: base (rootfs/group install), extra (post-base),
|
||||
# conditional (feature/version-gated, appended by task files). DNF distros also
|
||||
# carry repos and use base as group names.
|
||||
bootstrap_rhel:
|
||||
repos:
|
||||
- "rhel{{ os_version_major }}-baseos"
|
||||
- "rhel{{ os_version_major }}-appstream"
|
||||
base:
|
||||
- core
|
||||
- base
|
||||
@@ -51,6 +70,7 @@ bootstrap_rhel:
|
||||
+ (['python39'] if os_version_major | default('') == '8' else ['python'])
|
||||
+ (['kernel'] if os_version_major | default('') == '10' else [])
|
||||
+ (['zram-generator'] if os_version_major | default('') in ['9', '10'] else [])
|
||||
+ bootstrap_el_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -85,8 +105,8 @@ bootstrap_almalinux:
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['dbus-daemon'] if (os_version_major | default('10') | int) >= 9 else [])
|
||||
+ (['dhcp-client'] if (os_version_major | default('10') | int) < 10 else [])
|
||||
(['dhcp-client'] if (os_version_major | default('10') | int) < 10 else [])
|
||||
+ bootstrap_el_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -125,6 +145,7 @@ bootstrap_rocky:
|
||||
conditional: >-
|
||||
{{
|
||||
(['dhcp-client'] if (os_version_major | default('9') | int) < 10 else [])
|
||||
+ bootstrap_el_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -158,7 +179,6 @@ bootstrap_fedora:
|
||||
- nc
|
||||
- nfs-utils
|
||||
- nfsv4-client-utils
|
||||
- polkit
|
||||
- ppp
|
||||
- python3
|
||||
- ripgrep
|
||||
@@ -169,7 +189,7 @@ bootstrap_fedora:
|
||||
- zoxide
|
||||
- zram-generator
|
||||
- zstd
|
||||
conditional: "{{ bootstrap_common_conditional }}"
|
||||
conditional: "{{ bootstrap_el_runtime + bootstrap_common_conditional }}"
|
||||
|
||||
bootstrap_debian:
|
||||
base:
|
||||
@@ -187,28 +207,22 @@ bootstrap_debian:
|
||||
- python3
|
||||
- xfsprogs
|
||||
extra:
|
||||
- apparmor-utils
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- entr
|
||||
- fish
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- linux-image-amd64
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- needrestart
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- tcpd
|
||||
- vim
|
||||
@@ -223,6 +237,7 @@ bootstrap_debian:
|
||||
+ (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else [])
|
||||
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
|
||||
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ bootstrap_deb_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -244,10 +259,8 @@ bootstrap_ubuntu:
|
||||
- python3
|
||||
- xfsprogs
|
||||
extra:
|
||||
- apparmor-utils
|
||||
- bash-completion
|
||||
- bat
|
||||
- chrony
|
||||
- curl
|
||||
- dnsutils
|
||||
- duf
|
||||
@@ -259,20 +272,16 @@ bootstrap_ubuntu:
|
||||
- fzf
|
||||
- htop
|
||||
- jq
|
||||
- libpam-pwquality
|
||||
- lrzsz
|
||||
- mtr
|
||||
- ncdu
|
||||
- ncurses-term
|
||||
- needrestart
|
||||
- net-tools
|
||||
- network-manager
|
||||
- python-is-python3
|
||||
- ripgrep
|
||||
- rsync
|
||||
- screen
|
||||
- software-properties-common
|
||||
- sudo
|
||||
- syslog-ng
|
||||
- systemd-zram-generator
|
||||
- tcpd
|
||||
@@ -285,8 +294,8 @@ bootstrap_ubuntu:
|
||||
- zstd
|
||||
conditional: >-
|
||||
{{
|
||||
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
|
||||
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
(['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ bootstrap_deb_runtime
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
@@ -313,7 +322,6 @@ bootstrap_archlinux:
|
||||
- nfs-utils
|
||||
- ppp
|
||||
- python
|
||||
- reflector
|
||||
- rsync
|
||||
- sudo
|
||||
- tldr
|
||||
@@ -326,75 +334,6 @@ bootstrap_archlinux:
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['iptables-nft'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (['sbctl'] if system_cfg.features.secure_boot.enabled | bool else [])
|
||||
+ (['reflector'] if system_cfg.content.url | length == 0 else [])
|
||||
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
|
||||
}}
|
||||
|
||||
bootstrap_alpine:
|
||||
base:
|
||||
- alpine-base
|
||||
extra:
|
||||
- btrfs-progs
|
||||
- chrony
|
||||
- curl
|
||||
- e2fsprogs
|
||||
- linux-lts
|
||||
- logrotate
|
||||
- lvm2
|
||||
- python3
|
||||
- rsync
|
||||
- sudo
|
||||
- util-linux
|
||||
- vim
|
||||
- xfsprogs
|
||||
conditional: >-
|
||||
{{
|
||||
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ bootstrap_common_conditional
|
||||
}}
|
||||
|
||||
bootstrap_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,91 +1,4 @@
|
||||
---
|
||||
# User-facing API: override via top-level `cis` dict in inventory.
|
||||
# Merged with these defaults in _normalize.yml → cis_cfg.
|
||||
cis_defaults:
|
||||
modules_blacklist:
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- cramfs
|
||||
- udf
|
||||
- usb-storage
|
||||
- dccp
|
||||
- sctp
|
||||
- rds
|
||||
- tipc
|
||||
- firewire-core
|
||||
- firewire-sbp2
|
||||
- thunderbolt
|
||||
sysctl:
|
||||
fs.suid_dumpable: 0
|
||||
kernel.dmesg_restrict: 1
|
||||
kernel.kptr_restrict: 2
|
||||
kernel.perf_event_paranoid: 3
|
||||
kernel.unprivileged_bpf_disabled: 1
|
||||
kernel.yama.ptrace_scope: 2
|
||||
kernel.randomize_va_space: 2
|
||||
net.ipv4.ip_forward: 0
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
net.ipv4.conf.all.log_martians: 1
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.all.secure_redirects: 0
|
||||
net.ipv4.conf.all.send_redirects: 0
|
||||
net.ipv4.conf.all.accept_redirects: 0
|
||||
net.ipv4.conf.all.accept_source_route: 0
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
net.ipv4.conf.default.log_martians: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
net.ipv4.conf.default.secure_redirects: 0
|
||||
net.ipv4.conf.default.send_redirects: 0
|
||||
net.ipv4.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.all.accept_redirects: 0
|
||||
net.ipv6.conf.all.disable_ipv6: 1
|
||||
net.ipv6.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.default.disable_ipv6: 1
|
||||
net.ipv6.conf.lo.disable_ipv6: 1
|
||||
sshd_options:
|
||||
- { option: LogLevel, value: VERBOSE }
|
||||
- { option: LoginGraceTime, value: "60" }
|
||||
- { option: PermitRootLogin, value: "no" }
|
||||
- { option: StrictModes, value: "yes" }
|
||||
- { option: MaxAuthTries, value: "4" }
|
||||
- { option: MaxSessions, value: "10" }
|
||||
- { option: MaxStartups, value: "10:30:60" }
|
||||
- { option: PubkeyAuthentication, value: "yes" }
|
||||
- { option: HostbasedAuthentication, value: "no" }
|
||||
- { option: IgnoreRhosts, value: "yes" }
|
||||
- { option: PasswordAuthentication, value: "no" }
|
||||
- { option: PermitEmptyPasswords, value: "no" }
|
||||
- { option: KerberosAuthentication, value: "no" }
|
||||
- { option: GSSAPIAuthentication, value: "no" }
|
||||
- { option: AllowAgentForwarding, value: "no" }
|
||||
- { option: AllowTcpForwarding, value: "no" }
|
||||
- { option: KbdInteractiveAuthentication, value: "no" }
|
||||
- { option: GatewayPorts, value: "no" }
|
||||
- { option: X11Forwarding, value: "no" }
|
||||
- { option: PermitUserEnvironment, value: "no" }
|
||||
- { option: ClientAliveInterval, value: "300" }
|
||||
- { option: ClientAliveCountMax, value: "1" }
|
||||
- { option: PermitTunnel, value: "no" }
|
||||
- { option: Banner, value: /etc/issue.net }
|
||||
pwquality_minlen: 14
|
||||
tmout: 900
|
||||
umask: "077"
|
||||
umask_profile: "027"
|
||||
faillock_deny: 5
|
||||
faillock_unlock_time: 900
|
||||
password_remember: 5
|
||||
|
||||
# Platform-specific binary names for CIS permission targets
|
||||
cis_fusermount_binary: "{{ 'fusermount3' if is_rhel | default(false) | bool else 'fusermount' }}"
|
||||
cis_write_binary: "{{ 'write' if is_rhel | default(false) | bool else 'wall' }}"
|
||||
|
||||
cis: {}
|
||||
|
||||
cis_permission_targets:
|
||||
- {path: "/mnt/etc/ssh/sshd_config", mode: "0600"}
|
||||
- {path: "/mnt/etc/cron.hourly", mode: "0700"}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
---
|
||||
- name: Normalize CIS input
|
||||
- name: Determine CIS profile
|
||||
ansible.builtin.set_fact:
|
||||
cis_enabled: "{{ cis is defined and (cis is mapping or cis | bool) }}"
|
||||
cis_input: "{{ cis if cis is mapping else {} }}"
|
||||
cis_profile: "{{ system_cfg.features.cis.profile | default('default') }}"
|
||||
|
||||
- name: Normalize CIS configuration
|
||||
when: cis_enabled and cis_cfg is not defined
|
||||
- name: Validate CIS profile selection
|
||||
ansible.builtin.assert:
|
||||
that: cis_profile in cis_profiles
|
||||
fail_msg: >-
|
||||
system.features.cis.profile '{{ cis_profile }}' is unknown
|
||||
(valid: {{ cis_profiles.keys() | list | join(', ') }}).
|
||||
quiet: true
|
||||
|
||||
- name: Resolve CIS rules and parameters
|
||||
vars:
|
||||
_cis: "{{ system_cfg.features.cis | default({}) }}"
|
||||
ansible.builtin.set_fact:
|
||||
cis_cfg: "{{ cis_defaults | combine(cis_input, recursive=True) }}"
|
||||
cis_effective_rules: "{{ cis_profiles[cis_profile] | combine(_cis.rules | default({})) }}"
|
||||
cis_cfg: >-
|
||||
{{ cis_param_defaults
|
||||
| combine(cis_profile_params[cis_profile] | default({}), recursive=True)
|
||||
| combine(_cis.params | default({}), recursive=True) }}
|
||||
# l1/l2 add the stricter CIS-server controls on top of the legacy `default`
|
||||
# baseline; gate those tasks on this so `default` stays byte-for-byte unchanged.
|
||||
cis_strict: "{{ cis_profile in ['l1', 'l2'] }}"
|
||||
|
||||
42
roles/cis/tasks/aide.yml
Normal file
42
roles/cis/tasks/aide.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
- name: Install AIDE
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
# Debian's aideinit lives in aide-common (only Recommended, so absent under
|
||||
# the installer's --no-install-recommends); pull it explicitly.
|
||||
ansible.builtin.command: "{{ cis_pkg_install }} {{ 'aide aide-common' if is_debian | bool else 'aide' }}"
|
||||
register: cis_aide_install
|
||||
changed_when: cis_aide_install.rc == 0
|
||||
|
||||
- name: Initialize the AIDE database
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
# Absolute path: arch-chroot's PATH omits /usr/sbin, so bare aide/aideinit is rc127.
|
||||
# Debian's aideinit assembles its split config; RHEL/Arch run --init on /etc/aide.conf.
|
||||
ansible.builtin.command: "{{ chroot_command }} {{ '/usr/sbin/aideinit -y -f' if is_debian | bool else '/usr/sbin/aide --init' }}"
|
||||
register: cis_aide_init
|
||||
changed_when: cis_aide_init.rc == 0
|
||||
|
||||
- name: Locate the freshly built AIDE database
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/var/lib/aide
|
||||
patterns: "aide.db.new*"
|
||||
register: cis_aide_newdb
|
||||
|
||||
- name: Activate the AIDE database
|
||||
when:
|
||||
- cis_effective_rules.aide | default(false)
|
||||
- cis_aide_newdb.files | length > 0
|
||||
ansible.builtin.copy:
|
||||
src: "{{ cis_aide_newdb.files[0].path }}"
|
||||
dest: "{{ cis_aide_newdb.files[0].path | regex_replace('\\.new', '') }}"
|
||||
remote_src: true
|
||||
mode: "0600"
|
||||
|
||||
- name: Schedule the daily AIDE integrity check
|
||||
when: cis_effective_rules.aide | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/cron.d/cis-aide
|
||||
mode: "0644"
|
||||
content: |
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
{{ cis_cfg.aide_cron_minute }} {{ cis_cfg.aide_cron_hour }} * * * root aide --check
|
||||
42
roles/cis/tasks/auditd.yml
Normal file
42
roles/cis/tasks/auditd.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
- name: Install the audit daemon
|
||||
when: cis_effective_rules.auditd | default(false)
|
||||
ansible.builtin.command: "{{ cis_pkg_install }} {{ 'auditd' if is_debian | bool else 'audit' }}"
|
||||
register: cis_auditd_install
|
||||
changed_when: cis_auditd_install.rc == 0
|
||||
|
||||
- name: Deploy the CIS audit rule set
|
||||
when: cis_effective_rules.auditd | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/audit/rules.d/cis.rules
|
||||
mode: "0640"
|
||||
content: |
|
||||
## CIS baseline audit rules
|
||||
-D
|
||||
-b 8192
|
||||
-f 1
|
||||
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
|
||||
-w /etc/localtime -p wa -k time-change
|
||||
-w /etc/group -p wa -k identity
|
||||
-w /etc/passwd -p wa -k identity
|
||||
-w /etc/shadow -p wa -k identity
|
||||
-w /etc/gshadow -p wa -k identity
|
||||
-w /etc/security/opasswd -p wa -k identity
|
||||
-a always,exit -F arch=b64 -S sethostname,setdomainname -k system-locale
|
||||
-w /etc/hosts -p wa -k system-locale
|
||||
-w /var/log/lastlog -p wa -k logins
|
||||
-w /var/run/faillock -p wa -k logins
|
||||
-w /var/run/utmp -p wa -k session
|
||||
-w /var/log/wtmp -p wa -k session
|
||||
-w /var/log/btmp -p wa -k session
|
||||
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat,chown,fchown,fchownat,lchown -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-w /etc/sudoers -p wa -k scope
|
||||
-w /etc/sudoers.d -p wa -k scope
|
||||
-a always,exit -F arch=b64 -S init_module,delete_module -k modules
|
||||
-e 2
|
||||
|
||||
- name: Enable the audit daemon
|
||||
when: cis_effective_rules.auditd | default(false)
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable auditd"
|
||||
register: cis_auditd_enable
|
||||
changed_when: "'Created symlink' in cis_auditd_enable.stderr"
|
||||
@@ -1,12 +1,35 @@
|
||||
---
|
||||
- name: Ensure the Default UMASK is Set Correctly
|
||||
when: cis_effective_rules.umask_default | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/mnt/etc/profile"
|
||||
regexp: "^(\\s*)umask\\s+\\d+"
|
||||
line: "umask {{ cis_cfg.umask_profile }}"
|
||||
|
||||
- name: Set the login.defs UMASK (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.umask_default | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/login.defs
|
||||
regexp: '^\s*#?\s*UMASK\b'
|
||||
line: "UMASK\t\t{{ cis_cfg.umask_profile }}"
|
||||
|
||||
# authselect regenerates system-auth from the profile, so a direct edit is lost
|
||||
# on the next apply; without-nullok is the supported way to drop nullok there.
|
||||
- name: Prevent Login to Accounts With Empty Password (authselect)
|
||||
when:
|
||||
- cis_effective_rules.empty_password_login | default(false)
|
||||
- is_authselect | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} authselect enable-feature without-nullok"
|
||||
register: cis_nullok_result
|
||||
changed_when: cis_nullok_result.rc == 0
|
||||
|
||||
# Non-RHEL/non-Debian distros: loop evaluates to [] (intentional skip)
|
||||
- name: Prevent Login to Accounts With Empty Password
|
||||
when:
|
||||
- cis_effective_rules.empty_password_login | default(false)
|
||||
- not is_authselect | bool
|
||||
ansible.builtin.replace:
|
||||
dest: "{{ item }}"
|
||||
regexp: "\\s*nullok"
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
---
|
||||
# Fedora ships its own crypto-policies preset and update-crypto-policies
|
||||
# behaves differently; applying DEFAULT:NO-SHA1 can break package signing.
|
||||
# EL10 dropped the NO-SHA1 subpolicy module (DEFAULT already disables SHA-1
|
||||
# signatures), so the modifier is set only on EL9 and below.
|
||||
- name: Configure System Cryptography Policy
|
||||
when: os in (os_family_rhel | difference(['fedora']))
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1"
|
||||
vars:
|
||||
_cis_crypto_policy: "{{ 'DEFAULT' if (os_version_major | int >= 10) else 'DEFAULT:NO-SHA1' }}"
|
||||
when:
|
||||
- cis_effective_rules.crypto_policy | default(false)
|
||||
- os in (os_family_rhel | difference(['fedora']))
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set {{ _cis_crypto_policy }}"
|
||||
register: cis_crypto_policy_result
|
||||
changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout"
|
||||
|
||||
- name: Mask Systemd Services
|
||||
when: cis_effective_rules.mask_services | default(false)
|
||||
ansible.builtin.command: >
|
||||
{{ chroot_command }} systemctl mask {{ 'nftables' if system_cfg.features.firewall.toolkit == 'iptables' else 'iptables' }} bluetooth rpcbind
|
||||
register: cis_mask_services_result
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
- name: Ensure files exist
|
||||
- name: Ensure cron and at access files exist
|
||||
when: cis_effective_rules.cron_at_access | default(false)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: touch
|
||||
@@ -7,10 +8,19 @@
|
||||
loop:
|
||||
- /mnt/etc/at.allow
|
||||
- /mnt/etc/cron.allow
|
||||
|
||||
- name: Ensure TCP wrapper files exist
|
||||
when: cis_effective_rules.tcp_wrappers | default(false)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: touch
|
||||
mode: "0600"
|
||||
loop:
|
||||
- /mnt/etc/hosts.allow
|
||||
- /mnt/etc/hosts.deny
|
||||
|
||||
- name: Ensure files do not exist
|
||||
- name: Ensure cron and at deny files do not exist
|
||||
when: cis_effective_rules.cron_at_access | default(false)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
|
||||
31
roles/cis/tasks/grub_password.yml
Normal file
31
roles/cis/tasks/grub_password.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# Opt-in only: a GRUB superuser password blocks unattended menu edits; the default entry still boots.
|
||||
- name: Assert a GRUB password hash is supplied
|
||||
when: cis_effective_rules.grub_password | default(false)
|
||||
ansible.builtin.assert:
|
||||
that: cis_cfg.grub_password_hash | length > 0
|
||||
fail_msg: >-
|
||||
system.features.cis.rules.grub_password is enabled but
|
||||
system.features.cis.params.grub_password_hash is empty. Generate one with
|
||||
grub2-mkpasswd-pbkdf2 and set it there.
|
||||
quiet: true
|
||||
|
||||
- name: Deploy the GRUB superuser password
|
||||
when: cis_effective_rules.grub_password | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/grub.d/01_cis_password
|
||||
mode: "0755"
|
||||
content: |
|
||||
#!/bin/sh
|
||||
cat <<'EOF'
|
||||
set superusers="root"
|
||||
password_pbkdf2 root {{ cis_cfg.grub_password_hash }}
|
||||
EOF
|
||||
|
||||
- name: Regenerate the GRUB configuration
|
||||
when: cis_effective_rules.grub_password | default(false)
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }}
|
||||
{{ 'grub2-mkconfig -o /boot/grub2/grub.cfg' if is_rhel | bool else 'grub-mkconfig -o /boot/grub/grub.cfg' }}
|
||||
register: cis_grub_regen
|
||||
changed_when: cis_grub_regen.rc == 0
|
||||
@@ -3,7 +3,6 @@
|
||||
ansible.builtin.import_tasks: _normalize.yml
|
||||
|
||||
- name: Apply CIS hardening
|
||||
when: cis_enabled
|
||||
block:
|
||||
- name: Include CIS hardening tasks
|
||||
ansible.builtin.include_tasks: "{{ cis_task }}"
|
||||
@@ -16,5 +15,11 @@
|
||||
- security_lines.yml
|
||||
- permissions.yml
|
||||
- sshd.yml
|
||||
- warning_banners.yml
|
||||
- password_expiry.yml
|
||||
- aide.yml
|
||||
- auditd.yml
|
||||
- packages.yml
|
||||
- grub_password.yml
|
||||
loop_control:
|
||||
loop_var: cis_task
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
- name: Disable Kernel Modules
|
||||
when: cis_effective_rules.module_blacklist | default(false)
|
||||
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_all: "{{ cis_cfg.modules_blacklist + cis_modules_squashfs }}"
|
||||
ansible.builtin.copy:
|
||||
@@ -14,11 +15,13 @@
|
||||
{% endfor %}
|
||||
|
||||
- name: Remove old USB rules file
|
||||
when: cis_effective_rules.usb_lockdown | default(false)
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/udev/rules.d/10-cis_usb_devices.sh
|
||||
state: absent
|
||||
|
||||
- name: Create USB rules
|
||||
when: cis_effective_rules.usb_lockdown | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/udev/rules.d/10-cis_usb_devices.rules
|
||||
mode: "0644"
|
||||
|
||||
29
roles/cis/tasks/packages.yml
Normal file
29
roles/cis/tasks/packages.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
# CIS L1 names legacy cleartext clients (telnet) for removal. They are absent on
|
||||
# a fresh minimal install; query first and remove only when present so the run
|
||||
# stays idempotent (a chroot package-manager remove cannot use the package module).
|
||||
- name: Check for insecure cleartext clients
|
||||
when: cis_strict | default(false)
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }}
|
||||
{{ 'dpkg -s' if is_debian | bool else 'pacman -Q' if os == 'archlinux' else 'rpm -q' }}
|
||||
{{ item }}
|
||||
loop: "{{ cis_cfg.insecure_packages }}"
|
||||
register: cis_insecure_present
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Remove insecure cleartext clients (CIS L1+)
|
||||
when:
|
||||
- cis_strict | default(false)
|
||||
- item.rc == 0
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }}
|
||||
{{ 'apt-get remove -y' if is_debian | bool else 'pacman -R --noconfirm' if os == 'archlinux' else 'dnf remove -y' }}
|
||||
{{ item.item }}
|
||||
loop: "{{ cis_insecure_present.results | default([]) }}"
|
||||
changed_when: true
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
22
roles/cis/tasks/password_expiry.yml
Normal file
22
roles/cis/tasks/password_expiry.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# login.defs sets policy for future accounts; existing service accounts are intentionally not chage-aged.
|
||||
- name: Configure password aging defaults
|
||||
when: cis_effective_rules.password_expiry | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/login.defs
|
||||
regexp: '^#?\s*{{ item.key }}\b'
|
||||
line: "{{ item.key }}\t{{ item.value }}"
|
||||
loop:
|
||||
- {key: PASS_MAX_DAYS, value: "{{ cis_cfg.pass_max_days }}"}
|
||||
- {key: PASS_MIN_DAYS, value: "{{ cis_cfg.pass_min_days }}"}
|
||||
- {key: PASS_WARN_AGE, value: "{{ cis_cfg.pass_warn_age }}"}
|
||||
loop_control:
|
||||
label: "{{ item.key }}"
|
||||
|
||||
# account_disable_post_pw_expiration: lock accounts INACTIVE days after expiry.
|
||||
- name: Set the default account inactivity lock period
|
||||
when: cis_effective_rules.password_expiry | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/default/useradd
|
||||
regexp: '^\s*#?\s*INACTIVE\s*='
|
||||
line: "INACTIVE={{ cis_cfg.pass_inactive }}"
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
- name: Check CIS permission targets
|
||||
when: cis_effective_rules.file_permissions | default(false)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item.path }}"
|
||||
loop: "{{ cis_permission_targets }}"
|
||||
@@ -9,12 +10,14 @@
|
||||
changed_when: false
|
||||
|
||||
- name: Set permissions for existing targets
|
||||
when:
|
||||
- cis_effective_rules.file_permissions | default(false)
|
||||
- item.stat.exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.item.path }}"
|
||||
owner: "{{ item.item.owner | default(omit) }}"
|
||||
group: "{{ item.item.group | default(omit) }}"
|
||||
mode: "{{ item.item.mode }}"
|
||||
loop: "{{ cis_permission_stats.results }}"
|
||||
loop: "{{ cis_permission_stats.results | default([]) }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.path }}"
|
||||
when: item.stat.exists
|
||||
|
||||
@@ -1,62 +1,218 @@
|
||||
---
|
||||
- name: Add Security related lines into config files
|
||||
- name: Restrict core dumps
|
||||
when: cis_effective_rules.core_dumps | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/limits.conf
|
||||
regexp: '^\*\s+hard\s+core\s+'
|
||||
line: "* hard core 0"
|
||||
|
||||
- name: Ensure the systemd coredump drop-in directory exists (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.core_dumps | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/coredump.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Disable systemd core dump storage and backtraces (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.core_dumps | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/coredump.conf.d/10-cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Coredump]
|
||||
Storage=none
|
||||
ProcessSizeMax=0
|
||||
|
||||
- name: Set password quality requirements
|
||||
when: cis_effective_rules.pwquality | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/pwquality.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- {regexp: '^\s*#?\s*minlen\s*=', line: "minlen = {{ cis_cfg.pwquality_minlen }}"}
|
||||
- {regexp: '^\s*#?\s*dcredit\s*=', line: "dcredit = -1"}
|
||||
- {regexp: '^\s*#?\s*ucredit\s*=', line: "ucredit = -1"}
|
||||
- {regexp: '^\s*#?\s*ocredit\s*=', line: "ocredit = -1"}
|
||||
- {regexp: '^\s*#?\s*lcredit\s*=', line: "lcredit = -1"}
|
||||
loop_control:
|
||||
label: "{{ item.line }}"
|
||||
|
||||
# Stricter complexity SSG cis_server_l1 checks; affects only new-password changes.
|
||||
- name: Set strict password quality requirements (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.pwquality | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/pwquality.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- {regexp: '^\s*#?\s*difok\s*=', line: "difok = {{ cis_cfg.pwquality_difok }}"}
|
||||
- {regexp: '^\s*#?\s*maxrepeat\s*=', line: "maxrepeat = {{ cis_cfg.pwquality_maxrepeat }}"}
|
||||
- {regexp: '^\s*#?\s*maxsequence\s*=', line: "maxsequence = {{ cis_cfg.pwquality_maxsequence }}"}
|
||||
- {regexp: '^\s*#?\s*minclass\s*=', line: "minclass = {{ cis_cfg.pwquality_minclass }}"}
|
||||
- {regexp: '^\s*#?\s*dictcheck\s*=', line: "dictcheck = {{ cis_cfg.pwquality_dictcheck }}"}
|
||||
- {regexp: '^\s*#?\s*enforce_for_root\b', line: "enforce_for_root"}
|
||||
loop_control:
|
||||
label: "{{ item.line }}"
|
||||
|
||||
- name: Set the default shell umask
|
||||
when: cis_effective_rules.umask_default | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*umask\s+\d+'
|
||||
line: "umask {{ cis_cfg.umask }}"
|
||||
|
||||
- name: Set the shell idle timeout
|
||||
when: cis_effective_rules.shell_timeout | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*(export\s+)?TMOUT='
|
||||
line: "export TMOUT={{ cis_cfg.tmout }}"
|
||||
|
||||
# A drop-in survives systemd upgrades; the RHEL vendor journald.conf does not.
|
||||
- name: Ensure the journald drop-in directory exists
|
||||
when: cis_effective_rules.journald_persistent | default(false)
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/journald.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Enable persistent journald storage
|
||||
when: cis_effective_rules.journald_persistent | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/journald.conf.d/10-cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
|
||||
- name: Compress large journald log files (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.journald_persistent | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/journald.conf.d/20-cis-compress.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Journal]
|
||||
Compress=yes
|
||||
|
||||
- name: Log sudo commands
|
||||
when: cis_effective_rules.sudo_logfile | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+logfile='
|
||||
line: 'Defaults logfile="/var/log/sudo.log"'
|
||||
|
||||
- name: Require a pty for sudo (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.sudo_logfile | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+use_pty\b'
|
||||
line: "Defaults use_pty"
|
||||
|
||||
- name: Restrict su to the wheel group
|
||||
when: cis_effective_rules.su_restriction | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/pam.d/su
|
||||
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
|
||||
line: auth required pam_wheel.so
|
||||
|
||||
# authselect wires the pam_faillock stack via the feature; deny/unlock_time live
|
||||
# in faillock.conf, the supported place (pam_faillock(8) deprecates module args).
|
||||
- name: Configure account lockout (authselect)
|
||||
when:
|
||||
- cis_effective_rules.faillock | default(false)
|
||||
- is_authselect | bool
|
||||
block:
|
||||
- name: Enable the authselect faillock feature
|
||||
ansible.builtin.command: "{{ chroot_command }} authselect enable-feature with-faillock"
|
||||
register: cis_faillock_result
|
||||
changed_when: cis_faillock_result.rc == 0
|
||||
|
||||
- name: Set faillock thresholds
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/security/faillock.conf
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
create: true
|
||||
mode: "0644"
|
||||
loop:
|
||||
- {regexp: '^\s*#?\s*deny\s*=', line: "deny = {{ cis_cfg.faillock_deny }}"}
|
||||
- {regexp: '^\s*#?\s*unlock_time\s*=', line: "unlock_time = {{ cis_cfg.faillock_unlock_time }}"}
|
||||
loop_control:
|
||||
label: "{{ item.line }}"
|
||||
|
||||
- name: Configure account lockout
|
||||
when:
|
||||
- cis_effective_rules.faillock | default(false)
|
||||
- not is_authselect | bool
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.content }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- { path: /mnt/etc/security/limits.conf, regexp: '^\*\s+hard\s+core\s+', content: "* hard core 0" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*minlen\s*=', content: "minlen = {{ cis_cfg.pwquality_minlen }}" }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*dcredit\s*=', content: dcredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ucredit\s*=', content: ucredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ocredit\s*=', content: ocredit = -1 }
|
||||
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*lcredit\s*=', content: lcredit = -1 }
|
||||
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*umask\s+\d+'
|
||||
content: "umask {{ cis_cfg.umask }}"
|
||||
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
|
||||
regexp: '^\s*(export\s+)?TMOUT='
|
||||
content: "export TMOUT={{ cis_cfg.tmout }}"
|
||||
- path: '/mnt/{{ "usr/lib/systemd/journald.conf" if is_rhel | bool else "etc/systemd/journald.conf" }}'
|
||||
regexp: '^\s*#?\s*Storage='
|
||||
content: Storage=persistent
|
||||
- path: /mnt/etc/sudoers
|
||||
regexp: '^\s*Defaults\s+logfile='
|
||||
content: Defaults logfile="/var/log/sudo.log"
|
||||
- path: /mnt/etc/pam.d/su
|
||||
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
|
||||
content: auth required pam_wheel.so
|
||||
- path: >-
|
||||
/mnt/etc/{{
|
||||
"pam.d/common-auth"
|
||||
if is_debian | bool
|
||||
else "authselect/system-auth"
|
||||
if os == "fedora"
|
||||
else "pam.d/system-auth"
|
||||
}}
|
||||
- path: '/mnt/etc/{{ "pam.d/common-auth" if is_debian | bool else "pam.d/system-auth" }}'
|
||||
regexp: '^\s*auth\s+required\s+pam_faillock\.so'
|
||||
content: >-
|
||||
line: >-
|
||||
auth required pam_faillock.so onerr=fail audit silent deny={{ cis_cfg.faillock_deny }} unlock_time={{ cis_cfg.faillock_unlock_time }}
|
||||
- path: >-
|
||||
/mnt/etc/{{
|
||||
"pam.d/common-account"
|
||||
if is_debian | bool
|
||||
else "authselect/system-auth"
|
||||
if os == "fedora"
|
||||
else "pam.d/system-auth"
|
||||
}}
|
||||
- path: '/mnt/etc/{{ "pam.d/common-account" if is_debian | bool else "pam.d/system-auth" }}'
|
||||
regexp: '^\s*account\s+required\s+pam_faillock\.so'
|
||||
content: account required pam_faillock.so
|
||||
- path: >-
|
||||
line: account required pam_faillock.so
|
||||
loop_control:
|
||||
label: "{{ item.regexp }}"
|
||||
|
||||
- name: Enforce password history
|
||||
when: cis_effective_rules.password_history | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: >-
|
||||
/mnt/etc/pam.d/{{
|
||||
"common-password"
|
||||
if is_debian | bool
|
||||
else "passwd"
|
||||
}}
|
||||
regexp: '^\s*password\s+\[success=1.*\]\s+pam_unix\.so'
|
||||
content: >-
|
||||
line: >-
|
||||
password [success=1 default=ignore] pam_unix.so obscure sha512 remember={{ cis_cfg.password_remember }}
|
||||
- { path: /mnt/etc/hosts.deny, regexp: '^ALL:\s*ALL', content: "ALL: ALL" }
|
||||
- { path: /mnt/etc/hosts.allow, regexp: '^sshd:\s*ALL', content: "sshd: ALL" }
|
||||
|
||||
# SSG cis_server_l1 checks pam_pwhistory (not pam_unix remember) in the auth-stack
|
||||
# files; affects only password changes, so no login-lockout risk. EL9 has no
|
||||
# authselect path here (same direct-edit the faillock rule above uses).
|
||||
- name: Enforce password reuse limit via pam_pwhistory (CIS L1+)
|
||||
when:
|
||||
- cis_effective_rules.password_history | default(false)
|
||||
- cis_strict | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item }}"
|
||||
regexp: '^\s*password\s+(requisite|required)\s+pam_pwhistory\.so'
|
||||
line: "password requisite pam_pwhistory.so use_authtok remember={{ cis_cfg.pwhistory_remember }} enforce_for_root"
|
||||
insertbefore: '^\s*password\s+.*pam_unix\.so'
|
||||
loop: >-
|
||||
{{
|
||||
['/mnt/etc/pam.d/system-auth', '/mnt/etc/pam.d/password-auth']
|
||||
if is_rhel | bool
|
||||
else (['/mnt/etc/pam.d/common-password'] if is_debian | bool else [])
|
||||
}}
|
||||
loop_control:
|
||||
label: "{{ item.content }}"
|
||||
label: "{{ item }}"
|
||||
|
||||
|
||||
- name: Configure TCP wrappers
|
||||
when: cis_effective_rules.tcp_wrappers | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item.path }}"
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- {path: /mnt/etc/hosts.deny, regexp: '^ALL:\s*ALL', line: "ALL: ALL"}
|
||||
- {path: /mnt/etc/hosts.allow, regexp: '^sshd:\s*ALL', line: "sshd: ALL"}
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
- name: Adjust SSHD config
|
||||
when: cis_effective_rules.sshd_hardening | default(false)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/ssh/sshd_config
|
||||
regexp: ^\s*#?{{ item.option }}\s+.*$
|
||||
@@ -9,6 +10,7 @@
|
||||
label: "{{ item.option }}"
|
||||
|
||||
- name: Detect target OpenSSH version
|
||||
when: cis_effective_rules.sshd_hardening | default(false)
|
||||
ansible.builtin.shell: >-
|
||||
set -o pipefail && {{ chroot_command }} ssh -V 2>&1 | grep -oP 'OpenSSH_\K[0-9]+\.[0-9]+'
|
||||
args:
|
||||
@@ -18,6 +20,7 @@
|
||||
failed_when: false
|
||||
|
||||
- name: Append CIS specific configurations to sshd_config
|
||||
when: cis_effective_rules.sshd_hardening | default(false)
|
||||
vars:
|
||||
cis_sshd_has_mlkem: "{{ (cis_sshd_openssh_version.stdout | default('0.0') is version('9.9', '>=')) }}"
|
||||
cis_sshd_kex: >-
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
---
|
||||
- name: Create a consolidated sysctl configuration file
|
||||
when: cis_effective_rules.sysctl_hardening | default(false)
|
||||
vars:
|
||||
# ipv6_disable is a separate rule: when off, drop the disable_ipv6 keys but keep the rest.
|
||||
_cis_sysctl: >-
|
||||
{{ cis_cfg.sysctl
|
||||
if (cis_effective_rules.ipv6_disable | default(false))
|
||||
else (cis_cfg.sysctl | dict2items | rejectattr('key', 'search', 'disable_ipv6') | items2dict) }}
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/sysctl.d/10-cis.conf
|
||||
# 99- so CIS wins: a 10- name loses to vendor /usr/lib/sysctl.d/10-default-yama-scope.conf
|
||||
# (later basename applies last), which reset kernel.yama.ptrace_scope back to 0.
|
||||
dest: /mnt/etc/sysctl.d/99-cis.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
## CIS Sysctl configurations
|
||||
{% for key, value in cis_cfg.sysctl | dictsort %}
|
||||
{% for key, value in _cis_sysctl | dictsort %}
|
||||
{{ key }}={{ value }}
|
||||
{% endfor %}
|
||||
|
||||
11
roles/cis/tasks/warning_banners.yml
Normal file
11
roles/cis/tasks/warning_banners.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Set login warning banners
|
||||
when: cis_effective_rules.warning_banners | default(false)
|
||||
ansible.builtin.copy:
|
||||
dest: "/mnt/etc/{{ item }}"
|
||||
content: "{{ cis_cfg.banner_text }}\n"
|
||||
mode: "0644"
|
||||
loop:
|
||||
- issue
|
||||
- issue.net
|
||||
- motd
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
# OS-specific binary names for CIS permission targets.
|
||||
# fusermount3 is the modern name; older distros still use fusermount.
|
||||
# fusermount3 is the modern name; older distros still ship fusermount.
|
||||
cis_fusermount_binary: >-
|
||||
{{
|
||||
'fusermount3'
|
||||
@@ -19,3 +18,235 @@ cis_write_binary: >-
|
||||
if (os == 'debian' and (os_version | string) == '11')
|
||||
else 'write'
|
||||
}}
|
||||
|
||||
cis_pkg_install: >-
|
||||
{{ chroot_command }} {{
|
||||
'apt-get install -y'
|
||||
if is_debian | bool
|
||||
else 'pacman -S --noconfirm'
|
||||
if os == 'archlinux'
|
||||
else 'dnf install -y'
|
||||
}}
|
||||
|
||||
# Rule catalog: control -> CIS level + whether a task implements it.
|
||||
# `default` enables only implemented rules; `l1`/`l2` add the level-tagged ones.
|
||||
cis_rule_catalog:
|
||||
module_blacklist: {level: l1, implemented: true} # fs/net modprobe blacklist (list per profile)
|
||||
usb_lockdown: {level: l2, implemented: true} # udev authorized_default=0 (aggressive)
|
||||
sysctl_hardening: {level: l1, implemented: true}
|
||||
ipv6_disable: {level: l2, implemented: true} # disable_ipv6 subset of the sysctl set
|
||||
umask_default: {level: l1, implemented: true}
|
||||
empty_password_login: {level: l1, implemented: true}
|
||||
pwquality: {level: l1, implemented: true}
|
||||
core_dumps: {level: l1, implemented: true}
|
||||
shell_timeout: {level: l1, implemented: true}
|
||||
journald_persistent: {level: l1, implemented: true}
|
||||
sudo_logfile: {level: l1, implemented: true}
|
||||
su_restriction: {level: l1, implemented: true}
|
||||
faillock: {level: l1, implemented: true}
|
||||
password_history: {level: l1, implemented: true}
|
||||
tcp_wrappers: {level: l1, implemented: true}
|
||||
crypto_policy: {level: l1, implemented: true} # RedHat non-Fedora only
|
||||
mask_services: {level: l1, implemented: true}
|
||||
cron_at_access: {level: l1, implemented: true}
|
||||
file_permissions: {level: l1, implemented: true}
|
||||
sshd_hardening: {level: l1, implemented: true}
|
||||
password_expiry: {level: l1, implemented: true} # login.defs aging policy
|
||||
aide: {level: l1, implemented: true} # file-integrity db + daily check
|
||||
warning_banners: {level: l1, implemented: true} # /etc/issue, issue.net, motd
|
||||
auditd: {level: l2, implemented: true} # audit daemon + CIS rule set
|
||||
grub_password: {level: l1, implemented: true} # opt-in only; needs params.grub_password_hash
|
||||
|
||||
# Rules not listed are off. A per-host system.features.cis.rules map overlays this.
|
||||
cis_profiles:
|
||||
# default = established house behaviour, kept byte-for-byte unchanged.
|
||||
default:
|
||||
module_blacklist: true
|
||||
usb_lockdown: true
|
||||
sysctl_hardening: true
|
||||
ipv6_disable: true
|
||||
umask_default: true
|
||||
empty_password_login: true
|
||||
pwquality: true
|
||||
core_dumps: true
|
||||
shell_timeout: true
|
||||
journald_persistent: true
|
||||
sudo_logfile: true
|
||||
su_restriction: true
|
||||
faillock: true
|
||||
password_history: true
|
||||
tcp_wrappers: true
|
||||
crypto_policy: true
|
||||
mask_services: true
|
||||
cron_at_access: true
|
||||
file_permissions: true
|
||||
sshd_hardening: true
|
||||
# l1 = clean CIS Level 1: drops the L2 extras (usb_lockdown, ipv6_disable).
|
||||
l1:
|
||||
module_blacklist: true
|
||||
sysctl_hardening: true
|
||||
umask_default: true
|
||||
empty_password_login: true
|
||||
pwquality: true
|
||||
core_dumps: true
|
||||
shell_timeout: true
|
||||
journald_persistent: true
|
||||
sudo_logfile: true
|
||||
su_restriction: true
|
||||
faillock: true
|
||||
password_history: true
|
||||
tcp_wrappers: true
|
||||
crypto_policy: true
|
||||
mask_services: true
|
||||
cron_at_access: true
|
||||
file_permissions: true
|
||||
sshd_hardening: true
|
||||
password_expiry: true
|
||||
aide: true
|
||||
warning_banners: true
|
||||
# l2 = l1 plus the defence-in-depth Level 2 controls.
|
||||
l2:
|
||||
module_blacklist: true
|
||||
usb_lockdown: true
|
||||
sysctl_hardening: true
|
||||
ipv6_disable: true
|
||||
umask_default: true
|
||||
empty_password_login: true
|
||||
pwquality: true
|
||||
core_dumps: true
|
||||
shell_timeout: true
|
||||
journald_persistent: true
|
||||
sudo_logfile: true
|
||||
su_restriction: true
|
||||
faillock: true
|
||||
password_history: true
|
||||
tcp_wrappers: true
|
||||
crypto_policy: true
|
||||
mask_services: true
|
||||
cron_at_access: true
|
||||
file_permissions: true
|
||||
sshd_hardening: true
|
||||
password_expiry: true
|
||||
aide: true
|
||||
warning_banners: true
|
||||
auditd: true
|
||||
|
||||
# Override per host via system.features.cis.params: dicts deep-merge,
|
||||
# list-valued keys (e.g. sshd_options) replace wholesale.
|
||||
cis_param_defaults:
|
||||
modules_blacklist:
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- cramfs
|
||||
- udf
|
||||
- usb-storage
|
||||
- dccp
|
||||
- sctp
|
||||
- rds
|
||||
- tipc
|
||||
- firewire-core
|
||||
- firewire-sbp2
|
||||
- thunderbolt
|
||||
sysctl:
|
||||
fs.suid_dumpable: 0
|
||||
kernel.dmesg_restrict: 1
|
||||
kernel.kptr_restrict: 2
|
||||
kernel.perf_event_paranoid: 3
|
||||
kernel.unprivileged_bpf_disabled: 1
|
||||
kernel.yama.ptrace_scope: 2
|
||||
kernel.randomize_va_space: 2
|
||||
net.ipv4.ip_forward: 0
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
net.ipv4.conf.all.log_martians: 1
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.all.secure_redirects: 0
|
||||
net.ipv4.conf.all.send_redirects: 0
|
||||
net.ipv4.conf.all.accept_redirects: 0
|
||||
net.ipv4.conf.all.accept_source_route: 0
|
||||
net.ipv4.conf.all.arp_ignore: 1
|
||||
net.ipv4.conf.all.arp_announce: 2
|
||||
net.ipv4.conf.default.log_martians: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
net.ipv4.conf.default.secure_redirects: 0
|
||||
net.ipv4.conf.default.send_redirects: 0
|
||||
net.ipv4.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.all.accept_redirects: 0
|
||||
net.ipv6.conf.all.disable_ipv6: 1
|
||||
net.ipv6.conf.default.accept_redirects: 0
|
||||
net.ipv6.conf.default.disable_ipv6: 1
|
||||
net.ipv6.conf.lo.disable_ipv6: 1
|
||||
sshd_options:
|
||||
- {option: LogLevel, value: VERBOSE}
|
||||
- {option: LoginGraceTime, value: "60"}
|
||||
- {option: PermitRootLogin, value: "no"}
|
||||
- {option: StrictModes, value: "yes"}
|
||||
- {option: MaxAuthTries, value: "4"}
|
||||
- {option: MaxSessions, value: "10"}
|
||||
- {option: MaxStartups, value: "10:30:60"}
|
||||
- {option: PubkeyAuthentication, value: "yes"}
|
||||
- {option: HostbasedAuthentication, value: "no"}
|
||||
- {option: IgnoreRhosts, value: "yes"}
|
||||
- {option: PasswordAuthentication, value: "no"}
|
||||
- {option: PermitEmptyPasswords, value: "no"}
|
||||
- {option: KerberosAuthentication, value: "no"}
|
||||
- {option: GSSAPIAuthentication, value: "no"}
|
||||
- {option: AllowAgentForwarding, value: "no"}
|
||||
- {option: AllowTcpForwarding, value: "no"}
|
||||
- {option: KbdInteractiveAuthentication, value: "no"}
|
||||
- {option: GatewayPorts, value: "no"}
|
||||
- {option: X11Forwarding, value: "no"}
|
||||
- {option: PermitUserEnvironment, value: "no"}
|
||||
- {option: ClientAliveInterval, value: "300"}
|
||||
- {option: ClientAliveCountMax, value: "1"}
|
||||
- {option: PermitTunnel, value: "no"}
|
||||
- {option: Banner, value: /etc/issue.net}
|
||||
pwquality_minlen: 14
|
||||
# pwquality strict set (l1/l2 only, cis_strict): SSG cis_server_l1 values.
|
||||
pwquality_difok: 2
|
||||
pwquality_maxrepeat: 3
|
||||
pwquality_maxsequence: 3
|
||||
pwquality_minclass: 4
|
||||
pwquality_dictcheck: 1
|
||||
tmout: 900
|
||||
umask: "077"
|
||||
umask_profile: "027"
|
||||
faillock_deny: 5
|
||||
faillock_unlock_time: 900
|
||||
password_remember: 5
|
||||
# pwhistory remember (l1/l2 only, cis_strict): SSG wants 24 via pam_pwhistory.
|
||||
pwhistory_remember: 24
|
||||
# password_expiry (l1/l2): /etc/login.defs aging.
|
||||
pass_max_days: 365
|
||||
pass_min_days: 1
|
||||
pass_warn_age: 7
|
||||
# account_disable_post_pw_expiration (l1/l2): days after expiry to lock (SSG=45).
|
||||
pass_inactive: 45
|
||||
# aide (l1/l2): daily integrity-check schedule.
|
||||
aide_cron_hour: "5"
|
||||
aide_cron_minute: "0"
|
||||
# warning_banners (l1/l2): login/MOTD text.
|
||||
banner_text: "Authorized access only. All activity may be monitored and reported."
|
||||
# grub_password (opt-in only): a grub2 pbkdf2 hash; empty unless opted in.
|
||||
grub_password_hash: ""
|
||||
# insecure_packages (l1/l2 only, cis_strict): legacy cleartext clients to remove.
|
||||
insecure_packages:
|
||||
- telnet
|
||||
|
||||
# Only the module blacklist differs by profile: l1 trims to the L1 filesystem
|
||||
# modules; default/l2 keep the full list.
|
||||
cis_profile_params:
|
||||
default: {}
|
||||
l1:
|
||||
modules_blacklist:
|
||||
- cramfs
|
||||
- freevxfs
|
||||
- jffs2
|
||||
- hfs
|
||||
- hfsplus
|
||||
- udf
|
||||
- usb-storage
|
||||
l2: {}
|
||||
|
||||
@@ -16,13 +16,20 @@
|
||||
loop: >-
|
||||
{{
|
||||
['ide0', 'ide2']
|
||||
+ (['ide1'] if not (os == 'rhel' and system_cfg.features.rhel_repo.source == 'iso') else [])
|
||||
+ (['ide1'] if not (os == 'rhel' and system_cfg.content.source == 'dvd') else [])
|
||||
}}
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Start the VM
|
||||
- name: Ensure the installer environment is powered off
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: restarted
|
||||
state: stopped
|
||||
force: true
|
||||
no_log: true
|
||||
|
||||
- name: Boot the installed OS
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: started
|
||||
no_log: true
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
- name: Unmount Disks
|
||||
become: true
|
||||
block:
|
||||
- name: Unmount the bootstrap package cache
|
||||
ansible.posix.mount:
|
||||
path: /mnt/var/cache
|
||||
state: unmounted
|
||||
|
||||
- name: Remove the bootstrap package cache so it is not sealed into the image
|
||||
ansible.builtin.file:
|
||||
path: /mnt/.bootstrap-cache
|
||||
state: absent
|
||||
|
||||
- name: Disable Swap
|
||||
ansible.builtin.command: swapoff -a
|
||||
register: cleanup_swapoff_result
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
]
|
||||
if (rhel_iso is defined and rhel_iso | length > 0
|
||||
and not (os == 'rhel' and system_cfg.features.rhel_repo.source == 'iso'))
|
||||
and not (os == 'rhel' and system_cfg.content.source == 'dvd'))
|
||||
else []
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
# Network configuration dispatch — maps OS name to the task file
|
||||
# that writes network config. Default (NetworkManager) applies to
|
||||
# all OSes not explicitly listed.
|
||||
configuration_network_task_map:
|
||||
alpine: network_alpine.yml
|
||||
void: network_void.yml
|
||||
# Network backend is detected per host from the target rootfs in network.yml;
|
||||
# no static map needed.
|
||||
|
||||
@@ -41,6 +41,18 @@
|
||||
|
||||
- name: Configure sudo banner
|
||||
when: system_cfg.features.banner.sudo | bool
|
||||
block:
|
||||
- name: Detect the target sudo implementation
|
||||
ansible.builtin.command: "{{ chroot_command }} /usr/bin/sudo --version"
|
||||
register: configuration_sudo_version
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
# sudo-rs (Ubuntu 25.10+) implements neither `lecture` nor `lecture_file`
|
||||
# and warns on every sudo call when they are set. It prints its version banner
|
||||
# to stderr, not stdout, so match against both streams.
|
||||
- name: Configure the sudo lecture
|
||||
when: "'sudo-rs' not in (configuration_sudo_version.stdout ~ configuration_sudo_version.stderr)"
|
||||
block:
|
||||
- name: Create sudo lecture file
|
||||
ansible.builtin.copy:
|
||||
|
||||
@@ -155,5 +155,5 @@
|
||||
ansible.builtin.include_tasks: encryption/dracut.yml
|
||||
|
||||
- name: Configure GRUB for LUKS
|
||||
when: _initramfs_generator | default('') != 'dracut' or os_family != 'RedHat'
|
||||
when: _initramfs_generator | default('') != 'dracut'
|
||||
ansible.builtin.include_tasks: encryption/grub.yml
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
install_items+=" {{ configuration_luks_keyfile_path }} "
|
||||
{% endif %}
|
||||
{% if configuration_luks_auto_method == 'tpm2' %}
|
||||
add_dracutmodules+=" tpm2-tss "
|
||||
install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} "
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
# --- Kernel cmdline: write rd.luks.* args for dracut ---
|
||||
- name: Ensure kernel cmdline directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/kernel
|
||||
@@ -58,7 +58,6 @@
|
||||
mode: "0644"
|
||||
content: "{{ _dracut_kernel_cmdline }}\n"
|
||||
|
||||
# --- BLS entries: RedHat-specific ---
|
||||
- name: Update BLS entries with LUKS kernel cmdline
|
||||
when: os_family == 'RedHat'
|
||||
vars:
|
||||
|
||||
@@ -8,8 +8,18 @@
|
||||
when:
|
||||
- configuration_luks_auto_method == 'tpm2'
|
||||
- _tpm2_method | default('') == 'clevis'
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} apt install -y clevis clevis-luks clevis-tpm2 clevis-initramfs tpm2-tools
|
||||
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
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Sets _initramfs_generator and _tpm2_method facts.
|
||||
#
|
||||
# Generator detection: derived from the platform's initramfs_cmd
|
||||
# (dracut → dracut, mkinitcpio → mkinitcpio, else → initramfs-tools)
|
||||
# (dracut -> dracut, mkinitcpio -> mkinitcpio, else -> initramfs-tools)
|
||||
# TPM2 method: systemd-cryptenroll when generator supports tpm2-device,
|
||||
# clevis fallback otherwise. Non-native dracut installed automatically.
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
LUKS keyfile enrollment failed — falling back to manual unlock at boot.
|
||||
LUKS keyfile enrollment failed - falling back to manual unlock at boot.
|
||||
The system will prompt for the LUKS passphrase during startup.
|
||||
|
||||
- name: Fallback to manual LUKS unlock if keyfile enrollment failed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
# 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.
|
||||
# remains as a backup unlock method - no auto-generated keyfiles.
|
||||
- name: Enroll TPM2 for LUKS
|
||||
block:
|
||||
- name: Create temporary passphrase file for TPM2 enrollment
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
- name: Create zram config
|
||||
when:
|
||||
- (os != "debian" or (os_version | string) != "11") and os != "rhel"
|
||||
- os not in ["alpine", "void"]
|
||||
- system_cfg.features.swap.enabled | bool
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/systemd/zram-generator.conf
|
||||
|
||||
34
roles/configuration/tasks/firewall.yml
Normal file
34
roles/configuration/tasks/firewall.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
- name: Enable the firewall daemon in the install chroot
|
||||
when:
|
||||
- firewall_phase == 'install'
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ system_cfg.features.firewall.backend }}"
|
||||
register: _firewall_enable
|
||||
changed_when: _firewall_enable.rc == 0
|
||||
failed_when: >-
|
||||
_firewall_enable.rc != 0
|
||||
and 'No such file or directory' not in (_firewall_enable.stderr | default(''))
|
||||
and 'does not exist' not in (_firewall_enable.stderr | default(''))
|
||||
|
||||
# ufw's CLI needs a running kernel and is a no-op in the chroot (leaves ENABLED=no),
|
||||
# so its activation and SSH rule are applied here, after reboot.
|
||||
- name: Allow SSH through ufw before enabling
|
||||
when:
|
||||
- firewall_phase == 'postreboot'
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
- system_cfg.features.ssh.enabled | bool
|
||||
ansible.builtin.command: ufw allow 22/tcp
|
||||
register: _ufw_allow
|
||||
changed_when: "'added' in _ufw_allow.stdout or 'updated' in _ufw_allow.stdout"
|
||||
|
||||
- name: Activate ufw on the booted target
|
||||
when:
|
||||
- firewall_phase == 'postreboot'
|
||||
- system_cfg.features.firewall.backend == 'ufw'
|
||||
- system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: ufw --force enable
|
||||
register: _ufw_enable
|
||||
changed_when: "'active' in _ufw_enable.stdout"
|
||||
@@ -26,7 +26,7 @@
|
||||
- name: Remove RHEL ISO fstab entry when not using local repo
|
||||
when:
|
||||
- os == "rhel"
|
||||
- system_cfg.features.rhel_repo.source != "iso"
|
||||
- system_cfg.content.source != "dvd"
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/fstab
|
||||
regexp: "^.*\\/dvd.*$"
|
||||
@@ -35,7 +35,7 @@
|
||||
- name: Replace ISO UUID entry with /dev/sr0 in fstab
|
||||
when:
|
||||
- os == "rhel"
|
||||
- system_cfg.features.rhel_repo.source == "iso"
|
||||
- system_cfg.content.source == "dvd"
|
||||
vars:
|
||||
configuration_fstab_dvd_line: >-
|
||||
{{
|
||||
@@ -53,7 +53,7 @@
|
||||
when:
|
||||
- os == "rhel"
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.features.rhel_repo.source == "iso"
|
||||
- system_cfg.content.source == "dvd"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- dd
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
line: "{{ item.line }}"
|
||||
loop:
|
||||
- regexp: ^GRUB_CMDLINE_LINUX_DEFAULT=
|
||||
line: GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
|
||||
line: 'GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3{{ (" " ~ (_hardware_profile_kernel_params | join(" "))) if (_hardware_profile_kernel_params | default([]) | length > 0) else "" }}"'
|
||||
- regexp: ^GRUB_TIMEOUT=
|
||||
line: GRUB_TIMEOUT=1
|
||||
loop_control:
|
||||
@@ -43,19 +43,21 @@
|
||||
}}
|
||||
grub_root_flags: >-
|
||||
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }}
|
||||
# String-concat (not list-concat like grub_kernel_cmdline_base below): ansible-lint's
|
||||
# jinja render trips on list+list when grub_lvm_args leads the expression here.
|
||||
grub_cmdline_linux_base: >-
|
||||
{{
|
||||
(['crashkernel=auto'] + grub_lvm_args)
|
||||
| join(' ')
|
||||
((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
|
||||
}}
|
||||
grub_kernel_cmdline_base: >-
|
||||
{{
|
||||
(
|
||||
(['root=UUID=' + grub_root_uuid]
|
||||
if grub_root_uuid | length > 0 else [])
|
||||
+ ['ro', 'crashkernel=auto']
|
||||
+ ['ro']
|
||||
+ grub_lvm_args
|
||||
+ grub_root_flags
|
||||
+ (_hardware_profile_kernel_params | default([]))
|
||||
)
|
||||
| join(' ')
|
||||
}}
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
- name: Include configuration tasks
|
||||
when: configuration_task.when | default(true)
|
||||
ansible.builtin.include_tasks: "{{ configuration_task.file }}"
|
||||
vars:
|
||||
firewall_phase: install
|
||||
loop:
|
||||
- file: repositories.yml
|
||||
when: "{{ os_family == 'Debian' }}"
|
||||
- file: banner.yml
|
||||
- file: fstab.yml
|
||||
- file: locales.yml
|
||||
- file: ssh.yml
|
||||
- file: services.yml
|
||||
- file: firewall.yml
|
||||
- file: grub.yml
|
||||
- file: encryption.yml
|
||||
when: "{{ system_cfg.luks.enabled | bool }}"
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
---
|
||||
- name: Read network interfaces
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ip
|
||||
- -o
|
||||
- link
|
||||
- show
|
||||
register: configuration_ip_link
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Detect available network interface names
|
||||
vars:
|
||||
configuration_detected_interfaces: >-
|
||||
{{
|
||||
configuration_ip_link.stdout
|
||||
| default('')
|
||||
| regex_findall('^[0-9]+: ([^:]+):', multiline=True)
|
||||
| reject('equalto', 'lo')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_detected_interfaces: "{{ configuration_detected_interfaces }}"
|
||||
|
||||
- name: Validate at least one network interface detected
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- configuration_detected_interfaces | length > 0
|
||||
fail_msg: Failed to detect any network interfaces.
|
||||
|
||||
- name: Set DNS configuration facts
|
||||
ansible.builtin.set_fact:
|
||||
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
|
||||
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
|
||||
|
||||
- name: Configure networking
|
||||
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"
|
||||
# 2+ unnamed interfaces would all match the first-ethernet glob, leaving the rest unconfigured.
|
||||
- name: Require an explicit name on every interface for multi-NIC
|
||||
vars:
|
||||
_unnamed: "{{ system_cfg.network.interfaces | map(attribute='name', default='') | map('string') | select('equalto', '') | list | length }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.network.interfaces | length <= 1 or _unnamed == 0
|
||||
fail_msg: >-
|
||||
Multi-NIC (system.network.interfaces with 2+ entries) requires a name on
|
||||
every interface; the first-adapter glob only binds a single NIC.
|
||||
|
||||
# Probe /mnt to detect the stack the installed rootfs will run (nothing runs in
|
||||
# the chroot). NM is checked first and wins, since bootstrap installs it on every
|
||||
# family; the rest are the fallback for a non-NM base image.
|
||||
- name: Probe the installed network stack on the target rootfs
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
register: configuration_net_probe
|
||||
loop:
|
||||
- /mnt/usr/bin/nmcli
|
||||
- /mnt/usr/lib/systemd/system/NetworkManager.service
|
||||
- /mnt/usr/sbin/netplan
|
||||
- /mnt/etc/netplan
|
||||
- /mnt/sbin/ifup
|
||||
- /mnt/usr/sbin/ifup
|
||||
- /mnt/etc/systemd/system/multi-user.target.wants/systemd-networkd.service
|
||||
- /mnt/etc/systemd/system/dbus-org.freedesktop.network1.service
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Resolve the network backend from the probe
|
||||
vars:
|
||||
_found: "{{ configuration_net_probe.results | selectattr('stat.exists') | map(attribute='item') | list }}"
|
||||
ansible.builtin.set_fact:
|
||||
configuration_network_backend: >-
|
||||
{{
|
||||
'nm' if (['/mnt/usr/bin/nmcli', '/mnt/usr/lib/systemd/system/NetworkManager.service'] | intersect(_found))
|
||||
else 'netplan' if (['/mnt/usr/sbin/netplan', '/mnt/etc/netplan'] | intersect(_found))
|
||||
else 'eni' if (['/mnt/sbin/ifup', '/mnt/usr/sbin/ifup'] | intersect(_found))
|
||||
else 'networkd' if (['/mnt/etc/systemd/system/multi-user.target.wants/systemd-networkd.service', '/mnt/etc/systemd/system/dbus-org.freedesktop.network1.service'] | intersect(_found))
|
||||
else 'nm'
|
||||
}}
|
||||
|
||||
- name: Configure networking for the detected backend {{ configuration_network_backend }}
|
||||
ansible.builtin.include_tasks: "network_{{ configuration_network_backend }}.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 %}
|
||||
35
roles/configuration/tasks/network_eni.yml
Normal file
35
roles/configuration/tasks/network_eni.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
# ifupdown can't glob iface stanzas (no mapping on ifupdown2/Proxmox), so ENI binds
|
||||
# a literal name detected here. The chroot only sees live-ISO names: on a real
|
||||
# ifupdown base, set system.network.interfaces[].name to the installed name. Bootstrap
|
||||
# installs NetworkManager, so this fires only on a non-NM base image.
|
||||
- name: Detect ethernet interface names
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ip
|
||||
- -o
|
||||
- link
|
||||
- show
|
||||
register: configuration_eni_link
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resolve detected ethernet interface names
|
||||
ansible.builtin.set_fact:
|
||||
configuration_eni_detected: >-
|
||||
{{
|
||||
configuration_eni_link.stdout | default('')
|
||||
| regex_findall('^[0-9]+: ([^:@]+)[@:].*?link/ether', multiline=True)
|
||||
}}
|
||||
|
||||
- name: Ensure the network configuration directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/network
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write the ifupdown interfaces file
|
||||
ansible.builtin.template:
|
||||
src: network_eni.j2
|
||||
dest: /mnt/etc/network/interfaces
|
||||
mode: "0644"
|
||||
12
roles/configuration/tasks/network_netplan.yml
Normal file
12
roles/configuration/tasks/network_netplan.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- name: Ensure the netplan directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/netplan
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write the netplan configuration
|
||||
ansible.builtin.template:
|
||||
src: network_netplan.j2
|
||||
dest: /mnt/etc/netplan/10-sg.yaml
|
||||
mode: "0600"
|
||||
18
roles/configuration/tasks/network_networkd.yml
Normal file
18
roles/configuration/tasks/network_networkd.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
- name: Ensure the systemd-networkd directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/network
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write systemd-networkd configuration per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
ansible.builtin.template:
|
||||
src: network_networkd.j2
|
||||
dest: "/mnt/etc/systemd/network/10-static-{{ idx }}.network"
|
||||
mode: "0644"
|
||||
loop: "{{ system_cfg.network.interfaces }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
label: "10-static-{{ idx }}"
|
||||
@@ -2,7 +2,6 @@
|
||||
- name: Copy NetworkManager keyfile per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
configuration_iface_name: "{{ item.name | default(configuration_detected_interfaces[idx] | default('')) }}"
|
||||
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
|
||||
ansible.builtin.template:
|
||||
src: network.j2
|
||||
|
||||
@@ -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 %}
|
||||
@@ -1,25 +1,84 @@
|
||||
---
|
||||
- name: Write final sources.list
|
||||
# Config runs against the chroot, so these write /mnt directly via templates
|
||||
# rather than apt_repository/yum_repository, which would touch the live host.
|
||||
- name: Write the apt sources.list
|
||||
when: os_family == 'Debian'
|
||||
vars:
|
||||
_debian_release_map:
|
||||
"10": buster
|
||||
"11": bullseye
|
||||
"12": bookworm
|
||||
"13": trixie
|
||||
unstable: sid
|
||||
_ubuntu_release_map:
|
||||
ubuntu: questing
|
||||
ubuntu-lts: noble
|
||||
ubuntu-lts: resolute
|
||||
ansible.builtin.template:
|
||||
src: "{{ os | replace('-lts', '') }}.sources.list.j2"
|
||||
dest: /mnt/etc/apt/sources.list
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure apt performance configuration persists
|
||||
- name: Ensure apt performance and content-proxy configuration
|
||||
when: os_family == 'Debian'
|
||||
ansible.builtin.copy:
|
||||
dest: /mnt/etc/apt/apt.conf.d/99performance
|
||||
content: |
|
||||
Acquire::Retries "3";
|
||||
Acquire::http::Pipeline-Depth "10";
|
||||
APT::Install-Recommends "false";
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
Acquire::http::Proxy "{{ system_cfg.content.proxy }}";
|
||||
Acquire::https::Proxy "{{ system_cfg.content.proxy }}";
|
||||
{% endif %}
|
||||
mode: "0644"
|
||||
|
||||
- name: Drop the install-time DVD repo from the target on non-dvd sources
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source != 'dvd'
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/yum.repos.d/redhat.repo
|
||||
state: absent
|
||||
|
||||
- name: Write the EL mirror repo on the target
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length > 0
|
||||
ansible.builtin.template:
|
||||
src: el_mirror.repo.j2
|
||||
dest: "/mnt/etc/yum.repos.d/{{ os }}.repo"
|
||||
mode: "0644"
|
||||
|
||||
- name: Find the stock vendor repos shipped by the release package
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length > 0
|
||||
ansible.builtin.find:
|
||||
paths: /mnt/etc/yum.repos.d
|
||||
patterns: "*.repo"
|
||||
excludes: "{{ os }}.repo"
|
||||
register: el_stock_repos
|
||||
|
||||
- name: Remove the stock vendor repos so only the custom mirror is reachable
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.source == 'mirror'
|
||||
- system_cfg.content.url | length > 0
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ el_stock_repos.files | default([]) }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Configure the dnf content proxy on the target
|
||||
when:
|
||||
- os_family == 'RedHat'
|
||||
- system_cfg.content.proxy | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/dnf/dnf.conf
|
||||
line: "proxy={{ system_cfg.content.proxy }}"
|
||||
regexp: "^proxy="
|
||||
create: true
|
||||
mode: "0644"
|
||||
state: present
|
||||
|
||||
45
roles/configuration/tasks/satellite_register.yml
Normal file
45
roles/configuration/tasks/satellite_register.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
# Invoked post-reboot on the booted host, not in the chroot: subscription-manager
|
||||
# needs a running systemd and the live network.
|
||||
- name: Add the Satellite host to /etc/hosts
|
||||
when: system_cfg.content.satellite.ip | length > 0
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
line: "{{ system_cfg.content.satellite.ip }} {{ system_cfg.content.satellite.host }}"
|
||||
regexp: "[[:space:]]{{ system_cfg.content.satellite.host | regex_escape }}([[:space:]]|$)"
|
||||
state: present
|
||||
|
||||
- name: Fetch the Katello CA consumer RPM
|
||||
ansible.builtin.get_url:
|
||||
url: >-
|
||||
{{ system_cfg.content.satellite.ca_url
|
||||
if (system_cfg.content.satellite.ca_url | length > 0)
|
||||
else 'https://' ~ system_cfg.content.satellite.host ~ '/pub/katello-ca-consumer-latest.noarch.rpm' }}
|
||||
dest: /tmp/katello-ca-consumer-latest.noarch.rpm
|
||||
validate_certs: false
|
||||
mode: "0644"
|
||||
|
||||
- name: Install the Katello CA consumer RPM
|
||||
ansible.builtin.dnf:
|
||||
name: /tmp/katello-ca-consumer-latest.noarch.rpm
|
||||
state: present
|
||||
disable_gpg_check: true
|
||||
|
||||
- name: Clean any stale subscription identity
|
||||
ansible.builtin.command: subscription-manager clean
|
||||
changed_when: true
|
||||
|
||||
- name: Register with Satellite via activation key
|
||||
no_log: true
|
||||
community.general.redhat_subscription:
|
||||
state: present
|
||||
server_hostname: "{{ system_cfg.content.satellite.host }}"
|
||||
org_id: "{{ system_cfg.content.satellite.org }}"
|
||||
activationkey: "{{ system_cfg.content.satellite.activation_key }}"
|
||||
environment: "{{ system_cfg.content.satellite.environment | default(omit, true) }}"
|
||||
force_register: true
|
||||
server_proxy_hostname: "{{ (system_cfg.content.proxy | urlsplit('hostname')) | default(omit, true) }}"
|
||||
server_proxy_port: "{{ (system_cfg.content.proxy | urlsplit('port')) | default(omit, true) }}"
|
||||
syspurpose:
|
||||
service_level_agreement: "{{ system_cfg.content.satellite.service_level | default(omit, true) }}"
|
||||
sync: true
|
||||
@@ -1,4 +1,15 @@
|
||||
---
|
||||
- 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
|
||||
|
||||
@@ -11,6 +11,16 @@
|
||||
register: configuration_setfiles_result
|
||||
changed_when: configuration_setfiles_result.rc == 0
|
||||
|
||||
# setfiles in the chroot misses paths created at first boot (e.g. /var/lib/sss),
|
||||
# leaving unlabeled_t files that block services under enforcing SELinux. Force a
|
||||
# complete relabel on first boot; fixfiles consumes and removes the flag.
|
||||
- name: Force a complete SELinux relabel on first boot
|
||||
when: os in ['almalinux', 'rocky', 'rhel'] and system_cfg.features.selinux.enabled | bool
|
||||
ansible.builtin.file:
|
||||
path: /mnt/.autorelabel
|
||||
state: touch
|
||||
mode: "0644"
|
||||
|
||||
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
|
||||
# is left permissive and expected to relabel on first boot.
|
||||
- name: Disable SELinux
|
||||
|
||||
@@ -1,105 +1,248 @@
|
||||
---
|
||||
- name: Resolve desktop facts
|
||||
when: system_cfg.features.desktop.enabled | bool
|
||||
vars:
|
||||
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
||||
ansible.builtin.set_fact:
|
||||
# KDE resolves to the plasmalogin unit on Arch/Fedora44+ (Plasma 6.6), else sddm.
|
||||
_desktop_dm: >-
|
||||
{{
|
||||
('plasmalogin'
|
||||
if system_cfg.features.desktop.display_manager == 'plasma-login-manager'
|
||||
else system_cfg.features.desktop.display_manager)
|
||||
if (system_cfg.features.desktop.display_manager | length > 0)
|
||||
else (
|
||||
('plasmalogin'
|
||||
if (os == 'archlinux' or (os == 'fedora' and (os_version | int) >= 44))
|
||||
else 'sddm')
|
||||
if system_cfg.features.desktop.environment == 'kde'
|
||||
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
|
||||
when: _configuration_platform.init_system == 'systemd'
|
||||
vars:
|
||||
_desktop_dm: >-
|
||||
{{
|
||||
system_cfg.features.desktop.display_manager
|
||||
if (system_cfg.features.desktop.display_manager | length > 0)
|
||||
else (configuration_desktop_dm_map[system_cfg.features.desktop.environment] | default(''))
|
||||
}}
|
||||
configuration_systemd_services: >-
|
||||
{{
|
||||
['NetworkManager']
|
||||
+ (['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
|
||||
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
|
||||
['NetworkManager', _configuration_platform.time_sync_service]
|
||||
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
|
||||
+ ([_desktop_dm] if system_cfg.features.desktop.enabled | bool and _desktop_dm | length > 0 else [])
|
||||
+ (['logrotate'] if os == 'archlinux' else [])
|
||||
+ (['bluetooth'] if system_cfg.features.desktop.enabled | bool else [])
|
||||
}}
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
|
||||
loop: "{{ configuration_systemd_services }}"
|
||||
register: configuration_enable_service_result
|
||||
changed_when: configuration_enable_service_result.rc == 0
|
||||
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: 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: Check for the EL qemu-guest-agent RPC allow-list
|
||||
ansible.builtin.stat:
|
||||
path: /mnt/etc/sysconfig/qemu-ga
|
||||
register: configuration_qga_sysconfig
|
||||
|
||||
- name: Set default systemd target to graphical
|
||||
- name: Allow clone-stamping RPCs in the EL qemu-guest-agent allow-list
|
||||
when: configuration_qga_sysconfig.stat.exists
|
||||
ansible.builtin.replace:
|
||||
path: /mnt/etc/sysconfig/qemu-ga
|
||||
regexp: '^(FILTER_RPC_ARGS="--allow-rpcs=(?:(?!guest-exec)[^"])*)"'
|
||||
replace: '\1,guest-exec,guest-exec-status,guest-file-open,guest-file-close,guest-file-read,guest-file-write"'
|
||||
|
||||
- name: Enable display manager for selected desktop
|
||||
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
|
||||
- _desktop_dm | length > 0
|
||||
- _desktop_dm != 'ly'
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ _desktop_dm }}"
|
||||
register: configuration_enable_dm_result
|
||||
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: Enable OpenRC services
|
||||
when: _configuration_platform.init_system == 'openrc'
|
||||
- name: Enable ly on its tty
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'ly'
|
||||
vars:
|
||||
configuration_openrc_services: >-
|
||||
{{
|
||||
['networking']
|
||||
+ (['sshd'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ ([system_cfg.features.firewall.backend] if system_cfg.features.firewall.enabled | bool else [])
|
||||
}}
|
||||
_ly_tty: tty2
|
||||
block:
|
||||
- name: Ensure OpenRC runlevel directory exists
|
||||
- name: Enable ly display manager
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl enable ly@{{ _ly_tty }}.service"
|
||||
register: configuration_enable_ly_result
|
||||
changed_when: configuration_enable_ly_result.rc == 0
|
||||
failed_when: >-
|
||||
configuration_enable_ly_result.rc != 0
|
||||
or 'No such file or directory' in (configuration_enable_ly_result.stderr | default(''))
|
||||
or 'does not exist' in (configuration_enable_ly_result.stderr | default(''))
|
||||
|
||||
# ly drives the VT itself; mask getty so logind never spawns a login on that tty.
|
||||
- name: Mask getty on ly's tty
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl mask getty@{{ _ly_tty }}.service"
|
||||
register: configuration_mask_getty_result
|
||||
changed_when: configuration_mask_getty_result.rc == 0
|
||||
failed_when: >-
|
||||
configuration_mask_getty_result.rc != 0
|
||||
and 'No such file or directory' not in (configuration_mask_getty_result.stderr | default(''))
|
||||
and 'does not exist' not in (configuration_mask_getty_result.stderr | default(''))
|
||||
|
||||
- name: Set default systemd target
|
||||
when: _configuration_platform.init_system == 'systemd'
|
||||
vars:
|
||||
_default_target: "{{ 'graphical.target' if system_cfg.features.desktop.enabled | bool else 'multi-user.target' }}"
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl set-default {{ _default_target }}"
|
||||
register: _set_default_target_result
|
||||
changed_when: _set_default_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:
|
||||
- name: Ensure greetd config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/runlevels/default
|
||||
path: /mnt/etc/greetd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Check OpenRC init scripts
|
||||
ansible.builtin.stat:
|
||||
path: "/mnt/etc/init.d/{{ item }}"
|
||||
loop: "{{ configuration_openrc_services }}"
|
||||
register: configuration_openrc_service_stats
|
||||
- name: Write greetd config.toml
|
||||
ansible.builtin.template:
|
||||
src: greetd-config.toml.j2
|
||||
dest: /mnt/etc/greetd/config.toml
|
||||
mode: "0644"
|
||||
|
||||
- name: Enable OpenRC services
|
||||
ansible.builtin.file:
|
||||
src: "/mnt/etc/init.d/{{ item.item }}"
|
||||
dest: "/mnt/etc/runlevels/default/{{ item.item }}"
|
||||
state: link
|
||||
loop: "{{ configuration_openrc_service_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: item.stat.exists
|
||||
|
||||
- name: Enable runit services
|
||||
when: _configuration_platform.init_system == 'runit'
|
||||
- name: Configure GDM autologin
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'gdm'
|
||||
- _desktop_autologin_user | length > 0
|
||||
vars:
|
||||
configuration_runit_services: >-
|
||||
{{
|
||||
['dhcpcd']
|
||||
+ (['sshd'] if system_cfg.features.ssh.enabled | bool else [])
|
||||
+ ([system_cfg.features.firewall.backend] if system_cfg.features.firewall.enabled | bool else [])
|
||||
}}
|
||||
# Debian gdm3 reads daemon.conf; RedHat/Arch gdm read custom.conf.
|
||||
_gdm_dir: "/mnt/etc/{{ 'gdm3' if os_family == 'Debian' else 'gdm' }}"
|
||||
_gdm_conf: "{{ 'daemon.conf' if os_family == 'Debian' else 'custom.conf' }}"
|
||||
block:
|
||||
- name: Ensure runit service directory exists
|
||||
- name: Ensure GDM config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/var/service
|
||||
path: "{{ _gdm_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Check runit service definitions
|
||||
ansible.builtin.stat:
|
||||
path: "/mnt/etc/sv/{{ item }}"
|
||||
loop: "{{ configuration_runit_services }}"
|
||||
register: configuration_runit_service_stats
|
||||
- name: Write GDM autologin config
|
||||
ansible.builtin.template:
|
||||
src: gdm-custom.conf.j2
|
||||
dest: "{{ _gdm_dir }}/{{ _gdm_conf }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Enable runit services
|
||||
# SDDM and plasma-login-manager share the [Autologin] format and the KDE Wayland
|
||||
# session; only the config dir differs (sddm.conf.d vs plasmalogin.conf.d).
|
||||
- name: Configure SDDM / plasma-login-manager autologin
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm in ['sddm', 'plasmalogin']
|
||||
- _desktop_autologin_user | length > 0
|
||||
vars:
|
||||
_autologin_conf_dir: "/mnt/etc/{{ 'plasmalogin.conf.d' if _desktop_dm == 'plasmalogin' else 'sddm.conf.d' }}"
|
||||
block:
|
||||
- name: Ensure KDE login-manager config directory exists
|
||||
ansible.builtin.file:
|
||||
src: "/mnt/etc/sv/{{ item.item }}"
|
||||
dest: "/mnt/var/service/{{ item.item }}"
|
||||
state: link
|
||||
loop: "{{ configuration_runit_service_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: item.stat.exists
|
||||
path: "{{ _autologin_conf_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
# Plasma 6 ships the Wayland session as plasma.desktop; Plasma 5 ships it as
|
||||
# plasmawayland.desktop (plasma.desktop is the X11 session there). Pick the
|
||||
# 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 KDE login-manager autologin drop-in
|
||||
ansible.builtin.template:
|
||||
src: sddm-autologin.conf.j2
|
||||
dest: "{{ _autologin_conf_dir }}/10-autologin.conf"
|
||||
mode: "0644"
|
||||
|
||||
# ly ships a flat (sectionless) config.ini; edit it in place to keep upstream
|
||||
# defaults. Both keys are required: an unresolved session writes 'null', which
|
||||
# disables autologin rather than leaving it half-configured.
|
||||
- name: Configure ly autologin
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- system_cfg.features.desktop.enabled | bool
|
||||
- _desktop_dm == 'ly'
|
||||
- _desktop_autologin_user | length > 0
|
||||
community.general.ini_file:
|
||||
path: /mnt/etc/ly/config.ini
|
||||
option: "{{ item.key }}"
|
||||
value: "{{ item.value }}"
|
||||
create: false
|
||||
mode: "0644"
|
||||
loop:
|
||||
- key: auto_login_user
|
||||
value: "{{ _desktop_autologin_user }}"
|
||||
- key: auto_login_session
|
||||
value: "{{ _greetd_session if (_greetd_session | length > 0) else 'null' }}"
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
validate: /usr/sbin/visudo --check --file=%s
|
||||
|
||||
- name: Deploy per-user sudoers rules
|
||||
when: item.value.sudo is defined and (item.value.sudo | string | length > 0)
|
||||
# Jinja truthiness: bool true / a rule string => deploy; false / '' / unset => skip.
|
||||
when: item.value.sudo | default(false)
|
||||
vars:
|
||||
configuration_sudoers_rule: >-
|
||||
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
when: (system_cfg.root.password | default('') | string | length) > 0
|
||||
ansible.builtin.shell: >-
|
||||
set -o pipefail &&
|
||||
echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} /usr/sbin/chpasswd -e
|
||||
echo 'root:{{ system_cfg.root.password if (system_cfg.root.password | string)[:1] == "$" else system_cfg.root.password | password_hash("sha512") }}'
|
||||
| {{ chroot_command }} /usr/sbin/chpasswd -e
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: configuration_root_result
|
||||
@@ -26,11 +27,15 @@
|
||||
- name: Create user accounts
|
||||
vars:
|
||||
configuration_user_group: "{{ _configuration_platform.user_group }}"
|
||||
# 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: >-
|
||||
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
|
||||
--uid {{ 1000 + _idx }}
|
||||
--groups {{ configuration_user_group }} {{ item.key }}
|
||||
{{ ('--password ' ~ (item.value.password | password_hash('sha512'))) if (item.value.password | default('') | string | length > 0) else '' }}
|
||||
{{ ('--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 }}"
|
||||
loop: "{{ system_cfg.users | dict2items }}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = _debian_release_map[os_version | string] | default('trixie') %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
|
||||
{% set mirror = system_cfg.content.url | default('http://deb.debian.org/debian', true) %}
|
||||
{% set components = 'main contrib non-free non-free-firmware' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
deb-src {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
17
roles/configuration/templates/el_mirror.repo.j2
Normal file
17
roles/configuration/templates/el_mirror.repo.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
[{{ os }}{{ os_version_major }}-baseos]
|
||||
name={{ os }} {{ os_version_major }} BaseOS
|
||||
baseurl={{ system_cfg.content.url }}/BaseOS
|
||||
enabled=1
|
||||
gpgcheck={{ 1 if system_cfg.content.gpgcheck | bool else 0 }}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
|
||||
[{{ os }}{{ os_version_major }}-appstream]
|
||||
name={{ os }} {{ os_version_major }} AppStream
|
||||
baseurl={{ system_cfg.content.url }}/AppStream
|
||||
enabled=1
|
||||
gpgcheck={{ 1 if system_cfg.content.gpgcheck | bool else 0 }}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
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 %}
|
||||
@@ -3,12 +3,18 @@ id=LAN-{{ idx }}
|
||||
uuid={{ configuration_net_uuid }}
|
||||
type=ethernet
|
||||
autoconnect-priority=10
|
||||
{% if configuration_iface_name | length > 0 %}
|
||||
interface-name={{ configuration_iface_name }}
|
||||
{% endif %}
|
||||
|
||||
[ipv4]
|
||||
{% set iface = configuration_iface %}
|
||||
{% if iface.name | default('') | string | length %}
|
||||
interface-name={{ iface.name }}
|
||||
|
||||
{% else %}
|
||||
{# Bind the first available ethernet by name glob, never a MAC: a clone with a new adapter/MAC stays networked (#12). #}
|
||||
|
||||
[match]
|
||||
interface-name=en*;eth*;
|
||||
|
||||
{% endif %}
|
||||
[ipv4]
|
||||
{% set dns_list = configuration_dns_list %}
|
||||
{% set search_list = configuration_dns_search %}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
|
||||
23
roles/configuration/templates/network_eni.j2
Normal file
23
roles/configuration/templates/network_eni.j2
Normal file
@@ -0,0 +1,23 @@
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
{% set ifname = iface.name if (iface.name | default('') | string | length) else (configuration_eni_detected[loop.index0] | default('eth' ~ loop.index0)) %}
|
||||
auto {{ ifname }}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
iface {{ ifname }} inet static
|
||||
address {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
gateway {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_list %}
|
||||
dns-nameservers {{ configuration_dns_list | join(' ') }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_search %}
|
||||
dns-search {{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
iface {{ ifname }} inet dhcp
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
29
roles/configuration/templates/network_netplan.j2
Normal file
29
roles/configuration/templates/network_netplan.j2
Normal file
@@ -0,0 +1,29 @@
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
lan{{ loop.index0 }}:
|
||||
{# Unnamed binds the first ethernet by name glob (e* = en*/eth*, netplan match.name takes one glob), never a MAC (#12). #}
|
||||
match:
|
||||
name: "{{ iface.name if (iface.name | default('') | string | length) else 'e*' }}"
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
addresses:
|
||||
- {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
routes:
|
||||
- to: default
|
||||
via: {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
dhcp4: true
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and (configuration_dns_list or configuration_dns_search) %}
|
||||
nameservers:
|
||||
{% if configuration_dns_list %}
|
||||
addresses: [{{ configuration_dns_list | join(', ') }}]
|
||||
{% endif %}
|
||||
{% if configuration_dns_search %}
|
||||
search: [{{ configuration_dns_search | join(', ') }}]
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
27
roles/configuration/templates/network_networkd.j2
Normal file
27
roles/configuration/templates/network_networkd.j2
Normal file
@@ -0,0 +1,27 @@
|
||||
[Match]
|
||||
{% set iface = configuration_iface %}
|
||||
{% if iface.name | default('') | string | length %}
|
||||
Name={{ iface.name }}
|
||||
{% else %}
|
||||
{# First available ethernet by name glob + device type, never a MAC (#12). #}
|
||||
Name=en* eth*
|
||||
Type=ether
|
||||
{% endif %}
|
||||
|
||||
[Network]
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
Address={{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
Gateway={{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
DHCP=yes
|
||||
{% endif %}
|
||||
{% if idx | int == 0 and configuration_dns_list %}
|
||||
{% for dns in configuration_dns_list %}
|
||||
DNS={{ dns }}
|
||||
{% endfor %}
|
||||
{% if configuration_dns_search %}
|
||||
Domains={{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Managed by Ansible.
|
||||
{% set release = _ubuntu_release_map[os] | default('noble') %}
|
||||
{% set mirror = system_cfg.mirror %}
|
||||
{% set release = _ubuntu_release_map[os] | default('resolute') %}
|
||||
{% set mirror = system_cfg.content.url %}
|
||||
{% set components = 'main restricted universe multiverse' %}
|
||||
|
||||
deb {{ mirror }} {{ release }} {{ components }}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
# Platform-specific configuration values keyed by os_family.
|
||||
# Consumed as _configuration_platform in tasks via:
|
||||
# configuration_platform_config[os_family]
|
||||
# Keyed by os_family; tasks read configuration_platform_config[os_family] as _configuration_platform.
|
||||
configuration_platform_config:
|
||||
RedHat:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
time_sync_service: chronyd
|
||||
efi_loader: shimx64.efi
|
||||
grub_install: false
|
||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
||||
@@ -17,6 +16,7 @@ configuration_platform_config:
|
||||
user_group: sudo
|
||||
sudo_group: "%sudo"
|
||||
ssh_service: ssh
|
||||
time_sync_service: chrony
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: >-
|
||||
@@ -29,51 +29,27 @@ configuration_platform_config:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
time_sync_service: systemd-timesyncd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: "/usr/sbin/mkinitcpio -P"
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
Suse:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: true
|
||||
init_system: systemd
|
||||
Alpine:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: ""
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: false
|
||||
init_system: openrc
|
||||
Void:
|
||||
user_group: wheel
|
||||
sudo_group: "%wheel"
|
||||
ssh_service: sshd
|
||||
efi_loader: grubx64.efi
|
||||
grub_install: true
|
||||
initramfs_cmd: ""
|
||||
grub_mkconfig_prefix: grub-mkconfig
|
||||
locale_gen: false
|
||||
init_system: runit
|
||||
|
||||
# Display manager auto-detection from desktop environment name.
|
||||
configuration_desktop_dm_map:
|
||||
gnome: gdm
|
||||
kde: sddm
|
||||
xfce: lightdm
|
||||
sway: greetd
|
||||
hyprland: ly@tty2
|
||||
cinnamon: lightdm
|
||||
mate: lightdm
|
||||
lxqt: sddm
|
||||
budgie: gdm
|
||||
hyprland: greetd
|
||||
|
||||
# greetd session commands for sway/hyprland (gnome/kde use a DM instead).
|
||||
configuration_desktop_session_cmd_map:
|
||||
sway: sway
|
||||
hyprland: Hyprland
|
||||
|
||||
# pipewire/pipewire-pulse are socket-activated; wireplumber ships no socket.
|
||||
configuration_desktop_audio_units:
|
||||
- pipewire.socket
|
||||
- pipewire-pulse.socket
|
||||
- wireplumber.service
|
||||
|
||||
@@ -1,10 +1,60 @@
|
||||
---
|
||||
# Connection and timing
|
||||
environment_wait_timeout: 180
|
||||
environment_wait_delay: 5
|
||||
|
||||
# Pacman installer settings
|
||||
environment_parallel_downloads: 20
|
||||
environment_pacman_lock_timeout: 120
|
||||
environment_pacman_retries: 4
|
||||
environment_pacman_retry_delay: 15
|
||||
|
||||
# Installer-tool libraries whose soname may have bumped past the ISO. Each one's
|
||||
# installed reverse-deps are co-upgraded so the install stays a consistent
|
||||
# transaction. Extend if a future transition breaks the install.
|
||||
environment_partial_upgrade_libs:
|
||||
- nettle
|
||||
- leancrypto
|
||||
|
||||
# PCI vendor ID -> vendor code. 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,
|
||||
# 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
|
||||
|
||||
# USB vendor IDs of common Bluetooth controllers. A fallback: detection also
|
||||
# matches the literal "Bluetooth" string in `lsusb` for adapters that omit it.
|
||||
environment_bluetooth_vendor_ids:
|
||||
- "8087" # Intel (AX2xx combo cards)
|
||||
- "0a12" # Cambridge Silicon Radio (CSR)
|
||||
- "0bda" # Realtek
|
||||
- "0cf3" # Qualcomm Atheros
|
||||
- "13d3" # IMC / AzureWave
|
||||
- "0489" # Foxconn / Lite-On
|
||||
- "04ca" # Lite-On
|
||||
- "0b05" # ASUS
|
||||
|
||||
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
|
||||
84
roles/environment/tasks/_detect_hardware.yml
Normal file
84
roles/environment/tasks/_detect_hardware.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
# A user-supplied override profile skips detection (golden-image flow: bake an
|
||||
# image with a fixed profile).
|
||||
- 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 }}"
|
||||
audio: "{{ _hardware_profile_override.audio | default([]) | map('lower') | list }}"
|
||||
fingerprint: "{{ _hardware_profile_override.fingerprint | default(false) | bool }}"
|
||||
bluetooth: "{{ _hardware_profile_override.bluetooth | default(false) | bool }}"
|
||||
camera:
|
||||
uvc: "{{ _hardware_profile_override.camera.uvc | default(false) | bool }}"
|
||||
ipu6: "{{ _hardware_profile_override.camera.ipu6 | 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
|
||||
ansible.builtin.include_tasks: _resolve_hardware_profile.yml
|
||||
|
||||
- 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: []
|
||||
audio: []
|
||||
fingerprint: false
|
||||
bluetooth: false
|
||||
camera: { uvc: false, ipu6: false }
|
||||
|
||||
- name: Merge declarative hardware group over detection
|
||||
when: _hardware_detection_needed | bool
|
||||
ansible.builtin.include_tasks: _merge_hardware_profile.yml
|
||||
|
||||
- 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) }},
|
||||
audio={{ hardware_profile_active.audio | default([]) | join(',') | default('-', true) }},
|
||||
fingerprint={{ hardware_profile_active.fingerprint | default(false) }},
|
||||
bluetooth={{ hardware_profile_active.bluetooth | default(false) }},
|
||||
camera={{ 'uvc' if hardware_profile_active.camera.uvc | default(false) else '' }}{{ '+ipu6' if hardware_profile_active.camera.ipu6 | default(false) else '' }}
|
||||
@@ -77,13 +77,10 @@
|
||||
MaxStartups 50:30:100
|
||||
ClientAliveInterval 30
|
||||
ClientAliveCountMax 10
|
||||
register: _sshd_config_result
|
||||
notify: Restart sshd
|
||||
|
||||
- name: Restart sshd immediately if config was changed
|
||||
when: _sshd_config_result is changed
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
- 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
|
||||
when:
|
||||
|
||||
22
roles/environment/tasks/_merge_hardware_profile.yml
Normal file
22
roles/environment/tasks/_merge_hardware_profile.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# Supplements whatever profile is active (detected or full-override) rather than
|
||||
# replacing it: vendor lists union, booleans OR, cpu overrides when set.
|
||||
- name: Merge declarative hardware group over detection
|
||||
vars:
|
||||
_hw: "{{ system_cfg.features.hardware }}"
|
||||
_det: "{{ hardware_profile_active }}"
|
||||
ansible.builtin.set_fact:
|
||||
hardware_profile_active:
|
||||
cpu: "{{ (_hw.cpu | default('') | string | lower) if (_hw.cpu | default('') | length > 0) else _det.cpu }}"
|
||||
gpus: "{{ ((_det.gpus | default([])) + (_hw.gpus | default([]) | map('lower') | list)) | unique | list }}"
|
||||
nvidia_supports_open: "{{ _det.nvidia_supports_open | default(true) | bool }}"
|
||||
wireless: "{{ ((_det.wireless | default([])) + (_hw.wireless | default([]) | map('lower') | list)) | unique | list }}"
|
||||
audio: "{{ ((_det.audio | default([])) + (_hw.audio | default([]) | map('lower') | list)) | unique | list }}"
|
||||
fingerprint: "{{ (_det.fingerprint | default(false) | bool) or (_hw.fingerprint | default(false) | bool) }}"
|
||||
bluetooth: "{{ (_det.bluetooth | default(false) | bool) or (_hw.bluetooth | default(false) | bool) }}"
|
||||
camera:
|
||||
uvc: "{{ (_det.camera.uvc | default(false) | bool) or (_hw.camera.uvc | default(false) | bool) }}"
|
||||
ipu6: "{{ (_det.camera.ipu6 | default(false) | bool) or (_hw.camera.ipu6 | default(false) | bool) }}"
|
||||
_hardware_profile_packages: "{{ _hw.packages | default({}) }}"
|
||||
_hardware_profile_disable: "{{ _hw.disable | default([]) | list }}"
|
||||
_hardware_profile_kernel_params: "{{ _hw.kernel_params | default([]) | list }}"
|
||||
@@ -14,24 +14,52 @@
|
||||
timeout: "{{ environment_pacman_lock_timeout }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Setup Pacman
|
||||
- name: Resolve installer tools for the target OS
|
||||
when: not (custom_iso | bool)
|
||||
ansible.builtin.set_fact:
|
||||
environment_installer_tools: >-
|
||||
{{
|
||||
['glibc']
|
||||
+ (['lua', 'dnf'] if os in ['almalinux', 'fedora', 'rhel', 'rocky'] else [])
|
||||
+ (['debootstrap'] if os in ['debian', 'ubuntu', 'ubuntu-lts'] else [])
|
||||
+ (['debian-archive-keyring'] if os == 'debian' else [])
|
||||
+ (['ubuntu-keyring'] if os in ['ubuntu', 'ubuntu-lts'] else [])
|
||||
}}
|
||||
|
||||
- name: Query reverse-dependencies of transition-sensitive libraries
|
||||
when:
|
||||
- not (custom_iso | bool)
|
||||
- item.os is not defined or os in item.os
|
||||
- environment_partial_upgrade_libs | length > 0
|
||||
ansible.builtin.command: "pacman -Qi {{ item }}"
|
||||
loop: "{{ environment_partial_upgrade_libs }}"
|
||||
register: environment_revdep_query
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
# Co-upgrade each transition library with its installed reverse-deps so a soname
|
||||
# bump moves the whole closure in one transaction, not a partial upgrade.
|
||||
- name: Setup Pacman
|
||||
when: not (custom_iso | bool)
|
||||
vars:
|
||||
environment_pacman_closure: >-
|
||||
{{
|
||||
(
|
||||
environment_installer_tools
|
||||
+ (environment_revdep_query.results | default([])
|
||||
| selectattr('rc', 'equalto', 0) | map(attribute='item') | list)
|
||||
+ (environment_revdep_query.results | default([])
|
||||
| selectattr('rc', 'equalto', 0) | map(attribute='stdout')
|
||||
| map('regex_search', 'Required By\s*:\s*(.+)', '\1')
|
||||
| map('first') | map('split') | flatten)
|
||||
)
|
||||
| reject('equalto', 'None') | unique
|
||||
}}
|
||||
community.general.pacman:
|
||||
update_cache: true
|
||||
force: true
|
||||
name: "{{ item.name }}"
|
||||
name: "{{ environment_pacman_closure }}"
|
||||
state: latest
|
||||
loop:
|
||||
- { name: glibc }
|
||||
- { name: lua, os: [almalinux, fedora, rhel, rocky] }
|
||||
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
|
||||
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
|
||||
- { name: debian-archive-keyring, os: [debian] }
|
||||
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: environment_tool_install
|
||||
until: environment_tool_install is succeeded
|
||||
retries: "{{ environment_pacman_retries }}"
|
||||
delay: "{{ environment_pacman_retry_delay }}"
|
||||
|
||||
@@ -45,28 +73,25 @@
|
||||
mode: "0755"
|
||||
|
||||
- name: Detect RHEL ISO device
|
||||
ansible.builtin.command: lsblk -rno NAME,TYPE
|
||||
ansible.builtin.command: lsblk -rbno NAME,TYPE,SIZE
|
||||
register: environment_lsblk_result
|
||||
changed_when: false
|
||||
|
||||
- name: Select RHEL ISO device
|
||||
vars:
|
||||
_rom_devices: >-
|
||||
{{
|
||||
environment_lsblk_result.stdout_lines
|
||||
| map('split', ' ')
|
||||
| selectattr('1', 'equalto', 'rom')
|
||||
| map('first')
|
||||
| map('regex_replace', '^', '/dev/')
|
||||
| list
|
||||
}}
|
||||
_roms: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for line in environment_lsblk_result.stdout_lines -%}
|
||||
{%- set p = line.split() -%}
|
||||
{%- if (p | length) >= 3 and p[1] == 'rom' -%}
|
||||
{%- set _ = out.append({'name': p[0], 'size': p[2] | int}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
ansible.builtin.set_fact:
|
||||
environment_rhel_iso_device: >-
|
||||
{{
|
||||
_rom_devices[-1]
|
||||
if _rom_devices | length > 1
|
||||
else (_rom_devices[0] | default('/dev/sr1'))
|
||||
}}
|
||||
{{ ('/dev/' ~ (_roms | sort(attribute='size') | last).name)
|
||||
if (_roms | length) > 0 else '/dev/sr1' }}
|
||||
|
||||
- name: Mount RHEL ISO
|
||||
ansible.posix.mount:
|
||||
@@ -76,10 +101,8 @@
|
||||
opts: "ro,loop"
|
||||
state: mounted
|
||||
|
||||
# Security note: RPM Sequoia signature policy is relaxed to allow
|
||||
# bootstrapping RHEL-family distros from the Arch ISO, where the
|
||||
# host rpm/dnf does not trust target distro GPG keys. Package
|
||||
# integrity is verified by the target system's own rpm after reboot.
|
||||
# RPM Sequoia signature policy is relaxed because the Arch ISO host does not
|
||||
# trust target-distro GPG keys; the target's own rpm re-verifies after reboot.
|
||||
- name: Create RPM macros directory
|
||||
when: is_rhel | bool
|
||||
ansible.builtin.file:
|
||||
|
||||
57
roles/environment/tasks/_resolve_hardware_profile.yml
Normal file
57
roles/environment/tasks/_resolve_hardware_profile.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
# Split out of _detect_hardware.yml so fixtures can seed the lscpu/lspci/lsusb
|
||||
# registers and assert the result with no real hardware. Keep regex exprs
|
||||
# double-quoted single-line: ansible-core 2.21 set_fact mangles backslash escapes
|
||||
# inside folded (>-) scalars.
|
||||
- name: Resolve detected hardware profile
|
||||
vars:
|
||||
_vendor_keys: "{{ environment_pci_vendor_map.keys() | list }}"
|
||||
_cpu_vendor_raw: "{{ _hardware_lscpu.stdout | regex_findall('(?im)^Vendor ID:\\s*(\\S+)') | first | default('') }}"
|
||||
_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 | join('\n')) | regex_findall('\\[([0-9a-f]{4}):([0-9a-f]{4})\\]') | 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 | join('\n')) | regex_findall('\\[([0-9a-f]{4}):[0-9a-f]{4}\\]') | select('in', _vendor_keys) | list }}"
|
||||
_wifi_vendors: "{{ _wifi_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
|
||||
# PCI class 0403 = audio device (HD-audio controller). Vendor drives SOF/firmware.
|
||||
_audio_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0403\\]:') | list }}"
|
||||
_audio_vendor_ids: "{{ (_audio_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):[0-9a-f]{4}\\]') | select('in', _vendor_keys) | list }}"
|
||||
_audio_vendors: "{{ _audio_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 }}"
|
||||
_camera_uvc_present: "{{ (_hardware_lsusb.stdout | default('')) is search('(?i)camera|webcam') }}"
|
||||
# Intel IPU6 MIPI camera: PCI class 0480 (multimedia) under Intel 8086, or an ISP description. Out-of-tree userspace.
|
||||
_camera_ipu6_desc: "{{ (_hardware_lspci.stdout | default('')) is search('(?i)image signal processor|IPU6') }}"
|
||||
_camera_ipu6_pci: "{{ (_hardware_lspci.stdout_lines | select('search', '\\[0480\\]:') | select('search', '\\[8086:') | list) | length > 0 }}"
|
||||
# No backslash escapes here, so a folded scalar is safe (unlike the \[..\] regexes above).
|
||||
_bluetooth_present: >-
|
||||
{{
|
||||
((_hardware_lsusb.stdout | default('')) | regex_search('(?i)ID (' ~ (environment_bluetooth_vendor_ids | join('|')) ~ '):') is not none)
|
||||
or ((_hardware_lsusb.stdout | default('')) is search('(?i)bluetooth'))
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
hardware_profile_active:
|
||||
cpu: "{{ _cpu_vendor }}"
|
||||
gpus: "{{ _gpu_vendors }}"
|
||||
nvidia_supports_open: "{{ _nvidia_supports_open | bool }}"
|
||||
wireless: "{{ _wifi_vendors }}"
|
||||
audio: "{{ _audio_vendors }}"
|
||||
fingerprint: "{{ _fingerprint_present | bool }}"
|
||||
bluetooth: "{{ _bluetooth_present | bool }}"
|
||||
camera:
|
||||
uvc: "{{ _camera_uvc_present | bool }}"
|
||||
ipu6: "{{ (_camera_ipu6_desc | bool) or (_camera_ipu6_pci | bool) }}"
|
||||
@@ -11,5 +11,8 @@
|
||||
- name: Prepare installer environment
|
||||
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
|
||||
ansible.builtin.include_tasks: _thirdparty.yml
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# gpgcheck off: bootstrap-time only; the Arch live env has no AlmaLinux key.
|
||||
[appstream]
|
||||
name=AlmaLinux $releasever - AppStream
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/AppStream/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgcheck=0
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
@@ -14,7 +15,7 @@ name=AlmaLinux $releasever - BaseOS
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/BaseOS/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgcheck=0
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
@@ -25,7 +26,7 @@ name=AlmaLinux $releasever - Extras
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/extras
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/extras/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgcheck=0
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
@@ -36,7 +37,7 @@ name=AlmaLinux $releasever - HighAvailability
|
||||
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/highavailability
|
||||
# baseurl=https://repo.almalinux.org/almalinux/$releasever/HighAvailability/$basearch/os/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgcheck=0
|
||||
countme=1
|
||||
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
|
||||
metadata_expire=86400
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
{% set _baseurl = system_cfg.content.url if system_cfg.content.source == 'mirror' else 'file:///usr/local/install/redhat/dvd' %}
|
||||
[rhel{{ os_version_major }}-baseos]
|
||||
name=RHEL {{ os_version_major }} BaseOS
|
||||
baseurl=file:///usr/local/install/redhat/dvd/BaseOS
|
||||
baseurl={{ _baseurl }}/BaseOS
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
{% if system_cfg.content.source != 'mirror' %}
|
||||
gpgkey=file:///usr/local/install/redhat/dvd/RPM-GPG-KEY-redhat-release
|
||||
{% endif %}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
|
||||
[rhel{{ os_version_major }}-appstream]
|
||||
name=RHEL {{ os_version_major }} AppStream
|
||||
baseurl=file:///usr/local/install/redhat/dvd/AppStream
|
||||
baseurl={{ _baseurl }}/AppStream
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
{% if system_cfg.content.source != 'mirror' %}
|
||||
gpgkey=file:///usr/local/install/redhat/dvd/RPM-GPG-KEY-redhat-release
|
||||
{% endif %}
|
||||
{% if system_cfg.content.proxy | length > 0 %}
|
||||
proxy={{ system_cfg.content.proxy }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# gpgcheck off: bootstrap-time only; the Arch live env has no Rocky key.
|
||||
[baseos]
|
||||
name=Rocky Linux $releasever - BaseOS
|
||||
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=BaseOS-$releasever
|
||||
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/BaseOS/$basearch/os/
|
||||
gpgcheck=1
|
||||
gpgcheck=0
|
||||
enabled=1
|
||||
countme=1
|
||||
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
|
||||
@@ -13,7 +14,7 @@ enabled_metadata=1
|
||||
name=Rocky Linux $releasever - AppStream
|
||||
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=AppStream-$releasever
|
||||
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/AppStream/$basearch/os/
|
||||
gpgcheck=1
|
||||
gpgcheck=0
|
||||
enabled=1
|
||||
countme=1
|
||||
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
# OS family lists — single source of truth for platform detection and validation
|
||||
os_family_rhel:
|
||||
- almalinux
|
||||
- fedora
|
||||
@@ -10,33 +9,26 @@ os_family_debian:
|
||||
- ubuntu
|
||||
- ubuntu-lts
|
||||
|
||||
# OS → family mapping — aligns with the main project's ansible_os_family pattern.
|
||||
# Enables platform_config dict lookups per role instead of inline when: is_rhel chains.
|
||||
# OS -> family, so roles do platform_config lookups instead of is_rhel when-chains.
|
||||
os_family_map:
|
||||
almalinux: RedHat
|
||||
alpine: Alpine
|
||||
archlinux: Archlinux
|
||||
debian: Debian
|
||||
fedora: RedHat
|
||||
opensuse: Suse
|
||||
rhel: RedHat
|
||||
rocky: RedHat
|
||||
ubuntu: Debian
|
||||
ubuntu-lts: Debian
|
||||
void: Void
|
||||
|
||||
os_supported:
|
||||
- almalinux
|
||||
- alpine
|
||||
- archlinux
|
||||
- debian
|
||||
- fedora
|
||||
- opensuse
|
||||
- rhel
|
||||
- rocky
|
||||
- ubuntu
|
||||
- ubuntu-lts
|
||||
- void
|
||||
|
||||
# User input. Normalized into hypervisor_cfg + hypervisor_type.
|
||||
hypervisor:
|
||||
@@ -46,6 +38,8 @@ hypervisor_defaults:
|
||||
url: ""
|
||||
username: ""
|
||||
password: ""
|
||||
token_id: ""
|
||||
token_secret: ""
|
||||
node: ""
|
||||
storage: ""
|
||||
datacenter: ""
|
||||
@@ -64,6 +58,8 @@ system_defaults:
|
||||
version: ""
|
||||
filesystem: "ext4"
|
||||
name: ""
|
||||
# consumed by the golden produce/deploy wrappers, not the bootstrap itself
|
||||
source: ""
|
||||
id: ""
|
||||
cpus: 0
|
||||
memory: 0 # MiB
|
||||
@@ -82,7 +78,22 @@ system_defaults:
|
||||
timezone: "Europe/Vienna"
|
||||
locale: "en_US.UTF-8"
|
||||
keymap: "us"
|
||||
mirror: ""
|
||||
# source: dvd|mirror|satellite|none ('' -> family default: EL=dvd, else mirror).
|
||||
# satellite values come from inventory/vault only, never committed code.
|
||||
content:
|
||||
source: ""
|
||||
url: ""
|
||||
proxy: ""
|
||||
gpgcheck: true
|
||||
satellite:
|
||||
host: ""
|
||||
ip: "" # optional /etc/hosts entry when DNS does not resolve host
|
||||
org: ""
|
||||
activation_key: ""
|
||||
ca_url: ""
|
||||
service_level: ""
|
||||
environment: ""
|
||||
install: false
|
||||
packages: []
|
||||
disks: []
|
||||
users: {}
|
||||
@@ -106,16 +117,19 @@ system_defaults:
|
||||
iter: 4000
|
||||
bits: 512
|
||||
pbkdf: "argon2id"
|
||||
urandom: true
|
||||
verify: true
|
||||
features:
|
||||
# On only for the clone-deploy golden path; off keeps ansible-direct + smaller image.
|
||||
cloud_init: false
|
||||
cis:
|
||||
enabled: false
|
||||
profile: default # default|l1|l2 (default = current house behaviour)
|
||||
rules: {} # per-rule overrides, e.g. {usb_lockdown: false}
|
||||
params: {} # parameter overrides, e.g. {pwquality_minlen: 16}
|
||||
selinux:
|
||||
enabled: true
|
||||
firewall:
|
||||
enabled: true
|
||||
backend: "firewalld" # firewalld|ufw
|
||||
backend: "" # '' -> family default (EL/arch=firewalld, debian/ubuntu=ufw); override: firewalld|ufw
|
||||
toolkit: "nftables" # nftables|iptables
|
||||
ssh:
|
||||
enabled: true
|
||||
@@ -126,30 +140,52 @@ system_defaults:
|
||||
banner:
|
||||
motd: false
|
||||
sudo: true
|
||||
rhel_repo:
|
||||
source: "iso" # iso|satellite|none — how RHEL systems get packages post-install
|
||||
url: "" # Satellite/custom repo URL when source=satellite
|
||||
aur:
|
||||
enabled: false
|
||||
helper: "yay" # yay|paru
|
||||
user: "_aur_builder"
|
||||
chroot:
|
||||
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn
|
||||
initramfs:
|
||||
generator: "" # auto-detected; override: dracut|mkinitcpio|initramfs-tools
|
||||
desktop:
|
||||
enabled: false
|
||||
environment: "" # gnome|kde|xfce|sway|hyprland|cinnamon|mate|lxqt|budgie
|
||||
display_manager: "" # auto from environment when empty; override: gdm|sddm|lightdm|greetd
|
||||
environment: "" # gnome|kde|sway|hyprland
|
||||
display_manager: "" # auto from environment when empty; override: gdm|sddm|greetd|plasma-login-manager|ly
|
||||
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" # auto|true|false (auto = install when detected)
|
||||
camera: "auto" # v4l-utils when a UVC/IPU6 camera is detected
|
||||
audio: "auto" # SOF firmware + ALSA UCM when an audio device is present
|
||||
bluetooth: "auto" # bluez when a Bluetooth controller is present
|
||||
displaylink: false
|
||||
hardware:
|
||||
profile: {} # full override: non-empty SKIPS detection (golden image)
|
||||
# The keys below MERGE over detection: lists union, booleans OR, packages
|
||||
# and kernel_params append, disable[] force-off applied last.
|
||||
cpu: "" # pin a CPU vendor (intel|amd); empty = use detection
|
||||
gpus: [] # extra GPU vendor codes to force
|
||||
wireless: [] # extra wireless vendor codes to force
|
||||
audio: [] # extra audio vendor codes to force
|
||||
camera: {} # {uvc: true, ipu6: true} to force a camera kind
|
||||
fingerprint: false # force-on a fingerprint reader detection missed
|
||||
bluetooth: false # force-on a Bluetooth controller detection missed
|
||||
packages: {} # per-os_family extra packages, e.g. {Archlinux: [intel-ipu6-dkms]}
|
||||
disable: [] # feature/vendor names to force-off (audio|bluetooth|camera|fingerprint|displaylink|<vendor>)
|
||||
kernel_params: [] # extra kernel cmdline params (quirks), e.g. ["i915.enable_psr=0"]
|
||||
|
||||
# Per-hypervisor required fields — drives data-driven validation.
|
||||
# All virtual types additionally require network bridge or interfaces.
|
||||
# Drives data-driven validation. Virtual types also require a network bridge or interfaces.
|
||||
hypervisor_required_fields:
|
||||
proxmox:
|
||||
hypervisor: [url, username, password, node, storage]
|
||||
hypervisor: [url, username, node, storage]
|
||||
system: [id]
|
||||
vmware:
|
||||
hypervisor: [url, username, password, datacenter, storage]
|
||||
@@ -161,15 +197,20 @@ hypervisor_required_fields:
|
||||
hypervisor: []
|
||||
system: []
|
||||
|
||||
# Hypervisor-to-disk device prefix mapping for virtual machines.
|
||||
# Physical installs must set system.disks[].device explicitly.
|
||||
# Used when content.url is empty.
|
||||
content_mirror_defaults:
|
||||
debian: "https://deb.debian.org/debian/"
|
||||
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
||||
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
|
||||
|
||||
# Virtual-only; physical installs must set system.disks[].device explicitly.
|
||||
hypervisor_disk_device_map:
|
||||
libvirt: "/dev/vd"
|
||||
xen: "/dev/xvd"
|
||||
proxmox: "/dev/sd"
|
||||
vmware: "/dev/sd"
|
||||
|
||||
# Mountpoints managed by the partitioning role — forbidden for extra disks.
|
||||
# Mountpoints managed by the partitioning role - forbidden for extra disks.
|
||||
reserved_mounts:
|
||||
- /boot
|
||||
- /boot/efi
|
||||
|
||||
25
roles/global_defaults/tasks/_apply_family_defaults.yml
Normal file
25
roles/global_defaults/tasks/_apply_family_defaults.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# Shared by both the fresh-run path (_normalize_system.yml) and the pre-computed
|
||||
# enrichment path (system.yml) so the family-default rules live in one place.
|
||||
- name: Apply family defaults to system_cfg
|
||||
vars:
|
||||
_os: "{{ system_cfg.os | default('') | string | lower }}"
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: >-
|
||||
{{
|
||||
system_cfg | combine({
|
||||
'content': {
|
||||
'source': system_cfg.content.source
|
||||
if (system_cfg.content.source | default('') | string | trim | length > 0)
|
||||
else ('dvd' if _os == 'rhel' else 'mirror'),
|
||||
'url': system_cfg.content.url
|
||||
if (system_cfg.content.url | default('') | string | trim | length > 0)
|
||||
else (content_mirror_defaults[_os] | default('')),
|
||||
},
|
||||
'features': {'firewall': {'backend':
|
||||
system_cfg.features.firewall.backend
|
||||
if (system_cfg.features.firewall.backend | default('') | string | trim | length > 0)
|
||||
else ('ufw' if _os in os_family_debian else 'firewalld')
|
||||
}},
|
||||
}, recursive=True)
|
||||
}}
|
||||
@@ -10,39 +10,49 @@
|
||||
if (system_raw.name | default('') | string | trim | length) > 0
|
||||
else inventory_hostname
|
||||
}}
|
||||
_mirror_defaults:
|
||||
debian: "https://deb.debian.org/debian/"
|
||||
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
||||
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg:
|
||||
# --- Identity & platform ---
|
||||
type: "{{ system_type }}"
|
||||
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
|
||||
version: "{{ system_raw.version | default('') | string }}"
|
||||
filesystem: "{{ system_raw.filesystem | default('') | string | lower }}"
|
||||
name: "{{ system_name }}"
|
||||
id: "{{ system_raw.id | default('') | string }}"
|
||||
# --- VM sizing (ignored for physical) ---
|
||||
cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}"
|
||||
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
|
||||
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
|
||||
# --- Network ---
|
||||
# Flat fields (bridge, ip, etc.) and interfaces[] are mutually exclusive.
|
||||
# When interfaces[] is set, flat fields are populated from the first
|
||||
# interface in the "Populate primary network fields" task below.
|
||||
# When only flat fields are set, a synthetic interfaces[] entry is built.
|
||||
# Flat fields and interfaces[] describe the same primary NIC: each is
|
||||
# backfilled from the other so consumers reading either form still work.
|
||||
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 }}"
|
||||
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: >-
|
||||
{{
|
||||
(system_raw.network.prefix | int | string)
|
||||
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:
|
||||
servers: "{{ system_raw.network.dns.servers | default([]) }}"
|
||||
search: "{{ system_raw.network.dns.search | default([]) }}"
|
||||
@@ -67,16 +77,24 @@
|
||||
else []
|
||||
)
|
||||
}}
|
||||
# --- Locale & environment ---
|
||||
timezone: "{{ system_raw.timezone | string }}"
|
||||
locale: "{{ system_raw.locale | string }}"
|
||||
keymap: "{{ system_raw.keymap | string }}"
|
||||
mirror: >-
|
||||
{{
|
||||
system_raw.mirror | string | trim
|
||||
if (system_raw.mirror | default('') | string | trim | length) > 0
|
||||
else _mirror_defaults[system_raw.os | default('') | string | lower] | default('')
|
||||
}}
|
||||
content:
|
||||
# Family defaults for empty source/url are applied by _apply_family_defaults.yml.
|
||||
source: "{{ system_raw.content.source | default('') | string | lower | trim }}"
|
||||
url: "{{ system_raw.content.url | default('') | string | trim }}"
|
||||
proxy: "{{ system_raw.content.proxy | default('') | string | trim }}"
|
||||
gpgcheck: "{{ system_raw.content.gpgcheck | default(true) | bool }}"
|
||||
satellite:
|
||||
host: "{{ system_raw.content.satellite.host | default('') | string | trim }}"
|
||||
ip: "{{ system_raw.content.satellite.ip | default('') | string | trim }}"
|
||||
org: "{{ system_raw.content.satellite.org | default('') | string }}"
|
||||
activation_key: "{{ system_raw.content.satellite.activation_key | default('') | string }}"
|
||||
ca_url: "{{ system_raw.content.satellite.ca_url | default('') | string | trim }}"
|
||||
service_level: "{{ system_raw.content.satellite.service_level | default('') | string }}"
|
||||
environment: "{{ system_raw.content.satellite.environment | default('') | string }}"
|
||||
install: "{{ system_raw.content.satellite.install | default(false) | bool }}"
|
||||
path: >-
|
||||
{{
|
||||
(system_raw.path | default('') | string)
|
||||
@@ -94,13 +112,11 @@
|
||||
| reject('equalto', '')
|
||||
| list
|
||||
}}
|
||||
# --- Storage & accounts ---
|
||||
disks: "{{ system_raw.disks | default([]) }}"
|
||||
users: "{{ system_raw.users | default({}) }}"
|
||||
root:
|
||||
password: "{{ system_raw.root.password | string }}"
|
||||
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
|
||||
# --- LUKS disk encryption ---
|
||||
luks:
|
||||
enabled: "{{ system_raw.luks.enabled | bool }}"
|
||||
passphrase: "{{ system_raw.luks.passphrase | string }}"
|
||||
@@ -118,17 +134,19 @@
|
||||
iter: "{{ system_raw.luks.iter | int }}"
|
||||
bits: "{{ system_raw.luks.bits | int }}"
|
||||
pbkdf: "{{ system_raw.luks.pbkdf | string }}"
|
||||
urandom: "{{ system_raw.luks.urandom | bool }}"
|
||||
verify: "{{ system_raw.luks.verify | bool }}"
|
||||
# --- Feature flags ---
|
||||
features:
|
||||
cloud_init: "{{ system_raw.features.cloud_init | default(false) | bool }}"
|
||||
cis:
|
||||
enabled: "{{ system_raw.features.cis.enabled | bool }}"
|
||||
profile: "{{ system_raw.features.cis.profile | default('default') | string }}"
|
||||
rules: "{{ system_raw.features.cis.rules | default({}) }}"
|
||||
params: "{{ system_raw.features.cis.params | default({}) }}"
|
||||
selinux:
|
||||
enabled: "{{ system_raw.features.selinux.enabled | bool }}"
|
||||
firewall:
|
||||
enabled: "{{ system_raw.features.firewall.enabled | bool }}"
|
||||
backend: "{{ system_raw.features.firewall.backend | string | lower }}"
|
||||
# Empty backend is family-resolved by _apply_family_defaults.yml.
|
||||
backend: "{{ system_raw.features.firewall.backend | default('') | string | lower | trim }}"
|
||||
toolkit: "{{ system_raw.features.firewall.toolkit | string | lower }}"
|
||||
ssh:
|
||||
enabled: "{{ system_raw.features.ssh.enabled | bool }}"
|
||||
@@ -139,9 +157,6 @@
|
||||
banner:
|
||||
motd: "{{ system_raw.features.banner.motd | bool }}"
|
||||
sudo: "{{ system_raw.features.banner.sudo | bool }}"
|
||||
rhel_repo:
|
||||
source: "{{ system_raw.features.rhel_repo.source | default('iso') | string | lower }}"
|
||||
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
|
||||
chroot:
|
||||
tool: "{{ system_raw.features.chroot.tool | string }}"
|
||||
initramfs:
|
||||
@@ -150,9 +165,82 @@
|
||||
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)
|
||||
}}
|
||||
# Kept tri-state ('auto'|'true'|'false'): 'auto' resolves at install time from detection.
|
||||
fingerprint: >-
|
||||
{{
|
||||
'auto'
|
||||
if (system_raw.features.peripherals.fingerprint | string | lower) == 'auto'
|
||||
else (system_raw.features.peripherals.fingerprint | bool | string | lower)
|
||||
}}
|
||||
camera: >-
|
||||
{{
|
||||
'auto'
|
||||
if (system_raw.features.peripherals.camera | string | lower) == 'auto'
|
||||
else (system_raw.features.peripherals.camera | bool | string | lower)
|
||||
}}
|
||||
audio: >-
|
||||
{{
|
||||
'auto'
|
||||
if (system_raw.features.peripherals.audio | string | lower) == 'auto'
|
||||
else (system_raw.features.peripherals.audio | bool | string | lower)
|
||||
}}
|
||||
bluetooth: >-
|
||||
{{
|
||||
'auto'
|
||||
if (system_raw.features.peripherals.bluetooth | string | lower) == 'auto'
|
||||
else (system_raw.features.peripherals.bluetooth | bool | string | lower)
|
||||
}}
|
||||
displaylink: "{{ system_raw.features.peripherals.displaylink | bool }}"
|
||||
hardware:
|
||||
profile: "{{ system_raw.features.hardware.profile | default({}) }}"
|
||||
cpu: "{{ system_raw.features.hardware.cpu | default('') | string }}"
|
||||
gpus: "{{ system_raw.features.hardware.gpus | default([]) | list }}"
|
||||
wireless: "{{ system_raw.features.hardware.wireless | default([]) | list }}"
|
||||
audio: "{{ system_raw.features.hardware.audio | default([]) | list }}"
|
||||
camera: "{{ system_raw.features.hardware.camera | default({}) }}"
|
||||
fingerprint: "{{ system_raw.features.hardware.fingerprint | default(false) | bool }}"
|
||||
bluetooth: "{{ system_raw.features.hardware.bluetooth | default(false) | bool }}"
|
||||
packages: "{{ system_raw.features.hardware.packages | default({}) }}"
|
||||
disable: "{{ system_raw.features.hardware.disable | default([]) | list }}"
|
||||
kernel_params: "{{ system_raw.features.hardware.kernel_params | default([]) | list }}"
|
||||
hostname: "{{ system_name }}"
|
||||
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
|
||||
os_version: "{{ system_raw.version | default('') | string }}"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
label: "system.features.{{ item }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (system.features[item] | default({})) is mapping
|
||||
- (system_defaults.features[item] is not mapping) or ((system.features[item] | default({})) is mapping)
|
||||
fail_msg: "system.features.{{ item }} must be a dictionary."
|
||||
quiet: true
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
# Centralized normalization — all input dicts (system, hypervisor, disks)
|
||||
# are normalized here into system_cfg, hypervisor_cfg, etc.
|
||||
# Downstream roles consume these computed facts directly and do NOT need
|
||||
# per-role _normalize.yml (except CIS, which has its own input dict).
|
||||
# Normalizes all input dicts into system_cfg/hypervisor_cfg/etc. here, so downstream
|
||||
# roles consume the computed facts directly with no per-role _normalize (except CIS).
|
||||
- name: Global defaults loaded
|
||||
ansible.builtin.debug:
|
||||
msg: Global defaults loaded.
|
||||
@@ -27,11 +25,15 @@
|
||||
_proxmox_auth:
|
||||
api_host: "{{ hypervisor_cfg.url }}"
|
||||
api_user: "{{ hypervisor_cfg.username }}"
|
||||
api_password: "{{ hypervisor_cfg.password }}"
|
||||
api_password: "{{ hypervisor_cfg.password | default(omit, true) }}"
|
||||
api_token_id: "{{ hypervisor_cfg.token_id | default(omit, true) }}"
|
||||
api_token_secret: "{{ hypervisor_cfg.token_secret | default(omit, true) }}"
|
||||
_proxmox_auth_node:
|
||||
api_host: "{{ hypervisor_cfg.url }}"
|
||||
api_user: "{{ hypervisor_cfg.username }}"
|
||||
api_password: "{{ hypervisor_cfg.password }}"
|
||||
api_password: "{{ hypervisor_cfg.password | default(omit, true) }}"
|
||||
api_token_id: "{{ hypervisor_cfg.token_id | default(omit, true) }}"
|
||||
api_token_secret: "{{ hypervisor_cfg.token_secret | default(omit, true) }}"
|
||||
node: "{{ hypervisor_cfg.node }}"
|
||||
no_log: true
|
||||
|
||||
@@ -61,6 +63,12 @@
|
||||
ansible.builtin.set_fact:
|
||||
os_version_major: "{{ (os_version | string).split('.')[0] }}"
|
||||
|
||||
# EL>=10 and Fedora dropped the static /etc/pam.d/system-auth shipped by pam;
|
||||
# the PAM stack is generated by authselect and absent until a profile is selected.
|
||||
- name: Flag authselect-managed PAM stacks
|
||||
ansible.builtin.set_fact:
|
||||
is_authselect: "{{ is_rhel | bool and (os_version_major | default('0') | int) >= 10 }}"
|
||||
|
||||
- name: Set chroot command wrapper
|
||||
ansible.builtin.set_fact:
|
||||
chroot_command: >-
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
---
|
||||
# Two code paths:
|
||||
# 1. Fresh run (system_cfg undefined): normalize from raw `system` input.
|
||||
# 2. Pre-computed (system_cfg already set, e.g. from main project's deploy_iac):
|
||||
# merge with bootstrap system_defaults to fill missing fields (luks, features,
|
||||
# etc.) that bootstrap expects but the main project doesn't set, then derive
|
||||
# convenience facts (hostname, os, os_version).
|
||||
# Fresh run normalizes raw `system` input. A pre-computed system_cfg (from the main
|
||||
# project's deploy_iac) is instead merged with system_defaults to fill the fields
|
||||
# bootstrap expects, then convenience facts are derived.
|
||||
- name: Normalize system and disk configuration
|
||||
when: system_cfg is not defined
|
||||
block:
|
||||
@@ -50,25 +47,6 @@
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: "{{ system_defaults | combine(system | default({}), recursive=True) | combine(system_cfg, recursive=True) }}"
|
||||
|
||||
- name: Apply mirror default for pre-computed system_cfg
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- _bootstrap_needs_enrichment | default(false) | bool
|
||||
- system_cfg.mirror | default('') | string | trim | length == 0
|
||||
vars:
|
||||
# Same as _normalize_system.yml — kept in sync manually.
|
||||
_mirror_defaults:
|
||||
debian: "https://deb.debian.org/debian/"
|
||||
ubuntu: "http://archive.ubuntu.com/ubuntu/"
|
||||
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: >-
|
||||
{{
|
||||
system_cfg | combine({
|
||||
'mirror': _mirror_defaults[system_cfg.os | default('') | string | lower] | default('')
|
||||
}, recursive=True)
|
||||
}}
|
||||
|
||||
- name: Populate primary network fields from first interface (pre-computed)
|
||||
when:
|
||||
- system_cfg is defined
|
||||
@@ -105,3 +83,8 @@
|
||||
- system_cfg is defined
|
||||
- install_drive is not defined
|
||||
ansible.builtin.include_tasks: _normalize_disks.yml
|
||||
|
||||
# Runs on every path before validation, so an empty firewall.backend / content.source
|
||||
# resolves to the family default even when system_cfg arrived pre-computed.
|
||||
- name: Apply family defaults (content source, firewall backend)
|
||||
ansible.builtin.include_tasks: _apply_family_defaults.yml
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
quiet: true
|
||||
|
||||
- name: Validate system.features leaf schemas
|
||||
loop: "{{ system_defaults.features | dict2items }}"
|
||||
loop: "{{ system_defaults.features | dict2items | selectattr('value', 'mapping') }}"
|
||||
loop_control:
|
||||
label: "system.features.{{ item.key }}"
|
||||
vars:
|
||||
@@ -121,18 +121,18 @@
|
||||
- >-
|
||||
os_version is not defined or (os_version | string | length) == 0
|
||||
or (
|
||||
os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"]
|
||||
os == "debian" and (os_version | string) in ["12", "13", "unstable"]
|
||||
) or (
|
||||
os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 43
|
||||
os == "fedora" and (os_version | int) >= 43 and (os_version | int) <= 44
|
||||
) or (
|
||||
os in ["rocky", "almalinux"]
|
||||
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
|
||||
and (os_version | string) is match("^(9|10)(\\.\\d+)?$")
|
||||
) or (
|
||||
os == "rhel"
|
||||
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
|
||||
and (os_version | string) is match("^(9|10)(\\.\\d+)?$")
|
||||
) or (
|
||||
os == "ubuntu"
|
||||
and (os_version | string) is match("^(2[0-9])\\.04$")
|
||||
and (os_version | string) is match("^(2[0-9])\\.(04|10)$")
|
||||
) or (
|
||||
os == "ubuntu-lts"
|
||||
and (os_version | string) is match("^(2[0-9])\\.04$")
|
||||
@@ -140,7 +140,7 @@
|
||||
os in ["ubuntu", "ubuntu-lts"]
|
||||
and (os_version | default('') | string | length) == 0
|
||||
) or (
|
||||
os in ["alpine", "archlinux", "opensuse", "void"]
|
||||
os == "archlinux"
|
||||
)
|
||||
fail_msg: "Invalid os/version specified. Please check README.md for supported values."
|
||||
quiet: true
|
||||
@@ -148,8 +148,8 @@
|
||||
- name: Validate RHEL ISO requirement
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- os != "rhel" or (rhel_iso is defined and (rhel_iso | string | length) > 0)
|
||||
fail_msg: "rhel_iso is required when os=rhel."
|
||||
- os != "rhel" or system_cfg.content.source == "mirror" or (rhel_iso is defined and (rhel_iso | string | length) > 0)
|
||||
fail_msg: "rhel_iso is required when os=rhel unless content.source is mirror."
|
||||
quiet: true
|
||||
|
||||
- name: Validate hypervisor-specific required fields
|
||||
@@ -166,6 +166,21 @@
|
||||
label: "hypervisor.{{ item }}"
|
||||
no_log: true
|
||||
|
||||
- name: Validate Proxmox authentication (password or API token)
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
- hypervisor_type == "proxmox"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(hypervisor_cfg.password | default('') | string | length > 0)
|
||||
or (hypervisor_cfg.token_id | default('') | string | length > 0
|
||||
and hypervisor_cfg.token_secret | default('') | string | length > 0)
|
||||
fail_msg: >-
|
||||
Proxmox requires either hypervisor.password or
|
||||
hypervisor.token_id + hypervisor.token_secret (API token).
|
||||
quiet: true
|
||||
|
||||
- name: Validate VMware placement (cluster or node required, mutually exclusive)
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
@@ -232,6 +247,83 @@
|
||||
fail_msg: Invalid feature flags were specified, please check your inventory/vars.
|
||||
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.camera in ["auto", "true", "false"]
|
||||
- system_cfg.features.peripherals.audio in ["auto", "true", "false"]
|
||||
- system_cfg.features.peripherals.bluetooth in ["auto", "true", "false"]
|
||||
- system_cfg.features.peripherals.displaylink is defined
|
||||
- system_cfg.features.hardware.profile is mapping
|
||||
- system_cfg.features.hardware.packages is mapping
|
||||
- system_cfg.features.hardware.camera is mapping
|
||||
- system_cfg.features.hardware.disable is sequence
|
||||
- system_cfg.features.hardware.kernel_params is sequence
|
||||
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/camera/audio/
|
||||
bluetooth 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", "plasma-login-manager", "ly"]
|
||||
- >-
|
||||
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", "plasma-login-manager"]
|
||||
- >-
|
||||
system_cfg.features.desktop.display_manager | default('') != "plasma-login-manager"
|
||||
or os == "archlinux" or (os == "fedora" and (os_version | int) >= 44)
|
||||
- >-
|
||||
system_cfg.features.desktop.display_manager | default('') != "ly"
|
||||
or os == "archlinux"
|
||||
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->plasma-login-manager on Arch/Fedora44+ else sddm,
|
||||
sway/hyprland->greetd. ly is an explicit override on Arch only and may
|
||||
front any environment. 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
|
||||
when: system_cfg.type == "virtual"
|
||||
ansible.builtin.assert:
|
||||
@@ -242,7 +334,7 @@
|
||||
- (system_cfg.disks[0].size | float) > 0
|
||||
- (system_cfg.disks[0].size | float) >= 20
|
||||
# Btrfs minimum disk: swap_size + 5.5 GiB overhead (subvolumes + metadata).
|
||||
# Swap sizing: memory < 16 GiB → max(memory_GiB, 2); memory >= 16 GiB → memory/2.
|
||||
# Swap sizing: memory < 16 GiB -> max(memory_GiB, 2); memory >= 16 GiB -> memory/2.
|
||||
- >-
|
||||
system_cfg.filesystem != "btrfs"
|
||||
or (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user