mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2024-12-14 11:57:51 +00:00
usb: Add support for USB device discovery
This builds on the PCI support to enable the discovery of USB devices. This is primarily intended to be used for the discovery of Edge-based heterogeneous accelerators that are connected via USB, such as the Coral USB Accelerator and the Intel NCS2 - our main motivation for adding this capability to NFD, and as part of our work in the SODALITE H2020 project. USB devices may define their base class at either the device or interface levels. In the case where no device class is set, the per-device interfaces are enumerated instead. USB devices may furthermore have multiple interfaces, which may or may not use the identical class across each interface. We therefore report device existence for each unique class definition to enable more fine-grained labelling and node selection. The default labelling format includes the class, vendor and device (product) IDs, as follows: feature.node.kubernetes.io/usb-fe_1a6e_089a.present=true As with PCI, a subset of device classes are whitelisted for matching. By default, there are only a subset of device classes under which accelerators tend to be mapped, which is used as the basis for the whitelist. These are: - Video - Miscellaneous - Application Specific - Vendor Specific For those interested in matching other classes, this may be extended by using the UsbId rule provided through the custom source. A full list of class codes is provided by the USB-IF at: https://www.usb.org/defined-class-codes For the moment, owing to a lack of a demonstrable use case, neither the subclass nor the protocol information are exposed. If this becomes necessary, support for these attributes can be trivially added. Signed-off-by: Paul Mundt <paul.mundt@adaptant.io>
This commit is contained in:
parent
1bdf0525f9
commit
c0ea69411b
11 changed files with 412 additions and 9 deletions
48
README.md
48
README.md
|
@ -129,7 +129,7 @@ nfd-worker.
|
||||||
in testing
|
in testing
|
||||||
[Default: ]
|
[Default: ]
|
||||||
--sources=<sources> Comma separated list of feature sources.
|
--sources=<sources> Comma separated list of feature sources.
|
||||||
[Default: cpu,iommu,kernel,local,memory,network,pci,storage,system]
|
[Default: cpu,iommu,kernel,local,memory,network,pci,storage,system,usb]
|
||||||
--no-publish Do not publish discovered features to the
|
--no-publish Do not publish discovered features to the
|
||||||
cluster-local Kubernetes API server.
|
cluster-local Kubernetes API server.
|
||||||
--label-whitelist=<pattern> Regular expression to filter label names to
|
--label-whitelist=<pattern> Regular expression to filter label names to
|
||||||
|
@ -161,6 +161,7 @@ The current set of feature sources are the following:
|
||||||
- PCI
|
- PCI
|
||||||
- Storage
|
- Storage
|
||||||
- System
|
- System
|
||||||
|
- USB
|
||||||
- Local (hooks for user-specific features)
|
- Local (hooks for user-specific features)
|
||||||
|
|
||||||
### Feature labels
|
### Feature labels
|
||||||
|
@ -193,6 +194,7 @@ feature logically has sub-hierarchy, e.g. `sriov.capable` and
|
||||||
"feature.node.kubernetes.io/pci-<device label>.present": "true",
|
"feature.node.kubernetes.io/pci-<device label>.present": "true",
|
||||||
"feature.node.kubernetes.io/storage-<feature-name>": "true",
|
"feature.node.kubernetes.io/storage-<feature-name>": "true",
|
||||||
"feature.node.kubernetes.io/system-<feature name>": "<feature value>",
|
"feature.node.kubernetes.io/system-<feature name>": "<feature value>",
|
||||||
|
"feature.node.kubernetes.io/usb-<device label>.present": "<feature value>",
|
||||||
"feature.node.kubernetes.io/<file name>-<feature name>": "<feature value>"
|
"feature.node.kubernetes.io/<file name>-<feature name>": "<feature value>"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -316,6 +318,28 @@ Matching is done by performing a logical _OR_ between Elements of an Attribute a
|
||||||
each PCI device in the system.
|
each PCI device in the system.
|
||||||
At least one Attribute must be specified. Missing attributes will not partake in the matching process.
|
At least one Attribute must be specified. Missing attributes will not partake in the matching process.
|
||||||
|
|
||||||
|
##### UsbId Rule
|
||||||
|
###### Nomenclature
|
||||||
|
```
|
||||||
|
Attribute :A USB attribute.
|
||||||
|
Element :An identifier of the USB attribute.
|
||||||
|
```
|
||||||
|
|
||||||
|
The UsbId Rule allows matching the USB devices in the system on the following Attributes: `class`,`vendor` and
|
||||||
|
`device`. A list of Elements is provided for each Attribute.
|
||||||
|
|
||||||
|
###### Format
|
||||||
|
```yaml
|
||||||
|
usbId :
|
||||||
|
class: [<class id>, ...]
|
||||||
|
vendor: [<vendor id>, ...]
|
||||||
|
device: [<device id>, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Matching is done by performing a logical _OR_ between Elements of an Attribute and logical _AND_ between the specified Attributes for
|
||||||
|
each USB device in the system.
|
||||||
|
At least one Attribute must be specified. Missing attributes will not partake in the matching process.
|
||||||
|
|
||||||
##### LoadedKMod Rule
|
##### LoadedKMod Rule
|
||||||
###### Nomenclature
|
###### Nomenclature
|
||||||
```
|
```
|
||||||
|
@ -342,6 +366,11 @@ custom:
|
||||||
- pciId:
|
- pciId:
|
||||||
vendor: ["15b3"]
|
vendor: ["15b3"]
|
||||||
device: ["1014", "1017"]
|
device: ["1014", "1017"]
|
||||||
|
- name: "my.usb.feature"
|
||||||
|
matchOn:
|
||||||
|
- usbId:
|
||||||
|
vendor: ["1d6b"]
|
||||||
|
device: ["0003"]
|
||||||
- name: "my.combined.feature"
|
- name: "my.combined.feature"
|
||||||
matchOn:
|
matchOn:
|
||||||
- loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
|
- loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
|
||||||
|
@ -361,6 +390,8 @@ __In the example above:__
|
||||||
if the node has `kmod1` _AND_ `kmod2` kernel modules loaded.
|
if the node has `kmod1` _AND_ `kmod2` kernel modules loaded.
|
||||||
- A node would contain the label: `feature.node.kubernetes.io/custom-my.pci.feature=true`
|
- A node would contain the label: `feature.node.kubernetes.io/custom-my.pci.feature=true`
|
||||||
if the node contains a PCI device with a PCI vendor ID of `15b3` _AND_ PCI device ID of `1014` _OR_ `1017`.
|
if the node contains a PCI device with a PCI vendor ID of `15b3` _AND_ PCI device ID of `1014` _OR_ `1017`.
|
||||||
|
- A node would contain the label: `feature.node.kubernetes.io/custom-my.usb.feature=true`
|
||||||
|
if the node contains a USB device with a USB vendor ID of `1d6b` _AND_ USB device ID of `0003`.
|
||||||
- A node would contain the label: `feature.node.kubernetes.io/custom-my.combined.feature=true`
|
- A node would contain the label: `feature.node.kubernetes.io/custom-my.combined.feature=true`
|
||||||
if `vendor_kmod1` _AND_ `vendor_kmod2` kernel modules are loaded __AND__ the node contains a PCI device
|
if `vendor_kmod1` _AND_ `vendor_kmod2` kernel modules are loaded __AND__ the node contains a PCI device
|
||||||
with a PCI vendor ID of `15b3` _AND_ PCI device ID of `1014` _or_ `1017`.
|
with a PCI vendor ID of `15b3` _AND_ PCI device ID of `1014` _or_ `1017`.
|
||||||
|
@ -433,6 +464,21 @@ Also the set of PCI device classes that the feature source detects is
|
||||||
configurable. By default, device classes (0x)03, (0x)0b40 and (0x)12, i.e.
|
configurable. By default, device classes (0x)03, (0x)0b40 and (0x)12, i.e.
|
||||||
GPUs, co-processors and accelerator cards are detected.
|
GPUs, co-processors and accelerator cards are detected.
|
||||||
|
|
||||||
|
### USB Features
|
||||||
|
|
||||||
|
| Feature | Attribute | Description |
|
||||||
|
| -------------------- | ------------- | ----------------------------------------- |
|
||||||
|
| <device label> | present | USB device is detected
|
||||||
|
|
||||||
|
`<device label>` is composed of raw USB IDs, separated by underscores.
|
||||||
|
The set of fields used in `<device label>` is configurable, valid fields being
|
||||||
|
`class`, `vendor`, and `device`.
|
||||||
|
Defaults are `class`, `vendor` and `device`. An example label using the default
|
||||||
|
label fields:
|
||||||
|
```
|
||||||
|
feature.node.kubernetes.io/usb-fe_1a6e_089a.present=true
|
||||||
|
```
|
||||||
|
|
||||||
See [configuration options](#configuration-options)
|
See [configuration options](#configuration-options)
|
||||||
for more information on NFD config.
|
for more information on NFD config.
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ func argsParse(argv []string) (worker.Args, error) {
|
||||||
in testing
|
in testing
|
||||||
[Default: ]
|
[Default: ]
|
||||||
--sources=<sources> Comma separated list of feature sources.
|
--sources=<sources> Comma separated list of feature sources.
|
||||||
[Default: cpu,custom,iommu,kernel,local,memory,network,pci,storage,system]
|
[Default: cpu,custom,iommu,kernel,local,memory,network,pci,storage,system,usb]
|
||||||
--no-publish Do not publish discovered features to the
|
--no-publish Do not publish discovered features to the
|
||||||
cluster-local Kubernetes API server.
|
cluster-local Kubernetes API server.
|
||||||
--label-whitelist=<pattern> Regular expression to filter label names to
|
--label-whitelist=<pattern> Regular expression to filter label names to
|
||||||
|
|
|
@ -23,7 +23,7 @@ import (
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allSources = []string{"cpu", "custom", "iommu", "kernel", "local", "memory", "network", "pci", "storage", "system"}
|
var allSources = []string{"cpu", "custom", "iommu", "kernel", "local", "memory", "network", "pci", "storage", "system", "usb"}
|
||||||
|
|
||||||
func TestArgsParse(t *testing.T) {
|
func TestArgsParse(t *testing.T) {
|
||||||
Convey("When parsing command line arguments", t, func() {
|
Convey("When parsing command line arguments", t, func() {
|
||||||
|
|
|
@ -44,6 +44,16 @@
|
||||||
# - "device"
|
# - "device"
|
||||||
# - "subsystem_vendor"
|
# - "subsystem_vendor"
|
||||||
# - "subsystem_device"
|
# - "subsystem_device"
|
||||||
|
# usb:
|
||||||
|
# deviceClassWhitelist:
|
||||||
|
# - "0e"
|
||||||
|
# - "ef"
|
||||||
|
# - "fe"
|
||||||
|
# - "ff"
|
||||||
|
# deviceLabelFields:
|
||||||
|
# - "class"
|
||||||
|
# - "vendor"
|
||||||
|
# - "device"
|
||||||
# custom:
|
# custom:
|
||||||
# - name: "my.kernel.feature"
|
# - name: "my.kernel.feature"
|
||||||
# matchOn:
|
# matchOn:
|
||||||
|
@ -57,6 +67,16 @@
|
||||||
# - pciId :
|
# - pciId :
|
||||||
# vendor: ["8086"]
|
# vendor: ["8086"]
|
||||||
# device: ["1000", "1100"]
|
# device: ["1000", "1100"]
|
||||||
|
# - name: "my.usb.feature"
|
||||||
|
# matchOn:
|
||||||
|
# - usbId:
|
||||||
|
# class: ["ff"]
|
||||||
|
# vendor: ["03e7"]
|
||||||
|
# device: ["2485"]
|
||||||
|
# - usbId:
|
||||||
|
# class: ["fe"]
|
||||||
|
# vendor: ["1a6e"]
|
||||||
|
# device: ["089a"]
|
||||||
# - name: "my.combined.feature"
|
# - name: "my.combined.feature"
|
||||||
# matchOn:
|
# matchOn:
|
||||||
# - pciId:
|
# - pciId:
|
||||||
|
|
|
@ -46,6 +46,7 @@ import (
|
||||||
"sigs.k8s.io/node-feature-discovery/source/pci"
|
"sigs.k8s.io/node-feature-discovery/source/pci"
|
||||||
"sigs.k8s.io/node-feature-discovery/source/storage"
|
"sigs.k8s.io/node-feature-discovery/source/storage"
|
||||||
"sigs.k8s.io/node-feature-discovery/source/system"
|
"sigs.k8s.io/node-feature-discovery/source/system"
|
||||||
|
"sigs.k8s.io/node-feature-discovery/source/usb"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ type NFDConfig struct {
|
||||||
Cpu *cpu.NFDConfig `json:"cpu,omitempty"`
|
Cpu *cpu.NFDConfig `json:"cpu,omitempty"`
|
||||||
Kernel *kernel.NFDConfig `json:"kernel,omitempty"`
|
Kernel *kernel.NFDConfig `json:"kernel,omitempty"`
|
||||||
Pci *pci.NFDConfig `json:"pci,omitempty"`
|
Pci *pci.NFDConfig `json:"pci,omitempty"`
|
||||||
|
Usb *usb.NFDConfig `json:"usb,omitempty"`
|
||||||
Custom *custom.NFDConfig `json:"custom,omitempty"`
|
Custom *custom.NFDConfig `json:"custom,omitempty"`
|
||||||
} `json:"sources,omitempty"`
|
} `json:"sources,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -236,6 +238,7 @@ func configParse(filepath string, overrides string) error {
|
||||||
config.Sources.Cpu = &cpu.Config
|
config.Sources.Cpu = &cpu.Config
|
||||||
config.Sources.Kernel = &kernel.Config
|
config.Sources.Kernel = &kernel.Config
|
||||||
config.Sources.Pci = &pci.Config
|
config.Sources.Pci = &pci.Config
|
||||||
|
config.Sources.Usb = &usb.Config
|
||||||
config.Sources.Custom = &custom.Config
|
config.Sources.Custom = &custom.Config
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(filepath)
|
data, err := ioutil.ReadFile(filepath)
|
||||||
|
@ -279,6 +282,7 @@ func configureParameters(sourcesWhiteList []string, labelWhiteListStr string) (e
|
||||||
pci.Source{},
|
pci.Source{},
|
||||||
storage.Source{},
|
storage.Source{},
|
||||||
system.Source{},
|
system.Source{},
|
||||||
|
usb.Source{},
|
||||||
custom.Source{},
|
custom.Source{},
|
||||||
// local needs to be the last source so that it is able to override
|
// local needs to be the last source so that it is able to override
|
||||||
// labels from other sources
|
// labels from other sources
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
// Custom Features Configurations
|
// Custom Features Configurations
|
||||||
type MatchRule struct {
|
type MatchRule struct {
|
||||||
PciId *rules.PciIdRule `json:"pciId,omitempty""`
|
PciId *rules.PciIdRule `json:"pciId,omitempty""`
|
||||||
|
UsbId *rules.UsbIdRule `json:"usbId,omitempty""`
|
||||||
LoadedKMod *rules.LoadedKModRule `json:"loadedKMod,omitempty""`
|
LoadedKMod *rules.LoadedKModRule `json:"loadedKMod,omitempty""`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +78,16 @@ func (s Source) discoverFeature(feature CustomFeature) (bool, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// USB ID rule
|
||||||
|
if rule.UsbId != nil {
|
||||||
|
match, err := rule.UsbId.Match()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
// Loaded kernel module rule
|
// Loaded kernel module rule
|
||||||
if rule.LoadedKMod != nil {
|
if rule.LoadedKMod != nil {
|
||||||
match, err := rule.LoadedKMod.Match()
|
match, err := rule.LoadedKMod.Match()
|
||||||
|
|
79
source/custom/rules/usb_id_rule.go
Normal file
79
source/custom/rules/usb_id_rule.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rule that matches on the following USB device attributes: <class, vendor, device>
|
||||||
|
// each device attribute will be a list elements(strings).
|
||||||
|
// Match operation: OR will be performed per element and AND will be performed per attribute.
|
||||||
|
// An empty attribute will not be included in the matching process.
|
||||||
|
type UsbIdRuleInput struct {
|
||||||
|
Class []string `json:"class,omitempty"`
|
||||||
|
Vendor []string `json:"vendor,omitempty"`
|
||||||
|
Device []string `json:"device,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsbIdRule struct {
|
||||||
|
UsbIdRuleInput
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match USB devices on provided USB device attributes
|
||||||
|
func (r *UsbIdRule) Match() (bool, error) {
|
||||||
|
devAttr := map[string]bool{}
|
||||||
|
for _, attr := range []string{"class", "vendor", "device"} {
|
||||||
|
devAttr[attr] = true
|
||||||
|
}
|
||||||
|
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 classDevs {
|
||||||
|
// match rule on a single device
|
||||||
|
if r.matchDevOnRule(dev) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UsbIdRule) matchDevOnRule(dev usbutils.UsbDeviceInfo) bool {
|
||||||
|
if len(r.Class) == 0 && len(r.Vendor) == 0 && len(r.Device) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Class) > 0 && !in(dev["class"], r.Class) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Vendor) > 0 && !in(dev["vendor"], r.Vendor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Device) > 0 && !in(dev["device"], r.Device) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package pciutils
|
package busutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -31,7 +31,7 @@ var ExtraPciDevAttrs = []string{"sriov_totalvfs"}
|
||||||
|
|
||||||
// Read a single PCI device attribute
|
// Read a single PCI device attribute
|
||||||
// A PCI attribute in this context, maps to the corresponding sysfs file
|
// A PCI attribute in this context, maps to the corresponding sysfs file
|
||||||
func readSingleAttribute(devPath string, attrName string) (string, error) {
|
func readSinglePciAttribute(devPath string, attrName string) (string, error) {
|
||||||
data, err := ioutil.ReadFile(path.Join(devPath, attrName))
|
data, err := ioutil.ReadFile(path.Join(devPath, attrName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read device attribute %s: %v", attrName, err)
|
return "", fmt.Errorf("failed to read device attribute %s: %v", attrName, err)
|
||||||
|
@ -48,11 +48,11 @@ func readSingleAttribute(devPath string, attrName string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read information of one PCI device
|
// Read information of one PCI device
|
||||||
func readDevInfo(devPath string, deviceAttrSpec map[string]bool) (PciDeviceInfo, error) {
|
func readPciDevInfo(devPath string, deviceAttrSpec map[string]bool) (PciDeviceInfo, error) {
|
||||||
info := PciDeviceInfo{}
|
info := PciDeviceInfo{}
|
||||||
|
|
||||||
for attr, must := range deviceAttrSpec {
|
for attr, must := range deviceAttrSpec {
|
||||||
attrVal, err := readSingleAttribute(devPath, attr)
|
attrVal, err := readSinglePciAttribute(devPath, attr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if must {
|
if must {
|
||||||
return info, fmt.Errorf("Failed to read device %s: %s", attr, err)
|
return info, fmt.Errorf("Failed to read device %s: %s", attr, err)
|
||||||
|
@ -85,7 +85,7 @@ func DetectPci(deviceAttrSpec map[string]bool) (map[string][]PciDeviceInfo, erro
|
||||||
|
|
||||||
// Iterate over devices
|
// Iterate over devices
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
info, err := readDevInfo(path.Join(basePath, device.Name()), deviceAttrSpec)
|
info, err := readPciDevInfo(path.Join(basePath, device.Name()), deviceAttrSpec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
continue
|
continue
|
||||||
|
|
138
source/internal/usb_utils.go
Normal file
138
source/internal/usb_utils.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 busutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UsbDeviceInfo map[string]string
|
||||||
|
type UsbClassMap map[string]UsbDeviceInfo
|
||||||
|
|
||||||
|
var DefaultUsbDevAttrs = []string{"class", "vendor", "device"}
|
||||||
|
|
||||||
|
// The USB device sysfs files do not have terribly user friendly names, map
|
||||||
|
// these for consistency with the PCI matcher.
|
||||||
|
var devAttrFileMap = map[string]string{
|
||||||
|
"class": "bDeviceClass",
|
||||||
|
"device": "idProduct",
|
||||||
|
"vendor": "idVendor",
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSingleUsbSysfsAttribute(path string) (string, error) {
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read device attribute %s: %v", filepath.Base(path), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrVal := strings.TrimSpace(string(data))
|
||||||
|
|
||||||
|
return attrVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a single USB device attribute
|
||||||
|
// A USB attribute in this context, maps to the corresponding sysfs file
|
||||||
|
func readSingleUsbAttribute(devPath string, attrName string) (string, error) {
|
||||||
|
return readSingleUsbSysfsAttribute(path.Join(devPath, devAttrFileMap[attrName]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read information of one USB device
|
||||||
|
func readUsbDevInfo(devPath string, deviceAttrSpec map[string]bool) (UsbClassMap, error) {
|
||||||
|
classmap := UsbClassMap{}
|
||||||
|
info := UsbDeviceInfo{}
|
||||||
|
|
||||||
|
for attr, _ := range deviceAttrSpec {
|
||||||
|
attrVal, _ := readSingleUsbAttribute(devPath, attr)
|
||||||
|
if len(attrVal) > 0 {
|
||||||
|
info[attr] = attrVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// USB devices encode their class information either at the device or the interface level. If the device class
|
||||||
|
// is set, return as-is.
|
||||||
|
if info["class"] != "00" {
|
||||||
|
classmap[info["class"]] = info
|
||||||
|
} else {
|
||||||
|
// Otherwise, if a 00 is presented at the device level, descend to the interface level.
|
||||||
|
interfaces, err := filepath.Glob(devPath + "/*/bInterfaceClass")
|
||||||
|
if err != nil {
|
||||||
|
return classmap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// A device may, notably, have multiple interfaces with mixed classes, so we create a unique device for each
|
||||||
|
// unique interface class.
|
||||||
|
for _, intf := range interfaces {
|
||||||
|
// Determine the interface class
|
||||||
|
attrVal, err := readSingleUsbSysfsAttribute(intf)
|
||||||
|
if err != nil {
|
||||||
|
return classmap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := UsbDeviceInfo{}
|
||||||
|
for k, v := range info {
|
||||||
|
attr[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
attr["class"] = attrVal
|
||||||
|
classmap[attrVal] = attr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classmap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List available USB devices and retrieve device attributes.
|
||||||
|
// deviceAttrSpec is a map which specifies which attributes to retrieve.
|
||||||
|
// 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
|
||||||
|
// devices. We work around this by globbing anything that includes a
|
||||||
|
// valid product ID.
|
||||||
|
const devicePath = "/sys/bus/usb/devices/*/idProduct"
|
||||||
|
devInfo := make(map[string][]UsbDeviceInfo)
|
||||||
|
|
||||||
|
devices, err := filepath.Glob(devicePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// "class" is a mandatory attribute, inject it to spec if needed.
|
||||||
|
deviceAttrSpec["class"] = true
|
||||||
|
|
||||||
|
// Iterate over devices
|
||||||
|
for _, device := range devices {
|
||||||
|
devMap, err := readUsbDevInfo(filepath.Dir(device), deviceAttrSpec)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for class, info := range devMap {
|
||||||
|
devInfo[class] = append(devInfo[class], info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devInfo, nil
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sigs.k8s.io/node-feature-discovery/source"
|
"sigs.k8s.io/node-feature-discovery/source"
|
||||||
"sigs.k8s.io/node-feature-discovery/source/internal"
|
pciutils "sigs.k8s.io/node-feature-discovery/source/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NFDConfig struct {
|
type NFDConfig struct {
|
||||||
|
|
105
source/usb/usb.go
Normal file
105
source/usb/usb.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"sigs.k8s.io/node-feature-discovery/source"
|
||||||
|
usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NFDConfig struct {
|
||||||
|
DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"`
|
||||||
|
DeviceLabelFields []string `json:"deviceLabelFields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Config = NFDConfig{
|
||||||
|
// Whitelist specific USB classes: https://www.usb.org/defined-class-codes
|
||||||
|
// By default these include classes where different accelerators are typically mapped:
|
||||||
|
// Video (0e), Miscellaneous (ef), Application Specific (fe), and Vendor Specific (ff).
|
||||||
|
DeviceClassWhitelist: []string{"0e", "ef", "fe", "ff"},
|
||||||
|
DeviceLabelFields: []string{"class", "vendor", "device"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement FeatureSource interface
|
||||||
|
type Source struct{}
|
||||||
|
|
||||||
|
// Return name of the feature source
|
||||||
|
func (s Source) Name() string { return "usb" }
|
||||||
|
|
||||||
|
// Discover features
|
||||||
|
func (s Source) Discover() (source.Features, error) {
|
||||||
|
features := source.Features{}
|
||||||
|
|
||||||
|
// Construct a device label format, a sorted list of valid attributes
|
||||||
|
deviceLabelFields := []string{}
|
||||||
|
configLabelFields := map[string]bool{}
|
||||||
|
for _, field := range Config.DeviceLabelFields {
|
||||||
|
configLabelFields[field] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attr := range usbutils.DefaultUsbDevAttrs {
|
||||||
|
if _, ok := configLabelFields[attr]; ok {
|
||||||
|
deviceLabelFields = append(deviceLabelFields, attr)
|
||||||
|
delete(configLabelFields, attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(configLabelFields) > 0 {
|
||||||
|
keys := []string{}
|
||||||
|
for key := range configLabelFields {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
log.Printf("WARNING: invalid fields '%v' in deviceLabelFields, ignoring...", keys)
|
||||||
|
}
|
||||||
|
if len(deviceLabelFields) == 0 {
|
||||||
|
log.Printf("WARNING: no valid fields in deviceLabelFields defined, using the defaults")
|
||||||
|
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
|
||||||
|
for class, classDevs := range devs {
|
||||||
|
for _, white := range Config.DeviceClassWhitelist {
|
||||||
|
if strings.HasPrefix(class, strings.ToLower(white)) {
|
||||||
|
for _, dev := range classDevs {
|
||||||
|
devLabel := ""
|
||||||
|
for i, attr := range deviceLabelFields {
|
||||||
|
devLabel += dev[attr]
|
||||||
|
if i < len(deviceLabelFields)-1 {
|
||||||
|
devLabel += "_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features[devLabel+".present"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return features, nil
|
||||||
|
}
|
Loading…
Reference in a new issue