blob: 028ab0bef1fee425f026573610eadbfe75909577 [file] [log] [blame]
Adrian Villin4677d922024-06-14 09:32:39 +02001package hst
2
3import (
4 "bufio"
5 "errors"
6 "flag"
7 "fmt"
8 "io"
9 "log"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "runtime"
14 "strings"
15 "time"
16
Adrian Villin25140012024-07-09 15:31:36 +020017 containerTypes "github.com/docker/docker/api/types/container"
18 "github.com/docker/docker/client"
Adrian Villin4677d922024-06-14 09:32:39 +020019 "github.com/onsi/gomega/gmeasure"
20 "gopkg.in/yaml.v3"
21
Adrian Villin4677d922024-06-14 09:32:39 +020022 . "github.com/onsi/ginkgo/v2"
23 . "github.com/onsi/gomega"
24)
25
26const (
27 DEFAULT_NETWORK_NUM int = 1
28)
29
30var IsPersistent = flag.Bool("persist", false, "persists topology config")
31var IsVerbose = flag.Bool("verbose", false, "verbose test output")
32var IsUnconfiguring = flag.Bool("unconfigure", false, "remove topology")
33var IsVppDebug = flag.Bool("debug", false, "attach gdb to vpp")
34var NConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp")
35var VppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory")
Adrian Villinb4516bb2024-06-19 06:20:00 -040036var IsDebugBuild = flag.Bool("debug_build", false, "some paths are different with debug build")
Adrian Villin5d171eb2024-06-17 08:51:27 +020037var UseCpu0 = flag.Bool("cpu0", false, "use cpu0")
Matus Fabiane99d2662024-07-19 16:04:09 +020038var IsLeakCheck = flag.Bool("leak_check", false, "run leak-check tests")
Adrian Villin5d171eb2024-06-17 08:51:27 +020039var NumaAwareCpuAlloc bool
Adrian Villin4677d922024-06-14 09:32:39 +020040var SuiteTimeout time.Duration
41
42type HstSuite struct {
43 Containers map[string]*Container
44 StartedContainers []*Container
45 Volumes []string
46 NetConfigs []NetConfig
47 NetInterfaces map[string]*NetInterface
48 Ip4AddrAllocator *Ip4AddressAllocator
49 TestIds map[string]string
50 CpuAllocator *CpuAllocatorT
51 CpuContexts []*CpuContext
Adrian Villinb69ee002024-07-17 14:38:48 +020052 CpuCount int
Adrian Villin4677d922024-06-14 09:32:39 +020053 Ppid string
54 ProcessIndex string
55 Logger *log.Logger
56 LogFile *os.File
Adrian Villin25140012024-07-09 15:31:36 +020057 Docker *client.Client
Adrian Villin4677d922024-06-14 09:32:39 +020058}
59
60func getTestFilename() string {
61 _, filename, _, _ := runtime.Caller(2)
62 return filepath.Base(filename)
63}
64
Adrian Villin25140012024-07-09 15:31:36 +020065func (s *HstSuite) newDockerClient() {
66 var err error
67 s.Docker, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
68 s.AssertNil(err)
69 s.Log("docker client created")
70}
71
Adrian Villin4677d922024-06-14 09:32:39 +020072func (s *HstSuite) SetupSuite() {
73 s.CreateLogger()
Adrian Villin25140012024-07-09 15:31:36 +020074 s.newDockerClient()
Adrian Villin4677d922024-06-14 09:32:39 +020075 s.Log("Suite Setup")
76 RegisterFailHandler(func(message string, callerSkip ...int) {
77 s.HstFail()
78 Fail(message, callerSkip...)
79 })
80 var err error
81 s.Ppid = fmt.Sprint(os.Getppid())
82 // remove last number so we have space to prepend a process index (interfaces have a char limit)
83 s.Ppid = s.Ppid[:len(s.Ppid)-1]
84 s.ProcessIndex = fmt.Sprint(GinkgoParallelProcess())
85 s.CpuAllocator, err = CpuAllocator()
86 if err != nil {
87 Fail("failed to init cpu allocator: " + fmt.Sprint(err))
88 }
Adrian Villinb69ee002024-07-17 14:38:48 +020089 s.CpuCount = *NConfiguredCpus
Adrian Villin4677d922024-06-14 09:32:39 +020090}
91
92func (s *HstSuite) AllocateCpus() []int {
Adrian Villinb69ee002024-07-17 14:38:48 +020093 cpuCtx, err := s.CpuAllocator.Allocate(len(s.StartedContainers), s.CpuCount)
Adrian Villin5d171eb2024-06-17 08:51:27 +020094 // using Fail instead of AssertNil to make error message more readable
95 if err != nil {
96 Fail(fmt.Sprint(err))
97 }
Adrian Villin4677d922024-06-14 09:32:39 +020098 s.AddCpuContext(cpuCtx)
99 return cpuCtx.cpus
100}
101
102func (s *HstSuite) AddCpuContext(cpuCtx *CpuContext) {
103 s.CpuContexts = append(s.CpuContexts, cpuCtx)
104}
105
106func (s *HstSuite) TearDownSuite() {
107 defer s.LogFile.Close()
Adrian Villin25140012024-07-09 15:31:36 +0200108 defer s.Docker.Close()
Adrian Villin4677d922024-06-14 09:32:39 +0200109 s.Log("Suite Teardown")
110 s.UnconfigureNetworkTopology()
111}
112
113func (s *HstSuite) TearDownTest() {
114 s.Log("Test Teardown")
115 if *IsPersistent {
116 return
117 }
118 s.ResetContainers()
Adrian Villinb69ee002024-07-17 14:38:48 +0200119
120 if s.Ip4AddrAllocator != nil {
121 s.Ip4AddrAllocator.DeleteIpAddresses()
122 }
Adrian Villin4677d922024-06-14 09:32:39 +0200123}
124
125func (s *HstSuite) SkipIfUnconfiguring() {
126 if *IsUnconfiguring {
127 s.Skip("skipping to unconfigure")
128 }
129}
130
131func (s *HstSuite) SetupTest() {
132 s.Log("Test Setup")
133 s.StartedContainers = s.StartedContainers[:0]
134 s.SkipIfUnconfiguring()
Adrian Villin4677d922024-06-14 09:32:39 +0200135 s.SetupContainers()
136}
137
Adrian Villin4677d922024-06-14 09:32:39 +0200138func (s *HstSuite) SetupContainers() {
139 for _, container := range s.Containers {
140 if !container.IsOptional {
141 container.Run()
142 }
143 }
144}
145
146func (s *HstSuite) LogVppInstance(container *Container, maxLines int) {
147 if container.VppInstance == nil {
148 return
149 }
150
151 logSource := container.GetHostWorkDir() + defaultLogFilePath
152 file, err := os.Open(logSource)
153
154 if err != nil {
155 return
156 }
157 defer file.Close()
158
159 scanner := bufio.NewScanner(file)
160 var lines []string
161 var counter int
162
163 for scanner.Scan() {
164 lines = append(lines, scanner.Text())
165 counter++
166 if counter > maxLines {
167 lines = lines[1:]
168 counter--
169 }
170 }
171
172 s.Log("vvvvvvvvvvvvvvv " + container.Name + " [VPP instance]:")
173 for _, line := range lines {
174 s.Log(line)
175 }
176 s.Log("^^^^^^^^^^^^^^^\n\n")
177}
178
179func (s *HstSuite) HstFail() {
180 for _, container := range s.StartedContainers {
181 out, err := container.log(20)
182 if err != nil {
183 s.Log("An error occured while obtaining '" + container.Name + "' container logs: " + fmt.Sprint(err))
184 s.Log("The container might not be running - check logs in " + container.getLogDirPath())
185 continue
186 }
187 s.Log("\nvvvvvvvvvvvvvvv " +
188 container.Name + ":\n" +
189 out +
190 "^^^^^^^^^^^^^^^\n\n")
191 s.LogVppInstance(container, 20)
192 }
193}
194
195func (s *HstSuite) AssertNil(object interface{}, msgAndArgs ...interface{}) {
196 Expect(object).To(BeNil(), msgAndArgs...)
197}
198
199func (s *HstSuite) AssertNotNil(object interface{}, msgAndArgs ...interface{}) {
200 Expect(object).ToNot(BeNil(), msgAndArgs...)
201}
202
203func (s *HstSuite) AssertEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
204 Expect(actual).To(Equal(expected), msgAndArgs...)
205}
206
207func (s *HstSuite) AssertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
208 Expect(actual).ToNot(Equal(expected), msgAndArgs...)
209}
210
211func (s *HstSuite) AssertContains(testString, contains interface{}, msgAndArgs ...interface{}) {
212 Expect(testString).To(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
213}
214
215func (s *HstSuite) AssertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) {
216 Expect(testString).ToNot(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
217}
218
Adrian Villin25140012024-07-09 15:31:36 +0200219func (s *HstSuite) AssertEmpty(object interface{}, msgAndArgs ...interface{}) {
220 Expect(object).To(BeEmpty(), msgAndArgs...)
221}
222
Adrian Villin4677d922024-06-14 09:32:39 +0200223func (s *HstSuite) AssertNotEmpty(object interface{}, msgAndArgs ...interface{}) {
224 Expect(object).ToNot(BeEmpty(), msgAndArgs...)
225}
226
Matus Fabiand086a362024-06-27 13:20:10 +0200227func (s *HstSuite) AssertMatchError(actual, expected error, msgAndArgs ...interface{}) {
228 Expect(actual).To(MatchError(expected))
229}
230
Adrian Villin4677d922024-06-14 09:32:39 +0200231func (s *HstSuite) CreateLogger() {
232 suiteName := s.GetCurrentSuiteName()
233 var err error
234 s.LogFile, err = os.Create("summary/" + suiteName + ".log")
235 if err != nil {
236 Fail("Unable to create log file.")
237 }
238 s.Logger = log.New(io.Writer(s.LogFile), "", log.LstdFlags)
239}
240
241// Logs to files by default, logs to stdout when VERBOSE=true with GinkgoWriter
242// to keep console tidy
243func (s *HstSuite) Log(arg any) {
244 logs := strings.Split(fmt.Sprint(arg), "\n")
245 for _, line := range logs {
246 s.Logger.Println(line)
247 }
248 if *IsVerbose {
249 GinkgoWriter.Println(arg)
250 }
251}
252
253func (s *HstSuite) Skip(args string) {
254 Skip(args)
255}
256
257func (s *HstSuite) SkipIfMultiWorker(args ...any) {
258 if *NConfiguredCpus > 1 {
259 s.Skip("test case not supported with multiple vpp workers")
260 }
261}
262
Adrian Villinb69ee002024-07-17 14:38:48 +0200263func (s *HstSuite) SkipIfNotEnoughAvailableCpus() bool {
264 var MaxRequestedCpu int
265
266 if s.CpuAllocator.runningInCi {
267 MaxRequestedCpu = ((s.CpuAllocator.buildNumber + 1) * s.CpuAllocator.maxContainerCount * s.CpuCount)
268 } else {
269 MaxRequestedCpu = (GinkgoParallelProcess() * s.CpuAllocator.maxContainerCount * s.CpuCount)
270 }
Hadi Rayan Al-Sandide0e85132024-06-24 10:28:58 +0200271
272 if len(s.CpuAllocator.cpus)-1 < MaxRequestedCpu {
Adrian Villinb69ee002024-07-17 14:38:48 +0200273 s.Skip(fmt.Sprintf("test case cannot allocate requested cpus (%d cpus * %d containers)", s.CpuCount, s.CpuAllocator.maxContainerCount))
Hadi Rayan Al-Sandide0e85132024-06-24 10:28:58 +0200274 }
275
276 return true
277}
278
Adrian Villin4677d922024-06-14 09:32:39 +0200279func (s *HstSuite) SkipUnlessExtendedTestsBuilt() {
280 imageName := "hs-test/nginx-http3"
281
282 cmd := exec.Command("docker", "images", imageName)
283 byteOutput, err := cmd.CombinedOutput()
284 if err != nil {
285 s.Log("error while searching for docker image")
286 return
287 }
288 if !strings.Contains(string(byteOutput), imageName) {
289 s.Skip("extended tests not built")
290 }
291}
292
Matus Fabiane99d2662024-07-19 16:04:09 +0200293func (s *HstSuite) SkipUnlessLeakCheck() {
294 if !*IsLeakCheck {
295 s.Skip("leak-check tests excluded")
296 }
297}
Adrian Villin4677d922024-06-14 09:32:39 +0200298func (s *HstSuite) ResetContainers() {
299 for _, container := range s.StartedContainers {
300 container.stop()
Adrian Villin25140012024-07-09 15:31:36 +0200301 s.Log("Removing container " + container.Name)
302 if err := s.Docker.ContainerRemove(container.ctx, container.ID, containerTypes.RemoveOptions{RemoveVolumes: true}); err != nil {
303 s.Log(err)
304 }
Adrian Villin4677d922024-06-14 09:32:39 +0200305 }
306}
307
308func (s *HstSuite) GetNetNamespaceByName(name string) string {
309 return s.ProcessIndex + name + s.Ppid
310}
311
312func (s *HstSuite) GetInterfaceByName(name string) *NetInterface {
313 return s.NetInterfaces[s.ProcessIndex+name+s.Ppid]
314}
315
316func (s *HstSuite) GetContainerByName(name string) *Container {
317 return s.Containers[s.ProcessIndex+name+s.Ppid]
318}
319
320/*
321 * Create a copy and return its address, so that individial tests which call this
322 * are not able to modify the original container and affect other tests by doing that
323 */
324func (s *HstSuite) GetTransientContainerByName(name string) *Container {
325 containerCopy := *s.Containers[s.ProcessIndex+name+s.Ppid]
326 return &containerCopy
327}
328
329func (s *HstSuite) LoadContainerTopology(topologyName string) {
330 data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml")
331 if err != nil {
332 Fail("read error: " + fmt.Sprint(err))
333 }
334 var yamlTopo YamlTopology
335 err = yaml.Unmarshal(data, &yamlTopo)
336 if err != nil {
337 Fail("unmarshal error: " + fmt.Sprint(err))
338 }
339
340 for _, elem := range yamlTopo.Volumes {
341 volumeMap := elem["volume"].(VolumeConfig)
342 hostDir := volumeMap["host-dir"].(string)
343 workingVolumeDir := logDir + s.GetCurrentTestName() + volumeDir
344 volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
345 hostDir = volDirReplacer.Replace(hostDir)
346 s.Volumes = append(s.Volumes, hostDir)
347 }
348
349 s.Containers = make(map[string]*Container)
350 for _, elem := range yamlTopo.Containers {
351 newContainer, err := newContainer(s, elem)
352 newContainer.Suite = s
353 newContainer.Name = newContainer.Suite.ProcessIndex + newContainer.Name + newContainer.Suite.Ppid
354 if err != nil {
355 Fail("container config error: " + fmt.Sprint(err))
356 }
357 s.Containers[newContainer.Name] = newContainer
358 }
359}
360
361func (s *HstSuite) LoadNetworkTopology(topologyName string) {
362 data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml")
363 if err != nil {
364 Fail("read error: " + fmt.Sprint(err))
365 }
366 var yamlTopo YamlTopology
367 err = yaml.Unmarshal(data, &yamlTopo)
368 if err != nil {
369 Fail("unmarshal error: " + fmt.Sprint(err))
370 }
371
372 s.Ip4AddrAllocator = NewIp4AddressAllocator()
373 s.NetInterfaces = make(map[string]*NetInterface)
374
375 for _, elem := range yamlTopo.Devices {
376 if _, ok := elem["name"]; ok {
377 elem["name"] = s.ProcessIndex + elem["name"].(string) + s.Ppid
378 }
379
380 if peer, ok := elem["peer"].(NetDevConfig); ok {
381 if peer["name"].(string) != "" {
382 peer["name"] = s.ProcessIndex + peer["name"].(string) + s.Ppid
383 }
384 if _, ok := peer["netns"]; ok {
385 peer["netns"] = s.ProcessIndex + peer["netns"].(string) + s.Ppid
386 }
387 }
388
389 if _, ok := elem["netns"]; ok {
390 elem["netns"] = s.ProcessIndex + elem["netns"].(string) + s.Ppid
391 }
392
393 if _, ok := elem["interfaces"]; ok {
394 interfaceCount := len(elem["interfaces"].([]interface{}))
395 for i := 0; i < interfaceCount; i++ {
396 elem["interfaces"].([]interface{})[i] = s.ProcessIndex + elem["interfaces"].([]interface{})[i].(string) + s.Ppid
397 }
398 }
399
400 switch elem["type"].(string) {
401 case NetNs:
402 {
403 if namespace, err := newNetNamespace(elem); err == nil {
404 s.NetConfigs = append(s.NetConfigs, &namespace)
405 } else {
406 Fail("network config error: " + fmt.Sprint(err))
407 }
408 }
409 case Veth, Tap:
410 {
411 if netIf, err := newNetworkInterface(elem, s.Ip4AddrAllocator); err == nil {
412 s.NetConfigs = append(s.NetConfigs, netIf)
413 s.NetInterfaces[netIf.Name()] = netIf
414 } else {
415 Fail("network config error: " + fmt.Sprint(err))
416 }
417 }
418 case Bridge:
419 {
420 if bridge, err := newBridge(elem); err == nil {
421 s.NetConfigs = append(s.NetConfigs, &bridge)
422 } else {
423 Fail("network config error: " + fmt.Sprint(err))
424 }
425 }
426 }
427 }
428}
429
430func (s *HstSuite) ConfigureNetworkTopology(topologyName string) {
431 s.LoadNetworkTopology(topologyName)
432
433 if *IsUnconfiguring {
434 return
435 }
436
437 for _, nc := range s.NetConfigs {
438 s.Log(nc.Name())
439 if err := nc.configure(); err != nil {
440 Fail("Network config error: " + fmt.Sprint(err))
441 }
442 }
443}
444
445func (s *HstSuite) UnconfigureNetworkTopology() {
446 if *IsPersistent {
447 return
448 }
449 for _, nc := range s.NetConfigs {
450 nc.unconfigure()
451 }
452}
453
454func (s *HstSuite) GetTestId() string {
455 testName := s.GetCurrentTestName()
456
457 if s.TestIds == nil {
458 s.TestIds = map[string]string{}
459 }
460
461 if _, ok := s.TestIds[testName]; !ok {
462 s.TestIds[testName] = time.Now().Format("2006-01-02_15-04-05")
463 }
464
465 return s.TestIds[testName]
466}
467
468func (s *HstSuite) GetCurrentTestName() string {
469 return strings.Split(CurrentSpecReport().LeafNodeText, "/")[1]
470}
471
472func (s *HstSuite) GetCurrentSuiteName() string {
473 return CurrentSpecReport().ContainerHierarchyTexts[0]
474}
475
476// Returns last 3 digits of PID + Ginkgo process index as the 4th digit
477func (s *HstSuite) GetPortFromPpid() string {
478 port := s.Ppid
479 for len(port) < 3 {
480 port += "0"
481 }
482 return port[len(port)-3:] + s.ProcessIndex
483}
484
485func (s *HstSuite) StartServerApp(running chan error, done chan struct{}, env []string) {
486 cmd := exec.Command("iperf3", "-4", "-s", "-p", s.GetPortFromPpid())
487 if env != nil {
488 cmd.Env = env
489 }
490 s.Log(cmd)
491 err := cmd.Start()
492 if err != nil {
493 msg := fmt.Errorf("failed to start iperf server: %v", err)
494 running <- msg
495 return
496 }
497 running <- nil
498 <-done
499 cmd.Process.Kill()
500}
501
502func (s *HstSuite) StartClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) {
503 defer func() {
504 clnCh <- nil
505 }()
506
507 nTries := 0
508
509 for {
510 cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g", "-p", s.GetPortFromPpid())
511 if env != nil {
512 cmd.Env = env
513 }
514 s.Log(cmd)
515 o, err := cmd.CombinedOutput()
516 if err != nil {
517 if nTries > 5 {
518 clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o)
519 return
520 }
521 time.Sleep(1 * time.Second)
522 nTries++
523 continue
524 } else {
525 clnRes <- fmt.Sprintf("Client output: %s", o)
526 }
527 break
528 }
529}
530
531func (s *HstSuite) StartHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) {
532 cmd := newCommand([]string{"./http_server", addressPort, s.Ppid, s.ProcessIndex}, netNs)
533 err := cmd.Start()
534 s.Log(cmd)
535 if err != nil {
536 s.Log("Failed to start http server: " + fmt.Sprint(err))
537 return
538 }
539 running <- struct{}{}
540 <-done
541 cmd.Process.Kill()
542}
543
544func (s *HstSuite) StartWget(finished chan error, server_ip, port, query, netNs string) {
545 defer func() {
546 finished <- errors.New("wget error")
547 }()
548
549 cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query},
550 netNs)
551 s.Log(cmd)
552 o, err := cmd.CombinedOutput()
553 if err != nil {
554 finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o)
555 return
556 } else if !strings.Contains(string(o), "200 OK") {
557 finished <- fmt.Errorf("wget error: response not 200 OK")
558 return
559 }
560 finished <- nil
561}
562
563/*
564runBenchmark creates Gomega's experiment with the passed-in name and samples the passed-in callback repeatedly (samplesNum times),
565passing in suite context, experiment and your data.
566
567You can also instruct runBenchmark to run with multiple concurrent workers.
Matus Fabian5c4c1b62024-06-28 16:11:04 +0200568Note that if running in parallel Gomega returns from Sample when spins up all samples and does not wait until all finished.
Adrian Villin4677d922024-06-14 09:32:39 +0200569You can record multiple named measurements (float64 or duration) within passed-in callback.
570runBenchmark then produces report to show statistical distribution of measurements.
571*/
572func (s *HstSuite) RunBenchmark(name string, samplesNum, parallelNum int, callback func(s *HstSuite, e *gmeasure.Experiment, data interface{}), data interface{}) {
573 experiment := gmeasure.NewExperiment(name)
574
575 experiment.Sample(func(idx int) {
576 defer GinkgoRecover()
577 callback(s, experiment, data)
578 }, gmeasure.SamplingConfig{N: samplesNum, NumParallel: parallelNum})
579 AddReportEntry(experiment.Name, experiment)
580}