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