mirror of https://github.com/containers/qm.git
Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
|
c9868e8507 | |
|
5938fa91fe | |
|
3c960da3cc | |
|
1e6111f2d4 | |
|
c84b01d598 | |
|
08fb1c1a66 | |
|
d3cd2a04e4 | |
|
6ad8cb5ff4 | |
|
da31efb4d8 | |
|
595be738b9 | |
|
257e73d65d | |
|
8f137784f2 | |
|
f0b8127906 | |
|
fab260d36c | |
|
c063b36fbb | |
|
034378e7c8 | |
|
9f2801ec0f |
|
@ -0,0 +1,47 @@
|
|||
name: OCI Hooks Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'oci-hooks/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'oci-hooks/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: python:3.11-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y jq
|
||||
pip install tox
|
||||
# Install shfmt for shell script formatting
|
||||
go install mvdan.cc/sh/v3/cmd/shfmt@latest
|
||||
|
||||
- name: Make OCI hook scripts executable
|
||||
run: |
|
||||
chmod +x oci-hooks/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
|
||||
- name: Run code quality checks
|
||||
run: |
|
||||
cd oci-hooks
|
||||
tox -e lint
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
FORCE_MOCK_DEVICES: true
|
||||
run: |
|
||||
cd oci-hooks
|
||||
tox -e all
|
|
@ -32,12 +32,14 @@ jobs:
|
|||
job: copr_build
|
||||
trigger: commit
|
||||
branch: main
|
||||
owner: rhcontainerbot
|
||||
project: podman-next
|
||||
owner: "@centos-automotive-sig"
|
||||
project: qm-next
|
||||
enable_net: true
|
||||
notifications:
|
||||
failure_comment:
|
||||
message: "QM build failed for merged commit {commit_sha}. Please check logs {logs_url}"
|
||||
|
||||
- <<: *copr
|
||||
project: qm
|
||||
targets:
|
||||
- fedora-stable-aarch64
|
||||
- fedora-stable-ppc64le
|
||||
|
|
10
Makefile
10
Makefile
|
@ -58,7 +58,15 @@ dist: ## - Creates the QM distribution package
|
|||
--exclude='.git' \
|
||||
--dereference \
|
||||
--exclude='.gitignore' \
|
||||
--exclude='.fmf' \
|
||||
--exclude='.packit.*' \
|
||||
--exclude='.pre-commit*' \
|
||||
--exclude='.readthedocs.yaml' \
|
||||
--exclude='demos' \
|
||||
--exclude='docs' \
|
||||
--exclude='plans' \
|
||||
--exclude='subsystems' \
|
||||
--exclude='tests' \
|
||||
--exclude='.github' \
|
||||
--transform s/qm/qm-${VERSION}/ \
|
||||
-f /tmp/v${VERSION}.tar.gz ../qm
|
||||
|
@ -100,3 +108,5 @@ install: man all ## - Install QM files (including selinux)
|
|||
install -D -m 644 containers.conf ${DESTDIR}${DATADIR}/qm/containers.conf
|
||||
install -D -m 644 qm.container ${DESTDIR}${DATADIR}/containers/systemd/qm.container
|
||||
install -D -m 755 tools/qm-is-ostree ${DESTDIR}${DATADIR}/qm/qm-is-ostree
|
||||
install -D -m 755 tools/qmctl/qmctl ${DESTDIR}${PREFIX}/bin/qmctl
|
||||
install -D -m 644 tools/qmctl/qmctl.1 ${DESTDIR}${DATADIR}/man/man1/qmctl.1
|
||||
|
|
|
@ -33,7 +33,7 @@ Choose one of the following sub-packages and build using make.
|
|||
```bash
|
||||
git clone git@github.com:containers/qm.git && cd qm
|
||||
|
||||
Example of subpackages: input, kvm, sound, tty7, ttyUSB0, video, windowmanager
|
||||
Example of subpackages: kvm, qm-oci-hooks, ros2, text2speech, windowmanager
|
||||
|
||||
make TARGETS=input subpackages
|
||||
ls rpmbuild/RPMS/noarch/
|
||||
|
@ -80,125 +80,6 @@ Example changing the spec and triggering the build via make (feel free to automa
|
|||
make TARGETS=windowmanager subpackages
|
||||
```
|
||||
|
||||
## QM sub-package Input
|
||||
|
||||
The `input` sub-package exposes `/dev/input/*` devices (such as keyboards, mice, touchpads, etc.) from the host system to the QM container.
|
||||
|
||||
Follow the steps below to verify that the input sub-package properly mounts and exposes input devices inside the QM container.
|
||||
|
||||
### Step 1: Verify input devices are NOT visible inside QM
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm ls /dev/input
|
||||
ls: cannot access '/dev/input': No such file or directory
|
||||
```
|
||||
|
||||
### Step 2: Build and install the input sub-package
|
||||
|
||||
```bash
|
||||
host> make TARGETS=input subpackages
|
||||
host> sudo dnf install ./rpmbuild/RPMS/noarch/qm_mount_bind_input-0.7.4-1.fc41.noarch.rpm
|
||||
```
|
||||
|
||||
### Step 3: Confirm input devices exist on the host
|
||||
|
||||
```bash
|
||||
host> ls /dev/input
|
||||
by-id event0 event2 event4 js0 mouse0 mouse2
|
||||
by-path event1 event3 event5 mice mouse1
|
||||
```
|
||||
|
||||
### Step 4: Restart QM to apply the mount bind configuration
|
||||
|
||||
```bash
|
||||
host> sudo systemctl daemon-reload
|
||||
host> sudo podman restart qm
|
||||
```
|
||||
|
||||
### Step 5: Re-check input devices inside QM
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm ls /dev/input
|
||||
event0 event2 event4 js0 mouse0 mouse2
|
||||
event1 event3 event5 mice mouse1
|
||||
```
|
||||
|
||||
## QM sub-package tty7
|
||||
|
||||
The tty7 sub-package exposes `/dev/tty7` to the container. `/dev/tty7` is typically the virtual terminal associated with the graphical user interface (GUI) on Linux systems.
|
||||
|
||||
Follow the steps below to verify that the input sub-package properly mounts and exposes input devices inside the QM container.
|
||||
|
||||
### Step 1: Verify tty7 is NOT visible inside QM
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm ls -l /dev/tty7
|
||||
ls: cannot access '/dev/tty7': No such file or directory
|
||||
```
|
||||
|
||||
### Step 2: Build and install the tty7 sub-package
|
||||
|
||||
```bash
|
||||
host> make TARGETS=tty7 subpackages
|
||||
host> sudo dnf install ./rpmbuild/RPMS/noarch/qm-mount-bind-tty7-0.7.4-1.fc41.noarch.rpm
|
||||
```
|
||||
|
||||
### Step 3: Restart QM to apply the mount bind configuration
|
||||
|
||||
```bash
|
||||
host> sudo systemctl daemon-reload
|
||||
host> sudo podman restart qm
|
||||
```
|
||||
|
||||
### Step 4: Re-check tty7 inside QM
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm ls -l /dev/tty7
|
||||
crw--w----. 1 root tty 4, 7 Apr 15 13:34 /dev/tty7
|
||||
```
|
||||
|
||||
## QM sub-package ttyUSB0
|
||||
|
||||
The ttyUSB0 sub-package exposes /dev/ttyUSB0 to the QM container. This device node is commonly used for USB-to-serial adapters, which are widely used to connect embedded systems, IoT devices, or other serial-based equipment.
|
||||
|
||||
### Step 1: Verify ttyUSB0 is NOT visible inside QM
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm ls -l /dev/ttyUSB0
|
||||
ls: cannot access '/dev/ttyUSB0': No such file or directory
|
||||
```
|
||||
|
||||
### Step 2: Build and install the ttyUSB0 sub-package
|
||||
|
||||
```bash
|
||||
host> make TARGETS=ttyUSB0 subpackages
|
||||
host> sudo dnf install ./rpmbuild/RPMS/noarch/qm-mount-bind-ttyUSB0-0.7.4-1.fc41.noarch.rpm
|
||||
```
|
||||
|
||||
### Step 3: Restart QM to apply the configuration
|
||||
|
||||
```bash
|
||||
host> sudo systemctl daemon-reload
|
||||
host> sudo podman restart qm
|
||||
```
|
||||
|
||||
### Step 4: Re-check ttyUSB0 inside QM
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm ls -l /dev/ttyUSB0
|
||||
crw-rw-rw-. 1 root root 4, 64 Apr 24 08:50 /dev/ttyUSB0
|
||||
```
|
||||
|
||||
### Additional Notes
|
||||
|
||||
- Make sure the USB-to-serial device is connected to the host machine before restarting QM.
|
||||
- You can fake ttyUSB0 connection on host machine for testing reasons with:
|
||||
|
||||
```bash
|
||||
sudo mknod /dev/ttyUSB0 c 4 64
|
||||
sudo chmod 666 /dev/ttyUSB0
|
||||
```
|
||||
|
||||
## QM sub-package Video
|
||||
|
||||
The video sub-package exposes `/dev/video0` (or many video devices required) to the container. This feature is useful for demonstrating how to share a camera from the host system into a container using Podman drop-in. To showcase this functionality, we provide the following demo:
|
||||
|
@ -332,6 +213,242 @@ hw:1,0: sound card 1, device 0
|
|||
-r 48000: sample rate of 48 kHz
|
||||
```
|
||||
|
||||
## QM sub-package OCI Hooks
|
||||
|
||||
The QM sub-package OCI Hooks provides dynamic device access management for containers through OCI runtime hooks. This subpackage includes essential hooks that enable secure and flexible device sharing between the host system and containers with robust error handling and comprehensive testing.
|
||||
|
||||
### Components
|
||||
|
||||
The `qm-oci-hooks` subpackage includes:
|
||||
|
||||
- **qm-device-manager**: Dynamic device mounting hook that provides access to various hardware devices based on container annotations
|
||||
- **wayland-client-devices**: Hook for GPU hardware acceleration access for Wayland client applications running as nested containers
|
||||
|
||||
### Supported Device Types
|
||||
|
||||
#### QM Device Manager
|
||||
|
||||
The `qm-device-manager` hook supports the following device types through container annotations:
|
||||
|
||||
| Device Type | Annotation | Devices Provided |
|
||||
|-------------|------------|------------------|
|
||||
| Audio | `org.containers.qm.device.audio=true` | `/dev/snd/*` (audio devices) |
|
||||
| Video | `org.containers.qm.device.video=true` | `/dev/video*`, `/dev/media*` (cameras, video devices) |
|
||||
| Input | `org.containers.qm.device.input=true` | `/dev/input/*` (keyboards, mice, touchpads) |
|
||||
| TTYs | `org.containers.qm.device.ttys=true` | `/dev/tty0-7` (virtual terminals) |
|
||||
| TTY USB | `org.containers.qm.device.ttyUSB=true` | `/dev/ttyUSB*` (USB TTY devices) |
|
||||
| DVB | `org.containers.qm.device.dvb=true` | `/dev/dvb/*` (digital TV devices) |
|
||||
| Radio | `org.containers.qm.device.radio=true` | `/dev/radio*` (radio devices) |
|
||||
| Multi-seat Support | `org.containers.qm.wayland.seat=seat0` | Input devices, render devices, and display devices associated with the specified systemd-logind seat |
|
||||
|
||||
#### Wayland Client Devices
|
||||
|
||||
The `wayland-client-devices` hook supports:
|
||||
|
||||
| Functionality | Annotation | Devices Provided |
|
||||
|---------------|------------|------------------|
|
||||
| GPU Acceleration | `org.containers.qm.wayland-client.gpu=true` | GPU render devices (`/dev/dri/render*`) for hardware acceleration |
|
||||
|
||||
### Features
|
||||
|
||||
- **Dynamic Device Discovery**: Automatically discovers and mounts available devices at container startup
|
||||
- **Annotation-Based Security**: Devices are only mounted when explicitly requested via annotations
|
||||
- **Multi-seat Support**: Enables proper device access in systemd-logind multi-seat environments
|
||||
- **Comprehensive Mock Device Support**: Full testing infrastructure with mock devices for all device types
|
||||
- **GPU Acceleration**: Provides hardware acceleration for Wayland client applications
|
||||
- **Comprehensive Logging**: All hooks provide detailed logging for monitoring and debugging:
|
||||
- Device Manager: `/var/log/qm-device-manager.log`
|
||||
- Wayland Client: `/var/log/qm-wayland-client-devices.log`
|
||||
- **Runtime Flexibility**: No system restart required when adding device access to new containers
|
||||
- **Lightweight Implementation**: Shell script-based hooks with minimal dependencies
|
||||
|
||||
### Building and Installing
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containers/qm.git && cd qm
|
||||
make TARGETS=qm-oci-hooks subpackages
|
||||
sudo dnf install rpmbuild/RPMS/noarch/qm-oci-hooks-*.noarch.rpm
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Example 1: Serial communication with USB TTY devices
|
||||
|
||||
```bash
|
||||
# Create a container with access to all USB TTY devices
|
||||
cat > /etc/containers/systemd/serial-app.container << EOF
|
||||
[Unit]
|
||||
Description=Serial Communication Application
|
||||
|
||||
[Container]
|
||||
Image=my-serial-app:latest
|
||||
Annotation=org.containers.qm.device.ttyUSB=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Example 2: Multi-seat Wayland compositor with session devices
|
||||
|
||||
```bash
|
||||
# Create a dropin for QM container to enable multi-seat support
|
||||
mkdir -p /etc/containers/systemd/qm.container.d/
|
||||
cat > /etc/containers/systemd/qm.container.d/wayland-seat.conf << EOF
|
||||
[Container]
|
||||
Annotation=org.containers.qm.wayland.seat=seat0
|
||||
EOF
|
||||
|
||||
# Create a Wayland compositor container that runs inside QM
|
||||
cat > /etc/qm/containers/systemd/wayland-compositor.container << EOF
|
||||
[Unit]
|
||||
Description=Wayland Compositor with Multi-seat Support
|
||||
|
||||
[Container]
|
||||
Image=wayland-compositor:latest
|
||||
Annotation=org.containers.qm.wayland.seat=seat0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Example 3: Wayland client application with GPU acceleration
|
||||
|
||||
```bash
|
||||
# Create a Wayland client container with GPU hardware acceleration
|
||||
cat > /etc/qm/containers/systemd/gpu-app.container << EOF
|
||||
[Unit]
|
||||
Description=GPU-accelerated Application
|
||||
|
||||
[Container]
|
||||
Image=my-gpu-app:latest
|
||||
Annotation=org.containers.qm.wayland-client.gpu=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
To verify the hooks are installed and working:
|
||||
|
||||
```bash
|
||||
# Check all OCI hook installations
|
||||
ls -la /usr/share/containers/oci/hooks.d/
|
||||
|
||||
# Check hook executables
|
||||
ls -la /usr/libexec/oci/hooks.d/
|
||||
|
||||
# Verify hook JSON configurations are valid
|
||||
find /usr/share/containers/oci/hooks.d/ -name "*.json" -exec jq . {} \;
|
||||
|
||||
# View hook logs (all hooks provide comprehensive logging)
|
||||
tail -f /var/log/qm-device-manager.log
|
||||
tail -f /var/log/qm-wayland-client-devices.log
|
||||
|
||||
# Test device access with qm-device-manager
|
||||
podman exec -it my-audio-app ls -la /dev/snd/
|
||||
|
||||
# Test GPU access with wayland-client-devices (if applicable)
|
||||
podman exec -it gpu-app ls -la /dev/dri/
|
||||
|
||||
# Run hook tests (if source available)
|
||||
cd oci-hooks && tox -e all
|
||||
```
|
||||
|
||||
### Testing and Development
|
||||
|
||||
The OCI hooks include a comprehensive test suite for development and validation:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cd oci-hooks && tox -e all
|
||||
|
||||
# Run specific test categories
|
||||
tox -e unit # Unit tests only
|
||||
tox -e integration # Integration tests only
|
||||
tox -e performance # Performance tests only
|
||||
|
||||
# Run linting and formatting
|
||||
tox -e lint # Code linting
|
||||
tox -e format # Code formatting
|
||||
|
||||
# Test with mock devices (useful for CI environments)
|
||||
FORCE_MOCK_DEVICES=true tox -e unit -- -k "mock_devices"
|
||||
```
|
||||
|
||||
### OCI Hooks Specification and Documentation
|
||||
|
||||
The QM OCI hooks are implemented according to the [Open Container Initiative (OCI) Runtime Specification](https://github.com/opencontainers/runtime-spec).
|
||||
|
||||
#### OCI Hook Configuration Format
|
||||
|
||||
OCI hooks are configured using JSON files that define when and how the hooks should be executed. Each hook configuration follows the structure defined in the [OCI config schema](https://github.com/opencontainers/runtime-spec/blob/main/schema/config-schema.json).
|
||||
|
||||
**Key components of hook configuration:**
|
||||
|
||||
- **version**: OCI specification version (e.g., `"1.0.0"`)
|
||||
- **hook**: Object defining the hook executable and arguments
|
||||
- **when**: Object defining trigger conditions for the hook
|
||||
- **stages**: Array of lifecycle stages when the hook should run
|
||||
|
||||
#### Hook Lifecycle Stages
|
||||
|
||||
According to the [OCI POSIX Platform Hooks specification](https://github.com/opencontainers/runtime-spec/blob/main/config.md#posix-platform-hooks), hooks can be executed at three stages:
|
||||
|
||||
1. **prestart**: Hooks called after the container process is spawned, but before the user-supplied command is executed
|
||||
2. **poststart**: Hooks called after the user-supplied command is executed
|
||||
3. **poststop**: Hooks called after the container process is terminated
|
||||
|
||||
QM hooks execution stages:
|
||||
|
||||
- `qm-device-manager`: Runs during **prestart** to mount devices before the container starts
|
||||
- `wayland-client-devices`: Runs during **prestart** to provide GPU access before the container starts
|
||||
|
||||
#### Hook Input/Output Specification
|
||||
|
||||
OCI hooks receive container state information via **stdin** and communicate results via **stdout/stderr** and **exit codes**:
|
||||
|
||||
**Input (stdin)**: JSON object containing container state according to OCI spec:
|
||||
|
||||
```json
|
||||
{
|
||||
"ociVersion": "1.0.0",
|
||||
"id": "container-id",
|
||||
"status": "creating",
|
||||
"pid": 1234,
|
||||
"bundle": "/path/to/bundle",
|
||||
"annotations": {
|
||||
"org.containers.qm.device.audio": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output**:
|
||||
|
||||
- **Exit code 0**: Success
|
||||
- **Exit code non-zero**: Failure (container creation aborted)
|
||||
- **stderr**: Error messages and diagnostics
|
||||
- **stdout**: Hook output (typically empty for QM hooks)
|
||||
|
||||
#### Annotation Pattern Matching
|
||||
|
||||
QM hooks use regular expressions in their `when.annotations` configuration to match container annotations:
|
||||
|
||||
- `org\\.containers\\.qm\\.device\\.(audio|video|input|ttys|ttyUSB|dvb|radio)`: Matches device-specific annotations
|
||||
- `org\\.containers\\.qm\\.wayland\\.seat`: Matches Wayland seat annotations with any value
|
||||
- `org\\.containers\\.qm\\.wayland-client\\.gpu`: Matches GPU acceleration requests
|
||||
|
||||
#### Hook Installation Locations
|
||||
|
||||
QM hooks are installed in standard OCI locations:
|
||||
|
||||
- Hook executables: `/usr/libexec/oci/hooks.d/`
|
||||
- Hook configurations: `/usr/share/containers/oci/hooks.d/`
|
||||
- Hook libraries: `/usr/libexec/oci/lib/`
|
||||
|
||||
## QM sub-package ROS2
|
||||
|
||||
The QM sub-package ROS2 (a.k.a "The Robot Operating System" or middleware for robots) is widely used by open-source projects, enterprises, companies, edge env and government agencies, including NASA, to advance robotics and autonomous systems. Enabled by Quadlet in QM, ROS2 on top of QM provides a secure environment where robots can operate and communicate safely, benefiting from QM's "Freedom from Interference" frequently tested layer. This ensures robots can function without external interference, enhancing their reliability and security.
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/dvbX Digital TV
|
||||
#
|
||||
# In a typical vehicle system, dvb is connected to car's onboard computer via a CAN bus
|
||||
# (Controller Area Network), which transmits signals from the dvbs to the car’s system for real-time
|
||||
# processing.
|
||||
#
|
||||
# However, it's possible to create a simulation environment using traditional hardware and open-source
|
||||
# software, eliminating the need for actual car dvb or CAN bus integration. By using open-source
|
||||
# tools like Podman containers and dvb processing libraries, virtual
|
||||
# dvbs can be simulated.
|
||||
#
|
||||
# "/dev/dvb0:/dev/dvb0", # Stereo Radio
|
||||
#
|
||||
# Camera System Layout (Top-Down View)
|
||||
#
|
||||
# ┌─────────────────────────────┐
|
||||
# │ /dev/dvb0 │
|
||||
# └────────────┬────────────────┘
|
||||
# │
|
||||
# ┌─────────────────────┴────────────────────────────────┐
|
||||
# │ Vehicle Body (Top View) │
|
||||
# │ │
|
||||
# └──────────────────────────────────────────────────────┘
|
||||
#
|
||||
# Drop-in configuration for Podman to mount bind /dev/dvb from host to container
|
||||
#
|
||||
[Container]
|
||||
AddDevice=-/dev/dvb0
|
|
@ -1,45 +0,0 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/input from host to container
|
||||
#
|
||||
# Input Device Nodes
|
||||
# ========================
|
||||
# The files inside /dev/input are typically character device nodes that
|
||||
# correspond to input hardware such:
|
||||
#
|
||||
# - Keyboards
|
||||
# - Mice
|
||||
# - Touchpads
|
||||
# - Joysticks
|
||||
# - Multimedia keys
|
||||
# - Device files like /dev/input/event0, /dev/input/event1, etc., represent
|
||||
# different input events from connected devices.
|
||||
#
|
||||
# Event Interface
|
||||
# ==================
|
||||
# The Linux kernel uses an event-based input system where all user-space
|
||||
# programs interact with devices via event nodes (/dev/input/event*).
|
||||
# Each input device (keyboard, mouse, etc.) is associated with an event
|
||||
# node that captures raw input events, such as key presses, mouse movs, etc.
|
||||
#
|
||||
# Programs or services can read input events by opening these device nodes
|
||||
# and processing the input in real-time.
|
||||
#
|
||||
# Types of Devices in /dev/input
|
||||
# ================================
|
||||
# /dev/input/mice: All mice input, providing a unified interface
|
||||
# for all attached mice.
|
||||
#
|
||||
# /dev/input/js0, /dev/input/js1: Joystick devices that represent
|
||||
# individual game controllers.
|
||||
#
|
||||
# /dev/input/eventX: Event files where X is a number corresponding to
|
||||
# each input device's unique event file.
|
||||
#
|
||||
# /dev/input/by-id/: Symbolic links to input devices based on their
|
||||
# unique hardware ID.
|
||||
#
|
||||
# /dev/input/by-path/: Symbolic links based on the physical path the
|
||||
# input device is connected to (useful for distinguishing between
|
||||
# identical devices connected to different ports).
|
||||
#
|
||||
[Container]
|
||||
AddDevice=-/dev/input
|
|
@ -1,29 +0,0 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/radioX Stereo Radio
|
||||
#
|
||||
# In a typical vehicle system, radio is connected to car's onboard computer via a CAN bus
|
||||
# (Controller Area Network), which transmits signals from the radios to the car’s system for real-time
|
||||
# processing.
|
||||
#
|
||||
# However, it's possible to create a simulation environment using traditional hardware and open-source
|
||||
# software, eliminating the need for actual car radio or CAN bus integration. By using open-source
|
||||
# tools like Podman containers and radio processing libraries, virtual
|
||||
# radios can be simulated.
|
||||
#
|
||||
# "/dev/radio0:/dev/radio0", # Stereo Radio
|
||||
#
|
||||
# Camera System Layout (Top-Down View)
|
||||
#
|
||||
# ┌─────────────────────────────┐
|
||||
# │ /dev/radio0 │
|
||||
# │ (Stereo Radio) │
|
||||
# └────────────┬────────────────┘
|
||||
# │
|
||||
# ┌─────────────────────┴────────────────────────────────┐
|
||||
# │ Vehicle Body (Top View) │
|
||||
# │ │
|
||||
# └──────────────────────────────────────────────────────┘
|
||||
#
|
||||
# Drop-in configuration for Podman to mount bind /dev/radio0 from host to container
|
||||
#
|
||||
[Container]
|
||||
AddDevice=-/dev/radio0
|
|
@ -39,4 +39,4 @@
|
|||
#
|
||||
# qm_dropin_mount_bind_snd.conf
|
||||
[Container]
|
||||
AddDevice=-/dev/snd
|
||||
Annotation=org.containers.qm.device.audio=true
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/tty7 from host to container
|
||||
#
|
||||
# /dev/tty7 is typically the virtual terminal associated
|
||||
# with the graphical user interface (GUI) on Linux systems.
|
||||
# It is where the X server or the Wayland display server usually runs,
|
||||
# handling the graphical display, input, and windowing environment.
|
||||
# When you start a graphical session (such as GNOME, KDE, etc.),
|
||||
# it usually runs on this virtual console.
|
||||
[Container]
|
||||
AddDevice=-/dev/tty7
|
|
@ -1,72 +0,0 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/ttyUSB0 from host to QM
|
||||
# Containers.
|
||||
#
|
||||
# USB (Universal Serial Bus) can be used in two main modes: as a standard USB
|
||||
# device or for serial communication.
|
||||
#
|
||||
# 1. USB as a Standard Device
|
||||
# -------------------------
|
||||
# Most USB devices (e.g., flash drives, keyboards, mice) communicate using
|
||||
# the USB protocol. These devices use their own drivers and are typically
|
||||
# represented by device files such as /dev/sda for storage devices, or
|
||||
# /dev/input/ for input devices. Standard USB devices offer high data
|
||||
# transfer rates and advanced features like plug-and-play.
|
||||
#
|
||||
# 2. USB for Serial Communication
|
||||
# -----------------------------
|
||||
# USB can also be used to emulate traditional serial communication (e.g., RS-232).
|
||||
# This is common when using USB-to-serial adapters or devices like OBDII
|
||||
# (On-Board Diagnostics) interfaces, which convert vehicle diagnostic information
|
||||
# from the OBDII port to serial, then to USB, allowing the computer to read the data.
|
||||
# In this case, Linux creates device files like /dev/ttyUSB0 or /dev/ttyUSB1,
|
||||
# which represent virtual serial ports. These ports allow software to interact
|
||||
# with the OBDII device as if it were using a classic serial connection.
|
||||
#
|
||||
# Please NOTE
|
||||
# ------------
|
||||
# You can find ready-made ELM327 OBD2 (Serial) to USB cables available for
|
||||
# purchase, so there’s no need to build one from scratch unless you really want to.
|
||||
# Just make sure that the cable is compatible with your vehicle's manufacturer.
|
||||
#
|
||||
#
|
||||
# 3. ASCII Diagram
|
||||
# -------------
|
||||
# See below an ASCII diagram to show ELM327 cable connected into OBDII Diagnostic
|
||||
# Port and USB computer port and shared to QM container and it's nested containers.
|
||||
#
|
||||
# +----------------------------------+
|
||||
# | Vehicle |
|
||||
# | +----------+ |
|
||||
# | | OBDII | | The Vehicle OBDII Diagnostic Port is
|
||||
# | | Diagnostic| | usually *under the dashboard* near the
|
||||
# | | Port | | the driver's seat.
|
||||
# | +-----+-----+ |
|
||||
# | | |
|
||||
# +----------------------------------+
|
||||
# |
|
||||
# | ELM327 Cable (OBDII to USB)
|
||||
# +----------------------------------+
|
||||
# |
|
||||
# v
|
||||
# +------------------------------------------------------------+
|
||||
# | Laptop Machine |
|
||||
# | |
|
||||
# | /dev/ttyUSB0 <-- USB device for ELM327 |
|
||||
# | |
|
||||
# | +-----------------------------------------+ |
|
||||
# | | QM Container | |
|
||||
# | | | |
|
||||
# | | /dev/ttyUSB0 <-- Shared USB device | |
|
||||
# | | | |
|
||||
# | | +-----------------------------+ | |
|
||||
# | | | Nested Container | | |
|
||||
# | | | | | |
|
||||
# | | | /dev/ttyUSB0 <-- Shared | | |
|
||||
# | | | USB device | | |
|
||||
# | | +-----------------------------+ | |
|
||||
# | +-----------------------------------------+ |
|
||||
# +------------------------------------------------------------+
|
||||
#
|
||||
#
|
||||
[Container]
|
||||
AddDevice=-/dev/ttyUSB0
|
|
@ -1,11 +1,4 @@
|
|||
# Drop-in configuration for Podman to mount bind tty from host to container
|
||||
#
|
||||
[Container]
|
||||
Mount=type=bind,source=/dev/tty0,target=/dev/tty0
|
||||
Mount=type=bind,source=/dev/tty1,target=/dev/tty1
|
||||
Mount=type=bind,source=/dev/tty2,target=/dev/tty2
|
||||
Mount=type=bind,source=/dev/tty3,target=/dev/tty3
|
||||
Mount=type=bind,source=/dev/tty4,target=/dev/tty4
|
||||
Mount=type=bind,source=/dev/tty5,target=/dev/tty5
|
||||
Mount=type=bind,source=/dev/tty6,target=/dev/tty6
|
||||
Mount=type=bind,source=/dev/tty7,target=/dev/tty7
|
||||
Annotation=org.containers.qm.device.ttys=true
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
# Common Utility Library for OCI Hooks
|
||||
# This file provides shared utility functions for all OCI hooks.
|
||||
|
||||
# Common logging function for OCI hooks
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message
|
||||
message="$(date '+%Y-%m-%d %H:%M:%S') - ${HOOK_NAME:-oci-hook} - $level - $*"
|
||||
|
||||
# Write to log file if LOGFILE is set
|
||||
if [[ -n "${LOGFILE:-}" ]]; then
|
||||
echo "$message" >>"$LOGFILE"
|
||||
fi
|
||||
|
||||
# Also write errors to stderr
|
||||
if [[ "$level" == "ERROR" ]]; then
|
||||
echo "$message" >&2
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
# Device Support Library for OCI Hooks
|
||||
# This file provides standard device discovery functionality for OCI hooks.
|
||||
|
||||
# Source common utilities
|
||||
# shellcheck source=./common.sh disable=SC1091
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# Device discovery function using standard filesystem operations
|
||||
discover_devices() {
|
||||
local pattern="$1"
|
||||
local device_type="$2"
|
||||
|
||||
# Extract directory path from pattern to check existence first
|
||||
local dir_path=""
|
||||
if [[ "$pattern" =~ find[[:space:]]+(/[^[:space:]]+) ]]; then
|
||||
dir_path="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Check if specific directory exists (for patterns that search specific dirs)
|
||||
if [[ -n "$dir_path" && "$dir_path" != "/dev" ]]; then
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
# Directory doesn't exist - return empty, not an error
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normal device discovery
|
||||
eval "$pattern" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get device information for a given device path
|
||||
get_device_info() {
|
||||
local device_path="$1"
|
||||
|
||||
if [[ ! -e "$device_path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -c "$device_path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local stat_output
|
||||
if ! stat_output=$(stat -c "%F:%t:%T:%f:%u:%g" "$device_path" 2>/dev/null); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local major minor file_mode uid gid
|
||||
IFS=':' read -r _ major minor file_mode uid gid <<<"$stat_output"
|
||||
|
||||
# Convert hex to decimal
|
||||
major=$((0x$major))
|
||||
minor=$((0x$minor))
|
||||
file_mode=$((0x$file_mode))
|
||||
|
||||
# Determine device type
|
||||
local device_type="c"
|
||||
|
||||
# Return colon-separated values
|
||||
echo "$device_type:$major:$minor:$file_mode:$uid:$gid"
|
||||
}
|
||||
|
||||
# Check if device directory exists
|
||||
should_process_device_type() {
|
||||
local device_type="$1"
|
||||
local directory="$2"
|
||||
|
||||
# Process if the directory exists and is accessible
|
||||
[[ -d "$directory" ]] 2>/dev/null
|
||||
}
|
||||
|
||||
# GPU device discovery for wayland-client-devices
|
||||
discover_gpu_devices() {
|
||||
# Normal device discovery - check if directory exists first
|
||||
if [[ -d "/dev/dri" ]]; then
|
||||
find /dev/dri -type c \( -regex ".*/render.*" \) 2>/dev/null || true
|
||||
else
|
||||
# No GPU devices directory - return empty (not an error)
|
||||
return 0
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
#!/bin/bash
|
||||
# Mock Device Support Library for OCI Hooks Testing
|
||||
# This file provides device mocking functionality for testing OCI hooks
|
||||
# without requiring actual system devices.
|
||||
|
||||
# Source common utilities
|
||||
# shellcheck source=./common.sh disable=SC1091
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# Device discovery function with test override support
|
||||
discover_devices() {
|
||||
local pattern="$1"
|
||||
local device_type="$2"
|
||||
|
||||
# Check for test override environment variable
|
||||
local override_var="TEST_MOCK_${device_type^^}_DEVICES"
|
||||
local override_value="${!override_var:-}"
|
||||
|
||||
if [[ -n "$override_value" ]]; then
|
||||
# Use test-provided mock devices
|
||||
log "DEBUG" "Using mock $device_type devices: $override_value"
|
||||
# Convert comma-separated devices to null-terminated individual paths
|
||||
IFS=',' read -ra device_array <<<"$override_value"
|
||||
for device in "${device_array[@]}"; do
|
||||
printf '%s\0' "$device"
|
||||
done
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract directory path from pattern to check existence first
|
||||
local dir_path=""
|
||||
if [[ "$pattern" =~ find[[:space:]]+(/[^[:space:]]+) ]]; then
|
||||
dir_path="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Check if specific directory exists (for patterns that search specific dirs)
|
||||
if [[ -n "$dir_path" && "$dir_path" != "/dev" ]]; then
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
# Directory doesn't exist - return empty, not an error
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normal device discovery fallback
|
||||
eval "$pattern" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get device information - works with real temporary device files created by mknod
|
||||
get_device_info() {
|
||||
local device_path="$1"
|
||||
|
||||
# First try to get real device info (works if mknod succeeded)
|
||||
if [[ -e "$device_path" && -c "$device_path" ]]; then
|
||||
local stat_output
|
||||
if stat_output=$(stat -c "%F:%t:%T:%f:%u:%g" "$device_path" 2>/dev/null); then
|
||||
local major minor file_mode uid gid
|
||||
IFS=':' read -r _ major minor file_mode uid gid <<<"$stat_output"
|
||||
|
||||
# Convert hex to decimal
|
||||
major=$((0x$major))
|
||||
minor=$((0x$minor))
|
||||
file_mode=$((0x$file_mode))
|
||||
|
||||
# Determine device type
|
||||
local device_type="c"
|
||||
|
||||
# Return colon-separated values
|
||||
echo "$device_type:$major:$minor:$file_mode:$uid:$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# If real device doesn't exist and we're in test mode, generate mock device info
|
||||
if [[ "${FORCE_MOCK_DEVICES:-false}" == "true" || -n "${TEST_LOGFILE:-}" ]]; then
|
||||
# Check if this path matches any of our test mock device patterns
|
||||
local is_mock=false
|
||||
for env_var in TEST_MOCK_AUDIO_DEVICES TEST_MOCK_VIDEO_DEVICES TEST_MOCK_INPUT_DEVICES \
|
||||
TEST_MOCK_GPU_DEVICES TEST_MOCK_TTYUSB_DEVICES TEST_MOCK_DVB_DEVICES \
|
||||
TEST_MOCK_RADIO_DEVICES TEST_MOCK_TTYS_DEVICES; do
|
||||
if [[ -n "${!env_var:-}" ]]; then
|
||||
IFS=',' read -ra mock_devices <<<"${!env_var}"
|
||||
for mock_device in "${mock_devices[@]}"; do
|
||||
if [[ "$mock_device" == "$device_path" ]]; then
|
||||
is_mock=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [[ "$is_mock" == "true" ]]; then
|
||||
# Generate mock device info based on device path patterns
|
||||
local major minor file_mode=8624 uid=0 gid=0 # Default values
|
||||
local device_type="c"
|
||||
|
||||
case "$device_path" in
|
||||
*/snd/*)
|
||||
major=116
|
||||
minor=$((RANDOM % 20 + 1))
|
||||
;; # ALSA devices
|
||||
*/video*)
|
||||
major=81
|
||||
minor=$((RANDOM % 10))
|
||||
;; # Video devices
|
||||
*/input/*)
|
||||
major=13
|
||||
minor=$((RANDOM % 32))
|
||||
;; # Input devices
|
||||
*/dri/*)
|
||||
major=226
|
||||
minor=$((RANDOM % 10 + 128))
|
||||
;; # DRI/GPU devices
|
||||
*/ttyUSB*)
|
||||
major=188
|
||||
minor=$((RANDOM % 10))
|
||||
;; # USB serial
|
||||
*/dvb/*)
|
||||
major=212
|
||||
minor=$((RANDOM % 10))
|
||||
;; # DVB devices
|
||||
*/radio*)
|
||||
major=81
|
||||
minor=$((RANDOM % 10 + 64))
|
||||
;; # Radio devices
|
||||
*/tty[0-9]*)
|
||||
major=4
|
||||
minor=$((RANDOM % 10))
|
||||
;; # TTY devices
|
||||
*)
|
||||
major=1
|
||||
minor=$((RANDOM % 10))
|
||||
;; # Fallback
|
||||
esac
|
||||
|
||||
# Adjust gid for audio devices (usually audio group = 63)
|
||||
if [[ "$device_path" == */snd/* ]]; then
|
||||
gid=63
|
||||
fi
|
||||
|
||||
echo "$device_type:$major:$minor:$file_mode:$uid:$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Neither real device nor mock - fail
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if device directory exists or we have mock devices for the type
|
||||
should_process_device_type() {
|
||||
local device_type="$1"
|
||||
local directory="$2"
|
||||
|
||||
local override_var="TEST_MOCK_${device_type^^}_DEVICES"
|
||||
local override_value="${!override_var:-}"
|
||||
|
||||
# Process if we have mock devices OR the directory exists and is accessible
|
||||
[[ -n "$override_value" ]] || [[ -d "$directory" ]] 2>/dev/null
|
||||
}
|
||||
|
||||
# GPU device discovery with mocking support for wayland-client-devices
|
||||
discover_gpu_devices() {
|
||||
if [[ -n "${TEST_MOCK_GPU_DEVICES:-}" ]]; then
|
||||
# Use test-provided mock devices
|
||||
log "DEBUG" "Using mock GPU devices: $TEST_MOCK_GPU_DEVICES"
|
||||
echo "$TEST_MOCK_GPU_DEVICES" | tr ',' ' '
|
||||
else
|
||||
# Normal device discovery - check if directory exists first
|
||||
if [[ -d "/dev/dri" ]]; then
|
||||
find /dev/dri -type c \( -regex ".*/render.*" \) 2>/dev/null || true
|
||||
else
|
||||
# No GPU devices directory - return empty (not an error)
|
||||
log "DEBUG" "No /dev/dri directory found - no GPU devices available"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
# QM Device Manager OCI Hook
|
||||
|
||||
The QM Device Manager OCI Hook provides dynamic device access management for QM containers. This lightweight shell script hook replaces static drop-in configurations with a flexible, annotation-based approach to device mounting.
|
||||
|
||||
## Overview
|
||||
|
||||
The device manager hook allows containers to request specific device access through annotations. The hook dynamically discovers and mounts the requested devices at container creation time. It supports both device categories and Wayland seat-based device access.
|
||||
|
||||
## How Device Injection Works
|
||||
|
||||
The QM Device Manager hook operates during the OCI container **precreate** phase, intercepting container creation to dynamically inject device access. When QM starts a container with device annotations, the hook:
|
||||
|
||||
1. Discovers available devices on the host system based on annotation patterns
|
||||
2. Validates device accessibility and permissions
|
||||
3. Injects device access into the container's OCI specification via `linux.resources.devices[]`
|
||||
4. Adds device mounts to make devices available inside the container
|
||||
|
||||
### OCI Device Injection Format
|
||||
|
||||
The hook modifies the container's OCI runtime specification to include device access rules:
|
||||
|
||||
```json
|
||||
{
|
||||
"linux": {
|
||||
"resources": {
|
||||
"devices": [
|
||||
{
|
||||
"allow": true,
|
||||
"type": "c",
|
||||
"major": 116,
|
||||
"minor": 0,
|
||||
"access": "rwm"
|
||||
},
|
||||
{
|
||||
"allow": true,
|
||||
"type": "c",
|
||||
"major": 116,
|
||||
"minor": 1,
|
||||
"access": "rwm"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
"source": "/dev/snd/controlC0",
|
||||
"destination": "/dev/snd/controlC0",
|
||||
"type": "bind",
|
||||
"options": ["bind", "rprivate"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This ensures the container has both cgroup device access permissions and the actual device nodes mounted.
|
||||
|
||||
### Testing and Verification
|
||||
|
||||
The device injection process is comprehensively tested in the test suite:
|
||||
|
||||
- Device Discovery Tests (`test_qm_devices.py`): Verifies correct device discovery for all supported types
|
||||
- Annotation Processing (`test_qm_validation.py`): Tests annotation parsing and validation
|
||||
- Mock Device Support (`test_utils.py`): Provides mock device infrastructure for CI testing
|
||||
- Integration Tests (`test_qm_performance.py`): End-to-end hook execution testing
|
||||
|
||||
Run tests to verify device injection:
|
||||
|
||||
```bash
|
||||
cd oci-hooks && tox -e unit -- -k "device"
|
||||
```
|
||||
|
||||
### Library Architecture
|
||||
|
||||
The QM Device Manager hook uses a modular library architecture for maintainability and testing:
|
||||
|
||||
#### Core Libraries
|
||||
|
||||
- `common.sh`: Shared logging and utility functions used across all OCI hooks
|
||||
- `device-support.sh`: Production device discovery and management functions
|
||||
- `mock-device-support.sh`: Testing-specific device simulation functions
|
||||
|
||||
#### Mock Device Support for Testing
|
||||
|
||||
The `mock-device-support.sh` library enables comprehensive testing without requiring actual hardware devices. It works by:
|
||||
|
||||
Environment Variable Control - Tests set environment variables like:
|
||||
|
||||
```bash
|
||||
TEST_MOCK_AUDIO_DEVICES="/tmp/test_devices/snd/controlC0,/tmp/test_devices/snd/pcmC0D0p"
|
||||
TEST_MOCK_VIDEO_DEVICES="/tmp/test_devices/video0,/tmp/test_devices/video1"
|
||||
TEST_LOGFILE="/tmp/test.log" # Enables test mode
|
||||
```
|
||||
|
||||
Mock Device Creation - Python test framework creates temporary device files:
|
||||
|
||||
```python
|
||||
# In test_utils.py DeviceDetector class
|
||||
device_path = Path("/tmp/test_devices/snd/controlC0")
|
||||
device_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
device_path.touch() # Creates regular file as mock device
|
||||
```
|
||||
|
||||
Hook Library Selection - The hook script automatically detects test mode and sources the appropriate library:
|
||||
|
||||
```bash
|
||||
# In oci-qm-device-manager script
|
||||
if [[ -n "${TEST_LOGFILE:-}" ]]; then
|
||||
source /usr/libexec/oci/lib/mock-device-support.sh
|
||||
else
|
||||
source /usr/libexec/oci/lib/device-support.sh
|
||||
fi
|
||||
```
|
||||
|
||||
**NOTE**: mock device library is not supported in a productive environment.
|
||||
|
||||
Mock Functions - `mock-device-support.sh` overrides discovery functions to return test devices:
|
||||
|
||||
```bash
|
||||
discover_audio_devices() {
|
||||
if [[ -n "${TEST_MOCK_AUDIO_DEVICES:-}" ]]; then
|
||||
echo "${TEST_MOCK_AUDIO_DEVICES}" | tr ',' '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
get_device_info() {
|
||||
local device_path="$1"
|
||||
# Returns mock device info for regular files in test mode
|
||||
echo "c 116:0" # Mock major:minor for audio devices
|
||||
}
|
||||
```
|
||||
|
||||
This architecture allows the same hook script to work with both real hardware devices in production and simulated devices in CI testing environments.
|
||||
|
||||
## Supported Device Types
|
||||
|
||||
### Traditional Device Categories
|
||||
|
||||
| Device Type | Annotation | Devices Mounted | Description |
|
||||
|-------------|------------|-----------------|-------------|
|
||||
| audio | `org.containers.qm.device.audio=true` | `/dev/snd/*` | ALSA sound devices |
|
||||
| video | `org.containers.qm.device.video=true` | `/dev/video*`, `/dev/media*` | V4L2 video devices |
|
||||
| input | `org.containers.qm.device.input=true` | `/dev/input/*` | Input devices (keyboard, mouse, etc.) |
|
||||
| ttys | `org.containers.qm.device.ttys=true` | `/dev/tty0-7` | Virtual TTY devices for window managers |
|
||||
| ttyUSB | `org.containers.qm.device.ttyUSB=true` | `/dev/ttyUSB*` | USB TTY devices for serial communication |
|
||||
| dvb | `org.containers.qm.device.dvb=true` | `/dev/dvb/*` | DVB digital TV devices |
|
||||
| radio | `org.containers.qm.device.radio=true` | `/dev/radio*` | Radio devices |
|
||||
|
||||
### Wayland Seat Support
|
||||
|
||||
| Annotation | Purpose | Description |
|
||||
|------------|---------|-------------|
|
||||
| `org.containers.qm.wayland.seat=<seat_name>` | Multi-seat support | Mounts devices associated with a specific Wayland seat (e.g., `seat0`) |
|
||||
|
||||
The Wayland seat functionality dynamically discovers and mounts devices associated with the specified seat using `loginctl seat-status`. This enables proper multi-seat support for Wayland environments where different users may be logged into different seats.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Systemd Drop-In Files
|
||||
|
||||
1. Create a drop-in directory and file:
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/containers/systemd/qm.container.d/
|
||||
```
|
||||
|
||||
2. Create a dropin file (e.g., `devices.conf`):
|
||||
|
||||
```ini
|
||||
[Container]
|
||||
Annotation=org.containers.qm.device.audio=true
|
||||
Annotation=org.containers.qm.device.ttys=true
|
||||
# Wayland seat support
|
||||
Annotation=org.containers.qm.wayland.seat=seat0
|
||||
```
|
||||
|
||||
### Generated Systemd Service
|
||||
|
||||
When using Quadlet (`.container` files), the systemd generator creates the actual `ExecStart` commands that execute `podman run`. You can preview these with:
|
||||
|
||||
```bash
|
||||
# View generated systemd service commands
|
||||
/usr/lib/systemd/system-generators/podman-system-generator --dryrun
|
||||
|
||||
# Check generated service files
|
||||
ls -la /var/lib/systemd/generated/
|
||||
cat /var/lib/systemd/generated/container-name.service
|
||||
```
|
||||
|
||||
#### Example Generated Commands within QM
|
||||
|
||||
For a Quadlet container with device annotations:
|
||||
|
||||
Quadlet File (`/etc/containers/systemd/audio-app.container`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Audio Application Container
|
||||
After=local-fs.target
|
||||
|
||||
[Container]
|
||||
Image=my-audio-app:latest
|
||||
Annotation=org.containers.qm.device.audio=true
|
||||
Annotation=org.containers.qm.device.video=true
|
||||
Exec=sleep infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Generated ExecStart Command:
|
||||
|
||||
```bash
|
||||
ExecStart=/usr/bin/podman run \
|
||||
--cidfile=%t/%N.cid \
|
||||
--cgroups=split \
|
||||
--replace \
|
||||
--rm -d \
|
||||
--sdnotify=container \
|
||||
--name=audio-app \
|
||||
--annotation=org.containers.qm.device.audio=true \
|
||||
--annotation=org.containers.qm.device.video=true \
|
||||
localhost/qm/my-audio-app:latest sleep infinity
|
||||
```
|
||||
|
||||
When this service starts, the `qm-device-manager` hook intercepts the container creation and dynamically adds device mounts like:
|
||||
|
||||
- `--device=/dev/snd/controlC0:/dev/snd/controlC0`
|
||||
- `--device=/dev/snd/pcmC0D0p:/dev/snd/pcmC0D0p`
|
||||
- `--device=/dev/video0:/dev/video0`
|
||||
|
||||
## Logging
|
||||
|
||||
The hook logs activity to `/var/log/qm-device-manager.log` for debugging and monitoring:
|
||||
|
||||
```bash
|
||||
# View hook activity
|
||||
tail -f /var/log/qm-device-manager.log
|
||||
|
||||
# Check device discovery for a specific container
|
||||
grep "audio" /var/log/qm-device-manager.log
|
||||
|
||||
# Check Wayland seat processing
|
||||
grep "Wayland seat" /var/log/qm-device-manager.log
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Only devices that exist on the host are mounted
|
||||
- Device permissions are preserved from the host
|
||||
- Hook validates device accessibility before mounting
|
||||
- Annotation-based activation prevents accidental device exposure
|
||||
- Wayland seat integration respects systemd-logind seat assignments
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Device Not Available
|
||||
|
||||
If a requested device is not mounted:
|
||||
|
||||
1. Check if the device exists on the host: `ls -la /dev/snd/`
|
||||
2. Verify the annotation syntax: `org.containers.qm.device.audio=true`
|
||||
3. Check hook logs: `grep ERROR /var/log/qm-device-manager.log`
|
||||
4. Ensure the device is accessible: `test -c /dev/snd/controlC0`
|
||||
|
||||
### Wayland Seat Issues
|
||||
|
||||
If Wayland seat devices are not mounted:
|
||||
|
||||
1. Check seat status: `loginctl seat-status seat0`
|
||||
2. Verify seat annotation: `org.containers.qm.wayland.seat=seat0`
|
||||
3. Check systemd-logind service: `systemctl status systemd-logind`
|
||||
4. Review seat logs: `grep "seat0" /var/log/qm-device-manager.log`
|
||||
|
||||
### Hook Not Triggering
|
||||
|
||||
If the hook is not being called:
|
||||
|
||||
1. Verify hook installation: `ls -la /usr/libexec/oci/hooks.d/`
|
||||
2. Check hook configuration: `cat /usr/share/containers/oci/hooks.d/oci-qm-device-manager.json`
|
||||
3. Validate annotation pattern matching
|
||||
4. Check podman/crun OCI hook support
|
|
@ -0,0 +1,391 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
# QM Device Manager OCI Hook
|
||||
#
|
||||
# This hook dynamically manages device access for QM containers based on annotations.
|
||||
# It replaces the static drop-in configurations from individual subpackages with
|
||||
# dynamic device mounting based on container annotations.
|
||||
#
|
||||
# Supported devices:
|
||||
# - audio: /dev/snd/* (ALSA sound devices)
|
||||
# - video: /dev/video*, /dev/media* (V4L2 video devices)
|
||||
# - input: /dev/input/* (input devices like keyboards, mice)
|
||||
# - ttys: /dev/tty0-7 (virtual TTY devices for window managers)
|
||||
# - ttyUSB: /dev/ttyUSB* (USB TTY devices for serial communication)
|
||||
# - dvb: /dev/dvb/* (DVB digital TV devices)
|
||||
# - radio: /dev/radio* (radio devices)
|
||||
#
|
||||
# Supported device annotations:
|
||||
# - org.containers.qm.device.audio=true # /dev/snd/* (ALSA sound devices)
|
||||
# - org.containers.qm.device.video=true # /dev/video*, /dev/media* (V4L2 video devices)
|
||||
# - org.containers.qm.device.input=true # /dev/input/* (input devices)
|
||||
# - org.containers.qm.device.ttys=true # /dev/tty0-7 (virtual TTY devices)
|
||||
# - org.containers.qm.device.ttyUSB=true # /dev/ttyUSB* (USB TTY devices)
|
||||
# - org.containers.qm.device.dvb=true # /dev/dvb/* (DVB digital TV devices)
|
||||
# - org.containers.qm.device.radio=true # /dev/radio* (radio devices)
|
||||
#
|
||||
# Supported Wayland annotations:
|
||||
# - org.containers.qm.wayland.seat=<seat_name> # Devices for specific systemd-logind seat
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source common utilities and appropriate device support library
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# shellcheck source=../lib/common.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
|
||||
if [[ -n "${TEST_LOGFILE:-}" ]]; then
|
||||
# Test mode - use mock device support
|
||||
# shellcheck source=../lib/mock-device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/mock-device-support.sh"
|
||||
else
|
||||
# Normal mode - use standard device support
|
||||
# shellcheck source=../lib/device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/device-support.sh"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
LOGFILE="${TEST_LOGFILE:-/var/log/qm-device-manager.log}"
|
||||
# shellcheck disable=SC2034 # Used by log() function in device-support.sh
|
||||
HOOK_NAME="qm-device-manager"
|
||||
|
||||
# Process input devices with optional filtering and different output modes
|
||||
process_input_devices() {
|
||||
local mode="$1" # "direct" or "collect"
|
||||
local spec_json="$2" # current spec (for direct mode)
|
||||
local devname_list_var="$3" # variable name for device list (for collect mode)
|
||||
local filter_pattern="${4:-}" # optional regex filter
|
||||
local log_prefix="${5:-input}" # log message prefix
|
||||
|
||||
local device_count=0
|
||||
|
||||
if [[ ! -d "/dev/input" && -z "${TEST_LOGFILE:-}" ]]; then
|
||||
if [[ "$mode" == "direct" ]]; then
|
||||
log "INFO" "Added $device_count $log_prefix devices (no /dev/input directory)"
|
||||
echo "$spec_json"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "collect" ]]; then
|
||||
# For collect mode, we need to use nameref to modify the array
|
||||
local -n devname_list_ref="$devname_list_var"
|
||||
fi
|
||||
|
||||
while IFS= read -r -d '' device_path; do
|
||||
# Apply filter if provided
|
||||
if [[ -n "$filter_pattern" ]] && [[ ! "$device_path" =~ $filter_pattern ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "direct" ]]; then
|
||||
# Add device directly to spec
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
elif [[ "$mode" == "collect" ]]; then
|
||||
# Add device to collection list
|
||||
devname_list_ref+=("$device_path")
|
||||
log "INFO" "Adding $log_prefix device: $device_path"
|
||||
((device_count++))
|
||||
fi
|
||||
done < <(discover_devices "find /dev/input -type c -print0" "input")
|
||||
|
||||
if [[ "$mode" == "direct" ]]; then
|
||||
log "INFO" "Added $device_count $log_prefix devices"
|
||||
echo "$spec_json"
|
||||
fi
|
||||
}
|
||||
|
||||
# Add device to OCI spec using jq
|
||||
add_device_to_spec() {
|
||||
local spec_json="$1"
|
||||
local device_path="$2"
|
||||
local add_resources="${3:-false}"
|
||||
local device_info major minor file_mode uid gid device_type
|
||||
|
||||
if ! device_info=$(get_device_info "$device_path"); then
|
||||
log "WARNING" "Failed to get device info for $device_path"
|
||||
echo "$spec_json"
|
||||
return
|
||||
fi
|
||||
|
||||
IFS=':' read -r device_type major minor file_mode uid gid <<<"$device_info"
|
||||
log "INFO" "Adding device: $device_path (type=$device_type, major=$major, minor=$minor)"
|
||||
|
||||
# Ensure .linux.devices array exists
|
||||
local temp_spec
|
||||
if ! temp_spec=$(echo "$spec_json" | jq --compact-output 'if .linux.devices == null then .linux.devices = [] else . end' 2>/dev/null); then
|
||||
log "ERROR" "Failed to ensure .linux.devices array exists for $device_path"
|
||||
echo "$spec_json"
|
||||
return
|
||||
fi
|
||||
|
||||
# Add device if it doesn't already exist
|
||||
local result
|
||||
if ! result=$(echo "$temp_spec" | jq --compact-output \
|
||||
--arg path "$device_path" \
|
||||
--arg type "$device_type" \
|
||||
--argjson major "$major" \
|
||||
--argjson minor "$minor" \
|
||||
--argjson fileMode "$file_mode" \
|
||||
--argjson uid "$uid" \
|
||||
--argjson gid "$gid" \
|
||||
'if (.linux.devices | map(.path) | index($path)) == null then .linux.devices += [{"path": $path, "type": $type, "major": $major, "minor": $minor, "fileMode": $fileMode, "uid": $uid, "gid": $gid}] else . end' 2>/dev/null); then
|
||||
log "ERROR" "Failed to add device $device_path to spec"
|
||||
echo "$temp_spec" # Return the spec with array at least initialized
|
||||
return
|
||||
fi
|
||||
|
||||
# Add device resources if requested (for Wayland devices)
|
||||
if [[ "$add_resources" == "true" ]]; then
|
||||
if ! result=$(echo "$result" | jq --compact-output \
|
||||
--arg type "$device_type" \
|
||||
--argjson major "$major" \
|
||||
--argjson minor "$minor" \
|
||||
'if .linux.resources == null then .linux.resources = {} else . end |
|
||||
if .linux.resources.devices == null then .linux.resources.devices = [] else . end |
|
||||
if (.linux.resources.devices | map(select(.type == $type and .major == $major and .minor == $minor)) | length) == 0 then
|
||||
.linux.resources.devices += [{"allow": true, "type": $type, "major": $major, "minor": $minor, "access": "rwm"}]
|
||||
else . end' 2>/dev/null); then
|
||||
log "WARNING" "Failed to add device resources for $device_path, continuing with device only"
|
||||
else
|
||||
log "INFO" "Added device resources for: $device_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Process device annotations (org.containers.qm.device.*)
|
||||
process_device_annotation() {
|
||||
local spec_json="$1"
|
||||
local device_type="$2"
|
||||
|
||||
log "INFO" "Processing device type: $device_type"
|
||||
|
||||
case "$device_type" in
|
||||
"audio")
|
||||
# ALSA sound devices
|
||||
local device_count=0
|
||||
if should_process_device_type "audio" "/dev/snd"; then
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev/snd -type c -print0" "audio")
|
||||
fi
|
||||
log "INFO" "Added $device_count audio devices"
|
||||
;;
|
||||
"video")
|
||||
# V4L2 video devices
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 \\( -name \"video*\" -o -name \"media*\" \\) -type c -print0" "video")
|
||||
log "INFO" "Added $device_count video devices"
|
||||
;;
|
||||
"input")
|
||||
# Input devices
|
||||
spec_json=$(process_input_devices "direct" "$spec_json" "" "" "input")
|
||||
;;
|
||||
"ttys")
|
||||
# Virtual TTY devices (tty0-7)
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 -name 'tty[0-7]' -type c -print0" "ttys")
|
||||
log "INFO" "Added $device_count TTY devices"
|
||||
;;
|
||||
"ttyUSB")
|
||||
# USB TTY devices
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 -name \"ttyUSB*\" -type c -print0" "ttyUSB")
|
||||
log "INFO" "Added $device_count USB TTY devices"
|
||||
;;
|
||||
"dvb")
|
||||
# DVB digital TV devices
|
||||
local device_count=0
|
||||
if should_process_device_type "dvb" "/dev/dvb"; then
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev/dvb -type c -print0" "dvb")
|
||||
fi
|
||||
log "INFO" "Added $device_count DVB devices"
|
||||
;;
|
||||
"radio")
|
||||
# Radio devices
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 -name \"radio*\" -type c -print0" "radio")
|
||||
log "INFO" "Added $device_count radio devices"
|
||||
;;
|
||||
*)
|
||||
log "WARNING" "Unknown device type: $device_type"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$spec_json"
|
||||
}
|
||||
|
||||
# Process Wayland seat annotation (org.containers.qm.wayland.seat)
|
||||
process_wayland_seat() {
|
||||
local spec_json="$1"
|
||||
local seat_name="$2"
|
||||
|
||||
log "INFO" "Processing Wayland seat: $seat_name"
|
||||
|
||||
local device_count=0
|
||||
local devname_list=()
|
||||
|
||||
# Get devices associated with the systemd-logind seat
|
||||
if command -v loginctl >/dev/null 2>&1; then
|
||||
local seat_devices
|
||||
if seat_devices=$(loginctl seat-status "$seat_name" 2>/dev/null | grep -oP '/sys\S+'); then
|
||||
log "INFO" "Found seat system devices for $seat_name"
|
||||
|
||||
while IFS= read -r device; do
|
||||
if [[ -n "$device" ]]; then
|
||||
local devname
|
||||
if devname=$(udevadm info -x "$device" 2>/dev/null | grep -oP '^E: DEVNAME=\K.*'); then
|
||||
if [[ -n "$devname" && -e "$devname" ]]; then
|
||||
devname_list+=("$devname")
|
||||
log "INFO" "Found seat device: $devname"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <<<"$seat_devices"
|
||||
else
|
||||
log "WARNING" "No devices found for seat $seat_name or seat does not exist"
|
||||
fi
|
||||
else
|
||||
log "WARNING" "loginctl not available, cannot query seat devices"
|
||||
fi
|
||||
|
||||
# Add common input devices
|
||||
process_input_devices "collect" "" "devname_list" "/dev/input/(event[0-9]+|mice[0-9]*|mouse[0-9]+)$" "input"
|
||||
|
||||
# Add GPU render devices
|
||||
if [[ -d "/dev/dri" ]]; then
|
||||
while IFS= read -r -d '' device_path; do
|
||||
if [[ "$device_path" =~ /dev/dri/render.* ]]; then
|
||||
devname_list+=("$device_path")
|
||||
log "INFO" "Adding render device: $device_path"
|
||||
fi
|
||||
done < <(discover_devices "find /dev/dri -type c -name \"render*\" -print0" "gpu")
|
||||
fi
|
||||
|
||||
# Add all devices to spec with resources
|
||||
for device_path in "${devname_list[@]}"; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path" "true")
|
||||
((device_count++))
|
||||
done
|
||||
|
||||
log "INFO" "Added $device_count Wayland seat devices for $seat_name"
|
||||
echo "$spec_json"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local spec_json
|
||||
local annotations
|
||||
local total_devices=0
|
||||
|
||||
# Read OCI spec from stdin
|
||||
if ! spec_json=$(cat); then
|
||||
log "ERROR" "Failed to read OCI spec from stdin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure linux section exists
|
||||
if ! echo "$spec_json" | jq -e '.linux' >/dev/null 2>&1; then
|
||||
spec_json=$(echo "$spec_json" | jq '.linux = {}')
|
||||
fi
|
||||
|
||||
# Get all QM-related annotations
|
||||
annotations=$(echo "$spec_json" | jq -r '.annotations // {} | to_entries[] | select(.key | startswith("org.containers.qm.")) | "\(.key)=\(.value)"' 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$annotations" ]]; then
|
||||
log "INFO" "No QM device annotations found"
|
||||
echo "$spec_json"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "INFO" "Processing QM device annotations"
|
||||
|
||||
# Process each annotation
|
||||
while IFS= read -r annotation; do
|
||||
if [[ -z "$annotation" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract annotation key and value
|
||||
annotation_key="${annotation%%=*}"
|
||||
annotation_value="${annotation#*=}"
|
||||
|
||||
log "INFO" "Processing annotation: $annotation"
|
||||
|
||||
case "$annotation_key" in
|
||||
"org.containers.qm.device."*)
|
||||
# Traditional device annotation
|
||||
device_type="${annotation_key#org.containers.qm.device.}"
|
||||
|
||||
# Skip if value is not true/1/yes
|
||||
if [[ ! "$annotation_value" =~ ^(true|1|yes)$ ]]; then
|
||||
log "INFO" "Skipping device annotation with invalid value: $annotation"
|
||||
continue
|
||||
fi
|
||||
|
||||
spec_json=$(process_device_annotation "$spec_json" "$device_type" "$annotation_value")
|
||||
;;
|
||||
"org.containers.qm.wayland.seat")
|
||||
# Wayland seat annotation
|
||||
if [[ -n "$annotation_value" && "$annotation_value" != "null" ]]; then
|
||||
spec_json=$(process_wayland_seat "$spec_json" "$annotation_value")
|
||||
else
|
||||
log "INFO" "Skipping Wayland seat annotation with empty value"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log "INFO" "Skipping unknown QM annotation: $annotation_key"
|
||||
;;
|
||||
esac
|
||||
|
||||
done <<<"$annotations"
|
||||
|
||||
# Count total devices added
|
||||
total_devices=$(echo "$spec_json" | jq '.linux.devices // [] | length' 2>/dev/null || echo "0")
|
||||
log "INFO" "Total devices in spec: $total_devices"
|
||||
|
||||
# Output the modified spec
|
||||
echo "$spec_json"
|
||||
|
||||
log "INFO" "QM Device Manager hook completed successfully"
|
||||
}
|
||||
|
||||
# Ensure log file exists
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
touch "$LOGFILE"
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"hook": {
|
||||
"path": "/usr/libexec/oci/hooks.d/oci-qm-device-manager"
|
||||
},
|
||||
"when": {
|
||||
"annotations": {
|
||||
"^org\\.containers\\.qm\\.device\\..*$": "^.*$",
|
||||
"^org\\.containers\\.qm\\.wayland\\.seat": "^.*$"
|
||||
}
|
||||
},
|
||||
"stages": ["precreate"]
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
.venv/
|
||||
.env/
|
||||
|
||||
# Tox environments and cache
|
||||
.tox/
|
||||
.tox-*/
|
||||
|
||||
# Test reports and artifacts
|
||||
reports/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.nyc_output/
|
||||
|
||||
# Python cache and bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# IDEs and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
/tmp/
|
||||
.tmp/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
|
@ -0,0 +1,50 @@
|
|||
[MAIN]
|
||||
# Specify a score threshold to be exceeded before program exits with error
|
||||
fail-under=10.0
|
||||
|
||||
# Use multiple processes to speed up Pylint
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# Disable specific warnings that are common/acceptable in test files
|
||||
disable=
|
||||
# Import related (pytest/test dependencies are OK)
|
||||
import-error,
|
||||
# Test-specific patterns
|
||||
unused-argument, # Fixtures often have unused parameters
|
||||
redefined-outer-name, # Fixture names often match test parameters
|
||||
unused-variable, # Test outputs not always used
|
||||
inconsistent-return-statements, # pytest.fail() is intentional pattern
|
||||
# Less critical for tests
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-locals,
|
||||
duplicate-code, # Some duplication in tests is acceptable
|
||||
# File organization
|
||||
wrong-import-order, # Less critical in test files
|
||||
# Naming
|
||||
invalid-name, # Test method names can be long and descriptive
|
||||
|
||||
[DESIGN]
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=8
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=20
|
||||
|
||||
[SIMILARITIES]
|
||||
# Minimum lines number of a similarity
|
||||
min-similarity-lines=10
|
||||
|
||||
[BASIC]
|
||||
# Naming style matching correct function names (relaxed for tests)
|
||||
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Naming style matching correct method names (relaxed for tests)
|
||||
method-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Naming style matching correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
|
@ -0,0 +1,107 @@
|
|||
# OCI Hooks Unit Tests
|
||||
|
||||
Simple unit tests for QM OCI hooks using pytest and tox.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Set up virtual environment
|
||||
cd oci-hooks
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Install tox
|
||||
pip install tox
|
||||
|
||||
# Run unit tests
|
||||
tox -e unit
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Test Files
|
||||
|
||||
- `test_qm_device_manager.py` - Tests for the unified device manager hook
|
||||
- `test_wayland_client_devices.py` - Tests for the Wayland client GPU hook
|
||||
|
||||
### Test Categories
|
||||
|
||||
- **Unit tests**: Fast tests with no external dependencies
|
||||
- **Integration tests**: Tests that may require system resources
|
||||
- **Performance tests**: Tests with timing requirements
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Using Tox
|
||||
|
||||
```bash
|
||||
tox -e all # Run all tests
|
||||
tox -e lint # Check code formatting for python and shell
|
||||
tox -e format # Auto-format code with black
|
||||
```
|
||||
|
||||
### Direct Pytest
|
||||
|
||||
If you prefer to avoid tox, you can run pytest directly:
|
||||
|
||||
```bash
|
||||
# Set up virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
pytest -m unit --tb=short -v # Unit tests only
|
||||
pytest test_qm_device_manager.py # Specific test file
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically in GitHub Actions on:
|
||||
|
||||
- Push to `oci-hooks/**` paths
|
||||
- Pull requests affecting `oci-hooks/**` paths
|
||||
|
||||
Matrix testing across Python versions 3.9-3.12.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Make changes** to OCI hook scripts or tests
|
||||
2. **Run tests locally**: `tox -e unit`
|
||||
3. **Check code quality**: `tox -e lint`
|
||||
4. **Auto-format if needed**: `tox -e format`
|
||||
5. **Fix any failures** before committing
|
||||
6. **Push changes** - CI will run full test suite
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Issues
|
||||
|
||||
If hooks fail with permission errors, ensure scripts are executable:
|
||||
|
||||
```bash
|
||||
chmod +x ../qm-device-manager/oci-qm-device-manager
|
||||
chmod +x ../wayland-client-devices/oci-qm-wayland-client-devices
|
||||
```
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
```bash
|
||||
# For tox
|
||||
pip install tox
|
||||
|
||||
# For direct pytest
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Test Environment
|
||||
|
||||
Tests use temporary log files to avoid permission issues:
|
||||
|
||||
- Set `TEST_LOGFILE` environment variable for custom log location
|
||||
- Default: `/tmp/test-oci-hooks.log` for test runs
|
|
@ -0,0 +1,98 @@
|
|||
"""Pytest configuration and fixtures for OCI hooks testing."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from device_utils import DeviceCounter, TempDevices
|
||||
from test_utils import (
|
||||
HookRunner,
|
||||
OciSpecValidator,
|
||||
LogChecker,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_env():
|
||||
"""Set up test environment with temporary log files."""
|
||||
with tempfile.TemporaryDirectory(prefix="oci_hooks_test_") as temp_dir:
|
||||
env = os.environ.copy()
|
||||
env["TEST_LOGFILE"] = os.path.join(temp_dir, "test-hook.log")
|
||||
yield {
|
||||
"env": env,
|
||||
"log_file": env["TEST_LOGFILE"],
|
||||
"temp_dir": temp_dir,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hook_runner(test_env):
|
||||
"""Create a HookRunner instance with test environment and hook paths."""
|
||||
yield HookRunner(test_env["env"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oci_spec_validator():
|
||||
"""Provide validator for OCI specification JSON."""
|
||||
return OciSpecValidator.validate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_specs():
|
||||
"""Sample OCI specifications for testing."""
|
||||
yield {
|
||||
"audio_device": {
|
||||
"annotations": {"org.containers.qm.device.audio": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
"multiple_devices": {
|
||||
"annotations": {
|
||||
"org.containers.qm.device.audio": "true",
|
||||
"org.containers.qm.device.input": "true",
|
||||
"org.containers.qm.device.ttys": "true",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
"wayland_seat": {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "seat0"},
|
||||
"linux": {},
|
||||
},
|
||||
"wayland_gpu": {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
"combined": {
|
||||
"annotations": {
|
||||
"org.containers.qm.device.audio": "true",
|
||||
"org.containers.qm.wayland.seat": "seat0",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
"invalid_device": {
|
||||
"annotations": {"org.containers.qm.device.audio": "false"},
|
||||
"linux": {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def log_checker(test_env):
|
||||
"""Check hook log files for expected content."""
|
||||
return LogChecker(test_env["log_file"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_counter():
|
||||
"""Fixture providing device counting utilities."""
|
||||
return DeviceCounter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_devices():
|
||||
"""Fixture providing on-demand temporary device creation."""
|
||||
with tempfile.TemporaryDirectory(prefix="test_devices_") as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
yield TempDevices(temp_path)
|
|
@ -0,0 +1,223 @@
|
|||
"""Device testing utilities for OCI hooks."""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
class DeviceCounter:
|
||||
"""Helper class for counting devices in OCI specifications."""
|
||||
|
||||
@staticmethod
|
||||
def count_devices(
|
||||
spec: Dict[str, Any], device_pattern: Optional[str] = None
|
||||
) -> int:
|
||||
"""Count devices in OCI spec, optionally filtered by path pattern."""
|
||||
devices = spec.get("linux", {}).get("devices", [])
|
||||
|
||||
if device_pattern is None:
|
||||
return len(devices)
|
||||
|
||||
return len([d for d in devices if device_pattern in d.get("path", "")])
|
||||
|
||||
@staticmethod
|
||||
def count_resources(spec: Dict[str, Any]) -> int:
|
||||
"""Count device resources in OCI spec."""
|
||||
resources = (
|
||||
spec.get("linux", {}).get("resources", {}).get("devices", [])
|
||||
)
|
||||
return len(resources)
|
||||
|
||||
@staticmethod
|
||||
def has_device_path(spec: Dict[str, Any], path: str) -> bool:
|
||||
"""Check if OCI spec contains a device with specific path."""
|
||||
devices = spec.get("linux", {}).get("devices", [])
|
||||
return any(d.get("path") == path for d in devices)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceDetector:
|
||||
"""Class for detecting real devices for testing."""
|
||||
|
||||
paths: List[str] = field(default_factory=list)
|
||||
expected_count: int = 0
|
||||
|
||||
def mock_devices(self, device_type) -> Optional[Dict[str, str]]:
|
||||
"""Mock devices for testing."""
|
||||
return {device_type: ",".join(self.paths)}
|
||||
|
||||
|
||||
class TempDevices:
|
||||
"""Class for creating temporary device files on-demand for testing."""
|
||||
|
||||
def __init__(self, temp_path: Path):
|
||||
"""Initialize TempDevices with temporary path."""
|
||||
self.temp_path = temp_path
|
||||
self._created_devices = {}
|
||||
|
||||
# Create base directory structure
|
||||
for subdir in ["snd", "dri", "input", "dvb"]:
|
||||
(self.temp_path / subdir).mkdir(exist_ok=True)
|
||||
|
||||
def _create_device(
|
||||
self, device_path: Path, major: int, minor: int
|
||||
) -> Path:
|
||||
"""Create device node with mknod if possible, fall back to env vars."""
|
||||
device_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if os.environ.get("FORCE_MOCK_DEVICES", "false").lower() != "true":
|
||||
try:
|
||||
# Try to create real device nodes when not forced
|
||||
os.mknod(
|
||||
device_path, stat.S_IFCHR | 0o600, os.makedev(major, minor)
|
||||
)
|
||||
except OSError:
|
||||
# Fallback to regular files if mknod fails
|
||||
device_path.touch()
|
||||
else:
|
||||
# Create regular files when forced to use mock mode
|
||||
device_path.touch()
|
||||
|
||||
return device_path
|
||||
|
||||
def _set_mock_env_for_devices(self, device_type: str, devices: List[Path]):
|
||||
"""Set mock environment variables for device discovery."""
|
||||
env_var = f"TEST_MOCK_{device_type.upper()}_DEVICES"
|
||||
os.environ[env_var] = ",".join(str(d) for d in devices)
|
||||
|
||||
def get_audio_devices(self) -> DeviceDetector:
|
||||
"""Get audio device paths for testing."""
|
||||
if "audio" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "snd" / "controlC0", 116, 0
|
||||
),
|
||||
self._create_device(
|
||||
self.temp_path / "snd" / "pcmC0D0p", 116, 24
|
||||
),
|
||||
]
|
||||
self._created_devices["audio"] = devices
|
||||
self._set_mock_env_for_devices("audio", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["audio"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_video_devices(self) -> DeviceDetector:
|
||||
"""Get video device paths for testing."""
|
||||
if "video" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "video0", 81, 0),
|
||||
self._create_device(self.temp_path / "video1", 81, 1),
|
||||
]
|
||||
self._created_devices["video"] = devices
|
||||
self._set_mock_env_for_devices("video", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["video"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_input_devices(self) -> DeviceDetector:
|
||||
"""Get input device paths for testing."""
|
||||
if "input" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "input" / "event0", 13, 64
|
||||
),
|
||||
self._create_device(
|
||||
self.temp_path / "input" / "event1", 13, 65
|
||||
),
|
||||
self._create_device(self.temp_path / "input" / "mice", 13, 63),
|
||||
]
|
||||
self._created_devices["input"] = devices
|
||||
self._set_mock_env_for_devices("input", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["input"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_gpu_devices(self) -> DeviceDetector:
|
||||
"""Get GPU device paths for testing."""
|
||||
if "gpu" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "dri" / "renderD128", 226, 128
|
||||
)
|
||||
]
|
||||
self._created_devices["gpu"] = devices
|
||||
self._set_mock_env_for_devices("gpu", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["gpu"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_dvb_devices(self) -> DeviceDetector:
|
||||
"""Get DVB device paths for testing."""
|
||||
if "dvb" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "dvb" / "adapter0" / "frontend0", 212, 4
|
||||
),
|
||||
self._create_device(
|
||||
self.temp_path / "dvb" / "adapter0" / "demux0", 212, 5
|
||||
),
|
||||
]
|
||||
self._created_devices["dvb"] = devices
|
||||
self._set_mock_env_for_devices("dvb", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["dvb"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_ttyusb_devices(self) -> DeviceDetector:
|
||||
"""Get USB TTY device paths for testing."""
|
||||
if "ttyUSB" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "ttyUSB0", 188, 0),
|
||||
self._create_device(self.temp_path / "ttyUSB1", 188, 1),
|
||||
]
|
||||
self._created_devices["ttyUSB"] = devices
|
||||
self._set_mock_env_for_devices("ttyUSB", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["ttyUSB"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_radio_devices(self) -> DeviceDetector:
|
||||
"""Get radio device paths for testing."""
|
||||
if "radio" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "radio0", 81, 64),
|
||||
self._create_device(self.temp_path / "radio1", 81, 65),
|
||||
]
|
||||
self._created_devices["radio"] = devices
|
||||
self._set_mock_env_for_devices("radio", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["radio"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_ttys_devices(self) -> DeviceDetector:
|
||||
"""Get TTY device paths for testing."""
|
||||
if "ttys" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "tty0", 4, 0),
|
||||
self._create_device(self.temp_path / "tty1", 4, 1),
|
||||
self._create_device(self.temp_path / "tty2", 4, 2),
|
||||
]
|
||||
self._created_devices["ttys"] = devices
|
||||
self._set_mock_env_for_devices("ttys", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["ttys"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
[pytest]
|
||||
testpaths = .
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
--strict-markers
|
||||
--verbose
|
||||
--tb=short
|
||||
markers =
|
||||
unit: Unit tests that do not require external dependencies
|
||||
integration: Integration tests that may require system resources
|
||||
performance: Performance tests with timing requirements
|
||||
slow: Tests that take more than 1 second to run
|
||||
hook_execution: Tests that actually execute OCI hooks
|
||||
json_validation: Tests that validate JSON structure
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s
|
||||
log_cli_date_format = %Y-%m-%d %H:%M:%S
|
||||
junit_family = xunit2
|
|
@ -0,0 +1,5 @@
|
|||
# Basic testing framework
|
||||
pytest>=7.0.0
|
||||
|
||||
# Test fixtures and utilities
|
||||
jsonschema>=4.0.0
|
|
@ -0,0 +1,443 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""Tests for OCI hook configuration files."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
from test_utils import HookConfigLoader
|
||||
|
||||
|
||||
TEST_SPEC = {
|
||||
"version": "1.0.0",
|
||||
"process": {
|
||||
"terminal": False,
|
||||
"user": {"uid": 0, "gid": 0},
|
||||
"args": ["echo", "test"],
|
||||
"env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"cwd": "/",
|
||||
"capabilities": {
|
||||
"bounding": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"effective": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"inheritable": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"permitted": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"ambient": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
},
|
||||
"rlimits": [{"type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024}],
|
||||
"noNewPrivileges": True,
|
||||
},
|
||||
"root": {"path": "rootfs", "readonly": True},
|
||||
"hostname": "testing",
|
||||
"mounts": [
|
||||
{"destination": "/proc", "type": "proc", "source": "proc"},
|
||||
{
|
||||
"destination": "/dev",
|
||||
"type": "tmpfs",
|
||||
"source": "tmpfs",
|
||||
"options": ["nosuid", "strictatime", "mode=755", "size=65536k"],
|
||||
},
|
||||
],
|
||||
"annotations": {},
|
||||
"linux": {
|
||||
"resources": {},
|
||||
"namespaces": [
|
||||
{"type": "pid"},
|
||||
{"type": "network"},
|
||||
{"type": "ipc"},
|
||||
{"type": "uts"},
|
||||
{"type": "mount"},
|
||||
],
|
||||
"maskedPaths": [
|
||||
"/proc/acpi",
|
||||
"/proc/asound",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
],
|
||||
"readonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestHookConfigurations:
|
||||
"""Test class for validating OCI hook JSON configurations."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def hook_configs(self):
|
||||
"""Load all hook configuration files."""
|
||||
return HookConfigLoader.load_all_hook_configs()
|
||||
|
||||
@staticmethod
|
||||
def _get_hook_names():
|
||||
"""Get hook names for parametrized tests."""
|
||||
return HookConfigLoader.get_hook_names()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_json_syntax_validity(self, hook_configs, hook_name):
|
||||
"""Test that hook JSON file has valid syntax."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
# Re-read and parse to ensure JSON is valid
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
pytest.fail(f"Invalid JSON syntax in {config_path}: {e}")
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
@pytest.mark.parametrize(
|
||||
"required_field", ["version", "hook", "when", "stages"]
|
||||
)
|
||||
def test_required_schema_fields(
|
||||
self, hook_configs, hook_name, required_field
|
||||
):
|
||||
"""Test that hook config has required OCI hook schema fields."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
assert (
|
||||
required_field in config
|
||||
), f"Missing required field '{required_field}' in {config_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_hook_path_field(self, hook_configs, hook_name):
|
||||
"""Test that hook config has required OCI hook schema field 'path'."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
hook_config = hook_data["config"].get("hook", {})
|
||||
config_path = hook_data["path"]
|
||||
|
||||
assert (
|
||||
"path" in hook_config
|
||||
), f"Missing required hook field 'path' in {config_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_version_format(self, hook_configs, hook_name):
|
||||
"""Test that version field follows semantic versioning."""
|
||||
semver_pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
||||
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
version = config.get("version")
|
||||
assert version, f"Version field is empty in {config_path}"
|
||||
assert semver_pattern.match(
|
||||
version
|
||||
), f"Invalid version format '{version}' in {config_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_valid_stages(self, hook_configs, hook_name):
|
||||
"""Test that stages contain only valid OCI hook stages."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
valid_stages = ["prestart", "precreate", "poststart", "poststop"]
|
||||
|
||||
stages = config.get("stages", [])
|
||||
assert isinstance(
|
||||
stages, list
|
||||
), f"Stages must be a list in {config_path}"
|
||||
assert (
|
||||
len(stages) > 0
|
||||
), f"At least one stage must be specified in {config_path}"
|
||||
|
||||
assert all(
|
||||
stage in valid_stages for stage in stages
|
||||
), f"Invalid stages in {config_path}. Valid stages: {valid_stages}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_annotation_patterns(self, hook_configs, hook_name):
|
||||
"""Test that annotation patterns are valid regular expressions."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
when_section = config.get("when", {})
|
||||
annotations = when_section.get("annotations", {})
|
||||
|
||||
def is_valid_regex(pattern):
|
||||
try:
|
||||
re.compile(pattern)
|
||||
return True
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
invalid_patterns = {
|
||||
p: "key" for p in annotations.keys() if not is_valid_regex(p)
|
||||
}
|
||||
invalid_value_patterns = {
|
||||
v: "value"
|
||||
for v in annotations.values()
|
||||
if isinstance(v, str) and not is_valid_regex(v)
|
||||
}
|
||||
|
||||
assert not invalid_patterns, (
|
||||
f"Invalid regex in annotation keys: {invalid_patterns} "
|
||||
f"in {config_path}"
|
||||
)
|
||||
assert not invalid_value_patterns, (
|
||||
f"Invalid regex in annotation values: {invalid_value_patterns} "
|
||||
f"in {config_path}"
|
||||
)
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_hook_executable_exists(self, hook_configs, hook_name):
|
||||
"""Test that hook executables exist and are executable."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config_path = hook_data["path"]
|
||||
hook_path = Path(hook_data["config"]["hook"]["path"])
|
||||
|
||||
# For tests, the hook might be in the source directory rather than
|
||||
# installed location - try absolute path first, then relative
|
||||
try:
|
||||
# Check if absolute path works
|
||||
hook_path.stat()
|
||||
except OSError:
|
||||
# If absolute path fails, try relative to config file
|
||||
hook_path = config_path.parent / hook_path.name
|
||||
|
||||
assert hook_path.exists(), f"Hook executable not found: {hook_path}"
|
||||
assert hook_path.is_file(), f"Hook executable not a file: {hook_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_device_manager_annotations(self, hook_configs):
|
||||
"""Test that device manager config matches supported annotations."""
|
||||
hook_name = "oci-qm-device-manager"
|
||||
assert hook_name in hook_configs, f"Hook {hook_name} not found"
|
||||
|
||||
device_manager_config = hook_configs[hook_name]["config"]
|
||||
annotations = device_manager_config.get("when", {}).get(
|
||||
"annotations", {}
|
||||
)
|
||||
|
||||
# Check that device patterns exist
|
||||
device_patterns = [p for p in annotations.keys() if "device" in p]
|
||||
assert (
|
||||
device_patterns
|
||||
), "Device manager should support device annotations"
|
||||
|
||||
# Check that wayland patterns exist
|
||||
wayland_patterns = [p for p in annotations.keys() if "wayland" in p]
|
||||
assert (
|
||||
wayland_patterns
|
||||
), "Device manager should support Wayland annotations"
|
||||
|
||||
# Test that device patterns match expected device types
|
||||
test_annotations = [
|
||||
"org.containers.qm.device.audio",
|
||||
"org.containers.qm.device.video",
|
||||
"org.containers.qm.device.input",
|
||||
"org.containers.qm.device.ttys",
|
||||
]
|
||||
|
||||
unmatched_patterns = [
|
||||
p
|
||||
for p in device_patterns
|
||||
if not all(re.match(p, a) for a in test_annotations)
|
||||
]
|
||||
|
||||
assert not unmatched_patterns, (
|
||||
f"Device patterns {unmatched_patterns} failed to match all "
|
||||
"expected annotations"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wayland_client_annotations(self, hook_configs):
|
||||
"""Test that wayland client config matches supported annotations."""
|
||||
hook_name = "oci-qm-wayland-client-devices"
|
||||
assert hook_name in hook_configs, f"Hook {hook_name} not found"
|
||||
|
||||
wayland_config = hook_configs[hook_name]["config"]
|
||||
annotations = wayland_config.get("when", {}).get("annotations", {})
|
||||
|
||||
# Check that wayland-client patterns exist
|
||||
wayland_patterns = [
|
||||
p for p in annotations.keys() if "wayland-client" in p
|
||||
]
|
||||
assert (
|
||||
wayland_patterns
|
||||
), "Wayland client should have wayland-client pattern"
|
||||
|
||||
# Test that patterns match expected annotations
|
||||
test_annotations = ["org.containers.qm.wayland-client.gpu"]
|
||||
|
||||
unmatched_patterns = [
|
||||
p
|
||||
for p in wayland_patterns
|
||||
if not all(re.match(p, a) for a in test_annotations)
|
||||
]
|
||||
|
||||
assert not unmatched_patterns, (
|
||||
f"Wayland patterns {unmatched_patterns} failed to match all "
|
||||
"expected annotations"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_duplicate_annotations(self, hook_configs):
|
||||
"""Test that there are no overlapping annotation patterns."""
|
||||
# Create a flat list of all annotation patterns from all hooks
|
||||
all_patterns = [
|
||||
pattern
|
||||
for hook_data in hook_configs.values()
|
||||
for pattern in hook_data["config"]
|
||||
.get("when", {})
|
||||
.get("annotations", {})
|
||||
.keys()
|
||||
]
|
||||
|
||||
# Count occurrences of each pattern
|
||||
pattern_counts = Counter(all_patterns)
|
||||
|
||||
# Assert no duplicate patterns exist using named expression
|
||||
assert not (
|
||||
duplicates := {
|
||||
pattern: count
|
||||
for pattern, count in pattern_counts.items()
|
||||
if count > 1
|
||||
}
|
||||
), "Duplicate annotation patterns found: {}".format( # pylint: disable=C0209 # noqa: E501
|
||||
{
|
||||
pattern: [
|
||||
name
|
||||
for name, data in hook_configs.items()
|
||||
if pattern
|
||||
in data["config"].get("when", {}).get("annotations", {})
|
||||
]
|
||||
for pattern in duplicates
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_json_formatting(self, hook_configs, hook_name):
|
||||
"""Test that JSON file is properly formatted."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config_path = hook_data["path"]
|
||||
config = hook_data["config"]
|
||||
|
||||
# Read the original file content
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
original_content = f.read()
|
||||
|
||||
# Generate formatted JSON
|
||||
_ = json.dumps(config, indent=2, sort_keys=False) + "\n"
|
||||
|
||||
# Check if formatting is consistent (allowing for minor variations)
|
||||
# This is a basic check - in practice you might want to be
|
||||
# more lenient
|
||||
assert (
|
||||
len(original_content.strip()) > 0
|
||||
), f"JSON file {config_path} appears to be empty"
|
||||
|
||||
# Check that it's valid JSON by trying to parse it
|
||||
try:
|
||||
json.loads(original_content)
|
||||
except json.JSONDecodeError as e:
|
||||
pytest.fail(f"Invalid JSON in {config_path}: {e}")
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_hook_config_integration(self, hook_configs, hook_name):
|
||||
"""Test that hook config integrates correctly with its executable."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
# Get hook executable path
|
||||
hook_path = config.get("hook", {}).get("path", "")
|
||||
assert hook_path, f"Hook path not found in {config_path}"
|
||||
|
||||
# Try absolute path first, then relative to config directory
|
||||
full_hook_path = Path(hook_path)
|
||||
try:
|
||||
# Check if absolute path works
|
||||
full_hook_path.stat()
|
||||
except OSError:
|
||||
# If absolute path fails, try relative to config directory
|
||||
full_hook_path = config_path.parent / Path(hook_path).name
|
||||
|
||||
# Check that executable exists
|
||||
assert full_hook_path.exists(), (
|
||||
f"Hook executable not found: {full_hook_path} "
|
||||
f"(configured in {config_path})"
|
||||
)
|
||||
|
||||
# Check that it's executable
|
||||
assert (
|
||||
full_hook_path.is_file()
|
||||
), f"Hook path is not a file: {full_hook_path}"
|
||||
|
||||
# Basic executable check (if we can check permissions)
|
||||
try:
|
||||
assert (
|
||||
full_hook_path.stat().st_mode & 0o111
|
||||
), f"Hook file is not executable: {full_hook_path}"
|
||||
except OSError:
|
||||
# Skip permission check if we can't access file stats
|
||||
pass
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_stage_appropriateness(self, hook_configs, hook_name):
|
||||
"""Test that hook uses appropriate stages for its functionality."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
stages = config.get("stages", [])
|
||||
|
||||
# Device management hooks should use precreate stage
|
||||
# because they need to modify the OCI spec before container creation
|
||||
assert "precreate" in stages, (
|
||||
f"Hook {hook_name} should use 'precreate' stage for device "
|
||||
"management"
|
||||
)
|
||||
|
||||
# Should not use poststop for device hooks
|
||||
assert (
|
||||
"poststop" not in stages
|
||||
), f"Device hook {hook_name} should not use 'poststop' stage"
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Basic functionality tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import DeviceCounter
|
||||
|
||||
|
||||
class TestQMOCIHooksBasic:
|
||||
"""Basic functionality tests for QM device manager."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"hook_name", ["qm_device_manager", "wayland_client_devices"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"json_spec",
|
||||
[
|
||||
{"annotations": {}, "linux": {}},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.unknown": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": "false"},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["empty_spec", "unknown_spec", "invalid_spec"],
|
||||
)
|
||||
def test_basic_annotations(
|
||||
self, hook_name, json_spec, hook_runner, oci_spec_validator
|
||||
):
|
||||
"""Test hook with basic annotations returns unchanged spec."""
|
||||
result = hook_runner.run_hook(hook_name, json_spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert oci_spec_validator(result.output_spec), "Output spec is invalid"
|
||||
assert (
|
||||
DeviceCounter.count_devices(result.output_spec) == 0
|
||||
), "No devices should be added"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_spec",
|
||||
[
|
||||
# Missing required 'linux' property
|
||||
{"annotations": {"com.example": "value"}},
|
||||
# 'annotations' should be an object, not a string
|
||||
{"annotations": "not-an-object", "linux": {}},
|
||||
# Add a property not defined in the schema
|
||||
{
|
||||
"annotations": {"com.example": "value"},
|
||||
"linux": {},
|
||||
"extra": 123,
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_invalid_annotations(self, oci_spec_validator, invalid_spec):
|
||||
"""Test invalid annotations are rejected."""
|
||||
assert not oci_spec_validator(
|
||||
invalid_spec
|
||||
), "Invalid spec should be rejected"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_missing_linux_section(self, hook_runner):
|
||||
"""Test hook creates linux section if missing."""
|
||||
malformed_json = {
|
||||
"annotations": {"org.containers.qm.device.audio": "true"}
|
||||
# Missing linux section
|
||||
}
|
||||
result = hook_runner.run_hook("qm_device_manager", malformed_json)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert (
|
||||
"linux" in result.output_spec
|
||||
), "Hook should create linux section"
|
||||
assert isinstance(
|
||||
result.output_spec["linux"], dict
|
||||
), "Linux section should be a dict"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"json_spec",
|
||||
[
|
||||
# Non-wayland annotations ignored
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
# Unknown wayland annotations ignored
|
||||
{
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland-client.unknown": "true"
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["non_wayland_ignored", "unknown_wayland_ignored"],
|
||||
)
|
||||
def test_wayland_annotations(self, hook_runner, json_spec, device_counter):
|
||||
"""Test wayland annotations are processed correctly."""
|
||||
result = hook_runner.run_hook("wayland_client_devices", json_spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert (
|
||||
device_counter.count_devices(result.output_spec) == 0
|
||||
), "No devices should be added"
|
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Device detection and annotation tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import DeviceCounter
|
||||
|
||||
|
||||
class TestQMDeviceManagerDevices:
|
||||
"""Device annotation tests for QM device manager."""
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.hook_execution
|
||||
def test_audio_device_annotation(
|
||||
self, hook_runner, sample_specs, device_counter
|
||||
):
|
||||
"""Test audio device annotation executes successfully."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Hook should succeed and produce valid output regardless of
|
||||
# available devices (device-specific testing in temp_devices tests)
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert total_devices >= 0, "Device count should be non-negative"
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.hook_execution
|
||||
def test_multiple_device_annotations(
|
||||
self, hook_runner, sample_specs, device_counter
|
||||
):
|
||||
"""Test multiple device annotations."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["multiple_devices"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should have devices for at least one of the requested types
|
||||
# (depending on what's available in test environment)
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
|
||||
# Even if no devices are available, hook should succeed
|
||||
assert total_devices >= 0, "Device count should be non-negative"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"device_type,annotation_key",
|
||||
[
|
||||
("audio", "org.containers.qm.device.audio"),
|
||||
("video", "org.containers.qm.device.video"),
|
||||
("input", "org.containers.qm.device.input"),
|
||||
("ttys", "org.containers.qm.device.ttys"),
|
||||
("ttyUSB", "org.containers.qm.device.ttyUSB"),
|
||||
("dvb", "org.containers.qm.device.dvb"),
|
||||
("radio", "org.containers.qm.device.radio"),
|
||||
],
|
||||
)
|
||||
def test_individual_device_types(
|
||||
self, hook_runner, device_type, annotation_key
|
||||
):
|
||||
"""Test individual device type annotations."""
|
||||
spec = {"annotations": {annotation_key: "true"}, "linux": {}}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed for {device_type}: {result.stderr}"
|
||||
|
||||
# Validate result.output_spec structure
|
||||
assert "linux" in result.output_spec
|
||||
assert ("devices" in result.output_spec.get("linux", {})) or (
|
||||
DeviceCounter.count_devices(result.output_spec) == 0
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_audio_device_annotation_with_temp_devices(
|
||||
self, hook_runner, sample_specs, device_counter, temp_devices
|
||||
):
|
||||
"""Test audio device annotation with specific device creation."""
|
||||
device_detector = temp_devices.get_audio_devices()
|
||||
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager",
|
||||
sample_specs["audio_device"],
|
||||
mock_devices=device_detector.mock_devices(device_type="audio"),
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Verify expected number of devices
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
assert device_count == device_detector.expected_count, (
|
||||
f"Expected {device_detector.expected_count} audio devices, "
|
||||
f"got {device_count}"
|
||||
)
|
||||
|
||||
# Verify the specific temporary devices are present
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
found_paths = [d["path"] for d in devices]
|
||||
|
||||
assert all(
|
||||
temp_path in found_paths for temp_path in device_detector.paths
|
||||
), (
|
||||
f"All temporary audio devices should be found in output. "
|
||||
f"Expected: {device_detector.paths}, Found: {found_paths}"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"device_type,device_method",
|
||||
[
|
||||
("video", "get_video_devices"),
|
||||
("input", "get_input_devices"),
|
||||
("dvb", "get_dvb_devices"),
|
||||
("ttyUSB", "get_ttyusb_devices"),
|
||||
("radio", "get_radio_devices"),
|
||||
("ttys", "get_ttys_devices"),
|
||||
],
|
||||
)
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
def test_device_annotation_with_temp_devices(
|
||||
self,
|
||||
hook_runner,
|
||||
device_counter,
|
||||
temp_devices,
|
||||
device_type,
|
||||
device_method,
|
||||
):
|
||||
"""Test various device types with specific device creation."""
|
||||
device_detector = getattr(temp_devices, device_method)()
|
||||
|
||||
annotation_key = f"org.containers.qm.device.{device_type}"
|
||||
spec = {
|
||||
"annotations": {annotation_key: "true"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager",
|
||||
spec,
|
||||
mock_devices=device_detector.mock_devices(device_type),
|
||||
)
|
||||
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed for {device_type}: {result.stderr}"
|
||||
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
assert device_count == device_detector.expected_count, (
|
||||
f"Expected {device_detector.expected_count} {device_type} "
|
||||
f"devices, got {device_count}"
|
||||
)
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Error handling and edge case tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerErrorHandling:
|
||||
"""Error handling and edge case tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_non_qm_annotations_ignored(self, hook_runner, device_counter):
|
||||
"""Test that non-QM annotations are ignored."""
|
||||
spec = {
|
||||
"annotations": {
|
||||
"org.opencontainers.image.title": "test",
|
||||
"com.example.custom": "value",
|
||||
},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert (
|
||||
device_counter.count_devices(result.output_spec) == 0
|
||||
), "No devices should be added for non-QM annotations"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_hook_with_existing_devices(self, hook_runner):
|
||||
"""Test hook behavior when devices already exist."""
|
||||
spec_with_devices = {
|
||||
"annotations": {"org.containers.qm.device.audio": "true"},
|
||||
"linux": {
|
||||
"devices": [
|
||||
{
|
||||
"path": "/dev/existing",
|
||||
"type": "c",
|
||||
"major": 1,
|
||||
"minor": 3,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec_with_devices)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should preserve existing devices and potentially add new ones
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
existing_paths = [d["path"] for d in devices]
|
||||
assert (
|
||||
"/dev/existing" in existing_paths
|
||||
), "Existing device should be preserved"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_hook_with_existing_resources(self, hook_runner):
|
||||
"""Test hook behavior when resources already exist."""
|
||||
spec_with_resources = {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "true"},
|
||||
"linux": {
|
||||
"resources": {"devices": [{"allow": False, "type": "a"}]}
|
||||
},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec_with_resources)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should preserve existing resources
|
||||
resources = result.output_spec.get("linux", {}).get("resources", {})
|
||||
devices_limit = resources.get("devices", [])
|
||||
|
||||
# Check that the existing rule is preserved
|
||||
deny_all_rule = {"allow": False, "type": "a"}
|
||||
assert (
|
||||
deny_all_rule in devices_limit
|
||||
), "Existing resource rules should be preserved"
|
||||
|
||||
|
||||
class TestWaylandClientDevicesErrorHandling:
|
||||
"""Error handling tests for Wayland client devices."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_annotation_isolation(self, hook_runner, device_counter):
|
||||
"""Test that invalid annotations don't affect valid ones."""
|
||||
spec = {
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland-client.gpu": "true",
|
||||
"org.containers.qm.wayland-client.invalid": "bad_value",
|
||||
"org.opencontainers.image.title": "test",
|
||||
},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Valid GPU annotation should still be processed
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert total_devices >= 0, "Valid annotations should be processed"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_annotation_value(self, hook_runner, device_counter):
|
||||
"""Test handling of empty annotation values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": ""},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Empty value should be treated as falsy
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), "Empty annotation value should not add devices"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_annotation_values(self, hook_runner, device_counter):
|
||||
"""Test handling of unknown annotation values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": "maybe"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Unknown values should typically be treated as falsy for safety
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), "Unknown annotation values should not add devices"
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Logging tests for QM device manager and Wayland client devices."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerLogging:
|
||||
"""Logging functionality tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_file_creation(self, hook_runner, log_checker):
|
||||
"""Test that hook creates log file."""
|
||||
empty_spec = {"annotations": {}, "linux": {}}
|
||||
result = hook_runner.run_hook("qm_device_manager", empty_spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker("qm-device-manager"), "Log should contain hook name"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_content_validation(
|
||||
self, hook_runner, sample_specs, log_checker
|
||||
):
|
||||
"""Test that hook logs contain expected content."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker(
|
||||
"QM Device Manager"
|
||||
), "Log should contain hook description"
|
||||
assert log_checker("completed successfully")
|
||||
|
||||
|
||||
class TestWaylandClientDevicesLogging:
|
||||
"""Logging functionality tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_file_creation(self, hook_runner, log_checker):
|
||||
"""Test that hook creates log file."""
|
||||
empty_spec = {"annotations": {}, "linux": {}}
|
||||
result = hook_runner.run_hook("wayland_client_devices", empty_spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker(
|
||||
"qm-wayland-client-devices"
|
||||
), "Log should contain hook name"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_content_validation(
|
||||
self, hook_runner, sample_specs, log_checker
|
||||
):
|
||||
"""Test that hook logs contain expected content."""
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker(
|
||||
"qm-wayland-client-devices"
|
||||
), "Log should contain hook name"
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Performance tests for QM device manager and Wayland client devices."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerPerformance:
|
||||
"""Performance and timing tests."""
|
||||
|
||||
@pytest.mark.performance
|
||||
def test_hook_performance(self, hook_runner, sample_specs):
|
||||
"""Test that hook executes within reasonable time."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["multiple_devices"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
# Hook should complete within 5 seconds (generous limit)
|
||||
assert (
|
||||
execution_time < 5.0
|
||||
), f"Hook took too long: {execution_time:.2f}s"
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.parametrize("iteration", range(5))
|
||||
def test_multiple_executions_performance(
|
||||
self, hook_runner, sample_specs, iteration
|
||||
):
|
||||
"""Test consistent performance across multiple executions."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed on iteration {iteration}: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
assert (
|
||||
execution_time < 3.0
|
||||
), f"Execution {iteration} too slow: {execution_time:.2f}s"
|
||||
|
||||
|
||||
class TestWaylandClientDevicesPerformance:
|
||||
"""Performance tests for Wayland client devices."""
|
||||
|
||||
@pytest.mark.performance
|
||||
def test_hook_performance(self, hook_runner, sample_specs):
|
||||
"""Test Wayland client devices hook performance."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
assert (
|
||||
execution_time < 3.0
|
||||
), f"Hook took too long: {execution_time:.2f}s"
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.parametrize("iteration", range(3))
|
||||
def test_multiple_executions_performance(
|
||||
self, hook_runner, sample_specs, iteration
|
||||
):
|
||||
"""Test consistent performance across multiple executions."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed on iteration {iteration}: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
assert (
|
||||
execution_time < 2.0
|
||||
), f"Execution {iteration} too slow: {execution_time:.2f}s"
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validation tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerValidation:
|
||||
"""JSON and structure validation tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_output_json_structure(
|
||||
self, hook_runner, sample_specs, oci_spec_validator
|
||||
):
|
||||
"""Test that hook output follows OCI specification structure."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert oci_spec_validator(result.output_spec), "Output spec is invalid"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_device_structure_validation(self, hook_runner, sample_specs):
|
||||
"""Test that device entries have correct structure."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["multiple_devices"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
|
||||
# Check that all devices have required fields
|
||||
required_fields = ["path", "type", "major", "minor"]
|
||||
assert all(
|
||||
field in device for device in devices for field in required_fields
|
||||
), "All devices must have required fields: path, type, major, minor"
|
||||
|
||||
# Check path types
|
||||
assert all(
|
||||
isinstance(d["path"], str) for d in devices
|
||||
), "Path must be str"
|
||||
# Check type values
|
||||
assert all(
|
||||
d["type"] in ["c", "b"] for d in devices
|
||||
), "Type must be c or b"
|
||||
# Check major/minor are integers
|
||||
assert all(
|
||||
isinstance(d["major"], int) for d in devices
|
||||
), "Major must be int"
|
||||
assert all(
|
||||
isinstance(d["minor"], int) for d in devices
|
||||
), "Minor must be int"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resource_structure_validation(self, hook_runner):
|
||||
"""Test that resource entries have correct structure."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "true"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Check devices limit structure
|
||||
devices_limit = (
|
||||
result.output_spec.get("linux", {})
|
||||
.get("resources", {})
|
||||
.get("devices", [])
|
||||
)
|
||||
|
||||
# Validate all device rules have required fields
|
||||
assert all(
|
||||
"allow" in device_rule for device_rule in devices_limit
|
||||
), "All device rules must have 'allow' field"
|
||||
|
||||
assert all(
|
||||
"type" in device_rule for device_rule in devices_limit
|
||||
), "All device rules must have 'type' field"
|
||||
|
||||
assert all(
|
||||
isinstance(device_rule["allow"], bool)
|
||||
for device_rule in devices_limit
|
||||
), "All 'allow' fields must be bool"
|
||||
|
||||
assert all(
|
||||
device_rule["type"] in ["c", "b", "a"]
|
||||
for device_rule in devices_limit
|
||||
), "All device types must be 'c', 'b', or 'a'"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_annotations_validation(
|
||||
self, hook_runner, oci_spec_validator
|
||||
):
|
||||
"""Test validation with empty annotations."""
|
||||
empty_spec = {"annotations": {}, "linux": {}}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", empty_spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert oci_spec_validator(result.output_spec), "Output should be valid"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"malformed_spec",
|
||||
[
|
||||
# Non-string annotation values should be handled gracefully
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": True},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": 123},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": []},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["boolean_value", "integer_value", "list_value"],
|
||||
)
|
||||
def test_malformed_annotation_values(self, hook_runner, malformed_spec):
|
||||
"""Test handling of malformed annotation values."""
|
||||
result = hook_runner.run_hook("qm_device_manager", malformed_spec)
|
||||
# Hook should not crash, but may not process the annotation
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook should handle malformed values: {result.stderr}"
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wayland seat functionality tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerWayland:
|
||||
"""Wayland seat annotation tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"seat_annotation",
|
||||
[
|
||||
"org.containers.qm.wayland.seat",
|
||||
"org.containers.qm.wayland.seat.0",
|
||||
"org.containers.qm.wayland.seat.1",
|
||||
],
|
||||
)
|
||||
def test_wayland_seat_annotation(
|
||||
self, hook_runner, seat_annotation, device_counter
|
||||
):
|
||||
"""Test Wayland seat annotations."""
|
||||
spec = {
|
||||
"annotations": {seat_annotation: "true"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Verify devices and resources are added for seat
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
resource_count = device_counter.count_resources(result.output_spec)
|
||||
|
||||
assert device_count >= 0, "Device count should be non-negative"
|
||||
assert resource_count >= 0, "Resource count should be non-negative"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wayland_seat_with_existing_devices(self, hook_runner):
|
||||
"""Test wayland seat annotation with existing devices."""
|
||||
spec_with_devices = {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "true"},
|
||||
"linux": {
|
||||
"devices": [
|
||||
{
|
||||
"path": "/dev/existing",
|
||||
"type": "c",
|
||||
"major": 1,
|
||||
"minor": 3,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec_with_devices)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should preserve existing devices
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
existing_paths = [d["path"] for d in devices]
|
||||
assert (
|
||||
"/dev/existing" in existing_paths
|
||||
), "Existing device should be preserved"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"spec",
|
||||
[
|
||||
{
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland.seat.0": "true",
|
||||
"org.containers.qm.wayland.seat.1": "true",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland.seat": "seat0",
|
||||
"org.containers.qm.wayland.seat.0": "true",
|
||||
"org.containers.qm.wayland.seat.1": "true",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["multiple_seats", "conflicting_seats"],
|
||||
)
|
||||
def test_multiple_wayland_seats(self, hook_runner, device_counter, spec):
|
||||
"""Test multiple Wayland seat annotations."""
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should process both seats
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
resource_count = device_counter.count_resources(result.output_spec)
|
||||
|
||||
assert device_count >= 0, "Should handle multiple seats"
|
||||
assert resource_count >= 0, "Resource count should be non-negative"
|
|
@ -0,0 +1,238 @@
|
|||
"""Shared test utilities for OCI hooks testing."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import jsonschema
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookResult:
|
||||
"""Result of running an OCI hook."""
|
||||
|
||||
success: bool
|
||||
output_spec: Dict[str, Any]
|
||||
stdout: str
|
||||
stderr: str
|
||||
execution_time: float
|
||||
|
||||
|
||||
class HookRunner:
|
||||
"""Class for running OCI hooks in test environment."""
|
||||
|
||||
def __init__(self, test_env: Dict[str, str]):
|
||||
"""Initialize with test environment variables."""
|
||||
self.test_env = test_env
|
||||
|
||||
def run_hook(
|
||||
self,
|
||||
hook_name: str,
|
||||
input_spec: Dict[str, Any],
|
||||
mock_devices: Optional[Dict[str, str]] = None,
|
||||
) -> HookResult:
|
||||
"""Run a hook with given input spec and return result."""
|
||||
hook_paths = {
|
||||
"qm_device_manager": "../qm-device-manager/oci-qm-device-manager",
|
||||
"wayland_client_devices": (
|
||||
"../wayland-client-devices/oci-qm-wayland-client-devices"
|
||||
),
|
||||
}
|
||||
|
||||
if hook_name not in hook_paths:
|
||||
raise ValueError(f"Unknown hook: {hook_name}")
|
||||
|
||||
hook_path = hook_paths[hook_name]
|
||||
input_json = json.dumps(input_spec)
|
||||
|
||||
# Use provided test environment (including TEST_LOGFILE)
|
||||
env = self.test_env.copy()
|
||||
|
||||
# Add mock device environment variables if provided
|
||||
if mock_devices:
|
||||
for device_type, device_list in mock_devices.items():
|
||||
env_var = f"TEST_MOCK_{device_type.upper()}_DEVICES"
|
||||
env[env_var] = device_list
|
||||
|
||||
start_time = time.time()
|
||||
result = subprocess.run(
|
||||
[hook_path],
|
||||
input=input_json,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
cwd=Path(__file__).parent,
|
||||
check=False,
|
||||
)
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
# Parse output JSON if hook succeeded
|
||||
output_spec = {}
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
try:
|
||||
output_spec = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
# If output is not valid JSON, treat as failure
|
||||
pass
|
||||
|
||||
return HookResult(
|
||||
success=result.returncode == 0,
|
||||
output_spec=output_spec,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
execution_time=execution_time,
|
||||
)
|
||||
|
||||
|
||||
class HookConfigLoader:
|
||||
"""Utility for loading and managing hook configurations."""
|
||||
|
||||
@staticmethod
|
||||
def load_all_hook_configs() -> Dict[str, Dict[str, Any]]:
|
||||
"""Load all hook configuration files."""
|
||||
hook_dir = Path(__file__).parent.parent
|
||||
|
||||
def _load_config(json_file):
|
||||
with open(json_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
return {
|
||||
json_file.stem: {
|
||||
"path": json_file,
|
||||
"config": _load_config(json_file),
|
||||
}
|
||||
for json_file in hook_dir.rglob("*.json")
|
||||
if "oci-qm-" in json_file.name
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_hook_names() -> list:
|
||||
"""Get list of all hook names."""
|
||||
return list(HookConfigLoader.load_all_hook_configs().keys())
|
||||
|
||||
|
||||
class OciSpecValidator:
|
||||
"""JSON schema validator for OCI specifications."""
|
||||
|
||||
# Simplified OCI spec schema for validation
|
||||
OCI_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
},
|
||||
"linux": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"devices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"type": {"type": "string"},
|
||||
"major": {"type": "integer"},
|
||||
"minor": {"type": "integer"},
|
||||
"fileMode": {"type": "integer"},
|
||||
"uid": {"type": "integer"},
|
||||
"gid": {"type": "integer"},
|
||||
},
|
||||
"required": ["path", "type", "major", "minor"],
|
||||
},
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"devices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {"type": "boolean"},
|
||||
"type": {"type": "string"},
|
||||
"major": {"type": "integer"},
|
||||
"minor": {"type": "integer"},
|
||||
"access": {"type": "string"},
|
||||
},
|
||||
"required": [
|
||||
"allow",
|
||||
"type",
|
||||
"major",
|
||||
"minor",
|
||||
"access",
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
"required": ["annotations", "linux"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate(cls, spec: Dict[str, Any]) -> bool:
|
||||
"""Validate an OCI spec against the schema."""
|
||||
try:
|
||||
jsonschema.validate(spec, cls.OCI_SCHEMA)
|
||||
return True
|
||||
except jsonschema.ValidationError:
|
||||
return False
|
||||
|
||||
|
||||
class LogChecker:
|
||||
"""Utility for checking hook log files."""
|
||||
|
||||
def __init__(self, log_file_path: str):
|
||||
"""Initialize with log file path."""
|
||||
self.log_file_path = Path(log_file_path)
|
||||
|
||||
def __call__(self, pattern: str, should_exist: bool = True) -> bool:
|
||||
"""
|
||||
Check if pattern exists in hook log file (backward compatibility).
|
||||
|
||||
Args:
|
||||
pattern: String pattern to search for
|
||||
should_exist: Whether pattern should exist (True) or not (False)
|
||||
|
||||
Returns:
|
||||
True if check passes, False otherwise
|
||||
"""
|
||||
if not self.log_exists():
|
||||
return not should_exist
|
||||
|
||||
try:
|
||||
content = self.log_file_path.read_text(encoding="utf-8")
|
||||
pattern_found = pattern in content
|
||||
return pattern_found if should_exist else not pattern_found
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def log_exists(self) -> bool:
|
||||
"""Check if log file exists."""
|
||||
return self.log_file_path.exists()
|
||||
|
||||
def log_contains(self, text: str) -> bool:
|
||||
"""Check if log contains specific text."""
|
||||
if not self.log_exists():
|
||||
return False
|
||||
content = self.log_file_path.read_text(encoding="utf-8")
|
||||
return text in content
|
||||
|
||||
def get_log_lines(self) -> list:
|
||||
"""Get all log lines as a list."""
|
||||
try:
|
||||
return (
|
||||
self.log_file_path.read_text(encoding="utf-8")
|
||||
.strip()
|
||||
.split("\n")
|
||||
)
|
||||
except OSError:
|
||||
return []
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wayland GPU device tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestWaylandClientDevicesGPU:
|
||||
"""GPU device detection tests for Wayland client containers."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_gpu_annotation_enabled(self, hook_runner, sample_specs):
|
||||
"""Test GPU annotation processes correctly."""
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_value", ["true", "1", "yes", "True", "YES"])
|
||||
def test_gpu_annotation_valid_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with various valid truthy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with value '{gpu_value}': {result.stderr}"
|
||||
|
||||
# For truthy values, devices should be processed
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices >= 0
|
||||
), f"Truthy value '{gpu_value}' should enable GPU processing"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_value", ["false", "0", "no", "False", "NO"])
|
||||
def test_gpu_annotation_invalid_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with various falsy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with falsy value '{gpu_value}': {result.stderr}"
|
||||
|
||||
# For falsy values, no devices should be added
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), f"Falsy value '{gpu_value}' should not add GPU devices"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"gpu_value", ["TrUe", "YeS", "tRuE", "yEs", "TRUE"]
|
||||
)
|
||||
def test_gpu_annotation_case_insensitive_truthy_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with mixed-case truthy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with mixed-case value '{gpu_value}': {result.stderr}"
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert total_devices >= 0, (
|
||||
f"Mixed-case truthy value '{gpu_value}' should enable GPU "
|
||||
"processing"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"gpu_value", ["FaLsE", "nO", "fAlSe", "No", "FALSE"]
|
||||
)
|
||||
def test_gpu_annotation_case_insensitive_falsy_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with mixed-case falsy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with mixed-case value '{gpu_value}': {result.stderr}"
|
||||
|
||||
# For falsy values, no devices should be added
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), f"Mixed-case falsy value '{gpu_value}' should not add GPU devices"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"gpu_value",
|
||||
[
|
||||
"2", # Number other than 1
|
||||
"42", # Random number
|
||||
"maybe", # Unrelated string
|
||||
"truthy", # Similar but not exact
|
||||
"YES_PLEASE", # Contains valid word but extra chars
|
||||
"null", # Null-like value
|
||||
"", # Empty string
|
||||
"[]", # JSON array string
|
||||
"{}", # JSON object string
|
||||
"true false", # Multiple values
|
||||
"TRUE;true", # Semicolon separated
|
||||
"on", # Common boolean-like value
|
||||
"off", # Common boolean-like value
|
||||
"enable", # Enable-like value
|
||||
"disable", # Disable-like value
|
||||
],
|
||||
)
|
||||
def test_gpu_annotation_malformed_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with malformed or unexpected values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
|
||||
# Hook should handle malformed values gracefully without failing
|
||||
assert result.success, (
|
||||
f"Hook should not fail with malformed value '{gpu_value}': "
|
||||
f"{result.stderr}"
|
||||
)
|
||||
|
||||
# Malformed values should be treated as falsy (no GPU devices added)
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), f"Malformed value '{gpu_value}' should not enable GPU processing"
|
||||
|
||||
|
||||
class TestWaylandClientDevicesGPUDevices:
|
||||
"""GPU device tests for Wayland client containers."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_gpu_annotation(
|
||||
self, hook_runner, sample_specs, device_counter, temp_devices
|
||||
):
|
||||
"""Test GPU annotation with temp devices."""
|
||||
device_detector = temp_devices.get_gpu_devices()
|
||||
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices",
|
||||
sample_specs["wayland_gpu"],
|
||||
mock_devices=device_detector.mock_devices("gpu"),
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
assert device_count == device_detector.expected_count, (
|
||||
f"Expected {device_detector.expected_count} GPU devices, "
|
||||
f"got {device_count}"
|
||||
)
|
||||
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
found_paths = [d["path"] for d in devices]
|
||||
|
||||
assert all(
|
||||
temp_path in found_paths for temp_path in device_detector.paths
|
||||
), "Temporary GPU device not found in output"
|
|
@ -0,0 +1,82 @@
|
|||
[tox]
|
||||
envlist = unit
|
||||
skipsdist = true
|
||||
|
||||
[testenv:unit]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest -m unit --tb=short -v {posargs}
|
||||
|
||||
[testenv:integration]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest -m integration --tb=short -v {posargs}
|
||||
|
||||
[testenv:performance]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest -m performance --tb=short -v {posargs}
|
||||
|
||||
[testenv:all]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest --tb=short -v {posargs}
|
||||
|
||||
[testenv:lint]
|
||||
deps =
|
||||
black
|
||||
pylint
|
||||
shellcheck-py
|
||||
allowlist_externals = shfmt
|
||||
commands =
|
||||
black --check --diff --line-length 79 tests/
|
||||
pylint --rcfile=tests/.pylintrc tests/*.py
|
||||
shellcheck qm-device-manager/oci-qm-device-manager
|
||||
shellcheck wayland-client-devices/oci-qm-wayland-client-devices
|
||||
shellcheck lib/common.sh
|
||||
shellcheck lib/device-support.sh
|
||||
shellcheck lib/mock-device-support.sh
|
||||
shfmt -d -i 4 qm-device-manager/oci-qm-device-manager
|
||||
shfmt -d -i 4 wayland-client-devices/oci-qm-wayland-client-devices
|
||||
shfmt -d -i 4 lib/common.sh
|
||||
shfmt -d -i 4 lib/device-support.sh
|
||||
shfmt -d -i 4 lib/mock-device-support.sh
|
||||
|
||||
[testenv:format]
|
||||
deps = black
|
||||
allowlist_externals = shfmt
|
||||
commands =
|
||||
black --line-length 79 tests/
|
||||
shfmt -w -i 4 qm-device-manager/oci-qm-device-manager
|
||||
shfmt -w -i 4 wayland-client-devices/oci-qm-wayland-client-devices
|
||||
shfmt -w -i 4 lib/common.sh
|
||||
shfmt -w -i 4 lib/device-support.sh
|
||||
shfmt -w -i 4 lib/mock-device-support.sh
|
||||
|
||||
[flake8]
|
||||
max-line-length = 79
|
||||
extend-ignore = E203, W503
|
|
@ -0,0 +1,63 @@
|
|||
# Wayland-client-devices
|
||||
|
||||
The wayland-client-devices OCI hook enables containers to access GPU hardware acceleration devices for Wayland client
|
||||
applications that would run as qm's nested containers.
|
||||
|
||||
## Key Functionality
|
||||
|
||||
1. **GPU Device Discovery**: The hook automatically discovers and configures access to GPU render devices when
|
||||
GPU support is enabled, including:
|
||||
|
||||
- GPU hardware acceleration devices
|
||||
|
||||
2. **Container Configuration**: It dynamically modifies the container's OCI configuration to include the necessary
|
||||
GPU device permissions and access controls for Wayland client applications that require hardware acceleration.
|
||||
|
||||
3. **Comprehensive Logging**: All operations are logged to `/var/log/qm-wayland-client-devices.log` for monitoring and debugging.
|
||||
|
||||
## Configuration
|
||||
|
||||
The hook supports the following container annotation:
|
||||
|
||||
- `org.containers.qm.wayland-client.gpu`: Enables GPU device access for Wayland clients. When this annotation is
|
||||
present, the hook will automatically detect and configure access to available render devices in `/dev/dri/`.
|
||||
|
||||
## Logging
|
||||
|
||||
The hook provides detailed logging of all operations:
|
||||
|
||||
- **Log File**: `/var/log/qm-wayland-client-devices.log`
|
||||
- **Log Format**: `YYYY-MM-DD HH:MM:SS - qm-wayland-client-devices - LEVEL - MESSAGE`
|
||||
- **Log Levels**: INFO, WARNING, ERROR
|
||||
|
||||
### Example Log Output
|
||||
|
||||
```text
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Processing Wayland client GPU annotation: true
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Scanning for GPU render devices
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Adding GPU render device: /dev/dri/renderD128
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Found 1 GPU render devices
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Processing 1 GPU devices for Wayland client
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Added GPU device: /dev/dri/renderD128
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Successfully processed all GPU devices for Wayland client
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Total devices in final spec: 1
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - QM Wayland Client Devices hook completed successfully
|
||||
```
|
||||
|
||||
## Example Configuration
|
||||
|
||||
To use the Wayland-client-devices hook, you can create a dropin configuration file to add the necessary annotation
|
||||
to your container:
|
||||
|
||||
1. Create a dropin directory and file:
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/containers/systemd/myapp.container.d/
|
||||
```
|
||||
|
||||
2. Create a dropin file (e.g., `wayland-client.conf`):
|
||||
|
||||
```ini
|
||||
[Container]
|
||||
Annotation=org.containers.qm.wayland-client.gpu=true
|
||||
```
|
|
@ -0,0 +1,123 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source common utilities and appropriate device support library
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# shellcheck source=../lib/common.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
|
||||
if [[ -n "${TEST_LOGFILE:-}" ]]; then
|
||||
# Test mode - use mock device support
|
||||
# shellcheck source=../lib/mock-device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/mock-device-support.sh"
|
||||
else
|
||||
# Normal mode - use standard device support
|
||||
# shellcheck source=../lib/device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/device-support.sh"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
LOGFILE="${TEST_LOGFILE:-/var/log/qm-wayland-client-devices.log}"
|
||||
# shellcheck disable=SC2034 # Used by log() function in device-support.sh
|
||||
HOOK_NAME="qm-wayland-client-devices"
|
||||
|
||||
input="-"
|
||||
CONTAINER_CONFIG=$(cat "$input")
|
||||
|
||||
if [[ -z "$CONTAINER_CONFIG" ]]; then
|
||||
log "ERROR" "Failed to read OCI spec from stdin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GPU_ENABLED=$(echo "$CONTAINER_CONFIG" | jq -r '.annotations["org.containers.qm.wayland-client.gpu"] // empty')
|
||||
|
||||
DEVNAME_LIST=()
|
||||
|
||||
if [[ "$GPU_ENABLED" =~ ^(true|1|yes|True|TRUE|YES)$ ]]; then
|
||||
log "INFO" "Processing Wayland client GPU annotation: $GPU_ENABLED (enabled)"
|
||||
|
||||
# Find all the render devices available
|
||||
RENDER_DEVICES=$(discover_gpu_devices)
|
||||
log "INFO" "Scanning for GPU render devices"
|
||||
|
||||
for RENDER_DEVICE in $RENDER_DEVICES; do
|
||||
DEVNAME_LIST+=("$RENDER_DEVICE")
|
||||
log "INFO" "Adding GPU render device: $RENDER_DEVICE"
|
||||
done
|
||||
|
||||
log "INFO" "Found ${#DEVNAME_LIST[@]} GPU render devices"
|
||||
elif [ -n "$GPU_ENABLED" ]; then
|
||||
log "INFO" "Wayland client GPU annotation present but disabled: $GPU_ENABLED"
|
||||
else
|
||||
log "INFO" "No Wayland client GPU annotation found"
|
||||
fi
|
||||
|
||||
# Iterate over the DEVNAME_LIST to include the required information in the CONTAINER_CONFIG
|
||||
if [ ${#DEVNAME_LIST[@]} -gt 0 ]; then
|
||||
log "INFO" "Processing ${#DEVNAME_LIST[@]} GPU devices for Wayland client"
|
||||
|
||||
for DEVICE in "${DEVNAME_LIST[@]}"; do
|
||||
if ! jq -e ".linux.devices[] | select(.path == \"$DEVICE\")" <<<"$CONTAINER_CONFIG" >/dev/null 2>&1; then
|
||||
# Get device info using device support library
|
||||
if ! device_info=$(get_device_info "$DEVICE"); then
|
||||
log "WARNING" "Failed to get device info for $DEVICE, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse device info: type:major:minor:file_mode:uid:gid
|
||||
IFS=':' read -r dev_type major minor filemode uid gid <<<"$device_info"
|
||||
|
||||
NEW_DEVICE=$(jq -n --arg path "$DEVICE" \
|
||||
--arg dev_type "$dev_type" \
|
||||
--arg major "$major" \
|
||||
--arg minor "$minor" \
|
||||
--arg filemode "$filemode" \
|
||||
--arg uid "$uid" \
|
||||
--arg gid "$gid" \
|
||||
'{
|
||||
"path": $path,
|
||||
"type": $dev_type,
|
||||
"major": $major|tonumber,
|
||||
"minor": $minor|tonumber,
|
||||
"fileMode": $filemode|tonumber,
|
||||
"uid": $uid|tonumber,
|
||||
"gid": $gid|tonumber,
|
||||
}')
|
||||
|
||||
NEW_DEV_RESOURCE=$(jq -n \
|
||||
--arg dev_type "$dev_type" \
|
||||
--arg major "$major" \
|
||||
--arg minor "$minor" \
|
||||
'{
|
||||
"allow": true,
|
||||
"type": $dev_type,
|
||||
"major": $major|tonumber,
|
||||
"minor": $minor|tonumber,
|
||||
"access": "rwm"
|
||||
}')
|
||||
|
||||
CONTAINER_CONFIG=$(jq ".linux.devices += [$NEW_DEVICE]" <<<"$CONTAINER_CONFIG")
|
||||
CONTAINER_CONFIG=$(jq ".linux.resources.devices += [$NEW_DEV_RESOURCE]" <<<"$CONTAINER_CONFIG")
|
||||
log "INFO" "Added GPU device: $DEVICE"
|
||||
else
|
||||
log "INFO" "GPU device already exists in spec: $DEVICE"
|
||||
fi
|
||||
done
|
||||
|
||||
log "INFO" "Successfully processed all GPU devices for Wayland client"
|
||||
else
|
||||
log "INFO" "No GPU devices to process for Wayland client"
|
||||
fi
|
||||
|
||||
# Initialize log file directory
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
touch "$LOGFILE"
|
||||
|
||||
# Count total devices in final spec
|
||||
total_devices=$(echo "$CONTAINER_CONFIG" | jq '.linux.devices | length // 0')
|
||||
log "INFO" "Total devices in final spec: $total_devices"
|
||||
log "INFO" "QM Wayland Client Devices hook completed successfully"
|
||||
|
||||
echo "$CONTAINER_CONFIG" | jq .
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"hook": {
|
||||
"path": "/usr/libexec/oci/hooks.d/oci-qm-wayland-client-devices"
|
||||
},
|
||||
"when": {
|
||||
"annotations": {
|
||||
"^org\\.containers\\.qm\\.wayland-client\\..*$": "^.*$"
|
||||
}
|
||||
},
|
||||
"stages": ["precreate"]
|
||||
}
|
2
qm.te
2
qm.te
|
@ -1,4 +1,4 @@
|
|||
policy_module(qm, 0.8)
|
||||
policy_module(qm, 0.9)
|
||||
|
||||
gen_require(`
|
||||
attribute container_file_type;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
Name: qm-mount-bind-dvb
|
||||
Version: 0
|
||||
Release: 1%{?dist}
|
||||
Summary: Drop-in configuration for QM containers to mount bind /dev/dvb
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-dvb-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
Requires: qm >= %{version}
|
||||
|
||||
%description
|
||||
This subpackage installs a drop-in configuration for QM containers to mount bind `/dev/dvb`.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-dvb-%{version}
|
||||
|
||||
%install
|
||||
# Create the directory for drop-in configurations
|
||||
install -d %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d
|
||||
|
||||
# Install the dvb drop-in configuration file
|
||||
install -m 644 %{_builddir}/qm-dvb-%{version}/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_dvb.conf \
|
||||
%{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_dvb.conf
|
||||
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md SECURITY.md
|
||||
%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_dvb.conf
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Added dvb mount bind drop-in configuration.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
Name: qm-mount-bind-input
|
||||
Version: 0
|
||||
Release: 1%{?dist}
|
||||
Summary: Drop-in configuration for QM containers to mount bind input devices
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-input-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
Requires: qm >= %{version}
|
||||
|
||||
%description
|
||||
This sub-package installs drop-in configurations for QM containers to mount bind input devices.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-input-%{version}
|
||||
|
||||
%install
|
||||
# Create the directory for drop-in configurations
|
||||
install -d %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d
|
||||
|
||||
# Install the input drop-in configuration file
|
||||
install -m 644 %{_builddir}/qm-input-%{version}/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_input.conf \
|
||||
%{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_input.conf
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_input.conf
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Added input mount bind drop-in configuration.
|
|
@ -0,0 +1,120 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
# Define path macro for QM rootfs
|
||||
%global qm_rootfs_prefix /usr/lib/qm/rootfs
|
||||
|
||||
Name: qm-oci-hooks
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: OCI hooks for QM containers
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-oci-hooks-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
# Runtime dependencies
|
||||
Requires: qm >= %{version}
|
||||
Requires: jq
|
||||
|
||||
%description
|
||||
This subpackage provides OCI hooks for QM containers that enable dynamic
|
||||
device access and Wayland display server integration. The hooks are installed
|
||||
both on the host system and inside the QM rootfs to support nested containers.
|
||||
|
||||
Included hooks:
|
||||
- qm-device-manager: Dynamic device mounting based on container annotations
|
||||
- wayland-client-devices: GPU hardware acceleration for Wayland clients
|
||||
|
||||
The hooks are available in two locations:
|
||||
- Host system: /usr/libexec/oci/hooks.d/ and /usr/share/containers/oci/hooks.d/
|
||||
- QM rootfs: /usr/lib/qm/rootfs/usr/libexec/oci/hooks.d/ and /usr/lib/qm/rootfs/usr/share/containers/oci/hooks.d/
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-oci-hooks-%{version}
|
||||
|
||||
%build
|
||||
# No build required for OCI hooks
|
||||
|
||||
%install
|
||||
# Create OCI hook directories
|
||||
install -d %{buildroot}%{_libexecdir}/oci/hooks.d
|
||||
install -d %{buildroot}%{_libexecdir}/oci/lib
|
||||
install -d %{buildroot}%{_datadir}/containers/oci/hooks.d
|
||||
|
||||
# Note: QM rootfs directories (/usr/lib/qm/rootfs/*) are handled by ghost directories in main qm package
|
||||
# We only need to create the specific directories we're installing into
|
||||
install -d %{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d
|
||||
install -d %{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/lib
|
||||
install -d %{buildroot}%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d
|
||||
|
||||
# Install QM Device Manager hook
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager \
|
||||
%{buildroot}%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager.json \
|
||||
%{buildroot}%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager.json \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
|
||||
# Install Wayland Client Devices hook
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices \
|
||||
%{buildroot}%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices.json \
|
||||
%{buildroot}%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices.json \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
|
||||
# Install common libraries
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/common.sh \
|
||||
%{buildroot}%{_libexecdir}/oci/lib/common.sh
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/device-support.sh \
|
||||
%{buildroot}%{_libexecdir}/oci/lib/device-support.sh
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/common.sh \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/common.sh
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/device-support.sh \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/device-support.sh
|
||||
|
||||
# Create documentation directory and install component-specific README files with unique names
|
||||
install -d %{buildroot}%{_docdir}/qm-oci-hooks
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/README.md \
|
||||
%{buildroot}%{_docdir}/qm-oci-hooks/README-qm-device-manager.md
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/README.md \
|
||||
%{buildroot}%{_docdir}/qm-oci-hooks/README-wayland-client-devices.md
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc CODE-OF-CONDUCT.md README.md SECURITY.md
|
||||
%{_docdir}/qm-oci-hooks/README-qm-device-manager.md
|
||||
%{_docdir}/qm-oci-hooks/README-wayland-client-devices.md
|
||||
|
||||
# OCI hook executables and libraries (host system)
|
||||
%dir %{_libexecdir}/oci
|
||||
%dir %{_libexecdir}/oci/hooks.d
|
||||
%dir %{_libexecdir}/oci/lib
|
||||
%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
%{_libexecdir}/oci/lib/common.sh
|
||||
%{_libexecdir}/oci/lib/device-support.sh
|
||||
|
||||
# OCI hook configurations (host system)
|
||||
%dir %{_datadir}/containers/oci
|
||||
%dir %{_datadir}/containers/oci/hooks.d
|
||||
%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
|
||||
# OCI hook executables and libraries (QM rootfs for nested containers)
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/common.sh
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/device-support.sh
|
||||
|
||||
# OCI hook configurations (QM rootfs for nested containers)
|
||||
%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Initial release of consolidated QM OCI hooks package
|
21
rpm/qm.spec
21
rpm/qm.spec
|
@ -64,7 +64,6 @@ BuildRequires: selinux-policy >= %_selinux_policy_version
|
|||
BuildRequires: selinux-policy-devel >= %_selinux_policy_version
|
||||
BuildRequires: bluechi-selinux
|
||||
|
||||
Requires: iptables
|
||||
Requires: parted
|
||||
Requires: containers-common
|
||||
Requires: selinux-policy >= %_selinux_policy_version
|
||||
|
@ -90,6 +89,20 @@ automatically isolated from the host. If developers need to further
|
|||
isolate there applications from other processes in the QM they should
|
||||
use container tools like Podman.
|
||||
|
||||
###################
|
||||
### qmctl ###
|
||||
###################
|
||||
|
||||
%package ctl
|
||||
Summary: QM service controller command line tool
|
||||
Requires: %{name} = %{version}-%{release}
|
||||
Requires: python3 >= 3.9
|
||||
|
||||
%description ctl
|
||||
QM is a containerized environment for running Quality Management software.
|
||||
This package contains the service controller command line tool for managing
|
||||
and interacting with QM containers and services.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n %{name}-%{version}
|
||||
sed -i 's/^install: man all/install:/' Makefile
|
||||
|
@ -163,6 +176,12 @@ fi
|
|||
%ghost %dir %{_installscriptdir}/rootfs
|
||||
%ghost %{_installscriptdir}/rootfs/*
|
||||
|
||||
%files ctl
|
||||
%doc README.md
|
||||
%license LICENSE
|
||||
%{_bindir}/qmctl
|
||||
%{_mandir}/man1/qmctl.*
|
||||
|
||||
%changelog
|
||||
%if %{defined autochangelog}
|
||||
%autochangelog
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
Name: qm-mount-bind-radio
|
||||
Version: 0
|
||||
Release: 1%{?dist}
|
||||
Summary: Drop-in configuration for QM containers to mount bind /dev/radio
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-radio-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
Requires: qm >= %{version}
|
||||
|
||||
%description
|
||||
This subpackage installs a drop-in configuration for QM containers to mount bind `/dev/radio`.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-radio-%{version}
|
||||
|
||||
%install
|
||||
# Create the directory for drop-in configurations
|
||||
install -d %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d
|
||||
# Install the KVM drop-in configuration file
|
||||
install -m 644 %{_builddir}/qm-radio-%{version}/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_radio.conf \
|
||||
%{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_radio.conf
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md SECURITY.md
|
||||
%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_radio.conf
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Added radio mount bind drop-in configuration.
|
||||
|
|
@ -13,6 +13,7 @@ Source0: %{url}/archive/qm-sound-%{version}.tar.gz
|
|||
BuildArch: noarch
|
||||
|
||||
Requires: qm >= %{version}
|
||||
Requires: qm-oci-hooks
|
||||
|
||||
%description
|
||||
This subpackage installs a drop-in configuration for QM containers,
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
Name: qm-mount-bind-tty7
|
||||
Version: 0
|
||||
Release: 1%{?dist}
|
||||
Summary: Drop-in configuration for QM containers to mount bind /dev/tty7
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-tty7-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
Requires: qm >= %{version}
|
||||
|
||||
%description
|
||||
This subpackage installs a drop-in configuration for QM containers to mount bind `/dev/tty7`.
|
||||
`/dev/tty7` is typically associated with the virtual terminal running the GUI session on Linux systems.
|
||||
This configuration is useful when graphical applications require access to the host’s GUI display server.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-tty7-%{version}
|
||||
|
||||
%build
|
||||
# No build required for configuration files
|
||||
|
||||
%install
|
||||
# Create the directory for drop-in configurations
|
||||
install -d %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d
|
||||
|
||||
# Install the KVM drop-in configuration file
|
||||
install -m 644 %{_builddir}/qm-tty7-%{version}/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_tty7.conf \
|
||||
%{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_tty7.conf
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md SECURITY.md
|
||||
%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_tty7.conf
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Added drop-in configuration to mount bind /dev/tty7.
|
|
@ -1,34 +0,0 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
Name: qm-mount-bind-ttyUSB0
|
||||
Version: 0
|
||||
Release: 1%{?dist}
|
||||
Summary: Drop-in configuration for QM containers to mount bind ttyUSB0
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-ttyUSB0-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
Requires: qm >= %{version}
|
||||
|
||||
%description
|
||||
This sub-package installs drop-in configurations for QM containers to mount bind ttyUSB0.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-ttyUSB0-%{version}
|
||||
|
||||
%install
|
||||
# Create the directory for drop-in configurations
|
||||
install -d %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d
|
||||
|
||||
# Install the ttyusb0 drop-in configuration file
|
||||
install -m 644 %{_builddir}/qm-ttyUSB0-%{version}/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_ttyUSB0.conf %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_ttyUSB0.conf
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_ttyUSB0.conf
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Added ttyUSB0 mount bind drop-in configuration.
|
|
@ -12,6 +12,7 @@ Source0: %{url}/archive/qm-windowmanager-%{version}.tar.gz
|
|||
BuildArch: noarch
|
||||
|
||||
Requires: qm >= %{version}
|
||||
Requires: qm-oci-hooks
|
||||
|
||||
%description
|
||||
This sub-package installs an experimental window manager for the QM environment.
|
||||
|
|
56
setup
56
setup
|
@ -20,7 +20,7 @@ AGENT_HOSTNAME="$(hostname)"
|
|||
AGENTCONF="/etc/bluechi/agent.conf.d/agent.conf"
|
||||
QM_CONTAINER_IDS=1000000000:1500000000
|
||||
CONTAINER_IDS=2500000000:1500000000
|
||||
PACKAGES_TO_INSTALL="selinux-policy-targeted podman systemd bluechi-agent procps-ng iptables-nft jq"
|
||||
PACKAGES_TO_INSTALL="selinux-policy-targeted podman systemd procps-ng iptables-nft jq hostname"
|
||||
|
||||
# RHEL kernel uses iptables-nft, not iptables-legacy, the tool should
|
||||
# make sure this package is removed.
|
||||
|
@ -195,9 +195,13 @@ install() {
|
|||
dnf --installroot "${ROOTFS}" remove "${PACKAGES_TO_REMOVE}" -y
|
||||
fi
|
||||
|
||||
# check if "${ROOTFS}"/etc/yum.repos.d
|
||||
if [ -z "$(find "${ROOTFS}"/etc/yum.repos.d -mindepth 1 -maxdepth 1)" ]; then
|
||||
cat > "${ROOTFS}/etc/yum.repos.d/autosd.repo" << EOF
|
||||
# check if "${ROOTFS}"/etc/yum.repos.d/
|
||||
if test ! -d "${ROOTFS}"/etc/yum.repos.d ; then
|
||||
mkdir -p -Z "${ROOTFS}"/etc/yum.repos.d
|
||||
fi
|
||||
# check if "${ROOTFS}"/etc/yum.repos.d/autosd.repo
|
||||
if test ! -f "${ROOTFS}"/etc/yum.repos.d/autosd.repo; then
|
||||
cat > "${ROOTFS}"/etc/yum.repos.d/autosd.repo << EOF
|
||||
[autosd]
|
||||
name=Automotive-Sig \$releasever
|
||||
baseurl=https://autosd.sig.centos.org/AutoSD-9/nightly/repos/AutoSD/compose/AutoSD/\$basearch/os
|
||||
|
@ -206,6 +210,49 @@ gpgcheck=0
|
|||
EOF
|
||||
fi
|
||||
|
||||
# Add libkrun & bluechi after rootfs created with repo files in qm
|
||||
if [ "$ID" == "centos" ]; then
|
||||
# check if "${ROOTFS}"/etc/yum.repos.d/libkrun.repo
|
||||
if ! test -f "${ROOTFS}"/etc/yum.repos.d/libkrun.repo; then
|
||||
cat > "${ROOTFS}"/etc/yum.repos.d/libkrun.repo << EOF
|
||||
[copr:copr.fedorainfracloud.org:group_centos-automotive-sig:libkrun]
|
||||
name=Copr repo for libkrun owned by @centos-automotive-sig
|
||||
baseurl=https://download.copr.fedorainfracloud.org/results/@centos-automotive-sig/libkrun/centos-stream-\$releasever-\$basearch/
|
||||
type=rpm-md
|
||||
skip_if_unavailable=True
|
||||
gpgcheck=1
|
||||
gpgkey=https://download.copr.fedorainfracloud.org/results/@centos-automotive-sig/libkrun/pubkey.gpg
|
||||
repo_gpgcheck=0
|
||||
enabled=1
|
||||
enabled_metadata=1
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# check if "${ROOTFS}"/etc/yum.repos.d/bluechi.repo
|
||||
if ! test -f "${ROOTFS}"/etc/yum.repos.d/bluechi.repo; then
|
||||
cat > "${ROOTFS}"/etc/yum.repos.d/bluechi.repo << EOF
|
||||
[copr:copr.fedorainfracloud.org:group_centos-automotive-sig:bluechi-snapshot]
|
||||
name=Copr repo for bluechi-snapshot owned by @centos-automotive-sig
|
||||
baseurl=https://download.copr.fedorainfracloud.org/results/@centos-automotive-sig/bluechi-snapshot/fedora-\$releasever-\$basearch/
|
||||
type=rpm-md
|
||||
skip_if_unavailable=True
|
||||
gpgcheck=1
|
||||
gpgkey=https://download.copr.fedorainfracloud.org/results/@centos-automotive-sig/bluechi-snapshot/pubkey.gpg
|
||||
repo_gpgcheck=0
|
||||
enabled=1
|
||||
enabled_metadata=1
|
||||
EOF
|
||||
|
||||
if [ "$ID" == "centos" ]; then
|
||||
# set bluchi-repo to centos-stream
|
||||
sed -i "s|fedora\-|centos-stream\-|" "${ROOTFS}/etc/yum.repos.d/bluechi.repo"
|
||||
fi
|
||||
fi
|
||||
|
||||
cmd_dnf_install="dnf -y install --releasever=${VERSION_ID} --setopt=reposdir=${ROOTFS}/etc/yum.repos.d --installroot ${ROOTFS} libkrun crun-krun bluechi-agent ${EXTRA_FLAG}"
|
||||
${cmd_dnf_install}
|
||||
|
||||
dnf -y update --installroot "${ROOTFS}"
|
||||
rm -rf "${ROOTFS}"/etc/selinux/targeted/contexts/files/file_contexts/*
|
||||
|
||||
|
@ -309,6 +356,7 @@ if [ "${REMOVE_QM_ROOTFS}" == "Y" ]; then
|
|||
# Get the one path below, i.e: /usr/lib/qm instead /usr/lib/qm/rootfs
|
||||
path_qm_rootfs=$(${QM_ROOTFS_TOOL} | sed 's|/[^/]*$||')
|
||||
rm -rf "${path_qm_rootfs}"
|
||||
rm -rf "${RWETCFS}" "${RWVARFS}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
SPECFILE_SUBPACKAGE_DVB ?= ${ROOTDIR}/rpm/dvb/dvb.spec
|
||||
PACKAGE_NAME = qm-mount-bind-dvb
|
||||
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM dvb package
|
||||
cd $(ROOTDIR) && tar cvz \
|
||||
--dereference \
|
||||
--transform s/qm/qm-dvb-${VERSION}/ \
|
||||
-f /tmp/qm-dvb-${VERSION}.tar.gz \
|
||||
../qm/README.md \
|
||||
../qm/SECURITY.md \
|
||||
../qm/LICENSE \
|
||||
../qm/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_dvb.conf
|
||||
cd $(ROOTDIR) && mv /tmp/qm-dvb-${VERSION}.tar.gz ./rpm
|
||||
|
||||
.PHONY: dvb
|
||||
dvb: dist ## - Creates a local RPM package, useful for development
|
||||
cd $(ROOTDIR) && mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
cd $(ROOTDIR) && tools/version-update -v ${VERSION}
|
||||
cd $(ROOTDIR) && cp ./rpm/qm-dvb-${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE_SUBPACKAGE_DVB}
|
||||
if [ ! -f ${RPM_TOPDIR}/RPMS/noarch/${PACKAGE_NAME}-${VERSION}*.noarch.rpm ]; then \
|
||||
echo "rpmbuild failed to build: ${PACKAGE_NAME}"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
SPECFILE_SUBPACKAGE_INPUT ?= ${ROOTDIR}/rpm/input/input.spec
|
||||
PACKAGE_NAME = qm-mount-bind-input
|
||||
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM input package
|
||||
cd $(ROOTDIR) && tar cvz \
|
||||
--dereference \
|
||||
--transform s/qm/qm-input-${VERSION}/ \
|
||||
-f /tmp/qm-input-${VERSION}.tar.gz \
|
||||
../qm/README.md \
|
||||
../qm/SECURITY.md \
|
||||
../qm/LICENSE \
|
||||
../qm/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_input.conf
|
||||
cd $(ROOTDIR) && mv /tmp/qm-input-${VERSION}.tar.gz ./rpm
|
||||
|
||||
.PHONY: input
|
||||
input: dist ## - Creates a local RPM package, useful for development
|
||||
cd $(ROOTDIR) && mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
cd $(ROOTDIR) && tools/version-update -v ${VERSION}
|
||||
cd $(ROOTDIR) && cp ./rpm/qm-input-${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE_SUBPACKAGE_INPUT}
|
||||
if [ ! -f ${RPM_TOPDIR}/RPMS/noarch/${PACKAGE_NAME}-${VERSION}*.noarch.rpm ]; then \
|
||||
echo "rpmbuild failed to build: ${PACKAGE_NAME}"; \
|
||||
exit 1; \
|
||||
fi
|
|
@ -7,7 +7,7 @@ ARCHS=("amd64" "aarch64")
|
|||
IMAGE_NAME="kvm"
|
||||
TAG="latest"
|
||||
MANIFEST_NAME="${IMAGE_NAME}-manifest:${TAG}"
|
||||
FEDORA_USER_PASSWORD=${FEDORA_USER_PASSWORD:-$(openssl rand -base64 12)}
|
||||
FEDORA_USER_PASSWORD=${FEDORA_USER_PASSWORD:-fedora}
|
||||
|
||||
#IMG_REG=quay.io
|
||||
#IMG_ORG=qm-images
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
SPECFILE_SUBPACKAGE_OCI_HOOKS ?= ${ROOTDIR}/rpm/oci-hooks/qm-oci-hooks.spec
|
||||
PACKAGE_NAME = qm-oci-hooks
|
||||
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM OCI hooks package
|
||||
cd $(ROOTDIR) && tar cvz \
|
||||
--dereference \
|
||||
--transform 's|subsystems/qm-oci-hooks/Makefile|Makefile|' \
|
||||
--transform 's|rpm/oci-hooks/qm-oci-hooks.spec|qm-oci-hooks.spec|' \
|
||||
--transform 's|qm|qm-oci-hooks-${VERSION}|' \
|
||||
-f /tmp/qm-oci-hooks-${VERSION}.tar.gz \
|
||||
../qm/rpm/oci-hooks/qm-oci-hooks.spec \
|
||||
../qm/subsystems/qm-oci-hooks/Makefile \
|
||||
../qm/tools/version-update \
|
||||
../qm/VERSION \
|
||||
../qm/README.md \
|
||||
../qm/SECURITY.md \
|
||||
../qm/LICENSE \
|
||||
../qm/CODE-OF-CONDUCT.md \
|
||||
../qm/oci-hooks/qm-device-manager/oci-qm-device-manager \
|
||||
../qm/oci-hooks/qm-device-manager/oci-qm-device-manager.json \
|
||||
../qm/oci-hooks/qm-device-manager/README.md \
|
||||
../qm/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices \
|
||||
../qm/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices.json \
|
||||
../qm/oci-hooks/wayland-client-devices/README.md \
|
||||
../qm/oci-hooks/lib/common.sh \
|
||||
../qm/oci-hooks/lib/device-support.sh
|
||||
cd $(ROOTDIR) && mv /tmp/qm-oci-hooks-${VERSION}.tar.gz ./rpm
|
||||
|
||||
.PHONY: qm-oci-hooks
|
||||
qm-oci-hooks: dist ## - Creates a local RPM package, useful for development
|
||||
@echo ${VERSION}
|
||||
cd $(ROOTDIR) && mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
cd $(ROOTDIR) && tools/version-update -v ${VERSION}
|
||||
cd $(ROOTDIR) && cp ./rpm/qm-oci-hooks-${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE_SUBPACKAGE_OCI_HOOKS}
|
||||
if [ ! -f ${RPM_TOPDIR}/RPMS/noarch/${PACKAGE_NAME}-${VERSION}*.noarch.rpm ]; then \
|
||||
echo "rpmbuild failed to build: ${PACKAGE_NAME}"; \
|
||||
exit 1; \
|
||||
fi
|
|
@ -1,32 +0,0 @@
|
|||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
SPECFILE_SUBPACKAGE_RADIO ?= ${ROOTDIR}/rpm/radio/radio.spec
|
||||
PACKAGE_NAME = qm-mount-bind-radio
|
||||
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM radio package
|
||||
cd $(ROOTDIR) && tar cvz \
|
||||
--dereference \
|
||||
--transform s/qm/qm-radio-${VERSION}/ \
|
||||
-f /tmp/qm-radio-${VERSION}.tar.gz \
|
||||
../qm/README.md \
|
||||
../qm/SECURITY.md \
|
||||
../qm/LICENSE \
|
||||
../qm/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_radio.conf
|
||||
cd $(ROOTDIR) && mv /tmp/qm-radio-${VERSION}.tar.gz ./rpm
|
||||
|
||||
|
||||
.PHONY: radio
|
||||
radio: dist ## - Creates a local RPM package, useful for development
|
||||
cd $(ROOTDIR) && mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
cd $(ROOTDIR) && tools/version-update -v ${VERSION}
|
||||
cd $(ROOTDIR) && cp ./rpm/qm-radio-${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE_SUBPACKAGE_RADIO}
|
||||
if [ ! -f ${RPM_TOPDIR}/RPMS/noarch/${PACKAGE_NAME}-${VERSION}*.noarch.rpm ]; then \
|
||||
echo "rpmbuild failed to build: ${PACKAGE_NAME}"; \
|
||||
exit 1; \
|
||||
fi
|
|
@ -1,37 +0,0 @@
|
|||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
SPECFILE_SUBPACKAGE_TTY7 ?= ${ROOTDIR}/rpm/tty7/tty7.spec
|
||||
PACKAGE_NAME = qm-mount-bind-tty7
|
||||
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM tty7 package
|
||||
cd $(ROOTDIR) && tar cvz \
|
||||
--dereference \
|
||||
--transform 's|subsystems/tty7/Makefile|Makefile|' \
|
||||
--transform 's|rpm/tty7/tty7.spec|tty7.spec|' \
|
||||
--transform s/qm/qm-tty7-${VERSION}/ \
|
||||
-f /tmp/qm-tty7-${VERSION}.tar.gz \
|
||||
../qm/rpm/tty7/tty7.spec \
|
||||
../qm/subsystems/tty7/Makefile \
|
||||
../qm/tools/version-update \
|
||||
../qm/VERSION \
|
||||
../qm/README.md \
|
||||
../qm/SECURITY.md \
|
||||
../qm/LICENSE \
|
||||
../qm/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_tty7.conf
|
||||
cd $(ROOTDIR) && mv /tmp/qm-tty7-${VERSION}.tar.gz ./rpm
|
||||
|
||||
.PHONY: tty7
|
||||
tty7: dist ## - Creates a local RPM package, useful for development
|
||||
cd $(ROOTDIR) && mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
cd $(ROOTDIR) && tools/version-update -v ${VERSION}
|
||||
cd $(ROOTDIR) && cp ./rpm/qm-tty7-${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE_SUBPACKAGE_TTY7}
|
||||
if [ ! -f ${RPM_TOPDIR}/RPMS/noarch/${PACKAGE_NAME}-${VERSION}*.noarch.rpm ]; then \
|
||||
echo "rpmbuild failed to build: ${PACKAGE_NAME}"; \
|
||||
exit 1; \
|
||||
fi
|
|
@ -1,37 +0,0 @@
|
|||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
SPECFILE_SUBPACKAGE_TTYUSB0 ?= ${ROOTDIR}/rpm/ttyUSB0/ttyUSB0.spec
|
||||
PACKAGE_NAME = qm-mount-bind-ttyUSB0
|
||||
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM ttyUSB0 package
|
||||
cd $(ROOTDIR) && tar cvz \
|
||||
--dereference \
|
||||
--transform s/qm/qm-ttyUSB0-${VERSION}/ \
|
||||
--transform 's|subsystems/ttyUSB0/Makefile|Makefile|' \
|
||||
--transform 's|rpm/ttyUSB0/ttyUSB0.spec|ttyUSB0.spec|' \
|
||||
-f /tmp/qm-ttyUSB0-${VERSION}.tar.gz \
|
||||
../qm/rpm/ttyUSB0/ttyUSB0.spec \
|
||||
../qm/subsystems/ttyUSB0/Makefile \
|
||||
../qm/tools/version-update \
|
||||
../qm/VERSION \
|
||||
../qm/README.md \
|
||||
../qm/SECURITY.md \
|
||||
../qm/LICENSE \
|
||||
../qm/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_ttyUSB0.conf
|
||||
cd $(ROOTDIR) && mv /tmp/qm-ttyUSB0-${VERSION}.tar.gz ./rpm
|
||||
|
||||
.PHONY: ttyUSB0
|
||||
ttyUSB0: dist ## - Creates a local RPM package, useful for development
|
||||
cd $(ROOTDIR) && mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
cd $(ROOTDIR) && tools/version-update -v ${VERSION}
|
||||
cd $(ROOTDIR) && cp ./rpm/qm-ttyUSB0-${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE_SUBPACKAGE_TTYUSB0}
|
||||
if [ ! -f ${RPM_TOPDIR}/RPMS/noarch/${PACKAGE_NAME}-${VERSION}*.noarch.rpm ]; then \
|
||||
echo "rpmbuild failed to build: ${PACKAGE_NAME}"; \
|
||||
exit 1; \
|
||||
fi
|
|
@ -3,7 +3,7 @@
|
|||
# Download basic QM manifest
|
||||
curl -o qm.aib.yml "https://gitlab.com/CentOS/automotive/src/automotive-image-builder/-/raw/main/examples/qm.aib.yml?ref_type=heads"
|
||||
|
||||
USE_QM_COPR="${PACKIT_COPR_PROJECT:-rhcontainerbot/qm}"
|
||||
USE_QM_COPR="${PACKIT_COPR_PROJECT:-@centos-automotive-sig/qm-next}"
|
||||
COPR_URL="https://download.copr.fedorainfracloud.org/results/${USE_QM_COPR}/epel-9-$(uname -m)/"
|
||||
#shellcheck disable=SC2089
|
||||
EXTRA_REPOS='extra_repos=[{"id":"qm_build","baseurl":"'"$COPR_URL"'"}]'
|
||||
|
|
|
@ -45,7 +45,7 @@ install_qm_rpms() {
|
|||
###########################################################################
|
||||
|
||||
if [[ -n "${USE_QM_COPR}" ]]; then
|
||||
USE_QM_COPR="${PACKIT_COPR_PROJECT:-rhcontainerbot/qm}"
|
||||
USE_QM_COPR="${PACKIT_COPR_PROJECT:-@centos-automotive-sig/qm-next}"
|
||||
fi
|
||||
info_message "Installing qm setup rpm"
|
||||
info_message "Installing qm using ${USE_QM_COPR} repo"
|
||||
|
|
|
@ -39,7 +39,7 @@ export BUILD_QM_FROM_GH_URL=""
|
|||
export BUILD_BLUECHI_FROM_GH_URL=""
|
||||
export BRANCH_BLUECHI=""
|
||||
export BRANCH_QM=""
|
||||
export USE_QM_COPR="${PACKIT_COPR_PROJECT:-rhcontainerbot/qm}"
|
||||
export USE_QM_COPR="${PACKIT_COPR_PROJECT:-@centos-automotive-sig/qm-next}"
|
||||
|
||||
# If no additional nodes are required, use 1
|
||||
if [ -z "${NUMBER_OF_NODES}" ]; then
|
||||
|
@ -83,7 +83,7 @@ Usage: ./run-test-e2e [OPTIONS]
|
|||
--branch-qm
|
||||
Specify which branch the GitHub repo will be set. Requires --build-qm-from-gh-url
|
||||
--use-qm-copr
|
||||
Specify to install rpms from rhcontainerbot/qm copr
|
||||
Specify to install rpms from @centos-automotive-sig/qm-next copr
|
||||
--number-of-nodes
|
||||
Specify number of nodes. (default 1)
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ export BUILD_BLUECHI_FROM_GH_URL=""
|
|||
export QM_GH_URL=""
|
||||
export BRANCH_QM=""
|
||||
export SET_QM_PART=""
|
||||
export USE_QM_COPR="${PACKIT_COPR_PROJECT:-rhcontainerbot/qm}"
|
||||
export USE_QM_COPR="${PACKIT_COPR_PROJECT:-@centos-automotive-sig/qm-next}"
|
||||
|
||||
RED='\033[91m'
|
||||
GRN='\033[92m'
|
||||
|
@ -91,7 +91,7 @@ Usage: ./set-ffi-env-e2e [OPTIONS]
|
|||
Specify if disk partition neede for /var/qm needed
|
||||
|
||||
--use-qm-copr
|
||||
Specify to install rpms from rhcontainerbot/qm copr
|
||||
Specify to install rpms from @centos-automotive-sig/qm-next copr
|
||||
|
||||
Examples:
|
||||
|
||||
|
|
|
@ -51,6 +51,13 @@ disk_cleanup() {
|
|||
reload_config() {
|
||||
exec_cmd "systemctl daemon-reload"
|
||||
exec_cmd "systemctl restart qm"
|
||||
# Add verification loop for qm status
|
||||
if timeout 30 bash -c "until systemctl is-active qm; do sleep 1; done"; then
|
||||
info_message "PASS: Service QM is Active."
|
||||
else
|
||||
info_message "FAIL: Service QM is not Active"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
running_container_in_qm() {
|
||||
|
|
|
@ -15,3 +15,5 @@ This test suite do some basic sanity tests for qm to confirm that qm has been in
|
|||
5. Confirm that podman run and exec container in qm with service file successfully.
|
||||
|
||||
6. Confirm that /var partition exist.
|
||||
|
||||
7. Confirm that QM OCI hooks are installed and working properly.
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
summary: Test QM OCI hooks are installed and working
|
||||
test: /bin/bash ./check_qm_oci_hooks_are_ok.sh
|
||||
duration: 10m
|
||||
tier: 0
|
||||
tag: kvm
|
||||
framework: shell
|
||||
id: 7f8e9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/bash -euvx
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source ../e2e/lib/utils
|
||||
|
||||
# Setup OCI hooks directly from source tree (TMT_TREE approach)
|
||||
setup_qm_oci_hooks_from_source() {
|
||||
info_message "setup_qm_oci_hooks_from_source(): Setting up OCI hooks directly from TMT_TREE"
|
||||
|
||||
local hooks_config_dir="/usr/share/containers/oci/hooks.d"
|
||||
local hooks_exec_dir="/usr/libexec/oci/hooks.d"
|
||||
|
||||
# Create directories if they don't exist
|
||||
exec_cmd "mkdir -p $hooks_config_dir"
|
||||
exec_cmd "mkdir -p $hooks_exec_dir"
|
||||
|
||||
# Copy QM Device Manager hook
|
||||
exec_cmd "cp ${TMT_TREE}/oci-hooks/qm-device-manager/oci-qm-device-manager.json $hooks_config_dir/"
|
||||
exec_cmd "cp ${TMT_TREE}/oci-hooks/qm-device-manager/oci-qm-device-manager $hooks_exec_dir/"
|
||||
exec_cmd "chmod +x $hooks_exec_dir/oci-qm-device-manager"
|
||||
|
||||
# Copy Wayland Client Devices hook
|
||||
exec_cmd "cp ${TMT_TREE}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices.json $hooks_config_dir/"
|
||||
exec_cmd "cp ${TMT_TREE}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices $hooks_exec_dir/"
|
||||
exec_cmd "chmod +x $hooks_exec_dir/oci-qm-wayland-client-devices"
|
||||
|
||||
# Copy essential library files (excluding mock support)
|
||||
exec_cmd "mkdir -p $hooks_exec_dir/../lib"
|
||||
exec_cmd "cp ${TMT_TREE}/oci-hooks/lib/common.sh $hooks_exec_dir/../lib/"
|
||||
exec_cmd "cp ${TMT_TREE}/oci-hooks/lib/device-support.sh $hooks_exec_dir/../lib/"
|
||||
|
||||
info_message "setup_qm_oci_hooks_from_source(): All OCI hooks copied successfully from TMT_TREE"
|
||||
}
|
||||
|
||||
# Cleanup function to remove test hooks on exit
|
||||
cleanup_oci_hooks() {
|
||||
info_message "cleanup_oci_hooks(): Cleaning up test OCI hooks"
|
||||
|
||||
local hooks_config_dir="/usr/share/containers/oci/hooks.d"
|
||||
local hooks_exec_dir="/usr/libexec/oci/hooks.d"
|
||||
|
||||
# Remove test hooks
|
||||
rm -f "$hooks_config_dir/oci-qm-device-manager.json" || true
|
||||
rm -f "$hooks_config_dir/oci-qm-wayland-client-devices.json" || true
|
||||
|
||||
rm -f "$hooks_exec_dir/oci-qm-device-manager" || true
|
||||
rm -f "$hooks_exec_dir/oci-qm-wayland-client-devices" || true
|
||||
|
||||
# Clean up library files
|
||||
rm -rf "$hooks_exec_dir/../lib" || true
|
||||
|
||||
# Clean up test containers
|
||||
local test_containers=("qm-oci-hooks-device-test")
|
||||
for test_container in "${test_containers[@]}"; do
|
||||
systemctl stop "$test_container" 2>/dev/null || true
|
||||
rm -f "/etc/containers/systemd/${test_container}.container" || true
|
||||
done
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
trap cleanup_oci_hooks EXIT
|
||||
|
||||
# Verify QM OCI hooks are working properly
|
||||
check_qm_oci_hooks_are_ok(){
|
||||
info_message "check_qm_oci_hooks_are_ok(): Starting OCI hooks sanity test"
|
||||
|
||||
# Setup hooks directly from source tree
|
||||
setup_qm_oci_hooks_from_source
|
||||
|
||||
# Validate hook JSON configurations
|
||||
info_message "check_qm_oci_hooks_are_ok(): Validating hook JSON configurations"
|
||||
|
||||
local hooks_config_dir="/usr/share/containers/oci/hooks.d"
|
||||
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
for hook_json in "$hooks_config_dir"/*.json; do
|
||||
if [[ -f "$hook_json" ]]; then
|
||||
if jq . "$hook_json" > /dev/null 2>&1; then
|
||||
info_message "check_qm_oci_hooks_are_ok(): Valid JSON: $(basename "$hook_json")"
|
||||
else
|
||||
info_message "FAIL: check_qm_oci_hooks_are_ok(): Invalid JSON: $(basename "$hook_json")"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
info_message "check_qm_oci_hooks_are_ok(): jq not available, skipping JSON validation"
|
||||
fi
|
||||
|
||||
# Test QM Device Manager hook with TTY devices
|
||||
local test_container="qm-oci-hooks-device-test"
|
||||
|
||||
cat > "/tmp/${test_container}.container" << EOF
|
||||
[Unit]
|
||||
Description=OCI Hooks Sanity Test Container
|
||||
After=local-fs.target
|
||||
|
||||
[Container]
|
||||
Image=registry.access.redhat.com/ubi9-minimal:latest
|
||||
Exec=sleep 10
|
||||
Annotation=org.containers.qm.device.ttys=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target default.target
|
||||
EOF
|
||||
|
||||
info_message "check_qm_oci_hooks_are_ok(): Testing quadlet container with OCI hook annotation"
|
||||
|
||||
# Deploy and test the container
|
||||
exec_cmd "cp /tmp/${test_container}.container /etc/containers/systemd/"
|
||||
exec_cmd "systemctl daemon-reload"
|
||||
|
||||
# Start the container and verify it works
|
||||
if exec_cmd "systemctl start $test_container" && sleep 5; then
|
||||
if systemctl is-active "$test_container" > /dev/null; then
|
||||
info_message "check_qm_oci_hooks_are_ok(): Quadlet container with OCI hook annotation started successfully"
|
||||
|
||||
# Check if device manager hook logged activity
|
||||
if [[ -f "/var/log/qm-device-manager.log" ]]; then
|
||||
if tail -20 "/var/log/qm-device-manager.log" | grep -i "ttys\|device" > /dev/null 2>&1; then
|
||||
info_message "check_qm_oci_hooks_are_ok(): OCI hook processing detected in logs"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up test container
|
||||
systemctl stop "$test_container" 2>/dev/null || true
|
||||
rm -f "/etc/containers/systemd/${test_container}.container" || true
|
||||
|
||||
else
|
||||
info_message "FAIL: check_qm_oci_hooks_are_ok(): Quadlet container with OCI hook annotation is not active"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
info_message "FAIL: check_qm_oci_hooks_are_ok(): Quadlet container with OCI hook annotation failed to start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec_cmd "systemctl daemon-reload"
|
||||
info_message "PASS: check_qm_oci_hooks_are_ok(): QM Device Manager hook tested successfully"
|
||||
}
|
||||
|
||||
check_qm_oci_hooks_are_ok
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Licensed under the Apache License, Version 2.0
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -60,26 +60,18 @@ sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/kvm/q
|
|||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/ros2/ros2_rolling.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/sound/sound.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/video/video.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/radio/radio.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/dvb/dvb.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/tty7/tty7.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/input/input.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/ttyUSB0/ttyUSB0.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/windowmanager/windowmanager.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/text2speech/text2speech.spec"
|
||||
sed -i "s/Version: ${PREV_VERSION}$/Version: ${VERSION}/g" "${BASEDIR}/rpm/oci-hooks/qm-oci-hooks.spec"
|
||||
# Otherwise, set the new version.
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/qm.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/kvm/qm-kvm.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/ros2/ros2_rolling.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/sound/sound.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/video/video.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/radio/radio.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/dvb/dvb.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/tty7/tty7.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/input/input.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/ttyUSB0/ttyUSB0.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/windowmanager/windowmanager.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/text2speech/text2speech.spec"
|
||||
sed -i "s/Version: 0$/Version: ${VERSION}/g" "${BASEDIR}/rpm/oci-hooks/qm-oci-hooks.spec"
|
||||
# Execute the changes on the rest of the files.
|
||||
for file in "${FILES[@]}"; do
|
||||
sed -i "s/${PREV_VERSION}/${VERSION}/g" "${BASEDIR}/${file}"
|
||||
|
|
Loading…
Reference in New Issue