mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-04-23 20:57:10 +00:00
Merge pull request #604 from marquiz/devel/feature-source-conversion
source: implement FeatureSource interface
This commit is contained in:
commit
5299ca2ab4
35 changed files with 831 additions and 396 deletions
pkg/api/feature
source
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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++ {
|
|
@ -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++ {
|
|
@ -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())
|
|
@ -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++ {
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -58,7 +58,7 @@ type customSource struct {
|
|||
|
||||
// Singleton source instance
|
||||
var (
|
||||
src customSource
|
||||
src = customSource{config: newDefaultConfig()}
|
||||
_ source.LabelSource = &src
|
||||
_ source.ConfigurableSource = &src
|
||||
)
|
||||
|
|
|
@ -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{}{}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
|
@ -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() {
|
||||
|
|
36
source/kernel/kernel_test.go
Normal file
36
source/kernel/kernel_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 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
44
source/kernel/modules.go
Normal 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
61
source/kernel/version.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
36
source/local/local_test.go
Normal file
36
source/local/local_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 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)
|
||||
|
||||
}
|
|
@ -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
36
source/pci/pci_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 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)
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
36
source/system/system_test.go
Normal file
36
source/system/system_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 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)
|
||||
|
||||
}
|
|
@ -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
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.
|
||||
*/
|
||||
|
||||
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
|
Loading…
Add table
Add a link
Reference in a new issue