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:
parent
af0c683f60
commit
df27327f14
4 changed files with 127 additions and 86 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
36
source/usb/usb_test.go
Normal 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)
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
Loading…
Reference in a new issue