k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e_node/cpu_manager_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package e2enode
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os/exec"
    23  	"regexp"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	v1 "k8s.io/api/core/v1"
    29  	"k8s.io/apimachinery/pkg/api/resource"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
    32  	"k8s.io/kubelet/pkg/types"
    33  	kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
    34  	"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager"
    35  	admissionapi "k8s.io/pod-security-admission/api"
    36  	"k8s.io/utils/cpuset"
    37  
    38  	"github.com/onsi/ginkgo/v2"
    39  	"github.com/onsi/gomega"
    40  	"k8s.io/kubernetes/test/e2e/feature"
    41  	"k8s.io/kubernetes/test/e2e/framework"
    42  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    43  	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
    44  	"k8s.io/kubernetes/test/e2e/nodefeature"
    45  )
    46  
    47  // Helper for makeCPUManagerPod().
    48  type ctnAttribute struct {
    49  	ctnName       string
    50  	cpuRequest    string
    51  	cpuLimit      string
    52  	restartPolicy *v1.ContainerRestartPolicy
    53  }
    54  
    55  // makeCPUMangerPod returns a pod with the provided ctnAttributes.
    56  func makeCPUManagerPod(podName string, ctnAttributes []ctnAttribute) *v1.Pod {
    57  	var containers []v1.Container
    58  	for _, ctnAttr := range ctnAttributes {
    59  		cpusetCmd := fmt.Sprintf("grep Cpus_allowed_list /proc/self/status | cut -f2 && sleep 1d")
    60  		ctn := v1.Container{
    61  			Name:  ctnAttr.ctnName,
    62  			Image: busyboxImage,
    63  			Resources: v1.ResourceRequirements{
    64  				Requests: v1.ResourceList{
    65  					v1.ResourceCPU:    resource.MustParse(ctnAttr.cpuRequest),
    66  					v1.ResourceMemory: resource.MustParse("100Mi"),
    67  				},
    68  				Limits: v1.ResourceList{
    69  					v1.ResourceCPU:    resource.MustParse(ctnAttr.cpuLimit),
    70  					v1.ResourceMemory: resource.MustParse("100Mi"),
    71  				},
    72  			},
    73  			Command: []string{"sh", "-c", cpusetCmd},
    74  		}
    75  		containers = append(containers, ctn)
    76  	}
    77  
    78  	return &v1.Pod{
    79  		ObjectMeta: metav1.ObjectMeta{
    80  			Name: podName,
    81  		},
    82  		Spec: v1.PodSpec{
    83  			RestartPolicy: v1.RestartPolicyNever,
    84  			Containers:    containers,
    85  		},
    86  	}
    87  }
    88  
    89  // makeCPUMangerInitContainersPod returns a pod with init containers with the
    90  // provided ctnAttributes.
    91  func makeCPUManagerInitContainersPod(podName string, ctnAttributes []ctnAttribute) *v1.Pod {
    92  	var containers []v1.Container
    93  	cpusetCmd := "grep Cpus_allowed_list /proc/self/status | cut -f2"
    94  	cpusetAndSleepCmd := "grep Cpus_allowed_list /proc/self/status | cut -f2 && sleep 1d"
    95  	for _, ctnAttr := range ctnAttributes {
    96  		ctn := v1.Container{
    97  			Name:  ctnAttr.ctnName,
    98  			Image: busyboxImage,
    99  			Resources: v1.ResourceRequirements{
   100  				Requests: v1.ResourceList{
   101  					v1.ResourceCPU:    resource.MustParse(ctnAttr.cpuRequest),
   102  					v1.ResourceMemory: resource.MustParse("100Mi"),
   103  				},
   104  				Limits: v1.ResourceList{
   105  					v1.ResourceCPU:    resource.MustParse(ctnAttr.cpuLimit),
   106  					v1.ResourceMemory: resource.MustParse("100Mi"),
   107  				},
   108  			},
   109  			Command:       []string{"sh", "-c", cpusetCmd},
   110  			RestartPolicy: ctnAttr.restartPolicy,
   111  		}
   112  		if ctnAttr.restartPolicy != nil && *ctnAttr.restartPolicy == v1.ContainerRestartPolicyAlways {
   113  			ctn.Command = []string{"sh", "-c", cpusetAndSleepCmd}
   114  		}
   115  		containers = append(containers, ctn)
   116  	}
   117  
   118  	return &v1.Pod{
   119  		ObjectMeta: metav1.ObjectMeta{
   120  			Name: podName,
   121  		},
   122  		Spec: v1.PodSpec{
   123  			RestartPolicy:  v1.RestartPolicyNever,
   124  			InitContainers: containers,
   125  			Containers: []v1.Container{
   126  				{
   127  					Name:  "regular",
   128  					Image: busyboxImage,
   129  					Resources: v1.ResourceRequirements{
   130  						Requests: v1.ResourceList{
   131  							v1.ResourceCPU:    resource.MustParse("1000m"),
   132  							v1.ResourceMemory: resource.MustParse("100Mi"),
   133  						},
   134  						Limits: v1.ResourceList{
   135  							v1.ResourceCPU:    resource.MustParse("1000m"),
   136  							v1.ResourceMemory: resource.MustParse("100Mi"),
   137  						},
   138  					},
   139  					Command: []string{"sh", "-c", cpusetAndSleepCmd},
   140  				},
   141  			},
   142  		},
   143  	}
   144  }
   145  
   146  func deletePodSyncByName(ctx context.Context, f *framework.Framework, podName string) {
   147  	gp := int64(0)
   148  	delOpts := metav1.DeleteOptions{
   149  		GracePeriodSeconds: &gp,
   150  	}
   151  	e2epod.NewPodClient(f).DeleteSync(ctx, podName, delOpts, e2epod.DefaultPodDeletionTimeout)
   152  }
   153  
   154  func deletePods(ctx context.Context, f *framework.Framework, podNames []string) {
   155  	for _, podName := range podNames {
   156  		deletePodSyncByName(ctx, f, podName)
   157  	}
   158  }
   159  
   160  func getLocalNodeCPUDetails(ctx context.Context, f *framework.Framework) (cpuCapVal int64, cpuAllocVal int64, cpuResVal int64) {
   161  	localNodeCap := getLocalNode(ctx, f).Status.Capacity
   162  	cpuCap := localNodeCap[v1.ResourceCPU]
   163  	localNodeAlloc := getLocalNode(ctx, f).Status.Allocatable
   164  	cpuAlloc := localNodeAlloc[v1.ResourceCPU]
   165  	cpuRes := cpuCap.DeepCopy()
   166  	cpuRes.Sub(cpuAlloc)
   167  
   168  	// RoundUp reserved CPUs to get only integer cores.
   169  	cpuRes.RoundUp(0)
   170  
   171  	return cpuCap.Value(), cpuCap.Value() - cpuRes.Value(), cpuRes.Value()
   172  }
   173  
   174  func waitForContainerRemoval(ctx context.Context, containerName, podName, podNS string) {
   175  	rs, _, err := getCRIClient()
   176  	framework.ExpectNoError(err)
   177  	gomega.Eventually(ctx, func(ctx context.Context) bool {
   178  		containers, err := rs.ListContainers(ctx, &runtimeapi.ContainerFilter{
   179  			LabelSelector: map[string]string{
   180  				types.KubernetesPodNameLabel:       podName,
   181  				types.KubernetesPodNamespaceLabel:  podNS,
   182  				types.KubernetesContainerNameLabel: containerName,
   183  			},
   184  		})
   185  		if err != nil {
   186  			return false
   187  		}
   188  		return len(containers) == 0
   189  	}, 2*time.Minute, 1*time.Second).Should(gomega.BeTrue())
   190  }
   191  
   192  func isHTEnabled() bool {
   193  	outData, err := exec.Command("/bin/sh", "-c", "lscpu | grep \"Thread(s) per core:\" | cut -d \":\" -f 2").Output()
   194  	framework.ExpectNoError(err)
   195  
   196  	threadsPerCore, err := strconv.Atoi(strings.TrimSpace(string(outData)))
   197  	framework.ExpectNoError(err)
   198  
   199  	return threadsPerCore > 1
   200  }
   201  
   202  func isMultiNUMA() bool {
   203  	outData, err := exec.Command("/bin/sh", "-c", "lscpu | grep \"NUMA node(s):\" | cut -d \":\" -f 2").Output()
   204  	framework.ExpectNoError(err)
   205  
   206  	numaNodes, err := strconv.Atoi(strings.TrimSpace(string(outData)))
   207  	framework.ExpectNoError(err)
   208  
   209  	return numaNodes > 1
   210  }
   211  
   212  func getSMTLevel() int {
   213  	cpuID := 0 // this is just the most likely cpu to be present in a random system. No special meaning besides this.
   214  	out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("cat /sys/devices/system/cpu/cpu%d/topology/thread_siblings_list | tr -d \"\n\r\"", cpuID)).Output()
   215  	framework.ExpectNoError(err)
   216  	// how many thread sibling you have = SMT level
   217  	// example: 2-way SMT means 2 threads sibling for each thread
   218  	cpus, err := cpuset.Parse(strings.TrimSpace(string(out)))
   219  	framework.ExpectNoError(err)
   220  	return cpus.Size()
   221  }
   222  
   223  func getCPUSiblingList(cpuRes int64) string {
   224  	out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("cat /sys/devices/system/cpu/cpu%d/topology/thread_siblings_list | tr -d \"\n\r\"", cpuRes)).Output()
   225  	framework.ExpectNoError(err)
   226  	return string(out)
   227  }
   228  
   229  func getCoreSiblingList(cpuRes int64) string {
   230  	out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("cat /sys/devices/system/cpu/cpu%d/topology/core_siblings_list | tr -d \"\n\r\"", cpuRes)).Output()
   231  	framework.ExpectNoError(err)
   232  	return string(out)
   233  }
   234  
   235  type cpuManagerKubeletArguments struct {
   236  	policyName              string
   237  	enableCPUManagerOptions bool
   238  	reservedSystemCPUs      cpuset.CPUSet
   239  	options                 map[string]string
   240  }
   241  
   242  func configureCPUManagerInKubelet(oldCfg *kubeletconfig.KubeletConfiguration, kubeletArguments *cpuManagerKubeletArguments) *kubeletconfig.KubeletConfiguration {
   243  	newCfg := oldCfg.DeepCopy()
   244  	if newCfg.FeatureGates == nil {
   245  		newCfg.FeatureGates = make(map[string]bool)
   246  	}
   247  
   248  	newCfg.FeatureGates["CPUManagerPolicyOptions"] = kubeletArguments.enableCPUManagerOptions
   249  	newCfg.FeatureGates["CPUManagerPolicyBetaOptions"] = kubeletArguments.enableCPUManagerOptions
   250  	newCfg.FeatureGates["CPUManagerPolicyAlphaOptions"] = kubeletArguments.enableCPUManagerOptions
   251  
   252  	newCfg.CPUManagerPolicy = kubeletArguments.policyName
   253  	newCfg.CPUManagerReconcilePeriod = metav1.Duration{Duration: 1 * time.Second}
   254  
   255  	if kubeletArguments.options != nil {
   256  		newCfg.CPUManagerPolicyOptions = kubeletArguments.options
   257  	}
   258  
   259  	if kubeletArguments.reservedSystemCPUs.Size() > 0 {
   260  		cpus := kubeletArguments.reservedSystemCPUs.String()
   261  		framework.Logf("configureCPUManagerInKubelet: using reservedSystemCPUs=%q", cpus)
   262  		newCfg.ReservedSystemCPUs = cpus
   263  	} else {
   264  		// The Kubelet panics if either kube-reserved or system-reserved is not set
   265  		// when CPU Manager is enabled. Set cpu in kube-reserved > 0 so that
   266  		// kubelet doesn't panic.
   267  		if newCfg.KubeReserved == nil {
   268  			newCfg.KubeReserved = map[string]string{}
   269  		}
   270  
   271  		if _, ok := newCfg.KubeReserved["cpu"]; !ok {
   272  			newCfg.KubeReserved["cpu"] = "200m"
   273  		}
   274  	}
   275  
   276  	return newCfg
   277  }
   278  
   279  func runGuPodTest(ctx context.Context, f *framework.Framework, cpuCount int) {
   280  	var pod *v1.Pod
   281  
   282  	ctnAttrs := []ctnAttribute{
   283  		{
   284  			ctnName:    "gu-container",
   285  			cpuRequest: fmt.Sprintf("%dm", 1000*cpuCount),
   286  			cpuLimit:   fmt.Sprintf("%dm", 1000*cpuCount),
   287  		},
   288  	}
   289  	pod = makeCPUManagerPod("gu-pod", ctnAttrs)
   290  	pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   291  
   292  	ginkgo.By("checking if the expected cpuset was assigned")
   293  	// any full CPU is fine - we cannot nor we should predict which one, though
   294  	for _, cnt := range pod.Spec.Containers {
   295  		ginkgo.By(fmt.Sprintf("validating the container %s on Gu pod %s", cnt.Name, pod.Name))
   296  
   297  		logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, cnt.Name)
   298  		framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]", cnt.Name, pod.Name)
   299  
   300  		framework.Logf("got pod logs: %v", logs)
   301  		cpus, err := cpuset.Parse(strings.TrimSpace(logs))
   302  		framework.ExpectNoError(err, "parsing cpuset from logs for [%s] of pod [%s]", cnt.Name, pod.Name)
   303  
   304  		gomega.Expect(cpus.Size()).To(gomega.Equal(cpuCount), "expected cpu set size == %d, got %q", cpuCount, cpus.String())
   305  	}
   306  
   307  	ginkgo.By("by deleting the pods and waiting for container removal")
   308  	deletePods(ctx, f, []string{pod.Name})
   309  	waitForAllContainerRemoval(ctx, pod.Name, pod.Namespace)
   310  }
   311  
   312  func runNonGuPodTest(ctx context.Context, f *framework.Framework, cpuCap int64) {
   313  	var ctnAttrs []ctnAttribute
   314  	var err error
   315  	var pod *v1.Pod
   316  	var expAllowedCPUsListRegex string
   317  
   318  	ctnAttrs = []ctnAttribute{
   319  		{
   320  			ctnName:    "non-gu-container",
   321  			cpuRequest: "100m",
   322  			cpuLimit:   "200m",
   323  		},
   324  	}
   325  	pod = makeCPUManagerPod("non-gu-pod", ctnAttrs)
   326  	pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   327  
   328  	ginkgo.By("checking if the expected cpuset was assigned")
   329  	expAllowedCPUsListRegex = fmt.Sprintf("^0-%d\n$", cpuCap-1)
   330  	// on the single CPU node the only possible value is 0
   331  	if cpuCap == 1 {
   332  		expAllowedCPUsListRegex = "^0\n$"
   333  	}
   334  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod.Name, pod.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   335  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   336  		pod.Spec.Containers[0].Name, pod.Name)
   337  
   338  	ginkgo.By("by deleting the pods and waiting for container removal")
   339  	deletePods(ctx, f, []string{pod.Name})
   340  	waitForContainerRemoval(ctx, pod.Spec.Containers[0].Name, pod.Name, pod.Namespace)
   341  }
   342  
   343  func mustParseCPUSet(s string) cpuset.CPUSet {
   344  	res, err := cpuset.Parse(s)
   345  	framework.ExpectNoError(err)
   346  	return res
   347  }
   348  
   349  func runAutomaticallyRemoveInactivePodsFromCPUManagerStateFile(ctx context.Context, f *framework.Framework) {
   350  	var cpu1 int
   351  	var ctnAttrs []ctnAttribute
   352  	var pod *v1.Pod
   353  	var cpuList []int
   354  	var expAllowedCPUsListRegex string
   355  	var err error
   356  	// First running a Gu Pod,
   357  	// second disable cpu manager in kubelet,
   358  	// then delete the Gu Pod,
   359  	// then enable cpu manager in kubelet,
   360  	// at last wait for the reconcile process cleaned up the state file, if the assignments map is empty,
   361  	// it proves that the automatic cleanup in the reconcile process is in effect.
   362  	ginkgo.By("running a Gu pod for test remove")
   363  	ctnAttrs = []ctnAttribute{
   364  		{
   365  			ctnName:    "gu-container-testremove",
   366  			cpuRequest: "1000m",
   367  			cpuLimit:   "1000m",
   368  		},
   369  	}
   370  	pod = makeCPUManagerPod("gu-pod-testremove", ctnAttrs)
   371  	pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   372  
   373  	ginkgo.By("checking if the expected cpuset was assigned")
   374  	cpu1 = 1
   375  	if isHTEnabled() {
   376  		cpuList = mustParseCPUSet(getCPUSiblingList(0)).List()
   377  		cpu1 = cpuList[1]
   378  	} else if isMultiNUMA() {
   379  		cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   380  		if len(cpuList) > 1 {
   381  			cpu1 = cpuList[1]
   382  		}
   383  	}
   384  	expAllowedCPUsListRegex = fmt.Sprintf("^%d\n$", cpu1)
   385  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod.Name, pod.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   386  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   387  		pod.Spec.Containers[0].Name, pod.Name)
   388  
   389  	deletePodSyncByName(ctx, f, pod.Name)
   390  	// we need to wait for all containers to really be gone so cpumanager reconcile loop will not rewrite the cpu_manager_state.
   391  	// this is in turn needed because we will have an unavoidable (in the current framework) race with the
   392  	// reconcile loop which will make our attempt to delete the state file and to restore the old config go haywire
   393  	waitForAllContainerRemoval(ctx, pod.Name, pod.Namespace)
   394  
   395  }
   396  
   397  func runMultipleGuNonGuPods(ctx context.Context, f *framework.Framework, cpuCap int64, cpuAlloc int64) {
   398  	var cpuListString, expAllowedCPUsListRegex string
   399  	var cpuList []int
   400  	var cpu1 int
   401  	var cset cpuset.CPUSet
   402  	var err error
   403  	var ctnAttrs []ctnAttribute
   404  	var pod1, pod2 *v1.Pod
   405  
   406  	ctnAttrs = []ctnAttribute{
   407  		{
   408  			ctnName:    "gu-container",
   409  			cpuRequest: "1000m",
   410  			cpuLimit:   "1000m",
   411  		},
   412  	}
   413  	pod1 = makeCPUManagerPod("gu-pod", ctnAttrs)
   414  	pod1 = e2epod.NewPodClient(f).CreateSync(ctx, pod1)
   415  
   416  	ctnAttrs = []ctnAttribute{
   417  		{
   418  			ctnName:    "non-gu-container",
   419  			cpuRequest: "200m",
   420  			cpuLimit:   "300m",
   421  		},
   422  	}
   423  	pod2 = makeCPUManagerPod("non-gu-pod", ctnAttrs)
   424  	pod2 = e2epod.NewPodClient(f).CreateSync(ctx, pod2)
   425  
   426  	ginkgo.By("checking if the expected cpuset was assigned")
   427  	cpu1 = 1
   428  	if isHTEnabled() {
   429  		cpuList = mustParseCPUSet(getCPUSiblingList(0)).List()
   430  		cpu1 = cpuList[1]
   431  	} else if isMultiNUMA() {
   432  		cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   433  		if len(cpuList) > 1 {
   434  			cpu1 = cpuList[1]
   435  		}
   436  	}
   437  	expAllowedCPUsListRegex = fmt.Sprintf("^%d\n$", cpu1)
   438  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod1.Name, pod1.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   439  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   440  		pod1.Spec.Containers[0].Name, pod1.Name)
   441  
   442  	cpuListString = "0"
   443  	if cpuAlloc > 2 {
   444  		cset = mustParseCPUSet(fmt.Sprintf("0-%d", cpuCap-1))
   445  		cpuListString = fmt.Sprintf("%s", cset.Difference(cpuset.New(cpu1)))
   446  	}
   447  	expAllowedCPUsListRegex = fmt.Sprintf("^%s\n$", cpuListString)
   448  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod2.Name, pod2.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   449  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   450  		pod2.Spec.Containers[0].Name, pod2.Name)
   451  	ginkgo.By("by deleting the pods and waiting for container removal")
   452  	deletePods(ctx, f, []string{pod1.Name, pod2.Name})
   453  	waitForContainerRemoval(ctx, pod1.Spec.Containers[0].Name, pod1.Name, pod1.Namespace)
   454  	waitForContainerRemoval(ctx, pod2.Spec.Containers[0].Name, pod2.Name, pod2.Namespace)
   455  }
   456  
   457  func runMultipleCPUGuPod(ctx context.Context, f *framework.Framework) {
   458  	var cpuListString, expAllowedCPUsListRegex string
   459  	var cpuList []int
   460  	var cset cpuset.CPUSet
   461  	var err error
   462  	var ctnAttrs []ctnAttribute
   463  	var pod *v1.Pod
   464  
   465  	ctnAttrs = []ctnAttribute{
   466  		{
   467  			ctnName:    "gu-container",
   468  			cpuRequest: "2000m",
   469  			cpuLimit:   "2000m",
   470  		},
   471  	}
   472  	pod = makeCPUManagerPod("gu-pod", ctnAttrs)
   473  	pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   474  
   475  	ginkgo.By("checking if the expected cpuset was assigned")
   476  	cpuListString = "1-2"
   477  	if isMultiNUMA() {
   478  		cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   479  		if len(cpuList) > 1 {
   480  			cset = mustParseCPUSet(getCPUSiblingList(int64(cpuList[1])))
   481  			if !isHTEnabled() && len(cpuList) > 2 {
   482  				cset = mustParseCPUSet(fmt.Sprintf("%d-%d", cpuList[1], cpuList[2]))
   483  			}
   484  			cpuListString = fmt.Sprintf("%s", cset)
   485  		}
   486  	} else if isHTEnabled() {
   487  		cpuListString = "2-3"
   488  		cpuList = mustParseCPUSet(getCPUSiblingList(0)).List()
   489  		if cpuList[1] != 1 {
   490  			cset = mustParseCPUSet(getCPUSiblingList(1))
   491  			cpuListString = fmt.Sprintf("%s", cset)
   492  		}
   493  	}
   494  	expAllowedCPUsListRegex = fmt.Sprintf("^%s\n$", cpuListString)
   495  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod.Name, pod.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   496  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   497  		pod.Spec.Containers[0].Name, pod.Name)
   498  
   499  	ginkgo.By("by deleting the pods and waiting for container removal")
   500  	deletePods(ctx, f, []string{pod.Name})
   501  	waitForContainerRemoval(ctx, pod.Spec.Containers[0].Name, pod.Name, pod.Namespace)
   502  }
   503  
   504  func runMultipleCPUContainersGuPod(ctx context.Context, f *framework.Framework) {
   505  	var expAllowedCPUsListRegex string
   506  	var cpuList []int
   507  	var cpu1, cpu2 int
   508  	var err error
   509  	var ctnAttrs []ctnAttribute
   510  	var pod *v1.Pod
   511  	ctnAttrs = []ctnAttribute{
   512  		{
   513  			ctnName:    "gu-container1",
   514  			cpuRequest: "1000m",
   515  			cpuLimit:   "1000m",
   516  		},
   517  		{
   518  			ctnName:    "gu-container2",
   519  			cpuRequest: "1000m",
   520  			cpuLimit:   "1000m",
   521  		},
   522  	}
   523  	pod = makeCPUManagerPod("gu-pod", ctnAttrs)
   524  	pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   525  
   526  	ginkgo.By("checking if the expected cpuset was assigned")
   527  	cpu1, cpu2 = 1, 2
   528  	if isHTEnabled() {
   529  		cpuList = mustParseCPUSet(getCPUSiblingList(0)).List()
   530  		if cpuList[1] != 1 {
   531  			cpu1, cpu2 = cpuList[1], 1
   532  		}
   533  		if isMultiNUMA() {
   534  			cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   535  			if len(cpuList) > 1 {
   536  				cpu2 = cpuList[1]
   537  			}
   538  		}
   539  	} else if isMultiNUMA() {
   540  		cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   541  		if len(cpuList) > 2 {
   542  			cpu1, cpu2 = cpuList[1], cpuList[2]
   543  		}
   544  	}
   545  	expAllowedCPUsListRegex = fmt.Sprintf("^%d|%d\n$", cpu1, cpu2)
   546  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod.Name, pod.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   547  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   548  		pod.Spec.Containers[0].Name, pod.Name)
   549  
   550  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod.Name, pod.Spec.Containers[1].Name, expAllowedCPUsListRegex)
   551  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   552  		pod.Spec.Containers[1].Name, pod.Name)
   553  
   554  	ginkgo.By("by deleting the pods and waiting for container removal")
   555  	deletePods(ctx, f, []string{pod.Name})
   556  	waitForContainerRemoval(ctx, pod.Spec.Containers[0].Name, pod.Name, pod.Namespace)
   557  	waitForContainerRemoval(ctx, pod.Spec.Containers[1].Name, pod.Name, pod.Namespace)
   558  }
   559  
   560  func runMultipleGuPods(ctx context.Context, f *framework.Framework) {
   561  	var expAllowedCPUsListRegex string
   562  	var cpuList []int
   563  	var cpu1, cpu2 int
   564  	var err error
   565  	var ctnAttrs []ctnAttribute
   566  	var pod1, pod2 *v1.Pod
   567  
   568  	ctnAttrs = []ctnAttribute{
   569  		{
   570  			ctnName:    "gu-container1",
   571  			cpuRequest: "1000m",
   572  			cpuLimit:   "1000m",
   573  		},
   574  	}
   575  	pod1 = makeCPUManagerPod("gu-pod1", ctnAttrs)
   576  	pod1 = e2epod.NewPodClient(f).CreateSync(ctx, pod1)
   577  
   578  	ctnAttrs = []ctnAttribute{
   579  		{
   580  			ctnName:    "gu-container2",
   581  			cpuRequest: "1000m",
   582  			cpuLimit:   "1000m",
   583  		},
   584  	}
   585  	pod2 = makeCPUManagerPod("gu-pod2", ctnAttrs)
   586  	pod2 = e2epod.NewPodClient(f).CreateSync(ctx, pod2)
   587  
   588  	ginkgo.By("checking if the expected cpuset was assigned")
   589  	cpu1, cpu2 = 1, 2
   590  	if isHTEnabled() {
   591  		cpuList = mustParseCPUSet(getCPUSiblingList(0)).List()
   592  		if cpuList[1] != 1 {
   593  			cpu1, cpu2 = cpuList[1], 1
   594  		}
   595  		if isMultiNUMA() {
   596  			cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   597  			if len(cpuList) > 1 {
   598  				cpu2 = cpuList[1]
   599  			}
   600  		}
   601  	} else if isMultiNUMA() {
   602  		cpuList = mustParseCPUSet(getCoreSiblingList(0)).List()
   603  		if len(cpuList) > 2 {
   604  			cpu1, cpu2 = cpuList[1], cpuList[2]
   605  		}
   606  	}
   607  	expAllowedCPUsListRegex = fmt.Sprintf("^%d\n$", cpu1)
   608  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod1.Name, pod1.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   609  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   610  		pod1.Spec.Containers[0].Name, pod1.Name)
   611  
   612  	expAllowedCPUsListRegex = fmt.Sprintf("^%d\n$", cpu2)
   613  	err = e2epod.NewPodClient(f).MatchContainerOutput(ctx, pod2.Name, pod2.Spec.Containers[0].Name, expAllowedCPUsListRegex)
   614  	framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]",
   615  		pod2.Spec.Containers[0].Name, pod2.Name)
   616  	ginkgo.By("by deleting the pods and waiting for container removal")
   617  	deletePods(ctx, f, []string{pod1.Name, pod2.Name})
   618  	waitForContainerRemoval(ctx, pod1.Spec.Containers[0].Name, pod1.Name, pod1.Namespace)
   619  	waitForContainerRemoval(ctx, pod2.Spec.Containers[0].Name, pod2.Name, pod2.Namespace)
   620  }
   621  
   622  func runCPUManagerTests(f *framework.Framework) {
   623  	var cpuCap, cpuAlloc int64
   624  	var oldCfg *kubeletconfig.KubeletConfiguration
   625  
   626  	ginkgo.BeforeEach(func(ctx context.Context) {
   627  		var err error
   628  		if oldCfg == nil {
   629  			oldCfg, err = getCurrentKubeletConfig(ctx)
   630  			framework.ExpectNoError(err)
   631  		}
   632  	})
   633  
   634  	ginkgo.It("should assign CPUs as expected based on the Pod spec", func(ctx context.Context) {
   635  		cpuCap, cpuAlloc, _ = getLocalNodeCPUDetails(ctx, f)
   636  
   637  		// Skip CPU Manager tests altogether if the CPU capacity < 2.
   638  		if cpuCap < 2 {
   639  			e2eskipper.Skipf("Skipping CPU Manager tests since the CPU capacity < 2")
   640  		}
   641  
   642  		// Enable CPU Manager in the kubelet.
   643  		newCfg := configureCPUManagerInKubelet(oldCfg, &cpuManagerKubeletArguments{
   644  			policyName:         string(cpumanager.PolicyStatic),
   645  			reservedSystemCPUs: cpuset.CPUSet{},
   646  		})
   647  		updateKubeletConfig(ctx, f, newCfg, true)
   648  
   649  		ginkgo.By("running a non-Gu pod")
   650  		runNonGuPodTest(ctx, f, cpuCap)
   651  
   652  		ginkgo.By("running a Gu pod")
   653  		runGuPodTest(ctx, f, 1)
   654  
   655  		ginkgo.By("running multiple Gu and non-Gu pods")
   656  		runMultipleGuNonGuPods(ctx, f, cpuCap, cpuAlloc)
   657  
   658  		// Skip rest of the tests if CPU capacity < 3.
   659  		if cpuCap < 3 {
   660  			e2eskipper.Skipf("Skipping rest of the CPU Manager tests since CPU capacity < 3")
   661  		}
   662  
   663  		ginkgo.By("running a Gu pod requesting multiple CPUs")
   664  		runMultipleCPUGuPod(ctx, f)
   665  
   666  		ginkgo.By("running a Gu pod with multiple containers requesting integer CPUs")
   667  		runMultipleCPUContainersGuPod(ctx, f)
   668  
   669  		ginkgo.By("running multiple Gu pods")
   670  		runMultipleGuPods(ctx, f)
   671  
   672  		ginkgo.By("test for automatically remove inactive pods from cpumanager state file.")
   673  		runAutomaticallyRemoveInactivePodsFromCPUManagerStateFile(ctx, f)
   674  	})
   675  
   676  	ginkgo.It("should assign CPUs as expected with enhanced policy based on strict SMT alignment", func(ctx context.Context) {
   677  		fullCPUsOnlyOpt := fmt.Sprintf("option=%s", cpumanager.FullPCPUsOnlyOption)
   678  		_, cpuAlloc, _ = getLocalNodeCPUDetails(ctx, f)
   679  		smtLevel := getSMTLevel()
   680  
   681  		// strict SMT alignment is trivially verified and granted on non-SMT systems
   682  		if smtLevel < 2 {
   683  			e2eskipper.Skipf("Skipping CPU Manager %s tests since SMT disabled", fullCPUsOnlyOpt)
   684  		}
   685  
   686  		// our tests want to allocate a full core, so we need at last 2*2=4 virtual cpus
   687  		if cpuAlloc < int64(smtLevel*2) {
   688  			e2eskipper.Skipf("Skipping CPU Manager %s tests since the CPU capacity < 4", fullCPUsOnlyOpt)
   689  		}
   690  
   691  		framework.Logf("SMT level %d", smtLevel)
   692  
   693  		// TODO: we assume the first available CPUID is 0, which is pretty fair, but we should probably
   694  		// check what we do have in the node.
   695  		cpuPolicyOptions := map[string]string{
   696  			cpumanager.FullPCPUsOnlyOption: "true",
   697  		}
   698  		newCfg := configureCPUManagerInKubelet(oldCfg,
   699  			&cpuManagerKubeletArguments{
   700  				policyName:              string(cpumanager.PolicyStatic),
   701  				reservedSystemCPUs:      cpuset.New(0),
   702  				enableCPUManagerOptions: true,
   703  				options:                 cpuPolicyOptions,
   704  			},
   705  		)
   706  		updateKubeletConfig(ctx, f, newCfg, true)
   707  
   708  		// the order between negative and positive doesn't really matter
   709  		runSMTAlignmentNegativeTests(ctx, f)
   710  		runSMTAlignmentPositiveTests(ctx, f, smtLevel)
   711  	})
   712  
   713  	f.It("should not reuse CPUs of restartable init containers", nodefeature.SidecarContainers, func(ctx context.Context) {
   714  		cpuCap, cpuAlloc, _ = getLocalNodeCPUDetails(ctx, f)
   715  
   716  		// Skip rest of the tests if CPU capacity < 3.
   717  		if cpuCap < 3 {
   718  			e2eskipper.Skipf("Skipping rest of the CPU Manager tests since CPU capacity < 3, got %d", cpuCap)
   719  		}
   720  
   721  		// Enable CPU Manager in the kubelet.
   722  		newCfg := configureCPUManagerInKubelet(oldCfg, &cpuManagerKubeletArguments{
   723  			policyName:         string(cpumanager.PolicyStatic),
   724  			reservedSystemCPUs: cpuset.CPUSet{},
   725  		})
   726  		updateKubeletConfig(ctx, f, newCfg, true)
   727  
   728  		ginkgo.By("running a Gu pod with a regular init container and a restartable init container")
   729  		ctrAttrs := []ctnAttribute{
   730  			{
   731  				ctnName:    "gu-init-container1",
   732  				cpuRequest: "1000m",
   733  				cpuLimit:   "1000m",
   734  			},
   735  			{
   736  				ctnName:       "gu-restartable-init-container2",
   737  				cpuRequest:    "1000m",
   738  				cpuLimit:      "1000m",
   739  				restartPolicy: &containerRestartPolicyAlways,
   740  			},
   741  		}
   742  		pod := makeCPUManagerInitContainersPod("gu-pod", ctrAttrs)
   743  		pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   744  
   745  		ginkgo.By("checking if the expected cpuset was assigned")
   746  		logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, pod.Spec.InitContainers[0].Name)
   747  		framework.ExpectNoError(err, "expected log not found in init container [%s] of pod [%s]", pod.Spec.InitContainers[0].Name, pod.Name)
   748  
   749  		framework.Logf("got pod logs: %v", logs)
   750  		reusableCPUs, err := cpuset.Parse(strings.TrimSpace(logs))
   751  		framework.ExpectNoError(err, "parsing cpuset from logs for [%s] of pod [%s]", pod.Spec.InitContainers[0].Name, pod.Name)
   752  
   753  		gomega.Expect(reusableCPUs.Size()).To(gomega.Equal(1), "expected cpu set size == 1, got %q", reusableCPUs.String())
   754  
   755  		logs, err = e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, pod.Spec.InitContainers[1].Name)
   756  		framework.ExpectNoError(err, "expected log not found in init container [%s] of pod [%s]", pod.Spec.InitContainers[1].Name, pod.Name)
   757  
   758  		framework.Logf("got pod logs: %v", logs)
   759  		nonReusableCPUs, err := cpuset.Parse(strings.TrimSpace(logs))
   760  		framework.ExpectNoError(err, "parsing cpuset from logs for [%s] of pod [%s]", pod.Spec.InitContainers[1].Name, pod.Name)
   761  
   762  		gomega.Expect(nonReusableCPUs.Size()).To(gomega.Equal(1), "expected cpu set size == 1, got %q", nonReusableCPUs.String())
   763  
   764  		logs, err = e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, pod.Spec.Containers[0].Name)
   765  		framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]", pod.Spec.Containers[0].Name, pod.Name)
   766  
   767  		framework.Logf("got pod logs: %v", logs)
   768  		cpus, err := cpuset.Parse(strings.TrimSpace(logs))
   769  		framework.ExpectNoError(err, "parsing cpuset from logs for [%s] of pod [%s]", pod.Spec.Containers[0].Name, pod.Name)
   770  
   771  		gomega.Expect(cpus.Size()).To(gomega.Equal(1), "expected cpu set size == 1, got %q", cpus.String())
   772  
   773  		gomega.Expect(reusableCPUs.Equals(nonReusableCPUs)).To(gomega.BeTrue(), "expected reusable cpuset [%s] to be equal to non-reusable cpuset [%s]", reusableCPUs.String(), nonReusableCPUs.String())
   774  		gomega.Expect(nonReusableCPUs.Intersection(cpus).IsEmpty()).To(gomega.BeTrue(), "expected non-reusable cpuset [%s] to be disjoint from cpuset [%s]", nonReusableCPUs.String(), cpus.String())
   775  
   776  		ginkgo.By("by deleting the pods and waiting for container removal")
   777  		deletePods(ctx, f, []string{pod.Name})
   778  		waitForContainerRemoval(ctx, pod.Spec.InitContainers[0].Name, pod.Name, pod.Namespace)
   779  		waitForContainerRemoval(ctx, pod.Spec.InitContainers[1].Name, pod.Name, pod.Namespace)
   780  		waitForContainerRemoval(ctx, pod.Spec.Containers[0].Name, pod.Name, pod.Namespace)
   781  	})
   782  
   783  	ginkgo.AfterEach(func(ctx context.Context) {
   784  		updateKubeletConfig(ctx, f, oldCfg, true)
   785  	})
   786  }
   787  
   788  func runSMTAlignmentNegativeTests(ctx context.Context, f *framework.Framework) {
   789  	// negative test: try to run a container whose requests aren't a multiple of SMT level, expect a rejection
   790  	ctnAttrs := []ctnAttribute{
   791  		{
   792  			ctnName:    "gu-container-neg",
   793  			cpuRequest: "1000m",
   794  			cpuLimit:   "1000m",
   795  		},
   796  	}
   797  	pod := makeCPUManagerPod("gu-pod", ctnAttrs)
   798  	// CreateSync would wait for pod to become Ready - which will never happen if production code works as intended!
   799  	pod = e2epod.NewPodClient(f).Create(ctx, pod)
   800  
   801  	err := e2epod.WaitForPodCondition(ctx, f.ClientSet, f.Namespace.Name, pod.Name, "Failed", 30*time.Second, func(pod *v1.Pod) (bool, error) {
   802  		if pod.Status.Phase != v1.PodPending {
   803  			return true, nil
   804  		}
   805  		return false, nil
   806  	})
   807  	framework.ExpectNoError(err)
   808  	pod, err = e2epod.NewPodClient(f).Get(ctx, pod.Name, metav1.GetOptions{})
   809  	framework.ExpectNoError(err)
   810  
   811  	if pod.Status.Phase != v1.PodFailed {
   812  		framework.Failf("pod %s not failed: %v", pod.Name, pod.Status)
   813  	}
   814  	if !isSMTAlignmentError(pod) {
   815  		framework.Failf("pod %s failed for wrong reason: %q", pod.Name, pod.Status.Reason)
   816  	}
   817  
   818  	deletePodSyncByName(ctx, f, pod.Name)
   819  	// we need to wait for all containers to really be gone so cpumanager reconcile loop will not rewrite the cpu_manager_state.
   820  	// this is in turn needed because we will have an unavoidable (in the current framework) race with th
   821  	// reconcile loop which will make our attempt to delete the state file and to restore the old config go haywire
   822  	waitForAllContainerRemoval(ctx, pod.Name, pod.Namespace)
   823  }
   824  
   825  func runSMTAlignmentPositiveTests(ctx context.Context, f *framework.Framework, smtLevel int) {
   826  	// positive test: try to run a container whose requests are a multiple of SMT level, check allocated cores
   827  	// 1. are core siblings
   828  	// 2. take a full core
   829  	// WARNING: this assumes 2-way SMT systems - we don't know how to access other SMT levels.
   830  	//          this means on more-than-2-way SMT systems this test will prove nothing
   831  	ctnAttrs := []ctnAttribute{
   832  		{
   833  			ctnName:    "gu-container-pos",
   834  			cpuRequest: "2000m",
   835  			cpuLimit:   "2000m",
   836  		},
   837  	}
   838  	pod := makeCPUManagerPod("gu-pod", ctnAttrs)
   839  	pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
   840  
   841  	for _, cnt := range pod.Spec.Containers {
   842  		ginkgo.By(fmt.Sprintf("validating the container %s on Gu pod %s", cnt.Name, pod.Name))
   843  
   844  		logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, cnt.Name)
   845  		framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]", cnt.Name, pod.Name)
   846  
   847  		framework.Logf("got pod logs: %v", logs)
   848  		cpus, err := cpuset.Parse(strings.TrimSpace(logs))
   849  		framework.ExpectNoError(err, "parsing cpuset from logs for [%s] of pod [%s]", cnt.Name, pod.Name)
   850  
   851  		validateSMTAlignment(cpus, smtLevel, pod, &cnt)
   852  	}
   853  
   854  	deletePodSyncByName(ctx, f, pod.Name)
   855  	// we need to wait for all containers to really be gone so cpumanager reconcile loop will not rewrite the cpu_manager_state.
   856  	// this is in turn needed because we will have an unavoidable (in the current framework) race with th
   857  	// reconcile loop which will make our attempt to delete the state file and to restore the old config go haywire
   858  	waitForAllContainerRemoval(ctx, pod.Name, pod.Namespace)
   859  }
   860  
   861  func validateSMTAlignment(cpus cpuset.CPUSet, smtLevel int, pod *v1.Pod, cnt *v1.Container) {
   862  	framework.Logf("validating cpus: %v", cpus)
   863  
   864  	if cpus.Size()%smtLevel != 0 {
   865  		framework.Failf("pod %q cnt %q received non-smt-multiple cpuset %v (SMT level %d)", pod.Name, cnt.Name, cpus, smtLevel)
   866  	}
   867  
   868  	// now check all the given cpus are thread siblings.
   869  	// to do so the easiest way is to rebuild the expected set of siblings from all the cpus we got.
   870  	// if the expected set matches the given set, the given set was good.
   871  	siblingsCPUs := cpuset.New()
   872  	for _, cpuID := range cpus.UnsortedList() {
   873  		threadSiblings, err := cpuset.Parse(strings.TrimSpace(getCPUSiblingList(int64(cpuID))))
   874  		framework.ExpectNoError(err, "parsing cpuset from logs for [%s] of pod [%s]", cnt.Name, pod.Name)
   875  		siblingsCPUs = siblingsCPUs.Union(threadSiblings)
   876  	}
   877  
   878  	framework.Logf("siblings cpus: %v", siblingsCPUs)
   879  	if !siblingsCPUs.Equals(cpus) {
   880  		framework.Failf("pod %q cnt %q received non-smt-aligned cpuset %v (expected %v)", pod.Name, cnt.Name, cpus, siblingsCPUs)
   881  	}
   882  }
   883  
   884  func isSMTAlignmentError(pod *v1.Pod) bool {
   885  	re := regexp.MustCompile(`SMT.*Alignment.*Error`)
   886  	return re.MatchString(pod.Status.Reason)
   887  }
   888  
   889  // Serial because the test updates kubelet configuration.
   890  var _ = SIGDescribe("CPU Manager", framework.WithSerial(), feature.CPUManager, func() {
   891  	f := framework.NewDefaultFramework("cpu-manager-test")
   892  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
   893  
   894  	ginkgo.Context("With kubeconfig updated with static CPU Manager policy run the CPU Manager tests", func() {
   895  		runCPUManagerTests(f)
   896  	})
   897  })