diff --git a/README.md b/README.md index 99282767b..4b028575d 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,36 @@ loadedKMod : [, ...] Matching is done by performing logical _AND_ for each provided Element, i.e the Rule will match if all provided Elements (kernel modules) are loaded in the system. +##### CpuId Rule +###### Nomenclature +``` +Element :A CPUID flag +``` + +The Rule allows matching the available CPUID flags in the system against a provided list of Elements. + +###### Format +```yaml +cpuId : [, ...] +``` + Matching is done by performing logical _AND_ for each provided Element, i.e the Rule will match if all provided Elements (CPUID flag strings) are available + in the system. + +##### Kconfig Rule +###### Nomenclature +``` +Element :A Kconfig option +``` + +The Rule allows matching the kconfig options in the system against a provided list of Elements. + +###### Format +```yaml +kConfig: ['>, ...] +``` + Matching is done by performing logical _AND_ for each provided Element, i.e the Rule will match if all provided Elements (kernel config options) are enabled ('y' or 'm') or matching '='. + in the kernel. + #### Example ```yaml @@ -419,6 +449,14 @@ custom: - pciId: vendor: ["15b3"] device: ["1014", "1017"] + - name: "my.kernel.featureneedscpu" + matchOn: + - kConfig: ["KVM_INTEL"] + - cpuId: ["VMX"] + - name: "my.kernel.modulecompiler" + matchOn: + - kConfig: ["GCC_VERSION=100101"] + loadedKMod: ["kmod1"] ``` __In the example above:__ @@ -434,6 +472,9 @@ with a PCI vendor ID of `15b3` _AND_ PCI device ID of `1014` _or_ `1017`. - A node would contain the label: `feature.node.kubernetes.io/custom-my.accumulated.feature=true` if `some_kmod1` _AND_ `some_kmod2` kernel modules are loaded __OR__ the node contains a PCI device with a PCI vendor ID of `15b3` _AND_ PCI device ID of `1014` _OR_ `1017`. +- A node would contain the label: `feature.node.kubernetes.io/custom-my.kernel.featureneedscpu=true` +if `KVM_INTEL` kernel config is enabled __AND__ the node CPU supports `VMX` virtual machine extensions +- A node would contain the label: `feature.node.kubernetes.io/custom-my.kernel.modulecompiler=true` if the in-tree `kmod1` kernel module is loaded __AND__ it's built with `GCC_VERSION=100101`. #### Statically defined features diff --git a/source/cpu/cpu.go b/source/cpu/cpu.go index b116feeea..285b780ef 100644 --- a/source/cpu/cpu.go +++ b/source/cpu/cpu.go @@ -21,6 +21,7 @@ import ( "log" "sigs.k8s.io/node-feature-discovery/source" + "sigs.k8s.io/node-feature-discovery/source/internal/cpuidutils" ) // Configuration file options @@ -119,7 +120,7 @@ func (s *Source) Discover() (source.Features, error) { } // Detect CPUID - cpuidFlags := getCpuidFlags() + cpuidFlags := cpuidutils.GetCpuidFlags() for _, f := range cpuidFlags { if s.cpuidFilter.unmask(f) { features["cpuid."+f] = true diff --git a/source/custom/custom.go b/source/custom/custom.go index 67b39ef16..f77e0dcd6 100644 --- a/source/custom/custom.go +++ b/source/custom/custom.go @@ -17,7 +17,6 @@ limitations under the License. package custom import ( - "fmt" "log" "sigs.k8s.io/node-feature-discovery/source" @@ -29,6 +28,8 @@ type MatchRule struct { PciID *rules.PciIDRule `json:"pciId,omitempty"` UsbID *rules.UsbIDRule `json:"usbId,omitempty"` LoadedKMod *rules.LoadedKModRule `json:"loadedKMod,omitempty"` + CpuID *rules.CpuIDRule `json:"cpuId,omitempty"` + Kconfig *rules.KconfigRule `json:"kConfig,omitempty"` } type FeatureSpec struct { @@ -76,7 +77,8 @@ func (s Source) Discover() (source.Features, error) { for _, customFeature := range allFeatureConfig { featureExist, err := s.discoverFeature(customFeature) if err != nil { - return features, fmt.Errorf("failed to discover feature: %s. %s", customFeature.Name, err.Error()) + log.Printf("ERROR: failed to discover feature: %q: %s", customFeature.Name, err.Error()) + continue } if featureExist { features[customFeature.Name] = true @@ -119,6 +121,26 @@ func (s Source) discoverFeature(feature FeatureSpec) (bool, error) { continue } } + // cpuid rule + if rule.CpuID != nil { + match, err := rule.CpuID.Match() + if err != nil { + return false, err + } + if !match { + continue + } + } + // kconfig rule + if rule.Kconfig != nil { + match, err := rule.Kconfig.Match() + if err != nil { + return false, err + } + if !match { + continue + } + } return true, nil } return false, nil diff --git a/source/custom/rules/cpuid_rule.go b/source/custom/rules/cpuid_rule.go new file mode 100644 index 000000000..c7a070887 --- /dev/null +++ b/source/custom/rules/cpuid_rule.go @@ -0,0 +1,42 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "sigs.k8s.io/node-feature-discovery/source/internal/cpuidutils" +) + +// CpuIDRule implements Rule +type CpuIDRule []string + +var cpuIdFlags map[string]struct{} + +func (cpuids *CpuIDRule) Match() (bool, error) { + for _, f := range *cpuids { + if _, ok := cpuIdFlags[f]; !ok { + return false, nil + } + } + return true, nil +} + +func init() { + cpuIdFlags = make(map[string]struct{}) + for _, f := range cpuidutils.GetCpuidFlags() { + cpuIdFlags[f] = struct{}{} + } +} diff --git a/source/custom/rules/kconfig_rule.go b/source/custom/rules/kconfig_rule.go new file mode 100644 index 000000000..85b2a3083 --- /dev/null +++ b/source/custom/rules/kconfig_rule.go @@ -0,0 +1,51 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "sigs.k8s.io/node-feature-discovery/source/internal/kernelutils" +) + +// KconfigRule implements Rule +type KconfigRule []string + +var kConfigs map[string]struct{} + +func (kconfigs *KconfigRule) Match() (bool, error) { + for _, f := range *kconfigs { + if _, ok := kConfigs[f]; !ok { + return false, nil + } + } + return true, nil +} + +func init() { + kConfigs = make(map[string]struct{}) + + kconfig, err := kernelutils.ParseKconfig("") + if err == nil { + for k, v := range kconfig { + if v != "true" { + kConfigs[fmt.Sprintf("%s=%s", k, v)] = struct{}{} + } else { + kConfigs[k] = struct{}{} + } + } + } +} diff --git a/source/cpu/cpuid_amd64.go b/source/internal/cpuidutils/cpuid_amd64.go similarity index 93% rename from source/cpu/cpuid_amd64.go rename to source/internal/cpuidutils/cpuid_amd64.go index 67a919223..9e40e3b6b 100644 --- a/source/cpu/cpuid_amd64.go +++ b/source/internal/cpuidutils/cpuid_amd64.go @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cpu +package cpuidutils import ( "github.com/klauspost/cpuid" ) // Discover returns feature names for all the supported CPU features. -func getCpuidFlags() []string { +func GetCpuidFlags() []string { return cpuid.CPU.Features.Strings() } diff --git a/source/cpu/cpuid_arm.go b/source/internal/cpuidutils/cpuid_arm.go similarity index 98% rename from source/cpu/cpuid_arm.go rename to source/internal/cpuidutils/cpuid_arm.go index 6a142e913..bc9a6632d 100644 --- a/source/cpu/cpuid_arm.go +++ b/source/internal/cpuidutils/cpuid_arm.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cpu +package cpuidutils /* #include @@ -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++ { diff --git a/source/cpu/cpuid_arm64.go b/source/internal/cpuidutils/cpuid_arm64.go similarity index 98% rename from source/cpu/cpuid_arm64.go rename to source/internal/cpuidutils/cpuid_arm64.go index e4175ae49..0d307b82a 100644 --- a/source/cpu/cpuid_arm64.go +++ b/source/internal/cpuidutils/cpuid_arm64.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cpu +package cpuidutils /* #include @@ -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++ { diff --git a/source/cpu/cpuid_ppc64le.go b/source/internal/cpuidutils/cpuid_ppc64le.go similarity index 99% rename from source/cpu/cpuid_ppc64le.go rename to source/internal/cpuidutils/cpuid_ppc64le.go index 307739717..d56152a22 100644 --- a/source/cpu/cpuid_ppc64le.go +++ b/source/internal/cpuidutils/cpuid_ppc64le.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cpu +package cpuidutils /* #include @@ -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()) diff --git a/source/cpu/cpuid_s390x.go b/source/internal/cpuidutils/cpuid_s390x.go similarity index 97% rename from source/cpu/cpuid_s390x.go rename to source/internal/cpuidutils/cpuid_s390x.go index fbd47098d..6a4a02ea8 100644 --- a/source/cpu/cpuid_s390x.go +++ b/source/internal/cpuidutils/cpuid_s390x.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cpu +package cpuidutils /* #include @@ -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++ { diff --git a/source/internal/kernelutils/kernel_kconfig.go b/source/internal/kernelutils/kernel_kconfig.go new file mode 100644 index 000000000..9d8187d28 --- /dev/null +++ b/source/internal/kernelutils/kernel_kconfig.go @@ -0,0 +1,121 @@ +/* +Copyright 2018-2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kernelutils + +import ( + "bytes" + "compress/gzip" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/node-feature-discovery/source" +) + +// Read gzipped kernel config +func readKconfigGzip(filename string) ([]byte, error) { + // Open file for reading + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + // Uncompress data + r, err := gzip.NewReader(f) + if err != nil { + return nil, err + } + defer r.Close() + + return ioutil.ReadAll(r) +} + +// Read kconfig into a map +func ParseKconfig(configPath string) (map[string]string, error) { + kconfig := map[string]string{} + raw := []byte(nil) + var err error + var searchPaths []string + + kVer, err := GetKernelVersion() + if err != nil { + searchPaths = []string{ + "/proc/config.gz", + "/usr/src/linux/.config", + } + } else { + // from k8s.io/system-validator used by kubeadm + // preflight checks + searchPaths = []string{ + "/proc/config.gz", + "/usr/src/linux-" + kVer + "/.config", + "/usr/src/linux/.config", + "/usr/lib/modules/" + kVer + "/config", + "/usr/lib/ostree-boot/config-" + kVer, + "/usr/lib/kernel/config-" + kVer, + "/usr/src/linux-headers-" + kVer + "/.config", + "/lib/modules/" + kVer + "/build/.config", + source.BootDir.Path("config-" + kVer), + } + } + + for _, path := range append([]string{configPath}, searchPaths...) { + if len(path) > 0 { + if ".gz" == filepath.Ext(path) { + if raw, err = readKconfigGzip(path); err == nil { + break + } + } else { + if raw, err = ioutil.ReadFile(path); err == nil { + break + } + } + } + } + + if raw == nil { + return nil, fmt.Errorf("Failed to read kernel config from %+v:", append([]string{configPath}, searchPaths...)) + } + + // Regexp for matching kconfig flags + re := regexp.MustCompile(`^CONFIG_(?P\w+)=(?P.+)`) + + // Process data, line-by-line + lines := bytes.Split(raw, []byte("\n")) + for _, line := range lines { + if m := re.FindStringSubmatch(string(line)); m != nil { + if m[2] == "y" || m[2] == "m" { + kconfig[m[1]] = "true" + } else { + value := strings.Trim(m[2], `"`) + if len(value) > validation.LabelValueMaxLength { + log.Printf("WARNING: ignoring kconfig option '%s': value exceeds max length of %d characters", m[1], validation.LabelValueMaxLength) + continue + } + kconfig[m[1]] = value + } + } + } + + return kconfig, nil +} diff --git a/source/internal/kernelutils/kernel_version.go b/source/internal/kernelutils/kernel_version.go new file mode 100644 index 000000000..2da110e0d --- /dev/null +++ b/source/internal/kernelutils/kernel_version.go @@ -0,0 +1,30 @@ +/* +Copyright 2018-2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kernelutils + +import ( + "io/ioutil" + "strings" +) + +func GetKernelVersion() (string, error) { + unameRaw, err := ioutil.ReadFile("/proc/sys/kernel/osrelease") + if err != nil { + return "", err + } + return strings.TrimSpace(string(unameRaw)), nil +} diff --git a/source/kernel/kernel.go b/source/kernel/kernel.go index 6592afe2f..c1334256f 100644 --- a/source/kernel/kernel.go +++ b/source/kernel/kernel.go @@ -17,16 +17,11 @@ limitations under the License. package kernel import ( - "bytes" - "compress/gzip" - "io/ioutil" "log" - "os" "regexp" - "strings" - "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/node-feature-discovery/source" + "sigs.k8s.io/node-feature-discovery/source/internal/kernelutils" ) // Configuration file options @@ -85,7 +80,7 @@ func (s *Source) Discover() (source.Features, error) { } // Read kconfig - kconfig, err := s.parseKconfig() + kconfig, err := kernelutils.ParseKconfig(s.config.KconfigFile) if err != nil { log.Printf("ERROR: Failed to read kconfig: %s", err) } @@ -111,14 +106,11 @@ func (s *Source) Discover() (source.Features, error) { func parseVersion() (map[string]string, error) { version := map[string]string{} - // Open file for reading - raw, err := ioutil.ReadFile("/proc/sys/kernel/osrelease") + full, err := kernelutils.GetKernelVersion() if err != nil { return nil, err } - full := strings.TrimSpace(string(raw)) - // Replace forbidden symbols fullRegex := regexp.MustCompile("[^-A-Za-z0-9_.]") full = fullRegex.ReplaceAllString(full, "_") @@ -137,82 +129,3 @@ func parseVersion() (map[string]string, error) { return version, nil } - -// Read gzipped kernel config -func readKconfigGzip(filename string) ([]byte, error) { - // Open file for reading - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - - // Uncompress data - r, err := gzip.NewReader(f) - if err != nil { - return nil, err - } - defer r.Close() - - return ioutil.ReadAll(r) -} - -// Read kconfig into a map -func (s *Source) parseKconfig() (map[string]string, error) { - kconfig := map[string]string{} - raw := []byte(nil) - var err error - - // First, try kconfig specified in the config file - if len(s.config.KconfigFile) > 0 { - raw, err = ioutil.ReadFile(s.config.KconfigFile) - if err != nil { - log.Printf("ERROR: Failed to read kernel config from %s: %s", s.config.KconfigFile, err) - } - } - - // Then, try to read from /proc - if raw == nil { - raw, err = readKconfigGzip("/proc/config.gz") - if err != nil { - log.Printf("Failed to read /proc/config.gz: %s", err) - } - } - - // Last, try to read from /boot/ - if raw == nil { - // Get kernel version - unameRaw, err := ioutil.ReadFile("/proc/sys/kernel/osrelease") - uname := strings.TrimSpace(string(unameRaw)) - if err != nil { - return nil, err - } - // Read kconfig - raw, err = ioutil.ReadFile(source.BootDir.Path("config-" + uname)) - if err != nil { - return nil, err - } - } - - // Regexp for matching kconfig flags - re := regexp.MustCompile(`^CONFIG_(?P\w+)=(?P.+)`) - - // Process data, line-by-line - lines := bytes.Split(raw, []byte("\n")) - for _, line := range lines { - if m := re.FindStringSubmatch(string(line)); m != nil { - if m[2] == "y" || m[2] == "m" { - kconfig[m[1]] = "true" - } else { - value := strings.Trim(m[2], `"`) - if len(value) > validation.LabelValueMaxLength { - log.Printf("WARNING: ignoring kconfig option '%s': value exceeds max length of %d characters", m[1], validation.LabelValueMaxLength) - continue - } - kconfig[m[1]] = value - } - } - } - - return kconfig, nil -}