Skip to content

Commit 5ba79d8

Browse files
committed
Allow to have optional devices
This is useful when you have hardware with different paths and different amounts of devices, where the current config isn't flexible enough. Because if some paths are missing, the whole device isn't available and the containers don't start. And if you configure it as multiple groups, it only mounts one of them, even if multiple devices are available. This now allows to specify multiple paths and mark them as optional. So the device is always available, even if some paths are missing, but it mounts all paths that are available. This doesn't handle changes while containers are running, so if paths only become available later, it doesn't restart containers using them. So this is just to handle different static hardware configurations more flexible.
1 parent b9723c8 commit 5ba79d8

File tree

3 files changed

+134
-2
lines changed

3 files changed

+134
-2
lines changed

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ For example, to expose a CH340 serial converter: {"name": "ch340", "groups": [{"
4747
A "count" can be specified to allow a discovered device group to be scheduled multiple times.
4848
For example, to permit allocation of the FUSE device 10 times: {"name": "fuse", "groups": [{"count": 10, "paths": [{"path": "/dev/fuse"}]}]}
4949
Note: if omitted, "count" is assumed to be 1
50+
An "optional" field can be specified for individual paths to allow containers to start even when some devices are missing.
51+
For example, to expose serial devices that may or may not be present: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyS0", "optional": true}, {"path": "/dev/ttyUSB0", "optional": true}]}]}
5052
If mountPath is a directory, the device will be mounted to the directory with the name of the device.
5153
For example, to expose the serial devices to the /dev/serial directory: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyUSB*", "mountPath": "/dev/serial/"}]}]}`)
5254
flag.String("plugin-directory", v1beta1.DevicePluginPath, "The directory in which to create plugin sockets.")

deviceplugin/path.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"crypto/sha1"
1919
"fmt"
2020
"io/fs"
21+
"math"
2122
"path/filepath"
2223
"sort"
2324
"strconv"
@@ -53,6 +54,11 @@ type Path struct {
5354
// then the group will provide 5 pairs of devices.
5455
// When unspecified, Limit defaults to 1.
5556
Limit uint `json:"limit,omitempty"`
57+
// Optional specifies whether this device path is optional.
58+
// When true, if the device path does not exist, it will be ignored instead of causing an error.
59+
// This allows containers to start even when some devices are not present on the system.
60+
// When unspecified, Optional defaults to false.
61+
Optional bool `json:"optional,omitempty"`
5662
}
5763

5864
// PathType represents the kinds of file-system nodes that can be scheduled.
@@ -71,19 +77,30 @@ func (gp *GenericPlugin) discoverPath() ([]device, error) {
7177
for _, group := range gp.ds.Groups {
7278
paths := make([][]string, len(group.Paths))
7379
var length int
74-
var limitLength int
80+
limitLength := math.MaxInt
81+
// Track which paths have matches (used for optional paths)
82+
pathHasMatches := make([]bool, len(group.Paths))
7583
// Discover all the devices matching each pattern in the Paths group.
7684
for i, path := range group.Paths {
7785
matches, err := fs.Glob(gp.fs, path.Path)
7886
if err != nil {
87+
// If the path is optional and we get an error, skip it
88+
if path.Optional {
89+
continue
90+
}
7991
return nil, err
8092
}
93+
// If no matches found and path is optional, skip it
94+
if len(matches) == 0 && path.Optional {
95+
continue
96+
}
97+
pathHasMatches[i] = true
8198
sort.Strings(matches)
8299
for j := uint(0); j < path.Limit; j++ {
83100
paths[i] = append(paths[i], matches...)
84101
}
85102
// Keep track of the shortest reusable length in the group.
86-
if i == 0 || len(paths[i]) < limitLength {
103+
if len(paths[i]) < limitLength {
87104
limitLength = len(paths[i])
88105
}
89106
// Keep track of the greatest natural length in the group.
@@ -105,6 +122,10 @@ func (gp *GenericPlugin) discoverPath() ([]device, error) {
105122
},
106123
}
107124
for k, path := range group.Paths {
125+
// Skip paths that had no matches (optional and missing)
126+
if !pathHasMatches[k] {
127+
continue
128+
}
108129
mountPath = path.MountPath
109130
if mountPath == "" {
110131
mountPath = paths[k][i]

deviceplugin/path_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,115 @@ func TestDiscoverPaths(t *testing.T) {
200200
},
201201
err: nil,
202202
},
203+
{
204+
name: "optional paths - some missing",
205+
ds: &DeviceSpec{
206+
Name: "serial",
207+
Groups: []*Group{
208+
{
209+
Paths: []*Path{
210+
{
211+
Path: "/dev/ttyS0",
212+
Optional: true,
213+
},
214+
{
215+
Path: "/dev/ttyUSB0",
216+
Optional: true,
217+
},
218+
{
219+
Path: "/dev/ttyUSB1",
220+
Optional: true,
221+
},
222+
},
223+
},
224+
},
225+
},
226+
fs: fstest.MapFS{
227+
"dev/ttyUSB0": {},
228+
},
229+
out: []device{
230+
{
231+
deviceSpecs: []*v1beta1.DeviceSpec{
232+
{
233+
ContainerPath: "/dev/ttyUSB0",
234+
HostPath: "/dev/ttyUSB0",
235+
},
236+
},
237+
},
238+
},
239+
err: nil,
240+
},
241+
{
242+
name: "optional paths - all present",
243+
ds: &DeviceSpec{
244+
Name: "serial",
245+
Groups: []*Group{
246+
{
247+
Paths: []*Path{
248+
{
249+
Path: "/dev/ttyS0",
250+
Optional: true,
251+
},
252+
{
253+
Path: "/dev/ttyUSB0",
254+
Optional: true,
255+
},
256+
{
257+
Path: "/dev/ttyUSB1",
258+
Optional: true,
259+
},
260+
},
261+
},
262+
},
263+
},
264+
fs: fstest.MapFS{
265+
"dev/ttyS0": {},
266+
"dev/ttyUSB0": {},
267+
"dev/ttyUSB1": {},
268+
},
269+
out: []device{
270+
{
271+
deviceSpecs: []*v1beta1.DeviceSpec{
272+
{
273+
ContainerPath: "/dev/ttyS0",
274+
HostPath: "/dev/ttyS0",
275+
},
276+
{
277+
ContainerPath: "/dev/ttyUSB0",
278+
HostPath: "/dev/ttyUSB0",
279+
},
280+
{
281+
ContainerPath: "/dev/ttyUSB1",
282+
HostPath: "/dev/ttyUSB1",
283+
},
284+
},
285+
},
286+
},
287+
err: nil,
288+
},
289+
{
290+
name: "optional paths - all missing",
291+
ds: &DeviceSpec{
292+
Name: "serial",
293+
Groups: []*Group{
294+
{
295+
Paths: []*Path{
296+
{
297+
Path: "/dev/ttyS0",
298+
Optional: true,
299+
},
300+
{
301+
Path: "/dev/ttyUSB0",
302+
Optional: true,
303+
},
304+
},
305+
},
306+
},
307+
},
308+
fs: fstest.MapFS{},
309+
out: []device{},
310+
err: nil,
311+
},
203312
} {
204313
t.Run(tc.name, func(t *testing.T) {
205314
tc.ds.Default()

0 commit comments

Comments
 (0)