Skip to content

Commit fd69275

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 fd69275

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ Usage of generic-device-plugin:
9494
A "count" can be specified to allow a discovered device group to be scheduled multiple times.
9595
For example, to permit allocation of the FUSE device 10 times: {"name": "fuse", "groups": [{"count": 10, "paths": [{"path": "/dev/fuse"}]}]}
9696
Note: if omitted, "count" is assumed to be 1
97+
An "optional" field can be specified for individual paths to allow containers to start even when some devices are missing.
98+
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}]}]}
9799
If mountPath is a directory, the device will be mounted to the directory with the name of the device.
98100
For example, to expose the serial devices to the /dev/serial directory: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyUSB*", "mountPath": "/dev/serial/"}]}]}
99101
--domain string The domain to use when when declaring devices. (default "squat.ai")

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: 19 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,26 @@ 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 {
7987
return nil, err
8088
}
89+
// If no matches found and path is optional, skip it.
90+
if len(matches) == 0 && path.Optional {
91+
continue
92+
}
93+
pathHasMatches[i] = true
8194
sort.Strings(matches)
8295
for j := uint(0); j < path.Limit; j++ {
8396
paths[i] = append(paths[i], matches...)
8497
}
8598
// Keep track of the shortest reusable length in the group.
86-
if i == 0 || len(paths[i]) < limitLength {
99+
if len(paths[i]) < limitLength {
87100
limitLength = len(paths[i])
88101
}
89102
// Keep track of the greatest natural length in the group.
@@ -105,6 +118,10 @@ func (gp *GenericPlugin) discoverPath() ([]device, error) {
105118
},
106119
}
107120
for k, path := range group.Paths {
121+
// Skip paths that had no matches (optional and missing).
122+
if !pathHasMatches[k] {
123+
continue
124+
}
108125
mountPath = path.MountPath
109126
if mountPath == "" {
110127
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)