1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-04-23 20:57:10 +00:00

Merge pull request from marquiz/devel/feature-source-conversion

source: implement FeatureSource interface
This commit is contained in:
Kubernetes Prow Robot 2021-11-11 09:20:08 -08:00 committed by GitHub
commit 5299ca2ab4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 831 additions and 396 deletions

View file

@ -20,28 +20,28 @@ package feature
// features to empty values
func NewDomainFeatures() *DomainFeatures {
return &DomainFeatures{
Keys: make(map[string]*KeyFeatureSet),
Values: make(map[string]*ValueFeatureSet),
Instances: make(map[string]*InstanceFeatureSet)}
Keys: make(map[string]KeyFeatureSet),
Values: make(map[string]ValueFeatureSet),
Instances: make(map[string]InstanceFeatureSet)}
}
func NewKeyFeatures(keys ...string) *KeyFeatureSet {
func NewKeyFeatures(keys ...string) KeyFeatureSet {
e := make(map[string]Nil, len(keys))
for _, k := range keys {
e[k] = Nil{}
}
return &KeyFeatureSet{Elements: e}
return KeyFeatureSet{Elements: e}
}
func NewValueFeatures(values map[string]string) *ValueFeatureSet {
func NewValueFeatures(values map[string]string) ValueFeatureSet {
if values == nil {
values = make(map[string]string)
}
return &ValueFeatureSet{Elements: values}
return ValueFeatureSet{Elements: values}
}
func NewInstanceFeatures(instances []InstanceFeature) *InstanceFeatureSet {
return &InstanceFeatureSet{Elements: instances}
func NewInstanceFeatures(instances []InstanceFeature) InstanceFeatureSet {
return InstanceFeatureSet{Elements: instances}
}
func NewInstanceFeature(attrs map[string]string) *InstanceFeature {

View file

@ -22,9 +22,9 @@ type Features map[string]*DomainFeatures
// DomainFeatures is the collection of all discovered features of one domain.
type DomainFeatures struct {
Keys map[string]*KeyFeatureSet `protobuf:"bytes,1,rep,name=keys"`
Values map[string]*ValueFeatureSet `protobuf:"bytes,2,rep,name=values"`
Instances map[string]*InstanceFeatureSet `protobuf:"bytes,3,rep,name=instances"`
Keys map[string]KeyFeatureSet `protobuf:"bytes,1,rep,name=keys"`
Values map[string]ValueFeatureSet `protobuf:"bytes,2,rep,name=values"`
Instances map[string]InstanceFeatureSet `protobuf:"bytes,3,rep,name=instances"`
}
// KeyFeatureSet is a set of simple features only containing names without values.

View file

@ -18,15 +18,26 @@ package cpu
import (
"io/ioutil"
"strconv"
"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/internal/cpuidutils"
)
const Name = "cpu"
const (
CpuidFeature = "cpuid"
CstateFeature = "cstate"
PstateFeature = "pstate"
RdtFeature = "rdt"
SstFeature = "sst"
TopologyFeature = "topology"
)
// Configuration file options
type cpuidConfig struct {
AttributeBlacklist []string `json:"attributeBlacklist,omitempty"`
@ -78,15 +89,17 @@ type keyFilter struct {
whitelist bool
}
// cpuSource implements the LabelSource and ConfigurableSource interfaces.
// cpuSource implements the FeatureSource, LabelSource and ConfigurableSource interfaces.
type cpuSource struct {
config *Config
cpuidFilter *keyFilter
features *feature.DomainFeatures
}
// Singleton source instance
var (
src cpuSource
src = cpuSource{config: newDefaultConfig(), cpuidFilter: &keyFilter{}}
_ source.FeatureSource = &src
_ source.LabelSource = &src
_ source.ConfigurableSource = &src
)
@ -115,57 +128,98 @@ func (s *cpuSource) Priority() int { return 0 }
// GetLabels method of the LabelSource interface
func (s *cpuSource) GetLabels() (source.FeatureLabels, error) {
features := source.FeatureLabels{}
labels := source.FeatureLabels{}
features := s.GetFeatures()
// Check if hyper-threading seems to be enabled
found, err := haveThreadSiblings()
if err != nil {
klog.Errorf("failed to detect hyper-threading: %v", err)
} else if found {
features["hardware_multithreading"] = true
// CPUID
for f := range features.Keys[CpuidFeature].Elements {
if s.cpuidFilter.unmask(f) {
labels["cpuid."+f] = true
}
}
// Check SST-BF
found, err = discoverSSTBF()
if err != nil {
klog.Errorf("failed to detect SST-BF: %v", err)
} else if found {
features["power.sst_bf.enabled"] = true
// Cstate
for k, v := range features.Values[CstateFeature].Elements {
labels["cstate."+k] = v
}
// Pstate
for k, v := range features.Values[PstateFeature].Elements {
labels["pstate."+k] = v
}
// RDT
for k := range features.Keys[RdtFeature].Elements {
labels["rdt."+k] = true
}
// SST
for k, v := range features.Values[SstFeature].Elements {
labels["power.sst_"+k] = v
}
// Hyperthreading
if v, ok := features.Values[TopologyFeature].Elements["hardware_multithreading"]; ok {
labels["hardware_multithreading"] = v
}
return labels, nil
}
// Discover method of the FeatureSource Interface
func (s *cpuSource) Discover() error {
s.features = feature.NewDomainFeatures()
// Detect CPUID
cpuidFlags := cpuidutils.GetCpuidFlags()
for _, f := range cpuidFlags {
if s.cpuidFilter.unmask(f) {
features["cpuid."+f] = true
}
s.features.Keys[CpuidFeature] = feature.NewKeyFeatures(getCpuidFlags()...)
// Detect cstate configuration
cstate, err := detectCstate()
if err != nil {
klog.Errorf("failed to detect cstate: %v", err)
} else {
s.features.Values[CstateFeature] = feature.NewValueFeatures(cstate)
}
// Detect pstate features
pstate, err := detectPstate()
if err != nil {
klog.Error(err)
} else {
for k, v := range pstate {
features["pstate."+k] = v
}
}
s.features.Values[PstateFeature] = feature.NewValueFeatures(pstate)
// Detect RDT features
rdt := discoverRDT()
for _, f := range rdt {
features["rdt."+f] = true
s.features.Keys[RdtFeature] = feature.NewKeyFeatures(discoverRDT()...)
// Detect SST features
s.features.Values[SstFeature] = feature.NewValueFeatures(discoverSST())
// Detect hyper-threading
s.features.Values[TopologyFeature] = feature.NewValueFeatures(discoverTopology())
utils.KlogDump(3, "discovered cpu features:", " ", s.features)
return nil
}
// GetFeatures method of the FeatureSource Interface
func (s *cpuSource) GetFeatures() *feature.DomainFeatures {
if s.features == nil {
s.features = feature.NewDomainFeatures()
}
return s.features
}
func discoverTopology() map[string]string {
features := make(map[string]string)
if ht, err := haveThreadSiblings(); err != nil {
klog.Errorf("failed to detect hyper-threading: %v", err)
} else {
features["hardware_multithreading"] = strconv.FormatBool(ht)
}
// Detect cstate configuration
cstate, ok, err := detectCstate()
if err != nil {
klog.Error(err)
} else if ok {
features["cstate.enabled"] = cstate
}
return features, nil
return features
}
// Check if any (online) CPUs have thread siblings

View file

@ -1,5 +1,5 @@
/*
Copyright 2018-2020 The Kubernetes Authors.
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.
@ -14,17 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package kernelutils
package cpu
import (
"io/ioutil"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
func GetKernelVersion() (string, error) {
unameRaw, err := ioutil.ReadFile("/proc/sys/kernel/osrelease")
if err != nil {
return "", err
}
return strings.TrimSpace(string(unameRaw)), nil
func TestCpuSource(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,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cpuidutils
package cpu
import (
"github.com/klauspost/cpuid/v2"
)
// GetCpuidFlags returns feature names for all the supported CPU features.
func GetCpuidFlags() []string {
// getCpuidFlags returns feature names for all the supported CPU features.
func getCpuidFlags() []string {
return cpuid.CPU.FeatureSet()
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cpuidutils
package cpu
/*
#include <sys/auxv.h>
@ -90,7 +90,7 @@ var flagNames_arm = map[uint64]string{
CPU_ARM_FEATURE_CRC32: "CRC32",
}
func GetCpuidFlags() []string {
func getCpuidFlags() []string {
r := make([]string, 0, 20)
hwcap := uint64(C.gethwcap())
for i := uint(0); i < 64; i++ {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cpuidutils
package cpu
/*
#include <sys/auxv.h>
@ -80,7 +80,7 @@ var flagNames_arm64 = map[uint64]string{
CPU_ARM64_FEATURE_SVE: "SVE",
}
func GetCpuidFlags() []string {
func getCpuidFlags() []string {
r := make([]string, 0, 20)
hwcap := uint64(C.gethwcap())
for i := uint(0); i < 64; i++ {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cpuidutils
package cpu
/*
#include <sys/auxv.h>
@ -126,7 +126,7 @@ var flag2Names_ppc64le = map[uint64]string{
PPC_FEATURE2_HTM_NO_SUSPEND: "HTM-NO-SUSPEND",
}
func GetCpuidFlags() []string {
func getCpuidFlags() []string {
r := make([]string, 0, 30)
hwcap := uint64(C.gethwcap())
hwcap2 := uint64(C.gethwcap2())

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cpuidutils
package cpu
/*
#include <sys/auxv.h>
@ -71,7 +71,7 @@ var flagNames_s390x = map[uint64]string{
HWCAP_S390_DFLT: "DFLT",
}
func GetCpuidFlags() []string {
func getCpuidFlags() []string {
r := make([]string, 0, 20)
hwcap := uint64(C.gethwcap())
for i := uint(0); i < 64; i++ {

View file

@ -30,38 +30,42 @@ import (
)
// Discover if c-states are enabled
func detectCstate() (bool, bool, error) {
func detectCstate() (map[string]string, error) {
cstate := make(map[string]string)
// Check that sysfs is available
sysfsBase := source.SysfsDir.Path("devices/system/cpu")
if _, err := os.Stat(sysfsBase); err != nil {
return false, false, fmt.Errorf("unable to detect cstate status: %w", err)
return cstate, fmt.Errorf("unable to detect cstate status: %w", err)
}
cpuidleDir := filepath.Join(sysfsBase, "cpuidle")
if _, err := os.Stat(cpuidleDir); os.IsNotExist(err) {
klog.V(1).Info("cpuidle disabled in the kernel")
return false, false, nil
return cstate, nil
}
// When the intel_idle driver is in use (default), check setting of max_cstates
driver, err := ioutil.ReadFile(filepath.Join(cpuidleDir, "current_driver"))
if err != nil {
return false, false, fmt.Errorf("cannot get driver for cpuidle: %w", err)
return cstate, fmt.Errorf("cannot get driver for cpuidle: %w", err)
}
if d := strings.TrimSpace(string(driver)); d != "intel_idle" {
// Currently only checking intel_idle driver for cstates
klog.V(1).Infof("intel_idle driver is not in use (%s is active)", d)
return false, false, nil
return cstate, nil
}
data, err := ioutil.ReadFile(source.SysfsDir.Path("module/intel_idle/parameters/max_cstate"))
if err != nil {
return false, false, fmt.Errorf("cannot determine cstate from max_cstates: %w", err)
return cstate, fmt.Errorf("cannot determine cstate from max_cstates: %w", err)
}
cstates, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return false, false, fmt.Errorf("non-integer value of cstates: %w", err)
return cstate, fmt.Errorf("non-integer value of cstates: %w", err)
} else {
cstate["enabled"] = strconv.FormatBool(cstates > 0)
}
return cstates > 0, true, nil
return cstate, nil
}

View file

@ -20,6 +20,6 @@ limitations under the License.
package cpu
// Discover if c-states are enabled
func detectCstate() (bool, bool, error) {
return false, false, nil
func detectCstate() (map[string]string, error) {
return nil, nil
}

View file

@ -23,6 +23,8 @@ import (
"strconv"
"strings"
"k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/pkg/cpuid"
"sigs.k8s.io/node-feature-discovery/source"
)
@ -32,6 +34,18 @@ const (
LEAF_PROCESSOR_FREQUENCY_INFORMATION = 0x16
)
func discoverSST() map[string]string {
features := make(map[string]string)
if bf, err := discoverSSTBF(); err != nil {
klog.Errorf("failed to detect SST-BF: %v", err)
} else if bf {
features["bf.enabled"] = strconv.FormatBool(bf)
}
return features
}
func discoverSSTBF() (bool, error) {
// Get processor's "nominal base frequency" (in MHz) from CPUID
freqInfo := cpuid.Cpuid(LEAF_PROCESSOR_FREQUENCY_INFORMATION, 0)

View file

@ -58,7 +58,7 @@ type customSource struct {
// Singleton source instance
var (
src customSource
src = customSource{config: newDefaultConfig()}
_ source.LabelSource = &src
_ source.ConfigurableSource = &src
)

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Kubernetes Authors.
Copyright 2020-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.
@ -17,26 +17,25 @@ limitations under the License.
package rules
import (
"sigs.k8s.io/node-feature-discovery/source/internal/cpuidutils"
"fmt"
"sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/cpu"
)
// CpuIDRule implements Rule
// CpuIDRule implements Rule for the custom source
type CpuIDRule []string
var cpuIdFlags map[string]struct{}
func (cpuids *CpuIDRule) Match() (bool, error) {
flags, ok := source.GetFeatureSource("cpu").GetFeatures().Keys[cpu.CpuidFeature]
if !ok {
return false, fmt.Errorf("cpuid information not available")
}
for _, f := range *cpuids {
if _, ok := cpuIdFlags[f]; !ok {
if _, ok := flags.Elements[f]; !ok {
return false, nil
}
}
return true, nil
}
func init() {
cpuIdFlags = make(map[string]struct{})
for _, f := range cpuidutils.GetCpuidFlags() {
cpuIdFlags[f] = struct{}{}
}
}

View file

@ -18,9 +18,11 @@ package rules
import (
"encoding/json"
"fmt"
"strings"
"sigs.k8s.io/node-feature-discovery/source/internal/kernelutils"
"sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/kernel"
)
// KconfigRule implements Rule
@ -31,11 +33,14 @@ type kconfig struct {
Value string
}
var kConfigs map[string]string
func (kconfigs *KconfigRule) Match() (bool, error) {
options, ok := source.GetFeatureSource("kernel").GetFeatures().Values[kernel.ConfigFeature]
if !ok {
return false, fmt.Errorf("kernel config options not available")
}
for _, f := range *kconfigs {
if v, ok := kConfigs[f.Name]; !ok || f.Value != v {
if v, ok := options.Elements[f.Name]; !ok || f.Value != v {
return false, nil
}
}
@ -57,14 +62,3 @@ func (c *kconfig) UnmarshalJSON(data []byte) error {
}
return nil
}
func init() {
kConfigs = make(map[string]string)
kconfig, err := kernelutils.ParseKconfig("")
if err == nil {
for k, v := range kconfig {
kConfigs[k] = v
}
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Kubernetes Authors.
Copyright 2020-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.
@ -18,44 +18,26 @@ package rules
import (
"fmt"
"io/ioutil"
"strings"
"sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/kernel"
)
// LoadedKModRule matches loaded kernel modules in the system
type LoadedKModRule []string
const kmodProcfsPath = "/proc/modules"
// Match loaded kernel modules on provided list of kernel modules
func (kmods *LoadedKModRule) Match() (bool, error) {
loadedModules, err := kmods.getLoadedModules()
if err != nil {
return false, fmt.Errorf("failed to get loaded kernel modules. %s", err.Error())
modules, ok := source.GetFeatureSource("kernel").GetFeatures().Keys[kernel.LoadedModuleFeature]
if !ok {
return false, fmt.Errorf("info about loaded modules not available")
}
for _, kmod := range *kmods {
if _, ok := loadedModules[kmod]; !ok {
if _, ok := modules.Elements[kmod]; !ok {
// kernel module not loaded
return false, nil
}
}
return true, nil
}
func (kmods *LoadedKModRule) getLoadedModules() (map[string]struct{}, error) {
out, err := ioutil.ReadFile(kmodProcfsPath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %s", kmodProcfsPath, err.Error())
}
loadedMods := make(map[string]struct{})
for _, line := range strings.Split(string(out), "\n") {
// skip empty lines
if len(line) == 0 {
continue
}
// append loaded module
loadedMods[strings.Fields(line)[0]] = struct{}{}
}
return loadedMods, nil
}

View file

@ -17,14 +17,13 @@ limitations under the License.
package rules
import (
"os"
"fmt"
"regexp"
"k8s.io/klog/v2"
)
var (
nodeName = os.Getenv("NODE_NAME")
"sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/system"
)
// NodenameRule matches on nodenames configured in a ConfigMap
@ -34,6 +33,11 @@ type NodenameRule []string
var _ Rule = NodenameRule{}
func (n NodenameRule) Match() (bool, error) {
nodeName, ok := source.GetFeatureSource("system").GetFeatures().Values[system.NameFeature].Elements["nodename"]
if !ok {
return false, fmt.Errorf("node name not available")
}
for _, nodenamePattern := range n {
klog.V(1).Infof("matchNodename %s", nodenamePattern)
match, err := regexp.MatchString(nodenamePattern, nodeName)

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Kubernetes Authors.
Copyright 2020-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.
@ -18,7 +18,10 @@ package rules
import (
"fmt"
pciutils "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/pci"
)
// Rule that matches on the following PCI device attributes: <class, vendor, device>
@ -37,40 +40,40 @@ type PciIDRule struct {
// Match PCI devices on provided PCI device attributes
func (r *PciIDRule) Match() (bool, error) {
devs, ok := source.GetFeatureSource("pci").GetFeatures().Instances[pci.DeviceFeature]
if !ok {
return false, fmt.Errorf("cpuid information not available")
}
devAttr := map[string]bool{}
for _, attr := range []string{"class", "vendor", "device"} {
devAttr[attr] = true
}
allDevs, err := pciutils.DetectPci(devAttr)
if err != nil {
return false, fmt.Errorf("failed to detect PCI 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
}
for _, dev := range devs.Elements {
// match rule on a single device
if r.matchDevOnRule(dev) {
return true, nil
}
}
return false, nil
}
func (r *PciIDRule) matchDevOnRule(dev pciutils.PciDeviceInfo) bool {
func (r *PciIDRule) matchDevOnRule(dev feature.InstanceFeature) 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) {
attrs := dev.Attributes
if len(r.Class) > 0 && !in(attrs["class"], r.Class) {
return false
}
if len(r.Vendor) > 0 && !in(dev["vendor"], r.Vendor) {
if len(r.Vendor) > 0 && !in(attrs["vendor"], r.Vendor) {
return false
}
if len(r.Device) > 0 && !in(dev["device"], r.Device) {
if len(r.Device) > 0 && !in(attrs["device"], r.Device) {
return false
}

View file

@ -18,7 +18,10 @@ package rules
import (
"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>
@ -38,44 +41,39 @@ type UsbIDRule struct {
// 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", "serial"} {
devAttr[attr] = true
}
allDevs, err := usbutils.DetectUsb(devAttr)
if err != nil {
return false, fmt.Errorf("failed to detect USB devices: %s", err.Error())
devs, ok := source.GetFeatureSource("usb").GetFeatures().Instances[usb.DeviceFeature]
if !ok {
return false, fmt.Errorf("usb device information not available")
}
for _, classDevs := range allDevs {
for _, dev := range classDevs {
// match rule on a single device
if r.matchDevOnRule(dev) {
return true, nil
}
for _, dev := range devs.Elements {
// match rule on a single device
if r.matchDevOnRule(dev) {
return true, 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 {
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
}
if len(r.Vendor) > 0 && !in(dev["vendor"], r.Vendor) {
if len(r.Vendor) > 0 && !in(attrs["vendor"], r.Vendor) {
return false
}
if len(r.Device) > 0 && !in(dev["device"], r.Device) {
if len(r.Device) > 0 && !in(attrs["device"], r.Device) {
return false
}
if len(r.Serial) > 0 && !in(dev["serial"], r.Serial) {
if len(r.Serial) > 0 && !in(attrs["serial"], r.Serial) {
return false
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package kernelutils
package kernel
import (
"bytes"
@ -26,9 +26,9 @@ import (
"regexp"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/util/validation"
"sigs.k8s.io/node-feature-discovery/source"
)
@ -52,13 +52,13 @@ func readKconfigGzip(filename string) ([]byte, error) {
}
// ParseKconfig reads kconfig and return a map
func ParseKconfig(configPath string) (map[string]string, error) {
func parseKconfig(configPath string) (map[string]string, error) {
kconfig := map[string]string{}
raw := []byte(nil)
var err error
var searchPaths []string
kVer, err := GetKernelVersion()
kVer, err := getVersion()
if err != nil {
searchPaths = []string{
"/proc/config.gz",

View file

@ -17,17 +17,24 @@ limitations under the License.
package kernel
import (
"regexp"
"strings"
"strconv"
"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/internal/kernelutils"
)
const Name = "kernel"
const (
ConfigFeature = "config"
LoadedModuleFeature = "loadedmodule"
SelinuxFeature = "selinux"
VersionFeature = "version"
)
// Configuration file options
type Config struct {
KconfigFile string
@ -47,14 +54,16 @@ func newDefaultConfig() *Config {
}
}
// kernelSource implements the LabelSource and ConfigurableSource interfaces.
// kernelSource implements the FeatureSource, LabelSource and ConfigurableSource interfaces.
type kernelSource struct {
config *Config
config *Config
features *feature.DomainFeatures
}
// Singleton source instance
var (
src kernelSource
src = kernelSource{config: newDefaultConfig()}
_ source.FeatureSource = &src
_ source.LabelSource = &src
_ source.ConfigurableSource = &src
)
@ -82,69 +91,68 @@ func (s *kernelSource) Priority() int { return 0 }
// GetLabels method of the LabelSource interface
func (s *kernelSource) GetLabels() (source.FeatureLabels, error) {
features := source.FeatureLabels{}
labels := source.FeatureLabels{}
features := s.GetFeatures()
// Read kernel version
version, err := parseVersion()
if err != nil {
klog.Errorf("failed to get kernel version: %s", err)
} else {
for key := range version {
features["version."+key] = version[key]
}
}
// Read kconfig
kconfig, err := kernelutils.ParseKconfig(s.config.KconfigFile)
if err != nil {
klog.Errorf("failed to read kconfig: %s", err)
for k, v := range features.Values[VersionFeature].Elements {
labels[VersionFeature+"."+k] = v
}
// Check flags
for _, opt := range s.config.ConfigOpts {
if val, ok := kconfig[opt]; ok {
features["config."+opt] = val
if val, ok := features.Values[ConfigFeature].Elements[opt]; ok {
labels[ConfigFeature+"."+opt] = val
}
}
selinux, err := SelinuxEnabled()
if err != nil {
klog.Warning(err)
} else if selinux {
features["selinux.enabled"] = true
for k, v := range features.Values[SelinuxFeature].Elements {
labels[SelinuxFeature+"."+k] = v
}
return features, nil
return labels, nil
}
// Read and parse kernel version
func parseVersion() (map[string]string, error) {
version := map[string]string{}
// Discover method of the FeatureSource interface
func (s *kernelSource) Discover() error {
s.features = feature.NewDomainFeatures()
full, err := kernelutils.GetKernelVersion()
if err != nil {
return nil, err
// Read kernel version
if version, err := parseVersion(); err != nil {
klog.Errorf("failed to get kernel version: %s", err)
} else {
s.features.Values[VersionFeature] = feature.NewValueFeatures(version)
}
// Replace forbidden symbols
fullRegex := regexp.MustCompile("[^-A-Za-z0-9_.]")
full = fullRegex.ReplaceAllString(full, "_")
// Label values must start and end with an alphanumeric
full = strings.Trim(full, "-_.")
version["full"] = full
// Regexp for parsing version components
re := regexp.MustCompile(`^(?P<major>\d+)(\.(?P<minor>\d+))?(\.(?P<revision>\d+))?(-.*)?$`)
if m := re.FindStringSubmatch(full); m != nil {
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
version[name] = m[i]
}
}
// Read kconfig
if kconfig, err := parseKconfig(s.config.KconfigFile); err != nil {
klog.Errorf("failed to read kconfig: %s", err)
} else {
s.features.Values[ConfigFeature] = feature.NewValueFeatures(kconfig)
}
return version, nil
if kmods, err := getLoadedModules(); err != nil {
klog.Errorf("failed to get loaded kernel modules: %v", err)
} else {
s.features.Keys[LoadedModuleFeature] = feature.NewKeyFeatures(kmods...)
}
if selinux, err := SelinuxEnabled(); err != nil {
klog.Warning(err)
} else {
s.features.Values[SelinuxFeature] = feature.NewValueFeatures(nil)
s.features.Values[SelinuxFeature].Elements["enabled"] = strconv.FormatBool(selinux)
}
utils.KlogDump(3, "discovered kernel features:", " ", s.features)
return nil
}
func (s *kernelSource) GetFeatures() *feature.DomainFeatures {
if s.features == nil {
s.features = feature.NewDomainFeatures()
}
return s.features
}
func init() {

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 kernel
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
func TestKernelSource(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)
}

44
source/kernel/modules.go Normal file
View file

@ -0,0 +1,44 @@
/*
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 kernel
import (
"fmt"
"io/ioutil"
"strings"
)
const kmodProcfsPath = "/proc/modules"
func getLoadedModules() ([]string, error) {
out, err := ioutil.ReadFile(kmodProcfsPath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %s", kmodProcfsPath, err.Error())
}
lines := strings.Split(string(out), "\n")
loadedMods := make([]string, len(lines))
for _, line := range lines {
// skip empty lines
if len(line) == 0 {
continue
}
// append loaded module
loadedMods = append(loadedMods, strings.Fields(line)[0])
}
return loadedMods, nil
}

61
source/kernel/version.go Normal file
View file

@ -0,0 +1,61 @@
/*
Copyright 2018-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 kernel
import (
"io/ioutil"
"regexp"
"strings"
)
// Read and parse kernel version
func parseVersion() (map[string]string, error) {
version := map[string]string{}
full, err := getVersion()
if err != nil {
return nil, err
}
// Replace forbidden symbols
fullRegex := regexp.MustCompile("[^-A-Za-z0-9_.]")
full = fullRegex.ReplaceAllString(full, "_")
// Label values must start and end with an alphanumeric
full = strings.Trim(full, "-_.")
version["full"] = full
// Regexp for parsing version components
re := regexp.MustCompile(`^(?P<major>\d+)(\.(?P<minor>\d+))?(\.(?P<revision>\d+))?(-.*)?$`)
if m := re.FindStringSubmatch(full); m != nil {
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
version[name] = m[i]
}
}
}
return version, nil
}
func getVersion() (string, error) {
unameRaw, err := ioutil.ReadFile("/proc/sys/kernel/osrelease")
if err != nil {
return "", err
}
return strings.TrimSpace(string(unameRaw)), nil
}

View file

@ -27,24 +27,31 @@ import (
"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"
)
const Name = "local"
const LabelFeature = "label"
// Config
var (
featureFilesDir = "/etc/kubernetes/node-feature-discovery/features.d/"
hookDir = "/etc/kubernetes/node-feature-discovery/source.d/"
)
// localSource implements the LabelSource interface.
type localSource struct{}
// localSource implements the FeatureSource and LabelSource interfaces.
type localSource struct {
features *feature.DomainFeatures
}
// Singleton source instance
var (
src localSource
_ source.LabelSource = &src
_ source.FeatureSource = &src
_ source.LabelSource = &src
)
// Name method of the LabelSource interface
@ -55,6 +62,19 @@ func (s *localSource) Priority() int { return 20 }
// GetLabels method of the LabelSource interface
func (s *localSource) GetLabels() (source.FeatureLabels, error) {
labels := make(source.FeatureLabels)
features := s.GetFeatures()
for k, v := range features.Values[LabelFeature].Elements {
labels[k] = v
}
return labels, nil
}
// Discover method of the FeatureSource interface
func (s *localSource) Discover() error {
s.features = feature.NewDomainFeatures()
featuresFromHooks, err := getFeaturesFromHooks()
if err != nil {
klog.Error(err)
@ -68,17 +88,28 @@ func (s *localSource) GetLabels() (source.FeatureLabels, error) {
// Merge features from hooks and files
for k, v := range featuresFromHooks {
if old, ok := featuresFromFiles[k]; ok {
klog.Warningf("overriding label '%s': value changed from '%s' to '%s'",
klog.Warningf("overriding '%s': value changed from '%s' to '%s'",
k, old, v)
}
featuresFromFiles[k] = v
}
s.features.Values[LabelFeature] = feature.NewValueFeatures(featuresFromFiles)
return featuresFromFiles, nil
utils.KlogDump(3, "discovered local features:", " ", s.features)
return nil
}
func parseFeatures(lines [][]byte, prefix string) source.FeatureLabels {
features := source.FeatureLabels{}
// GetFeatures method of the FeatureSource Interface
func (s *localSource) GetFeatures() *feature.DomainFeatures {
if s.features == nil {
s.features = feature.NewDomainFeatures()
}
return s.features
}
func parseFeatures(lines [][]byte, prefix string) map[string]string {
features := make(map[string]string)
for _, line := range lines {
if len(line) > 0 {
@ -109,8 +140,8 @@ func parseFeatures(lines [][]byte, prefix string) source.FeatureLabels {
}
// Run all hooks and get features
func getFeaturesFromHooks() (source.FeatureLabels, error) {
features := source.FeatureLabels{}
func getFeaturesFromHooks() (map[string]string, error) {
features := make(map[string]string)
files, err := ioutil.ReadDir(hookDir)
if err != nil {
@ -184,8 +215,8 @@ func runHook(file string) ([][]byte, error) {
}
// Read all files to get features
func getFeaturesFromFiles() (source.FeatureLabels, error) {
features := source.FeatureLabels{}
func getFeaturesFromFiles() (map[string]string, error) {
features := make(map[string]string)
files, err := ioutil.ReadDir(featureFilesDir)
if err != nil {

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 local
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
func TestLocalSource(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

@ -22,12 +22,15 @@ import (
"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"
pciutils "sigs.k8s.io/node-feature-discovery/source/internal"
)
const Name = "pci"
const DeviceFeature = "device"
type Config struct {
DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"`
DeviceLabelFields []string `json:"deviceLabelFields,omitempty"`
@ -41,14 +44,16 @@ func newDefaultConfig() *Config {
}
}
// pciSource implements the LabelSource and ConfigurableSource interfaces.
// pciSource implements the FeatureSource, LabelSource and ConfigurableSource interfaces.
type pciSource struct {
config *Config
config *Config
features *feature.DomainFeatures
}
// Singleton source instance
var (
src pciSource
src = pciSource{config: newDefaultConfig()}
_ source.FeatureSource = &src
_ source.LabelSource = &src
_ source.ConfigurableSource = &src
)
@ -77,16 +82,17 @@ func (s *pciSource) Priority() int { return 0 }
// GetLabels method of the LabelSource interface
func (s *pciSource) GetLabels() (source.FeatureLabels, error) {
features := source.FeatureLabels{}
labels := source.FeatureLabels{}
features := s.GetFeatures()
// Construct a device label format, a sorted list of valid attributes
deviceLabelFields := []string{}
configLabelFields := map[string]bool{}
deviceLabelFields := make([]string, 0)
configLabelFields := make(map[string]struct{}, len(s.config.DeviceLabelFields))
for _, field := range s.config.DeviceLabelFields {
configLabelFields[field] = true
configLabelFields[field] = struct{}{}
}
for _, attr := range pciutils.DefaultPciDevAttrs {
for _, attr := range mandatoryDevAttrs {
if _, ok := configLabelFields[attr]; ok {
deviceLabelFields = append(deviceLabelFields, attr)
delete(configLabelFields, attr)
@ -97,50 +103,59 @@ func (s *pciSource) GetLabels() (source.FeatureLabels, error) {
for key := range configLabelFields {
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 {
klog.Warningf("no valid fields in deviceLabelFields defined, using the defaults")
deviceLabelFields = []string{"class", "vendor"}
}
// Read extraDevAttrs + configured or default labels. Attributes
// set to 'true' are considered must-have.
deviceAttrs := map[string]bool{}
for _, label := range pciutils.ExtraPciDevAttrs {
deviceAttrs[label] = false
}
for _, label := range deviceLabelFields {
deviceAttrs[label] = true
}
devs, err := pciutils.DetectPci(deviceAttrs)
if err != nil {
return nil, fmt.Errorf("failed to detect PCI devices: %s", err.Error())
}
// 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 {
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
if _, ok := dev["sriov_totalvfs"]; ok {
features[devLabel+".sriov.capable"] = true
if strings.HasPrefix(string(class), strings.ToLower(white)) {
devLabel := ""
for i, attr := range deviceLabelFields {
devLabel += attrs[attr]
if i < len(deviceLabelFields)-1 {
devLabel += "_"
}
}
labels[devLabel+".present"] = true
if _, ok := attrs["sriov_totalvfs"]; ok {
labels[devLabel+".sriov.capable"] = true
}
break
}
}
}
return features, nil
return labels, nil
}
// Discover method of the FeatureSource interface
func (s *pciSource) Discover() error {
s.features = feature.NewDomainFeatures()
devs, err := detectPci()
if err != nil {
return fmt.Errorf("failed to detect PCI devices: %s", err.Error())
}
s.features.Instances[DeviceFeature] = feature.NewInstanceFeatures(devs)
utils.KlogDump(3, "discovered pci features:", " ", s.features)
return nil
}
// GetFeatures method of the FeatureSource Interface
func (s *pciSource) GetFeatures() *feature.DomainFeatures {
if s.features == nil {
s.features = feature.NewDomainFeatures()
}
return s.features
}
func init() {

36
source/pci/pci_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 pci
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
func TestPciSource(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,28 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package busutils
package pci
import (
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
"sigs.k8s.io/node-feature-discovery/source"
)
type PciDeviceInfo map[string]string
var DefaultPciDevAttrs = []string{"class", "vendor", "device", "subsystem_vendor", "subsystem_device"}
var ExtraPciDevAttrs = []string{"sriov_totalvfs"}
var mandatoryDevAttrs = []string{"class", "vendor", "device", "subsystem_vendor", "subsystem_device"}
var optionalDevAttrs = []string{"sriov_totalvfs"}
// Read a single PCI device attribute
// A PCI attribute in this context, maps to the corresponding sysfs file
func readSinglePciAttribute(devPath string, attrName string) (string, error) {
data, err := ioutil.ReadFile(path.Join(devPath, attrName))
data, err := ioutil.ReadFile(filepath.Join(devPath, attrName))
if err != nil {
return "", fmt.Errorf("failed to read device attribute %s: %v", attrName, err)
}
@ -51,49 +50,43 @@ func readSinglePciAttribute(devPath string, attrName string) (string, error) {
}
// Read information of one PCI device
func readPciDevInfo(devPath string, deviceAttrSpec map[string]bool) (PciDeviceInfo, error) {
info := PciDeviceInfo{}
for attr, must := range deviceAttrSpec {
func readPciDevInfo(devPath string) (*feature.InstanceFeature, error) {
attrs := make(map[string]string)
for _, attr := range mandatoryDevAttrs {
attrVal, err := readSinglePciAttribute(devPath, attr)
if err != nil {
if must {
return info, fmt.Errorf("failed to read device %s: %s", attr, err)
}
continue
return nil, fmt.Errorf("failed to read device %s: %s", attr, err)
}
info[attr] = attrVal
attrs[attr] = attrVal
}
return info, nil
for _, attr := range optionalDevAttrs {
attrVal, err := readSinglePciAttribute(devPath, attr)
if err == nil {
attrs[attr] = attrVal
}
}
return feature.NewInstanceFeature(attrs), nil
}
// DetectPci lists available PCI 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.
// will fail if the retrieval of a mandatory attribute fails.
func DetectPci(deviceAttrSpec map[string]bool) (map[string][]PciDeviceInfo, error) {
// detectPci detects available PCI devices and retrieves their device attributes.
// An error is returned if reading any of the mandatory attributes fails.
func detectPci() ([]feature.InstanceFeature, error) {
sysfsBasePath := source.SysfsDir.Path("bus/pci/devices")
devInfo := make(map[string][]PciDeviceInfo)
devices, err := ioutil.ReadDir(sysfsBasePath)
if err != nil {
return nil, err
}
// "class" is a mandatory attribute, inject it to spec if needed.
deviceAttrSpec["class"] = true
// Iterate over devices
devInfo := make([]feature.InstanceFeature, 0, len(devices))
for _, device := range devices {
info, err := readPciDevInfo(path.Join(sysfsBasePath, device.Name()), deviceAttrSpec)
info, err := readPciDevInfo(filepath.Join(sysfsBasePath, device.Name()))
if err != nil {
klog.Error(err)
continue
}
class := info["class"]
devInfo[class] = append(devInfo[class], info)
devInfo = append(devInfo, *info)
}
return devInfo, nil

View file

@ -65,6 +65,7 @@ func TestConfigurableSources(t *testing.T) {
func TestFeatureSources(t *testing.T) {
sources := source.GetAllFeatureSources()
assert.NotZero(t, len(sources))
for n, s := range sources {
msg := fmt.Sprintf("testing FeatureSource %q failed", n)

View file

@ -24,23 +24,35 @@ import (
"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"
)
var osReleaseFields = [...]string{
"ID",
"VERSION_ID",
"VERSION_ID.major",
"VERSION_ID.minor",
}
const Name = "system"
// systemSource implements the LabelSource interface.
type systemSource struct{}
const (
OsReleaseFeature = "osrelease"
NameFeature = "name"
)
// systemSource implements the FeatureSource and LabelSource interfaces.
type systemSource struct {
features *feature.DomainFeatures
}
// Singleton source instance
var (
src systemSource
_ source.LabelSource = &src
_ source.FeatureSource = &src
_ source.LabelSource = &src
)
func (s *systemSource) Name() string { return Name }
@ -50,29 +62,54 @@ func (s *systemSource) Priority() int { return 0 }
// GetLabels method of the LabelSource interface
func (s *systemSource) GetLabels() (source.FeatureLabels, error) {
features := source.FeatureLabels{}
labels := source.FeatureLabels{}
features := s.GetFeatures()
for _, key := range osReleaseFields {
if value, exists := features.Values[OsReleaseFeature].Elements[key]; exists {
feature := "os_release." + key
labels[feature] = value
}
}
return labels, nil
}
// Discover method of the FeatureSource interface
func (s *systemSource) Discover() error {
s.features = feature.NewDomainFeatures()
// Get node name
s.features.Values[NameFeature] = feature.NewValueFeatures(nil)
s.features.Values[NameFeature].Elements["nodename"] = os.Getenv("NODE_NAME")
// Get os-release information
release, err := parseOSRelease()
if err != nil {
klog.Errorf("failed to get os-release: %s", err)
} else {
for _, key := range osReleaseFields {
if value, exists := release[key]; exists {
feature := "os_release." + key
features[feature] = value
s.features.Values[OsReleaseFeature] = feature.NewValueFeatures(release)
if key == "VERSION_ID" {
versionComponents := splitVersion(value)
for subKey, subValue := range versionComponents {
if subValue != "" {
features[feature+"."+subKey] = subValue
}
}
if v, ok := release["VERSION_ID"]; ok {
versionComponents := splitVersion(v)
for subKey, subValue := range versionComponents {
if subValue != "" {
s.features.Values[OsReleaseFeature].Elements["VERSION_ID."+subKey] = subValue
}
}
}
}
return features, nil
utils.KlogDump(3, "discovered system features:", " ", s.features)
return nil
}
// GetFeatures method of the FeatureSource Interface
func (s *systemSource) GetFeatures() *feature.DomainFeatures {
if s.features == nil {
s.features = feature.NewDomainFeatures()
}
return s.features
}
// Read and parse os-release 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 system
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
func TestSystemSource(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

@ -22,12 +22,15 @@ import (
"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"
usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
)
const Name = "usb"
const DeviceFeature = "device"
type Config struct {
DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"`
DeviceLabelFields []string `json:"deviceLabelFields,omitempty"`
@ -46,12 +49,14 @@ func newDefaultConfig() *Config {
// usbSource implements the LabelSource and ConfigurableSource interfaces.
type usbSource struct {
config *Config
config *Config
features *feature.DomainFeatures
}
// Singleton source instance
var (
src usbSource
src = usbSource{config: newDefaultConfig()}
_ source.FeatureSource = &src
_ source.LabelSource = &src
_ source.ConfigurableSource = &src
)
@ -80,7 +85,8 @@ func (s *usbSource) Priority() int { return 0 }
// GetLabels method of the LabelSource interface
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
deviceLabelFields := []string{}
@ -89,7 +95,7 @@ func (s *usbSource) GetLabels() (source.FeatureLabels, error) {
configLabelFields[field] = true
}
for _, attr := range usbutils.DefaultUsbDevAttrs {
for _, attr := range devAttrs {
if _, ok := configLabelFields[attr]; ok {
deviceLabelFields = append(deviceLabelFields, attr)
delete(configLabelFields, attr)
@ -100,42 +106,55 @@ func (s *usbSource) GetLabels() (source.FeatureLabels, error) {
for key := range configLabelFields {
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 {
klog.Warningf("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 _, dev := range features.Instances[DeviceFeature].Elements {
attrs := dev.Attributes
class := attrs["class"]
for _, white := range s.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 += "_"
}
if strings.HasPrefix(string(class), strings.ToLower(white)) {
devLabel := ""
for i, attr := range deviceLabelFields {
devLabel += attrs[attr]
if i < len(deviceLabelFields)-1 {
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() {

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.
*/
package busutils
package usb
import (
"fmt"
@ -24,12 +24,11 @@ import (
"strings"
"k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
type UsbDeviceInfo map[string]string
type UsbClassMap map[string]UsbDeviceInfo
var DefaultUsbDevAttrs = []string{"class", "vendor", "device", "serial"}
var devAttrs = []string{"class", "vendor", "device", "serial"}
// The USB device sysfs files do not have terribly user friendly names, map
// 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
func readUsbDevInfo(devPath string, deviceAttrSpec map[string]bool) (UsbClassMap, error) {
classmap := UsbClassMap{}
info := UsbDeviceInfo{}
func readUsbDevInfo(devPath string) ([]feature.InstanceFeature, error) {
instances := make([]feature.InstanceFeature, 0)
attrs := make(map[string]string)
for attr := range deviceAttrSpec {
for _, attr := range devAttrs {
attrVal, _ := readSingleUsbAttribute(devPath, attr)
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
// is set, return as-is.
if info["class"] != "00" {
classmap[info["class"]] = info
if attrs["class"] != "00" {
instances = append(instances, *feature.NewInstanceFeature(attrs))
} 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
return nil, err
}
// 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
attrVal, err := readSingleUsbSysfsAttribute(intf)
if err != nil {
return classmap, err
return nil, err
}
attr := UsbDeviceInfo{}
for k, v := range info {
attr[k] = v
subdevAttrs := make(map[string]string, len(attrs))
for k, v := range attrs {
subdevAttrs[k] = v
}
subdevAttrs["class"] = attrVal
attr["class"] = attrVal
classmap[attrVal] = attr
instances = append(instances, *feature.NewInstanceFeature(subdevAttrs))
}
}
return classmap, nil
return instances, 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) {
// detectUsb detects available USB devices and retrieves their device attributes.
func detectUsb() ([]feature.InstanceFeature, 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)
const devPathGlob = "/sys/bus/usb/devices/*/idProduct"
devPaths, err := filepath.Glob(devPathGlob)
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)
devInfo := make([]feature.InstanceFeature, 0)
for _, devPath := range devPaths {
devs, err := readUsbDevInfo(filepath.Dir(devPath))
if err != nil {
klog.Error(err)
continue
}
for class, info := range devMap {
devInfo[class] = append(devInfo[class], info)
}
devInfo = append(devInfo, devs...)
}
return devInfo, nil