diff --git a/README.md b/README.md index 829a10e..2c4ce77 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via ` | `desktop.*` | dict | see below | Desktop environment settings (see [4.2.5](#425-systemfeaturesdesktop)) | | `firmware.*` | dict | see below | Vendor firmware blobs and CPU microcode (see [4.2.6](#426-systemfeaturesfirmware)) | | `gpu.*` | dict | see below | Mesa/Vulkan and per-vendor GPU userspace (see [4.2.7](#427-systemfeaturesgpu)) | -| `peripherals.*` | dict | see below | Fingerprint readers, webcams, DisplayLink (see [4.2.8](#428-systemfeaturesperipherals)) | +| `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. @@ -359,13 +359,20 @@ automatically when needed. Debian uses `nvidia-driver` from the `non-free` compo 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 | -| `webcam` | bool \| `auto` | `auto` | `v4l-utils` and userspace tooling. `auto` follows `enabled` | +| `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, @@ -381,7 +388,8 @@ must still be installed manually from DisplayLink's site after first boot. Arch | Key | Type | Default | Description | | --------- | ---- | ------- | -------------------------------------------------------------------- | -| `profile` | dict | `{}` | Hardware-detection override; empty means autodetect from live host | +| `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 @@ -402,6 +410,38 @@ system: 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 | Key | Type | Default | Description | diff --git a/roles/bootstrap/tasks/_hardware.yml b/roles/bootstrap/tasks/_hardware.yml index 7f49814..ff5a022 100644 --- a/roles/bootstrap/tasks/_hardware.yml +++ b/roles/bootstrap/tasks/_hardware.yml @@ -1,6 +1,4 @@ --- -# Installs vendor-matched microcode/firmware/GPU/peripheral packages into /mnt -# based on hardware_profile_active set by environment/_detect_hardware.yml. - name: Load hardware package definitions ansible.builtin.include_vars: file: hardware.yml @@ -22,7 +20,7 @@ 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([])) }}" + _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([]) }}" @@ -58,101 +56,7 @@ changed_when: _rpmfusion_result.rc == 0 - name: Resolve hardware package set - vars: - _family: "{{ bootstrap_hardware_packages[os_family] }}" - _cpu: "{{ hardware_profile_active.cpu | default('') | string }}" - _gpus: "{{ hardware_profile_active.gpus | default([]) | list }}" - _wifi: "{{ hardware_profile_active.wireless | default([]) | list }}" - _fp_detected: "{{ hardware_profile_active.fingerprint | default(false) | bool }}" - _firmware_on: "{{ system_cfg.features.firmware.enabled | bool }}" - _microcode_on: "{{ _firmware_on and (system_cfg.features.firmware.microcode | bool) }}" - _gpu_on: "{{ system_cfg.features.gpu.enabled | bool }}" - _peripherals_on: "{{ system_cfg.features.peripherals.enabled | bool }}" - _webcam_pref: "{{ system_cfg.features.peripherals.webcam | default('auto') }}" - _fp_pref: "{{ system_cfg.features.peripherals.fingerprint | default('auto') }}" - _dl_on: "{{ system_cfg.features.peripherals.displaylink | bool }}" - _webcam_on: >- - {{ - _peripherals_on - and (_webcam_pref == 'true' or (_webcam_pref == 'auto' and _peripherals_on)) - }} - _fp_on: >- - {{ - _peripherals_on - and (_fp_pref == 'true' or (_fp_pref == 'auto' and _fp_detected)) - }} - # Union of GPU/wireless/CPU vendors; CPU vendor is included so Intel-CPU - # systems pull i915/iwlwifi firmware via the same vendor split. - _cpu_vendor_list: "{{ ([_cpu] if (_cpu | length > 0) else []) | list }}" - _firmware_vendors: >- - {{ - (_firmware_on | ternary( - (_gpus + _wifi + _cpu_vendor_list) - | reject('equalto', '') | unique | list, - [] - )) - }} - _microcode_pkgs: >- - {{ - ((_microcode_on and _cpu | length > 0) | ternary( - _family.cpu_microcode[_cpu] | default([]), - [] - )) | list - }} - _firmware_pkgs: >- - {{ - (_firmware_on | ternary( - (_family.firmware_base | default([]) | list) - + (_firmware_vendors - | map('extract', _family.firmware | default({})) - | select('truthy') - | list - | sum(start=[])), - [] - )) | list - }} - _gpu_base_pkgs: "{{ (_gpu_on | ternary(_family.gpu_base | default([]), [])) | list }}" - _gpu_vendor_pkgs: >- - {{ - (_gpu_on | ternary( - (_gpus | reject('equalto', 'nvidia') | list) - | map('extract', _family.gpu | default({})) - | select('truthy') - | list - | sum(start=[]), - [] - )) | list - }} - _gpu_nvidia_pkgs: >- - {{ - ((_gpu_on and ('nvidia' in _gpus)) | ternary( - _family.gpu_nvidia[_nvidia_driver_resolved] | default([]), - [] - )) | list - }} - _peripherals_base_pkgs: >- - {{ - (_webcam_on | ternary(_family.peripherals_base | default([]), [])) | list - }} - _peripherals_fingerprint_pkgs: >- - {{ - (_fp_on | ternary(_family.peripherals_fingerprint | default([]), [])) | list - }} - _peripherals_displaylink_pkgs: >- - {{ - (_dl_on | ternary(_family.peripherals_displaylink | default([]), [])) | list - }} - ansible.builtin.set_fact: - _hardware_packages: >- - {{ - (_microcode_pkgs + _firmware_pkgs - + _gpu_base_pkgs + _gpu_vendor_pkgs + _gpu_nvidia_pkgs - + _peripherals_base_pkgs + _peripherals_fingerprint_pkgs - + _peripherals_displaylink_pkgs) - | reject('equalto', '') - | unique - | list - }} + ansible.builtin.include_tasks: _resolve_hardware_packages.yml - name: Report hardware package selection ansible.builtin.debug: @@ -165,6 +69,15 @@ 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: diff --git a/roles/bootstrap/tasks/_resolve_hardware_packages.yml b/roles/bootstrap/tasks/_resolve_hardware_packages.yml new file mode 100644 index 0000000..b2d21ca --- /dev/null +++ b/roles/bootstrap/tasks/_resolve_hardware_packages.yml @@ -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 + }} diff --git a/roles/bootstrap/vars/hardware.yml b/roles/bootstrap/vars/hardware.yml index 42a22e5..9ef0ccf 100644 --- a/roles/bootstrap/vars/hardware.yml +++ b/roles/bootstrap/vars/hardware.yml @@ -1,18 +1,7 @@ --- -# Hardware-aware package definitions keyed by os_family. Schema: -# cpu_microcode[intel|amd] CPU vendor microcode -# firmware_base unconditional firmware packages -# firmware[] vendor-split firmware (intel|amd|nvidia| -# atheros|broadcom|mediatek|marvell|realtek| -# qcom|cirrus|other) -# gpu_base mesa + vulkan loader -# gpu[intel|amd] per-GPU userspace -# gpu_nvidia[open|proprietary|nouveau] nvidia driver flavor -# peripherals_base webcam/scanner stack -# peripherals_fingerprint fprintd + libfprint -# peripherals_displaylink evdi kernel module + DisplayLink helpers -# Only packages matching detected hardware are installed; families without -# vendor splits collapse to a single firmware meta package. +# 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: @@ -40,9 +29,11 @@ bootstrap_hardware_packages: proprietary: [nvidia-dkms, nvidia-utils] # Wayland-only: kernel nouveau module + mesa/gbm drive the display; no Xorg DDX. nouveau: [vulkan-nouveau] - peripherals_base: [v4l-utils] + 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: @@ -72,9 +63,11 @@ bootstrap_hardware_packages: proprietary: [nvidia-driver, nvidia-vulkan-icd] # Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg. nouveau: [] - peripherals_base: [v4l-utils] + 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: @@ -103,6 +96,8 @@ bootstrap_hardware_packages: proprietary: [akmod-nvidia, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda] # Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg. nouveau: [] - peripherals_base: [v4l-utils] + 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] diff --git a/roles/configuration/tasks/grub.yml b/roles/configuration/tasks/grub.yml index d596ea3..a8c73b9 100644 --- a/roles/configuration/tasks/grub.yml +++ b/roles/configuration/tasks/grub.yml @@ -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(' ') }} diff --git a/roles/environment/defaults/main.yml b/roles/environment/defaults/main.yml index 73b1511..97ad481 100644 --- a/roles/environment/defaults/main.yml +++ b/roles/environment/defaults/main.yml @@ -1,24 +1,21 @@ --- -# 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 -# Libraries the installer tools pull whose soname may have bumped past the ISO. -# Each library's installed reverse-dependencies are upgraded together with the -# tools so a current install onto an older ISO stays a consistent transaction -# instead of a partial upgrade. Extend if a future transition breaks the install. +# 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 IDs -> vendor codes used by hardware detection. -# Only vendors that drive distinct firmware/driver packages are mapped. +# PCI vendor ID -> vendor code. Only vendors that drive distinct +# firmware/driver packages are mapped. environment_pci_vendor_map: "8086": intel "1002": amd @@ -37,8 +34,8 @@ environment_pci_vendor_map: "1cf3": cirrus "13d7": cirrus -# USB vendor IDs of fingerprint readers supported by libfprint / fprintd. -# Lowercase, four-digit hex; matched against `lsusb` output. +# 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) @@ -49,3 +46,15 @@ environment_fingerprint_vendor_ids: - "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 diff --git a/roles/environment/tasks/_detect_hardware.yml b/roles/environment/tasks/_detect_hardware.yml index 80a9c7d..4293038 100644 --- a/roles/environment/tasks/_detect_hardware.yml +++ b/roles/environment/tasks/_detect_hardware.yml @@ -1,22 +1,6 @@ --- -# Hardware detection on the live installer host. -# -# Resolves system_cfg.features.hardware.profile when not explicitly set, so -# downstream bootstrap can install vendor-matched microcode/firmware/GPU/ -# peripheral packages. When the user supplies an override profile, detection -# is skipped (golden-image flow: bake an image with a fixed profile). -# -# Output fact: hardware_profile_active = { -# cpu: 'intel'|'amd'|'', -# gpus: list of 'intel'|'amd'|'nvidia', -# nvidia_supports_open: bool, # true when all detected Nvidia GPUs are -# # Turing or newer (device id >= 0x1e00) -# wireless: list of vendor codes ('intel'|'realtek'|'atheros'|...), -# fingerprint: bool, # USB fingerprint reader detected -# } -# -# Skipped entirely when neither firmware/gpu/peripherals features are enabled. - +# 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: >- @@ -37,7 +21,12 @@ 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: @@ -61,71 +50,7 @@ failed_when: false - name: Resolve detected hardware profile - vars: - _vendor_keys: "{{ environment_pci_vendor_map.keys() | list }}" - _cpu_vendor_raw: >- - {{ - _hardware_lscpu.stdout - | regex_search('(?im)^Vendor ID:\\s*(\\S+)', '\\1') - | default([''], true) - | first - }} - _cpu_vendor: >- - {{ - 'intel' if _cpu_vendor_raw == 'GenuineIntel' - else ('amd' if _cpu_vendor_raw == 'AuthenticAMD' else '') - }} - # PCI classes: 0300 = VGA, 0302 = 3D, 0280 = wireless network controller. - _gpu_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[(0300|0302)\\]:') | list }}" - _gpu_pairs: >- - {{ - _gpu_lines - | map('regex_search', '\\[([0-9a-f]{4}):([0-9a-f]{4})\\]', '\\1', '\\2') - | select('truthy') - | list - }} - _gpu_vendor_ids: "{{ _gpu_pairs | map('first') | select('in', _vendor_keys) | list }}" - _gpu_vendors: "{{ _gpu_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}" - _nvidia_device_ids: >- - {{ - _gpu_pairs - | selectattr('0', 'equalto', '10de') - | map(attribute=1) - | list - }} - _nvidia_min_id: >- - {{ - (_nvidia_device_ids | map('int', base=16) | list | min) - if _nvidia_device_ids | length > 0 else 0 - }} - # 0x1e00 = 7680 = first Turing device id; Turing+ supports nvidia-open. - _nvidia_supports_open: "{{ _nvidia_device_ids | length > 0 and (_nvidia_min_id | int) >= 7680 }}" - _wifi_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0280\\]:') | list }}" - _wifi_vendor_ids: >- - {{ - _wifi_lines - | map('regex_search', '\\[([0-9a-f]{4}):[0-9a-f]{4}\\]', '\\1') - | select('truthy') - | map('first') - | select('in', _vendor_keys) - | list - }} - _wifi_vendors: "{{ _wifi_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}" - _fingerprint_present: >- - {{ - (_hardware_lsusb.stdout | default('')) - | regex_search( - '(?i)ID (' ~ (environment_fingerprint_vendor_ids | join('|')) ~ '):' - ) - is not none - }} - ansible.builtin.set_fact: - hardware_profile_active: - cpu: "{{ _cpu_vendor }}" - gpus: "{{ _gpu_vendors }}" - nvidia_supports_open: "{{ _nvidia_supports_open | bool }}" - wireless: "{{ _wifi_vendors }}" - fingerprint: "{{ _fingerprint_present | bool }}" + ansible.builtin.include_tasks: _resolve_hardware_profile.yml - name: Initialize empty hardware profile when detection skipped when: not (_hardware_detection_needed | bool) @@ -135,7 +60,14 @@ 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 @@ -146,4 +78,7 @@ gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }} {{ '(open-supported)' if hardware_profile_active.nvidia_supports_open | bool else '(legacy)' }}, wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }}, - fingerprint={{ hardware_profile_active.fingerprint | default(false) }} + 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 '' }} diff --git a/roles/environment/tasks/_merge_hardware_profile.yml b/roles/environment/tasks/_merge_hardware_profile.yml new file mode 100644 index 0000000..dbd250b --- /dev/null +++ b/roles/environment/tasks/_merge_hardware_profile.yml @@ -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 }}" diff --git a/roles/environment/tasks/_resolve_hardware_profile.yml b/roles/environment/tasks/_resolve_hardware_profile.yml new file mode 100644 index 0000000..f20a836 --- /dev/null +++ b/roles/environment/tasks/_resolve_hardware_profile.yml @@ -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) }}" diff --git a/roles/global_defaults/defaults/main.yml b/roles/global_defaults/defaults/main.yml index ac673ca..d683057 100644 --- a/roles/global_defaults/defaults/main.yml +++ b/roles/global_defaults/defaults/main.yml @@ -143,11 +143,27 @@ system_defaults: nvidia_driver: "auto" # auto | open | proprietary | nouveau peripherals: enabled: "auto" # auto = follows desktop.enabled - fingerprint: "auto" - webcam: "auto" + 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: {} # empty = autodetect; set to override (golden image) + profile: {} # full override: non-empty SKIPS detection (golden image) + # Declarative hardware group: a per-device profile that MERGES over + # auto-detect (auto-detect = base; these supplement/override it). Vendor + # lists union with detection, booleans OR with detection, packages append, + # disable[] force-off (applied last), kernel_params append to the cmdline. + 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|) + 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. diff --git a/roles/global_defaults/tasks/_normalize_system.yml b/roles/global_defaults/tasks/_normalize_system.yml index d70f1ee..7d4e2c3 100644 --- a/roles/global_defaults/tasks/_normalize_system.yml +++ b/roles/global_defaults/tasks/_normalize_system.yml @@ -206,23 +206,45 @@ if (system_raw.features.peripherals.enabled | string | lower) == 'auto' else (system_raw.features.peripherals.enabled | bool) }} - # fingerprint/webcam stay tri-state ('auto'|'true'|'false') because the - # 'auto' branch is resolved at install time using detection results. + # fingerprint/camera/audio/bluetooth stay tri-state ('auto'|'true'|'false') + # because the 'auto' branch is resolved at install time using detection results. fingerprint: >- {{ 'auto' if (system_raw.features.peripherals.fingerprint | string | lower) == 'auto' else (system_raw.features.peripherals.fingerprint | bool | string | lower) }} - webcam: >- + camera: >- {{ 'auto' - if (system_raw.features.peripherals.webcam | string | lower) == 'auto' - else (system_raw.features.peripherals.webcam | bool | string | lower) + 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 }}" diff --git a/roles/global_defaults/tasks/validation.yml b/roles/global_defaults/tasks/validation.yml index c7f3fd5..6557520 100644 --- a/roles/global_defaults/tasks/validation.yml +++ b/roles/global_defaults/tasks/validation.yml @@ -241,15 +241,21 @@ - system_cfg.features.gpu.nvidia_driver in ["auto", "open", "proprietary", "nouveau"] - system_cfg.features.peripherals.enabled is defined - system_cfg.features.peripherals.fingerprint in ["auto", "true", "false"] - - system_cfg.features.peripherals.webcam in ["auto", "true", "false"] + - system_cfg.features.peripherals.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 and - peripherals.webcam in [auto|true|false]; hardware.profile must be a dict. + [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 diff --git a/tests/hardware/_assert_fixture.yml b/tests/hardware/_assert_fixture.yml new file mode 100644 index 0000000..d41c4e7 --- /dev/null +++ b/tests/hardware/_assert_fixture.yml @@ -0,0 +1,24 @@ +--- +- name: "Seed lscpu/lspci/lsusb registers ({{ fixture.name }})" + ansible.builtin.set_fact: + _hardware_lscpu: { stdout: "{{ fixture.lscpu }}" } + _hardware_lspci: { stdout: "{{ fixture.lspci | join('\n') }}", stdout_lines: "{{ fixture.lspci }}" } + _hardware_lsusb: { stdout: "{{ fixture.lsusb }}" } + +- name: "Resolve hardware profile ({{ fixture.name }})" + ansible.builtin.include_tasks: ../../roles/environment/tasks/_resolve_hardware_profile.yml + +- name: "Assert resolved profile ({{ fixture.name }})" + ansible.builtin.assert: + that: + - hardware_profile_active.cpu == fixture.expect.cpu + - hardware_profile_active.gpus == fixture.expect.gpus + - hardware_profile_active.wireless == fixture.expect.wireless + - hardware_profile_active.audio == fixture.expect.audio + - (hardware_profile_active.fingerprint | bool) == (fixture.expect.fingerprint | bool) + - (hardware_profile_active.bluetooth | bool) == (fixture.expect.bluetooth | bool) + - (hardware_profile_active.camera.uvc | bool) == (fixture.expect.camera.uvc | bool) + - (hardware_profile_active.camera.ipu6 | bool) == (fixture.expect.camera.ipu6 | bool) + - (hardware_profile_active.nvidia_supports_open | bool) == (fixture.expect.nvidia_supports_open | bool) + fail_msg: "[{{ fixture.name }}] FAIL got {{ hardware_profile_active }}" + success_msg: "[{{ fixture.name }}] OK {{ hardware_profile_active }}" diff --git a/tests/hardware/_assert_merge.yml b/tests/hardware/_assert_merge.yml new file mode 100644 index 0000000..aa9de34 --- /dev/null +++ b/tests/hardware/_assert_merge.yml @@ -0,0 +1,44 @@ +--- +- name: "Seed detection + declarative group (merge {{ mf.name }})" + ansible.builtin.set_fact: + hardware_profile_active: "{{ mf.detected }}" + os_family: "{{ mf.os_family }}" + _nvidia_driver_resolved: "{{ mf.nvidia_driver_resolved | default('nouveau') }}" + system_cfg: + features: + firmware: { enabled: true, microcode: true } + gpu: { enabled: true } + peripherals: + enabled: true + camera: "auto" + fingerprint: "auto" + audio: "auto" + bluetooth: "auto" + displaylink: false + hardware: "{{ mf.hardware }}" + +- name: "Merge group over detection (merge {{ mf.name }})" + ansible.builtin.include_tasks: ../../roles/environment/tasks/_merge_hardware_profile.yml + +- name: "Assert merged profile (merge {{ mf.name }})" + ansible.builtin.assert: + that: + - (hardware_profile_active.fingerprint | bool) == (mf.expect_profile.fingerprint | bool) + - (hardware_profile_active.bluetooth | bool) == (mf.expect_profile.bluetooth | bool) + - _hardware_profile_kernel_params == (mf.hardware.kernel_params | default([])) + fail_msg: "[merge {{ mf.name }}] profile FAIL {{ hardware_profile_active }}" + +- name: "Load package map (merge {{ mf.name }})" + ansible.builtin.include_vars: + file: ../../roles/bootstrap/vars/hardware.yml + +- name: "Resolve packages (merge {{ mf.name }})" + ansible.builtin.include_tasks: ../../roles/bootstrap/tasks/_resolve_hardware_packages.yml + +- name: "Assert resolved package list (merge {{ mf.name }})" + ansible.builtin.assert: + that: + - (mf.expect_contains | default([])) | difference(_hardware_packages) | length == 0 + - (mf.expect_excludes | default([])) | intersect(_hardware_packages) | length == 0 + fail_msg: "[merge {{ mf.name }}] FAIL got {{ _hardware_packages }}" + success_msg: "[merge {{ mf.name }}] OK {{ _hardware_packages }}" diff --git a/tests/hardware/_assert_packages.yml b/tests/hardware/_assert_packages.yml new file mode 100644 index 0000000..2c8450b --- /dev/null +++ b/tests/hardware/_assert_packages.yml @@ -0,0 +1,32 @@ +--- +- name: "Seed resolve inputs (pkg {{ pf.name }})" + ansible.builtin.set_fact: + os_family: "{{ pf.os_family }}" + hardware_profile_active: "{{ pf.profile }}" + _nvidia_driver_resolved: "{{ pf.nvidia_driver_resolved | default('nouveau') }}" + system_cfg: + features: + firmware: { enabled: true, microcode: true } + gpu: { enabled: true } + peripherals: + enabled: true + camera: "auto" + fingerprint: "auto" + audio: "auto" + bluetooth: "auto" + displaylink: false + +- name: "Load package map (pkg {{ pf.name }})" + ansible.builtin.include_vars: + file: ../../roles/bootstrap/vars/hardware.yml + +- name: "Resolve packages (pkg {{ pf.name }})" + ansible.builtin.include_tasks: ../../roles/bootstrap/tasks/_resolve_hardware_packages.yml + +- name: "Assert resolved package list (pkg {{ pf.name }})" + ansible.builtin.assert: + that: + - (pf.expect_contains | default([])) | difference(_hardware_packages) | length == 0 + - (pf.expect_excludes | default([])) | intersect(_hardware_packages) | length == 0 + fail_msg: "[pkg {{ pf.name }}] FAIL got {{ _hardware_packages }}" + success_msg: "[pkg {{ pf.name }}] OK {{ _hardware_packages }}" diff --git a/tests/hardware/fixtures.yml b/tests/hardware/fixtures.yml new file mode 100644 index 0000000..2956720 --- /dev/null +++ b/tests/hardware/fixtures.yml @@ -0,0 +1,41 @@ +--- +# Canned lscpu/lspci/lsusb -> expected profile. lspci lines are `lspci -nn` shaped, +# lsusb is plain `lsusb`. +hardware_fixtures: + - name: intel-laptop-nvidia-turing + lscpu: "Architecture: x86_64\nVendor ID: GenuineIntel\nModel name: 12th Gen Core i7" + lspci: + - "00:02.0 VGA compatible controller [0300]: Intel Corporation Alder Lake-P GT2 [8086:46a6] (rev 0c)" + - "01:00.0 3D controller [0302]: NVIDIA Corporation TU117M [GeForce GTX 1650 Mobile] [10de:1f99] (rev a1)" + - "00:14.3 Network controller [0280]: Intel Corporation Wi-Fi 6 AX201 [8086:a0f0] (rev 11)" + - "00:1f.3 Audio device [0403]: Intel Corporation Alder Lake PCH-P High Definition Audio [8086:51c8] (rev 01)" + lsusb: |- + Bus 001 Device 003: ID 06cb:00bd Synaptics, Inc. Prometheus MIS Touch Fingerprint Reader + Bus 001 Device 004: ID 8087:0026 Intel Corp. AX201 Bluetooth + Bus 001 Device 005: ID 5986:118d Acer, Inc. Integrated Camera + expect: { cpu: intel, gpus: [intel, nvidia], nvidia_supports_open: true, wireless: [intel], audio: [intel], fingerprint: true, bluetooth: true, camera: { uvc: true, ipu6: false } } + + - name: amd-desktop-realtek + lscpu: "Architecture: x86_64\nVendor ID: AuthenticAMD\nModel name: AMD Ryzen 7 5800X" + lspci: + - "0a:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 22 [1002:73df] (rev c7)" + - "05:00.0 Network controller [0280]: Realtek Semiconductor Co., Ltd. RTL8822CE 802.11ac [10ec:c822]" + - "0b:00.4 Audio device [0403]: Advanced Micro Devices, Inc. [AMD] Starship/Matisse HD Audio [1022:1487]" + lsusb: "Bus 002 Device 002: ID 046d:c52b Logitech, Inc. Unifying Receiver" + expect: { cpu: amd, gpus: [amd], nvidia_supports_open: false, wireless: [realtek], audio: [amd], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } } + + - name: nvidia-legacy-maxwell + lscpu: "Vendor ID: GenuineIntel" + lspci: + - "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GM107 [GeForce GTX 750 Ti] [10de:1380] (rev a2)" + lsusb: "" + expect: { cpu: intel, gpus: [nvidia], nvidia_supports_open: false, wireless: [], audio: [], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } } + + - name: intel-laptop-ipu6-camera + lscpu: "Vendor ID: GenuineIntel" + lspci: + - "00:02.0 VGA compatible controller [0300]: Intel Corporation Raptor Lake-P [8086:a7a0] (rev 04)" + - "00:05.0 Multimedia controller [0480]: Intel Corporation Raptor Lake IPU6 [8086:a75d] (rev 04)" + - "00:14.3 Network controller [0280]: Intel Corporation Wi-Fi 6E AX211 [8086:51f0]" + lsusb: "Bus 003 Device 002: ID 8087:0033 Intel Corp. AX211 Bluetooth" + expect: { cpu: intel, gpus: [intel], nvidia_supports_open: false, wireless: [intel], audio: [], fingerprint: false, bluetooth: true, camera: { uvc: false, ipu6: true } } diff --git a/tests/hardware/merge_fixtures.yml b/tests/hardware/merge_fixtures.yml new file mode 100644 index 0000000..5575b73 --- /dev/null +++ b/tests/hardware/merge_fixtures.yml @@ -0,0 +1,37 @@ +--- +# Detected profile + declarative hardware.* -> merged profile + resolved packages. +# Exercises union/OR/force-on, disable[] force-off, and packages[os_family] append. +merge_fixtures: + - name: force-fingerprint-disable-audio-pin-ipu6 + os_family: Archlinux + nvidia_driver_resolved: nouveau + detected: { cpu: intel, gpus: [intel], nvidia_supports_open: true, wireless: [intel], audio: [intel], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: true } } + hardware: + fingerprint: true # force-on (detection missed it) + packages: { Archlinux: [intel-ipu6-dkms, v4l2-relayd] } + disable: [audio] # force-off audio even though detected + kernel_params: ["i915.enable_psr=0"] + expect_profile: { fingerprint: true, bluetooth: false } + expect_contains: [intel-ipu6-dkms, v4l2-relayd, fprintd, libfprint, v4l-utils, intel-ucode] + expect_excludes: [sof-firmware, alsa-ucm-conf, bluez] + + - name: union-vendors-force-bluetooth + os_family: Debian + nvidia_driver_resolved: nouveau + detected: { cpu: amd, gpus: [amd], nvidia_supports_open: false, wireless: [], audio: [amd], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } } + hardware: + wireless: [realtek] # union: add a wireless vendor detection missed + bluetooth: true # force-on bluetooth + expect_profile: { fingerprint: false, bluetooth: true } + expect_contains: [amd64-microcode, firmware-realtek, firmware-sof-signed, bluez] + expect_excludes: [fprintd, v4l-utils] + + - name: disable-vendor-drops-microcode-firmware-gpu + os_family: Archlinux + nvidia_driver_resolved: open + detected: { cpu: intel, gpus: [intel, nvidia], nvidia_supports_open: true, wireless: [intel], audio: [intel], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } } + hardware: + disable: [nvidia, intel] # drop the nvidia GPU and every intel-vendor contribution + expect_profile: { fingerprint: false, bluetooth: false } + expect_contains: [mesa, vulkan-icd-loader] + expect_excludes: [nvidia-open-dkms, intel-ucode, vulkan-intel, sof-firmware, linux-firmware-other] diff --git a/tests/hardware/pkg_fixtures.yml b/tests/hardware/pkg_fixtures.yml new file mode 100644 index 0000000..6c373a7 --- /dev/null +++ b/tests/hardware/pkg_fixtures.yml @@ -0,0 +1,22 @@ +--- +# profile + os_family -> resolved package list. expect_contains: all must be present; +# expect_excludes: none may be present. Features default all-on with auto peripherals. +package_fixtures: + - name: arch-intel-audio-bt-fp + os_family: Archlinux + profile: { cpu: intel, gpus: [intel], wireless: [intel], audio: [intel], fingerprint: true, bluetooth: true, camera: { uvc: true, ipu6: false } } + expect_contains: [intel-ucode, linux-firmware-other, mesa, vulkan-icd-loader, vulkan-intel, sof-firmware, alsa-ucm-conf, bluez, bluez-utils, fprintd, libfprint, v4l-utils] + expect_excludes: [nvidia-open-dkms, evdi] + + - name: debian-amd-audio-no-bt-no-fp + os_family: Debian + profile: { cpu: amd, gpus: [amd], wireless: [realtek], audio: [amd], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } } + expect_contains: [amd64-microcode, firmware-linux-free, firmware-amd-graphics, firmware-realtek, mesa-vulkan-drivers, firmware-sof-signed, alsa-ucm-conf] + expect_excludes: [bluez, fprintd, libpam-fprintd, v4l-utils] + + - name: redhat-intel-nvidia-bt + os_family: RedHat + profile: { cpu: intel, gpus: [intel, nvidia], wireless: [intel], audio: [intel], fingerprint: false, bluetooth: true, camera: { uvc: false, ipu6: true } } + nvidia_driver_resolved: open + expect_contains: [microcode_ctl, linux-firmware, mesa-dri-drivers, vulkan-loader, akmod-nvidia-open, alsa-sof-firmware, alsa-ucm, bluez, v4l-utils] + expect_excludes: [fprintd, evdi] diff --git a/tests/hardware/test_detection.yml b/tests/hardware/test_detection.yml new file mode 100644 index 0000000..7d10f6a --- /dev/null +++ b/tests/hardware/test_detection.yml @@ -0,0 +1,16 @@ +--- +# Run: ansible-playbook tests/hardware/test_detection.yml +- name: Hardware detection fixture tests + hosts: localhost + gather_facts: false + connection: local + vars_files: + - ../../roles/environment/defaults/main.yml + - fixtures.yml + tasks: + - name: Run each detection fixture + ansible.builtin.include_tasks: _assert_fixture.yml + loop: "{{ hardware_fixtures }}" + loop_control: + loop_var: fixture + label: "{{ fixture.name }}" diff --git a/tests/hardware/test_merge.yml b/tests/hardware/test_merge.yml new file mode 100644 index 0000000..e3e73e5 --- /dev/null +++ b/tests/hardware/test_merge.yml @@ -0,0 +1,15 @@ +--- +# Run: ansible-playbook tests/hardware/test_merge.yml +- name: Hardware merge-layer fixture tests + hosts: localhost + gather_facts: false + connection: local + vars_files: + - merge_fixtures.yml + tasks: + - name: Run each merge fixture + ansible.builtin.include_tasks: _assert_merge.yml + loop: "{{ merge_fixtures }}" + loop_control: + loop_var: mf + label: "{{ mf.name }}" diff --git a/tests/hardware/test_packages.yml b/tests/hardware/test_packages.yml new file mode 100644 index 0000000..c347942 --- /dev/null +++ b/tests/hardware/test_packages.yml @@ -0,0 +1,15 @@ +--- +# Run: ansible-playbook tests/hardware/test_packages.yml +- name: Hardware package-resolution fixture tests + hosts: localhost + gather_facts: false + connection: local + vars_files: + - pkg_fixtures.yml + tasks: + - name: Run each package fixture + ansible.builtin.include_tasks: _assert_packages.yml + loop: "{{ package_fixtures }}" + loop_control: + loop_var: pf + label: "{{ pf.name }}"