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 })