Skip to content

Commit 544056f

Browse files
committed
Rewrite USB logic
We're not actually using libusb for anything, so in this commit we drop all usage of libusb and CGO_ENABLED=1 to make things more portable. To find USB devices we scan /sys/bus/usb/devices/* to find devices, matching on idVendor and idProduct. Signed-off-by: Fredrik Lönnegren <[email protected]>
1 parent f146329 commit 544056f

File tree

7 files changed

+208
-102
lines changed

7 files changed

+208
-102
lines changed

Dockerfile.device-plugin

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# Build the manager binary
2-
FROM registry.opensuse.org/opensuse/tumbleweed:latest AS builder
2+
FROM --platform=$BUILDPLATFORM golang:1.25 AS builder
33
ARG TARGETOS
44
ARG TARGETARCH
55

6-
RUN zypper --non-interactive install --no-recommends go libusb-1_0-devel
76
WORKDIR /workspace
87

98
COPY go.mod go.mod
@@ -12,10 +11,10 @@ RUN go mod download
1211

1312
COPY cmd/device-plugin/main.go cmd/device-plugin/main.go
1413
COPY device-plugin device-plugin
15-
RUN CGO_ENABLED=1 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o deviceplugin cmd/device-plugin/main.go
14+
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o deviceplugin cmd/device-plugin/main.go
1615

17-
FROM registry.opensuse.org/opensuse/tumbleweed:latest
18-
RUN zypper --non-interactive install --no-recommends libusb-1_0-0
19-
WORKDIR /root
16+
FROM gcr.io/distroless/static:latest
17+
WORKDIR /
2018
COPY --from=builder /workspace/deviceplugin .
19+
2120
ENTRYPOINT ["./deviceplugin", "-logtostderr=true", "-stderrthreshold=INFO", "-v=5"]

cmd/device-plugin/main.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"flag"
55
"log/slog"
6+
"os"
67
"time"
78

89
"github.com/kubevirt/device-plugin-manager/pkg/dpm"
@@ -32,10 +33,7 @@ func (l *RadioDeviceLister) Discover(pluginListCh chan dpm.PluginNameList) {
3233

3334
func (l *RadioDeviceLister) NewPlugin(resourceLastName string) dpm.PluginInterface {
3435
if resourceLastName == rtlsdr.ResourceName {
35-
return &rtlsdr.Plugin{
36-
Heartbeat: l.Heartbeat,
37-
RtlSdrs: make(map[string]*rtlsdr.RtlSdrDev),
38-
}
36+
return rtlsdr.NewPlugin(l.Heartbeat, os.DirFS("/"))
3937
}
4038

4139
slog.Error("Unknown resource", "name", resourceLastName)

device-plugin/rtl-sdr/rtl-sdr.go

Lines changed: 39 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,93 +2,92 @@ package rtlsdr
22

33
import (
44
"context"
5-
"fmt"
5+
"io/fs"
66
"log/slog"
77

8-
"github.com/google/gousb"
98
pluginapi "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
109
)
1110

1211
const (
1312
ResourceName = "rtl-sdr"
1413
)
1514

16-
type Plugin struct {
17-
RtlSdrs map[string]*RtlSdrDev
18-
Heartbeat chan bool
15+
type plugin struct {
16+
devices map[string]*UsbDevice
17+
heartbeat chan bool
18+
fsys fs.FS
1919
}
2020

21-
func (p *Plugin) GetDevicePluginOptions(ctx context.Context, e *pluginapi.Empty) (*pluginapi.DevicePluginOptions, error) {
21+
func NewPlugin(heartbeat chan bool, fsys fs.FS) *plugin {
22+
return &plugin{
23+
heartbeat: heartbeat,
24+
fsys: fsys,
25+
devices: make(map[string]*UsbDevice),
26+
}
27+
}
28+
29+
func (p *plugin) GetDevicePluginOptions(ctx context.Context, e *pluginapi.Empty) (*pluginapi.DevicePluginOptions, error) {
2230
return &pluginapi.DevicePluginOptions{}, nil
2331
}
2432

25-
func (p *Plugin) PreStartContainer(ctx context.Context, r *pluginapi.PreStartContainerRequest) (*pluginapi.PreStartContainerResponse, error) {
33+
func (p *plugin) PreStartContainer(ctx context.Context, r *pluginapi.PreStartContainerRequest) (*pluginapi.PreStartContainerResponse, error) {
2634
return &pluginapi.PreStartContainerResponse{}, nil
2735
}
2836

29-
func (p *Plugin) UpdateDevices() error {
30-
rtls, err := ListDevices()
37+
func (p *plugin) UpdateDevices() ([]*pluginapi.Device, error) {
38+
connectedDevs, err := ListUsbDevices(p.fsys)
3139
if err != nil {
3240
slog.Info("Error listing devices", slog.Any("error", err))
33-
return err
41+
return nil, err
3442
}
3543

36-
slog.Info("Found devices", "len", len(rtls))
44+
slog.Info("Found devices", "len", len(connectedDevs))
3745

38-
for _, rtl := range p.RtlSdrs {
39-
rtl.Connected = false
46+
connectedDevsBySerial := map[string]*UsbDevice{}
47+
for i := range connectedDevs {
48+
connectedDevsBySerial[connectedDevs[i].Serial] = connectedDevs[i]
49+
p.devices[connectedDevs[i].Serial] = connectedDevs[i]
4050
}
4151

42-
for i := range rtls {
43-
p.RtlSdrs[rtls[i].SerialNumber] = rtls[i]
44-
}
45-
46-
return nil
47-
}
48-
49-
func (p *Plugin) GetDevices() []*pluginapi.Device {
50-
devs := make([]*pluginapi.Device, len(p.RtlSdrs))
52+
pdevs := make([]*pluginapi.Device, len(p.devices))
5153
i := 0
52-
for _, rtl := range p.RtlSdrs {
53-
devs[i] = &pluginapi.Device{
54-
ID: rtl.SerialNumber,
54+
for _, rtl := range p.devices {
55+
pdevs[i] = &pluginapi.Device{
56+
ID: rtl.Serial,
5557
Health: pluginapi.Unhealthy,
5658
}
5759

58-
if rtl.Connected {
59-
devs[i].Health = pluginapi.Healthy
60+
if _, ok := connectedDevsBySerial[rtl.Serial]; ok {
61+
pdevs[i].Health = pluginapi.Healthy
6062
}
6163

6264
i++
6365
}
6466

65-
return devs
67+
return pdevs, nil
6668
}
6769

68-
func (p *Plugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {
69-
err := p.UpdateDevices()
70+
func (p *plugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {
71+
devs, err := p.UpdateDevices()
7072
if err != nil {
7173
slog.Error("Error listing devices", slog.Any("error", err))
7274
}
7375

74-
devs := p.GetDevices()
75-
7676
err = s.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
7777
if err != nil {
7878
slog.Error("Error sending initial response", slog.Any("error", err))
7979
}
8080

8181
slog.Info("Waiting for updates...")
8282

83-
for range p.Heartbeat {
84-
err = p.UpdateDevices()
83+
for range p.heartbeat {
84+
devs, err = p.UpdateDevices()
8585
if err != nil {
8686
slog.Error("Error reading devices", slog.Any("error", err))
8787
continue
8888
}
8989

90-
devs := p.GetDevices()
91-
slog.Info("Devices updated", "len", len(devs))
90+
slog.Info("Devices updated", slog.Int("len", len(devs)))
9291

9392
err = s.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
9493
if err != nil {
@@ -100,11 +99,11 @@ func (p *Plugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListA
10099
return nil
101100
}
102101

103-
func (p *Plugin) GetPreferredAllocation(context.Context, *pluginapi.PreferredAllocationRequest) (*pluginapi.PreferredAllocationResponse, error) {
102+
func (p *plugin) GetPreferredAllocation(context.Context, *pluginapi.PreferredAllocationRequest) (*pluginapi.PreferredAllocationResponse, error) {
104103
return &pluginapi.PreferredAllocationResponse{}, nil
105104
}
106105

107-
func (p *Plugin) Allocate(ctx context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
106+
func (p *plugin) Allocate(ctx context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
108107
var response pluginapi.AllocateResponse
109108
var car pluginapi.ContainerAllocateResponse
110109
var dev *pluginapi.DeviceSpec
@@ -119,61 +118,12 @@ func (p *Plugin) Allocate(ctx context.Context, r *pluginapi.AllocateRequest) (*p
119118
for _, id := range req.DevicesIDs {
120119
slog.Info("Allocating device", slog.String("ID", id))
121120

122-
dev.HostPath = p.RtlSdrs[id].DevicePath()
123-
dev.ContainerPath = p.RtlSdrs[id].DevicePath()
121+
dev.HostPath = p.devices[id].DevicePath()
122+
dev.ContainerPath = p.devices[id].DevicePath()
124123
}
125124

126125
response.ContainerResponses = append(response.ContainerResponses, &car)
127126
}
128127

129128
return &response, nil
130129
}
131-
132-
type RtlSdrDev struct {
133-
*gousb.Device
134-
135-
SerialNumber string
136-
Connected bool
137-
}
138-
139-
func (r RtlSdrDev) DevicePath() string {
140-
return fmt.Sprintf("/dev/bus/usb/%03d/%03d", r.Device.Desc.Bus, r.Device.Desc.Address)
141-
}
142-
143-
func NewRtlSdrDev(dev *gousb.Device) *RtlSdrDev {
144-
serial, _ := dev.SerialNumber()
145-
146-
return &RtlSdrDev{
147-
Device: dev,
148-
SerialNumber: serial,
149-
Connected: true,
150-
}
151-
}
152-
153-
func ListDevices() ([]*RtlSdrDev, error) {
154-
ctx := gousb.NewContext()
155-
defer func() {
156-
_ = ctx.Close()
157-
}()
158-
159-
devs, err := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool {
160-
return desc.Vendor == 0x0bda
161-
})
162-
for i := range devs {
163-
defer func(i int) {
164-
_ = devs[i].Close()
165-
}(i)
166-
}
167-
168-
if err != nil {
169-
slog.Info("Error open device", slog.Int("len", len(devs)), slog.Any("error", err))
170-
return nil, err
171-
}
172-
173-
devices := make([]*RtlSdrDev, len(devs))
174-
for i := range devs {
175-
devices[i] = NewRtlSdrDev(devs[i])
176-
}
177-
178-
return devices, nil
179-
}

device-plugin/rtl-sdr/usb.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package rtlsdr
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io/fs"
7+
"log/slog"
8+
"path/filepath"
9+
"slices"
10+
"strconv"
11+
)
12+
13+
type UsbDevice struct {
14+
Serial string
15+
VendorID string
16+
ProductID string
17+
Bus int
18+
Dev int
19+
}
20+
21+
// supportedProducts is a map of supported products per vendor ID.
22+
var supportedProducts = map[string][]string{
23+
// 0bda = RealTek, 2838 = RTL2838UHIDIR
24+
"0bda": {"2838"},
25+
}
26+
27+
func (d UsbDevice) DevicePath() string {
28+
return fmt.Sprintf("/dev/bus/usb/%03d/%03d", d.Bus, d.Dev)
29+
}
30+
31+
func ListUsbDevices(fsys fs.FS) ([]*UsbDevice, error) {
32+
const devicesPath = "sys/bus/usb/devices"
33+
34+
entries, err := fs.ReadDir(fsys, devicesPath)
35+
if err != nil {
36+
return nil, fmt.Errorf("failed reading '%s': %w", devicesPath, err)
37+
}
38+
39+
devices := []*UsbDevice{}
40+
for i := range entries {
41+
// Check if it is a directory or symlink
42+
if !entries[i].IsDir() && entries[i].Type()&fs.ModeSymlink == 0 {
43+
slog.Info("Skipping", slog.String("name", entries[i].Name()))
44+
continue
45+
}
46+
47+
path := filepath.Join(devicesPath, entries[i].Name())
48+
dev, err := ReadUsbDevice(fsys, path)
49+
if err != nil {
50+
slog.Debug("failed reading USB device", slog.String("path", path), slog.Any("error", err))
51+
continue
52+
}
53+
54+
if !isSupportedProduct(dev.VendorID, dev.ProductID) {
55+
continue
56+
}
57+
58+
devices = append(devices, dev)
59+
}
60+
61+
return devices, nil
62+
}
63+
64+
func isSupportedProduct(vendorID, productID string) bool {
65+
if prods, ok := supportedProducts[vendorID]; ok {
66+
return slices.Contains(prods, productID)
67+
}
68+
69+
return false
70+
}
71+
72+
func ReadUsbDevice(fsys fs.FS, dir string) (*UsbDevice, error) {
73+
vendorID, err := fs.ReadFile(fsys, filepath.Join(dir, "idVendor"))
74+
if err != nil {
75+
return nil, fmt.Errorf("failed reading '%s/idVendor': %w", dir, err)
76+
}
77+
78+
productID, err := fs.ReadFile(fsys, filepath.Join(dir, "idProduct"))
79+
if err != nil {
80+
return nil, fmt.Errorf("failed reading '%s/idProduct': %w", dir, err)
81+
}
82+
83+
busnum, err := fs.ReadFile(fsys, filepath.Join(dir, "busnum"))
84+
if err != nil {
85+
return nil, fmt.Errorf("failed reading '%s/busnum': %w", dir, err)
86+
}
87+
88+
bus, err := strconv.Atoi(string(bytes.TrimSuffix(busnum, []byte("\n"))))
89+
if err != nil {
90+
return nil, fmt.Errorf("failed converting '%s' to int: %w", busnum, err)
91+
}
92+
93+
devnum, err := fs.ReadFile(fsys, filepath.Join(dir, "devnum"))
94+
if err != nil {
95+
return nil, fmt.Errorf("failed reading '%s/devnum': %w", dir, err)
96+
}
97+
98+
dev, err := strconv.Atoi(string(bytes.TrimSuffix(devnum, []byte("\n"))))
99+
if err != nil {
100+
return nil, fmt.Errorf("failed converting '%s' to int: %w", devnum, err)
101+
}
102+
103+
serial, err := fs.ReadFile(fsys, filepath.Join(dir, "serial"))
104+
if err != nil {
105+
return nil, fmt.Errorf("failed reading '%s/serial': %w", dir, err)
106+
}
107+
108+
return &UsbDevice{
109+
VendorID: string(bytes.TrimSuffix(vendorID, []byte("\n"))),
110+
ProductID: string(bytes.TrimSuffix(productID, []byte("\n"))),
111+
Bus: bus,
112+
Dev: dev,
113+
Serial: string(bytes.TrimSuffix(serial, []byte("\n"))),
114+
}, nil
115+
}

0 commit comments

Comments
 (0)