Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

e2e-topology-manager: single-NUMA-node test #87645

Merged
merged 13 commits into from Feb 14, 2020
Merged
1 change: 1 addition & 0 deletions hack/generate-bindata.sh
Expand Up @@ -47,6 +47,7 @@ BINDATA_OUTPUT="test/e2e/generated/bindata.go"
go-bindata -nometadata -o "${BINDATA_OUTPUT}.tmp" -pkg generated \
-ignore .jpg -ignore .png -ignore .md -ignore 'BUILD(\.bazel)?' \
"test/e2e/testing-manifests/..." \
"test/e2e_node/testing-manifests/..." \
"test/images/..." \
"test/fixtures/..."

Expand Down
3 changes: 3 additions & 0 deletions test/e2e/framework/test_context.go
Expand Up @@ -167,6 +167,9 @@ type TestContextType struct {

// ProgressReportURL is the URL which progress updates will be posted to as tests complete. If empty, no updates are sent.
ProgressReportURL string

// SriovdpConfigMapFile is the path to the ConfigMap to configure the SRIOV device plugin on this host.
SriovdpConfigMapFile string
}

// NodeKillerConfig describes configuration of NodeKiller -- a utility to
Expand Down
6 changes: 5 additions & 1 deletion test/e2e/framework/util.go
Expand Up @@ -1878,7 +1878,6 @@ func DumpDebugInfo(c clientset.Interface, ns string) {

// DsFromManifest reads a .json/yaml file and returns the daemonset in it.
func DsFromManifest(url string) (*appsv1.DaemonSet, error) {
var ds appsv1.DaemonSet
Logf("Parsing ds from %v", url)

var response *http.Response
Expand All @@ -1904,7 +1903,12 @@ func DsFromManifest(url string) (*appsv1.DaemonSet, error) {
if err != nil {
return nil, fmt.Errorf("Failed to read html response body: %v", err)
}
return DsFromData(data)
}

// DsFromData reads a byte slice and returns the daemonset in it.
func DsFromData(data []byte) (*appsv1.DaemonSet, error) {
var ds appsv1.DaemonSet
dataJSON, err := utilyaml.ToJSON(data)
if err != nil {
return nil, fmt.Errorf("Failed to parse data to json: %v", err)
Expand Down
1 change: 1 addition & 0 deletions test/e2e/generated/BUILD
Expand Up @@ -24,6 +24,7 @@ go_bindata(
name = "bindata",
srcs = [
"//test/e2e/testing-manifests:all-srcs",
"//test/e2e_node/testing-manifests:all-srcs",
"//test/fixtures:all-srcs",
"//test/images:all-srcs",
],
Expand Down
5 changes: 5 additions & 0 deletions test/e2e_node/BUILD
Expand Up @@ -15,8 +15,10 @@ go_library(
"framework.go",
"image_list.go",
"node_problem_detector_linux.go",
"numa_alignment.go",
"resource_collector.go",
"util.go",
"util_sriov.go",
"util_xfs_linux.go",
"util_xfs_unsupported.go",
],
Expand All @@ -30,6 +32,7 @@ go_library(
"//pkg/kubelet/apis/podresources/v1alpha1:go_default_library",
"//pkg/kubelet/apis/stats/v1alpha1:go_default_library",
"//pkg/kubelet/cm:go_default_library",
"//pkg/kubelet/cm/cpuset:go_default_library",
"//pkg/kubelet/kubeletconfig/util/codec:go_default_library",
"//pkg/kubelet/metrics:go_default_library",
"//pkg/kubelet/remote:go_default_library",
Expand All @@ -49,6 +52,7 @@ go_library(
"//test/e2e/framework/gpu:go_default_library",
"//test/e2e/framework/metrics:go_default_library",
"//test/e2e/framework/node:go_default_library",
"//test/e2e/framework/testfiles:go_default_library",
"//test/utils/image:go_default_library",
"//vendor/github.com/blang/semver:go_default_library",
"//vendor/github.com/coreos/go-systemd/util:go_default_library",
Expand Down Expand Up @@ -266,6 +270,7 @@ filegroup(
"//test/e2e_node/runner/remote:all-srcs",
"//test/e2e_node/services:all-srcs",
"//test/e2e_node/system:all-srcs",
"//test/e2e_node/testing-manifests:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
Expand Down
1 change: 1 addition & 0 deletions test/e2e_node/e2e_node_suite_test.go
Expand Up @@ -80,6 +80,7 @@ func registerNodeFlags(flags *flag.FlagSet) {
flags.StringVar(&framework.TestContext.ImageDescription, "image-description", "", "The description of the image which the test will be running on.")
flags.StringVar(&framework.TestContext.SystemSpecName, "system-spec-name", "", "The name of the system spec (e.g., gke) that's used in the node e2e test. The system specs are in test/e2e_node/system/specs/. This is used by the test framework to determine which tests to run for validating the system requirements.")
flags.Var(cliflag.NewMapStringString(&framework.TestContext.ExtraEnvs), "extra-envs", "The extra environment variables needed for node e2e tests. Format: a list of key=value pairs, e.g., env1=val1,env2=val2")
flags.StringVar(&framework.TestContext.SriovdpConfigMapFile, "sriovdp-configmap-file", "", "The name of the SRIOV device plugin Config Map to load.")
}

func init() {
Expand Down
25 changes: 25 additions & 0 deletions test/e2e_node/image_list.go
Expand Up @@ -31,6 +31,7 @@ import (
commontest "k8s.io/kubernetes/test/e2e/common"
"k8s.io/kubernetes/test/e2e/framework"
"k8s.io/kubernetes/test/e2e/framework/gpu"
"k8s.io/kubernetes/test/e2e/framework/testfiles"
imageutils "k8s.io/kubernetes/test/utils/image"
)

Expand Down Expand Up @@ -68,6 +69,7 @@ func updateImageWhiteList() {
framework.ImageWhiteList = NodeImageWhiteList.Union(commontest.CommonImageWhiteList)
// Images from extra envs
framework.ImageWhiteList.Insert(getNodeProblemDetectorImage())
framework.ImageWhiteList.Insert(getSRIOVDevicePluginImage())
}

func getNodeProblemDetectorImage() string {
Expand Down Expand Up @@ -184,3 +186,26 @@ func getGPUDevicePluginImage() string {
}
return ds.Spec.Template.Spec.Containers[0].Image
}

// getSRIOVDevicePluginImage returns the image of SRIOV device plugin.
func getSRIOVDevicePluginImage() string {
data, err := testfiles.Read(SRIOVDevicePluginDSYAML)
if err != nil {
klog.Errorf("Failed to read the device plugin manifest: %v", err)
return ""
}
ds, err := framework.DsFromData(data)
if err != nil {
klog.Errorf("Failed to parse the device plugin image: %v", err)
return ""
}
if ds == nil {
klog.Errorf("Failed to parse the device plugin image: the extracted DaemonSet is nil")
return ""
}
if len(ds.Spec.Template.Spec.Containers) < 1 {
klog.Errorf("Failed to parse the device plugin image: cannot extract the container from YAML")
return ""
}
return ds.Spec.Template.Spec.Containers[0].Image
}
212 changes: 212 additions & 0 deletions test/e2e_node/numa_alignment.go
@@ -0,0 +1,212 @@
/*
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 e2enode

import (
"fmt"
"io/ioutil"
"sort"
"strconv"
"strings"

v1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/kubelet/cm/cpuset"

"k8s.io/kubernetes/test/e2e/framework"
)

type numaPodResources struct {
CPUToNUMANode map[int]int
PCIDevsToNUMANode map[string]int
}

func (R *numaPodResources) CheckAlignment() bool {
nodeNum := -1 // not set
for _, cpuNode := range R.CPUToNUMANode {
if nodeNum == -1 {
nodeNum = cpuNode
} else if nodeNum != cpuNode {
return false
}
}
for _, devNode := range R.PCIDevsToNUMANode {
// TODO: explain -1
if devNode != -1 && nodeNum != devNode {
return false
}
}
return true
}

func (R *numaPodResources) String() string {
var b strings.Builder
// To store the keys in slice in sorted order
var cpuKeys []int
for ck := range R.CPUToNUMANode {
cpuKeys = append(cpuKeys, ck)
}
sort.Ints(cpuKeys)
for _, k := range cpuKeys {
nodeNum := R.CPUToNUMANode[k]
b.WriteString(fmt.Sprintf("CPU cpu#%03d=%02d\n", k, nodeNum))
}
var pciKeys []string
for pk := range R.PCIDevsToNUMANode {
pciKeys = append(pciKeys, pk)
}
sort.Strings(pciKeys)
for _, k := range pciKeys {
nodeNum := R.PCIDevsToNUMANode[k]
b.WriteString(fmt.Sprintf("PCI %s=%02d\n", k, nodeNum))
}
return b.String()
}

func getCPUsPerNUMANode(nodeNum int) ([]int, error) {
nodeCPUList, err := ioutil.ReadFile(fmt.Sprintf("/sys/devices/system/node/node%d/cpulist", nodeNum))
if err != nil {
return nil, err
}
cpus, err := cpuset.Parse(strings.TrimSpace(string(nodeCPUList)))
if err != nil {
return nil, err
}
return cpus.ToSlice(), nil
}

func getCPUToNUMANodeMapFromEnv(f *framework.Framework, pod *v1.Pod, environ map[string]string, numaNodes int) (map[int]int, error) {
var cpuIDs []int
cpuListAllowedEnvVar := "CPULIST_ALLOWED"

for name, value := range environ {
if name == cpuListAllowedEnvVar {
cpus, err := cpuset.Parse(value)
if err != nil {
return nil, err
}
cpuIDs = cpus.ToSlice()
}
}
if len(cpuIDs) == 0 {
return nil, fmt.Errorf("variable %q found in environ", cpuListAllowedEnvVar)
}

cpusPerNUMA := make(map[int][]int)
for numaNode := 0; numaNode < numaNodes; numaNode++ {
nodeCPUList := f.ExecCommandInContainer(pod.Name, pod.Spec.Containers[0].Name,
"/bin/cat", fmt.Sprintf("/sys/devices/system/node/node%d/cpulist", numaNode))

cpus, err := cpuset.Parse(nodeCPUList)
if err != nil {
return nil, err
}
cpusPerNUMA[numaNode] = cpus.ToSlice()
}

// CPU IDs -> NUMA Node ID
CPUToNUMANode := make(map[int]int)
for nodeNum, cpus := range cpusPerNUMA {
for _, cpu := range cpus {
CPUToNUMANode[cpu] = nodeNum
}
}

// filter out only the allowed CPUs
CPUMap := make(map[int]int)
for _, cpuID := range cpuIDs {
_, ok := CPUToNUMANode[cpuID]
if !ok {
return nil, fmt.Errorf("CPU %d not found on NUMA map: %v", cpuID, CPUToNUMANode)
}
CPUMap[cpuID] = CPUToNUMANode[cpuID]
}
return CPUMap, nil
}

func getPCIDeviceToNumaNodeMapFromEnv(f *framework.Framework, pod *v1.Pod, environ map[string]string) (map[string]int, error) {
pciDevPrefix := "PCIDEVICE_"
// at this point we don't care which plugin selected the device,
// we only need to know which devices were assigned to the POD.
// Hence, do prefix search for the variable and fetch the device(s).

NUMAPerDev := make(map[string]int)
for name, value := range environ {
if !strings.HasPrefix(name, pciDevPrefix) {
continue
}

// a single plugin can allocate more than a single device
pciDevs := strings.Split(value, ",")
for _, pciDev := range pciDevs {
pciDevNUMANode := f.ExecCommandInContainer(pod.Name, pod.Spec.Containers[0].Name,
"/bin/cat", fmt.Sprintf("/sys/bus/pci/devices/%s/numa_node", pciDev))

nodeNum, err := strconv.Atoi(pciDevNUMANode)
if err != nil {
return nil, err
}
NUMAPerDev[pciDev] = nodeNum
}
}
if len(NUMAPerDev) == 0 {
return nil, fmt.Errorf("no PCI devices found in environ")
}
return NUMAPerDev, nil
}

func makeEnvMap(logs string) (map[string]string, error) {
podEnv := strings.Split(logs, "\n")
envMap := make(map[string]string)
for _, envVar := range podEnv {
if len(envVar) == 0 {
continue
}
pair := strings.SplitN(envVar, "=", 2)
if len(pair) != 2 {
return nil, fmt.Errorf("unable to split %q", envVar)
}
envMap[pair[0]] = pair[1]
}
return envMap, nil
}

func checkNUMAAlignment(f *framework.Framework, pod *v1.Pod, logs string, numaNodes int) (numaPodResources, error) {
podEnv, err := makeEnvMap(logs)
if err != nil {
return numaPodResources{}, err
}

CPUToNUMANode, err := getCPUToNUMANodeMapFromEnv(f, pod, podEnv, numaNodes)
if err != nil {
return numaPodResources{}, err
}

PCIDevsToNUMANode, err := getPCIDeviceToNumaNodeMapFromEnv(f, pod, podEnv)
if err != nil {
return numaPodResources{}, err
}

numaRes := numaPodResources{
CPUToNUMANode: CPUToNUMANode,
PCIDevsToNUMANode: PCIDevsToNUMANode,
}
aligned := numaRes.CheckAlignment()
if !aligned {
return numaRes, fmt.Errorf("NUMA resources not aligned")
}
return numaRes, nil
}
14 changes: 14 additions & 0 deletions test/e2e_node/testing-manifests/BUILD
@@ -0,0 +1,14 @@
package(default_visibility = ["//visibility:public"])

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
36 changes: 36 additions & 0 deletions test/e2e_node/testing-manifests/sriovdp-cm.yaml
@@ -0,0 +1,36 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: sriovdp-config
namespace: kube-system
data:
config.json: |
{
"resourceList": [{
"resourceName": "intel_sriov_netdevice",
"selectors": {
"vendors": ["8086"],
"devices": ["154c", "10ed"],
"drivers": ["i40evf", "ixgbevf"]
}
},
{
"resourceName": "intel_sriov_dpdk",
"selectors": {
"vendors": ["8086"],
"devices": ["154c", "10ed"],
"drivers": ["vfio-pci"],
"pfNames": ["enp0s0f0","enp2s2f1"]
}
},
{
"resourceName": "mlnx_sriov_rdma",
"isRdma": true,
"selectors": {
"vendors": ["15b3"],
"devices": ["1018"],
"drivers": ["mlx5_ib"]
}
}
]
}