mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-28 02:37:11 +00:00
Merge pull request #334 from mythi/custom-adds
custom: Add rules for kconfig and cpuid
This commit is contained in:
commit
f0ef38b84e
13 changed files with 324 additions and 103 deletions
41
README.md
41
README.md
|
@ -390,6 +390,36 @@ loadedKMod : [<kernel module>, ...]
|
|||
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 : [<CPUID flag string>, ...]
|
||||
```
|
||||
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: [<kernel config option ('y' or 'm') or '=<value>'>, ...]
|
||||
```
|
||||
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 '=<value>'.
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
42
source/custom/rules/cpuid_rule.go
Normal file
42
source/custom/rules/cpuid_rule.go
Normal file
|
@ -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{}{}
|
||||
}
|
||||
}
|
51
source/custom/rules/kconfig_rule.go
Normal file
51
source/custom/rules/kconfig_rule.go
Normal file
|
@ -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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cpu
|
||||
package cpuidutils
|
||||
|
||||
/*
|
||||
#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 cpu
|
||||
package cpuidutils
|
||||
|
||||
/*
|
||||
#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 cpu
|
||||
package cpuidutils
|
||||
|
||||
/*
|
||||
#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 cpu
|
||||
package cpuidutils
|
||||
|
||||
/*
|
||||
#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++ {
|
121
source/internal/kernelutils/kernel_kconfig.go
Normal file
121
source/internal/kernelutils/kernel_kconfig.go
Normal file
|
@ -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<flag>\w+)=(?P<value>.+)`)
|
||||
|
||||
// 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
|
||||
}
|
30
source/internal/kernelutils/kernel_version.go
Normal file
30
source/internal/kernelutils/kernel_version.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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<flag>\w+)=(?P<value>.+)`)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue