Skip to content

Commit 7646b1b

Browse files
committed
Add support for cgroup-only device nodes
This change allows the following device node CDI spec to be considered valid: path: "*" type: "c" major: 195 minor: -1 permissions: "rwm" When this is included in the spec, no device nodes are added to the OCI runtime spec, but the linux resources (cgroups) are updated to allow access to all devices in the same way as the Docker `--device-cgroup-rule` flag. Note that a major or minor number of -1 is equivalent to a * cgroup rule. This is useful when starting a container that needs to be able to create device nodes that do not exist when it is started. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent db0a0da commit 7646b1b

4 files changed

Lines changed: 170 additions & 13 deletions

File tree

SPEC.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Released versions of the spec are available as Git tags.
3434
| v0.8.0 | | Remove .ToOCI() functions from specs-go package. |
3535
| v1.0.0 | | Move minimum version logic to specs-go package. |
3636
| v1.1.0 | | Add `NetDevices` to `ContainerEdits`, `Schemata` and `EnableMonitoring` to `IntelRdt`. Dropped `EnableCMT` and `EnableMBM` fields from `IntelRdt`. |
37+
| v1.2.0 | | Allow wildcard device nodes to be specified. These have a `Path` of `*` and allow for device `Major` and `Minor` values of `-1` which is mapped to the `*` device in the OCI runtime spec. |
3738

3839
*Note*: spec loading fails on unknown fields and when the minimum required version is higher than the version specified in the spec. The minimum required version is determined based on the usage of fields mentioned in the table above. For example the minimum required version is v0.6.0 if the `Annotations` field is used in the spec, but `IntelRdt` is not.
3940
`MinimumRequiredVersion` API can be used to get the minimum required version.
@@ -229,11 +230,11 @@ The `containerEdits` field is referenced in two places in the specification:
229230
The `containerEdits` field has the following definition:
230231
* `env` (array of strings in the format of "VARNAME=VARVALUE", OPTIONAL) describes the environment variables that should be set. These values are appended to the container environment array.
231232
* `deviceNodes` (array of objects, OPTIONAL) describes the device nodes that should be mounted:
232-
* `path` (string, REQUIRED) path of the device within the container.
233+
* `path` (string, REQUIRED) path of the device within the container. As of v1.2.0, if the path is `*` the device node is used to only update the cgroups for the container.
233234
* `hostPath` (string, OPTIONAL) path of the device node on the host. If not specified the value for `path` is used. Added in v0.5.0.
234235
* `type` (string, OPTIONAL) Device type: block, char, etc.
235-
* `major` (int64, OPTIONAL) Device major number.
236-
* `minor` (int64, OPTIONAL) Device minor number.
236+
* `major` (int64, OPTIONAL) Device major number. As of v1.2.0, if the `path` of the device node is `*` the value of `-1` is allowed and represents a group rule matching all device major numbers.
237+
* `minor` (int64, OPTIONAL) Device minor number. As of v1.2.0, if the `path` of the device node is `*` the value of `-1` is allowed and represents a group rule matching all device minor numbers.
237238
* `fileMode` (int64, OPTIONAL) file mode for the device.
238239
* `permissions` (string, OPTIONAL) Cgroups permissions of the device, candidates are one or more of:
239240
* r - allows container to read from the specified device.

pkg/cdi/container-edits.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -328,19 +328,11 @@ type DeviceNode struct {
328328

329329
// Validate a CDI Spec DeviceNode.
330330
func (d *DeviceNode) Validate() error {
331-
validTypes := map[string]struct{}{
332-
"": {},
333-
"b": {},
334-
"c": {},
335-
"u": {},
336-
"p": {},
337-
}
338-
339331
if d.Path == "" {
340332
return errors.New("invalid (empty) device path")
341333
}
342-
if _, ok := validTypes[d.Type]; !ok {
343-
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
334+
if strings.Contains(d.Path, "*") && d.Path != "*" {
335+
return errors.New("invalid device device path; wildcard device path")
344336
}
345337
switch {
346338
case d.Permissions == "":
@@ -350,10 +342,41 @@ func (d *DeviceNode) Validate() error {
350342
d.Path, d.Permissions)
351343
}
352344

345+
switch {
346+
case d.isWildcardDevice():
347+
validTypes := map[string]struct{}{"b": {}, "c": {}}
348+
if _, ok := validTypes[d.Type]; !ok {
349+
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
350+
}
351+
default:
352+
validTypes := map[string]struct{}{
353+
"": {},
354+
"b": {},
355+
"c": {},
356+
"u": {},
357+
"p": {},
358+
}
359+
if _, ok := validTypes[d.Type]; !ok {
360+
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
361+
}
362+
// For a regular device, we also check that we have valid major and
363+
// minor numbers.
364+
if d.Major == -1 {
365+
return fmt.Errorf("wildcard device major requires a wildcard device")
366+
}
367+
if d.Minor == -1 {
368+
return fmt.Errorf("wildcard device minor requires a wildcard device")
369+
}
370+
}
371+
353372
return nil
354373
}
355374

356375
func (d *DeviceNode) addToGenerator(specgen *ocigen.Generator, spec *oci.Spec) error {
376+
if d.isWildcardDevice() {
377+
return d.addAsWildcardDevice(specgen)
378+
}
379+
357380
err := d.fillMissingInfo()
358381
if err != nil {
359382
return err
@@ -389,8 +412,33 @@ func (d *DeviceNode) getAccessString() string {
389412
return d.Permissions
390413
}
391414
}
415+
416+
// isWildcardDevice returns whether the device node represents a "wildcard" device.
417+
// Such devices do not cause device nodes to be created in the container, but
418+
// do update the cgroups to allow device access. A wildcard device always has
419+
// the path specified as "*" and setting major or minor numbers to -1 will add
420+
// a cgroup rule that matches all major (or minor) numbers.
421+
func (d *DeviceNode) isWildcardDevice() bool {
422+
return d.Path == "*"
423+
}
424+
425+
func (d *DeviceNode) addAsWildcardDevice(specgen *ocigen.Generator) error {
426+
if d.Type != "b" && d.Type != "c" {
427+
return fmt.Errorf("wildcard device node not supported for device type %v", d.Type)
428+
}
429+
var major *int64
430+
if d.Major != -1 {
431+
major = &d.Major
432+
}
433+
var minor *int64
434+
if d.Minor != -1 {
435+
minor = &d.Minor
436+
}
437+
specgen.AddLinuxResourcesDevice(true, d.Type, major, minor, d.getAccessString())
438+
392439
return nil
393440
}
441+
394442
// Hook is a CDI Spec Hook wrapper, used for validating hooks.
395443
type Hook struct {
396444
*cdi.Hook

pkg/cdi/container-edits_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,31 @@ func TestValidateContainerEdits(t *testing.T) {
9797
},
9898
},
9999
},
100+
{
101+
name: "valid wildcard device nodes",
102+
edits: &cdi.ContainerEdits{
103+
DeviceNodes: []*cdi.DeviceNode{
104+
{
105+
Path: "*", Type: "c",
106+
},
107+
{
108+
Path: "*", Type: "b",
109+
},
110+
{
111+
Path: "*", Type: "c", Major: -1,
112+
},
113+
{
114+
Path: "*", Type: "b", Minor: -1,
115+
},
116+
{
117+
Path: "*", Type: "c", Permissions: NoPermissions,
118+
},
119+
{
120+
Path: "*", Type: "c", Permissions: "rwm",
121+
},
122+
},
123+
},
124+
},
100125
{
101126
name: "invalid device, empty path",
102127
edits: &cdi.ContainerEdits{
@@ -133,6 +158,54 @@ func TestValidateContainerEdits(t *testing.T) {
133158
},
134159
invalid: true,
135160
},
161+
{
162+
name: "invalid wildcard device, wildcard in path",
163+
edits: &cdi.ContainerEdits{
164+
DeviceNodes: []*cdi.DeviceNode{
165+
{
166+
Path: "/dev/vendor*",
167+
Type: "b",
168+
},
169+
},
170+
},
171+
invalid: true,
172+
},
173+
{
174+
name: "invalid wildcard device, non-wildcard with major",
175+
edits: &cdi.ContainerEdits{
176+
DeviceNodes: []*cdi.DeviceNode{
177+
{
178+
Path: "/dev/vendorctl",
179+
Major: -1,
180+
},
181+
},
182+
},
183+
invalid: true,
184+
},
185+
{
186+
name: "invalid wildcard device, non-wildcard with minor",
187+
edits: &cdi.ContainerEdits{
188+
DeviceNodes: []*cdi.DeviceNode{
189+
{
190+
Path: "/dev/vendorctl",
191+
Minor: -1,
192+
},
193+
},
194+
},
195+
invalid: true,
196+
},
197+
{
198+
name: "invalid wildcard device, with incorrect type",
199+
edits: &cdi.ContainerEdits{
200+
DeviceNodes: []*cdi.DeviceNode{
201+
{
202+
Path: "*",
203+
Type: "p",
204+
},
205+
},
206+
},
207+
invalid: true,
208+
},
136209
{
137210
name: "valid device, with NoPermissions",
138211
edits: &cdi.ContainerEdits{

specs-go/version.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
v080 version = "v0.8.0"
4242
v100 version = "v1.0.0"
4343
v110 version = "v1.1.0"
44+
v120 version = "v1.2.0"
4445

4546
// vEarliest is the earliest supported version of the CDI specification
4647
vEarliest version = v030
@@ -60,6 +61,7 @@ var validSpecVersions = requiredVersionMap{
6061
v080: requiresV080,
6162
v100: requiresV100,
6263
v110: requiresV110,
64+
v120: requiresV120,
6365
}
6466

6567
// ValidateVersion checks whether the specified spec version is valid.
@@ -142,6 +144,39 @@ func (r requiredVersionMap) requiredVersion(spec *Spec) version {
142144
return minVersion
143145
}
144146

147+
// requiresV120 returns true if the spec uses v1.2.0 features.
148+
func requiresV120(spec *Spec) bool {
149+
if spec.ContainerEdits.requiresV120() {
150+
return true
151+
}
152+
153+
for _, d := range spec.Devices {
154+
if d.ContainerEdits.requiresV120() {
155+
return true
156+
}
157+
}
158+
159+
return false
160+
}
161+
162+
// requiresV120 returns true if the specified containers uses v1.2.0 features.
163+
// These are:
164+
// - Wildcard devices that can be used to only inject cgroup rules for a
165+
// container.
166+
func (e ContainerEdits) requiresV120() bool {
167+
for _, dn := range e.DeviceNodes {
168+
switch {
169+
case dn.Path == "*":
170+
return true
171+
case dn.Major == -1:
172+
return true
173+
case dn.Minor == -1:
174+
return true
175+
}
176+
}
177+
return false
178+
}
179+
145180
// requiresV110 returns true if the spec uses v1.1.0 features.
146181
func requiresV110(spec *Spec) bool {
147182
if i := spec.ContainerEdits.IntelRdt; i != nil {

0 commit comments

Comments
 (0)