Skip to content

Commit 82fed66

Browse files
committed
test(features): add the remaining CDI scenarios
Add the scenarios for read-only and read-write mounts, library parent derivation, device nodes with and without a supplementary group, and a unix socket, with one test per scenario in the cdi suite. Each test delegates to the scenario wrapper, which checks the workload against the expectations recorded in the spec. Add the fixture machinery the scenarios need: a named group for the device, pipe and character device fixture types, and the echo socket, which a systemd service serves on every boot. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 29e9bc4 commit 82fed66

4 files changed

Lines changed: 334 additions & 1 deletion

File tree

‎tests/suites/features/cdi/tests/injection.rs‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,78 @@ use testkit::Context;
1919
async fn mount_under_read_only_baseline(cx: &mut Context) -> testkit::Result {
2020
cdi::scenario(cx, "mount-under-read-only-baseline").await
2121
}
22+
23+
/// A read-only file mounted outside any directory the sandbox baseline
24+
/// grants. The spec's expectations record whether the workload can read it.
25+
#[testkit::test]
26+
async fn read_only_file(cx: &mut Context) -> testkit::Result {
27+
cdi::scenario(cx, "read-only-file").await
28+
}
29+
30+
/// A read-only directory mounted outside the baseline grants. The
31+
/// expectations record whether its marker is readable and whether files can be
32+
/// created in it.
33+
#[testkit::test]
34+
async fn read_only_directory(cx: &mut Context) -> testkit::Result {
35+
cdi::scenario(cx, "read-only-directory").await
36+
}
37+
38+
/// A mount named like a shared library, `libfixture.so.1`, under a directory of
39+
/// its own. The expectations record whether the library's parent directory is
40+
/// granted for the workload.
41+
#[testkit::test]
42+
async fn library_parent(cx: &mut Context) -> testkit::Result {
43+
cdi::scenario(cx, "library-parent").await
44+
}
45+
46+
/// A mount named like a shared library directly under `/`. Granting its parent
47+
/// would grant the whole root, so the expectations record how that is handled.
48+
#[testkit::test]
49+
async fn broad_library_parent(cx: &mut Context) -> testkit::Result {
50+
cdi::scenario(cx, "broad-library-parent").await
51+
}
52+
53+
/// A file the CDI spec mounts read-write. The expectations record whether the
54+
/// workload can read or write it.
55+
#[testkit::test]
56+
async fn writable_file(cx: &mut Context) -> testkit::Result {
57+
cdi::scenario(cx, "writable-file").await
58+
}
59+
60+
/// A directory the CDI spec mounts read-write. The expectations record whether
61+
/// the workload can create files in it.
62+
#[testkit::test]
63+
async fn writable_directory(cx: &mut Context) -> testkit::Result {
64+
cdi::scenario(cx, "writable-directory").await
65+
}
66+
67+
/// A bind mount whose options omit both `ro` and `rw`, so the access it grants
68+
/// is implicit. The expectations record how the workload can use it.
69+
#[testkit::test]
70+
async fn omitted_access_file(cx: &mut Context) -> testkit::Result {
71+
cdi::scenario(cx, "omitted-access-file").await
72+
}
73+
74+
/// A character device node injected from `/dev/null` with read-write
75+
/// permissions. The expectations record whether a non-root workload can write
76+
/// it.
77+
#[testkit::test]
78+
async fn device_node(cx: &mut Context) -> testkit::Result {
79+
cdi::scenario(cx, "device-node").await
80+
}
81+
82+
/// A device node that is mode 0660 and owned by a group only the spec's
83+
/// `additionalGids` grants. The expectations record whether the workload gets
84+
/// that group and can use the device.
85+
#[testkit::test]
86+
async fn device_node_with_gid(cx: &mut Context) -> testkit::Result {
87+
cdi::scenario(cx, "device-node-with-gid").await
88+
}
89+
90+
/// A unix socket mounted on its own, without the directory it lives in. The
91+
/// expectations record that a sibling file in the host directory stays
92+
/// invisible.
93+
#[testkit::test]
94+
async fn unix_socket(cx: &mut Context) -> testkit::Result {
95+
cdi::scenario(cx, "unix-socket").await
96+
}

‎tests/tmachine/ansible/roles/cdi_fixtures/tasks/scenarios.yaml‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
---
5+
- name: Allocate a supplementary group for the CDI device fixture
6+
become: true
7+
ansible.builtin.group:
8+
name: openshell-cdi-fixture
9+
system: true
10+
register: cdi_fixture_group
11+
512
- name: Load named CDI scenarios
613
ansible.builtin.include_vars:
714
file: scenarios.yaml
@@ -45,6 +52,90 @@
4552
loop_control:
4653
label: "{{ item.path }}"
4754

55+
- name: Create scenario fixture named pipes
56+
become: true
57+
ansible.builtin.command:
58+
argv: [mkfifo, "--mode={{ item.mode }}", "{{ cdi_scenario_dir }}/{{ item.path }}"]
59+
creates: "{{ cdi_scenario_dir }}/{{ item.path }}"
60+
loop: "{{ cdi_scenario_fixtures | selectattr('type', 'eq', 'fifo') | list }}"
61+
loop_control:
62+
label: "{{ item.path }}"
63+
64+
- name: Create scenario fixture character devices
65+
become: true
66+
ansible.builtin.command:
67+
argv: [mknod, "{{ cdi_scenario_dir }}/{{ item.path }}", c, "{{ item.major }}", "{{ item.minor }}"]
68+
creates: "{{ cdi_scenario_dir }}/{{ item.path }}"
69+
loop: "{{ cdi_scenario_fixtures | selectattr('type', 'eq', 'chardev') | list }}"
70+
loop_control:
71+
label: "{{ item.path }}"
72+
73+
- name: Set scenario fixture device ownership
74+
become: true
75+
ansible.builtin.file:
76+
path: "{{ cdi_scenario_dir }}/{{ item.path }}"
77+
owner: "{{ item.owner | default('root') }}"
78+
group: "{{ item.group | default('root') }}"
79+
mode: "{{ item.mode }}"
80+
loop: "{{ cdi_scenario_fixtures | selectattr('type', 'in', ['chardev', 'fifo']) | list }}"
81+
loop_control:
82+
label: "{{ item.path }}"
83+
84+
# A service that creates the socket, not a socket unit: on SELinux hosts systemd
85+
# labels a socket it creates from the policy's default for the path, which
86+
# denies /var/lib.
87+
- name: Check for systemd-socket-activate
88+
ansible.builtin.stat:
89+
path: /usr/bin/systemd-socket-activate
90+
register: cdi_socket_activate
91+
92+
- name: Require systemd-socket-activate for the socket fixture
93+
ansible.builtin.assert:
94+
that: [cdi_socket_activate.stat.exists]
95+
fail_msg: systemd-socket-activate is not installed; the socket fixture needs it
96+
97+
- name: Install the socket fixture unit
98+
become: true
99+
ansible.builtin.template:
100+
src: openshell-cdi-echo.service.j2
101+
dest: /etc/systemd/system/openshell-cdi-echo.service
102+
owner: root
103+
group: root
104+
mode: "0644"
105+
106+
- name: Start the persistent socket fixture
107+
block:
108+
- name: Start the socket fixture service
109+
become: true
110+
ansible.builtin.systemd_service:
111+
name: openshell-cdi-echo.service
112+
daemon_reload: true
113+
enabled: true
114+
state: restarted
115+
116+
- name: Wait for the socket fixture
117+
become: true
118+
ansible.builtin.wait_for:
119+
path: "{{ cdi_scenario_dir }}/socket/echo.sock"
120+
state: present
121+
timeout: 15
122+
123+
rescue:
124+
- name: Collect socket fixture diagnostics
125+
become: true
126+
ansible.builtin.shell: |
127+
systemctl status openshell-cdi-echo.service --no-pager -l
128+
journalctl -u openshell-cdi-echo.service --no-pager -n 40
129+
ls -lZd {{ cdi_scenario_dir }}/socket
130+
(ausearch -m avc,user_avc -ts recent 2>&1 || true) | tail -20
131+
changed_when: false
132+
failed_when: false
133+
register: cdi_socket_diagnostics
134+
135+
- name: Report why the socket fixture did not start
136+
ansible.builtin.fail:
137+
msg: "openshell-cdi-echo.service did not start:\n{{ cdi_socket_diagnostics.stdout }}\n{{ cdi_socket_diagnostics.stderr }}"
138+
48139
- name: Generate named CDI scenario devices
49140
become: true
50141
ansible.builtin.template:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. #}
2+
{# SPDX-License-Identifier: Apache-2.0 #}
3+
[Unit]
4+
Description=OpenShell tmachine CDI Unix socket fixture
5+
After=local-fs.target
6+
7+
[Service]
8+
# The socket is created by this process, not by PID 1, and `cat` echoes each
9+
# connection. A zero umask makes the socket connectable by any user.
10+
UMask=0000
11+
ExecStartPre=/bin/rm -f {{ cdi_scenario_dir }}/socket/echo.sock
12+
ExecStart=/usr/bin/systemd-socket-activate --listen={{ cdi_scenario_dir }}/socket/echo.sock --accept /bin/cat
13+
Restart=always
14+
15+
[Install]
16+
WantedBy=multi-user.target

‎tests/tmachine/ansible/roles/cdi_fixtures/vars/scenarios.yaml‎

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
# Fixture `path` is relative to {{ cdi_fixture_root }}/scenarios. Types:
1111
# dir directory (parents must be declared first)
1212
# file regular file with `content`
13-
# Both accept `mode`, `owner` and `group`.
13+
# fifo named pipe
14+
# chardev character device with `major` and `minor`
15+
# All accept `mode`, `owner` and `group`.
1416
#
1517
# `expect` records the behavior the CDI feature tests assert for the scenario,
1618
# and is rendered to {{ cdi_fixture_root }}/scenarios.json for them. It states
@@ -72,3 +74,152 @@ cdi_scenarios:
7274
checks:
7375
- {type: contents, path: /usr/share/tmachine-cdi/baseline-read-only-file, expect: tmachine-cdi-baseline-read-only-file}
7476
- {type: write, path: /usr/share/tmachine-cdi/baseline-read-only-file, expect: denied}
77+
78+
- name: read-only-file
79+
fixtures:
80+
- {path: read-only-file, type: file, content: "tmachine-cdi-read-only-file\n", mode: "0644"}
81+
edits:
82+
mounts:
83+
- hostPath: "{{ cdi_scenario_dir }}/read-only-file"
84+
containerPath: /tmachine-cdi/read-only/file
85+
options: [bind, ro]
86+
expect:
87+
checks:
88+
- {type: filetype, path: /tmachine-cdi/read-only/file, expect: file}
89+
- {type: read, path: /tmachine-cdi/read-only/file, expect: denied}
90+
- {type: write, path: /tmachine-cdi/read-only/file, expect: denied}
91+
92+
- name: read-only-directory
93+
fixtures:
94+
- {path: read-only-directory, type: dir, mode: "0755"}
95+
- {path: read-only-directory/marker, type: file, content: "tmachine-cdi-read-only-directory/marker\n", mode: "0644"}
96+
edits:
97+
mounts:
98+
- hostPath: "{{ cdi_scenario_dir }}/read-only-directory"
99+
containerPath: /tmachine-cdi/read-only/directory
100+
options: [bind, ro]
101+
expect:
102+
checks:
103+
- {type: filetype, path: /tmachine-cdi/read-only/directory, expect: directory}
104+
- {type: read, path: /tmachine-cdi/read-only/directory/marker, expect: denied}
105+
- {type: write, path: /tmachine-cdi/read-only/directory/marker, expect: denied}
106+
- {type: write, path: /tmachine-cdi/read-only/directory/new-file, expect: denied}
107+
108+
# Both library cases bind the same file; they differ in whether its parent
109+
# directory is narrow or the container root.
110+
111+
- name: library-parent
112+
fixtures:
113+
- {path: libfixture.so.1, type: file, content: "tmachine-cdi-libfixture.so.1\n", mode: "0644"}
114+
edits:
115+
mounts:
116+
- hostPath: "{{ cdi_scenario_dir }}/libfixture.so.1"
117+
containerPath: /tmachine-cdi/library/libfixture.so.1
118+
options: [bind, ro]
119+
expect:
120+
checks:
121+
- {type: filetype, path: /tmachine-cdi/library/libfixture.so.1, expect: file}
122+
- {type: read, path: /tmachine-cdi/library/libfixture.so.1, expect: denied}
123+
- {type: write, path: /tmachine-cdi/library/libfixture.so.1, expect: denied}
124+
125+
- name: broad-library-parent
126+
edits:
127+
mounts:
128+
- hostPath: "{{ cdi_scenario_dir }}/libfixture.so.1"
129+
containerPath: /libtmachinefixture.so.1
130+
options: [bind, ro]
131+
expect:
132+
checks:
133+
- {type: filetype, path: /libtmachinefixture.so.1, expect: file}
134+
- {type: read, path: /libtmachinefixture.so.1, expect: denied}
135+
- {type: write, path: /libtmachinefixture.so.1, expect: denied}
136+
137+
- name: writable-file
138+
fixtures:
139+
- {path: writable-file, type: file, content: "tmachine-cdi-original\n", mode: "0666"}
140+
edits:
141+
mounts:
142+
- hostPath: "{{ cdi_scenario_dir }}/writable-file"
143+
containerPath: /tmachine-cdi/writable/file
144+
options: [bind, rw]
145+
expect:
146+
checks:
147+
- {type: filetype, path: /tmachine-cdi/writable/file, expect: file}
148+
- {type: read, path: /tmachine-cdi/writable/file, expect: denied}
149+
- {type: write, path: /tmachine-cdi/writable/file, expect: denied}
150+
151+
- name: writable-directory
152+
fixtures:
153+
- {path: writable-directory, type: dir, mode: "0777"}
154+
edits:
155+
mounts:
156+
- hostPath: "{{ cdi_scenario_dir }}/writable-directory"
157+
containerPath: /tmachine-cdi/writable/directory
158+
options: [bind, rw]
159+
expect:
160+
checks:
161+
- {type: filetype, path: /tmachine-cdi/writable/directory, expect: directory}
162+
- {type: write, path: /tmachine-cdi/writable/directory/probe, expect: denied}
163+
164+
- name: omitted-access-file
165+
edits:
166+
mounts:
167+
- hostPath: "{{ cdi_scenario_dir }}/writable-file"
168+
containerPath: /tmachine-cdi/writable/omitted-access
169+
options: [bind]
170+
expect:
171+
checks:
172+
- {type: filetype, path: /tmachine-cdi/writable/omitted-access, expect: file}
173+
- {type: read, path: /tmachine-cdi/writable/omitted-access, expect: denied}
174+
- {type: write, path: /tmachine-cdi/writable/omitted-access, expect: denied}
175+
176+
- name: device-node
177+
edits:
178+
deviceNodes:
179+
- path: /dev/tmachine-cdi-null
180+
hostPath: /dev/null
181+
permissions: rw
182+
expect:
183+
checks:
184+
- {type: filetype, path: /dev/tmachine-cdi-null, expect: character-device}
185+
- {type: non-root}
186+
- {type: write, path: /dev/tmachine-cdi-null, expect: denied}
187+
188+
# The device is mode 0660 with a group that only `additionalGids` grants.
189+
- name: device-node-with-gid
190+
fixtures:
191+
- {path: group-device, type: chardev, major: 1, minor: 3, mode: "0660", group: openshell-cdi-fixture}
192+
edits:
193+
additionalGids: ["{{ cdi_fixture_group.gid | int }}"]
194+
deviceNodes:
195+
- path: /dev/tmachine-cdi-group-null
196+
hostPath: "{{ cdi_scenario_dir }}/group-device"
197+
permissions: rw
198+
uid: 0
199+
gid: "{{ cdi_fixture_group.gid | int }}"
200+
fileMode: 432
201+
expect:
202+
checks:
203+
- {type: filetype, path: /dev/tmachine-cdi-group-null, expect: character-device}
204+
- {type: non-root}
205+
- {type: in-group-of, path: /dev/tmachine-cdi-group-null}
206+
- {type: write, path: /dev/tmachine-cdi-group-null, expect: denied}
207+
208+
# The echo socket is served by the systemd service installed in scenarios.yaml.
209+
# Its sibling file checks that only the socket path is exposed.
210+
211+
- name: unix-socket
212+
fixtures:
213+
- {path: socket, type: dir, mode: "0755"}
214+
- {path: socket/sibling, type: file, content: "tmachine-cdi-socket/sibling\n", mode: "0644"}
215+
edits:
216+
mounts:
217+
- hostPath: "{{ cdi_scenario_dir }}/socket/echo.sock"
218+
containerPath: /tmachine-cdi/socket/echo.sock
219+
options: [bind]
220+
expect:
221+
checks:
222+
- {type: filetype, path: /tmachine-cdi/socket/echo.sock, expect: socket}
223+
# Only the socket is mounted, so its sibling in the host directory is
224+
# not visible. Connecting to the socket is not checked yet.
225+
- {type: exists, path: /tmachine-cdi/socket/sibling, expect: false}

0 commit comments

Comments
 (0)