hs-test: add http prom test
[vpp.git] / extras / hs-test / vppinstance.go
1 package main
2
3 import (
4         "fmt"
5         "os"
6         "os/exec"
7         "os/signal"
8         "strconv"
9         "strings"
10         "syscall"
11         "time"
12
13         "github.com/edwarnicke/exechelper"
14
15         "go.fd.io/govpp"
16         "go.fd.io/govpp/api"
17         "go.fd.io/govpp/binapi/af_packet"
18         interfaces "go.fd.io/govpp/binapi/interface"
19         "go.fd.io/govpp/binapi/interface_types"
20         "go.fd.io/govpp/binapi/session"
21         "go.fd.io/govpp/binapi/tapv2"
22         "go.fd.io/govpp/binapi/vpe"
23         "go.fd.io/govpp/core"
24 )
25
26 const vppConfigTemplate = `unix {
27   nodaemon
28   log %[1]s%[4]s
29   full-coredump
30   cli-listen %[1]s%[2]s
31   runtime-dir %[1]s/var/run
32   gid vpp
33 }
34
35 api-trace {
36   on
37 }
38
39 api-segment {
40   gid vpp
41 }
42
43 socksvr {
44   socket-name %[1]s%[3]s
45 }
46
47 statseg {
48   socket-name %[1]s/var/run/vpp/stats.sock
49 }
50
51 plugins {
52   plugin default { disable }
53
54   plugin unittest_plugin.so { enable }
55   plugin quic_plugin.so { enable }
56   plugin af_packet_plugin.so { enable }
57   plugin hs_apps_plugin.so { enable }
58   plugin http_plugin.so { enable }
59   plugin http_static_plugin.so { enable }
60   plugin prom_plugin.so { enable }
61 }
62
63 logging {
64   default-log-level debug
65   default-syslog-log-level debug
66 }
67
68 `
69
70 const (
71         defaultCliSocketFilePath = "/var/run/vpp/cli.sock"
72         defaultApiSocketFilePath = "/var/run/vpp/api.sock"
73         defaultLogFilePath       = "/var/log/vpp/vpp.log"
74 )
75
76 type VppInstance struct {
77         container        *Container
78         additionalConfig []Stanza
79         connection       *core.Connection
80         apiChannel       api.Channel
81         cpus             []int
82 }
83
84 func (vpp *VppInstance) getSuite() *HstSuite {
85         return vpp.container.suite
86 }
87
88 func (vpp *VppInstance) getCliSocket() string {
89         return fmt.Sprintf("%s%s", vpp.container.getContainerWorkDir(), defaultCliSocketFilePath)
90 }
91
92 func (vpp *VppInstance) getRunDir() string {
93         return vpp.container.getContainerWorkDir() + "/var/run/vpp"
94 }
95
96 func (vpp *VppInstance) getLogDir() string {
97         return vpp.container.getContainerWorkDir() + "/var/log/vpp"
98 }
99
100 func (vpp *VppInstance) getEtcDir() string {
101         return vpp.container.getContainerWorkDir() + "/etc/vpp"
102 }
103
104 func (vpp *VppInstance) start() error {
105         // Create folders
106         containerWorkDir := vpp.container.getContainerWorkDir()
107
108         vpp.container.exec("mkdir --mode=0700 -p " + vpp.getRunDir())
109         vpp.container.exec("mkdir --mode=0700 -p " + vpp.getLogDir())
110         vpp.container.exec("mkdir --mode=0700 -p " + vpp.getEtcDir())
111
112         // Create startup.conf inside the container
113         configContent := fmt.Sprintf(
114                 vppConfigTemplate,
115                 containerWorkDir,
116                 defaultCliSocketFilePath,
117                 defaultApiSocketFilePath,
118                 defaultLogFilePath,
119         )
120         configContent += vpp.generateCpuConfig()
121         for _, c := range vpp.additionalConfig {
122                 configContent += c.toString()
123         }
124         startupFileName := vpp.getEtcDir() + "/startup.conf"
125         vpp.container.createFile(startupFileName, configContent)
126
127         // create wrapper script for vppctl with proper CLI socket path
128         cliContent := "#!/usr/bin/bash\nvppctl -s " + vpp.getRunDir() + "/cli.sock"
129         vppcliFileName := "/usr/bin/vppcli"
130         vpp.container.createFile(vppcliFileName, cliContent)
131         vpp.container.exec("chmod 0755 " + vppcliFileName)
132
133         if *isVppDebug {
134                 sig := make(chan os.Signal, 1)
135                 signal.Notify(sig, syscall.SIGINT)
136                 cont := make(chan bool, 1)
137                 go func() {
138                         <-sig
139                         cont <- true
140                 }()
141
142                 vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"")
143                 fmt.Println("run following command in different terminal:")
144                 fmt.Println("docker exec -it " + vpp.container.name + " gdb -ex \"attach $(docker exec " + vpp.container.name + " pidof vpp)\"")
145                 fmt.Println("Afterwards press CTRL+C to continue")
146                 <-cont
147                 fmt.Println("continuing...")
148         } else {
149                 // Start VPP
150                 vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"")
151         }
152
153         // Connect to VPP and store the connection
154         sockAddress := vpp.container.getHostWorkDir() + defaultApiSocketFilePath
155         conn, connEv, err := govpp.AsyncConnect(
156                 sockAddress,
157                 core.DefaultMaxReconnectAttempts,
158                 core.DefaultReconnectInterval)
159         if err != nil {
160                 fmt.Println("async connect error: ", err)
161         }
162         vpp.connection = conn
163
164         // ... wait for Connected event
165         e := <-connEv
166         if e.State != core.Connected {
167                 fmt.Println("connecting to VPP failed: ", e.Error)
168         }
169
170         // ... check compatibility of used messages
171         ch, err := conn.NewAPIChannel()
172         if err != nil {
173                 fmt.Println("creating channel failed: ", err)
174         }
175         if err := ch.CheckCompatiblity(vpe.AllMessages()...); err != nil {
176                 fmt.Println("compatibility error: ", err)
177         }
178         if err := ch.CheckCompatiblity(interfaces.AllMessages()...); err != nil {
179                 fmt.Println("compatibility error: ", err)
180         }
181         vpp.apiChannel = ch
182
183         return nil
184 }
185
186 func (vpp *VppInstance) vppctl(command string, arguments ...any) string {
187         vppCliCommand := fmt.Sprintf(command, arguments...)
188         containerExecCommand := fmt.Sprintf("docker exec --detach=false %[1]s vppctl -s %[2]s %[3]s",
189                 vpp.container.name, vpp.getCliSocket(), vppCliCommand)
190         vpp.getSuite().log(containerExecCommand)
191         output, err := exechelper.CombinedOutput(containerExecCommand)
192         vpp.getSuite().assertNil(err)
193
194         return string(output)
195 }
196
197 func (vpp *VppInstance) GetSessionStat(stat string) int {
198         o := vpp.vppctl("show session stats")
199         vpp.getSuite().log(o)
200         for _, line := range strings.Split(o, "\n") {
201                 if strings.Contains(line, stat) {
202                         tokens := strings.Split(strings.TrimSpace(line), " ")
203                         val, err := strconv.Atoi(tokens[0])
204                         if err != nil {
205                                 vpp.getSuite().FailNow("failed to parse stat value %s", err)
206                                 return 0
207                         }
208                         return val
209                 }
210         }
211         return 0
212 }
213
214 func (vpp *VppInstance) waitForApp(appName string, timeout int) {
215         for i := 0; i < timeout; i++ {
216                 o := vpp.vppctl("show app")
217                 if strings.Contains(o, appName) {
218                         return
219                 }
220                 time.Sleep(1 * time.Second)
221         }
222         vpp.getSuite().assertNil(1, "Timeout while waiting for app '%s'", appName)
223 }
224
225 func (vpp *VppInstance) createAfPacket(
226         veth *NetInterface,
227 ) (interface_types.InterfaceIndex, error) {
228         createReq := &af_packet.AfPacketCreateV2{
229                 UseRandomHwAddr: true,
230                 HostIfName:      veth.Name(),
231         }
232         if veth.hwAddress != (MacAddress{}) {
233                 createReq.UseRandomHwAddr = false
234                 createReq.HwAddr = veth.hwAddress
235         }
236         createReply := &af_packet.AfPacketCreateV2Reply{}
237
238         if err := vpp.apiChannel.SendRequest(createReq).ReceiveReply(createReply); err != nil {
239                 return 0, err
240         }
241         veth.index = createReply.SwIfIndex
242
243         // Set to up
244         upReq := &interfaces.SwInterfaceSetFlags{
245                 SwIfIndex: veth.index,
246                 Flags:     interface_types.IF_STATUS_API_FLAG_ADMIN_UP,
247         }
248         upReply := &interfaces.SwInterfaceSetFlagsReply{}
249
250         if err := vpp.apiChannel.SendRequest(upReq).ReceiveReply(upReply); err != nil {
251                 return 0, err
252         }
253
254         // Add address
255         if veth.addressWithPrefix() == (AddressWithPrefix{}) {
256                 var err error
257                 var ip4Address string
258                 if ip4Address, err = veth.ip4AddrAllocator.NewIp4InterfaceAddress(veth.peer.networkNumber); err == nil {
259                         veth.ip4Address = ip4Address
260                 } else {
261                         return 0, err
262                 }
263         }
264         addressReq := &interfaces.SwInterfaceAddDelAddress{
265                 IsAdd:     true,
266                 SwIfIndex: veth.index,
267                 Prefix:    veth.addressWithPrefix(),
268         }
269         addressReply := &interfaces.SwInterfaceAddDelAddressReply{}
270
271         if err := vpp.apiChannel.SendRequest(addressReq).ReceiveReply(addressReply); err != nil {
272                 return 0, err
273         }
274
275         return veth.index, nil
276 }
277
278 func (vpp *VppInstance) addAppNamespace(
279         secret uint64,
280         ifx interface_types.InterfaceIndex,
281         namespaceId string,
282 ) error {
283         req := &session.AppNamespaceAddDelV2{
284                 Secret:      secret,
285                 SwIfIndex:   ifx,
286                 NamespaceID: namespaceId,
287         }
288         reply := &session.AppNamespaceAddDelV2Reply{}
289
290         if err := vpp.apiChannel.SendRequest(req).ReceiveReply(reply); err != nil {
291                 return err
292         }
293
294         sessionReq := &session.SessionEnableDisable{
295                 IsEnable: true,
296         }
297         sessionReply := &session.SessionEnableDisableReply{}
298
299         if err := vpp.apiChannel.SendRequest(sessionReq).ReceiveReply(sessionReply); err != nil {
300                 return err
301         }
302
303         return nil
304 }
305
306 func (vpp *VppInstance) createTap(
307         tap *NetInterface,
308         tapId ...uint32,
309 ) error {
310         var id uint32 = 1
311         if len(tapId) > 0 {
312                 id = tapId[0]
313         }
314         createTapReq := &tapv2.TapCreateV2{
315                 ID:               id,
316                 HostIfNameSet:    true,
317                 HostIfName:       tap.Name(),
318                 HostIP4PrefixSet: true,
319                 HostIP4Prefix:    tap.ip4AddressWithPrefix(),
320         }
321         createTapReply := &tapv2.TapCreateV2Reply{}
322
323         // Create tap interface
324         if err := vpp.apiChannel.SendRequest(createTapReq).ReceiveReply(createTapReply); err != nil {
325                 return err
326         }
327
328         // Add address
329         addAddressReq := &interfaces.SwInterfaceAddDelAddress{
330                 IsAdd:     true,
331                 SwIfIndex: createTapReply.SwIfIndex,
332                 Prefix:    tap.peer.addressWithPrefix(),
333         }
334         addAddressReply := &interfaces.SwInterfaceAddDelAddressReply{}
335
336         if err := vpp.apiChannel.SendRequest(addAddressReq).ReceiveReply(addAddressReply); err != nil {
337                 return err
338         }
339
340         // Set interface to up
341         upReq := &interfaces.SwInterfaceSetFlags{
342                 SwIfIndex: createTapReply.SwIfIndex,
343                 Flags:     interface_types.IF_STATUS_API_FLAG_ADMIN_UP,
344         }
345         upReply := &interfaces.SwInterfaceSetFlagsReply{}
346
347         if err := vpp.apiChannel.SendRequest(upReq).ReceiveReply(upReply); err != nil {
348                 return err
349         }
350
351         return nil
352 }
353
354 func (vpp *VppInstance) saveLogs() {
355         logTarget := vpp.container.getLogDirPath() + "vppinstance-" + vpp.container.name + ".log"
356         logSource := vpp.container.getHostWorkDir() + defaultLogFilePath
357         cmd := exec.Command("cp", logSource, logTarget)
358         vpp.getSuite().T().Helper()
359         vpp.getSuite().log(cmd.String())
360         cmd.Run()
361 }
362
363 func (vpp *VppInstance) disconnect() {
364         vpp.connection.Disconnect()
365         vpp.apiChannel.Close()
366 }
367
368 func (vpp *VppInstance) generateCpuConfig() string {
369         var c Stanza
370         var s string
371         if len(vpp.cpus) < 1 {
372                 return ""
373         }
374         c.newStanza("cpu").
375                 append(fmt.Sprintf("main-core %d", vpp.cpus[0]))
376         workers := vpp.cpus[1:]
377
378         if len(workers) > 0 {
379                 for i := 0; i < len(workers); i++ {
380                         if i != 0 {
381                                 s = s + ", "
382                         }
383                         s = s + fmt.Sprintf("%d", workers[i])
384                 }
385                 c.append(fmt.Sprintf("corelist-workers %s", s))
386         }
387         return c.close().toString()
388 }