1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "slices"
9 "strings"
10 "time"
11
12 "github.com/anoopengineer/edidparser/edid"
13 "github.com/hashicorp/go-multierror"
14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15
16 "edge-infra.dev/pkg/k8s/runtime/sap"
17 "edge-infra.dev/pkg/k8s/unstructured"
18 "edge-infra.dev/pkg/lib/kernel/devices"
19 "edge-infra.dev/pkg/lib/kernel/drm"
20 cc "edge-infra.dev/pkg/sds/devices/agent/common"
21 dsv1 "edge-infra.dev/pkg/sds/devices/k8s/apis/v1"
22 "edge-infra.dev/pkg/sds/devices/logger"
23 )
24
25 const (
26 waitReadinessTimeout = time.Second * 5
27 )
28
29 const (
30 usbSubsystem = "usb"
31 )
32
33
34
35 var deviceNamingMapper = map[string]func(devices.Device) string{
36 usbSubsystem: fetchUSBDeviceName,
37 drm.SubSystem: fetchDisplayName,
38 }
39
40
41 func startDeviceClassPatchingWorker(ctx context.Context, resourceManager *sap.ResourceManager, deviceClassQueue chan map[string]*dsv1.DeviceClass) {
42 log := logger.FromContext(ctx)
43 for {
44 select {
45 case <-ctx.Done():
46 return
47 case deviceClasses := <-deviceClassQueue:
48 go func() {
49 if err := applyDeviceClassStatuses(ctx, resourceManager, deviceClasses); err != nil && !strings.Contains(err.Error(), "etcdserver: leader changed") {
50 log.Error("error patching device class", "error", err)
51 }
52 }()
53 }
54 }
55 }
56
57 func applyDeviceClassStatuses(ctx context.Context, resourceManager *sap.ResourceManager, deviceClasses map[string]*dsv1.DeviceClass) error {
58 log := logger.FromContext(ctx)
59 deviceStatuses := &dsv1.DeviceStatuses{
60 TypeMeta: metav1.TypeMeta{
61 Kind: "DeviceStatuses",
62 APIVersion: dsv1.GroupVersion.String(),
63 },
64 ObjectMeta: metav1.ObjectMeta{
65 Name: nodeName(),
66 },
67 Spec: dsv1.DeviceStatusesSpec{
68 Devices: map[string][]dsv1.DeviceState{},
69 DeviceGroups: map[string][]int64{},
70 },
71 }
72
73 deviceNames := map[string]string{}
74 for _, class := range deviceClasses {
75 deviceStates, deviceGroups, err := deviceStates(class)
76 if err != nil {
77 log.Debug("could not find device names", "error", err)
78 }
79 deviceStatuses.Spec.DeviceGroups[class.ClassName()] = deviceGroups
80 deviceStatuses.Spec.Devices[class.ClassName()] = deviceStates
81 for _, devState := range deviceStates {
82 deviceNames[devState.Name] = "connected"
83 }
84 }
85
86 uobj, err := unstructured.ToUnstructured(deviceStatuses)
87 if err != nil {
88 log.Error("error converting object to unstructured object", "error", err)
89 return err
90 }
91
92 changeset, err := resourceManager.Apply(ctx, uobj, sap.ApplyOptions{Force: true, WaitTimeout: waitReadinessTimeout})
93 if err != nil || changeset == nil || slices.Contains([]sap.Action{sap.DeletedAction, sap.UnchangedAction}, sap.Action(changeset.Action)) {
94 return err
95 }
96 log.Log(ctx, logger.LevelTrace, "applied device statuses", "action", changeset.Action, "devices", deviceNames)
97 return nil
98 }
99
100
101 func deviceStates(dc *dsv1.DeviceClass) ([]dsv1.DeviceState, []int64, error) {
102 var errs error
103 deviceStates := []dsv1.DeviceState{}
104 deviceGroups := []int64{cc.DeviceGroupID}
105 for _, device := range dc.DeviceIter() {
106 name := fetchDeviceName(device)
107 if name == "" {
108 errs = multierror.Append(errs, fmt.Errorf("could not find friendly name for device: %s", device.Path()))
109 } else {
110 deviceStates = append(deviceStates, dsv1.DeviceState{Name: name})
111 }
112
113 node, err := device.Node()
114 if err != nil {
115 errs = multierror.Append(errs, fmt.Errorf("failed to fetch device node: %s :%w", device.Path(), err))
116 continue
117 }
118
119 groupOwner, err := node.GroupID()
120 if err != nil {
121 errs = multierror.Append(errs, fmt.Errorf("failed to fetch device node group owner: %s :%w", node.Path(), err))
122 continue
123 }
124 if groupOwner == 0 {
125 continue
126 }
127 deviceGroups = append(deviceGroups, groupOwner)
128 }
129 slices.Sort(deviceGroups)
130 deviceGroups = slices.Compact(deviceGroups)
131 return deviceStates, deviceGroups, errs
132 }
133
134
135 func fetchDeviceName(device devices.Device) string {
136 subsystem, exists, err := device.Property("SUBSYSTEM")
137 if err != nil || !exists {
138 return ""
139 }
140 namingFn, ok := deviceNamingMapper[subsystem]
141 if !ok {
142 return ""
143 }
144 return namingFn(device)
145 }
146
147
148 func fetchUSBDeviceName(device devices.Device) string {
149 productName, exists, err := device.Attribute("product")
150 if !exists || err != nil {
151 return ""
152 }
153 vid, exists, err := device.Attribute("idVendor")
154 if !exists || err != nil {
155 return ""
156 }
157 pid, exists, err := device.Attribute("idProduct")
158 if !exists || err != nil {
159 return ""
160 }
161 friendlyName := fmt.Sprintf("%s:%s %s", vid, pid, productName)
162 return fmtName(friendlyName)
163 }
164
165
166 func fetchDisplayName(device devices.Device) string {
167 edidBytes, err := os.ReadFile(filepath.Join(device.Path(), "edid"))
168 if err != nil || len(edidBytes) == 0 {
169 return ""
170 }
171
172 edid, err := edid.NewEdid(edidBytes)
173 if err != nil || edid == nil {
174 return ""
175 }
176 connectorID := filepath.Base(device.Path())
177 displayName := fmt.Sprintf("%s %s-%d", connectorID, edid.ManufacturerId, edid.ProductCode)
178 return fmtName(displayName)
179 }
180
181 func fmtName(name string) string {
182 name = strings.ReplaceAll(name, "\n", "")
183 name = strings.TrimPrefix(name, "-")
184 name = strings.TrimSuffix(name, "-")
185 name = strings.TrimPrefix(name, " ")
186 name = strings.TrimSuffix(name, " ")
187 return name
188 }
189
View as plain text