1
16
17 package e2enode
18
19 import (
20 "context"
21 "fmt"
22 "os"
23 "os/exec"
24 "regexp"
25 "strconv"
26 "strings"
27 "sync"
28 "time"
29
30 v1 "k8s.io/api/core/v1"
31 "k8s.io/apimachinery/pkg/api/resource"
32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33 "k8s.io/apimachinery/pkg/runtime"
34 kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
35 "k8s.io/kubernetes/pkg/kubelet/cm/cpumanager"
36 "k8s.io/kubernetes/pkg/kubelet/cm/topologymanager"
37 admissionapi "k8s.io/pod-security-admission/api"
38
39 "k8s.io/kubernetes/test/e2e/feature"
40 "k8s.io/kubernetes/test/e2e/framework"
41 e2enode "k8s.io/kubernetes/test/e2e/framework/node"
42 e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
43 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
44 e2etestfiles "k8s.io/kubernetes/test/e2e/framework/testfiles"
45 testutils "k8s.io/kubernetes/test/utils"
46
47 "github.com/onsi/ginkgo/v2"
48 "github.com/onsi/gomega"
49 )
50
51 const (
52 numaAlignmentCommand = `export CPULIST_ALLOWED=$( awk -F":\t*" '/Cpus_allowed_list/ { print $2 }' /proc/self/status); env;`
53 numaAlignmentSleepCommand = numaAlignmentCommand + `sleep 1d;`
54 podScopeTopology = "pod"
55 containerScopeTopology = "container"
56
57 minNumaNodes = 2
58 minCoreCount = 4
59 minSriovResource = 7
60 )
61
62
63 type tmCtnAttribute struct {
64 ctnName string
65 cpuRequest string
66 cpuLimit string
67 deviceName string
68 deviceRequest string
69 deviceLimit string
70 }
71
72 func detectNUMANodes() int {
73 outData, err := exec.Command("/bin/sh", "-c", "lscpu | grep \"NUMA node(s):\" | cut -d \":\" -f 2").Output()
74 framework.ExpectNoError(err)
75
76 numaNodes, err := strconv.Atoi(strings.TrimSpace(string(outData)))
77 framework.ExpectNoError(err)
78
79 return numaNodes
80 }
81
82 func detectCoresPerSocket() int {
83 outData, err := exec.Command("/bin/sh", "-c", "lscpu | grep \"Core(s) per socket:\" | cut -d \":\" -f 2").Output()
84 framework.ExpectNoError(err)
85
86 coreCount, err := strconv.Atoi(strings.TrimSpace(string(outData)))
87 framework.ExpectNoError(err)
88
89 return coreCount
90 }
91
92 func detectThreadPerCore() int {
93 outData, err := exec.Command("/bin/sh", "-c", "lscpu | grep \"Thread(s) per core:\" | cut -d \":\" -f 2").Output()
94 framework.ExpectNoError(err)
95
96 threadCount, err := strconv.Atoi(strings.TrimSpace(string(outData)))
97 framework.ExpectNoError(err)
98
99 return threadCount
100 }
101
102 func makeContainers(ctnCmd string, ctnAttributes []tmCtnAttribute) (ctns []v1.Container) {
103 for _, ctnAttr := range ctnAttributes {
104 ctn := v1.Container{
105 Name: ctnAttr.ctnName,
106 Image: busyboxImage,
107 Resources: v1.ResourceRequirements{
108 Requests: v1.ResourceList{
109 v1.ResourceName(v1.ResourceCPU): resource.MustParse(ctnAttr.cpuRequest),
110 v1.ResourceName(v1.ResourceMemory): resource.MustParse("100Mi"),
111 },
112 Limits: v1.ResourceList{
113 v1.ResourceName(v1.ResourceCPU): resource.MustParse(ctnAttr.cpuLimit),
114 v1.ResourceName(v1.ResourceMemory): resource.MustParse("100Mi"),
115 },
116 },
117 Command: []string{"sh", "-c", ctnCmd},
118 }
119 if ctnAttr.deviceName != "" {
120 ctn.Resources.Requests[v1.ResourceName(ctnAttr.deviceName)] = resource.MustParse(ctnAttr.deviceRequest)
121 ctn.Resources.Limits[v1.ResourceName(ctnAttr.deviceName)] = resource.MustParse(ctnAttr.deviceLimit)
122 }
123 ctns = append(ctns, ctn)
124 }
125 return
126 }
127
128 func makeTopologyManagerTestPod(podName string, tmCtnAttributes, tmInitCtnAttributes []tmCtnAttribute) *v1.Pod {
129 var containers, initContainers []v1.Container
130 if len(tmInitCtnAttributes) > 0 {
131 initContainers = makeContainers(numaAlignmentCommand, tmInitCtnAttributes)
132 }
133 containers = makeContainers(numaAlignmentSleepCommand, tmCtnAttributes)
134
135 return &v1.Pod{
136 ObjectMeta: metav1.ObjectMeta{
137 Name: podName,
138 },
139 Spec: v1.PodSpec{
140 RestartPolicy: v1.RestartPolicyNever,
141 InitContainers: initContainers,
142 Containers: containers,
143 },
144 }
145 }
146
147 func findNUMANodeWithoutSRIOVDevicesFromConfigMap(configMap *v1.ConfigMap, numaNodes int) (int, bool) {
148 for nodeNum := 0; nodeNum < numaNodes; nodeNum++ {
149 value, ok := configMap.Annotations[fmt.Sprintf("pcidevice_node%d", nodeNum)]
150 if !ok {
151 framework.Logf("missing pcidevice annotation for NUMA node %d", nodeNum)
152 return -1, false
153 }
154 v, err := strconv.Atoi(value)
155 if err != nil {
156 framework.Failf("error getting the PCI device count on NUMA node %d: %v", nodeNum, err)
157 }
158 if v == 0 {
159 framework.Logf("NUMA node %d has no SRIOV devices attached", nodeNum)
160 return nodeNum, true
161 }
162 framework.Logf("NUMA node %d has %d SRIOV devices attached", nodeNum, v)
163 }
164 return -1, false
165 }
166
167 func findNUMANodeWithoutSRIOVDevicesFromSysfs(numaNodes int) (int, bool) {
168 pciDevs, err := getPCIDeviceInfo("/sys/bus/pci/devices")
169 if err != nil {
170 framework.Failf("error detecting the PCI device NUMA node: %v", err)
171 }
172
173 pciPerNuma := make(map[int]int)
174 for _, pciDev := range pciDevs {
175 if pciDev.IsVFn {
176 pciPerNuma[pciDev.NUMANode]++
177 }
178 }
179
180 if len(pciPerNuma) == 0 {
181 framework.Logf("failed to find any VF device from %v", pciDevs)
182 return -1, false
183 }
184
185 for nodeNum := 0; nodeNum < numaNodes; nodeNum++ {
186 v := pciPerNuma[nodeNum]
187 if v == 0 {
188 framework.Logf("NUMA node %d has no SRIOV devices attached", nodeNum)
189 return nodeNum, true
190 }
191 framework.Logf("NUMA node %d has %d SRIOV devices attached", nodeNum, v)
192 }
193 return -1, false
194 }
195
196 func findNUMANodeWithoutSRIOVDevices(configMap *v1.ConfigMap, numaNodes int) (int, bool) {
197
198 if nodeNum, found := findNUMANodeWithoutSRIOVDevicesFromConfigMap(configMap, numaNodes); found {
199 return nodeNum, found
200 }
201
202
203 return findNUMANodeWithoutSRIOVDevicesFromSysfs(numaNodes)
204 }
205
206 func configureTopologyManagerInKubelet(oldCfg *kubeletconfig.KubeletConfiguration, policy, scope string, configMap *v1.ConfigMap, numaNodes int) (*kubeletconfig.KubeletConfiguration, string) {
207
208 newCfg := oldCfg.DeepCopy()
209 if newCfg.FeatureGates == nil {
210 newCfg.FeatureGates = make(map[string]bool)
211 }
212
213
214 newCfg.TopologyManagerPolicy = policy
215
216 newCfg.TopologyManagerScope = scope
217
218
219 newCfg.CPUManagerPolicy = string(cpumanager.PolicyStatic)
220
221
222 newCfg.CPUManagerReconcilePeriod = metav1.Duration{Duration: 1 * time.Second}
223
224 if nodeNum, ok := findNUMANodeWithoutSRIOVDevices(configMap, numaNodes); ok {
225 cpus, err := getCPUsPerNUMANode(nodeNum)
226 framework.Logf("NUMA Node %d doesn't seem to have attached SRIOV devices and has cpus=%v", nodeNum, cpus)
227 framework.ExpectNoError(err)
228 newCfg.ReservedSystemCPUs = fmt.Sprintf("%d", cpus[len(cpus)-1])
229 } else {
230
231
232
233 if newCfg.KubeReserved == nil {
234 newCfg.KubeReserved = map[string]string{}
235 }
236
237 if _, ok := newCfg.KubeReserved["cpu"]; !ok {
238 newCfg.KubeReserved["cpu"] = "200m"
239 }
240 }
241
242 framework.Logf("New kubelet config is %s", newCfg.String())
243
244 return newCfg, newCfg.ReservedSystemCPUs
245 }
246
247
248 func getSRIOVDevicePluginPod() *v1.Pod {
249 data, err := e2etestfiles.Read(SRIOVDevicePluginDSYAML)
250 if err != nil {
251 framework.Fail(err.Error())
252 }
253
254 ds := readDaemonSetV1OrDie(data)
255 p := &v1.Pod{
256 ObjectMeta: metav1.ObjectMeta{
257 Name: SRIOVDevicePluginName,
258 Namespace: metav1.NamespaceSystem,
259 },
260
261 Spec: ds.Spec.Template.Spec,
262 }
263
264 return p
265 }
266
267 func readConfigMapV1OrDie(objBytes []byte) *v1.ConfigMap {
268 v1.AddToScheme(appsScheme)
269 requiredObj, err := runtime.Decode(appsCodecs.UniversalDecoder(v1.SchemeGroupVersion), objBytes)
270 if err != nil {
271 panic(err)
272 }
273 return requiredObj.(*v1.ConfigMap)
274 }
275
276 func readServiceAccountV1OrDie(objBytes []byte) *v1.ServiceAccount {
277 v1.AddToScheme(appsScheme)
278 requiredObj, err := runtime.Decode(appsCodecs.UniversalDecoder(v1.SchemeGroupVersion), objBytes)
279 if err != nil {
280 panic(err)
281 }
282 return requiredObj.(*v1.ServiceAccount)
283 }
284
285 func findSRIOVResource(node *v1.Node) (string, int64) {
286 framework.Logf("Node status allocatable: %v", node.Status.Allocatable)
287 re := regexp.MustCompile(`^intel.com/.*sriov.*`)
288 for key, val := range node.Status.Allocatable {
289 resource := string(key)
290 if re.MatchString(resource) {
291 v := val.Value()
292 if v > 0 {
293 return resource, v
294 }
295 }
296 }
297 return "", 0
298 }
299
300 func validatePodAlignment(ctx context.Context, f *framework.Framework, pod *v1.Pod, envInfo *testEnvInfo) {
301 for _, cnt := range pod.Spec.Containers {
302 ginkgo.By(fmt.Sprintf("validating the container %s on Gu pod %s", cnt.Name, pod.Name))
303
304 logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, cnt.Name)
305 framework.ExpectNoError(err, "expected log not found in container [%s] of pod [%s]", cnt.Name, pod.Name)
306
307 framework.Logf("got pod logs: %v", logs)
308 numaRes, err := checkNUMAAlignment(f, pod, &cnt, logs, envInfo)
309 framework.ExpectNoError(err, "NUMA Alignment check failed for [%s] of pod [%s]", cnt.Name, pod.Name)
310 if numaRes != nil {
311 framework.Logf("NUMA resources for %s/%s: %s", pod.Name, cnt.Name, numaRes.String())
312 }
313 }
314 }
315
316
317 func validatePodAlignmentWithPodScope(ctx context.Context, f *framework.Framework, pod *v1.Pod, envInfo *testEnvInfo) error {
318
319 podsNUMA := make(map[int]int)
320
321 ginkgo.By(fmt.Sprintf("validate pod scope alignment for %s pod", pod.Name))
322 for _, cnt := range pod.Spec.Containers {
323 logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, pod.Name, cnt.Name)
324 framework.ExpectNoError(err, "NUMA alignment failed for container [%s] of pod [%s]", cnt.Name, pod.Name)
325 envMap, err := makeEnvMap(logs)
326 framework.ExpectNoError(err, "NUMA alignment failed for container [%s] of pod [%s]", cnt.Name, pod.Name)
327 cpuToNUMA, err := getCPUToNUMANodeMapFromEnv(f, pod, &cnt, envMap, envInfo.numaNodes)
328 framework.ExpectNoError(err, "NUMA alignment failed for container [%s] of pod [%s]", cnt.Name, pod.Name)
329 for cpuID, numaID := range cpuToNUMA {
330 podsNUMA[cpuID] = numaID
331 }
332 }
333
334 numaRes := numaPodResources{
335 CPUToNUMANode: podsNUMA,
336 }
337 aligned := numaRes.CheckAlignment()
338 if !aligned {
339 return fmt.Errorf("resources were assigned from different NUMA nodes")
340 }
341
342 framework.Logf("NUMA locality confirmed: all pod's CPUs aligned to the same NUMA node")
343 return nil
344 }
345
346 func runTopologyManagerPolicySuiteTests(ctx context.Context, f *framework.Framework) {
347 var cpuCap, cpuAlloc int64
348
349 cpuCap, cpuAlloc, _ = getLocalNodeCPUDetails(ctx, f)
350 ginkgo.By(fmt.Sprintf("checking node CPU capacity (%d) and allocatable CPUs (%d)", cpuCap, cpuAlloc))
351
352
353
354 if cpuAlloc < 1 {
355 e2eskipper.Skipf("Skipping basic CPU Manager tests since CPU capacity < 2")
356 }
357
358 ginkgo.By("running a non-Gu pod")
359 runNonGuPodTest(ctx, f, cpuCap)
360
361 ginkgo.By("running a Gu pod")
362 runGuPodTest(ctx, f, 1)
363
364
365 if cpuAlloc < 3 {
366 e2eskipper.Skipf("Skipping rest of the CPU Manager tests since CPU capacity < 3")
367 }
368
369 ginkgo.By("running multiple Gu and non-Gu pods")
370 runMultipleGuNonGuPods(ctx, f, cpuCap, cpuAlloc)
371
372 ginkgo.By("running a Gu pod requesting multiple CPUs")
373 runMultipleCPUGuPod(ctx, f)
374
375 ginkgo.By("running a Gu pod with multiple containers requesting integer CPUs")
376 runMultipleCPUContainersGuPod(ctx, f)
377
378 ginkgo.By("running multiple Gu pods")
379 runMultipleGuPods(ctx, f)
380 }
381
382 func runTopologyManagerPositiveTest(ctx context.Context, f *framework.Framework, numPods int, ctnAttrs, initCtnAttrs []tmCtnAttribute, envInfo *testEnvInfo) {
383 podMap := make(map[string]*v1.Pod)
384
385 for podID := 0; podID < numPods; podID++ {
386 podName := fmt.Sprintf("gu-pod-%d", podID)
387 framework.Logf("creating pod %s attrs %v", podName, ctnAttrs)
388 pod := makeTopologyManagerTestPod(podName, ctnAttrs, initCtnAttrs)
389 pod = e2epod.NewPodClient(f).CreateSync(ctx, pod)
390 framework.Logf("created pod %s", podName)
391 podMap[podName] = pod
392 }
393
394
395
396 if envInfo.policy == topologymanager.PolicySingleNumaNode {
397 for _, pod := range podMap {
398 validatePodAlignment(ctx, f, pod, envInfo)
399 }
400 if envInfo.scope == podScopeTopology {
401 for _, pod := range podMap {
402 err := validatePodAlignmentWithPodScope(ctx, f, pod, envInfo)
403 framework.ExpectNoError(err)
404 }
405 }
406 }
407
408 deletePodsAsync(ctx, f, podMap)
409 }
410
411 func deletePodsAsync(ctx context.Context, f *framework.Framework, podMap map[string]*v1.Pod) {
412 var wg sync.WaitGroup
413 for _, pod := range podMap {
414 wg.Add(1)
415 go func(podNS, podName string) {
416 defer ginkgo.GinkgoRecover()
417 defer wg.Done()
418
419 deletePodSyncByName(ctx, f, podName)
420 waitForAllContainerRemoval(ctx, podName, podNS)
421 }(pod.Namespace, pod.Name)
422 }
423 wg.Wait()
424 }
425
426 func runTopologyManagerNegativeTest(ctx context.Context, f *framework.Framework, ctnAttrs, initCtnAttrs []tmCtnAttribute, envInfo *testEnvInfo) {
427 podName := "gu-pod"
428 framework.Logf("creating pod %s attrs %v", podName, ctnAttrs)
429 pod := makeTopologyManagerTestPod(podName, ctnAttrs, initCtnAttrs)
430
431 pod = e2epod.NewPodClient(f).Create(ctx, pod)
432 err := e2epod.WaitForPodCondition(ctx, f.ClientSet, f.Namespace.Name, pod.Name, "Failed", 30*time.Second, func(pod *v1.Pod) (bool, error) {
433 if pod.Status.Phase != v1.PodPending {
434 return true, nil
435 }
436 return false, nil
437 })
438 framework.ExpectNoError(err)
439 pod, err = e2epod.NewPodClient(f).Get(ctx, pod.Name, metav1.GetOptions{})
440 framework.ExpectNoError(err)
441
442 if pod.Status.Phase != v1.PodFailed {
443 framework.Failf("pod %s not failed: %v", pod.Name, pod.Status)
444 }
445 if !isTopologyAffinityError(pod) {
446 framework.Failf("pod %s failed for wrong reason: %q", pod.Name, pod.Status.Reason)
447 }
448
449 deletePodSyncByName(ctx, f, pod.Name)
450 }
451
452 func isTopologyAffinityError(pod *v1.Pod) bool {
453 re := regexp.MustCompile(`Topology.*Affinity.*Error`)
454 return re.MatchString(pod.Status.Reason)
455 }
456
457 func getSRIOVDevicePluginConfigMap(cmFile string) *v1.ConfigMap {
458 data, err := e2etestfiles.Read(SRIOVDevicePluginCMYAML)
459 if err != nil {
460 framework.Fail(err.Error())
461 }
462
463
464 framework.Logf("host-local SRIOV Device Plugin Config Map %q", cmFile)
465 if cmFile != "" {
466 data, err = os.ReadFile(cmFile)
467 if err != nil {
468 framework.Failf("unable to load the SRIOV Device Plugin ConfigMap: %v", err)
469 }
470 } else {
471 framework.Logf("Using built-in SRIOV Device Plugin Config Map")
472 }
473
474 return readConfigMapV1OrDie(data)
475 }
476
477 type sriovData struct {
478 configMap *v1.ConfigMap
479 serviceAccount *v1.ServiceAccount
480 pod *v1.Pod
481
482 resourceName string
483 resourceAmount int64
484 }
485
486 func setupSRIOVConfigOrFail(ctx context.Context, f *framework.Framework, configMap *v1.ConfigMap) *sriovData {
487 sd := createSRIOVConfigOrFail(ctx, f, configMap)
488
489 e2enode.WaitForNodeToBeReady(ctx, f.ClientSet, framework.TestContext.NodeName, 5*time.Minute)
490
491 sd.pod = createSRIOVPodOrFail(ctx, f)
492 return sd
493 }
494
495 func createSRIOVConfigOrFail(ctx context.Context, f *framework.Framework, configMap *v1.ConfigMap) *sriovData {
496 var err error
497
498 ginkgo.By(fmt.Sprintf("Creating configMap %v/%v", metav1.NamespaceSystem, configMap.Name))
499 if _, err = f.ClientSet.CoreV1().ConfigMaps(metav1.NamespaceSystem).Create(ctx, configMap, metav1.CreateOptions{}); err != nil {
500 framework.Failf("unable to create test configMap %s: %v", configMap.Name, err)
501 }
502
503 data, err := e2etestfiles.Read(SRIOVDevicePluginSAYAML)
504 if err != nil {
505 framework.Fail(err.Error())
506 }
507 serviceAccount := readServiceAccountV1OrDie(data)
508 ginkgo.By(fmt.Sprintf("Creating serviceAccount %v/%v", metav1.NamespaceSystem, serviceAccount.Name))
509 if _, err = f.ClientSet.CoreV1().ServiceAccounts(metav1.NamespaceSystem).Create(ctx, serviceAccount, metav1.CreateOptions{}); err != nil {
510 framework.Failf("unable to create test serviceAccount %s: %v", serviceAccount.Name, err)
511 }
512
513 return &sriovData{
514 configMap: configMap,
515 serviceAccount: serviceAccount,
516 }
517 }
518
519 func createSRIOVPodOrFail(ctx context.Context, f *framework.Framework) *v1.Pod {
520 dp := getSRIOVDevicePluginPod()
521 dp.Spec.NodeName = framework.TestContext.NodeName
522
523 ginkgo.By("Create SRIOV device plugin pod")
524 dpPod, err := f.ClientSet.CoreV1().Pods(metav1.NamespaceSystem).Create(ctx, dp, metav1.CreateOptions{})
525 framework.ExpectNoError(err)
526
527 if err = e2epod.WaitForPodCondition(ctx, f.ClientSet, metav1.NamespaceSystem, dp.Name, "Ready", 120*time.Second, testutils.PodRunningReady); err != nil {
528 framework.Logf("SRIOV Pod %v took too long to enter running/ready: %v", dp.Name, err)
529 }
530 framework.ExpectNoError(err)
531
532 return dpPod
533 }
534
535
536
537 func waitForSRIOVResources(ctx context.Context, f *framework.Framework, sd *sriovData) {
538 sriovResourceName := ""
539 var sriovResourceAmount int64
540 ginkgo.By("Waiting for devices to become available on the local node")
541 gomega.Eventually(ctx, func(ctx context.Context) bool {
542 node := getLocalNode(ctx, f)
543 sriovResourceName, sriovResourceAmount = findSRIOVResource(node)
544 return sriovResourceAmount > minSriovResource
545 }, 2*time.Minute, framework.Poll).Should(gomega.BeTrue())
546
547 sd.resourceName = sriovResourceName
548 sd.resourceAmount = sriovResourceAmount
549 framework.Logf("Detected SRIOV allocatable devices name=%q amount=%d", sd.resourceName, sd.resourceAmount)
550 }
551
552 func deleteSRIOVPodOrFail(ctx context.Context, f *framework.Framework, sd *sriovData) {
553 var err error
554 gp := int64(0)
555 deleteOptions := metav1.DeleteOptions{
556 GracePeriodSeconds: &gp,
557 }
558
559 ginkgo.By(fmt.Sprintf("Delete SRIOV device plugin pod %s/%s", sd.pod.Namespace, sd.pod.Name))
560 err = f.ClientSet.CoreV1().Pods(sd.pod.Namespace).Delete(ctx, sd.pod.Name, deleteOptions)
561 framework.ExpectNoError(err)
562 waitForAllContainerRemoval(ctx, sd.pod.Name, sd.pod.Namespace)
563 }
564
565 func removeSRIOVConfigOrFail(ctx context.Context, f *framework.Framework, sd *sriovData) {
566 var err error
567 gp := int64(0)
568 deleteOptions := metav1.DeleteOptions{
569 GracePeriodSeconds: &gp,
570 }
571
572 ginkgo.By(fmt.Sprintf("Deleting configMap %v/%v", metav1.NamespaceSystem, sd.configMap.Name))
573 err = f.ClientSet.CoreV1().ConfigMaps(metav1.NamespaceSystem).Delete(ctx, sd.configMap.Name, deleteOptions)
574 framework.ExpectNoError(err)
575
576 ginkgo.By(fmt.Sprintf("Deleting serviceAccount %v/%v", metav1.NamespaceSystem, sd.serviceAccount.Name))
577 err = f.ClientSet.CoreV1().ServiceAccounts(metav1.NamespaceSystem).Delete(ctx, sd.serviceAccount.Name, deleteOptions)
578 framework.ExpectNoError(err)
579 }
580
581 func teardownSRIOVConfigOrFail(ctx context.Context, f *framework.Framework, sd *sriovData) {
582 deleteSRIOVPodOrFail(ctx, f, sd)
583 removeSRIOVConfigOrFail(ctx, f, sd)
584 }
585
586 func runTMScopeResourceAlignmentTestSuite(ctx context.Context, f *framework.Framework, configMap *v1.ConfigMap, reservedSystemCPUs, policy string, numaNodes, coreCount int) {
587 threadsPerCore := getSMTLevel()
588 sd := setupSRIOVConfigOrFail(ctx, f, configMap)
589 var ctnAttrs, initCtnAttrs []tmCtnAttribute
590
591 waitForSRIOVResources(ctx, f, sd)
592
593 envInfo := &testEnvInfo{
594 numaNodes: numaNodes,
595 sriovResourceName: sd.resourceName,
596 policy: policy,
597 scope: podScopeTopology,
598 }
599
600 ginkgo.By(fmt.Sprintf("Admit two guaranteed pods. Both consist of 2 containers, each container with 1 CPU core. Use 1 %s device.", sd.resourceName))
601 ctnAttrs = []tmCtnAttribute{
602 {
603 ctnName: "ps-container-0",
604 cpuRequest: "1000m",
605 cpuLimit: "1000m",
606 deviceName: sd.resourceName,
607 deviceRequest: "1",
608 deviceLimit: "1",
609 },
610 {
611 ctnName: "ps-container-1",
612 cpuRequest: "1000m",
613 cpuLimit: "1000m",
614 deviceName: sd.resourceName,
615 deviceRequest: "1",
616 deviceLimit: "1",
617 },
618 }
619 runTopologyManagerPositiveTest(ctx, f, 2, ctnAttrs, initCtnAttrs, envInfo)
620
621 numCores := threadsPerCore * coreCount
622 coresReq := fmt.Sprintf("%dm", numCores*1000)
623 ginkgo.By(fmt.Sprintf("Admit a guaranteed pod requesting %d CPU cores, i.e., more than can be provided at every single NUMA node. Therefore, the request should be rejected.", numCores+1))
624 ctnAttrs = []tmCtnAttribute{
625 {
626 ctnName: "gu-container-1",
627 cpuRequest: coresReq,
628 cpuLimit: coresReq,
629 deviceRequest: "1",
630 deviceLimit: "1",
631 },
632 {
633 ctnName: "gu-container-2",
634 cpuRequest: "1000m",
635 cpuLimit: "1000m",
636 deviceRequest: "1",
637 deviceLimit: "1",
638 },
639 }
640 runTopologyManagerNegativeTest(ctx, f, ctnAttrs, initCtnAttrs, envInfo)
641
642
643
644
645
646
647 coresReq = fmt.Sprintf("%dm", (numCores/2+1)*1000)
648 ginkgo.By(fmt.Sprintf("Admit two guaranteed pods, each pod requests %d cores - the pods should be placed on different NUMA nodes", numCores/2+1))
649 initCtnAttrs = []tmCtnAttribute{
650 {
651 ctnName: "init-container-1",
652 cpuRequest: coresReq,
653 cpuLimit: coresReq,
654 deviceRequest: "1",
655 deviceLimit: "1",
656 },
657 {
658 ctnName: "init-container-2",
659 cpuRequest: "1000m",
660 cpuLimit: "1000m",
661 deviceRequest: "1",
662 deviceLimit: "1",
663 },
664 }
665 ctnAttrs = []tmCtnAttribute{
666 {
667 ctnName: "gu-container-0",
668 cpuRequest: "1000m",
669 cpuLimit: "1000m",
670 deviceRequest: "1",
671 deviceLimit: "1",
672 },
673 {
674 ctnName: "gu-container-1",
675 cpuRequest: "1000m",
676 cpuLimit: "1000m",
677 deviceRequest: "1",
678 deviceLimit: "1",
679 },
680 }
681 runTopologyManagerPositiveTest(ctx, f, 2, ctnAttrs, initCtnAttrs, envInfo)
682
683 teardownSRIOVConfigOrFail(ctx, f, sd)
684 }
685
686 func runTopologyManagerNodeAlignmentSuiteTests(ctx context.Context, f *framework.Framework, sd *sriovData, reservedSystemCPUs, policy string, numaNodes, coreCount int) {
687 threadsPerCore := getSMTLevel()
688
689 waitForSRIOVResources(ctx, f, sd)
690
691 envInfo := &testEnvInfo{
692 numaNodes: numaNodes,
693 sriovResourceName: sd.resourceName,
694 policy: policy,
695 }
696
697
698 var ctnAttrs, initCtnAttrs []tmCtnAttribute
699
700
701 ginkgo.By(fmt.Sprintf("Successfully admit one guaranteed pod with 1 core, 1 %s device", sd.resourceName))
702 ctnAttrs = []tmCtnAttribute{
703 {
704 ctnName: "gu-container",
705 cpuRequest: "1000m",
706 cpuLimit: "1000m",
707 deviceName: sd.resourceName,
708 deviceRequest: "1",
709 deviceLimit: "1",
710 },
711 }
712 runTopologyManagerPositiveTest(ctx, f, 1, ctnAttrs, initCtnAttrs, envInfo)
713
714 ginkgo.By(fmt.Sprintf("Successfully admit one guaranteed pod with 2 cores, 1 %s device", sd.resourceName))
715 ctnAttrs = []tmCtnAttribute{
716 {
717 ctnName: "gu-container",
718 cpuRequest: "2000m",
719 cpuLimit: "2000m",
720 deviceName: sd.resourceName,
721 deviceRequest: "1",
722 deviceLimit: "1",
723 },
724 }
725 runTopologyManagerPositiveTest(ctx, f, 1, ctnAttrs, initCtnAttrs, envInfo)
726
727 if reservedSystemCPUs != "" {
728
729
730 numCores := threadsPerCore * coreCount
731 allCoresReq := fmt.Sprintf("%dm", numCores*1000)
732 ginkgo.By(fmt.Sprintf("Successfully admit an entire socket (%d cores), 1 %s device", numCores, sd.resourceName))
733 ctnAttrs = []tmCtnAttribute{
734 {
735 ctnName: "gu-container",
736 cpuRequest: allCoresReq,
737 cpuLimit: allCoresReq,
738 deviceName: sd.resourceName,
739 deviceRequest: "1",
740 deviceLimit: "1",
741 },
742 }
743 runTopologyManagerPositiveTest(ctx, f, 1, ctnAttrs, initCtnAttrs, envInfo)
744 }
745
746 if sd.resourceAmount > 1 {
747
748
749 ginkgo.By(fmt.Sprintf("Successfully admit two guaranteed pods, each with 1 core, 1 %s device", sd.resourceName))
750 ctnAttrs = []tmCtnAttribute{
751 {
752 ctnName: "gu-container",
753 cpuRequest: "1000m",
754 cpuLimit: "1000m",
755 deviceName: sd.resourceName,
756 deviceRequest: "1",
757 deviceLimit: "1",
758 },
759 }
760 runTopologyManagerPositiveTest(ctx, f, 2, ctnAttrs, initCtnAttrs, envInfo)
761
762 ginkgo.By(fmt.Sprintf("Successfully admit two guaranteed pods, each with 2 cores, 1 %s device", sd.resourceName))
763 ctnAttrs = []tmCtnAttribute{
764 {
765 ctnName: "gu-container",
766 cpuRequest: "2000m",
767 cpuLimit: "2000m",
768 deviceName: sd.resourceName,
769 deviceRequest: "1",
770 deviceLimit: "1",
771 },
772 }
773 runTopologyManagerPositiveTest(ctx, f, 2, ctnAttrs, initCtnAttrs, envInfo)
774
775
776 }
777
778
779 if sd.resourceAmount >= 4 {
780 ginkgo.By(fmt.Sprintf("Successfully admit a guaranteed pod requesting for two containers, each with 2 cores, 1 %s device", sd.resourceName))
781 ctnAttrs = []tmCtnAttribute{
782 {
783 ctnName: "gu-container-0",
784 cpuRequest: "2000m",
785 cpuLimit: "2000m",
786 deviceName: sd.resourceName,
787 deviceRequest: "1",
788 deviceLimit: "1",
789 },
790 {
791 ctnName: "gu-container-1",
792 cpuRequest: "2000m",
793 cpuLimit: "2000m",
794 deviceName: sd.resourceName,
795 deviceRequest: "1",
796 deviceLimit: "1",
797 },
798 }
799 runTopologyManagerPositiveTest(ctx, f, 1, ctnAttrs, initCtnAttrs, envInfo)
800
801 ginkgo.By(fmt.Sprintf("Successfully admit two guaranteed pods, each with two containers, each with 1 core, 1 %s device", sd.resourceName))
802 ctnAttrs = []tmCtnAttribute{
803 {
804 ctnName: "gu-container-0",
805 cpuRequest: "1000m",
806 cpuLimit: "1000m",
807 deviceName: sd.resourceName,
808 deviceRequest: "1",
809 deviceLimit: "1",
810 },
811 {
812 ctnName: "gu-container-1",
813 cpuRequest: "1000m",
814 cpuLimit: "1000m",
815 deviceName: sd.resourceName,
816 deviceRequest: "1",
817 deviceLimit: "1",
818 },
819 }
820 runTopologyManagerPositiveTest(ctx, f, 2, ctnAttrs, initCtnAttrs, envInfo)
821
822 ginkgo.By(fmt.Sprintf("Successfully admit two guaranteed pods, each with two containers, both with with 2 cores, one with 1 %s device", sd.resourceName))
823 ctnAttrs = []tmCtnAttribute{
824 {
825 ctnName: "gu-container-dev",
826 cpuRequest: "2000m",
827 cpuLimit: "2000m",
828 deviceName: sd.resourceName,
829 deviceRequest: "1",
830 deviceLimit: "1",
831 },
832 {
833 ctnName: "gu-container-nodev",
834 cpuRequest: "2000m",
835 cpuLimit: "2000m",
836 },
837 }
838 runTopologyManagerPositiveTest(ctx, f, 2, ctnAttrs, initCtnAttrs, envInfo)
839 }
840
841
842 if policy == topologymanager.PolicySingleNumaNode {
843
844 numCores := 1 + (threadsPerCore * coreCount)
845 excessCoresReq := fmt.Sprintf("%dm", numCores*1000)
846 ginkgo.By(fmt.Sprintf("Trying to admit a guaranteed pods, with %d cores, 1 %s device - and it should be rejected", numCores, sd.resourceName))
847 ctnAttrs = []tmCtnAttribute{
848 {
849 ctnName: "gu-container",
850 cpuRequest: excessCoresReq,
851 cpuLimit: excessCoresReq,
852 deviceName: sd.resourceName,
853 deviceRequest: "1",
854 deviceLimit: "1",
855 },
856 }
857 runTopologyManagerNegativeTest(ctx, f, ctnAttrs, initCtnAttrs, envInfo)
858 }
859 }
860
861 func runTopologyManagerTests(f *framework.Framework) {
862 var oldCfg *kubeletconfig.KubeletConfiguration
863 var err error
864
865 var policies = []string{
866 topologymanager.PolicySingleNumaNode,
867 topologymanager.PolicyRestricted,
868 topologymanager.PolicyBestEffort,
869 topologymanager.PolicyNone,
870 }
871
872 ginkgo.It("run Topology Manager policy test suite", func(ctx context.Context) {
873 oldCfg, err = getCurrentKubeletConfig(ctx)
874 framework.ExpectNoError(err)
875
876 scope := containerScopeTopology
877 for _, policy := range policies {
878
879 ginkgo.By(fmt.Sprintf("by configuring Topology Manager policy to %s", policy))
880 framework.Logf("Configuring topology Manager policy to %s", policy)
881
882 newCfg, _ := configureTopologyManagerInKubelet(oldCfg, policy, scope, nil, 0)
883 updateKubeletConfig(ctx, f, newCfg, true)
884
885 runTopologyManagerPolicySuiteTests(ctx, f)
886 }
887 })
888
889 ginkgo.It("run Topology Manager node alignment test suite", func(ctx context.Context) {
890 numaNodes, coreCount := hostPrecheck()
891
892 configMap := getSRIOVDevicePluginConfigMap(framework.TestContext.SriovdpConfigMapFile)
893
894 oldCfg, err = getCurrentKubeletConfig(ctx)
895 framework.ExpectNoError(err)
896
897 sd := setupSRIOVConfigOrFail(ctx, f, configMap)
898 ginkgo.DeferCleanup(teardownSRIOVConfigOrFail, f, sd)
899
900 scope := containerScopeTopology
901 for _, policy := range policies {
902
903 ginkgo.By(fmt.Sprintf("by configuring Topology Manager policy to %s", policy))
904 framework.Logf("Configuring topology Manager policy to %s", policy)
905
906 newCfg, reservedSystemCPUs := configureTopologyManagerInKubelet(oldCfg, policy, scope, configMap, numaNodes)
907 updateKubeletConfig(ctx, f, newCfg, true)
908
909 runTopologyManagerNodeAlignmentSuiteTests(ctx, f, sd, reservedSystemCPUs, policy, numaNodes, coreCount)
910 }
911 })
912
913 ginkgo.It("run the Topology Manager pod scope alignment test suite", func(ctx context.Context) {
914 numaNodes, coreCount := hostPrecheck()
915
916 configMap := getSRIOVDevicePluginConfigMap(framework.TestContext.SriovdpConfigMapFile)
917
918 oldCfg, err = getCurrentKubeletConfig(ctx)
919 framework.ExpectNoError(err)
920
921 policy := topologymanager.PolicySingleNumaNode
922 scope := podScopeTopology
923
924 newCfg, reservedSystemCPUs := configureTopologyManagerInKubelet(oldCfg, policy, scope, configMap, numaNodes)
925 updateKubeletConfig(ctx, f, newCfg, true)
926
927 runTMScopeResourceAlignmentTestSuite(ctx, f, configMap, reservedSystemCPUs, policy, numaNodes, coreCount)
928 })
929
930 ginkgo.AfterEach(func(ctx context.Context) {
931 if oldCfg != nil {
932
933 updateKubeletConfig(ctx, f, oldCfg, true)
934 }
935 })
936 }
937
938 func hostPrecheck() (int, int) {
939
940
941
942 numaNodes := detectNUMANodes()
943 if numaNodes < minNumaNodes {
944 e2eskipper.Skipf("this test is intended to be run on a multi-node NUMA system")
945 }
946
947 coreCount := detectCoresPerSocket()
948 if coreCount < minCoreCount {
949 e2eskipper.Skipf("this test is intended to be run on a system with at least %d cores per socket", minCoreCount)
950 }
951
952 requireSRIOVDevices()
953
954 return numaNodes, coreCount
955 }
956
957
958 var _ = SIGDescribe("Topology Manager", framework.WithSerial(), feature.TopologyManager, func() {
959 f := framework.NewDefaultFramework("topology-manager-test")
960 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
961
962 ginkgo.Context("With kubeconfig updated to static CPU Manager policy run the Topology Manager tests", func() {
963 runTopologyManagerTests(f)
964 })
965 })
966
View as plain text