Skip to content

Commit fb39ac1

Browse files
committed
test(tmachine): assert CDI scenario behavior in a Docker guest
Add a cdi feature suite that creates a sandbox for each CDI scenario by selecting its device explicitly with --gpu 1 and the Docker cdi_devices driver option, then checks what the workload observes or that creation is rejected. The sandbox only reports facts; the test makes every assertion, so a failure names the check that disagreed. Each scenario is its own test. The expectations record the behavior the sandbox has today, loaded from the scenarios.json published by the fixtures role. A OPENSHELL_CDI_CAPTURE mode prints observations instead of asserting, to record that behavior for a new scenario. The test playbook enables caller driver configuration in the installed gateway's active TOML for the run and restores it afterwards. Build the suite's nextest archive in tests/artifacts.nix, expose it as build-artifacts-cdi-tests, and run the suite on the Docker environment with the deb installer in the branch and release matrices. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent f6f179c commit fb39ac1

12 files changed

Lines changed: 769 additions & 1 deletion

File tree

‎.github/workflows/branch-e2e.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ jobs:
275275
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
276276
test-matrix: >-
277277
[
278+
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"cdi"},
278279
{"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"},
279280
{"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"},
280281
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"},

‎.github/workflows/release-dev.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ jobs:
145145
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
146146
test-matrix: >-
147147
[
148+
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"cdi"},
148149
{"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"},
149150
{"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"},
150151
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"},

‎.github/workflows/release-qualification.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ jobs:
105105
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
106106
test-matrix: >-
107107
[
108+
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"cdi"},
108109
{"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"},
109110
{"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"},
110111
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"},

‎TESTING.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,77 @@ nix run .#tmachine -- test fedora-podman-rootless binaries e2e-podman
330330
nix run .#tmachine -- test fedora-podman-rootless binaries driver-podman
331331
```
332332

333+
The `cdi` feature suite tests device injection without GPU hardware in the
334+
`ubuntu-docker-rootful` environment. Build the candidate artifacts, then run:
335+
336+
```shell
337+
nix run .#build-artifacts
338+
nix run .#tmachine -- test ubuntu-docker-rootful deb cdi
339+
```
340+
341+
If candidate binaries, images, and packages are already built, rebuild just the
342+
CDI test archive with `nix run .#build-artifacts-cdi-tests`.
343+
344+
The `binaries` installer also supports this suite. Docker environment setup
345+
creates the named scenario fixtures and a CDI spec that defines one device per
346+
scenario, selected as `tmachine.example/cdi-scenario=<scenario>`. Persistent
347+
spec sources live under `/var/lib/openshell-test-inputs/cdi/specs`; a
348+
systemd-tmpfiles configuration recreates them in `/var/run/cdi` on every VM
349+
boot. This preserves the fixtures across tmachine's setup, installation, and
350+
test disk layers and makes them available to every suite using the Docker
351+
environment. The tests need no GPU hardware.
352+
353+
Each test creates a sandbox that selects one scenario explicitly with `--gpu 1`
354+
and the Docker `cdi_devices` driver option, then checks what the workload
355+
observes or that creation is rejected. Selection by count or default, and the
356+
`nvidia.com/gpu` inventory, are not covered here. Caller driver configuration
357+
is enabled temporarily in the installed gateway's active TOML. The test
358+
playbook restores the configuration and restarts the gateway even after test
359+
failure; the CDI fixtures remain part of the environment. Docker must support
360+
CDI. Branch and release feature-test matrices run this suite on Docker.
361+
362+
| Scenario | Injected resource | Asserted today | Expected with PR #4204 |
363+
| --- | --- | --- | --- |
364+
| `mount-under-read-only-baseline` | File at `/usr/share/tmachine-cdi/baseline-read-only-file`, under a directory the sandbox already makes read-only | Read succeeds, write denied. | Unchanged. |
365+
| `read-only-file` | File at `/tmachine-cdi/read-only/file` | Read and write denied. | Read succeeds outside the baseline grants; write fails. |
366+
| `read-only-directory` | Directory at `/tmachine-cdi/read-only/directory` | Reading its marker and creating files denied. | Reading its marker succeeds; write fails. |
367+
| `library-parent` | Marker named `/tmachine-cdi/library/libfixture.so.1` | Read and write denied. | Resolver derives the library parent; the marker is not an actual shared library. |
368+
| `broad-library-parent` | Marker named `/libtmachinefixture.so.1` | Read and write denied. | Launch rejects expansion to `/`. |
369+
| `writable-file` | Writable file at `/tmachine-cdi/writable/file` | Read and write denied. | Launch rejects it even with exact or ancestor write grants. |
370+
| `writable-directory` | Writable directory at `/tmachine-cdi/writable/directory` | Write denied. | Launch rejects it even with exact or ancestor write grants. |
371+
| `omitted-access-file` | Bind without `ro` or `rw` | Read and write denied. | Effective writable access causes launch rejection. |
372+
| `device-node` | Null character device at `/dev/tmachine-cdi-null` | Non-root, write denied. | Non-root read/write succeeds; an exact authored read-only entry fails launch. |
373+
| `device-node-with-gid` | Group-restricted null device at `/dev/tmachine-cdi-group-null` | Non-root with the device's group, write denied. | The generated supplementary GID permits non-root access. |
374+
| `unix-socket` | Echo socket at `/tmachine-cdi/socket/echo.sock` | Sibling file in the host directory is not visible. | Socket communication succeeds without deriving parent filesystem grants. |
375+
376+
Every scenario also supplies a spec-level environment marker and a read-only
377+
mount at `/usr/share/tmachine-cdi/common/spec-marker`, allowing checks of
378+
spec/device edit merging without depending on derived grants. The device group
379+
is allocated in the disposable guest. A systemd socket unit restores the echo
380+
endpoint after reboot; the existing tmpfiles configuration restores the
381+
scenario spec. Negative scenarios must be selected individually, not combined
382+
with positive cases.
383+
384+
The role publishes each scenario's expectations as `scenarios.json` next to the
385+
fixtures. The `scenario_*` tests load them and assert the behavior recorded in
386+
`vars/scenarios.yaml`, which is the sandbox's behavior today. A change that
387+
alters a scenario's behavior, such as #4204 deriving grants from the spec,
388+
updates those values in the same diff. To record the current behavior, run the
389+
tests with `OPENSHELL_CDI_CAPTURE=1` set for the test process; they then print
390+
what each scenario observed instead of asserting.
391+
392+
To add a scenario, add an entry to `cdi_scenarios` in
393+
`tests/ansible/roles/cdi_fixtures/vars/scenarios.yaml` with the host fixtures
394+
it needs, its CDI edits, and its `expect` (the checks on the workload, or a
395+
rejection), then list it in `cdi_scenarios!` in
396+
`tests/suites/features/cdi/tests/injection.rs`. A new spec file belongs in
397+
`cdi_fixture_spec_files` in the role defaults, which the setup and test
398+
playbooks share.
399+
400+
Directory absence, daemon configuration refresh, and restart tests require
401+
orchestration around these fixtures. Spec projection secrecy is a separate
402+
workload assertion. Hardware CUDA initialization remains outside this suite.
403+
333404
Print the exact tmachine archive selection as a shell `PODMAN_CI_TESTS` array:
334405

335406
```shell

‎flake.nix‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@
169169
type = "app";
170170
program = "${artifacts.testArchives}/bin/build-artifacts-test-archives";
171171
};
172+
build-artifacts-cdi-tests = {
173+
type = "app";
174+
program = "${artifacts.cdiArchive}/bin/build-openshell-cdi-test-archive";
175+
};
172176
build-artifacts-test-images = {
173177
type = "app";
174178
program = "${artifacts.testImages}/bin/build-artifacts-test-images";
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
---
5+
- name: Test CDI injection without GPU hardware
6+
hosts: all
7+
gather_facts: false
8+
vars_files:
9+
- ../../../roles/cdi_fixtures/defaults/main.yaml
10+
vars:
11+
cdi_test_root: /var/lib/openshell-feature-tests/cdi
12+
tasks:
13+
- name: Wait for SSH
14+
ansible.builtin.wait_for_connection:
15+
16+
- name: Resolve installed gateway context
17+
ansible.builtin.include_role:
18+
name: openshell_test_gateway
19+
20+
- name: Require environment CDI specs after boot
21+
ansible.builtin.stat:
22+
path: "/var/run/cdi/{{ item }}"
23+
loop: "{{ cdi_fixture_spec_files }}"
24+
register: cdi_environment_specs
25+
26+
- name: Require CDI fixtures supplied by the Docker environment
27+
ansible.builtin.assert:
28+
that: [item.stat.isreg | default(false)]
29+
fail_msg: "Docker environment did not recreate CDI spec {{ item.item }} after boot"
30+
loop: "{{ cdi_environment_specs.results }}"
31+
32+
- name: Save gateway configuration before enabling caller device selection
33+
become: true
34+
ansible.builtin.slurp:
35+
src: "{{ openshell_test_gateway.config_path }}"
36+
register: cdi_gateway_config
37+
38+
- name: Run CDI tests with caller device selection enabled
39+
block:
40+
- name: Enable caller driver configuration for CDI tests
41+
become: true
42+
ansible.builtin.blockinfile:
43+
path: "{{ openshell_test_gateway.config_path }}"
44+
insertafter: '^\[openshell\.drivers\.docker\]$'
45+
marker: "# {mark} OpenShell CDI test fixture"
46+
block: allow_driver_config = true
47+
48+
- name: Restart gateway with caller device selection enabled
49+
ansible.builtin.include_role:
50+
name: openshell_test_gateway
51+
tasks_from: restart.yaml
52+
53+
- name: Create CDI test directory
54+
become: true
55+
ansible.builtin.file:
56+
path: "{{ cdi_test_root }}"
57+
state: directory
58+
owner: tmachine
59+
group: tmachine
60+
mode: "0700"
61+
62+
- name: Install CDI test archive
63+
become: true
64+
ansible.builtin.unarchive:
65+
src: "{{ openshell_cdi_test_bundle }}"
66+
dest: "{{ cdi_test_root }}"
67+
owner: tmachine
68+
group: tmachine
69+
70+
- name: Run CDI feature tests
71+
ansible.builtin.command:
72+
argv: [cargo-nextest, nextest, run, --archive-file, "{{ cdi_test_root }}/tests.tar.zst", --workspace-remap, "{{ cdi_test_root }}", --no-fail-fast, --no-capture]
73+
environment:
74+
OPENSHELL_GATEWAY: "{{ openshell_test_gateway.name }}"
75+
OPENSHELL_BIN: "{{ openshell_test_cli.stdout }}"
76+
register: cdi_test_result
77+
changed_when: false
78+
failed_when: false
79+
80+
- name: Show CDI test result
81+
ansible.builtin.debug:
82+
var: cdi_test_result
83+
84+
- name: Collect gateway diagnostics after test failure
85+
ansible.builtin.include_role:
86+
name: openshell_test_gateway
87+
tasks_from: journal.yaml
88+
when: cdi_test_result.rc != 0
89+
90+
- name: Require CDI test success
91+
ansible.builtin.assert:
92+
that: [cdi_test_result.rc == 0]
93+
fail_msg: CDI feature tests failed
94+
95+
always:
96+
- name: Restore gateway configuration
97+
become: true
98+
ansible.builtin.copy:
99+
content: "{{ cdi_gateway_config.content | b64decode }}"
100+
dest: "{{ openshell_test_gateway.config_path }}"
101+
102+
- name: Restart gateway with its original configuration
103+
ansible.builtin.include_role:
104+
name: openshell_test_gateway
105+
tasks_from: restart.yaml

‎tests/artifacts.nix‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ let
9898
target = muslToolchain.target;
9999
output = "artifacts/test-archives/${muslToolchain.target}/provider-refresh-keycloak-tests.tar";
100100
};
101+
cdiArchive = mkTestArchive {
102+
name = "openshell-cdi";
103+
workspacePath = "tests/suites/features";
104+
manifestPath = "tests/suites/features/Cargo.toml";
105+
package = "openshell-test-feature-cdi";
106+
target = muslToolchain.target;
107+
output = "artifacts/test-archives/${muslToolchain.target}/openshell-cdi-tests.tar";
108+
};
101109
ociImageArchive = mkTestArchive {
102110
name = "oci-image";
103111
workspacePath = "tests/suites/features";
@@ -205,6 +213,7 @@ rec {
205213
inherit
206214
conformanceTestArchive
207215
providerRefreshKeycloakArchive
216+
cdiArchive
208217
ociImageArchive
209218
podmanDriverArchive
210219
podmanE2eArchive
@@ -304,13 +313,15 @@ rec {
304313
runtimeInputs = [
305314
conformanceTestArchive
306315
providerRefreshKeycloakArchive
316+
cdiArchive
307317
ociImageArchive
308318
podmanDriverArchive
309319
podmanE2eArchive
310320
];
311321
text = ''
312322
build-openshell-conformance-test-archive
313323
build-provider-refresh-keycloak-test-archive
324+
build-openshell-cdi-test-archive
314325
build-oci-image-test-archive
315326
build-podman-driver-test-archive
316327
build-podman-e2e-test-archive

‎tests/config.nix‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ let
170170
openshell_conformance_test_bundle = "../artifacts/test-archives/${muslTarget}/openshell-conformance-tests.tar";
171171
};
172172
}
173+
{
174+
name = "cdi";
175+
playbooks = [ "ansible/playbooks/features/cdi/tests.yaml" ];
176+
inputs = {
177+
openshell_cdi_test_bundle = "../artifacts/test-archives/${muslTarget}/openshell-cdi-tests.tar";
178+
};
179+
}
173180
{
174181
name = "provider-refresh";
175182
playbooks = [ "ansible/playbooks/features/provider-refresh/keycloak.yaml" ];

‎tests/suites/features/Cargo.lock‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/suites/features/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
[workspace]
55
resolver = "2"
6-
members = ["oci-image", "provider-refresh/keycloak"]
6+
members = ["cdi", "oci-image", "provider-refresh/keycloak"]

0 commit comments

Comments
 (0)