1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-05 08:17:04 +00:00

Support feature detector hooks

Implement a new feature source named 'local' whose only purpose is to
run feature source hooks found under
/etc/kubernetes/node-feature-discovery/source.d/ It tries to execute all
files found under the directory, in alphabetical order.

This feature source provides users a mechanism to implement custom
feature sources in a pluggable way, without modifying nfd source code or
Docker images.

The hooks are supposed to print all discovered features in stdout, one
feature per line. The output in stdout is used in the node label as is.
Full node label name will have the following format:
  feature.node.kubernetes.io/<hook name>-<feature name>
Stderr from the hooks is propagated to nfd log.
This commit is contained in:
Markus Lehtonen 2018-07-13 15:55:28 +03:00
parent 0259f0652a
commit a84b5c9d82
4 changed files with 174 additions and 6 deletions

View file

@ -54,7 +54,7 @@ node-feature-discovery.
will override settings read from the config file.
[Default: ]
--sources=<sources> Comma separated list of feature sources.
[Default: cpuid,iommu,kernel,memory,network,pci,pstate,rdt,selinux,storage]
[Default: cpuid,iommu,kernel,local,memory,network,pci,pstate,rdt,selinux,storage]
--no-publish Do not publish discovered features to the
cluster-local Kubernetes API server.
--label-whitelist=<pattern> Regular expression to filter label names to
@ -80,6 +80,7 @@ The current set of feature sources are the following:
- [CPUID][cpuid] for x86/Arm64 CPU details
- IOMMU
- Kernel
- Local (user-specific features)
- Memory
- Network
- Pstate ([Intel P-State driver][intel-pstate])
@ -122,7 +123,8 @@ the only label value published for features is the string `"true"`._
"feature.node.kubernetes.io/nfd-pstate-<feature-name>": "true",
"feature.node.kubernetes.io/nfd-rdt-<feature-name>": "true",
"feature.node.kubernetes.io/nfd-selinux-<feature-name>": "true",
"feature.node.kubernetes.io/nfd-storage-<feature-name>": "true"
"feature.node.kubernetes.io/nfd-storage-<feature-name>": "true",
"feature.node.kubernetes.io/<hook name>-<feature name>": "true"
}
```
@ -176,6 +178,47 @@ such as restricting discovered features with the --label-whitelist option._
| <br> | minor | Second component of the kernel version (e.g. '5')
| <br> | revision | Third component of the kernel version (e.g. '6')
### Local (User-specific Features)
NFD has a special feature source named *local* which is designed for running
user-specific feature detector hooks. It provides a mechanism for users to
implement custom feature sources in a pluggable way, without modifying nfd
source code or Docker images. The local feature source can be used to advertise
new user-specific features, and, for overriding labels created by the other
feature sources.
The *local* feature source tries to execute files found under
`/etc/kubernetes/node-feature-discovery/source.d/` directory. The hooks must be
available inside the Docker image so Volumes and VolumeMounts must be used if
standard NFD images are used.
The hook files must be executable. When executed, the hooks are supposed to
print all discovered features in `stdout`, one feature per line. The output in
stdout is used in the node label as is. Unlike other feature sources, the
source name (i.e. `local`) is not used as a prefix in the label. The full name
of node label name will conform to the following convention:
`feature.node.kubernetes.io/<hook name>-<feature name>`.
`stderr` output of the hooks is propagated to NFD log so it can be used for
debugging and logging.
**An example:**
User has a shell script
`/etc/kubernetes/node-feature-discovery/source.d/my-source` which has the
following `stdout` output:
```
MY_FEATURE_1
MY_FEATURE_2
```
which, in turn, will translate into the following node labels:
```
feature.node.kubernetes.io/my-source-MY_FEATURE_1=true
feature.node.kubernetes.io/my-source-MY_FEATURE_2=true
```
**NOTE!** NFD will blindly run any executables placed/mounted in the hooks
directory. It is the user's responsibility to review the hooks for e.g.
possible security implications.
### Memory Features
| Feature name | Description |

14
main.go
View file

@ -17,6 +17,7 @@ import (
"github.com/kubernetes-incubator/node-feature-discovery/source/fake"
"github.com/kubernetes-incubator/node-feature-discovery/source/iommu"
"github.com/kubernetes-incubator/node-feature-discovery/source/kernel"
"github.com/kubernetes-incubator/node-feature-discovery/source/local"
"github.com/kubernetes-incubator/node-feature-discovery/source/memory"
"github.com/kubernetes-incubator/node-feature-discovery/source/network"
"github.com/kubernetes-incubator/node-feature-discovery/source/panic_fake"
@ -179,7 +180,7 @@ func argsParse(argv []string) (args Args) {
will override settings read from the config file.
[Default: ]
--sources=<sources> Comma separated list of feature sources.
[Default: cpuid,iommu,kernel,memory,network,pci,pstate,rdt,selinux,storage]
[Default: cpuid,iommu,kernel,local,memory,network,pci,pstate,rdt,selinux,storage]
--no-publish Do not publish discovered features to the
cluster-local Kubernetes API server.
--label-whitelist=<pattern> Regular expression to filter label names to
@ -266,6 +267,9 @@ func configureParameters(sourcesWhiteList []string, labelWhiteListStr string) (e
rdt.Source{},
selinux.Source{},
storage.Source{},
// local needs to be the last source so that it is able to override
// labels from other sources
local.Source{},
}
enabledSources = []source.FeatureSource{}
@ -351,7 +355,13 @@ func getFeatureLabels(source source.FeatureSource) (labels Labels, err error) {
return nil, err
}
for k := range features {
labels[fmt.Sprintf("%s-%s", source.Name(), k)] = fmt.Sprintf("%v", features[k])
prefix := source.Name() + "-"
switch source.(type) {
case local.Source:
// Do not prefix labels from the hooks
prefix = ""
}
labels[fmt.Sprintf("%s%s", prefix, k)] = fmt.Sprintf("%v", features[k])
}
return labels, nil
}

View file

@ -152,7 +152,7 @@ func TestArgsParse(t *testing.T) {
So(args.sleepInterval, ShouldEqual, 60*time.Second)
So(args.noPublish, ShouldBeTrue)
So(args.oneshot, ShouldBeTrue)
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "local", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
So(len(args.labelWhiteList), ShouldEqual, 0)
})
})
@ -174,7 +174,7 @@ func TestArgsParse(t *testing.T) {
Convey("args.labelWhiteList is set to appropriate value and args.sources is set to default value", func() {
So(args.noPublish, ShouldBeFalse)
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
So(args.sources, ShouldResemble, []string{"cpuid", "iommu", "kernel", "local", "memory", "network", "pci", "pstate", "rdt", "selinux", "storage"})
So(args.labelWhiteList, ShouldResemble, ".*rdt.*")
})
})

115
source/local/local.go Normal file
View file

@ -0,0 +1,115 @@
/*
Copyright 2018 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 (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/kubernetes-incubator/node-feature-discovery/source"
)
// Config
var (
hookDir = "/etc/kubernetes/node-feature-discovery/source.d/"
logger = log.New(os.Stderr, "", log.LstdFlags)
)
// Implement FeatureSource interface
type Source struct{}
func (s Source) Name() string { return "local" }
func (s Source) Discover() (source.Features, error) {
features := source.Features{}
files, err := ioutil.ReadDir(hookDir)
if err != nil {
if os.IsNotExist(err) {
log.Printf("ERROR: hook directory %v does not exist", hookDir)
return features, nil
}
return features, fmt.Errorf("Unable to access %v: %v", hookDir, err)
}
for _, file := range files {
hook := file.Name()
hookFeatures, err := runHook(hook)
if err != nil {
log.Printf("ERROR: source hook '%v' failed: %v", hook, err)
continue
}
for _, feature := range hookFeatures {
features[hook+"-"+feature] = true
}
}
return features, nil
}
// Run one hook
func runHook(file string) ([]string, error) {
var features []string
path := filepath.Join(hookDir, file)
filestat, err := os.Stat(path)
if err != nil {
log.Printf("ERROR: skipping %v, failed to get stat: %v", path, err)
return features, err
}
if filestat.Mode().IsRegular() {
cmd := exec.Command(path)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
// Run hook
err = cmd.Run()
// Forward stderr to our logger
lines := bytes.Split(stderr.Bytes(), []byte("\n"))
for i, line := range lines {
if i == len(lines)-1 && len(line) == 0 {
// Don't print the last empty string
break
}
log.Printf("%v: %s", file, line)
}
// Do not return any features if an error occurred
if err != nil {
return features, err
}
// Return features printed to stdout
lines = bytes.Split(stdout.Bytes(), []byte("\n"))
for _, line := range lines {
if len(line) > 0 {
features = append(features, string(line))
}
}
}
return features, nil
}