4c6d5b2664b9720896c4e2c5000b45d12ecaa04c
[vpp.git] / extras / hs-test / hst_suite.go
1 package main
2
3 import (
4         "bufio"
5         "errors"
6         "flag"
7         "fmt"
8         "log/slog"
9         "os"
10         "os/exec"
11         "strings"
12         "time"
13
14         "github.com/edwarnicke/exechelper"
15         . "github.com/onsi/ginkgo/v2"
16         . "github.com/onsi/gomega"
17         "gopkg.in/yaml.v3"
18 )
19
20 const (
21         DEFAULT_NETWORK_NUM int = 1
22 )
23
24 var isPersistent = flag.Bool("persist", false, "persists topology config")
25 var isVerbose = flag.Bool("verbose", false, "verbose test output")
26 var isUnconfiguring = flag.Bool("unconfigure", false, "remove topology")
27 var isVppDebug = flag.Bool("debug", false, "attach gdb to vpp")
28 var nConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp")
29 var vppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory")
30
31 type HstSuite struct {
32         containers       map[string]*Container
33         volumes          []string
34         netConfigs       []NetConfig
35         netInterfaces    map[string]*NetInterface
36         ip4AddrAllocator *Ip4AddressAllocator
37         testIds          map[string]string
38         cpuAllocator     *CpuAllocatorT
39         cpuContexts      []*CpuContext
40         cpuPerVpp        int
41         pid              string
42 }
43
44 func (s *HstSuite) SetupSuite() {
45         var err error
46         s.pid = fmt.Sprint(os.Getpid())
47         s.cpuAllocator, err = CpuAllocator()
48         if err != nil {
49                 Fail("failed to init cpu allocator: " + fmt.Sprint(err))
50         }
51         s.cpuPerVpp = *nConfiguredCpus
52 }
53
54 func (s *HstSuite) AllocateCpus() []int {
55         cpuCtx, err := s.cpuAllocator.Allocate(s.cpuPerVpp)
56         s.assertNil(err)
57         s.AddCpuContext(cpuCtx)
58         return cpuCtx.cpus
59 }
60
61 func (s *HstSuite) AddCpuContext(cpuCtx *CpuContext) {
62         s.cpuContexts = append(s.cpuContexts, cpuCtx)
63 }
64
65 func (s *HstSuite) TearDownSuite() {
66         s.unconfigureNetworkTopology()
67 }
68
69 func (s *HstSuite) TearDownTest() {
70         if *isPersistent {
71                 return
72         }
73         for _, c := range s.cpuContexts {
74                 c.Release()
75         }
76         s.resetContainers()
77         s.removeVolumes()
78         s.ip4AddrAllocator.deleteIpAddresses()
79 }
80
81 func (s *HstSuite) skipIfUnconfiguring() {
82         if *isUnconfiguring {
83                 s.skip("skipping to unconfigure")
84         }
85 }
86
87 func (s *HstSuite) SetupTest() {
88         RegisterFailHandler(func(message string, callerSkip ...int) {
89                 s.hstFail()
90                 Fail(message, callerSkip...)
91         })
92         s.skipIfUnconfiguring()
93         s.setupVolumes()
94         s.setupContainers()
95 }
96
97 func (s *HstSuite) setupVolumes() {
98         for _, volume := range s.volumes {
99                 cmd := "docker volume create --name=" + volume
100                 s.log(cmd)
101                 exechelper.Run(cmd)
102         }
103 }
104
105 func (s *HstSuite) setupContainers() {
106         for _, container := range s.containers {
107                 if !container.isOptional {
108                         container.run()
109                 }
110         }
111 }
112
113 func logVppInstance(container *Container, maxLines int) {
114         if container.vppInstance == nil {
115                 return
116         }
117
118         logSource := container.getHostWorkDir() + defaultLogFilePath
119         file, err := os.Open(logSource)
120
121         if err != nil {
122                 return
123         }
124         defer file.Close()
125
126         scanner := bufio.NewScanner(file)
127         var lines []string
128         var counter int
129
130         for scanner.Scan() {
131                 lines = append(lines, scanner.Text())
132                 counter++
133                 if counter > maxLines {
134                         lines = lines[1:]
135                         counter--
136                 }
137         }
138
139         fmt.Println("vvvvvvvvvvvvvvv " + container.name + " [VPP instance]:")
140         for _, line := range lines {
141                 fmt.Println(line)
142         }
143         fmt.Printf("^^^^^^^^^^^^^^^\n\n")
144 }
145
146 func (s *HstSuite) hstFail() {
147         fmt.Println("Containers: " + fmt.Sprint(s.containers))
148         for _, container := range s.containers {
149                 out, err := container.log(20)
150                 if err != nil {
151                         fmt.Printf("An error occured while obtaining '%s' container logs: %s\n", container.name, fmt.Sprint(err))
152                         break
153                 }
154                 fmt.Printf("\nvvvvvvvvvvvvvvv " +
155                         container.name + ":\n" +
156                         out +
157                         "^^^^^^^^^^^^^^^\n\n")
158                 logVppInstance(container, 20)
159         }
160 }
161
162 func (s *HstSuite) assertNil(object interface{}, msgAndArgs ...interface{}) {
163         Expect(object).To(BeNil(), msgAndArgs...)
164 }
165
166 func (s *HstSuite) assertNotNil(object interface{}, msgAndArgs ...interface{}) {
167         Expect(object).ToNot(BeNil(), msgAndArgs...)
168 }
169
170 func (s *HstSuite) assertEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
171         Expect(actual).To(Equal(expected), msgAndArgs...)
172 }
173
174 func (s *HstSuite) assertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
175         Expect(actual).ToNot(Equal(expected), msgAndArgs...)
176 }
177
178 func (s *HstSuite) assertContains(testString, contains interface{}, msgAndArgs ...interface{}) {
179         Expect(testString).To(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
180 }
181
182 func (s *HstSuite) assertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) {
183         Expect(testString).ToNot(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
184 }
185
186 func (s *HstSuite) assertNotEmpty(object interface{}, msgAndArgs ...interface{}) {
187         Expect(object).ToNot(BeEmpty(), msgAndArgs...)
188 }
189
190 func (s *HstSuite) log(arg any) {
191         if *isVerbose {
192                 slog.Info(fmt.Sprint(arg))
193         }
194 }
195
196 func (s *HstSuite) skip(args string) {
197         Skip(args)
198 }
199
200 func (s *HstSuite) SkipIfMultiWorker(args ...any) {
201         if *nConfiguredCpus > 1 {
202                 s.skip("test case not supported with multiple vpp workers")
203         }
204 }
205
206 func (s *HstSuite) SkipUnlessExtendedTestsBuilt() {
207         imageName := "hs-test/nginx-http3"
208
209         cmd := exec.Command("docker", "images", imageName)
210         byteOutput, err := cmd.CombinedOutput()
211         if err != nil {
212                 s.log("error while searching for docker image")
213                 return
214         }
215         if !strings.Contains(string(byteOutput), imageName) {
216                 s.skip("extended tests not built")
217         }
218 }
219
220 func (s *HstSuite) resetContainers() {
221         for _, container := range s.containers {
222                 container.stop()
223         }
224 }
225
226 func (s *HstSuite) removeVolumes() {
227         for _, volumeName := range s.volumes {
228                 cmd := "docker volume rm " + volumeName
229                 exechelper.Run(cmd)
230                 os.RemoveAll(volumeName)
231         }
232 }
233
234 func (s *HstSuite) getNetNamespaceByName(name string) string {
235         return name + s.pid
236 }
237
238 func (s *HstSuite) getInterfaceByName(name string) *NetInterface {
239         return s.netInterfaces[name+s.pid]
240 }
241
242 func (s *HstSuite) getContainerByName(name string) *Container {
243         return s.containers[name+s.pid]
244 }
245
246 /*
247  * Create a copy and return its address, so that individial tests which call this
248  * are not able to modify the original container and affect other tests by doing that
249  */
250 func (s *HstSuite) getTransientContainerByName(name string) *Container {
251         containerCopy := *s.containers[name+s.pid]
252         return &containerCopy
253 }
254
255 func (s *HstSuite) loadContainerTopology(topologyName string) {
256         data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml")
257         if err != nil {
258                 Fail("read error: " + fmt.Sprint(err))
259         }
260         var yamlTopo YamlTopology
261         err = yaml.Unmarshal(data, &yamlTopo)
262         if err != nil {
263                 Fail("unmarshal error: " + fmt.Sprint(err))
264         }
265
266         for _, elem := range yamlTopo.Volumes {
267                 volumeMap := elem["volume"].(VolumeConfig)
268                 hostDir := volumeMap["host-dir"].(string)
269                 workingVolumeDir := logDir + CurrentSpecReport().LeafNodeText + s.pid + volumeDir
270                 volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
271                 hostDir = volDirReplacer.Replace(hostDir)
272                 s.volumes = append(s.volumes, hostDir)
273         }
274
275         s.containers = make(map[string]*Container)
276         for _, elem := range yamlTopo.Containers {
277                 newContainer, err := newContainer(s, elem)
278                 newContainer.suite = s
279                 newContainer.name += newContainer.suite.pid
280                 if err != nil {
281                         Fail("container config error: " + fmt.Sprint(err))
282                 }
283                 s.containers[newContainer.name] = newContainer
284         }
285 }
286
287 func (s *HstSuite) loadNetworkTopology(topologyName string) {
288         data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml")
289         if err != nil {
290                 Fail("read error: " + fmt.Sprint(err))
291         }
292         var yamlTopo YamlTopology
293         err = yaml.Unmarshal(data, &yamlTopo)
294         if err != nil {
295                 Fail("unmarshal error: " + fmt.Sprint(err))
296         }
297
298         s.ip4AddrAllocator = NewIp4AddressAllocator()
299         s.netInterfaces = make(map[string]*NetInterface)
300
301         for _, elem := range yamlTopo.Devices {
302                 if _, ok := elem["name"]; ok {
303                         elem["name"] = elem["name"].(string) + s.pid
304                 }
305
306                 if peer, ok := elem["peer"].(NetDevConfig); ok {
307                         if peer["name"].(string) != "" {
308                                 peer["name"] = peer["name"].(string) + s.pid
309                         }
310                         if _, ok := peer["netns"]; ok {
311                                 peer["netns"] = peer["netns"].(string) + s.pid
312                         }
313                 }
314
315                 if _, ok := elem["netns"]; ok {
316                         elem["netns"] = elem["netns"].(string) + s.pid
317                 }
318
319                 if _, ok := elem["interfaces"]; ok {
320                         interfaceCount := len(elem["interfaces"].([]interface{}))
321                         for i := 0; i < interfaceCount; i++ {
322                                 elem["interfaces"].([]interface{})[i] = elem["interfaces"].([]interface{})[i].(string) + s.pid
323                         }
324                 }
325
326                 switch elem["type"].(string) {
327                 case NetNs:
328                         {
329                                 if namespace, err := newNetNamespace(elem); err == nil {
330                                         s.netConfigs = append(s.netConfigs, &namespace)
331                                 } else {
332                                         Fail("network config error: " + fmt.Sprint(err))
333                                 }
334                         }
335                 case Veth, Tap:
336                         {
337                                 if netIf, err := newNetworkInterface(elem, s.ip4AddrAllocator); err == nil {
338                                         s.netConfigs = append(s.netConfigs, netIf)
339                                         s.netInterfaces[netIf.Name()] = netIf
340                                 } else {
341                                         Fail("network config error: " + fmt.Sprint(err))
342                                 }
343                         }
344                 case Bridge:
345                         {
346                                 if bridge, err := newBridge(elem); err == nil {
347                                         s.netConfigs = append(s.netConfigs, &bridge)
348                                 } else {
349                                         Fail("network config error: " + fmt.Sprint(err))
350                                 }
351                         }
352                 }
353         }
354 }
355
356 func (s *HstSuite) configureNetworkTopology(topologyName string) {
357         s.loadNetworkTopology(topologyName)
358
359         if *isUnconfiguring {
360                 return
361         }
362
363         for _, nc := range s.netConfigs {
364                 if err := nc.configure(); err != nil {
365                         Fail("Network config error: " + fmt.Sprint(err))
366                 }
367         }
368 }
369
370 func (s *HstSuite) unconfigureNetworkTopology() {
371         if *isPersistent {
372                 return
373         }
374         for _, nc := range s.netConfigs {
375                 nc.unconfigure()
376         }
377 }
378
379 func (s *HstSuite) getTestId() string {
380         testName := CurrentSpecReport().LeafNodeText
381
382         if s.testIds == nil {
383                 s.testIds = map[string]string{}
384         }
385
386         if _, ok := s.testIds[testName]; !ok {
387                 s.testIds[testName] = time.Now().Format("2006-01-02_15-04-05")
388         }
389
390         return s.testIds[testName]
391 }
392
393 // Returns last 4 digits of PID
394 func (s *HstSuite) getPortFromPid() string {
395         port := s.pid
396         for len(port) < 4 {
397                 port += "0"
398         }
399         return port[len(port)-4:]
400 }
401
402 func (s *HstSuite) startServerApp(running chan error, done chan struct{}, env []string) {
403         cmd := exec.Command("iperf3", "-4", "-s", "-p", s.getPortFromPid())
404         if env != nil {
405                 cmd.Env = env
406         }
407         s.log(cmd)
408         err := cmd.Start()
409         if err != nil {
410                 msg := fmt.Errorf("failed to start iperf server: %v", err)
411                 running <- msg
412                 return
413         }
414         running <- nil
415         <-done
416         cmd.Process.Kill()
417 }
418
419 func (s *HstSuite) startClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) {
420         defer func() {
421                 clnCh <- nil
422         }()
423
424         nTries := 0
425
426         for {
427                 cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g", "-p", s.getPortFromPid())
428                 if env != nil {
429                         cmd.Env = env
430                 }
431                 s.log(cmd)
432                 o, err := cmd.CombinedOutput()
433                 if err != nil {
434                         if nTries > 5 {
435                                 clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o)
436                                 return
437                         }
438                         time.Sleep(1 * time.Second)
439                         nTries++
440                         continue
441                 } else {
442                         clnRes <- fmt.Sprintf("Client output: %s", o)
443                 }
444                 break
445         }
446 }
447
448 func (s *HstSuite) startHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) {
449         cmd := newCommand([]string{"./http_server", addressPort, s.pid}, netNs)
450         err := cmd.Start()
451         s.log(cmd)
452         if err != nil {
453                 fmt.Println("Failed to start http server: " + fmt.Sprint(err))
454                 return
455         }
456         running <- struct{}{}
457         <-done
458         cmd.Process.Kill()
459 }
460
461 func (s *HstSuite) startWget(finished chan error, server_ip, port, query, netNs string) {
462         defer func() {
463                 finished <- errors.New("wget error")
464         }()
465
466         cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query},
467                 netNs)
468         s.log(cmd)
469         o, err := cmd.CombinedOutput()
470         if err != nil {
471                 finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o)
472                 return
473         } else if !strings.Contains(string(o), "200 OK") {
474                 finished <- fmt.Errorf("wget error: response not 200 OK")
475                 return
476         }
477         finished <- nil
478 }