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