...

Source file src/edge-infra.dev/pkg/sds/devices/agent/deviceclass.go

Documentation: edge-infra.dev/pkg/sds/devices/agent

     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  // deviceNamingMapper contains a map of subsystems to appropriate function
    34  // to fetch and source the friendly name for a device
    35  var deviceNamingMapper = map[string]func(devices.Device) string{
    36  	usbSubsystem:  fetchUSBDeviceName,
    37  	drm.SubSystem: fetchDisplayName,
    38  }
    39  
    40  // startDeviceClassPatchingWorker starts a worker to send patch updates to device class APIs
    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() { // avoid blocking main thread
    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  // devicStates will return a list of friendly device names connected to the device class
   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  // fetchDeviceName will return a friendly device name based on the subsystem of device
   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  // fetchUSBDeviceName attempts to fetch a friendly usb device name
   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  // fetchDisplayName attempts to fetch a friendly display name from edid
   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