1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2024-12-14 11:57:51 +00:00

source/usb: implement FeatureSource

Separate feature discovery and creation of feature labels in the usb
source.

Move usb_utils from source/internal to the source/usb package. Change
the implementation of the UsbID custom rule to utilize the FeatureSource
interface of the usb source.

Also, add minimalist unit test.
This commit is contained in:
Markus Lehtonen 2021-03-03 17:36:43 +02:00
parent af0c683f60
commit df27327f14
4 changed files with 127 additions and 86 deletions

View file

@ -18,7 +18,10 @@ package rules
import ( import (
"fmt" "fmt"
usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
"sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/usb"
) )
// Rule that matches on the following USB device attributes: <class, vendor, device> // Rule that matches on the following USB device attributes: <class, vendor, device>
@ -38,44 +41,39 @@ type UsbIDRule struct {
// Match USB devices on provided USB device attributes // Match USB devices on provided USB device attributes
func (r *UsbIDRule) Match() (bool, error) { func (r *UsbIDRule) Match() (bool, error) {
devAttr := map[string]bool{} devs, ok := source.GetFeatureSource("usb").GetFeatures().Instances[usb.DeviceFeature]
for _, attr := range []string{"class", "vendor", "device", "serial"} { if !ok {
devAttr[attr] = true return false, fmt.Errorf("usb device information not available")
}
allDevs, err := usbutils.DetectUsb(devAttr)
if err != nil {
return false, fmt.Errorf("failed to detect USB devices: %s", err.Error())
} }
for _, classDevs := range allDevs { for _, dev := range devs.Elements {
for _, dev := range classDevs { // match rule on a single device
// match rule on a single device if r.matchDevOnRule(dev) {
if r.matchDevOnRule(dev) { return true, nil
return true, nil
}
} }
} }
return false, nil return false, nil
} }
func (r *UsbIDRule) matchDevOnRule(dev usbutils.UsbDeviceInfo) bool { func (r *UsbIDRule) matchDevOnRule(dev feature.InstanceFeature) bool {
if len(r.Class) == 0 && len(r.Vendor) == 0 && len(r.Device) == 0 { if len(r.Class) == 0 && len(r.Vendor) == 0 && len(r.Device) == 0 {
return false return false
} }
if len(r.Class) > 0 && !in(dev["class"], r.Class) { attrs := dev.Attributes
if len(r.Class) > 0 && !in(attrs["class"], r.Class) {
return false return false
} }
if len(r.Vendor) > 0 && !in(dev["vendor"], r.Vendor) { if len(r.Vendor) > 0 && !in(attrs["vendor"], r.Vendor) {
return false return false
} }
if len(r.Device) > 0 && !in(dev["device"], r.Device) { if len(r.Device) > 0 && !in(attrs["device"], r.Device) {
return false return false
} }
if len(r.Serial) > 0 && !in(dev["serial"], r.Serial) { if len(r.Serial) > 0 && !in(attrs["serial"], r.Serial) {
return false return false
} }

View file

@ -22,12 +22,15 @@ import (
"k8s.io/klog/v2" "k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
"sigs.k8s.io/node-feature-discovery/pkg/utils"
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
) )
const Name = "usb" const Name = "usb"
const DeviceFeature = "device"
type Config struct { type Config struct {
DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"` DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"`
DeviceLabelFields []string `json:"deviceLabelFields,omitempty"` DeviceLabelFields []string `json:"deviceLabelFields,omitempty"`
@ -46,12 +49,14 @@ func newDefaultConfig() *Config {
// usbSource implements the LabelSource and ConfigurableSource interfaces. // usbSource implements the LabelSource and ConfigurableSource interfaces.
type usbSource struct { type usbSource struct {
config *Config config *Config
features *feature.DomainFeatures
} }
// Singleton source instance // Singleton source instance
var ( var (
src usbSource src = usbSource{config: newDefaultConfig()}
_ source.FeatureSource = &src
_ source.LabelSource = &src _ source.LabelSource = &src
_ source.ConfigurableSource = &src _ source.ConfigurableSource = &src
) )
@ -80,7 +85,8 @@ func (s *usbSource) Priority() int { return 0 }
// GetLabels method of the LabelSource interface // GetLabels method of the LabelSource interface
func (s *usbSource) GetLabels() (source.FeatureLabels, error) { func (s *usbSource) GetLabels() (source.FeatureLabels, error) {
features := source.FeatureLabels{} labels := source.FeatureLabels{}
features := s.GetFeatures()
// Construct a device label format, a sorted list of valid attributes // Construct a device label format, a sorted list of valid attributes
deviceLabelFields := []string{} deviceLabelFields := []string{}
@ -89,7 +95,7 @@ func (s *usbSource) GetLabels() (source.FeatureLabels, error) {
configLabelFields[field] = true configLabelFields[field] = true
} }
for _, attr := range usbutils.DefaultUsbDevAttrs { for _, attr := range devAttrs {
if _, ok := configLabelFields[attr]; ok { if _, ok := configLabelFields[attr]; ok {
deviceLabelFields = append(deviceLabelFields, attr) deviceLabelFields = append(deviceLabelFields, attr)
delete(configLabelFields, attr) delete(configLabelFields, attr)
@ -100,42 +106,55 @@ func (s *usbSource) GetLabels() (source.FeatureLabels, error) {
for key := range configLabelFields { for key := range configLabelFields {
keys = append(keys, key) keys = append(keys, key)
} }
klog.Warningf("invalid fields '%v' in deviceLabelFields, ignoring...", keys) klog.Warningf("invalid fields (%s) in deviceLabelFields, ignoring...", strings.Join(keys, ", "))
} }
if len(deviceLabelFields) == 0 { if len(deviceLabelFields) == 0 {
klog.Warningf("no valid fields in deviceLabelFields defined, using the defaults") klog.Warningf("no valid fields in deviceLabelFields defined, using the defaults")
deviceLabelFields = []string{"vendor", "device"} deviceLabelFields = []string{"vendor", "device"}
} }
// Read configured or default labels. Attributes set to 'true' are considered must-have.
deviceAttrs := map[string]bool{}
for _, label := range deviceLabelFields {
deviceAttrs[label] = true
}
devs, err := usbutils.DetectUsb(deviceAttrs)
if err != nil {
return nil, fmt.Errorf("failed to detect USB devices: %s", err.Error())
}
// Iterate over all device classes // Iterate over all device classes
for class, classDevs := range devs { for _, dev := range features.Instances[DeviceFeature].Elements {
attrs := dev.Attributes
class := attrs["class"]
for _, white := range s.config.DeviceClassWhitelist { for _, white := range s.config.DeviceClassWhitelist {
if strings.HasPrefix(class, strings.ToLower(white)) { if strings.HasPrefix(string(class), strings.ToLower(white)) {
for _, dev := range classDevs { devLabel := ""
devLabel := "" for i, attr := range deviceLabelFields {
for i, attr := range deviceLabelFields { devLabel += attrs[attr]
devLabel += dev[attr] if i < len(deviceLabelFields)-1 {
if i < len(deviceLabelFields)-1 { devLabel += "_"
devLabel += "_"
}
} }
features[devLabel+".present"] = true
} }
labels[devLabel+".present"] = true
break
} }
} }
} }
return features, nil return labels, nil
}
// Discover method of the FeatureSource interface
func (s *usbSource) Discover() error {
s.features = feature.NewDomainFeatures()
devs, err := detectUsb()
if err != nil {
return fmt.Errorf("failed to detect USB devices: %s", err.Error())
}
s.features.Instances[DeviceFeature] = feature.NewInstanceFeatures(devs)
utils.KlogDump(3, "discovered usb features:", " ", s.features)
return nil
}
// GetFeatures method of the FeatureSource Interface
func (s *usbSource) GetFeatures() *feature.DomainFeatures {
if s.features == nil {
s.features = feature.NewDomainFeatures()
}
return s.features
} }
func init() { func init() {

36
source/usb/usb_test.go Normal file
View file

@ -0,0 +1,36 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package usb
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
func TestUsbSource(t *testing.T) {
assert.Equal(t, src.Name(), Name)
// Check that GetLabels works with empty features
src.features = feature.NewDomainFeatures()
l, err := src.GetLabels()
assert.Nil(t, err, err)
assert.Empty(t, l)
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package busutils package usb
import ( import (
"fmt" "fmt"
@ -24,12 +24,11 @@ import (
"strings" "strings"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
) )
type UsbDeviceInfo map[string]string var devAttrs = []string{"class", "vendor", "device", "serial"}
type UsbClassMap map[string]UsbDeviceInfo
var DefaultUsbDevAttrs = []string{"class", "vendor", "device", "serial"}
// The USB device sysfs files do not have terribly user friendly names, map // The USB device sysfs files do not have terribly user friendly names, map
// these for consistency with the PCI matcher. // these for consistency with the PCI matcher.
@ -58,26 +57,26 @@ func readSingleUsbAttribute(devPath string, attrName string) (string, error) {
} }
// Read information of one USB device // Read information of one USB device
func readUsbDevInfo(devPath string, deviceAttrSpec map[string]bool) (UsbClassMap, error) { func readUsbDevInfo(devPath string) ([]feature.InstanceFeature, error) {
classmap := UsbClassMap{} instances := make([]feature.InstanceFeature, 0)
info := UsbDeviceInfo{} attrs := make(map[string]string)
for attr := range deviceAttrSpec { for _, attr := range devAttrs {
attrVal, _ := readSingleUsbAttribute(devPath, attr) attrVal, _ := readSingleUsbAttribute(devPath, attr)
if len(attrVal) > 0 { if len(attrVal) > 0 {
info[attr] = attrVal attrs[attr] = attrVal
} }
} }
// USB devices encode their class information either at the device or the interface level. If the device class // USB devices encode their class information either at the device or the interface level. If the device class
// is set, return as-is. // is set, return as-is.
if info["class"] != "00" { if attrs["class"] != "00" {
classmap[info["class"]] = info instances = append(instances, *feature.NewInstanceFeature(attrs))
} else { } else {
// Otherwise, if a 00 is presented at the device level, descend to the interface level. // Otherwise, if a 00 is presented at the device level, descend to the interface level.
interfaces, err := filepath.Glob(devPath + "/*/bInterfaceClass") interfaces, err := filepath.Glob(devPath + "/*/bInterfaceClass")
if err != nil { if err != nil {
return classmap, err return nil, err
} }
// A device may, notably, have multiple interfaces with mixed classes, so we create a unique device for each // A device may, notably, have multiple interfaces with mixed classes, so we create a unique device for each
@ -86,54 +85,43 @@ func readUsbDevInfo(devPath string, deviceAttrSpec map[string]bool) (UsbClassMap
// Determine the interface class // Determine the interface class
attrVal, err := readSingleUsbSysfsAttribute(intf) attrVal, err := readSingleUsbSysfsAttribute(intf)
if err != nil { if err != nil {
return classmap, err return nil, err
} }
attr := UsbDeviceInfo{} subdevAttrs := make(map[string]string, len(attrs))
for k, v := range info { for k, v := range attrs {
attr[k] = v subdevAttrs[k] = v
} }
subdevAttrs["class"] = attrVal
attr["class"] = attrVal instances = append(instances, *feature.NewInstanceFeature(subdevAttrs))
classmap[attrVal] = attr
} }
} }
return classmap, nil return instances, nil
} }
// List available USB devices and retrieve device attributes. // detectUsb detects available USB devices and retrieves their device attributes.
// deviceAttrSpec is a map which specifies which attributes to retrieve. func detectUsb() ([]feature.InstanceFeature, error) {
// a false value for a specific attribute marks the attribute as optional.
// a true value for a specific attribute marks the attribute as mandatory.
// "class" attribute is considered mandatory.
// DetectUsb() will fail if the retrieval of a mandatory attribute fails.
func DetectUsb(deviceAttrSpec map[string]bool) (map[string][]UsbDeviceInfo, error) {
// Unlike PCI, the USB sysfs interface includes entries not just for // Unlike PCI, the USB sysfs interface includes entries not just for
// devices. We work around this by globbing anything that includes a // devices. We work around this by globbing anything that includes a
// valid product ID. // valid product ID.
const devicePath = "/sys/bus/usb/devices/*/idProduct" const devPathGlob = "/sys/bus/usb/devices/*/idProduct"
devInfo := make(map[string][]UsbDeviceInfo) devPaths, err := filepath.Glob(devPathGlob)
devices, err := filepath.Glob(devicePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// "class" is a mandatory attribute, inject it to spec if needed.
deviceAttrSpec["class"] = true
// Iterate over devices // Iterate over devices
for _, device := range devices { devInfo := make([]feature.InstanceFeature, 0)
devMap, err := readUsbDevInfo(filepath.Dir(device), deviceAttrSpec) for _, devPath := range devPaths {
devs, err := readUsbDevInfo(filepath.Dir(devPath))
if err != nil { if err != nil {
klog.Error(err) klog.Error(err)
continue continue
} }
for class, info := range devMap { devInfo = append(devInfo, devs...)
devInfo[class] = append(devInfo[class], info)
}
} }
return devInfo, nil return devInfo, nil