feat(hardware): auto-detect audio, bluetooth, camera with declarative override
This commit is contained in:
@@ -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 '' }}
|
||||
|
||||
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 }}"
|
||||
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) }}"
|
||||
Reference in New Issue
Block a user