From 4677d920c0b0ff1f1aae81fb2f0052d939a2e89c Mon Sep 17 00:00:00 2001 From: Adrian Villin Date: Fri, 14 Jun 2024 09:32:39 +0200 Subject: hs-test: separate infra from tests - most functions and vars now start with a capital letter: needed to access them outside the package that declares them - updated README.md - very minor changes in MAKEFILE Type: test Change-Id: I4b5a194f08f09d59e372e57da6451fbb5a1de4da Signed-off-by: Adrian Villin --- extras/hs-test/Makefile | 34 +- extras/hs-test/README.rst | 176 ++++---- extras/hs-test/address_allocator.go | 98 ----- extras/hs-test/container.go | 380 ----------------- extras/hs-test/cpu.go | 100 ----- extras/hs-test/echo_test.go | 53 +-- extras/hs-test/framework_test.go | 9 +- extras/hs-test/hst_suite.go | 535 ------------------------ extras/hs-test/http_test.go | 661 +++++++++++++++--------------- extras/hs-test/infra/address_allocator.go | 98 +++++ extras/hs-test/infra/container.go | 380 +++++++++++++++++ extras/hs-test/infra/cpu.go | 100 +++++ extras/hs-test/infra/hst_suite.go | 544 ++++++++++++++++++++++++ extras/hs-test/infra/netconfig.go | 383 +++++++++++++++++ extras/hs-test/infra/suite_nginx.go | 137 +++++++ extras/hs-test/infra/suite_no_topo.go | 112 +++++ extras/hs-test/infra/suite_ns.go | 121 ++++++ extras/hs-test/infra/suite_tap.go | 88 ++++ extras/hs-test/infra/suite_veth.go | 146 +++++++ extras/hs-test/infra/topo.go | 25 ++ extras/hs-test/infra/utils.go | 119 ++++++ extras/hs-test/infra/vppinstance.go | 500 ++++++++++++++++++++++ extras/hs-test/ldp_test.go | 71 ++-- extras/hs-test/linux_iperf_test.go | 22 +- extras/hs-test/mirroring_test.go | 15 +- extras/hs-test/netconfig.go | 383 ----------------- extras/hs-test/proxy_test.go | 66 +-- extras/hs-test/raw_session_test.go | 32 +- extras/hs-test/suite_nginx_test.go | 137 ------- extras/hs-test/suite_no_topo_test.go | 112 ----- extras/hs-test/suite_ns_test.go | 121 ------ extras/hs-test/suite_tap_test.go | 88 ---- extras/hs-test/suite_veth_test.go | 146 ------- extras/hs-test/topo.go | 25 -- extras/hs-test/utils.go | 119 ------ extras/hs-test/vcl_test.go | 155 +++---- extras/hs-test/vppinstance.go | 499 ---------------------- 37 files changed, 3395 insertions(+), 3395 deletions(-) delete mode 100644 extras/hs-test/address_allocator.go delete mode 100644 extras/hs-test/container.go delete mode 100644 extras/hs-test/cpu.go delete mode 100644 extras/hs-test/hst_suite.go create mode 100644 extras/hs-test/infra/address_allocator.go create mode 100644 extras/hs-test/infra/container.go create mode 100644 extras/hs-test/infra/cpu.go create mode 100644 extras/hs-test/infra/hst_suite.go create mode 100644 extras/hs-test/infra/netconfig.go create mode 100644 extras/hs-test/infra/suite_nginx.go create mode 100644 extras/hs-test/infra/suite_no_topo.go create mode 100644 extras/hs-test/infra/suite_ns.go create mode 100644 extras/hs-test/infra/suite_tap.go create mode 100644 extras/hs-test/infra/suite_veth.go create mode 100644 extras/hs-test/infra/topo.go create mode 100644 extras/hs-test/infra/utils.go create mode 100644 extras/hs-test/infra/vppinstance.go delete mode 100644 extras/hs-test/netconfig.go delete mode 100644 extras/hs-test/suite_nginx_test.go delete mode 100644 extras/hs-test/suite_no_topo_test.go delete mode 100644 extras/hs-test/suite_ns_test.go delete mode 100644 extras/hs-test/suite_tap_test.go delete mode 100644 extras/hs-test/suite_veth_test.go delete mode 100644 extras/hs-test/topo.go delete mode 100644 extras/hs-test/utils.go delete mode 100644 extras/hs-test/vppinstance.go diff --git a/extras/hs-test/Makefile b/extras/hs-test/Makefile index 7fab6dca514..07423e527ed 100644 --- a/extras/hs-test/Makefile +++ b/extras/hs-test/Makefile @@ -53,28 +53,25 @@ ifeq ($(ARCH),) ARCH=$(shell dpkg --print-architecture) endif -list_tests = @go run github.com/onsi/ginkgo/v2/ginkgo --dry-run -v --no-color --seed=2 | head -n -1 | grep 'Test' | \ - sed 's/^/* /; s/\(Suite\) /\1\//g' - .PHONY: help help: @echo "Make targets:" - @echo " test - run tests" - @echo " test-debug - run tests (vpp debug image)" - @echo " build - build test infra" - @echo " build-cov - coverage build of VPP and Docker images" - @echo " build-debug - build test infra (vpp debug image)" - @echo " build-go - just build golang files" - @echo " checkstyle-go - check style of .go source files" - @echo " fixstyle-go - format .go source files" - @echo " cleanup-hst - stops and removes all docker contaiers and namespaces" - @echo " list-tests - list all tests" + @echo " test - run tests" + @echo " test-debug - run tests (vpp debug image)" + @echo " build - build test infra" + @echo " build-cov - coverage build of VPP and Docker images" + @echo " build-debug - build test infra (vpp debug image)" + @echo " build-go - just build golang files" + @echo " checkstyle-go - check style of .go source files" + @echo " fixstyle-go - format .go source files" + @echo " cleanup-hst - stops and removes all docker contaiers and namespaces" + @echo " list-tests - list all tests" @echo - @echo "make build arguments:" + @echo "'make build' arguments:" @echo " UBUNTU_VERSION - ubuntu version for docker image" - @echo " HST_EXTENDED_TESTS - build extended tests" + @echo " HST_EXTENDED_TESTS - build extended tests" @echo - @echo "make test arguments:" + @echo "'make test' arguments:" @echo " PERSIST=[true|false] - whether clean up topology and dockers after test" @echo " VERBOSE=[true|false] - verbose output" @echo " UNCONFIGURE=[true|false] - unconfigure selected test" @@ -86,11 +83,12 @@ help: @echo " REPEAT=[n] - repeat tests up to N times or until a failure occurs" @echo @echo "List of all tests:" - $(call list_tests) + @$(MAKE) list-tests .PHONY: list-tests list-tests: - $(call list_tests) + @go run github.com/onsi/ginkgo/v2/ginkgo --dry-run -v --no-color --seed=2 | head -n -1 | grep 'Test' | \ + sed 's/^/* /; s/\(Suite\) /\1\//g' .PHONY: build-vpp-release build-vpp-release: diff --git a/extras/hs-test/README.rst b/extras/hs-test/README.rst index a88251af617..7841211e3ab 100644 --- a/extras/hs-test/README.rst +++ b/extras/hs-test/README.rst @@ -26,15 +26,15 @@ Anatomy of a test case **Action flow when running a test case**: -#. It starts with running ``make test``. Optional arguments are VERBOSE, PERSIST (topology configuration isn't cleaned up after test run), +#. It starts with running ``make test``. Optional arguments are VERBOSE, PERSIST (topology configuration isn't cleaned up after test run, use ``make cleanup-hst`` to clean up), TEST= to run a specific test and PARALLEL=[n-cpus]. -#. ``make list-tests`` (or ``make help``) shows all tests. The current `list of tests`_ is at the bottom of this document. -#. ``Ginkgo`` looks for a spec suite in the current directory and then compiles it to a .test binary -#. The Ginkgo test framework runs each function that was registered manually using ``registerMySuiteTest(s *MySuite)``. Each of these functions correspond to a suite +#. ``make list-tests`` (or ``make help``) shows all tests. +#. ``Ginkgo`` looks for a spec suite in the current directory and then compiles it to a .test binary. +#. The Ginkgo test framework runs each function that was registered manually using ``Register[SuiteName]Test()``. Each of these functions correspond to a suite. #. Ginkgo's ``RunSpecs(t, "Suite description")`` function is the entry point and does the following: #. Ginkgo compiles the spec, builds a spec tree - #. ``Describe`` container nodes in suite\_\*_test.go files are run (in series by default, or in parallel with the argument PARALLEL=[n-cpus]) + #. ``Describe`` container nodes in suite\_\*.go files are run (in series by default, or in parallel with the argument PARALLEL=[n-cpus]) #. Suite is initialized. The topology is loaded and configured in this step #. Registered tests are run in generated ``It`` subject nodes #. Execute tear-down functions, which currently consists of stopping running containers @@ -47,46 +47,47 @@ This describes adding a new test case to an existing suite. For adding a new suite, please see `Modifying the framework`_ below. #. To write a new test case, create a file whose name ends with ``_test.go`` or pick one that already exists -#. Declare method whose name ends with ``Test`` and specifies its parameter as a pointer to the suite's struct (defined in ``suite_*_test.go``) +#. Declare method whose name ends with ``Test`` and specifies its parameter as a pointer to the suite's struct (defined in ``infra/suite_*.go``) #. Implement test behaviour inside the test method. This typically includes the following: - #. Retrieve a running container in which to run some action. Method ``getContainerByName`` + #. Import ``. "fd.io/hs-test/infra"`` + #. Retrieve a running container in which to run some action. Method ``GetContainerByName`` from ``HstSuite`` struct serves this purpose - #. Interact with VPP through the ``VppInstance`` struct embedded in container. It provides ``vppctl`` method to access debug CLI - #. Run arbitrary commands inside the containers with ``exec`` method - #. Run other external tool with one of the preexisting functions in the ``utils.go`` file. - For example, use ``wget`` with ``startWget`` function + #. Interact with VPP through the ``VppInstance`` struct embedded in container. It provides ``Vppctl`` method to access debug CLI + #. Run arbitrary commands inside the containers with ``Exec`` method + #. Run other external tool with one of the preexisting functions in the ``infra/utils.go`` file. + For example, use ``wget`` with ``StartWget`` function #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else - #. Verify results of your tests using ``assert`` methods provided by the test suite, implemented by HstSuite struct or use ``Gomega`` assert functions. + #. Verify results of your tests using ``Assert`` methods provided by the test suite. -#. Create an ``init()`` function and register the test using ``register*SuiteTests(testCaseFunction)`` +#. Create an ``init()`` function and register the test using ``Register[SuiteName]Tests(testCaseFunction)`` **Example test case** Assumed are two docker containers, each with its own VPP instance running. One VPP then pings the other. -This can be put in file ``extras/hs-test/my_test.go`` and run with command ``make test TEST=MyTest`` or ``ginkgo -v --trace --focus MyTest``. +This can be put in file ``extras/hs-test/my_test.go`` and run with command ``make test TEST=MyTest``. :: package main import ( - "fmt" + . "fd.io/hs-test/infra" ) func init(){ - registerMySuiteTest(MyTest) + RegisterMySuiteTest(MyTest) } func MyTest(s *MySuite) { - clientVpp := s.getContainerByName("client-vpp").vppInstance + clientVpp := s.GetContainerByName("client-vpp").VppInstance - serverVethAddress := s.netInterfaces["server-iface"].AddressString() + serverVethAddress := s.NetInterfaces["server-iface"].Ip4AddressString() - result := clientVpp.vppctl("ping " + serverVethAddress) - s.assertNotNil(result) - s.log(result) + result := clientVpp.Vppctl("ping " + serverVethAddress) + s.Log(result) + s.AssertNotNil(result) } @@ -94,23 +95,28 @@ Filtering test cases -------------------- The framework allows us to filter test cases in a few different ways, using ``make test TEST=``: -* Suite name -* File name -* Test name -* All of the above as long as they are ordered properly, e.g. ``make test TEST=VethsSuite.http_test.go.HeaderServerTest`` + + * Suite name + * File name + * Test name + * All of the above as long as they are ordered properly, e.g. ``make test TEST=VethsSuite.http_test.go.HeaderServerTest`` **Names are case sensitive!** Names don't have to be complete, as long as they are last: This is valid and will run all tests in every ``http`` file (if there is more than one): -``make test TEST=VethsSuite.http`` + +* ``make test TEST=VethsSuite.http`` + This is not valid: -``make test TEST=Veths.http`` + +* ``make test TEST=Veths.http`` They can also be left out: -``make test TEST=http_test.go`` will run every test in ``http_test.go`` -``make test TEST=Nginx`` will run everything that has 'Nginx' in its name - suites, files and tests. -``make test TEST=HeaderServerTest`` will only run the header server test + +* ``make test TEST=http_test.go`` will run every test in ``http_test.go`` +* ``make test TEST=Nginx`` will run everything that has 'Nginx' in its name - suites, files and tests. +* ``make test TEST=HeaderServerTest`` will only run the header server test Modifying the framework @@ -120,34 +126,37 @@ Modifying the framework .. _test-convention: -#. To add a new suite, create a new file. Naming convention for the suite files is ``suite_name_test.go`` where *name* will be replaced - by the actual name +#. To add a new suite, create a new file in the ``infra/`` folder. Naming convention for the suite files is ``suite_[name].go``. #. Make a ``struct``, in the suite file, with at least ``HstSuite`` struct as its member. HstSuite provides functionality that can be shared for all suites, like starting containers +#. Create a new map that will contain a file name where a test is located and test functions with a pointer to the suite's struct: ``var myTests = map[string][]func(s *MySuite){}`` + :: + var myTests = map[string][]func(s *MySuite){} + type MySuite struct { HstSuite } -#. Create a new slice that will contain test functions with a pointer to the suite's struct: ``var myTests = []func(s *MySuite){}`` -#. Then create a new function that will append test functions to that slice: +#. Then create a new function that will add tests to that map: :: - func registerMySuiteTests(tests ...func(s *MySuite)) { - nginxTests = append(myTests, tests...) + func RegisterMyTests(tests ...func(s *MySuite)) { + myTests[getTestFilename()] = tests } + #. In suite file, implement ``SetupSuite`` method which Ginkgo runs once before starting any of the tests. - It's important here to call ``configureNetworkTopology`` method, + It's important here to call ``ConfigureNetworkTopology()`` method, pass the topology name to the function in a form of file name of one of the *yaml* files in ``topo-network`` folder. Without the extension. In this example, *myTopology* corresponds to file ``extras/hs-test/topo-network/myTopology.yaml`` This will ensure network topology, such as network interfaces and namespaces, will be created. - Another important method to call is ``loadContainerTopology()`` which will load + Another important method to call is ``LoadContainerTopology()`` which will load containers and shared volumes used by the suite. This time the name passed to method corresponds to file in ``extras/hs-test/topo-containers`` folder @@ -158,8 +167,8 @@ Modifying the framework // Add custom setup code here - s.configureNetworkTopology("myTopology") - s.loadContainerTopology("2peerVeth") + s.ConfigureNetworkTopology("myTopology") + s.LoadContainerTopology("2peerVeth") } #. In suite file, implement ``SetupTest`` method which gets executed before each test. Starting containers and @@ -184,44 +193,50 @@ Modifying the framework :: var _ = Describe("MySuite", Ordered, ContinueOnFailure, func() { - var s MySuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() + var s MySuite + BeforeAll(func() { + s.SetupSuite() }) - for _, test := range mySuiteTests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) { - test(&s) - }, SpecTimeout(time.Minute*5)) - } + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range myTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } }) #. Notice the loop - it will generate multiple ``It`` nodes, each running a different test. ``test := test`` is necessary, otherwise only the last test in a suite will run. For a more detailed description, check Ginkgo's documentation: https://onsi.github.io/ginkgo/#dynamically-generating-specs\. -#. ``funcValue.Name()`` returns the full name of a function (e.g. ``fd.io/hs-test.MyTest``), however, we only need the test name (``MyTest``). +#. ``testName`` contains the test name in the following format: ``[name]_test.go/MyTest``. -#. To run certain tests solo, create a new slice that will only contain tests that have to run solo and a new register function. +#. To run certain tests solo, create a register function and a map that will only contain tests that have to run solo. Add a ``Serial`` decorator to the container node and ``Label("SOLO")`` to the ``It`` subject node: :: var _ = Describe("MySuiteSolo", Ordered, ContinueOnFailure, Serial, func() { ... - It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) { - test(&s) + It(testName, Label("SOLO"), func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) }, SpecTimeout(time.Minute*5)) }) @@ -308,36 +323,3 @@ or a new version incompatibility issue occurs. .. _ginkgo: https://onsi.github.io/ginkgo/ .. _volumes: https://docs.docker.com/storage/volumes/ - -**List of tests** - -.. _list of tests: - -Please update this list whenever you add a new test by pasting the output below. - -* NsSuite/HttpTpsTest -* NsSuite/VppProxyHttpTcpTest -* NsSuite/VppProxyHttpTlsTest -* NsSuite/EnvoyProxyHttpTcpTest -* NginxSuite/MirroringTest -* VethsSuiteSolo TcpWithLossTest [SOLO] -* NoTopoSuiteSolo HttpStaticPromTest [SOLO] -* TapSuite/LinuxIperfTest -* NoTopoSuite/NginxHttp3Test -* NoTopoSuite/NginxAsServerTest -* NoTopoSuite/NginxPerfCpsTest -* NoTopoSuite/NginxPerfRpsTest -* NoTopoSuite/NginxPerfWrkTest -* VethsSuite/EchoBuiltinTest -* VethsSuite/HttpCliTest -* VethsSuite/LDPreloadIperfVppTest -* VethsSuite/VppEchoQuicTest -* VethsSuite/VppEchoTcpTest -* VethsSuite/VppEchoUdpTest -* VethsSuite/XEchoVclClientUdpTest -* VethsSuite/XEchoVclClientTcpTest -* VethsSuite/XEchoVclServerUdpTest -* VethsSuite/XEchoVclServerTcpTest -* VethsSuite/VclEchoTcpTest -* VethsSuite/VclEchoUdpTest -* VethsSuite/VclRetryAttachTest diff --git a/extras/hs-test/address_allocator.go b/extras/hs-test/address_allocator.go deleted file mode 100644 index e05ea76b9bb..00000000000 --- a/extras/hs-test/address_allocator.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/edwarnicke/exechelper" -) - -type AddressCounter = int - -type Ip4AddressAllocator struct { - networks map[int]AddressCounter - chosenOctet int - assignedIps []string -} - -func (a *Ip4AddressAllocator) AddNetwork(networkNumber int) { - a.networks[networkNumber] = 1 -} - -func (a *Ip4AddressAllocator) NewIp4InterfaceAddress(inputNetworkNumber ...int) (string, error) { - var networkNumber int = 0 - if len(inputNetworkNumber) > 0 { - networkNumber = inputNetworkNumber[0] - } - - if _, ok := a.networks[networkNumber]; !ok { - a.AddNetwork(networkNumber) - } - - numberOfAddresses := a.networks[networkNumber] - - if numberOfAddresses == 254 { - return "", fmt.Errorf("no available IPv4 addresses") - } - - address, err := a.createIpAddress(networkNumber, numberOfAddresses) - - a.networks[networkNumber] = numberOfAddresses + 1 - - return address + "/24", err -} - -// Creates a file every time an IP is assigned: used to keep track of addresses in use. -// If an address is not in use, 'counter' is then copied to 'chosenOctet' and it is used for the remaining tests. -// Also checks host IP addresses. -func (a *Ip4AddressAllocator) createIpAddress(networkNumber int, numberOfAddresses int) (string, error) { - hostIps, _ := exechelper.CombinedOutput("ip a") - counter := 10 - var address string - - for { - if a.chosenOctet != 0 { - address = fmt.Sprintf("10.%v.%v.%v", a.chosenOctet, networkNumber, numberOfAddresses) - file, err := os.Create(address) - if err != nil { - return "", errors.New("unable to create file: " + fmt.Sprint(err)) - } - file.Close() - break - } else { - address = fmt.Sprintf("10.%v.%v.%v", counter, networkNumber, numberOfAddresses) - _, err := os.Stat(address) - if err == nil || strings.Contains(string(hostIps), address) { - counter++ - } else if os.IsNotExist(err) { - file, err := os.Create(address) - if err != nil { - return "", errors.New("unable to create file: " + fmt.Sprint(err)) - } - file.Close() - a.chosenOctet = counter - break - } else { - return "", errors.New("an error occurred while checking if a file exists: " + fmt.Sprint(err)) - } - } - } - - a.assignedIps = append(a.assignedIps, address) - return address, nil -} - -func (a *Ip4AddressAllocator) deleteIpAddresses() { - for ip := range a.assignedIps { - os.Remove(a.assignedIps[ip]) - } -} - -func NewIp4AddressAllocator() *Ip4AddressAllocator { - var ip4AddrAllocator = new(Ip4AddressAllocator) - ip4AddrAllocator.networks = make(map[int]AddressCounter) - ip4AddrAllocator.AddNetwork(0) - return ip4AddrAllocator -} diff --git a/extras/hs-test/container.go b/extras/hs-test/container.go deleted file mode 100644 index 97b71b5385f..00000000000 --- a/extras/hs-test/container.go +++ /dev/null @@ -1,380 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/exec" - "strings" - "text/template" - "time" - - "github.com/edwarnicke/exechelper" - . "github.com/onsi/ginkgo/v2" -) - -const ( - logDir string = "/tmp/hs-test/" - volumeDir string = "/volumes" -) - -var ( - workDir, _ = os.Getwd() -) - -type Volume struct { - hostDir string - containerDir string - isDefaultWorkDir bool -} - -type Container struct { - suite *HstSuite - isOptional bool - runDetached bool - name string - image string - extraRunningArgs string - volumes map[string]Volume - envVars map[string]string - vppInstance *VppInstance - allocatedCpus []int -} - -func newContainer(suite *HstSuite, yamlInput ContainerConfig) (*Container, error) { - containerName := yamlInput["name"].(string) - if len(containerName) == 0 { - err := fmt.Errorf("container name must not be blank") - return nil, err - } - - var container = new(Container) - container.volumes = make(map[string]Volume) - container.envVars = make(map[string]string) - container.name = containerName - container.suite = suite - - if image, ok := yamlInput["image"]; ok { - container.image = image.(string) - } else { - container.image = "hs-test/vpp" - } - - if args, ok := yamlInput["extra-args"]; ok { - container.extraRunningArgs = args.(string) - } else { - container.extraRunningArgs = "" - } - - if isOptional, ok := yamlInput["is-optional"]; ok { - container.isOptional = isOptional.(bool) - } else { - container.isOptional = false - } - - if runDetached, ok := yamlInput["run-detached"]; ok { - container.runDetached = runDetached.(bool) - } else { - container.runDetached = true - } - - if _, ok := yamlInput["volumes"]; ok { - workingVolumeDir := logDir + suite.getCurrentTestName() + volumeDir - workDirReplacer := strings.NewReplacer("$HST_DIR", workDir) - volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir) - for _, volu := range yamlInput["volumes"].([]interface{}) { - volumeMap := volu.(ContainerConfig) - hostDir := workDirReplacer.Replace(volumeMap["host-dir"].(string)) - hostDir = volDirReplacer.Replace(hostDir) - containerDir := volumeMap["container-dir"].(string) - isDefaultWorkDir := false - - if isDefault, ok := volumeMap["is-default-work-dir"]; ok { - isDefaultWorkDir = isDefault.(bool) - } - container.addVolume(hostDir, containerDir, isDefaultWorkDir) - } - } - - if _, ok := yamlInput["vars"]; ok { - for _, envVar := range yamlInput["vars"].([]interface{}) { - envVarMap := envVar.(ContainerConfig) - name := envVarMap["name"].(string) - value := envVarMap["value"].(string) - container.addEnvVar(name, value) - } - } - return container, nil -} - -func (c *Container) getWorkDirVolume() (res Volume, exists bool) { - for _, v := range c.volumes { - if v.isDefaultWorkDir { - res = v - exists = true - return - } - } - return -} - -func (c *Container) getHostWorkDir() (res string) { - if v, ok := c.getWorkDirVolume(); ok { - res = v.hostDir - } - return -} - -func (c *Container) getContainerWorkDir() (res string) { - if v, ok := c.getWorkDirVolume(); ok { - res = v.containerDir - } - return -} - -func (c *Container) getContainerArguments() string { - args := "--ulimit nofile=90000:90000 --cap-add=all --privileged --network host" - c.allocateCpus() - args += fmt.Sprintf(" --cpuset-cpus=\"%d-%d\"", c.allocatedCpus[0], c.allocatedCpus[len(c.allocatedCpus)-1]) - args += c.getVolumesAsCliOption() - args += c.getEnvVarsAsCliOption() - if *vppSourceFileDir != "" { - args += fmt.Sprintf(" -v %s:%s", *vppSourceFileDir, *vppSourceFileDir) - } - args += " --name " + c.name + " " + c.image - args += " " + c.extraRunningArgs - return args -} - -func (c *Container) runWithRetry(cmd string) error { - nTries := 5 - for i := 0; i < nTries; i++ { - err := exechelper.Run(cmd) - if err == nil { - return nil - } - time.Sleep(1 * time.Second) - } - return fmt.Errorf("failed to run container command") -} - -func (c *Container) create() error { - cmd := "docker create " + c.getContainerArguments() - c.suite.log(cmd) - return exechelper.Run(cmd) -} - -func (c *Container) allocateCpus() { - c.suite.startedContainers = append(c.suite.startedContainers, c) - c.allocatedCpus = c.suite.AllocateCpus() - c.suite.log("Allocated CPUs " + fmt.Sprint(c.allocatedCpus) + " to container " + c.name) -} - -func (c *Container) start() error { - cmd := "docker start " + c.name - c.suite.log(cmd) - return c.runWithRetry(cmd) -} - -func (c *Container) prepareCommand() (string, error) { - if c.name == "" { - return "", fmt.Errorf("run container failed: name is blank") - } - - cmd := "docker run " - if c.runDetached { - cmd += " -d" - } - - cmd += " " + c.getContainerArguments() - - c.suite.log(cmd) - return cmd, nil -} - -func (c *Container) combinedOutput() (string, error) { - cmd, err := c.prepareCommand() - if err != nil { - return "", err - } - - byteOutput, err := exechelper.CombinedOutput(cmd) - return string(byteOutput), err -} - -func (c *Container) run() error { - cmd, err := c.prepareCommand() - if err != nil { - return err - } - return c.runWithRetry(cmd) -} - -func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) { - var volume Volume - volume.hostDir = hostDir - volume.containerDir = containerDir - volume.isDefaultWorkDir = isDefaultWorkDir - c.volumes[hostDir] = volume -} - -func (c *Container) getVolumesAsCliOption() string { - cliOption := "" - - if len(c.volumes) > 0 { - for _, volume := range c.volumes { - cliOption += fmt.Sprintf(" -v %s:%s", volume.hostDir, volume.containerDir) - } - } - - return cliOption -} - -func (c *Container) addEnvVar(name string, value string) { - c.envVars[name] = value -} - -func (c *Container) getEnvVarsAsCliOption() string { - cliOption := "" - if len(c.envVars) == 0 { - return cliOption - } - - for name, value := range c.envVars { - cliOption += fmt.Sprintf(" -e %s=%s", name, value) - } - return cliOption -} - -func (c *Container) newVppInstance(cpus []int, additionalConfigs ...Stanza) (*VppInstance, error) { - vpp := new(VppInstance) - vpp.container = c - vpp.cpus = cpus - vpp.additionalConfig = append(vpp.additionalConfig, additionalConfigs...) - c.vppInstance = vpp - return vpp, nil -} - -func (c *Container) copy(sourceFileName string, targetFileName string) error { - cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName) - return cmd.Run() -} - -func (c *Container) createFile(destFileName string, content string) error { - f, err := os.CreateTemp("/tmp", "hst-config"+c.suite.ppid) - if err != nil { - return err - } - defer os.Remove(f.Name()) - - if _, err := f.Write([]byte(content)); err != nil { - return err - } - if err := f.Close(); err != nil { - return err - } - c.copy(f.Name(), destFileName) - return nil -} - -/* - * Executes in detached mode so that the started application can continue to run - * without blocking execution of test - */ -func (c *Container) execServer(command string, arguments ...any) { - serverCommand := fmt.Sprintf(command, arguments...) - containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() + - " " + c.name + " " + serverCommand - GinkgoHelper() - c.suite.log(containerExecCommand) - c.suite.assertNil(exechelper.Run(containerExecCommand)) -} - -func (c *Container) exec(command string, arguments ...any) string { - cliCommand := fmt.Sprintf(command, arguments...) - containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() + - " " + c.name + " " + cliCommand - GinkgoHelper() - c.suite.log(containerExecCommand) - byteOutput, err := exechelper.CombinedOutput(containerExecCommand) - c.suite.assertNil(err, fmt.Sprint(err)) - return string(byteOutput) -} - -func (c *Container) getLogDirPath() string { - testId := c.suite.getTestId() - testName := c.suite.getCurrentTestName() - logDirPath := logDir + testName + "/" + testId + "/" - - cmd := exec.Command("mkdir", "-p", logDirPath) - if err := cmd.Run(); err != nil { - Fail("mkdir error: " + fmt.Sprint(err)) - } - - return logDirPath -} - -func (c *Container) saveLogs() { - testLogFilePath := c.getLogDirPath() + "container-" + c.name + ".log" - - cmd := exec.Command("docker", "logs", "--details", "-t", c.name) - c.suite.log(cmd) - output, err := cmd.CombinedOutput() - if err != nil { - c.suite.log(err) - } - - f, err := os.Create(testLogFilePath) - if err != nil { - Fail("file create error: " + fmt.Sprint(err)) - } - fmt.Fprint(f, string(output)) - f.Close() -} - -// Outputs logs from docker containers. Set 'maxLines' to 0 to output the full log. -func (c *Container) log(maxLines int) (string, error) { - var cmd string - if maxLines == 0 { - cmd = "docker logs " + c.name - } else { - cmd = fmt.Sprintf("docker logs --tail %d %s", maxLines, c.name) - } - - c.suite.log(cmd) - o, err := exechelper.CombinedOutput(cmd) - return string(o), err -} - -func (c *Container) stop() error { - if c.vppInstance != nil && c.vppInstance.apiStream != nil { - c.vppInstance.saveLogs() - c.vppInstance.disconnect() - } - c.vppInstance = nil - c.saveLogs() - c.suite.log("docker stop " + c.name + " -t 0") - return exechelper.Run("docker stop " + c.name + " -t 0") -} - -func (c *Container) createConfig(targetConfigName string, templateName string, values any) { - template := template.Must(template.ParseFiles(templateName)) - - f, err := os.CreateTemp(logDir, "hst-config") - c.suite.assertNil(err, err) - defer os.Remove(f.Name()) - - err = template.Execute(f, values) - c.suite.assertNil(err, err) - - err = f.Close() - c.suite.assertNil(err, err) - - c.copy(f.Name(), targetConfigName) -} - -func init() { - cmd := exec.Command("mkdir", "-p", logDir) - if err := cmd.Run(); err != nil { - panic(err) - } -} diff --git a/extras/hs-test/cpu.go b/extras/hs-test/cpu.go deleted file mode 100644 index 49a7dfb02f8..00000000000 --- a/extras/hs-test/cpu.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "bufio" - "errors" - "fmt" - . "github.com/onsi/ginkgo/v2" - "os" - "os/exec" - "strings" -) - -var CgroupPath = "/sys/fs/cgroup/" - -type CpuContext struct { - cpuAllocator *CpuAllocatorT - cpus []int -} - -type CpuAllocatorT struct { - cpus []int -} - -var cpuAllocator *CpuAllocatorT = nil - -func (c *CpuAllocatorT) Allocate(containerCount int, nCpus int) (*CpuContext, error) { - var cpuCtx CpuContext - - // splitting cpus into equal parts; this will over-allocate cores but it's good enough for now - maxContainerCount := 4 - // skip CPU 0 - minCpu := ((GinkgoParallelProcess() - 1) * maxContainerCount * nCpus) + 1 - maxCpu := (GinkgoParallelProcess() * maxContainerCount * nCpus) - - if len(c.cpus)-1 < maxCpu { - err := fmt.Errorf("could not allocate %d CPUs; available: %d; attempted to allocate cores %d-%d", - nCpus*containerCount, len(c.cpus)-1, minCpu, maxCpu) - return nil, err - } - if containerCount == 1 { - cpuCtx.cpus = c.cpus[minCpu : minCpu+nCpus] - } else if containerCount > 1 && containerCount <= maxContainerCount { - cpuCtx.cpus = c.cpus[minCpu+(nCpus*(containerCount-1)) : minCpu+(nCpus*containerCount)] - } else { - return nil, fmt.Errorf("too many containers; CPU allocation for >%d containers is not implemented", maxContainerCount) - } - - cpuCtx.cpuAllocator = c - return &cpuCtx, nil -} - -func (c *CpuAllocatorT) readCpus() error { - var first, last int - - // Path depends on cgroup version. We need to check which version is in use. - // For that following command can be used: 'stat -fc %T /sys/fs/cgroup/' - // In case the output states 'cgroup2fs' then cgroups v2 is used, 'tmpfs' in case cgroups v1. - cmd := exec.Command("stat", "-fc", "%T", "/sys/fs/cgroup/") - byteOutput, err := cmd.CombinedOutput() - if err != nil { - return err - } - CpuPath := CgroupPath - if strings.Contains(string(byteOutput), "tmpfs") { - CpuPath += "cpuset/cpuset.effective_cpus" - } else if strings.Contains(string(byteOutput), "cgroup2fs") { - CpuPath += "cpuset.cpus.effective" - } else { - return errors.New("cgroup unknown fs: " + string(byteOutput)) - } - - file, err := os.Open(CpuPath) - if err != nil { - return err - } - defer file.Close() - - sc := bufio.NewScanner(file) - sc.Scan() - line := sc.Text() - _, err = fmt.Sscanf(line, "%d-%d", &first, &last) - if err != nil { - return err - } - for i := first; i <= last; i++ { - c.cpus = append(c.cpus, i) - } - return nil -} - -func CpuAllocator() (*CpuAllocatorT, error) { - if cpuAllocator == nil { - cpuAllocator = new(CpuAllocatorT) - err := cpuAllocator.readCpus() - if err != nil { - return nil, err - } - } - return cpuAllocator, nil -} diff --git a/extras/hs-test/echo_test.go b/extras/hs-test/echo_test.go index ce852bea3e0..6b4739a5457 100644 --- a/extras/hs-test/echo_test.go +++ b/extras/hs-test/echo_test.go @@ -1,51 +1,56 @@ package main +import ( + . "fd.io/hs-test/infra" +) + func init() { - registerVethTests(EchoBuiltinTest) - registerSoloVethTests(TcpWithLossTest) + RegisterVethTests(EchoBuiltinTest) + RegisterSoloVethTests(TcpWithLossTest) } func EchoBuiltinTest(s *VethsSuite) { - serverVpp := s.getContainerByName("server-vpp").vppInstance - serverVeth := s.getInterfaceByName(serverInterfaceName) + serverVpp := s.GetContainerByName("server-vpp").VppInstance + serverVeth := s.GetInterfaceByName(ServerInterfaceName) - serverVpp.vppctl("test echo server " + - " uri tcp://" + serverVeth.ip4AddressString() + "/1234") + serverVpp.Vppctl("test echo server " + + " uri tcp://" + serverVeth.Ip4AddressString() + "/1234") - clientVpp := s.getContainerByName("client-vpp").vppInstance + clientVpp := s.GetContainerByName("client-vpp").VppInstance - o := clientVpp.vppctl("test echo client nclients 100 bytes 1 verbose" + + o := clientVpp.Vppctl("test echo client nclients 100 bytes 1 verbose" + " syn-timeout 100 test-timeout 100" + - " uri tcp://" + serverVeth.ip4AddressString() + "/1234") - s.log(o) - s.assertNotContains(o, "failed:") + " uri tcp://" + serverVeth.Ip4AddressString() + "/1234") + s.Log(o) + s.AssertNotContains(o, "failed:") } // unstable with multiple workers func TcpWithLossTest(s *VethsSuite) { s.SkipIfMultiWorker() - serverVpp := s.getContainerByName("server-vpp").vppInstance + serverVpp := s.GetContainerByName("server-vpp").VppInstance - serverVeth := s.getInterfaceByName(serverInterfaceName) - serverVpp.vppctl("test echo server uri tcp://%s/20022", - serverVeth.ip4AddressString()) + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + serverVpp.Vppctl("test echo server uri tcp://%s/20022", + serverVeth.Ip4AddressString()) - clientVpp := s.getContainerByName("client-vpp").vppInstance + clientVpp := s.GetContainerByName("client-vpp").VppInstance // Ensure that VPP doesn't abort itself with NSIM enabled // Warning: Removing this ping will make VPP crash! - clientVpp.vppctl("ping %s", serverVeth.ip4AddressString()) + clientVpp.Vppctl("ping %s", serverVeth.Ip4AddressString()) // Add loss of packets with Network Delay Simulator - clientVpp.vppctl("set nsim poll-main-thread delay 0.01 ms bandwidth 40 gbit" + + clientVpp.Vppctl("set nsim poll-main-thread delay 0.01 ms bandwidth 40 gbit" + " packet-size 1400 packets-per-drop 1000") - clientVpp.vppctl("nsim output-feature enable-disable host-" + s.getInterfaceByName(clientInterfaceName).name) + name := s.GetInterfaceByName(ClientInterfaceName).Name() + clientVpp.Vppctl("nsim output-feature enable-disable host-" + name) // Do echo test from client-vpp container - output := clientVpp.vppctl("test echo client uri tcp://%s/20022 verbose echo-bytes mbytes 50", - serverVeth.ip4AddressString()) - s.log(output) - s.assertNotEqual(len(output), 0) - s.assertNotContains(output, "failed", output) + output := clientVpp.Vppctl("test echo client uri tcp://%s/20022 verbose echo-bytes mbytes 50", + serverVeth.Ip4AddressString()) + s.Log(output) + s.AssertNotEqual(len(output), 0) + s.AssertNotContains(output, "failed", output) } diff --git a/extras/hs-test/framework_test.go b/extras/hs-test/framework_test.go index b992659a4af..a086f75a5fc 100644 --- a/extras/hs-test/framework_test.go +++ b/extras/hs-test/framework_test.go @@ -8,23 +8,22 @@ import ( "testing" "time" + . "fd.io/hs-test/infra" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -var suiteTimeout time.Duration - func getTestFilename() string { _, filename, _, _ := runtime.Caller(2) return filepath.Base(filename) } func TestHst(t *testing.T) { - if *isVppDebug { + if *IsVppDebug { // 30 minute timeout so that the framework won't timeout while debugging - suiteTimeout = time.Minute * 30 + SuiteTimeout = time.Minute * 30 } else { - suiteTimeout = time.Minute * 5 + SuiteTimeout = time.Minute * 5 } // creates a file with PPID, used for 'make cleanup-hst' diff --git a/extras/hs-test/hst_suite.go b/extras/hs-test/hst_suite.go deleted file mode 100644 index 0bdaad28ef5..00000000000 --- a/extras/hs-test/hst_suite.go +++ /dev/null @@ -1,535 +0,0 @@ -package main - -import ( - "bufio" - "errors" - "flag" - "fmt" - "github.com/onsi/gomega/gmeasure" - "gopkg.in/yaml.v3" - "io" - "log" - "os" - "os/exec" - "strings" - "time" - - "github.com/edwarnicke/exechelper" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -const ( - DEFAULT_NETWORK_NUM int = 1 -) - -var isPersistent = flag.Bool("persist", false, "persists topology config") -var isVerbose = flag.Bool("verbose", false, "verbose test output") -var isUnconfiguring = flag.Bool("unconfigure", false, "remove topology") -var isVppDebug = flag.Bool("debug", false, "attach gdb to vpp") -var nConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp") -var vppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory") - -type HstSuite struct { - containers map[string]*Container - startedContainers []*Container - volumes []string - netConfigs []NetConfig - netInterfaces map[string]*NetInterface - ip4AddrAllocator *Ip4AddressAllocator - testIds map[string]string - cpuAllocator *CpuAllocatorT - cpuContexts []*CpuContext - cpuPerVpp int - ppid string - processIndex string - logger *log.Logger - logFile *os.File -} - -func (s *HstSuite) SetupSuite() { - s.createLogger() - s.log("Suite Setup") - RegisterFailHandler(func(message string, callerSkip ...int) { - s.hstFail() - Fail(message, callerSkip...) - }) - var err error - s.ppid = fmt.Sprint(os.Getppid()) - // remove last number so we have space to prepend a process index (interfaces have a char limit) - s.ppid = s.ppid[:len(s.ppid)-1] - s.processIndex = fmt.Sprint(GinkgoParallelProcess()) - s.cpuAllocator, err = CpuAllocator() - if err != nil { - Fail("failed to init cpu allocator: " + fmt.Sprint(err)) - } - s.cpuPerVpp = *nConfiguredCpus -} - -func (s *HstSuite) AllocateCpus() []int { - cpuCtx, err := s.cpuAllocator.Allocate(len(s.startedContainers), s.cpuPerVpp) - s.assertNil(err) - s.AddCpuContext(cpuCtx) - return cpuCtx.cpus -} - -func (s *HstSuite) AddCpuContext(cpuCtx *CpuContext) { - s.cpuContexts = append(s.cpuContexts, cpuCtx) -} - -func (s *HstSuite) TearDownSuite() { - defer s.logFile.Close() - s.log("Suite Teardown") - s.unconfigureNetworkTopology() -} - -func (s *HstSuite) TearDownTest() { - s.log("Test Teardown") - if *isPersistent { - return - } - s.resetContainers() - s.removeVolumes() - s.ip4AddrAllocator.deleteIpAddresses() -} - -func (s *HstSuite) skipIfUnconfiguring() { - if *isUnconfiguring { - s.skip("skipping to unconfigure") - } -} - -func (s *HstSuite) SetupTest() { - s.log("Test Setup") - s.startedContainers = s.startedContainers[:0] - s.skipIfUnconfiguring() - s.setupVolumes() - s.setupContainers() -} - -func (s *HstSuite) setupVolumes() { - for _, volume := range s.volumes { - cmd := "docker volume create --name=" + volume - s.log(cmd) - exechelper.Run(cmd) - } -} - -func (s *HstSuite) setupContainers() { - for _, container := range s.containers { - if !container.isOptional { - container.run() - } - } -} - -func (s *HstSuite) logVppInstance(container *Container, maxLines int) { - if container.vppInstance == nil { - return - } - - logSource := container.getHostWorkDir() + defaultLogFilePath - file, err := os.Open(logSource) - - if err != nil { - return - } - defer file.Close() - - scanner := bufio.NewScanner(file) - var lines []string - var counter int - - for scanner.Scan() { - lines = append(lines, scanner.Text()) - counter++ - if counter > maxLines { - lines = lines[1:] - counter-- - } - } - - s.log("vvvvvvvvvvvvvvv " + container.name + " [VPP instance]:") - for _, line := range lines { - s.log(line) - } - s.log("^^^^^^^^^^^^^^^\n\n") -} - -func (s *HstSuite) hstFail() { - for _, container := range s.startedContainers { - out, err := container.log(20) - if err != nil { - s.log("An error occured while obtaining '" + container.name + "' container logs: " + fmt.Sprint(err)) - s.log("The container might not be running - check logs in " + container.getLogDirPath()) - continue - } - s.log("\nvvvvvvvvvvvvvvv " + - container.name + ":\n" + - out + - "^^^^^^^^^^^^^^^\n\n") - s.logVppInstance(container, 20) - } -} - -func (s *HstSuite) assertNil(object interface{}, msgAndArgs ...interface{}) { - Expect(object).To(BeNil(), msgAndArgs...) -} - -func (s *HstSuite) assertNotNil(object interface{}, msgAndArgs ...interface{}) { - Expect(object).ToNot(BeNil(), msgAndArgs...) -} - -func (s *HstSuite) assertEqual(expected, actual interface{}, msgAndArgs ...interface{}) { - Expect(actual).To(Equal(expected), msgAndArgs...) -} - -func (s *HstSuite) assertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) { - Expect(actual).ToNot(Equal(expected), msgAndArgs...) -} - -func (s *HstSuite) assertContains(testString, contains interface{}, msgAndArgs ...interface{}) { - Expect(testString).To(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...) -} - -func (s *HstSuite) assertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) { - Expect(testString).ToNot(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...) -} - -func (s *HstSuite) assertNotEmpty(object interface{}, msgAndArgs ...interface{}) { - Expect(object).ToNot(BeEmpty(), msgAndArgs...) -} - -func (s *HstSuite) createLogger() { - suiteName := s.getCurrentSuiteName() - var err error - s.logFile, err = os.Create("summary/" + suiteName + ".log") - if err != nil { - Fail("Unable to create log file.") - } - s.logger = log.New(io.Writer(s.logFile), "", log.LstdFlags) -} - -// Logs to files by default, logs to stdout when VERBOSE=true with GinkgoWriter -// to keep console tidy -func (s *HstSuite) log(arg any) { - logs := strings.Split(fmt.Sprint(arg), "\n") - for _, line := range logs { - s.logger.Println(line) - } - if *isVerbose { - GinkgoWriter.Println(arg) - } -} - -func (s *HstSuite) skip(args string) { - Skip(args) -} - -func (s *HstSuite) SkipIfMultiWorker(args ...any) { - if *nConfiguredCpus > 1 { - s.skip("test case not supported with multiple vpp workers") - } -} - -func (s *HstSuite) SkipUnlessExtendedTestsBuilt() { - imageName := "hs-test/nginx-http3" - - cmd := exec.Command("docker", "images", imageName) - byteOutput, err := cmd.CombinedOutput() - if err != nil { - s.log("error while searching for docker image") - return - } - if !strings.Contains(string(byteOutput), imageName) { - s.skip("extended tests not built") - } -} - -func (s *HstSuite) resetContainers() { - for _, container := range s.startedContainers { - container.stop() - exechelper.Run("docker rm " + container.name) - } -} - -func (s *HstSuite) removeVolumes() { - for _, volumeName := range s.volumes { - cmd := "docker volume rm " + volumeName - exechelper.Run(cmd) - os.RemoveAll(volumeName) - } -} - -func (s *HstSuite) getNetNamespaceByName(name string) string { - return s.processIndex + name + s.ppid -} - -func (s *HstSuite) getInterfaceByName(name string) *NetInterface { - return s.netInterfaces[s.processIndex+name+s.ppid] -} - -func (s *HstSuite) getContainerByName(name string) *Container { - return s.containers[s.processIndex+name+s.ppid] -} - -/* - * Create a copy and return its address, so that individial tests which call this - * are not able to modify the original container and affect other tests by doing that - */ -func (s *HstSuite) getTransientContainerByName(name string) *Container { - containerCopy := *s.containers[s.processIndex+name+s.ppid] - return &containerCopy -} - -func (s *HstSuite) loadContainerTopology(topologyName string) { - data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml") - if err != nil { - Fail("read error: " + fmt.Sprint(err)) - } - var yamlTopo YamlTopology - err = yaml.Unmarshal(data, &yamlTopo) - if err != nil { - Fail("unmarshal error: " + fmt.Sprint(err)) - } - - for _, elem := range yamlTopo.Volumes { - volumeMap := elem["volume"].(VolumeConfig) - hostDir := volumeMap["host-dir"].(string) - workingVolumeDir := logDir + s.getCurrentTestName() + volumeDir - volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir) - hostDir = volDirReplacer.Replace(hostDir) - s.volumes = append(s.volumes, hostDir) - } - - s.containers = make(map[string]*Container) - for _, elem := range yamlTopo.Containers { - newContainer, err := newContainer(s, elem) - newContainer.suite = s - newContainer.name = newContainer.suite.processIndex + newContainer.name + newContainer.suite.ppid - if err != nil { - Fail("container config error: " + fmt.Sprint(err)) - } - s.containers[newContainer.name] = newContainer - } -} - -func (s *HstSuite) loadNetworkTopology(topologyName string) { - data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml") - if err != nil { - Fail("read error: " + fmt.Sprint(err)) - } - var yamlTopo YamlTopology - err = yaml.Unmarshal(data, &yamlTopo) - if err != nil { - Fail("unmarshal error: " + fmt.Sprint(err)) - } - - s.ip4AddrAllocator = NewIp4AddressAllocator() - s.netInterfaces = make(map[string]*NetInterface) - - for _, elem := range yamlTopo.Devices { - if _, ok := elem["name"]; ok { - elem["name"] = s.processIndex + elem["name"].(string) + s.ppid - } - - if peer, ok := elem["peer"].(NetDevConfig); ok { - if peer["name"].(string) != "" { - peer["name"] = s.processIndex + peer["name"].(string) + s.ppid - } - if _, ok := peer["netns"]; ok { - peer["netns"] = s.processIndex + peer["netns"].(string) + s.ppid - } - } - - if _, ok := elem["netns"]; ok { - elem["netns"] = s.processIndex + elem["netns"].(string) + s.ppid - } - - if _, ok := elem["interfaces"]; ok { - interfaceCount := len(elem["interfaces"].([]interface{})) - for i := 0; i < interfaceCount; i++ { - elem["interfaces"].([]interface{})[i] = s.processIndex + elem["interfaces"].([]interface{})[i].(string) + s.ppid - } - } - - switch elem["type"].(string) { - case NetNs: - { - if namespace, err := newNetNamespace(elem); err == nil { - s.netConfigs = append(s.netConfigs, &namespace) - } else { - Fail("network config error: " + fmt.Sprint(err)) - } - } - case Veth, Tap: - { - if netIf, err := newNetworkInterface(elem, s.ip4AddrAllocator); err == nil { - s.netConfigs = append(s.netConfigs, netIf) - s.netInterfaces[netIf.Name()] = netIf - } else { - Fail("network config error: " + fmt.Sprint(err)) - } - } - case Bridge: - { - if bridge, err := newBridge(elem); err == nil { - s.netConfigs = append(s.netConfigs, &bridge) - } else { - Fail("network config error: " + fmt.Sprint(err)) - } - } - } - } -} - -func (s *HstSuite) configureNetworkTopology(topologyName string) { - s.loadNetworkTopology(topologyName) - - if *isUnconfiguring { - return - } - - for _, nc := range s.netConfigs { - s.log(nc.Name()) - if err := nc.configure(); err != nil { - Fail("Network config error: " + fmt.Sprint(err)) - } - } -} - -func (s *HstSuite) unconfigureNetworkTopology() { - if *isPersistent { - return - } - for _, nc := range s.netConfigs { - nc.unconfigure() - } -} - -func (s *HstSuite) getTestId() string { - testName := s.getCurrentTestName() - - if s.testIds == nil { - s.testIds = map[string]string{} - } - - if _, ok := s.testIds[testName]; !ok { - s.testIds[testName] = time.Now().Format("2006-01-02_15-04-05") - } - - return s.testIds[testName] -} - -func (s *HstSuite) getCurrentTestName() string { - return strings.Split(CurrentSpecReport().LeafNodeText, "/")[1] -} - -func (s *HstSuite) getCurrentSuiteName() string { - return CurrentSpecReport().ContainerHierarchyTexts[0] -} - -// Returns last 3 digits of PID + Ginkgo process index as the 4th digit -func (s *HstSuite) getPortFromPpid() string { - port := s.ppid - for len(port) < 3 { - port += "0" - } - return port[len(port)-3:] + s.processIndex -} - -func (s *HstSuite) startServerApp(running chan error, done chan struct{}, env []string) { - cmd := exec.Command("iperf3", "-4", "-s", "-p", s.getPortFromPpid()) - if env != nil { - cmd.Env = env - } - s.log(cmd) - err := cmd.Start() - if err != nil { - msg := fmt.Errorf("failed to start iperf server: %v", err) - running <- msg - return - } - running <- nil - <-done - cmd.Process.Kill() -} - -func (s *HstSuite) startClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) { - defer func() { - clnCh <- nil - }() - - nTries := 0 - - for { - cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g", "-p", s.getPortFromPpid()) - if env != nil { - cmd.Env = env - } - s.log(cmd) - o, err := cmd.CombinedOutput() - if err != nil { - if nTries > 5 { - clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o) - return - } - time.Sleep(1 * time.Second) - nTries++ - continue - } else { - clnRes <- fmt.Sprintf("Client output: %s", o) - } - break - } -} - -func (s *HstSuite) startHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) { - cmd := newCommand([]string{"./http_server", addressPort, s.ppid, s.processIndex}, netNs) - err := cmd.Start() - s.log(cmd) - if err != nil { - s.log("Failed to start http server: " + fmt.Sprint(err)) - return - } - running <- struct{}{} - <-done - cmd.Process.Kill() -} - -func (s *HstSuite) startWget(finished chan error, server_ip, port, query, netNs string) { - defer func() { - finished <- errors.New("wget error") - }() - - cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query}, - netNs) - s.log(cmd) - o, err := cmd.CombinedOutput() - if err != nil { - finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o) - return - } else if !strings.Contains(string(o), "200 OK") { - finished <- fmt.Errorf("wget error: response not 200 OK") - return - } - finished <- nil -} - -/* -runBenchmark creates Gomega's experiment with the passed-in name and samples the passed-in callback repeatedly (samplesNum times), -passing in suite context, experiment and your data. - -You can also instruct runBenchmark to run with multiple concurrent workers. -You can record multiple named measurements (float64 or duration) within passed-in callback. -runBenchmark then produces report to show statistical distribution of measurements. -*/ -func (s *HstSuite) runBenchmark(name string, samplesNum, parallelNum int, callback func(s *HstSuite, e *gmeasure.Experiment, data interface{}), data interface{}) { - experiment := gmeasure.NewExperiment(name) - - experiment.Sample(func(idx int) { - defer GinkgoRecover() - callback(s, experiment, data) - }, gmeasure.SamplingConfig{N: samplesNum, NumParallel: parallelNum}) - AddReportEntry(experiment.Name, experiment) -} diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go index ca8b618a7b2..d83f2d10ffb 100644 --- a/extras/hs-test/http_test.go +++ b/extras/hs-test/http_test.go @@ -10,12 +10,13 @@ import ( "strings" "time" + . "fd.io/hs-test/infra" . "github.com/onsi/ginkgo/v2" ) func init() { - registerVethTests(HttpCliTest, HttpCliConnectErrorTest) - registerNoTopoTests(NginxHttp3Test, NginxAsServerTest, + RegisterVethTests(HttpCliTest, HttpCliConnectErrorTest) + RegisterNoTopoTests(NginxHttp3Test, NginxAsServerTest, NginxPerfCpsTest, NginxPerfRpsTest, NginxPerfWrkTest, HeaderServerTest, HttpStaticMovedTest, HttpStaticNotFoundTest, HttpCliMethodNotAllowedTest, HttpCliBadRequestTest, HttpStaticBuildInUrlGetIfStatsTest, HttpStaticBuildInUrlPostIfStatsTest, @@ -24,561 +25,561 @@ func init() { HttpStaticMacTimeTest, HttpStaticBuildInUrlGetVersionVerboseTest, HttpVersionNotSupportedTest, HttpInvalidContentLengthTest, HttpInvalidTargetSyntaxTest, HttpStaticPathTraversalTest, HttpUriDecodeTest, HttpHeadersTest) - registerNoTopoSoloTests(HttpStaticPromTest, HttpTpsTest) + RegisterNoTopoSoloTests(HttpStaticPromTest, HttpTpsTest) } const wwwRootPath = "/tmp/www_root" func httpDownloadBenchmark(s *HstSuite, experiment *gmeasure.Experiment, data interface{}) { url, isValid := data.(string) - s.assertEqual(true, isValid) - client := newHttpClient() + s.AssertEqual(true, isValid) + client := NewHttpClient() req, err := http.NewRequest("GET", url, nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) t := time.Now() resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) _, err = io.ReadAll(resp.Body) duration := time.Since(t) experiment.RecordValue("Download Speed", (float64(resp.ContentLength)/1024/1024)/duration.Seconds(), gmeasure.Units("MB/s"), gmeasure.Precision(2)) } func HttpTpsTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() url := "http://" + serverAddress + ":8080/test_file_10M" - vpp.vppctl("http tps uri tcp://0.0.0.0/8080") + vpp.Vppctl("http tps uri tcp://0.0.0.0/8080") - s.runBenchmark("HTTP tps 10M", 10, 0, httpDownloadBenchmark, url) + s.RunBenchmark("HTTP tps 10M", 10, 0, httpDownloadBenchmark, url) } func HttpCliTest(s *VethsSuite) { - serverContainer := s.getContainerByName("server-vpp") - clientContainer := s.getContainerByName("client-vpp") + serverContainer := s.GetContainerByName("server-vpp") + clientContainer := s.GetContainerByName("client-vpp") - serverVeth := s.getInterfaceByName(serverInterfaceName) + serverVeth := s.GetInterfaceByName(ServerInterfaceName) - serverContainer.vppInstance.vppctl("http cli server") + serverContainer.VppInstance.Vppctl("http cli server") - uri := "http://" + serverVeth.ip4AddressString() + "/80" + uri := "http://" + serverVeth.Ip4AddressString() + "/80" - o := clientContainer.vppInstance.vppctl("http cli client" + + o := clientContainer.VppInstance.Vppctl("http cli client" + " uri " + uri + " query /show/vlib/graph") - s.log(o) - s.assertContains(o, "", " not found in the result!") + s.Log(o) + s.AssertContains(o, "", " not found in the result!") } func HttpCliConnectErrorTest(s *VethsSuite) { - clientContainer := s.getContainerByName("client-vpp") + clientContainer := s.GetContainerByName("client-vpp") - serverVeth := s.getInterfaceByName(serverInterfaceName) + serverVeth := s.GetInterfaceByName(ServerInterfaceName) - uri := "http://" + serverVeth.ip4AddressString() + "/80" + uri := "http://" + serverVeth.Ip4AddressString() + "/80" - o := clientContainer.vppInstance.vppctl("http cli client" + + o := clientContainer.VppInstance.Vppctl("http cli client" + " uri " + uri + " query /show/vlib/graph") - s.log(o) - s.assertContains(o, "failed to connect") + s.Log(o) + s.AssertContains(o, "failed to connect") } func NginxHttp3Test(s *NoTopoSuite) { s.SkipUnlessExtendedTestsBuilt() query := "index.html" - nginxCont := s.getContainerByName("nginx-http3") - s.assertNil(nginxCont.run()) + nginxCont := s.GetContainerByName("nginx-http3") + s.AssertNil(nginxCont.Run()) - vpp := s.getContainerByName("vpp").vppInstance - vpp.waitForApp("nginx-", 5) - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + vpp := s.GetContainerByName("vpp").VppInstance + vpp.WaitForApp("nginx-", 5) + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() defer func() { os.Remove(query) }() - curlCont := s.getContainerByName("curl") + curlCont := s.GetContainerByName("curl") args := fmt.Sprintf("curl --noproxy '*' --local-port 55444 --http3-only -k https://%s:8443/%s", serverAddress, query) - curlCont.extraRunningArgs = args - o, err := curlCont.combinedOutput() - s.log(o) - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(o, "", " not found in the result!") + curlCont.ExtraRunningArgs = args + o, err := curlCont.CombinedOutput() + s.Log(o) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(o, "", " not found in the result!") } func HttpStaticPromTest(s *NoTopoSuite) { finished := make(chan error, 1) query := "stats.prom" - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers")) - s.log(vpp.vppctl("prom enable")) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers")) + s.Log(vpp.Vppctl("prom enable")) time.Sleep(time.Second * 5) go func() { defer GinkgoRecover() - s.startWget(finished, serverAddress, "80", query, "") + s.StartWget(finished, serverAddress, "80", query, "") }() err := <-finished - s.assertNil(err) + s.AssertNil(err) } func HttpStaticPathTraversalTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - vpp.container.exec("mkdir -p " + wwwRootPath) - vpp.container.exec("mkdir -p " + "/tmp/secret_folder") - vpp.container.createFile("/tmp/secret_folder/secret_file.txt", "secret") - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) - - client := newHttpClient() + vpp := s.GetContainerByName("vpp").VppInstance + vpp.Container.Exec("mkdir -p " + wwwRootPath) + vpp.Container.Exec("mkdir -p " + "/tmp/secret_folder") + vpp.Container.CreateFile("/tmp/secret_folder/secret_file.txt", "secret") + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) + + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/../secret_folder/secret_file.txt", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(404, resp.StatusCode) + s.AssertEqual(404, resp.StatusCode) } func HttpStaticMovedTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - vpp.container.exec("mkdir -p " + wwwRootPath + "/tmp.aaa") - vpp.container.createFile(wwwRootPath+"/tmp.aaa/index.html", "

Hello

") - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) + vpp := s.GetContainerByName("vpp").VppInstance + vpp.Container.Exec("mkdir -p " + wwwRootPath + "/tmp.aaa") + vpp.Container.CreateFile(wwwRootPath+"/tmp.aaa/index.html", "

Hello

") + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/tmp.aaa", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(301, resp.StatusCode) - s.assertNotEqual("", resp.Header.Get("Location")) + s.AssertEqual(301, resp.StatusCode) + s.AssertNotEqual("", resp.Header.Get("Location")) } func HttpStaticNotFoundTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - vpp.container.exec("mkdir -p " + wwwRootPath) - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) + vpp := s.GetContainerByName("vpp").VppInstance + vpp.Container.Exec("mkdir -p " + wwwRootPath) + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug")) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/notfound.html", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(404, resp.StatusCode) + s.AssertEqual(404, resp.StatusCode) } func HttpCliMethodNotAllowedTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("POST", "http://"+serverAddress+":80/test", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(405, resp.StatusCode) + s.AssertEqual(405, resp.StatusCode) // TODO: need to be fixed in http code - //s.assertNotEqual("", resp.Header.Get("Allow")) + //s.AssertNotEqual("", resp.Header.Get("Allow")) } func HttpCliBadRequestTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(400, resp.StatusCode) + s.AssertEqual(400, resp.StatusCode) } func HttpStaticBuildInUrlGetVersionTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/version.json", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(string(data), "vpp_details") - s.assertContains(string(data), "version") - s.assertContains(string(data), "build_date") - s.assertNotContains(string(data), "build_by") - s.assertNotContains(string(data), "build_host") - s.assertNotContains(string(data), "build_dir") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(string(data), "vpp_details") + s.AssertContains(string(data), "version") + s.AssertContains(string(data), "build_date") + s.AssertNotContains(string(data), "build_by") + s.AssertNotContains(string(data), "build_host") + s.AssertNotContains(string(data), "build_dir") } func HttpStaticBuildInUrlGetVersionVerboseTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/version.json?verbose=true", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(string(data), "vpp_details") - s.assertContains(string(data), "version") - s.assertContains(string(data), "build_date") - s.assertContains(string(data), "build_by") - s.assertContains(string(data), "build_host") - s.assertContains(string(data), "build_dir") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(string(data), "vpp_details") + s.AssertContains(string(data), "version") + s.AssertContains(string(data), "build_date") + s.AssertContains(string(data), "build_by") + s.AssertContains(string(data), "build_host") + s.AssertContains(string(data), "build_dir") } func HttpStaticBuildInUrlGetIfListTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/interface_list.json", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(string(data), "interface_list") - s.assertContains(string(data), s.getInterfaceByName(tapInterfaceName).peer.Name()) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(string(data), "interface_list") + s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).Peer.Name()) } func HttpStaticBuildInUrlGetIfStatsTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/interface_stats.json", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(string(data), "interface_stats") - s.assertContains(string(data), "local0") - s.assertContains(string(data), s.getInterfaceByName(tapInterfaceName).peer.Name()) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(string(data), "interface_stats") + s.AssertContains(string(data), "local0") + s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).Peer.Name()) } func validatePostInterfaceStats(s *NoTopoSuite, data string) { - s.assertContains(data, "interface_stats") - s.assertContains(data, s.getInterfaceByName(tapInterfaceName).peer.Name()) - s.assertNotContains(data, "error") - s.assertNotContains(data, "local0") + s.AssertContains(data, "interface_stats") + s.AssertContains(data, s.GetInterfaceByName(TapInterfaceName).Peer.Name()) + s.AssertNotContains(data, "error") + s.AssertNotContains(data, "local0") } func HttpStaticBuildInUrlPostIfStatsTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - body := []byte(s.getInterfaceByName(tapInterfaceName).peer.Name()) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + body := []byte(s.GetInterfaceByName(TapInterfaceName).Peer.Name()) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("POST", "http://"+serverAddress+":80/interface_stats.json", bytes.NewBuffer(body)) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) validatePostInterfaceStats(s, string(data)) } func HttpStaticMacTimeTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - s.log(vpp.vppctl("mactime enable-disable " + s.getInterfaceByName(tapInterfaceName).peer.Name())) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + s.Log(vpp.Vppctl("mactime enable-disable " + s.GetInterfaceByName(TapInterfaceName).Peer.Name())) - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/mactime.json", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(string(data), "mactime") - s.assertContains(string(data), s.getInterfaceByName(tapInterfaceName).ip4AddressString()) - s.assertContains(string(data), s.getInterfaceByName(tapInterfaceName).hwAddress.String()) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(string(data), "mactime") + s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).Ip4AddressString()) + s.AssertContains(string(data), s.GetInterfaceByName(TapInterfaceName).HwAddress.String()) } func HttpInvalidRequestLineTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - resp, err := tcpSendReceive(serverAddress+":80", "GET / HTTP/1.1") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "invalid framing not allowed") + resp, err := TcpSendReceive(serverAddress+":80", "GET / HTTP/1.1") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "invalid framing not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET / HTTP/1.1\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "invalid framing not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET / HTTP/1.1\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "invalid framing not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET /\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "HTTP-version must be present") + resp, err = TcpSendReceive(serverAddress+":80", "GET /\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "HTTP-version must be present") - resp, err = tcpSendReceive(serverAddress+":80", "GET HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "request-target must be present") + resp, err = TcpSendReceive(serverAddress+":80", "GET HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "request-target must be present") - resp, err = tcpSendReceive(serverAddress+":80", "GET HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "request-target must be present") + resp, err = TcpSendReceive(serverAddress+":80", "GET HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "request-target must be present") - resp, err = tcpSendReceive(serverAddress+":80", "GET / HTTP/x\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "'HTTP/x' invalid http version not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET / HTTP/x\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "'HTTP/x' invalid http version not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET / HTTP1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "'HTTP1.1' invalid http version not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET / HTTP1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "'HTTP1.1' invalid http version not allowed") } func HttpInvalidTargetSyntaxTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - resp, err := tcpSendReceive(serverAddress+":80", "GET /interface|stats.json HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "'|' not allowed in target path") + resp, err := TcpSendReceive(serverAddress+":80", "GET /interface|stats.json HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "'|' not allowed in target path") - resp, err = tcpSendReceive(serverAddress+":80", "GET /interface#stats.json HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "'#' not allowed in target path") + resp, err = TcpSendReceive(serverAddress+":80", "GET /interface#stats.json HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "'#' not allowed in target path") - resp, err = tcpSendReceive(serverAddress+":80", "GET /interface%stats.json HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /interface%stats.json HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "after '%' there must be two hex-digit characters in target path") - resp, err = tcpSendReceive(serverAddress+":80", "GET /interface%1stats.json HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /interface%1stats.json HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "after '%' there must be two hex-digit characters in target path") - resp, err = tcpSendReceive(serverAddress+":80", "GET /interface%Bstats.json HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /interface%Bstats.json HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "after '%' there must be two hex-digit characters in target path") - resp, err = tcpSendReceive(serverAddress+":80", "GET /interface%stats.json%B HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /interface%stats.json%B HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "after '%' there must be two hex-digit characters in target path") - resp, err = tcpSendReceive(serverAddress+":80", "GET /version.json?verbose>true HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "'>' not allowed in target query") + resp, err = TcpSendReceive(serverAddress+":80", "GET /version.json?verbose>true HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "'>' not allowed in target query") - resp, err = tcpSendReceive(serverAddress+":80", "GET /version.json?verbose%true HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /version.json?verbose%true HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "after '%' there must be two hex-digit characters in target query") - resp, err = tcpSendReceive(serverAddress+":80", "GET /version.json?verbose=%1 HTTP/1.1\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /version.json?verbose=%1 HTTP/1.1\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "after '%' there must be two hex-digit characters in target query") } func HttpInvalidContentLengthTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - resp, err := tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nContent-Length:\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "Content-Length value must be present") + resp, err := TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nContent-Length:\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "Content-Length value must be present") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nContent-Length: \r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "Content-Length value must be present") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nContent-Length: \r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "Content-Length value must be present") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nContent-Length: a\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nContent-Length: a\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "Content-Length value other than digit not allowed") } func HttpContentLengthTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) - ifName := s.getInterfaceByName(tapInterfaceName).peer.Name() + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + s.Log(vpp.Vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers debug")) + ifName := s.GetInterfaceByName(TapInterfaceName).Peer.Name() - resp, err := tcpSendReceive(serverAddress+":80", + resp, err := TcpSendReceive(serverAddress+":80", "POST /interface_stats.json HTTP/1.1\r\nContent-Length:4\r\n\r\n"+ifName) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) validatePostInterfaceStats(s, resp) - resp, err = tcpSendReceive(serverAddress+":80", + resp, err = TcpSendReceive(serverAddress+":80", "POST /interface_stats.json HTTP/1.1\r\n Content-Length: 4 \r\n\r\n"+ifName) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) validatePostInterfaceStats(s, resp) - resp, err = tcpSendReceive(serverAddress+":80", + resp, err = TcpSendReceive(serverAddress+":80", "POST /interface_stats.json HTTP/1.1\r\n\tContent-Length:\t\t4\r\n\r\n"+ifName) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) validatePostInterfaceStats(s, resp) } func HttpMethodNotImplementedTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("OPTIONS", "http://"+serverAddress+":80/show/version", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(501, resp.StatusCode) + s.AssertEqual(501, resp.StatusCode) } func HttpVersionNotSupportedTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - resp, err := tcpSendReceive(serverAddress+":80", "GET / HTTP/2\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 505 HTTP Version Not Supported") + resp, err := TcpSendReceive(serverAddress+":80", "GET / HTTP/2\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 505 HTTP Version Not Supported") } func HttpUriDecodeTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/sh%6fw%20versio%6E%20verbose", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual(200, resp.StatusCode) + s.AssertEqual(200, resp.StatusCode) data, err := io.ReadAll(resp.Body) - s.assertNil(err, fmt.Sprint(err)) - s.log(string(data)) - s.assertNotContains(string(data), "unknown input") - s.assertContains(string(data), "Compiler") + s.AssertNil(err, fmt.Sprint(err)) + s.Log(string(data)) + s.AssertNotContains(string(data), "unknown input") + s.AssertContains(string(data), "Compiler") } func HttpHeadersTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - resp, err := tcpSendReceive( + resp, err := TcpSendReceive( serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent:test\r\nAccept:text/xml\r\nAccept:\ttext/plain\t \r\nAccept:text/html\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 200 OK") - s.assertContains(resp, "Content-Type: text / plain") - s.assertNotContains(resp, "", "html content received instead of plain text") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 200 OK") + s.AssertContains(resp, "Content-Type: text / plain") + s.AssertNotContains(resp, "", "html content received instead of plain text") } func HttpInvalidHeadersTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - resp, err := tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nUser-Agent: test\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "Header section must end with CRLF CRLF") + resp, err := TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nUser-Agent: test\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "Header section must end with CRLF CRLF") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser@Agent:test\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "'@' not allowed in field name") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser@Agent:test\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "'@' not allowed in field name") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "incomplete field line not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "incomplete field line not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\n: test\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "empty field name not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\n: test\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "empty field name not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\rUser-Agent:test\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "invalid field line end not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\rUser-Agent:test\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "invalid field line end not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\nUser-Agent:test\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "invalid field line end not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\nUser-Agent:test\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "invalid field line end not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent:\r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "empty field value not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent:\r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "empty field value not allowed") - resp, err = tcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent: \r\n\r\n") - s.assertNil(err, fmt.Sprint(err)) - s.assertContains(resp, "HTTP/1.1 400 Bad Request", "empty field value not allowed") + resp, err = TcpSendReceive(serverAddress+":80", "GET /show/version HTTP/1.1\r\nHost:"+serverAddress+":80\r\nUser-Agent: \r\n\r\n") + s.AssertNil(err, fmt.Sprint(err)) + s.AssertContains(resp, "HTTP/1.1 400 Bad Request", "empty field value not allowed") } func HeaderServerTest(s *NoTopoSuite) { - vpp := s.getContainerByName("vpp").vppInstance - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() - vpp.vppctl("http cli server") + vpp := s.GetContainerByName("vpp").VppInstance + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() + vpp.Vppctl("http cli server") - client := newHttpClient() + client := NewHttpClient() req, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) resp, err := client.Do(req) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) defer resp.Body.Close() - s.assertEqual("http_cli_server", resp.Header.Get("Server")) + s.AssertEqual("http_cli_server", resp.Header.Get("Server")) } func NginxAsServerTest(s *NoTopoSuite) { query := "return_ok" finished := make(chan error, 1) - nginxCont := s.getContainerByName("nginx") - s.assertNil(nginxCont.run()) + nginxCont := s.GetContainerByName("nginx") + s.AssertNil(nginxCont.Run()) - vpp := s.getContainerByName("vpp").vppInstance - vpp.waitForApp("nginx-", 5) + vpp := s.GetContainerByName("vpp").VppInstance + vpp.WaitForApp("nginx-", 5) - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() defer func() { os.Remove(query) }() go func() { defer GinkgoRecover() - s.startWget(finished, serverAddress, "80", query, "") + s.StartWget(finished, serverAddress, "80", query, "") }() - s.assertNil(<-finished) + s.AssertNil(<-finished) } func parseString(s, pattern string) string { @@ -595,16 +596,16 @@ func runNginxPerf(s *NoTopoSuite, mode, ab_or_wrk string) error { nRequests := 1000000 nClients := 1000 - serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + serverAddress := s.GetInterfaceByName(TapInterfaceName).Peer.Ip4AddressString() - vpp := s.getContainerByName("vpp").vppInstance + vpp := s.GetContainerByName("vpp").VppInstance - nginxCont := s.getContainerByName(singleTopoContainerNginx) - s.assertNil(nginxCont.run()) - vpp.waitForApp("nginx-", 5) + nginxCont := s.GetContainerByName(SingleTopoContainerNginx) + s.AssertNil(nginxCont.Run()) + vpp.WaitForApp("nginx-", 5) if ab_or_wrk == "ab" { - abCont := s.getContainerByName("ab") + abCont := s.GetContainerByName("ab") args := fmt.Sprintf("-n %d -c %d", nRequests, nClients) if mode == "rps" { args += " -k" @@ -614,22 +615,22 @@ func runNginxPerf(s *NoTopoSuite, mode, ab_or_wrk string) error { // don't exit on socket receive errors args += " -r" args += " http://" + serverAddress + ":80/64B.json" - abCont.extraRunningArgs = args - o, err := abCont.combinedOutput() + abCont.ExtraRunningArgs = args + o, err := abCont.CombinedOutput() rps := parseString(o, "Requests per second:") - s.log(rps) - s.log(err) - s.assertNil(err, "err: '%s', output: '%s'", err, o) + s.Log(rps) + s.Log(err) + s.AssertNil(err, "err: '%s', output: '%s'", err, o) } else { - wrkCont := s.getContainerByName("wrk") + wrkCont := s.GetContainerByName("wrk") args := fmt.Sprintf("-c %d -t 2 -d 30 http://%s:80/64B.json", nClients, serverAddress) - wrkCont.extraRunningArgs = args - o, err := wrkCont.combinedOutput() + wrkCont.ExtraRunningArgs = args + o, err := wrkCont.CombinedOutput() rps := parseString(o, "requests") - s.log(rps) - s.log(err) - s.assertNil(err, "err: '%s', output: '%s'", err, o) + s.Log(rps) + s.Log(err) + s.AssertNil(err, "err: '%s', output: '%s'", err, o) } return nil } @@ -637,13 +638,13 @@ func runNginxPerf(s *NoTopoSuite, mode, ab_or_wrk string) error { // unstable with multiple workers func NginxPerfCpsTest(s *NoTopoSuite) { s.SkipIfMultiWorker() - s.assertNil(runNginxPerf(s, "cps", "ab")) + s.AssertNil(runNginxPerf(s, "cps", "ab")) } func NginxPerfRpsTest(s *NoTopoSuite) { - s.assertNil(runNginxPerf(s, "rps", "ab")) + s.AssertNil(runNginxPerf(s, "rps", "ab")) } func NginxPerfWrkTest(s *NoTopoSuite) { - s.assertNil(runNginxPerf(s, "", "wrk")) + s.AssertNil(runNginxPerf(s, "", "wrk")) } diff --git a/extras/hs-test/infra/address_allocator.go b/extras/hs-test/infra/address_allocator.go new file mode 100644 index 00000000000..cb647024412 --- /dev/null +++ b/extras/hs-test/infra/address_allocator.go @@ -0,0 +1,98 @@ +package hst + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/edwarnicke/exechelper" +) + +type AddressCounter = int + +type Ip4AddressAllocator struct { + networks map[int]AddressCounter + chosenOctet int + assignedIps []string +} + +func (a *Ip4AddressAllocator) AddNetwork(networkNumber int) { + a.networks[networkNumber] = 1 +} + +func (a *Ip4AddressAllocator) NewIp4InterfaceAddress(inputNetworkNumber ...int) (string, error) { + var networkNumber int = 0 + if len(inputNetworkNumber) > 0 { + networkNumber = inputNetworkNumber[0] + } + + if _, ok := a.networks[networkNumber]; !ok { + a.AddNetwork(networkNumber) + } + + numberOfAddresses := a.networks[networkNumber] + + if numberOfAddresses == 254 { + return "", fmt.Errorf("no available IPv4 addresses") + } + + address, err := a.createIpAddress(networkNumber, numberOfAddresses) + + a.networks[networkNumber] = numberOfAddresses + 1 + + return address + "/24", err +} + +// Creates a file every time an IP is assigned: used to keep track of addresses in use. +// If an address is not in use, 'counter' is then copied to 'chosenOctet' and it is used for the remaining tests. +// Also checks host IP addresses. +func (a *Ip4AddressAllocator) createIpAddress(networkNumber int, numberOfAddresses int) (string, error) { + hostIps, _ := exechelper.CombinedOutput("ip a") + counter := 10 + var address string + + for { + if a.chosenOctet != 0 { + address = fmt.Sprintf("10.%v.%v.%v", a.chosenOctet, networkNumber, numberOfAddresses) + file, err := os.Create(address) + if err != nil { + return "", errors.New("unable to create file: " + fmt.Sprint(err)) + } + file.Close() + break + } else { + address = fmt.Sprintf("10.%v.%v.%v", counter, networkNumber, numberOfAddresses) + _, err := os.Stat(address) + if err == nil || strings.Contains(string(hostIps), address) { + counter++ + } else if os.IsNotExist(err) { + file, err := os.Create(address) + if err != nil { + return "", errors.New("unable to create file: " + fmt.Sprint(err)) + } + file.Close() + a.chosenOctet = counter + break + } else { + return "", errors.New("an error occurred while checking if a file exists: " + fmt.Sprint(err)) + } + } + } + + a.assignedIps = append(a.assignedIps, address) + return address, nil +} + +func (a *Ip4AddressAllocator) DeleteIpAddresses() { + for ip := range a.assignedIps { + os.Remove(a.assignedIps[ip]) + } +} + +func NewIp4AddressAllocator() *Ip4AddressAllocator { + var ip4AddrAllocator = new(Ip4AddressAllocator) + ip4AddrAllocator.networks = make(map[int]AddressCounter) + ip4AddrAllocator.AddNetwork(0) + return ip4AddrAllocator +} diff --git a/extras/hs-test/infra/container.go b/extras/hs-test/infra/container.go new file mode 100644 index 00000000000..1dd82809f8a --- /dev/null +++ b/extras/hs-test/infra/container.go @@ -0,0 +1,380 @@ +package hst + +import ( + "fmt" + "os" + "os/exec" + "strings" + "text/template" + "time" + + "github.com/edwarnicke/exechelper" + . "github.com/onsi/ginkgo/v2" +) + +const ( + logDir string = "/tmp/hs-test/" + volumeDir string = "/volumes" +) + +var ( + workDir, _ = os.Getwd() +) + +type Volume struct { + HostDir string + ContainerDir string + IsDefaultWorkDir bool +} + +type Container struct { + Suite *HstSuite + IsOptional bool + RunDetached bool + Name string + Image string + ExtraRunningArgs string + Volumes map[string]Volume + EnvVars map[string]string + VppInstance *VppInstance + AllocatedCpus []int +} + +func newContainer(suite *HstSuite, yamlInput ContainerConfig) (*Container, error) { + containerName := yamlInput["name"].(string) + if len(containerName) == 0 { + err := fmt.Errorf("container name must not be blank") + return nil, err + } + + var container = new(Container) + container.Volumes = make(map[string]Volume) + container.EnvVars = make(map[string]string) + container.Name = containerName + container.Suite = suite + + if Image, ok := yamlInput["image"]; ok { + container.Image = Image.(string) + } else { + container.Image = "hs-test/vpp" + } + + if args, ok := yamlInput["extra-args"]; ok { + container.ExtraRunningArgs = args.(string) + } else { + container.ExtraRunningArgs = "" + } + + if isOptional, ok := yamlInput["is-optional"]; ok { + container.IsOptional = isOptional.(bool) + } else { + container.IsOptional = false + } + + if runDetached, ok := yamlInput["run-detached"]; ok { + container.RunDetached = runDetached.(bool) + } else { + container.RunDetached = true + } + + if _, ok := yamlInput["volumes"]; ok { + workingVolumeDir := logDir + suite.GetCurrentTestName() + volumeDir + workDirReplacer := strings.NewReplacer("$HST_DIR", workDir) + volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir) + for _, volu := range yamlInput["volumes"].([]interface{}) { + volumeMap := volu.(ContainerConfig) + hostDir := workDirReplacer.Replace(volumeMap["host-dir"].(string)) + hostDir = volDirReplacer.Replace(hostDir) + containerDir := volumeMap["container-dir"].(string) + isDefaultWorkDir := false + + if isDefault, ok := volumeMap["is-default-work-dir"]; ok { + isDefaultWorkDir = isDefault.(bool) + } + container.addVolume(hostDir, containerDir, isDefaultWorkDir) + } + } + + if _, ok := yamlInput["vars"]; ok { + for _, envVar := range yamlInput["vars"].([]interface{}) { + envVarMap := envVar.(ContainerConfig) + name := envVarMap["name"].(string) + value := envVarMap["value"].(string) + container.AddEnvVar(name, value) + } + } + return container, nil +} + +func (c *Container) getWorkDirVolume() (res Volume, exists bool) { + for _, v := range c.Volumes { + if v.IsDefaultWorkDir { + res = v + exists = true + return + } + } + return +} + +func (c *Container) GetHostWorkDir() (res string) { + if v, ok := c.getWorkDirVolume(); ok { + res = v.HostDir + } + return +} + +func (c *Container) GetContainerWorkDir() (res string) { + if v, ok := c.getWorkDirVolume(); ok { + res = v.ContainerDir + } + return +} + +func (c *Container) getContainerArguments() string { + args := "--ulimit nofile=90000:90000 --cap-add=all --privileged --network host" + c.allocateCpus() + args += fmt.Sprintf(" --cpuset-cpus=\"%d-%d\"", c.AllocatedCpus[0], c.AllocatedCpus[len(c.AllocatedCpus)-1]) + args += c.getVolumesAsCliOption() + args += c.getEnvVarsAsCliOption() + if *VppSourceFileDir != "" { + args += fmt.Sprintf(" -v %s:%s", *VppSourceFileDir, *VppSourceFileDir) + } + args += " --name " + c.Name + " " + c.Image + args += " " + c.ExtraRunningArgs + return args +} + +func (c *Container) runWithRetry(cmd string) error { + nTries := 5 + for i := 0; i < nTries; i++ { + err := exechelper.Run(cmd) + if err == nil { + return nil + } + time.Sleep(1 * time.Second) + } + return fmt.Errorf("failed to run container command") +} + +func (c *Container) Create() error { + cmd := "docker create " + c.getContainerArguments() + c.Suite.Log(cmd) + return exechelper.Run(cmd) +} + +func (c *Container) allocateCpus() { + c.Suite.StartedContainers = append(c.Suite.StartedContainers, c) + c.AllocatedCpus = c.Suite.AllocateCpus() + c.Suite.Log("Allocated CPUs " + fmt.Sprint(c.AllocatedCpus) + " to container " + c.Name) +} + +func (c *Container) Start() error { + cmd := "docker start " + c.Name + c.Suite.Log(cmd) + return c.runWithRetry(cmd) +} + +func (c *Container) prepareCommand() (string, error) { + if c.Name == "" { + return "", fmt.Errorf("run container failed: name is blank") + } + + cmd := "docker run " + if c.RunDetached { + cmd += " -d" + } + + cmd += " " + c.getContainerArguments() + + c.Suite.Log(cmd) + return cmd, nil +} + +func (c *Container) CombinedOutput() (string, error) { + cmd, err := c.prepareCommand() + if err != nil { + return "", err + } + + byteOutput, err := exechelper.CombinedOutput(cmd) + return string(byteOutput), err +} + +func (c *Container) Run() error { + cmd, err := c.prepareCommand() + if err != nil { + return err + } + return c.runWithRetry(cmd) +} + +func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) { + var volume Volume + volume.HostDir = hostDir + volume.ContainerDir = containerDir + volume.IsDefaultWorkDir = isDefaultWorkDir + c.Volumes[hostDir] = volume +} + +func (c *Container) getVolumesAsCliOption() string { + cliOption := "" + + if len(c.Volumes) > 0 { + for _, volume := range c.Volumes { + cliOption += fmt.Sprintf(" -v %s:%s", volume.HostDir, volume.ContainerDir) + } + } + + return cliOption +} + +func (c *Container) AddEnvVar(name string, value string) { + c.EnvVars[name] = value +} + +func (c *Container) getEnvVarsAsCliOption() string { + cliOption := "" + if len(c.EnvVars) == 0 { + return cliOption + } + + for name, value := range c.EnvVars { + cliOption += fmt.Sprintf(" -e %s=%s", name, value) + } + return cliOption +} + +func (c *Container) newVppInstance(cpus []int, additionalConfigs ...Stanza) (*VppInstance, error) { + vpp := new(VppInstance) + vpp.Container = c + vpp.Cpus = cpus + vpp.AdditionalConfig = append(vpp.AdditionalConfig, additionalConfigs...) + c.VppInstance = vpp + return vpp, nil +} + +func (c *Container) copy(sourceFileName string, targetFileName string) error { + cmd := exec.Command("docker", "cp", sourceFileName, c.Name+":"+targetFileName) + return cmd.Run() +} + +func (c *Container) CreateFile(destFileName string, content string) error { + f, err := os.CreateTemp("/tmp", "hst-config"+c.Suite.Ppid) + if err != nil { + return err + } + defer os.Remove(f.Name()) + + if _, err := f.Write([]byte(content)); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + c.copy(f.Name(), destFileName) + return nil +} + +/* + * Executes in detached mode so that the started application can continue to run + * without blocking execution of test + */ +func (c *Container) ExecServer(command string, arguments ...any) { + serverCommand := fmt.Sprintf(command, arguments...) + containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() + + " " + c.Name + " " + serverCommand + GinkgoHelper() + c.Suite.Log(containerExecCommand) + c.Suite.AssertNil(exechelper.Run(containerExecCommand)) +} + +func (c *Container) Exec(command string, arguments ...any) string { + cliCommand := fmt.Sprintf(command, arguments...) + containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() + + " " + c.Name + " " + cliCommand + GinkgoHelper() + c.Suite.Log(containerExecCommand) + byteOutput, err := exechelper.CombinedOutput(containerExecCommand) + c.Suite.AssertNil(err, fmt.Sprint(err)) + return string(byteOutput) +} + +func (c *Container) getLogDirPath() string { + testId := c.Suite.GetTestId() + testName := c.Suite.GetCurrentTestName() + logDirPath := logDir + testName + "/" + testId + "/" + + cmd := exec.Command("mkdir", "-p", logDirPath) + if err := cmd.Run(); err != nil { + Fail("mkdir error: " + fmt.Sprint(err)) + } + + return logDirPath +} + +func (c *Container) saveLogs() { + testLogFilePath := c.getLogDirPath() + "container-" + c.Name + ".log" + + cmd := exec.Command("docker", "logs", "--details", "-t", c.Name) + c.Suite.Log(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + c.Suite.Log(err) + } + + f, err := os.Create(testLogFilePath) + if err != nil { + Fail("file create error: " + fmt.Sprint(err)) + } + fmt.Fprint(f, string(output)) + f.Close() +} + +// Outputs logs from docker containers. Set 'maxLines' to 0 to output the full log. +func (c *Container) log(maxLines int) (string, error) { + var cmd string + if maxLines == 0 { + cmd = "docker logs " + c.Name + } else { + cmd = fmt.Sprintf("docker logs --tail %d %s", maxLines, c.Name) + } + + c.Suite.Log(cmd) + o, err := exechelper.CombinedOutput(cmd) + return string(o), err +} + +func (c *Container) stop() error { + if c.VppInstance != nil && c.VppInstance.ApiStream != nil { + c.VppInstance.saveLogs() + c.VppInstance.Disconnect() + } + c.VppInstance = nil + c.saveLogs() + c.Suite.Log("docker stop " + c.Name + " -t 0") + return exechelper.Run("docker stop " + c.Name + " -t 0") +} + +func (c *Container) CreateConfig(targetConfigName string, templateName string, values any) { + template := template.Must(template.ParseFiles(templateName)) + + f, err := os.CreateTemp(logDir, "hst-config") + c.Suite.AssertNil(err, err) + defer os.Remove(f.Name()) + + err = template.Execute(f, values) + c.Suite.AssertNil(err, err) + + err = f.Close() + c.Suite.AssertNil(err, err) + + c.copy(f.Name(), targetConfigName) +} + +func init() { + cmd := exec.Command("mkdir", "-p", logDir) + if err := cmd.Run(); err != nil { + panic(err) + } +} diff --git a/extras/hs-test/infra/cpu.go b/extras/hs-test/infra/cpu.go new file mode 100644 index 00000000000..b5555d85b98 --- /dev/null +++ b/extras/hs-test/infra/cpu.go @@ -0,0 +1,100 @@ +package hst + +import ( + "bufio" + "errors" + "fmt" + . "github.com/onsi/ginkgo/v2" + "os" + "os/exec" + "strings" +) + +var CgroupPath = "/sys/fs/cgroup/" + +type CpuContext struct { + cpuAllocator *CpuAllocatorT + cpus []int +} + +type CpuAllocatorT struct { + cpus []int +} + +var cpuAllocator *CpuAllocatorT = nil + +func (c *CpuAllocatorT) Allocate(containerCount int, nCpus int) (*CpuContext, error) { + var cpuCtx CpuContext + + // splitting cpus into equal parts; this will over-allocate cores but it's good enough for now + maxContainerCount := 4 + // skip CPU 0 + minCpu := ((GinkgoParallelProcess() - 1) * maxContainerCount * nCpus) + 1 + maxCpu := (GinkgoParallelProcess() * maxContainerCount * nCpus) + + if len(c.cpus)-1 < maxCpu { + err := fmt.Errorf("could not allocate %d CPUs; available: %d; attempted to allocate cores %d-%d", + nCpus*containerCount, len(c.cpus)-1, minCpu, maxCpu) + return nil, err + } + if containerCount == 1 { + cpuCtx.cpus = c.cpus[minCpu : minCpu+nCpus] + } else if containerCount > 1 && containerCount <= maxContainerCount { + cpuCtx.cpus = c.cpus[minCpu+(nCpus*(containerCount-1)) : minCpu+(nCpus*containerCount)] + } else { + return nil, fmt.Errorf("too many containers; CPU allocation for >%d containers is not implemented", maxContainerCount) + } + + cpuCtx.cpuAllocator = c + return &cpuCtx, nil +} + +func (c *CpuAllocatorT) readCpus() error { + var first, last int + + // Path depends on cgroup version. We need to check which version is in use. + // For that following command can be used: 'stat -fc %T /sys/fs/cgroup/' + // In case the output states 'cgroup2fs' then cgroups v2 is used, 'tmpfs' in case cgroups v1. + cmd := exec.Command("stat", "-fc", "%T", "/sys/fs/cgroup/") + byteOutput, err := cmd.CombinedOutput() + if err != nil { + return err + } + CpuPath := CgroupPath + if strings.Contains(string(byteOutput), "tmpfs") { + CpuPath += "cpuset/cpuset.effective_cpus" + } else if strings.Contains(string(byteOutput), "cgroup2fs") { + CpuPath += "cpuset.cpus.effective" + } else { + return errors.New("cgroup unknown fs: " + string(byteOutput)) + } + + file, err := os.Open(CpuPath) + if err != nil { + return err + } + defer file.Close() + + sc := bufio.NewScanner(file) + sc.Scan() + line := sc.Text() + _, err = fmt.Sscanf(line, "%d-%d", &first, &last) + if err != nil { + return err + } + for i := first; i <= last; i++ { + c.cpus = append(c.cpus, i) + } + return nil +} + +func CpuAllocator() (*CpuAllocatorT, error) { + if cpuAllocator == nil { + cpuAllocator = new(CpuAllocatorT) + err := cpuAllocator.readCpus() + if err != nil { + return nil, err + } + } + return cpuAllocator, nil +} diff --git a/extras/hs-test/infra/hst_suite.go b/extras/hs-test/infra/hst_suite.go new file mode 100644 index 00000000000..46aede7b7ef --- /dev/null +++ b/extras/hs-test/infra/hst_suite.go @@ -0,0 +1,544 @@ +package hst + +import ( + "bufio" + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/onsi/gomega/gmeasure" + "gopkg.in/yaml.v3" + + "github.com/edwarnicke/exechelper" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + DEFAULT_NETWORK_NUM int = 1 +) + +var IsPersistent = flag.Bool("persist", false, "persists topology config") +var IsVerbose = flag.Bool("verbose", false, "verbose test output") +var IsUnconfiguring = flag.Bool("unconfigure", false, "remove topology") +var IsVppDebug = flag.Bool("debug", false, "attach gdb to vpp") +var NConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp") +var VppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory") +var SuiteTimeout time.Duration + +type HstSuite struct { + Containers map[string]*Container + StartedContainers []*Container + Volumes []string + NetConfigs []NetConfig + NetInterfaces map[string]*NetInterface + Ip4AddrAllocator *Ip4AddressAllocator + TestIds map[string]string + CpuAllocator *CpuAllocatorT + CpuContexts []*CpuContext + CpuPerVpp int + Ppid string + ProcessIndex string + Logger *log.Logger + LogFile *os.File +} + +func getTestFilename() string { + _, filename, _, _ := runtime.Caller(2) + return filepath.Base(filename) +} + +func (s *HstSuite) SetupSuite() { + s.CreateLogger() + s.Log("Suite Setup") + RegisterFailHandler(func(message string, callerSkip ...int) { + s.HstFail() + Fail(message, callerSkip...) + }) + var err error + s.Ppid = fmt.Sprint(os.Getppid()) + // remove last number so we have space to prepend a process index (interfaces have a char limit) + s.Ppid = s.Ppid[:len(s.Ppid)-1] + s.ProcessIndex = fmt.Sprint(GinkgoParallelProcess()) + s.CpuAllocator, err = CpuAllocator() + if err != nil { + Fail("failed to init cpu allocator: " + fmt.Sprint(err)) + } + s.CpuPerVpp = *NConfiguredCpus +} + +func (s *HstSuite) AllocateCpus() []int { + cpuCtx, err := s.CpuAllocator.Allocate(len(s.StartedContainers), s.CpuPerVpp) + s.AssertNil(err) + s.AddCpuContext(cpuCtx) + return cpuCtx.cpus +} + +func (s *HstSuite) AddCpuContext(cpuCtx *CpuContext) { + s.CpuContexts = append(s.CpuContexts, cpuCtx) +} + +func (s *HstSuite) TearDownSuite() { + defer s.LogFile.Close() + s.Log("Suite Teardown") + s.UnconfigureNetworkTopology() +} + +func (s *HstSuite) TearDownTest() { + s.Log("Test Teardown") + if *IsPersistent { + return + } + s.ResetContainers() + s.RemoveVolumes() + s.Ip4AddrAllocator.DeleteIpAddresses() +} + +func (s *HstSuite) SkipIfUnconfiguring() { + if *IsUnconfiguring { + s.Skip("skipping to unconfigure") + } +} + +func (s *HstSuite) SetupTest() { + s.Log("Test Setup") + s.StartedContainers = s.StartedContainers[:0] + s.SkipIfUnconfiguring() + s.SetupVolumes() + s.SetupContainers() +} + +func (s *HstSuite) SetupVolumes() { + for _, volume := range s.Volumes { + cmd := "docker volume create --name=" + volume + s.Log(cmd) + exechelper.Run(cmd) + } +} + +func (s *HstSuite) SetupContainers() { + for _, container := range s.Containers { + if !container.IsOptional { + container.Run() + } + } +} + +func (s *HstSuite) LogVppInstance(container *Container, maxLines int) { + if container.VppInstance == nil { + return + } + + logSource := container.GetHostWorkDir() + defaultLogFilePath + file, err := os.Open(logSource) + + if err != nil { + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var lines []string + var counter int + + for scanner.Scan() { + lines = append(lines, scanner.Text()) + counter++ + if counter > maxLines { + lines = lines[1:] + counter-- + } + } + + s.Log("vvvvvvvvvvvvvvv " + container.Name + " [VPP instance]:") + for _, line := range lines { + s.Log(line) + } + s.Log("^^^^^^^^^^^^^^^\n\n") +} + +func (s *HstSuite) HstFail() { + for _, container := range s.StartedContainers { + out, err := container.log(20) + if err != nil { + s.Log("An error occured while obtaining '" + container.Name + "' container logs: " + fmt.Sprint(err)) + s.Log("The container might not be running - check logs in " + container.getLogDirPath()) + continue + } + s.Log("\nvvvvvvvvvvvvvvv " + + container.Name + ":\n" + + out + + "^^^^^^^^^^^^^^^\n\n") + s.LogVppInstance(container, 20) + } +} + +func (s *HstSuite) AssertNil(object interface{}, msgAndArgs ...interface{}) { + Expect(object).To(BeNil(), msgAndArgs...) +} + +func (s *HstSuite) AssertNotNil(object interface{}, msgAndArgs ...interface{}) { + Expect(object).ToNot(BeNil(), msgAndArgs...) +} + +func (s *HstSuite) AssertEqual(expected, actual interface{}, msgAndArgs ...interface{}) { + Expect(actual).To(Equal(expected), msgAndArgs...) +} + +func (s *HstSuite) AssertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) { + Expect(actual).ToNot(Equal(expected), msgAndArgs...) +} + +func (s *HstSuite) AssertContains(testString, contains interface{}, msgAndArgs ...interface{}) { + Expect(testString).To(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...) +} + +func (s *HstSuite) AssertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) { + Expect(testString).ToNot(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...) +} + +func (s *HstSuite) AssertNotEmpty(object interface{}, msgAndArgs ...interface{}) { + Expect(object).ToNot(BeEmpty(), msgAndArgs...) +} + +func (s *HstSuite) CreateLogger() { + suiteName := s.GetCurrentSuiteName() + var err error + s.LogFile, err = os.Create("summary/" + suiteName + ".log") + if err != nil { + Fail("Unable to create log file.") + } + s.Logger = log.New(io.Writer(s.LogFile), "", log.LstdFlags) +} + +// Logs to files by default, logs to stdout when VERBOSE=true with GinkgoWriter +// to keep console tidy +func (s *HstSuite) Log(arg any) { + logs := strings.Split(fmt.Sprint(arg), "\n") + for _, line := range logs { + s.Logger.Println(line) + } + if *IsVerbose { + GinkgoWriter.Println(arg) + } +} + +func (s *HstSuite) Skip(args string) { + Skip(args) +} + +func (s *HstSuite) SkipIfMultiWorker(args ...any) { + if *NConfiguredCpus > 1 { + s.Skip("test case not supported with multiple vpp workers") + } +} + +func (s *HstSuite) SkipUnlessExtendedTestsBuilt() { + imageName := "hs-test/nginx-http3" + + cmd := exec.Command("docker", "images", imageName) + byteOutput, err := cmd.CombinedOutput() + if err != nil { + s.Log("error while searching for docker image") + return + } + if !strings.Contains(string(byteOutput), imageName) { + s.Skip("extended tests not built") + } +} + +func (s *HstSuite) ResetContainers() { + for _, container := range s.StartedContainers { + container.stop() + exechelper.Run("docker rm " + container.Name) + } +} + +func (s *HstSuite) RemoveVolumes() { + for _, volumeName := range s.Volumes { + cmd := "docker volume rm " + volumeName + exechelper.Run(cmd) + os.RemoveAll(volumeName) + } +} + +func (s *HstSuite) GetNetNamespaceByName(name string) string { + return s.ProcessIndex + name + s.Ppid +} + +func (s *HstSuite) GetInterfaceByName(name string) *NetInterface { + return s.NetInterfaces[s.ProcessIndex+name+s.Ppid] +} + +func (s *HstSuite) GetContainerByName(name string) *Container { + return s.Containers[s.ProcessIndex+name+s.Ppid] +} + +/* + * Create a copy and return its address, so that individial tests which call this + * are not able to modify the original container and affect other tests by doing that + */ +func (s *HstSuite) GetTransientContainerByName(name string) *Container { + containerCopy := *s.Containers[s.ProcessIndex+name+s.Ppid] + return &containerCopy +} + +func (s *HstSuite) LoadContainerTopology(topologyName string) { + data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml") + if err != nil { + Fail("read error: " + fmt.Sprint(err)) + } + var yamlTopo YamlTopology + err = yaml.Unmarshal(data, &yamlTopo) + if err != nil { + Fail("unmarshal error: " + fmt.Sprint(err)) + } + + for _, elem := range yamlTopo.Volumes { + volumeMap := elem["volume"].(VolumeConfig) + hostDir := volumeMap["host-dir"].(string) + workingVolumeDir := logDir + s.GetCurrentTestName() + volumeDir + volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir) + hostDir = volDirReplacer.Replace(hostDir) + s.Volumes = append(s.Volumes, hostDir) + } + + s.Containers = make(map[string]*Container) + for _, elem := range yamlTopo.Containers { + newContainer, err := newContainer(s, elem) + newContainer.Suite = s + newContainer.Name = newContainer.Suite.ProcessIndex + newContainer.Name + newContainer.Suite.Ppid + if err != nil { + Fail("container config error: " + fmt.Sprint(err)) + } + s.Containers[newContainer.Name] = newContainer + } +} + +func (s *HstSuite) LoadNetworkTopology(topologyName string) { + data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml") + if err != nil { + Fail("read error: " + fmt.Sprint(err)) + } + var yamlTopo YamlTopology + err = yaml.Unmarshal(data, &yamlTopo) + if err != nil { + Fail("unmarshal error: " + fmt.Sprint(err)) + } + + s.Ip4AddrAllocator = NewIp4AddressAllocator() + s.NetInterfaces = make(map[string]*NetInterface) + + for _, elem := range yamlTopo.Devices { + if _, ok := elem["name"]; ok { + elem["name"] = s.ProcessIndex + elem["name"].(string) + s.Ppid + } + + if peer, ok := elem["peer"].(NetDevConfig); ok { + if peer["name"].(string) != "" { + peer["name"] = s.ProcessIndex + peer["name"].(string) + s.Ppid + } + if _, ok := peer["netns"]; ok { + peer["netns"] = s.ProcessIndex + peer["netns"].(string) + s.Ppid + } + } + + if _, ok := elem["netns"]; ok { + elem["netns"] = s.ProcessIndex + elem["netns"].(string) + s.Ppid + } + + if _, ok := elem["interfaces"]; ok { + interfaceCount := len(elem["interfaces"].([]interface{})) + for i := 0; i < interfaceCount; i++ { + elem["interfaces"].([]interface{})[i] = s.ProcessIndex + elem["interfaces"].([]interface{})[i].(string) + s.Ppid + } + } + + switch elem["type"].(string) { + case NetNs: + { + if namespace, err := newNetNamespace(elem); err == nil { + s.NetConfigs = append(s.NetConfigs, &namespace) + } else { + Fail("network config error: " + fmt.Sprint(err)) + } + } + case Veth, Tap: + { + if netIf, err := newNetworkInterface(elem, s.Ip4AddrAllocator); err == nil { + s.NetConfigs = append(s.NetConfigs, netIf) + s.NetInterfaces[netIf.Name()] = netIf + } else { + Fail("network config error: " + fmt.Sprint(err)) + } + } + case Bridge: + { + if bridge, err := newBridge(elem); err == nil { + s.NetConfigs = append(s.NetConfigs, &bridge) + } else { + Fail("network config error: " + fmt.Sprint(err)) + } + } + } + } +} + +func (s *HstSuite) ConfigureNetworkTopology(topologyName string) { + s.LoadNetworkTopology(topologyName) + + if *IsUnconfiguring { + return + } + + for _, nc := range s.NetConfigs { + s.Log(nc.Name()) + if err := nc.configure(); err != nil { + Fail("Network config error: " + fmt.Sprint(err)) + } + } +} + +func (s *HstSuite) UnconfigureNetworkTopology() { + if *IsPersistent { + return + } + for _, nc := range s.NetConfigs { + nc.unconfigure() + } +} + +func (s *HstSuite) GetTestId() string { + testName := s.GetCurrentTestName() + + if s.TestIds == nil { + s.TestIds = map[string]string{} + } + + if _, ok := s.TestIds[testName]; !ok { + s.TestIds[testName] = time.Now().Format("2006-01-02_15-04-05") + } + + return s.TestIds[testName] +} + +func (s *HstSuite) GetCurrentTestName() string { + return strings.Split(CurrentSpecReport().LeafNodeText, "/")[1] +} + +func (s *HstSuite) GetCurrentSuiteName() string { + return CurrentSpecReport().ContainerHierarchyTexts[0] +} + +// Returns last 3 digits of PID + Ginkgo process index as the 4th digit +func (s *HstSuite) GetPortFromPpid() string { + port := s.Ppid + for len(port) < 3 { + port += "0" + } + return port[len(port)-3:] + s.ProcessIndex +} + +func (s *HstSuite) StartServerApp(running chan error, done chan struct{}, env []string) { + cmd := exec.Command("iperf3", "-4", "-s", "-p", s.GetPortFromPpid()) + if env != nil { + cmd.Env = env + } + s.Log(cmd) + err := cmd.Start() + if err != nil { + msg := fmt.Errorf("failed to start iperf server: %v", err) + running <- msg + return + } + running <- nil + <-done + cmd.Process.Kill() +} + +func (s *HstSuite) StartClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) { + defer func() { + clnCh <- nil + }() + + nTries := 0 + + for { + cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g", "-p", s.GetPortFromPpid()) + if env != nil { + cmd.Env = env + } + s.Log(cmd) + o, err := cmd.CombinedOutput() + if err != nil { + if nTries > 5 { + clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o) + return + } + time.Sleep(1 * time.Second) + nTries++ + continue + } else { + clnRes <- fmt.Sprintf("Client output: %s", o) + } + break + } +} + +func (s *HstSuite) StartHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) { + cmd := newCommand([]string{"./http_server", addressPort, s.Ppid, s.ProcessIndex}, netNs) + err := cmd.Start() + s.Log(cmd) + if err != nil { + s.Log("Failed to start http server: " + fmt.Sprint(err)) + return + } + running <- struct{}{} + <-done + cmd.Process.Kill() +} + +func (s *HstSuite) StartWget(finished chan error, server_ip, port, query, netNs string) { + defer func() { + finished <- errors.New("wget error") + }() + + cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query}, + netNs) + s.Log(cmd) + o, err := cmd.CombinedOutput() + if err != nil { + finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o) + return + } else if !strings.Contains(string(o), "200 OK") { + finished <- fmt.Errorf("wget error: response not 200 OK") + return + } + finished <- nil +} + +/* +runBenchmark creates Gomega's experiment with the passed-in name and samples the passed-in callback repeatedly (samplesNum times), +passing in suite context, experiment and your data. + +You can also instruct runBenchmark to run with multiple concurrent workers. +You can record multiple named measurements (float64 or duration) within passed-in callback. +runBenchmark then produces report to show statistical distribution of measurements. +*/ +func (s *HstSuite) RunBenchmark(name string, samplesNum, parallelNum int, callback func(s *HstSuite, e *gmeasure.Experiment, data interface{}), data interface{}) { + experiment := gmeasure.NewExperiment(name) + + experiment.Sample(func(idx int) { + defer GinkgoRecover() + callback(s, experiment, data) + }, gmeasure.SamplingConfig{N: samplesNum, NumParallel: parallelNum}) + AddReportEntry(experiment.Name, experiment) +} diff --git a/extras/hs-test/infra/netconfig.go b/extras/hs-test/infra/netconfig.go new file mode 100644 index 00000000000..3f3d3e3e84c --- /dev/null +++ b/extras/hs-test/infra/netconfig.go @@ -0,0 +1,383 @@ +package hst + +import ( + "errors" + "fmt" + "os/exec" + "strings" + + "go.fd.io/govpp/binapi/ethernet_types" + "go.fd.io/govpp/binapi/interface_types" + "go.fd.io/govpp/binapi/ip_types" +) + +type ( + Cmd = exec.Cmd + MacAddress = ethernet_types.MacAddress + AddressWithPrefix = ip_types.AddressWithPrefix + IP4AddressWithPrefix = ip_types.IP4AddressWithPrefix + InterfaceIndex = interface_types.InterfaceIndex + + NetConfig interface { + configure() error + unconfigure() + Name() string + Type() string + } + + NetConfigBase struct { + name string + category string // what else to call this when `type` is reserved? + } + + NetInterface struct { + NetConfigBase + Ip4AddrAllocator *Ip4AddressAllocator + Ip4Address string + Index InterfaceIndex + HwAddress MacAddress + NetworkNamespace string + NetworkNumber int + Peer *NetInterface + } + + NetworkNamespace struct { + NetConfigBase + } + + NetworkBridge struct { + NetConfigBase + NetworkNamespace string + Interfaces []string + } +) + +const ( + NetNs string = "netns" + Veth string = "veth" + Tap string = "tap" + Bridge string = "bridge" +) + +type InterfaceAdder func(n *NetInterface) *Cmd + +var ( + ipCommandMap = map[string]InterfaceAdder{ + Veth: func(n *NetInterface) *Cmd { + return exec.Command("ip", "link", "add", n.name, "type", "veth", "peer", "name", n.Peer.name) + }, + Tap: func(n *NetInterface) *Cmd { + return exec.Command("ip", "tuntap", "add", n.name, "mode", "tap") + }, + } +) + +func newNetworkInterface(cfg NetDevConfig, a *Ip4AddressAllocator) (*NetInterface, error) { + var newInterface *NetInterface = &NetInterface{} + var err error + newInterface.Ip4AddrAllocator = a + newInterface.name = cfg["name"].(string) + newInterface.NetworkNumber = DEFAULT_NETWORK_NUM + + if interfaceType, ok := cfg["type"]; ok { + newInterface.category = interfaceType.(string) + } + + if presetHwAddress, ok := cfg["preset-hw-address"]; ok { + newInterface.HwAddress, err = ethernet_types.ParseMacAddress(presetHwAddress.(string)) + if err != nil { + return &NetInterface{}, err + } + } + + if netns, ok := cfg["netns"]; ok { + newInterface.NetworkNamespace = netns.(string) + } + + if ip, ok := cfg["ip4"]; ok { + if n, ok := ip.(NetDevConfig)["network"]; ok { + newInterface.NetworkNumber = n.(int) + } + newInterface.Ip4Address, err = newInterface.Ip4AddrAllocator.NewIp4InterfaceAddress( + newInterface.NetworkNumber, + ) + if err != nil { + return &NetInterface{}, err + } + } + + if _, ok := cfg["peer"]; !ok { + return newInterface, nil + } + + peer := cfg["peer"].(NetDevConfig) + + if newInterface.Peer, err = newNetworkInterface(peer, a); err != nil { + return &NetInterface{}, err + } + + return newInterface, nil +} + +func (n *NetInterface) configureUpState() error { + err := setDevUp(n.Name(), "") + if err != nil { + return fmt.Errorf("set link up failed: %v", err) + } + return nil +} + +func (n *NetInterface) configureNetworkNamespace() error { + if n.NetworkNamespace != "" { + err := linkSetNetns(n.name, n.NetworkNamespace) + if err != nil { + return err + } + } + return nil +} + +func (n *NetInterface) configureAddress() error { + if n.Ip4Address != "" { + if err := addAddress( + n.Name(), + n.Ip4Address, + n.NetworkNamespace, + ); err != nil { + return err + } + + } + return nil +} + +func (n *NetInterface) configure() error { + cmd := ipCommandMap[n.Type()](n) + _, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("creating interface '%v' failed: %v", n.Name(), err) + } + + if err := n.configureUpState(); err != nil { + return err + } + + if err := n.configureNetworkNamespace(); err != nil { + return err + } + + if err := n.configureAddress(); err != nil { + return err + } + + if n.Peer != nil && n.Peer.name != "" { + if err := n.Peer.configureUpState(); err != nil { + return err + } + + if err := n.Peer.configureNetworkNamespace(); err != nil { + return err + } + + if err := n.Peer.configureAddress(); err != nil { + return err + } + } + + return nil +} + +func (n *NetInterface) unconfigure() { + delLink(n.name) +} + +func (n *NetInterface) Name() string { + return n.name +} + +func (n *NetInterface) Type() string { + return n.category +} + +func (n *NetInterface) AddressWithPrefix() AddressWithPrefix { + address, _ := ip_types.ParseAddressWithPrefix(n.Ip4Address) + return address +} + +func (n *NetInterface) Ip4AddressWithPrefix() IP4AddressWithPrefix { + ip4Prefix, _ := ip_types.ParseIP4Prefix(n.Ip4Address) + Ip4AddressWithPrefix := ip_types.IP4AddressWithPrefix(ip4Prefix) + return Ip4AddressWithPrefix +} + +func (n *NetInterface) Ip4AddressString() string { + return strings.Split(n.Ip4Address, "/")[0] +} + +func (b *NetConfigBase) Name() string { + return b.name +} + +func (b *NetConfigBase) Type() string { + return b.category +} + +func newNetNamespace(cfg NetDevConfig) (NetworkNamespace, error) { + var networkNamespace NetworkNamespace + networkNamespace.name = cfg["name"].(string) + networkNamespace.category = NetNs + return networkNamespace, nil +} + +func (ns *NetworkNamespace) configure() error { + return addDelNetns(ns.name, true) +} + +func (ns *NetworkNamespace) unconfigure() { + addDelNetns(ns.name, false) +} + +func newBridge(cfg NetDevConfig) (NetworkBridge, error) { + var bridge NetworkBridge + bridge.name = cfg["name"].(string) + bridge.category = Bridge + for _, v := range cfg["interfaces"].([]interface{}) { + bridge.Interfaces = append(bridge.Interfaces, v.(string)) + } + + bridge.NetworkNamespace = "" + if netns, ok := cfg["netns"]; ok { + bridge.NetworkNamespace = netns.(string) + } + return bridge, nil +} + +func (b *NetworkBridge) configure() error { + return addBridge(b.name, b.Interfaces, b.NetworkNamespace) +} + +func (b *NetworkBridge) unconfigure() { + delBridge(b.name, b.NetworkNamespace) +} + +func delBridge(brName, ns string) error { + err := setDevDown(brName, ns) + if err != nil { + return err + } + + err = addDelBridge(brName, ns, false) + if err != nil { + return err + } + + return nil +} + +func setDevUp(dev, ns string) error { + return setDevUpDown(dev, ns, true) +} + +func setDevDown(dev, ns string) error { + return setDevUpDown(dev, ns, false) +} + +func delLink(ifName string) { + cmd := exec.Command("ip", "link", "del", ifName) + cmd.Run() +} + +func setDevUpDown(dev, ns string, isUp bool) error { + var op string + if isUp { + op = "up" + } else { + op = "down" + } + c := []string{"ip", "link", "set", "dev", dev, op} + cmd := appendNetns(c, ns) + err := cmd.Run() + if err != nil { + return fmt.Errorf("error bringing %s device %s! (cmd: '%s')", dev, op, cmd) + } + return nil +} + +func addDelNetns(name string, isAdd bool) error { + var op string + if isAdd { + op = "add" + } else { + op = "del" + } + cmd := exec.Command("ip", "netns", op, name) + _, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("add/del netns failed (cmd: '%s')", cmd) + } + return nil +} + +func linkSetNetns(ifName, ns string) error { + cmd := exec.Command("ip", "link", "set", "dev", ifName, "up", "netns", ns) + err := cmd.Run() + if err != nil { + return fmt.Errorf("error setting device '%s' to netns '%s: %v", ifName, ns, err) + } + return nil +} + +func newCommand(s []string, ns string) *exec.Cmd { + return appendNetns(s, ns) +} + +func appendNetns(s []string, ns string) *exec.Cmd { + var cmd *exec.Cmd + if ns == "" { + // use default namespace + cmd = exec.Command(s[0], s[1:]...) + } else { + var args = []string{"netns", "exec", ns} + args = append(args, s[:]...) + cmd = exec.Command("ip", args...) + } + return cmd +} + +func addDelBridge(brName, ns string, isAdd bool) error { + var op string + if isAdd { + op = "addbr" + } else { + op = "delbr" + } + var c = []string{"brctl", op, brName} + cmd := appendNetns(c, ns) + err := cmd.Run() + if err != nil { + s := fmt.Sprintf("%s %s failed! err: '%s'", op, brName, err) + return errors.New(s) + } + return nil +} + +func addBridge(brName string, ifs []string, ns string) error { + err := addDelBridge(brName, ns, true) + if err != nil { + return err + } + + for _, v := range ifs { + c := []string{"brctl", "addif", brName, v} + cmd := appendNetns(c, ns) + err = cmd.Run() + if err != nil { + return fmt.Errorf("error adding %s to bridge %s: %s", v, brName, err) + } + } + err = setDevUp(brName, ns) + if err != nil { + return err + } + return nil +} diff --git a/extras/hs-test/infra/suite_nginx.go b/extras/hs-test/infra/suite_nginx.go new file mode 100644 index 00000000000..f835262d591 --- /dev/null +++ b/extras/hs-test/infra/suite_nginx.go @@ -0,0 +1,137 @@ +package hst + +import ( + "reflect" + "runtime" + "strings" + + . "github.com/onsi/ginkgo/v2" +) + +// These correspond to names used in yaml config +const ( + VppProxyContainerName = "vpp-proxy" + NginxProxyContainerName = "nginx-proxy" + NginxServerContainerName = "nginx-server" + MirroringClientInterfaceName = "hstcln" + MirroringServerInterfaceName = "hstsrv" +) + +var nginxTests = map[string][]func(s *NginxSuite){} +var nginxSoloTests = map[string][]func(s *NginxSuite){} + +type NginxSuite struct { + HstSuite +} + +func RegisterNginxTests(tests ...func(s *NginxSuite)) { + nginxTests[getTestFilename()] = tests +} +func RegisterNginxSoloTests(tests ...func(s *NginxSuite)) { + nginxSoloTests[getTestFilename()] = tests +} + +func (s *NginxSuite) SetupSuite() { + s.HstSuite.SetupSuite() + s.LoadNetworkTopology("2taps") + s.LoadContainerTopology("nginxProxyAndServer") +} + +func (s *NginxSuite) SetupTest() { + s.HstSuite.SetupTest() + + // Setup test conditions + var sessionConfig Stanza + sessionConfig. + NewStanza("session"). + Append("enable"). + Append("use-app-socket-api").Close() + + // ... for proxy + vppProxyContainer := s.GetContainerByName(VppProxyContainerName) + proxyVpp, _ := vppProxyContainer.newVppInstance(vppProxyContainer.AllocatedCpus, sessionConfig) + s.AssertNil(proxyVpp.Start()) + + clientInterface := s.GetInterfaceByName(MirroringClientInterfaceName) + s.AssertNil(proxyVpp.createTap(clientInterface, 1)) + + serverInterface := s.GetInterfaceByName(MirroringServerInterfaceName) + s.AssertNil(proxyVpp.createTap(serverInterface, 2)) + + nginxContainer := s.GetTransientContainerByName(NginxProxyContainerName) + nginxContainer.Create() + + values := struct { + Proxy string + Server string + }{ + Proxy: clientInterface.Peer.Ip4AddressString(), + Server: serverInterface.Ip4AddressString(), + } + nginxContainer.CreateConfig( + "/nginx.conf", + "./resources/nginx/nginx_proxy_mirroring.conf", + values, + ) + s.AssertNil(nginxContainer.Start()) + + proxyVpp.WaitForApp("nginx-", 5) +} + +var _ = Describe("NginxSuite", Ordered, ContinueOnFailure, func() { + var s NginxSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range nginxTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) + +var _ = Describe("NginxSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { + var s NginxSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range nginxSoloTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, Label("SOLO"), func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) diff --git a/extras/hs-test/infra/suite_no_topo.go b/extras/hs-test/infra/suite_no_topo.go new file mode 100644 index 00000000000..c48e6fb1845 --- /dev/null +++ b/extras/hs-test/infra/suite_no_topo.go @@ -0,0 +1,112 @@ +package hst + +import ( + "reflect" + "runtime" + "strings" + + . "github.com/onsi/ginkgo/v2" +) + +const ( + SingleTopoContainerVpp = "vpp" + SingleTopoContainerNginx = "nginx" + TapInterfaceName = "htaphost" +) + +var noTopoTests = map[string][]func(s *NoTopoSuite){} +var noTopoSoloTests = map[string][]func(s *NoTopoSuite){} + +type NoTopoSuite struct { + HstSuite +} + +func RegisterNoTopoTests(tests ...func(s *NoTopoSuite)) { + noTopoTests[getTestFilename()] = tests +} +func RegisterNoTopoSoloTests(tests ...func(s *NoTopoSuite)) { + noTopoSoloTests[getTestFilename()] = tests +} + +func (s *NoTopoSuite) SetupSuite() { + s.HstSuite.SetupSuite() + s.LoadNetworkTopology("tap") + s.LoadContainerTopology("single") +} + +func (s *NoTopoSuite) SetupTest() { + s.HstSuite.SetupTest() + + // Setup test conditions + var sessionConfig Stanza + sessionConfig. + NewStanza("session"). + Append("enable"). + Append("use-app-socket-api").Close() + + container := s.GetContainerByName(SingleTopoContainerVpp) + vpp, _ := container.newVppInstance(container.AllocatedCpus, sessionConfig) + s.AssertNil(vpp.Start()) + + tapInterface := s.GetInterfaceByName(TapInterfaceName) + + s.AssertNil(vpp.createTap(tapInterface), "failed to create tap interface") +} + +var _ = Describe("NoTopoSuite", Ordered, ContinueOnFailure, func() { + var s NoTopoSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range noTopoTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) + +var _ = Describe("NoTopoSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { + var s NoTopoSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range noTopoSoloTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, Label("SOLO"), func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) diff --git a/extras/hs-test/infra/suite_ns.go b/extras/hs-test/infra/suite_ns.go new file mode 100644 index 00000000000..d88730b1c0b --- /dev/null +++ b/extras/hs-test/infra/suite_ns.go @@ -0,0 +1,121 @@ +package hst + +import ( + "fmt" + "reflect" + "runtime" + "strings" + + . "github.com/onsi/ginkgo/v2" +) + +// These correspond to names used in yaml config +const ( + ClientInterface = "hclnvpp" + ServerInterface = "hsrvvpp" +) + +var nsTests = map[string][]func(s *NsSuite){} +var nsSoloTests = map[string][]func(s *NsSuite){} + +type NsSuite struct { + HstSuite +} + +func RegisterNsTests(tests ...func(s *NsSuite)) { + nsTests[getTestFilename()] = tests +} +func RegisterNsSoloTests(tests ...func(s *NsSuite)) { + nsSoloTests[getTestFilename()] = tests +} + +func (s *NsSuite) SetupSuite() { + s.HstSuite.SetupSuite() + s.ConfigureNetworkTopology("ns") + s.LoadContainerTopology("ns") +} + +func (s *NsSuite) SetupTest() { + s.HstSuite.SetupTest() + + // Setup test conditions + var sessionConfig Stanza + sessionConfig. + NewStanza("session"). + Append("enable"). + Append("use-app-socket-api"). + Append("evt_qs_memfd_seg"). + Append("event-queue-length 100000").Close() + + container := s.GetContainerByName("vpp") + vpp, _ := container.newVppInstance(container.AllocatedCpus, sessionConfig) + s.AssertNil(vpp.Start()) + + idx, err := vpp.createAfPacket(s.GetInterfaceByName(ServerInterface)) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertNotEqual(0, idx) + + idx, err = vpp.createAfPacket(s.GetInterfaceByName(ClientInterface)) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertNotEqual(0, idx) + + container.Exec("chmod 777 -R %s", container.GetContainerWorkDir()) +} + +var _ = Describe("NsSuite", Ordered, ContinueOnFailure, func() { + var s NsSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range nsTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) + +var _ = Describe("NsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { + var s NsSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range nsSoloTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, Label("SOLO"), func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) diff --git a/extras/hs-test/infra/suite_tap.go b/extras/hs-test/infra/suite_tap.go new file mode 100644 index 00000000000..c02ab8e8535 --- /dev/null +++ b/extras/hs-test/infra/suite_tap.go @@ -0,0 +1,88 @@ +package hst + +import ( + "reflect" + "runtime" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" +) + +type TapSuite struct { + HstSuite +} + +var tapTests = map[string][]func(s *TapSuite){} +var tapSoloTests = map[string][]func(s *TapSuite){} + +func RegisterTapTests(tests ...func(s *TapSuite)) { + tapTests[getTestFilename()] = tests +} +func RegisterTapSoloTests(tests ...func(s *TapSuite)) { + tapSoloTests[getTestFilename()] = tests +} + +func (s *TapSuite) SetupSuite() { + time.Sleep(1 * time.Second) + s.HstSuite.SetupSuite() + s.ConfigureNetworkTopology("tap") +} + +var _ = Describe("TapSuite", Ordered, ContinueOnFailure, func() { + var s TapSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range tapTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) + +var _ = Describe("TapSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { + var s TapSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + for filename, tests := range tapSoloTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, Label("SOLO"), func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) diff --git a/extras/hs-test/infra/suite_veth.go b/extras/hs-test/infra/suite_veth.go new file mode 100644 index 00000000000..d7bfa55acd0 --- /dev/null +++ b/extras/hs-test/infra/suite_veth.go @@ -0,0 +1,146 @@ +package hst + +import ( + "fmt" + "reflect" + "runtime" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" +) + +// These correspond to names used in yaml config +const ( + ServerInterfaceName = "srv" + ClientInterfaceName = "cln" +) + +var vethTests = map[string][]func(s *VethsSuite){} +var vethSoloTests = map[string][]func(s *VethsSuite){} + +type VethsSuite struct { + HstSuite +} + +func RegisterVethTests(tests ...func(s *VethsSuite)) { + vethTests[getTestFilename()] = tests +} +func RegisterSoloVethTests(tests ...func(s *VethsSuite)) { + vethSoloTests[getTestFilename()] = tests +} + +func (s *VethsSuite) SetupSuite() { + time.Sleep(1 * time.Second) + s.HstSuite.SetupSuite() + s.ConfigureNetworkTopology("2peerVeth") + s.LoadContainerTopology("2peerVeth") +} + +func (s *VethsSuite) SetupTest() { + s.HstSuite.SetupTest() + + // Setup test conditions + var sessionConfig Stanza + sessionConfig. + NewStanza("session"). + Append("enable"). + Append("use-app-socket-api").Close() + + // ... For server + serverContainer := s.GetContainerByName("server-vpp") + + serverVpp, err := serverContainer.newVppInstance(serverContainer.AllocatedCpus, sessionConfig) + s.AssertNotNil(serverVpp, fmt.Sprint(err)) + + s.SetupServerVpp() + + // ... For client + clientContainer := s.GetContainerByName("client-vpp") + + clientVpp, err := clientContainer.newVppInstance(clientContainer.AllocatedCpus, sessionConfig) + s.AssertNotNil(clientVpp, fmt.Sprint(err)) + + s.setupClientVpp() +} + +func (s *VethsSuite) SetupServerVpp() { + serverVpp := s.GetContainerByName("server-vpp").VppInstance + s.AssertNil(serverVpp.Start()) + + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + idx, err := serverVpp.createAfPacket(serverVeth) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertNotEqual(0, idx) +} + +func (s *VethsSuite) setupClientVpp() { + clientVpp := s.GetContainerByName("client-vpp").VppInstance + s.AssertNil(clientVpp.Start()) + + clientVeth := s.GetInterfaceByName(ClientInterfaceName) + idx, err := clientVpp.createAfPacket(clientVeth) + s.AssertNil(err, fmt.Sprint(err)) + s.AssertNotEqual(0, idx) +} + +var _ = Describe("VethsSuite", Ordered, ContinueOnFailure, func() { + var s VethsSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + + }) + AfterEach(func() { + s.TearDownTest() + }) + + // https://onsi.github.io/ginkgo/#dynamically-generating-specs + for filename, tests := range vethTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) + +var _ = Describe("VethsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { + var s VethsSuite + BeforeAll(func() { + s.SetupSuite() + }) + BeforeEach(func() { + s.SetupTest() + }) + AfterAll(func() { + s.TearDownSuite() + }) + AfterEach(func() { + s.TearDownTest() + }) + + // https://onsi.github.io/ginkgo/#dynamically-generating-specs + for filename, tests := range vethSoloTests { + for _, test := range tests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] + It(testName, Label("SOLO"), func(ctx SpecContext) { + s.Log(testName + ": BEGIN") + test(&s) + }, SpecTimeout(SuiteTimeout)) + } + } +}) diff --git a/extras/hs-test/infra/topo.go b/extras/hs-test/infra/topo.go new file mode 100644 index 00000000000..f9c6528ba93 --- /dev/null +++ b/extras/hs-test/infra/topo.go @@ -0,0 +1,25 @@ +package hst + +import ( + "fmt" +) + +type NetDevConfig map[string]interface{} +type ContainerConfig map[string]interface{} +type VolumeConfig map[string]interface{} + +type YamlTopology struct { + Devices []NetDevConfig `yaml:"devices"` + Containers []ContainerConfig `yaml:"containers"` + Volumes []VolumeConfig `yaml:"volumes"` +} + +func addAddress(device, address, ns string) error { + c := []string{"ip", "addr", "add", address, "dev", device} + cmd := appendNetns(c, ns) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to set ip address for %s: %v", device, err) + } + return nil +} diff --git a/extras/hs-test/infra/utils.go b/extras/hs-test/infra/utils.go new file mode 100644 index 00000000000..9619efbbf63 --- /dev/null +++ b/extras/hs-test/infra/utils.go @@ -0,0 +1,119 @@ +package hst + +import ( + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + "time" +) + +const networkTopologyDir string = "topo-network/" +const containerTopologyDir string = "topo-containers/" + +type Stanza struct { + content string + pad int +} + +type ActionResult struct { + Err error + Desc string + ErrOutput string + StdOutput string +} + +type JsonResult struct { + Code int + Desc string + ErrOutput string + StdOutput string +} + +func AssertFileSize(f1, f2 string) error { + fi1, err := os.Stat(f1) + if err != nil { + return err + } + + fi2, err1 := os.Stat(f2) + if err1 != nil { + return err1 + } + + if fi1.Size() != fi2.Size() { + return fmt.Errorf("file sizes differ (%d vs %d)", fi1.Size(), fi2.Size()) + } + return nil +} + +func (c *Stanza) NewStanza(name string) *Stanza { + c.Append("\n" + name + " {") + c.pad += 2 + return c +} + +func (c *Stanza) Append(name string) *Stanza { + c.content += strings.Repeat(" ", c.pad) + c.content += name + "\n" + return c +} + +func (c *Stanza) Close() *Stanza { + c.content += "}\n" + c.pad -= 2 + return c +} + +func (s *Stanza) ToString() string { + return s.content +} + +func (s *Stanza) SaveToFile(fileName string) error { + fo, err := os.Create(fileName) + if err != nil { + return err + } + defer fo.Close() + + _, err = io.Copy(fo, strings.NewReader(s.content)) + return err +} + +// NewHttpClient creates [http.Client] with disabled proxy and redirects, it also sets timeout to 30seconds. +func NewHttpClient() *http.Client { + transport := http.DefaultTransport + transport.(*http.Transport).Proxy = nil + transport.(*http.Transport).DisableKeepAlives = true + client := &http.Client{ + Transport: transport, + Timeout: time.Second * 30, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }} + return client +} + +func TcpSendReceive(address, data string) (string, error) { + conn, err := net.DialTimeout("tcp", address, time.Second*30) + if err != nil { + return "", err + } + defer conn.Close() + err = conn.SetDeadline(time.Now().Add(time.Second * 30)) + if err != nil { + return "", err + } + _, err = conn.Write([]byte(data)) + if err != nil { + return "", err + } + reply := make([]byte, 1024) + _, err = conn.Read(reply) + if err != nil { + return "", err + } + return string(reply), nil +} diff --git a/extras/hs-test/infra/vppinstance.go b/extras/hs-test/infra/vppinstance.go new file mode 100644 index 00000000000..5164a54aa9a --- /dev/null +++ b/extras/hs-test/infra/vppinstance.go @@ -0,0 +1,500 @@ +package hst + +import ( + "context" + "fmt" + "go.fd.io/govpp/binapi/ethernet_types" + "io" + "net" + "os" + "os/exec" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "github.com/edwarnicke/exechelper" + . "github.com/onsi/ginkgo/v2" + "github.com/sirupsen/logrus" + + "go.fd.io/govpp" + "go.fd.io/govpp/api" + "go.fd.io/govpp/binapi/af_packet" + interfaces "go.fd.io/govpp/binapi/interface" + "go.fd.io/govpp/binapi/interface_types" + "go.fd.io/govpp/binapi/session" + "go.fd.io/govpp/binapi/tapv2" + "go.fd.io/govpp/core" +) + +const vppConfigTemplate = `unix { + nodaemon + log %[1]s%[4]s + full-coredump + cli-listen %[1]s%[2]s + runtime-dir %[1]s/var/run + gid vpp +} + +api-trace { + on +} + +api-segment { + gid vpp +} + +socksvr { + socket-name %[1]s%[3]s +} + +statseg { + socket-name %[1]s/var/run/vpp/stats.sock +} + +plugins { + plugin default { disable } + + plugin unittest_plugin.so { enable } + plugin quic_plugin.so { enable } + plugin af_packet_plugin.so { enable } + plugin hs_apps_plugin.so { enable } + plugin http_plugin.so { enable } + plugin http_static_plugin.so { enable } + plugin prom_plugin.so { enable } + plugin tlsopenssl_plugin.so { enable } + plugin ping_plugin.so { enable } + plugin nsim_plugin.so { enable } + plugin mactime_plugin.so { enable } +} + +logging { + default-log-level debug + default-syslog-log-level debug +} + +` + +const ( + defaultCliSocketFilePath = "/var/run/vpp/cli.sock" + defaultApiSocketFilePath = "/var/run/vpp/api.sock" + defaultLogFilePath = "/var/log/vpp/vpp.log" +) + +type VppInstance struct { + Container *Container + AdditionalConfig []Stanza + Connection *core.Connection + ApiStream api.Stream + Cpus []int +} + +func (vpp *VppInstance) getSuite() *HstSuite { + return vpp.Container.Suite +} + +func (vpp *VppInstance) getCliSocket() string { + return fmt.Sprintf("%s%s", vpp.Container.GetContainerWorkDir(), defaultCliSocketFilePath) +} + +func (vpp *VppInstance) getRunDir() string { + return vpp.Container.GetContainerWorkDir() + "/var/run/vpp" +} + +func (vpp *VppInstance) getLogDir() string { + return vpp.Container.GetContainerWorkDir() + "/var/log/vpp" +} + +func (vpp *VppInstance) getEtcDir() string { + return vpp.Container.GetContainerWorkDir() + "/etc/vpp" +} + +func (vpp *VppInstance) Start() error { + maxReconnectAttempts := 3 + // Replace default logger in govpp with our own + govppLogger := logrus.New() + govppLogger.SetOutput(io.MultiWriter(vpp.getSuite().Logger.Writer(), GinkgoWriter)) + core.SetLogger(govppLogger) + // Create folders + containerWorkDir := vpp.Container.GetContainerWorkDir() + + vpp.Container.Exec("mkdir --mode=0700 -p " + vpp.getRunDir()) + vpp.Container.Exec("mkdir --mode=0700 -p " + vpp.getLogDir()) + vpp.Container.Exec("mkdir --mode=0700 -p " + vpp.getEtcDir()) + + // Create startup.conf inside the container + configContent := fmt.Sprintf( + vppConfigTemplate, + containerWorkDir, + defaultCliSocketFilePath, + defaultApiSocketFilePath, + defaultLogFilePath, + ) + configContent += vpp.generateCpuConfig() + for _, c := range vpp.AdditionalConfig { + configContent += c.ToString() + } + startupFileName := vpp.getEtcDir() + "/startup.conf" + vpp.Container.CreateFile(startupFileName, configContent) + + // create wrapper script for vppctl with proper CLI socket path + cliContent := "#!/usr/bin/bash\nvppctl -s " + vpp.getRunDir() + "/cli.sock" + vppcliFileName := "/usr/bin/vppcli" + vpp.Container.CreateFile(vppcliFileName, cliContent) + vpp.Container.Exec("chmod 0755 " + vppcliFileName) + + vpp.getSuite().Log("starting vpp") + if *IsVppDebug { + // default = 3; VPP will timeout while debugging if there are not enough attempts + maxReconnectAttempts = 5000 + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGQUIT) + cont := make(chan bool, 1) + go func() { + <-sig + cont <- true + }() + + vpp.Container.ExecServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"") + fmt.Println("run following command in different terminal:") + fmt.Println("docker exec -it " + vpp.Container.Name + " gdb -ex \"attach $(docker exec " + vpp.Container.Name + " pidof vpp)\"") + fmt.Println("Afterwards press CTRL+\\ to continue") + <-cont + fmt.Println("continuing...") + } else { + // Start VPP + vpp.Container.ExecServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"") + } + + vpp.getSuite().Log("connecting to vpp") + // Connect to VPP and store the connection + sockAddress := vpp.Container.GetHostWorkDir() + defaultApiSocketFilePath + conn, connEv, err := govpp.AsyncConnect( + sockAddress, + maxReconnectAttempts, + core.DefaultReconnectInterval) + if err != nil { + vpp.getSuite().Log("async connect error: " + fmt.Sprint(err)) + return err + } + vpp.Connection = conn + + // ... wait for Connected event + e := <-connEv + if e.State != core.Connected { + vpp.getSuite().Log("connecting to VPP failed: " + fmt.Sprint(e.Error)) + } + + ch, err := conn.NewStream( + context.Background(), + core.WithRequestSize(50), + core.WithReplySize(50), + core.WithReplyTimeout(time.Second*5)) + if err != nil { + vpp.getSuite().Log("creating stream failed: " + fmt.Sprint(err)) + return err + } + vpp.ApiStream = ch + + return nil +} + +func (vpp *VppInstance) Vppctl(command string, arguments ...any) string { + vppCliCommand := fmt.Sprintf(command, arguments...) + containerExecCommand := fmt.Sprintf("docker exec --detach=false %[1]s vppctl -s %[2]s %[3]s", + vpp.Container.Name, vpp.getCliSocket(), vppCliCommand) + vpp.getSuite().Log(containerExecCommand) + output, err := exechelper.CombinedOutput(containerExecCommand) + vpp.getSuite().AssertNil(err) + + return string(output) +} + +func (vpp *VppInstance) GetSessionStat(stat string) int { + o := vpp.Vppctl("show session stats") + vpp.getSuite().Log(o) + for _, line := range strings.Split(o, "\n") { + if strings.Contains(line, stat) { + tokens := strings.Split(strings.TrimSpace(line), " ") + val, err := strconv.Atoi(tokens[0]) + if err != nil { + Fail("failed to parse stat value %s" + fmt.Sprint(err)) + return 0 + } + return val + } + } + return 0 +} + +func (vpp *VppInstance) WaitForApp(appName string, timeout int) { + vpp.getSuite().Log("waiting for app " + appName) + for i := 0; i < timeout; i++ { + o := vpp.Vppctl("show app") + if strings.Contains(o, appName) { + return + } + time.Sleep(1 * time.Second) + } + vpp.getSuite().AssertNil(1, "Timeout while waiting for app '%s'", appName) +} + +func (vpp *VppInstance) createAfPacket( + veth *NetInterface, +) (interface_types.InterfaceIndex, error) { + createReq := &af_packet.AfPacketCreateV3{ + Mode: 1, + UseRandomHwAddr: true, + HostIfName: veth.Name(), + Flags: af_packet.AfPacketFlags(11), + } + if veth.HwAddress != (MacAddress{}) { + createReq.UseRandomHwAddr = false + createReq.HwAddr = veth.HwAddress + } + + vpp.getSuite().Log("create af-packet interface " + veth.Name()) + if err := vpp.ApiStream.SendMsg(createReq); err != nil { + vpp.getSuite().HstFail() + return 0, err + } + replymsg, err := vpp.ApiStream.RecvMsg() + if err != nil { + return 0, err + } + reply := replymsg.(*af_packet.AfPacketCreateV3Reply) + err = api.RetvalToVPPApiError(reply.Retval) + if err != nil { + return 0, err + } + + veth.Index = reply.SwIfIndex + + // Set to up + upReq := &interfaces.SwInterfaceSetFlags{ + SwIfIndex: veth.Index, + Flags: interface_types.IF_STATUS_API_FLAG_ADMIN_UP, + } + + vpp.getSuite().Log("set af-packet interface " + veth.Name() + " up") + if err := vpp.ApiStream.SendMsg(upReq); err != nil { + return 0, err + } + replymsg, err = vpp.ApiStream.RecvMsg() + if err != nil { + return 0, err + } + reply2 := replymsg.(*interfaces.SwInterfaceSetFlagsReply) + if err = api.RetvalToVPPApiError(reply2.Retval); err != nil { + return 0, err + } + + // Add address + if veth.AddressWithPrefix() == (AddressWithPrefix{}) { + var err error + var ip4Address string + if ip4Address, err = veth.Ip4AddrAllocator.NewIp4InterfaceAddress(veth.Peer.NetworkNumber); err == nil { + veth.Ip4Address = ip4Address + } else { + return 0, err + } + } + addressReq := &interfaces.SwInterfaceAddDelAddress{ + IsAdd: true, + SwIfIndex: veth.Index, + Prefix: veth.AddressWithPrefix(), + } + + vpp.getSuite().Log("af-packet interface " + veth.Name() + " add address " + veth.Ip4Address) + if err := vpp.ApiStream.SendMsg(addressReq); err != nil { + return 0, err + } + replymsg, err = vpp.ApiStream.RecvMsg() + if err != nil { + return 0, err + } + reply3 := replymsg.(*interfaces.SwInterfaceAddDelAddressReply) + err = api.RetvalToVPPApiError(reply3.Retval) + if err != nil { + return 0, err + } + + return veth.Index, nil +} + +func (vpp *VppInstance) addAppNamespace( + secret uint64, + ifx interface_types.InterfaceIndex, + namespaceId string, +) error { + req := &session.AppNamespaceAddDelV4{ + IsAdd: true, + Secret: secret, + SwIfIndex: ifx, + NamespaceID: namespaceId, + SockName: defaultApiSocketFilePath, + } + + vpp.getSuite().Log("add app namespace " + namespaceId) + if err := vpp.ApiStream.SendMsg(req); err != nil { + return err + } + replymsg, err := vpp.ApiStream.RecvMsg() + if err != nil { + return err + } + reply := replymsg.(*session.AppNamespaceAddDelV4Reply) + if err = api.RetvalToVPPApiError(reply.Retval); err != nil { + return err + } + + sessionReq := &session.SessionEnableDisable{ + IsEnable: true, + } + + vpp.getSuite().Log("enable app namespace " + namespaceId) + if err := vpp.ApiStream.SendMsg(sessionReq); err != nil { + return err + } + replymsg, err = vpp.ApiStream.RecvMsg() + if err != nil { + return err + } + reply2 := replymsg.(*session.SessionEnableDisableReply) + if err = api.RetvalToVPPApiError(reply2.Retval); err != nil { + return err + } + + return nil +} + +func (vpp *VppInstance) createTap( + tap *NetInterface, + tapId ...uint32, +) error { + var id uint32 = 1 + if len(tapId) > 0 { + id = tapId[0] + } + createTapReq := &tapv2.TapCreateV3{ + ID: id, + HostIfNameSet: true, + HostIfName: tap.Name(), + HostIP4PrefixSet: true, + HostIP4Prefix: tap.Ip4AddressWithPrefix(), + } + + vpp.getSuite().Log("create tap interface " + tap.Name()) + // Create tap interface + if err := vpp.ApiStream.SendMsg(createTapReq); err != nil { + return err + } + replymsg, err := vpp.ApiStream.RecvMsg() + if err != nil { + return err + } + reply := replymsg.(*tapv2.TapCreateV3Reply) + if err = api.RetvalToVPPApiError(reply.Retval); err != nil { + return err + } + tap.Peer.Index = reply.SwIfIndex + + // Get name and mac + if err := vpp.ApiStream.SendMsg(&interfaces.SwInterfaceDump{ + SwIfIndex: reply.SwIfIndex, + }); err != nil { + return err + } + replymsg, err = vpp.ApiStream.RecvMsg() + if err != nil { + return err + } + ifDetails := replymsg.(*interfaces.SwInterfaceDetails) + tap.Peer.name = ifDetails.InterfaceName + tap.Peer.HwAddress = ifDetails.L2Address + + // Add address + addAddressReq := &interfaces.SwInterfaceAddDelAddress{ + IsAdd: true, + SwIfIndex: reply.SwIfIndex, + Prefix: tap.Peer.AddressWithPrefix(), + } + + vpp.getSuite().Log("tap interface " + tap.Name() + " add address " + tap.Peer.Ip4Address) + if err := vpp.ApiStream.SendMsg(addAddressReq); err != nil { + return err + } + replymsg, err = vpp.ApiStream.RecvMsg() + if err != nil { + return err + } + reply2 := replymsg.(*interfaces.SwInterfaceAddDelAddressReply) + if err = api.RetvalToVPPApiError(reply2.Retval); err != nil { + return err + } + + // Set interface to up + upReq := &interfaces.SwInterfaceSetFlags{ + SwIfIndex: reply.SwIfIndex, + Flags: interface_types.IF_STATUS_API_FLAG_ADMIN_UP, + } + + vpp.getSuite().Log("set tap interface " + tap.Name() + " up") + if err := vpp.ApiStream.SendMsg(upReq); err != nil { + return err + } + replymsg, err = vpp.ApiStream.RecvMsg() + if err != nil { + return err + } + reply3 := replymsg.(*interfaces.SwInterfaceSetFlagsReply) + if err = api.RetvalToVPPApiError(reply3.Retval); err != nil { + return err + } + + // Get host mac + netIntf, err := net.InterfaceByName(tap.Name()) + if err == nil { + tap.HwAddress, _ = ethernet_types.ParseMacAddress(netIntf.HardwareAddr.String()) + } + + return nil +} + +func (vpp *VppInstance) saveLogs() { + logTarget := vpp.Container.getLogDirPath() + "vppinstance-" + vpp.Container.Name + ".log" + logSource := vpp.Container.GetHostWorkDir() + defaultLogFilePath + cmd := exec.Command("cp", logSource, logTarget) + vpp.getSuite().Log(cmd.String()) + cmd.Run() +} + +func (vpp *VppInstance) Disconnect() { + vpp.Connection.Disconnect() + vpp.ApiStream.Close() +} + +func (vpp *VppInstance) generateCpuConfig() string { + var c Stanza + var s string + if len(vpp.Cpus) < 1 { + return "" + } + c.NewStanza("cpu"). + Append(fmt.Sprintf("main-core %d", vpp.Cpus[0])) + vpp.getSuite().Log(fmt.Sprintf("main-core %d", vpp.Cpus[0])) + workers := vpp.Cpus[1:] + + if len(workers) > 0 { + for i := 0; i < len(workers); i++ { + if i != 0 { + s = s + ", " + } + s = s + fmt.Sprintf("%d", workers[i]) + } + c.Append(fmt.Sprintf("corelist-workers %s", s)) + vpp.getSuite().Log("corelist-workers " + s) + } + return c.Close().ToString() +} diff --git a/extras/hs-test/ldp_test.go b/extras/hs-test/ldp_test.go index 24d2de39485..fc195f1572a 100644 --- a/extras/hs-test/ldp_test.go +++ b/extras/hs-test/ldp_test.go @@ -4,21 +4,22 @@ import ( "fmt" "os" + . "fd.io/hs-test/infra" . "github.com/onsi/ginkgo/v2" ) func init() { - registerVethTests(LDPreloadIperfVppTest) + RegisterVethTests(LDPreloadIperfVppTest) } func LDPreloadIperfVppTest(s *VethsSuite) { var clnVclConf, srvVclConf Stanza - serverContainer := s.getContainerByName("server-vpp") - serverVclFileName := serverContainer.getHostWorkDir() + "/vcl_srv.conf" + serverContainer := s.GetContainerByName("server-vpp") + serverVclFileName := serverContainer.GetHostWorkDir() + "/vcl_srv.conf" - clientContainer := s.getContainerByName("client-vpp") - clientVclFileName := clientContainer.getHostWorkDir() + "/vcl_cln.conf" + clientContainer := s.GetContainerByName("client-vpp") + clientVclFileName := clientContainer.GetHostWorkDir() + "/vcl_cln.conf" ldpreload := "LD_PRELOAD=../../build-root/build-vpp-native/vpp/lib/x86_64-linux-gnu/libvcl_ldpreload.so" @@ -26,58 +27,58 @@ func LDPreloadIperfVppTest(s *VethsSuite) { srvCh := make(chan error, 1) clnCh := make(chan error) - s.log("starting VPPs") + s.Log("starting VPPs") clientAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default", - clientContainer.getHostWorkDir()) + clientContainer.GetHostWorkDir()) err := clnVclConf. - newStanza("vcl"). - append("rx-fifo-size 4000000"). - append("tx-fifo-size 4000000"). - append("app-scope-local"). - append("app-scope-global"). - append("use-mq-eventfd"). - append(clientAppSocketApi).close(). - saveToFile(clientVclFileName) - s.assertNil(err, fmt.Sprint(err)) + NewStanza("vcl"). + Append("rx-fifo-size 4000000"). + Append("tx-fifo-size 4000000"). + Append("app-scope-local"). + Append("app-scope-global"). + Append("use-mq-eventfd"). + Append(clientAppSocketApi).Close(). + SaveToFile(clientVclFileName) + s.AssertNil(err, fmt.Sprint(err)) serverAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default", - serverContainer.getHostWorkDir()) + serverContainer.GetHostWorkDir()) err = srvVclConf. - newStanza("vcl"). - append("rx-fifo-size 4000000"). - append("tx-fifo-size 4000000"). - append("app-scope-local"). - append("app-scope-global"). - append("use-mq-eventfd"). - append(serverAppSocketApi).close(). - saveToFile(serverVclFileName) - s.assertNil(err, fmt.Sprint(err)) - - s.log("attaching server to vpp") + NewStanza("vcl"). + Append("rx-fifo-size 4000000"). + Append("tx-fifo-size 4000000"). + Append("app-scope-local"). + Append("app-scope-global"). + Append("use-mq-eventfd"). + Append(serverAppSocketApi).Close(). + SaveToFile(serverVclFileName) + s.AssertNil(err, fmt.Sprint(err)) + + s.Log("attaching server to vpp") srvEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+serverVclFileName) go func() { defer GinkgoRecover() - s.startServerApp(srvCh, stopServerCh, srvEnv) + s.StartServerApp(srvCh, stopServerCh, srvEnv) }() err = <-srvCh - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) - s.log("attaching client to vpp") + s.Log("attaching client to vpp") var clnRes = make(chan string, 1) clnEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+clientVclFileName) - serverVethAddress := s.getInterfaceByName(serverInterfaceName).ip4AddressString() + serverVethAddress := s.GetInterfaceByName(ServerInterfaceName).Ip4AddressString() go func() { defer GinkgoRecover() - s.startClientApp(serverVethAddress, clnEnv, clnCh, clnRes) + s.StartClientApp(serverVethAddress, clnEnv, clnCh, clnRes) }() - s.log(<-clnRes) + s.Log(<-clnRes) // wait for client's result err = <-clnCh - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) // stop server stopServerCh <- struct{}{} diff --git a/extras/hs-test/linux_iperf_test.go b/extras/hs-test/linux_iperf_test.go index e323f7fb721..f49d9bcf227 100644 --- a/extras/hs-test/linux_iperf_test.go +++ b/extras/hs-test/linux_iperf_test.go @@ -1,13 +1,13 @@ package main import ( + . "fd.io/hs-test/infra" "fmt" - . "github.com/onsi/ginkgo/v2" ) func init() { - registerTapTests(LinuxIperfTest) + RegisterTapTests(LinuxIperfTest) } func LinuxIperfTest(s *TapSuite) { @@ -21,20 +21,20 @@ func LinuxIperfTest(s *TapSuite) { go func() { defer GinkgoRecover() - s.startServerApp(srvCh, stopServerCh, nil) + s.StartServerApp(srvCh, stopServerCh, nil) }() err := <-srvCh - s.assertNil(err, fmt.Sprint(err)) - s.log("server running") + s.AssertNil(err, fmt.Sprint(err)) + s.Log("server running") - ipAddress := s.getInterfaceByName(tapInterfaceName).ip4AddressString() + ipAddress := s.GetInterfaceByName(TapInterfaceName).Ip4AddressString() go func() { defer GinkgoRecover() - s.startClientApp(ipAddress, nil, clnCh, clnRes) + s.StartClientApp(ipAddress, nil, clnCh, clnRes) }() - s.log("client running") - s.log(<-clnRes) + s.Log("client running") + s.Log(<-clnRes) err = <-clnCh - s.assertNil(err, "err: '%s', ip: '%s'", err, ipAddress) - s.log("Test completed") + s.AssertNil(err, "err: '%s', ip: '%s'", err, ipAddress) + s.Log("Test completed") } diff --git a/extras/hs-test/mirroring_test.go b/extras/hs-test/mirroring_test.go index 1fd15dde2f4..57099b73d98 100644 --- a/extras/hs-test/mirroring_test.go +++ b/extras/hs-test/mirroring_test.go @@ -1,26 +1,27 @@ package main import ( + . "fd.io/hs-test/infra" "github.com/edwarnicke/exechelper" ) func init() { - registerNginxTests(MirroringTest) + RegisterNginxTests(MirroringTest) } // broken when CPUS > 1 func MirroringTest(s *NginxSuite) { s.SkipIfMultiWorker() - proxyAddress := s.getInterfaceByName(mirroringClientInterfaceName).peer.ip4AddressString() + proxyAddress := s.GetInterfaceByName(MirroringClientInterfaceName).Peer.Ip4AddressString() path := "/64B.json" testCommand := "wrk -c 20 -t 10 -d 10 http://" + proxyAddress + ":80" + path - s.log(testCommand) + s.Log(testCommand) o, _ := exechelper.Output(testCommand) - s.log(string(o)) - s.assertNotEmpty(o) + s.Log(string(o)) + s.AssertNotEmpty(o) - vppProxyContainer := s.getContainerByName(vppProxyContainerName) - s.assertEqual(0, vppProxyContainer.vppInstance.GetSessionStat("no lcl port")) + vppProxyContainer := s.GetContainerByName(VppProxyContainerName) + s.AssertEqual(0, vppProxyContainer.VppInstance.GetSessionStat("no lcl port")) } diff --git a/extras/hs-test/netconfig.go b/extras/hs-test/netconfig.go deleted file mode 100644 index c76a0fda5f5..00000000000 --- a/extras/hs-test/netconfig.go +++ /dev/null @@ -1,383 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os/exec" - "strings" - - "go.fd.io/govpp/binapi/ethernet_types" - "go.fd.io/govpp/binapi/interface_types" - "go.fd.io/govpp/binapi/ip_types" -) - -type ( - Cmd = exec.Cmd - MacAddress = ethernet_types.MacAddress - AddressWithPrefix = ip_types.AddressWithPrefix - IP4AddressWithPrefix = ip_types.IP4AddressWithPrefix - InterfaceIndex = interface_types.InterfaceIndex - - NetConfig interface { - configure() error - unconfigure() - Name() string - Type() string - } - - NetConfigBase struct { - name string - category string // what else to call this when `type` is reserved? - } - - NetInterface struct { - NetConfigBase - ip4AddrAllocator *Ip4AddressAllocator - ip4Address string - index InterfaceIndex - hwAddress MacAddress - networkNamespace string - networkNumber int - peer *NetInterface - } - - NetworkNamespace struct { - NetConfigBase - } - - NetworkBridge struct { - NetConfigBase - networkNamespace string - interfaces []string - } -) - -const ( - NetNs string = "netns" - Veth string = "veth" - Tap string = "tap" - Bridge string = "bridge" -) - -type InterfaceAdder func(n *NetInterface) *Cmd - -var ( - ipCommandMap = map[string]InterfaceAdder{ - Veth: func(n *NetInterface) *Cmd { - return exec.Command("ip", "link", "add", n.name, "type", "veth", "peer", "name", n.peer.name) - }, - Tap: func(n *NetInterface) *Cmd { - return exec.Command("ip", "tuntap", "add", n.name, "mode", "tap") - }, - } -) - -func newNetworkInterface(cfg NetDevConfig, a *Ip4AddressAllocator) (*NetInterface, error) { - var newInterface *NetInterface = &NetInterface{} - var err error - newInterface.ip4AddrAllocator = a - newInterface.name = cfg["name"].(string) - newInterface.networkNumber = DEFAULT_NETWORK_NUM - - if interfaceType, ok := cfg["type"]; ok { - newInterface.category = interfaceType.(string) - } - - if presetHwAddress, ok := cfg["preset-hw-address"]; ok { - newInterface.hwAddress, err = ethernet_types.ParseMacAddress(presetHwAddress.(string)) - if err != nil { - return &NetInterface{}, err - } - } - - if netns, ok := cfg["netns"]; ok { - newInterface.networkNamespace = netns.(string) - } - - if ip, ok := cfg["ip4"]; ok { - if n, ok := ip.(NetDevConfig)["network"]; ok { - newInterface.networkNumber = n.(int) - } - newInterface.ip4Address, err = newInterface.ip4AddrAllocator.NewIp4InterfaceAddress( - newInterface.networkNumber, - ) - if err != nil { - return &NetInterface{}, err - } - } - - if _, ok := cfg["peer"]; !ok { - return newInterface, nil - } - - peer := cfg["peer"].(NetDevConfig) - - if newInterface.peer, err = newNetworkInterface(peer, a); err != nil { - return &NetInterface{}, err - } - - return newInterface, nil -} - -func (n *NetInterface) configureUpState() error { - err := setDevUp(n.Name(), "") - if err != nil { - return fmt.Errorf("set link up failed: %v", err) - } - return nil -} - -func (n *NetInterface) configureNetworkNamespace() error { - if n.networkNamespace != "" { - err := linkSetNetns(n.name, n.networkNamespace) - if err != nil { - return err - } - } - return nil -} - -func (n *NetInterface) configureAddress() error { - if n.ip4Address != "" { - if err := addAddress( - n.Name(), - n.ip4Address, - n.networkNamespace, - ); err != nil { - return err - } - - } - return nil -} - -func (n *NetInterface) configure() error { - cmd := ipCommandMap[n.Type()](n) - _, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("creating interface '%v' failed: %v", n.Name(), err) - } - - if err := n.configureUpState(); err != nil { - return err - } - - if err := n.configureNetworkNamespace(); err != nil { - return err - } - - if err := n.configureAddress(); err != nil { - return err - } - - if n.peer != nil && n.peer.name != "" { - if err := n.peer.configureUpState(); err != nil { - return err - } - - if err := n.peer.configureNetworkNamespace(); err != nil { - return err - } - - if err := n.peer.configureAddress(); err != nil { - return err - } - } - - return nil -} - -func (n *NetInterface) unconfigure() { - delLink(n.name) -} - -func (n *NetInterface) Name() string { - return n.name -} - -func (n *NetInterface) Type() string { - return n.category -} - -func (n *NetInterface) addressWithPrefix() AddressWithPrefix { - address, _ := ip_types.ParseAddressWithPrefix(n.ip4Address) - return address -} - -func (n *NetInterface) ip4AddressWithPrefix() IP4AddressWithPrefix { - ip4Prefix, _ := ip_types.ParseIP4Prefix(n.ip4Address) - ip4AddressWithPrefix := ip_types.IP4AddressWithPrefix(ip4Prefix) - return ip4AddressWithPrefix -} - -func (n *NetInterface) ip4AddressString() string { - return strings.Split(n.ip4Address, "/")[0] -} - -func (b *NetConfigBase) Name() string { - return b.name -} - -func (b *NetConfigBase) Type() string { - return b.category -} - -func newNetNamespace(cfg NetDevConfig) (NetworkNamespace, error) { - var networkNamespace NetworkNamespace - networkNamespace.name = cfg["name"].(string) - networkNamespace.category = NetNs - return networkNamespace, nil -} - -func (ns *NetworkNamespace) configure() error { - return addDelNetns(ns.name, true) -} - -func (ns *NetworkNamespace) unconfigure() { - addDelNetns(ns.name, false) -} - -func newBridge(cfg NetDevConfig) (NetworkBridge, error) { - var bridge NetworkBridge - bridge.name = cfg["name"].(string) - bridge.category = Bridge - for _, v := range cfg["interfaces"].([]interface{}) { - bridge.interfaces = append(bridge.interfaces, v.(string)) - } - - bridge.networkNamespace = "" - if netns, ok := cfg["netns"]; ok { - bridge.networkNamespace = netns.(string) - } - return bridge, nil -} - -func (b *NetworkBridge) configure() error { - return addBridge(b.name, b.interfaces, b.networkNamespace) -} - -func (b *NetworkBridge) unconfigure() { - delBridge(b.name, b.networkNamespace) -} - -func delBridge(brName, ns string) error { - err := setDevDown(brName, ns) - if err != nil { - return err - } - - err = addDelBridge(brName, ns, false) - if err != nil { - return err - } - - return nil -} - -func setDevUp(dev, ns string) error { - return setDevUpDown(dev, ns, true) -} - -func setDevDown(dev, ns string) error { - return setDevUpDown(dev, ns, false) -} - -func delLink(ifName string) { - cmd := exec.Command("ip", "link", "del", ifName) - cmd.Run() -} - -func setDevUpDown(dev, ns string, isUp bool) error { - var op string - if isUp { - op = "up" - } else { - op = "down" - } - c := []string{"ip", "link", "set", "dev", dev, op} - cmd := appendNetns(c, ns) - err := cmd.Run() - if err != nil { - return fmt.Errorf("error bringing %s device %s! (cmd: '%s')", dev, op, cmd) - } - return nil -} - -func addDelNetns(name string, isAdd bool) error { - var op string - if isAdd { - op = "add" - } else { - op = "del" - } - cmd := exec.Command("ip", "netns", op, name) - _, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("add/del netns failed (cmd: '%s')", cmd) - } - return nil -} - -func linkSetNetns(ifName, ns string) error { - cmd := exec.Command("ip", "link", "set", "dev", ifName, "up", "netns", ns) - err := cmd.Run() - if err != nil { - return fmt.Errorf("error setting device '%s' to netns '%s: %v", ifName, ns, err) - } - return nil -} - -func newCommand(s []string, ns string) *exec.Cmd { - return appendNetns(s, ns) -} - -func appendNetns(s []string, ns string) *exec.Cmd { - var cmd *exec.Cmd - if ns == "" { - // use default namespace - cmd = exec.Command(s[0], s[1:]...) - } else { - var args = []string{"netns", "exec", ns} - args = append(args, s[:]...) - cmd = exec.Command("ip", args...) - } - return cmd -} - -func addDelBridge(brName, ns string, isAdd bool) error { - var op string - if isAdd { - op = "addbr" - } else { - op = "delbr" - } - var c = []string{"brctl", op, brName} - cmd := appendNetns(c, ns) - err := cmd.Run() - if err != nil { - s := fmt.Sprintf("%s %s failed! err: '%s'", op, brName, err) - return errors.New(s) - } - return nil -} - -func addBridge(brName string, ifs []string, ns string) error { - err := addDelBridge(brName, ns, true) - if err != nil { - return err - } - - for _, v := range ifs { - c := []string{"brctl", "addif", brName, v} - cmd := appendNetns(c, ns) - err = cmd.Run() - if err != nil { - return fmt.Errorf("error adding %s to bridge %s: %s", v, brName, err) - } - } - err = setDevUp(brName, ns) - if err != nil { - return err - } - return nil -} diff --git a/extras/hs-test/proxy_test.go b/extras/hs-test/proxy_test.go index c57b927846e..5f7eb45be38 100644 --- a/extras/hs-test/proxy_test.go +++ b/extras/hs-test/proxy_test.go @@ -1,37 +1,37 @@ package main import ( + . "fd.io/hs-test/infra" "fmt" - "os" - "github.com/edwarnicke/exechelper" . "github.com/onsi/ginkgo/v2" + "os" ) func init() { - registerNsTests(VppProxyHttpTcpTest, VppProxyHttpTlsTest, EnvoyProxyHttpTcpTest) + RegisterNsTests(VppProxyHttpTcpTest, VppProxyHttpTlsTest, EnvoyProxyHttpTcpTest) } func testProxyHttpTcp(s *NsSuite, proto string) error { - var outputFile string = s.processIndex + "test" + s.ppid + ".data" - var srcFilePpid string = s.processIndex + "httpTestFile" + s.ppid + var outputFile string = s.ProcessIndex + "test" + s.Ppid + ".data" + var srcFilePpid string = s.ProcessIndex + "httpTestFile" + s.Ppid const srcFileNoPpid = "httpTestFile" const fileSize string = "10M" stopServer := make(chan struct{}, 1) serverRunning := make(chan struct{}, 1) - serverNetns := s.getNetNamespaceByName("srv") - clientNetns := s.getNetNamespaceByName("cln") + serverNetns := s.GetNetNamespaceByName("srv") + clientNetns := s.GetNetNamespaceByName("cln") // create test file err := exechelper.Run(fmt.Sprintf("ip netns exec %s truncate -s %s %s", serverNetns, fileSize, srcFilePpid)) - s.assertNil(err, "failed to run truncate command: "+fmt.Sprint(err)) + s.AssertNil(err, "failed to run truncate command: "+fmt.Sprint(err)) defer func() { os.Remove(srcFilePpid) }() - s.log("test file created...") + s.Log("test file created...") go func() { defer GinkgoRecover() - s.startHttpServer(serverRunning, stopServer, ":666", serverNetns) + s.StartHttpServer(serverRunning, stopServer, ":666", serverNetns) }() // TODO better error handling and recovery <-serverRunning @@ -40,76 +40,76 @@ func testProxyHttpTcp(s *NsSuite, proto string) error { stopServer <- struct{}{} }(stopServer) - s.log("http server started...") + s.Log("http server started...") - clientVeth := s.getInterfaceByName(clientInterface) + clientVeth := s.GetInterfaceByName(ClientInterface) c := fmt.Sprintf("ip netns exec %s wget --no-proxy --retry-connrefused"+ " --retry-on-http-error=503 --tries=10 -O %s ", clientNetns, outputFile) if proto == "tls" { c += " --secure-protocol=TLSv1_3 --no-check-certificate https://" } - c += fmt.Sprintf("%s:555/%s", clientVeth.ip4AddressString(), srcFileNoPpid) - s.log(c) + c += fmt.Sprintf("%s:555/%s", clientVeth.Ip4AddressString(), srcFileNoPpid) + s.Log(c) _, err = exechelper.CombinedOutput(c) defer func() { os.Remove(outputFile) }() - s.assertNil(err, "failed to run wget: '%s', cmd: %s", err, c) + s.AssertNil(err, "failed to run wget: '%s', cmd: %s", err, c) stopServer <- struct{}{} - s.assertNil(assertFileSize(outputFile, srcFilePpid)) + s.AssertNil(AssertFileSize(outputFile, srcFilePpid)) return nil } func configureVppProxy(s *NsSuite, proto string) { - serverVeth := s.getInterfaceByName(serverInterface) - clientVeth := s.getInterfaceByName(clientInterface) + serverVeth := s.GetInterfaceByName(ServerInterface) + clientVeth := s.GetInterfaceByName(ClientInterface) - testVppProxy := s.getContainerByName("vpp").vppInstance - output := testVppProxy.vppctl( + testVppProxy := s.GetContainerByName("vpp").VppInstance + output := testVppProxy.Vppctl( "test proxy server server-uri %s://%s/555 client-uri tcp://%s/666", proto, - clientVeth.ip4AddressString(), - serverVeth.peer.ip4AddressString(), + clientVeth.Ip4AddressString(), + serverVeth.Peer.Ip4AddressString(), ) - s.log("proxy configured: " + output) + s.Log("proxy configured: " + output) } func VppProxyHttpTcpTest(s *NsSuite) { proto := "tcp" configureVppProxy(s, proto) err := testProxyHttpTcp(s, proto) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) } func VppProxyHttpTlsTest(s *NsSuite) { proto := "tls" configureVppProxy(s, proto) err := testProxyHttpTcp(s, proto) - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) } func configureEnvoyProxy(s *NsSuite) { - envoyContainer := s.getContainerByName("envoy") - err := envoyContainer.create() - s.assertNil(err, "Error creating envoy container: %s", err) + envoyContainer := s.GetContainerByName("envoy") + err := envoyContainer.Create() + s.AssertNil(err, "Error creating envoy container: %s", err) - serverVeth := s.getInterfaceByName(serverInterface) + serverVeth := s.GetInterfaceByName(ServerInterface) address := struct { Server string }{ - Server: serverVeth.peer.ip4AddressString(), + Server: serverVeth.Peer.Ip4AddressString(), } - envoyContainer.createConfig( + envoyContainer.CreateConfig( "/etc/envoy/envoy.yaml", "resources/envoy/proxy.yaml", address, ) - s.assertNil(envoyContainer.start()) + s.AssertNil(envoyContainer.Start()) } func EnvoyProxyHttpTcpTest(s *NsSuite) { configureEnvoyProxy(s) err := testProxyHttpTcp(s, "tcp") - s.assertNil(err, fmt.Sprint(err)) + s.AssertNil(err, fmt.Sprint(err)) } diff --git a/extras/hs-test/raw_session_test.go b/extras/hs-test/raw_session_test.go index 5c66df0b1ce..438b7ba03a5 100644 --- a/extras/hs-test/raw_session_test.go +++ b/extras/hs-test/raw_session_test.go @@ -1,40 +1,42 @@ package main +import . "fd.io/hs-test/infra" + func init() { - registerVethTests(VppEchoQuicTest, VppEchoTcpTest) + RegisterVethTests(VppEchoQuicTest, VppEchoTcpTest) } func VppEchoQuicTest(s *VethsSuite) { - s.testVppEcho("quic") + testVppEcho(s, "quic") } // TODO: udp echo currently broken in vpp func VppEchoUdpTest(s *VethsSuite) { - s.testVppEcho("udp") + testVppEcho(s, "udp") } func VppEchoTcpTest(s *VethsSuite) { - s.testVppEcho("tcp") + testVppEcho(s, "tcp") } -func (s *VethsSuite) testVppEcho(proto string) { - serverVethAddress := s.getInterfaceByName(serverInterfaceName).ip4AddressString() +func testVppEcho(s *VethsSuite, proto string) { + serverVethAddress := s.GetInterfaceByName(ServerInterfaceName).Ip4AddressString() uri := proto + "://" + serverVethAddress + "/12344" - echoSrvContainer := s.getContainerByName("server-app") + echoSrvContainer := s.GetContainerByName("server-app") serverCommand := "vpp_echo server TX=RX" + - " socket-name " + echoSrvContainer.getContainerWorkDir() + "/var/run/app_ns_sockets/default" + + " socket-name " + echoSrvContainer.GetContainerWorkDir() + "/var/run/app_ns_sockets/default" + " use-app-socket-api" + " uri " + uri - s.log(serverCommand) - echoSrvContainer.execServer(serverCommand) + s.Log(serverCommand) + echoSrvContainer.ExecServer(serverCommand) - echoClnContainer := s.getContainerByName("client-app") + echoClnContainer := s.GetContainerByName("client-app") clientCommand := "vpp_echo client" + - " socket-name " + echoClnContainer.getContainerWorkDir() + "/var/run/app_ns_sockets/default" + + " socket-name " + echoClnContainer.GetContainerWorkDir() + "/var/run/app_ns_sockets/default" + " use-app-socket-api uri " + uri - s.log(clientCommand) - o := echoClnContainer.exec(clientCommand) - s.log(o) + s.Log(clientCommand) + o := echoClnContainer.Exec(clientCommand) + s.Log(o) } diff --git a/extras/hs-test/suite_nginx_test.go b/extras/hs-test/suite_nginx_test.go deleted file mode 100644 index 7caf16ef432..00000000000 --- a/extras/hs-test/suite_nginx_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "reflect" - "runtime" - "strings" - - . "github.com/onsi/ginkgo/v2" -) - -// These correspond to names used in yaml config -const ( - vppProxyContainerName = "vpp-proxy" - nginxProxyContainerName = "nginx-proxy" - nginxServerContainerName = "nginx-server" - mirroringClientInterfaceName = "hstcln" - mirroringServerInterfaceName = "hstsrv" -) - -var nginxTests = map[string][]func(s *NginxSuite){} -var nginxSoloTests = map[string][]func(s *NginxSuite){} - -type NginxSuite struct { - HstSuite -} - -func registerNginxTests(tests ...func(s *NginxSuite)) { - nginxTests[getTestFilename()] = tests -} -func registerNginxSoloTests(tests ...func(s *NginxSuite)) { - nginxSoloTests[getTestFilename()] = tests -} - -func (s *NginxSuite) SetupSuite() { - s.HstSuite.SetupSuite() - s.loadNetworkTopology("2taps") - s.loadContainerTopology("nginxProxyAndServer") -} - -func (s *NginxSuite) SetupTest() { - s.HstSuite.SetupTest() - - // Setup test conditions - var sessionConfig Stanza - sessionConfig. - newStanza("session"). - append("enable"). - append("use-app-socket-api").close() - - // ... for proxy - vppProxyContainer := s.getContainerByName(vppProxyContainerName) - proxyVpp, _ := vppProxyContainer.newVppInstance(vppProxyContainer.allocatedCpus, sessionConfig) - s.assertNil(proxyVpp.start()) - - clientInterface := s.getInterfaceByName(mirroringClientInterfaceName) - s.assertNil(proxyVpp.createTap(clientInterface, 1)) - - serverInterface := s.getInterfaceByName(mirroringServerInterfaceName) - s.assertNil(proxyVpp.createTap(serverInterface, 2)) - - nginxContainer := s.getTransientContainerByName(nginxProxyContainerName) - nginxContainer.create() - - values := struct { - Proxy string - Server string - }{ - Proxy: clientInterface.peer.ip4AddressString(), - Server: serverInterface.ip4AddressString(), - } - nginxContainer.createConfig( - "/nginx.conf", - "./resources/nginx/nginx_proxy_mirroring.conf", - values, - ) - s.assertNil(nginxContainer.start()) - - proxyVpp.waitForApp("nginx-", 5) -} - -var _ = Describe("NginxSuite", Ordered, ContinueOnFailure, func() { - var s NginxSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range nginxTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) - -var _ = Describe("NginxSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { - var s NginxSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range nginxSoloTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, Label("SOLO"), func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) diff --git a/extras/hs-test/suite_no_topo_test.go b/extras/hs-test/suite_no_topo_test.go deleted file mode 100644 index af0cf9a197a..00000000000 --- a/extras/hs-test/suite_no_topo_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "reflect" - "runtime" - "strings" - - . "github.com/onsi/ginkgo/v2" -) - -const ( - singleTopoContainerVpp = "vpp" - singleTopoContainerNginx = "nginx" - tapInterfaceName = "htaphost" -) - -var noTopoTests = map[string][]func(s *NoTopoSuite){} -var noTopoSoloTests = map[string][]func(s *NoTopoSuite){} - -type NoTopoSuite struct { - HstSuite -} - -func registerNoTopoTests(tests ...func(s *NoTopoSuite)) { - noTopoTests[getTestFilename()] = tests -} -func registerNoTopoSoloTests(tests ...func(s *NoTopoSuite)) { - noTopoSoloTests[getTestFilename()] = tests -} - -func (s *NoTopoSuite) SetupSuite() { - s.HstSuite.SetupSuite() - s.loadNetworkTopology("tap") - s.loadContainerTopology("single") -} - -func (s *NoTopoSuite) SetupTest() { - s.HstSuite.SetupTest() - - // Setup test conditions - var sessionConfig Stanza - sessionConfig. - newStanza("session"). - append("enable"). - append("use-app-socket-api").close() - - container := s.getContainerByName(singleTopoContainerVpp) - vpp, _ := container.newVppInstance(container.allocatedCpus, sessionConfig) - s.assertNil(vpp.start()) - - tapInterface := s.getInterfaceByName(tapInterfaceName) - - s.assertNil(vpp.createTap(tapInterface), "failed to create tap interface") -} - -var _ = Describe("NoTopoSuite", Ordered, ContinueOnFailure, func() { - var s NoTopoSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range noTopoTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) - -var _ = Describe("NoTopoSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { - var s NoTopoSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range noTopoSoloTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, Label("SOLO"), func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) diff --git a/extras/hs-test/suite_ns_test.go b/extras/hs-test/suite_ns_test.go deleted file mode 100644 index b4fa7718041..00000000000 --- a/extras/hs-test/suite_ns_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "runtime" - "strings" - - . "github.com/onsi/ginkgo/v2" -) - -// These correspond to names used in yaml config -const ( - clientInterface = "hclnvpp" - serverInterface = "hsrvvpp" -) - -var nsTests = map[string][]func(s *NsSuite){} -var nsSoloTests = map[string][]func(s *NsSuite){} - -type NsSuite struct { - HstSuite -} - -func registerNsTests(tests ...func(s *NsSuite)) { - nsTests[getTestFilename()] = tests -} -func registerNsSoloTests(tests ...func(s *NsSuite)) { - nsSoloTests[getTestFilename()] = tests -} - -func (s *NsSuite) SetupSuite() { - s.HstSuite.SetupSuite() - s.configureNetworkTopology("ns") - s.loadContainerTopology("ns") -} - -func (s *NsSuite) SetupTest() { - s.HstSuite.SetupTest() - - // Setup test conditions - var sessionConfig Stanza - sessionConfig. - newStanza("session"). - append("enable"). - append("use-app-socket-api"). - append("evt_qs_memfd_seg"). - append("event-queue-length 100000").close() - - container := s.getContainerByName("vpp") - vpp, _ := container.newVppInstance(container.allocatedCpus, sessionConfig) - s.assertNil(vpp.start()) - - idx, err := vpp.createAfPacket(s.getInterfaceByName(serverInterface)) - s.assertNil(err, fmt.Sprint(err)) - s.assertNotEqual(0, idx) - - idx, err = vpp.createAfPacket(s.getInterfaceByName(clientInterface)) - s.assertNil(err, fmt.Sprint(err)) - s.assertNotEqual(0, idx) - - container.exec("chmod 777 -R %s", container.getContainerWorkDir()) -} - -var _ = Describe("NsSuite", Ordered, ContinueOnFailure, func() { - var s NsSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range nsTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) - -var _ = Describe("NsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { - var s NsSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range nsSoloTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, Label("SOLO"), func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) diff --git a/extras/hs-test/suite_tap_test.go b/extras/hs-test/suite_tap_test.go deleted file mode 100644 index bb7082de480..00000000000 --- a/extras/hs-test/suite_tap_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "reflect" - "runtime" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" -) - -type TapSuite struct { - HstSuite -} - -var tapTests = map[string][]func(s *TapSuite){} -var tapSoloTests = map[string][]func(s *TapSuite){} - -func registerTapTests(tests ...func(s *TapSuite)) { - tapTests[getTestFilename()] = tests -} -func registerTapSoloTests(tests ...func(s *TapSuite)) { - tapSoloTests[getTestFilename()] = tests -} - -func (s *TapSuite) SetupSuite() { - time.Sleep(1 * time.Second) - s.HstSuite.SetupSuite() - s.configureNetworkTopology("tap") -} - -var _ = Describe("TapSuite", Ordered, ContinueOnFailure, func() { - var s TapSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range tapTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) - -var _ = Describe("TapSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { - var s TapSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - for filename, tests := range tapSoloTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, Label("SOLO"), func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) diff --git a/extras/hs-test/suite_veth_test.go b/extras/hs-test/suite_veth_test.go deleted file mode 100644 index 49a36cacb83..00000000000 --- a/extras/hs-test/suite_veth_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "runtime" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" -) - -// These correspond to names used in yaml config -const ( - serverInterfaceName = "srv" - clientInterfaceName = "cln" -) - -var vethTests = map[string][]func(s *VethsSuite){} -var vethSoloTests = map[string][]func(s *VethsSuite){} - -type VethsSuite struct { - HstSuite -} - -func registerVethTests(tests ...func(s *VethsSuite)) { - vethTests[getTestFilename()] = tests -} -func registerSoloVethTests(tests ...func(s *VethsSuite)) { - vethSoloTests[getTestFilename()] = tests -} - -func (s *VethsSuite) SetupSuite() { - time.Sleep(1 * time.Second) - s.HstSuite.SetupSuite() - s.configureNetworkTopology("2peerVeth") - s.loadContainerTopology("2peerVeth") -} - -func (s *VethsSuite) SetupTest() { - s.HstSuite.SetupTest() - - // Setup test conditions - var sessionConfig Stanza - sessionConfig. - newStanza("session"). - append("enable"). - append("use-app-socket-api").close() - - // ... For server - serverContainer := s.getContainerByName("server-vpp") - - serverVpp, err := serverContainer.newVppInstance(serverContainer.allocatedCpus, sessionConfig) - s.assertNotNil(serverVpp, fmt.Sprint(err)) - - s.setupServerVpp() - - // ... For client - clientContainer := s.getContainerByName("client-vpp") - - clientVpp, err := clientContainer.newVppInstance(clientContainer.allocatedCpus, sessionConfig) - s.assertNotNil(clientVpp, fmt.Sprint(err)) - - s.setupClientVpp() -} - -func (s *VethsSuite) setupServerVpp() { - serverVpp := s.getContainerByName("server-vpp").vppInstance - s.assertNil(serverVpp.start()) - - serverVeth := s.getInterfaceByName(serverInterfaceName) - idx, err := serverVpp.createAfPacket(serverVeth) - s.assertNil(err, fmt.Sprint(err)) - s.assertNotEqual(0, idx) -} - -func (s *VethsSuite) setupClientVpp() { - clientVpp := s.getContainerByName("client-vpp").vppInstance - s.assertNil(clientVpp.start()) - - clientVeth := s.getInterfaceByName(clientInterfaceName) - idx, err := clientVpp.createAfPacket(clientVeth) - s.assertNil(err, fmt.Sprint(err)) - s.assertNotEqual(0, idx) -} - -var _ = Describe("VethsSuite", Ordered, ContinueOnFailure, func() { - var s VethsSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - - }) - AfterEach(func() { - s.TearDownTest() - }) - - // https://onsi.github.io/ginkgo/#dynamically-generating-specs - for filename, tests := range vethTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) - -var _ = Describe("VethsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() { - var s VethsSuite - BeforeAll(func() { - s.SetupSuite() - }) - BeforeEach(func() { - s.SetupTest() - }) - AfterAll(func() { - s.TearDownSuite() - }) - AfterEach(func() { - s.TearDownTest() - }) - - // https://onsi.github.io/ginkgo/#dynamically-generating-specs - for filename, tests := range vethSoloTests { - for _, test := range tests { - test := test - pc := reflect.ValueOf(test).Pointer() - funcValue := runtime.FuncForPC(pc) - testName := filename + "/" + strings.Split(funcValue.Name(), ".")[2] - It(testName, Label("SOLO"), func(ctx SpecContext) { - s.log(testName + ": BEGIN") - test(&s) - }, SpecTimeout(suiteTimeout)) - } - } -}) diff --git a/extras/hs-test/topo.go b/extras/hs-test/topo.go deleted file mode 100644 index 6cb294511b3..00000000000 --- a/extras/hs-test/topo.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "fmt" -) - -type NetDevConfig map[string]interface{} -type ContainerConfig map[string]interface{} -type VolumeConfig map[string]interface{} - -type YamlTopology struct { - Devices []NetDevConfig `yaml:"devices"` - Containers []ContainerConfig `yaml:"containers"` - Volumes []VolumeConfig `yaml:"volumes"` -} - -func addAddress(device, address, ns string) error { - c := []string{"ip", "addr", "add", address, "dev", device} - cmd := appendNetns(c, ns) - err := cmd.Run() - if err != nil { - return fmt.Errorf("failed to set ip address for %s: %v", device, err) - } - return nil -} diff --git a/extras/hs-test/utils.go b/extras/hs-test/utils.go deleted file mode 100644 index b61ac4271d6..00000000000 --- a/extras/hs-test/utils.go +++ /dev/null @@ -1,119 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net" - "net/http" - "os" - "strings" - "time" -) - -const networkTopologyDir string = "topo-network/" -const containerTopologyDir string = "topo-containers/" - -type Stanza struct { - content string - pad int -} - -type ActionResult struct { - Err error - Desc string - ErrOutput string - StdOutput string -} - -type JsonResult struct { - Code int - Desc string - ErrOutput string - StdOutput string -} - -func assertFileSize(f1, f2 string) error { - fi1, err := os.Stat(f1) - if err != nil { - return err - } - - fi2, err1 := os.Stat(f2) - if err1 != nil { - return err1 - } - - if fi1.Size() != fi2.Size() { - return fmt.Errorf("file sizes differ (%d vs %d)", fi1.Size(), fi2.Size()) - } - return nil -} - -func (c *Stanza) newStanza(name string) *Stanza { - c.append("\n" + name + " {") - c.pad += 2 - return c -} - -func (c *Stanza) append(name string) *Stanza { - c.content += strings.Repeat(" ", c.pad) - c.content += name + "\n" - return c -} - -func (c *Stanza) close() *Stanza { - c.content += "}\n" - c.pad -= 2 - return c -} - -func (s *Stanza) toString() string { - return s.content -} - -func (s *Stanza) saveToFile(fileName string) error { - fo, err := os.Create(fileName) - if err != nil { - return err - } - defer fo.Close() - - _, err = io.Copy(fo, strings.NewReader(s.content)) - return err -} - -// newHttpClient creates [http.Client] with disabled proxy and redirects, it also sets timeout to 30seconds. -func newHttpClient() *http.Client { - transport := http.DefaultTransport - transport.(*http.Transport).Proxy = nil - transport.(*http.Transport).DisableKeepAlives = true - client := &http.Client{ - Transport: transport, - Timeout: time.Second * 30, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }} - return client -} - -func tcpSendReceive(address, data string) (string, error) { - conn, err := net.DialTimeout("tcp", address, time.Second*30) - if err != nil { - return "", err - } - defer conn.Close() - err = conn.SetDeadline(time.Now().Add(time.Second * 30)) - if err != nil { - return "", err - } - _, err = conn.Write([]byte(data)) - if err != nil { - return "", err - } - reply := make([]byte, 1024) - _, err = conn.Read(reply) - if err != nil { - return "", err - } - return string(reply), nil -} diff --git a/extras/hs-test/vcl_test.go b/extras/hs-test/vcl_test.go index fdcd60ad503..3b413b26bed 100644 --- a/extras/hs-test/vcl_test.go +++ b/extras/hs-test/vcl_test.go @@ -1,12 +1,13 @@ package main import ( + . "fd.io/hs-test/infra" "fmt" "time" ) func init() { - registerVethTests(XEchoVclClientUdpTest, XEchoVclClientTcpTest, XEchoVclServerUdpTest, + RegisterVethTests(XEchoVclClientUdpTest, XEchoVclClientTcpTest, XEchoVclServerUdpTest, XEchoVclServerTcpTest, VclEchoTcpTest, VclEchoUdpTest, VclRetryAttachTest) } @@ -16,141 +17,141 @@ func getVclConfig(c *Container, ns_id_optional ...string) string { if len(ns_id_optional) > 0 { ns_id = ns_id_optional[0] } - s.newStanza("vcl"). - append(fmt.Sprintf("app-socket-api %[1]s/var/run/app_ns_sockets/%[2]s", c.getContainerWorkDir(), ns_id)). - append("app-scope-global"). - append("app-scope-local"). - append("use-mq-eventfd") + s.NewStanza("vcl"). + Append(fmt.Sprintf("app-socket-api %[1]s/var/run/app_ns_sockets/%[2]s", c.GetContainerWorkDir(), ns_id)). + Append("app-scope-global"). + Append("app-scope-local"). + Append("use-mq-eventfd") if len(ns_id_optional) > 0 { - s.append(fmt.Sprintf("namespace-id %[1]s", ns_id)). - append(fmt.Sprintf("namespace-secret %[1]s", ns_id)) + s.Append(fmt.Sprintf("namespace-id %[1]s", ns_id)). + Append(fmt.Sprintf("namespace-secret %[1]s", ns_id)) } - return s.close().toString() + return s.Close().ToString() } func XEchoVclClientUdpTest(s *VethsSuite) { - s.testXEchoVclClient("udp") + testXEchoVclClient(s, "udp") } func XEchoVclClientTcpTest(s *VethsSuite) { - s.testXEchoVclClient("tcp") + testXEchoVclClient(s, "tcp") } -func (s *VethsSuite) testXEchoVclClient(proto string) { +func testXEchoVclClient(s *VethsSuite, proto string) { port := "12345" - serverVpp := s.getContainerByName("server-vpp").vppInstance + serverVpp := s.GetContainerByName("server-vpp").VppInstance - serverVeth := s.getInterfaceByName(serverInterfaceName) - serverVpp.vppctl("test echo server uri %s://%s/%s fifo-size 64k", proto, serverVeth.ip4AddressString(), port) + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + serverVpp.Vppctl("test echo server uri %s://%s/%s fifo-size 64k", proto, serverVeth.Ip4AddressString(), port) - echoClnContainer := s.getTransientContainerByName("client-app") - echoClnContainer.createFile("/vcl.conf", getVclConfig(echoClnContainer)) + echoClnContainer := s.GetTransientContainerByName("client-app") + echoClnContainer.CreateFile("/vcl.conf", getVclConfig(echoClnContainer)) - testClientCommand := "vcl_test_client -N 100 -p " + proto + " " + serverVeth.ip4AddressString() + " " + port - s.log(testClientCommand) - echoClnContainer.addEnvVar("VCL_CONFIG", "/vcl.conf") - o := echoClnContainer.exec(testClientCommand) - s.log(o) - s.assertContains(o, "CLIENT RESULTS") + testClientCommand := "vcl_test_client -N 100 -p " + proto + " " + serverVeth.Ip4AddressString() + " " + port + s.Log(testClientCommand) + echoClnContainer.AddEnvVar("VCL_CONFIG", "/vcl.conf") + o := echoClnContainer.Exec(testClientCommand) + s.Log(o) + s.AssertContains(o, "CLIENT RESULTS") } func XEchoVclServerUdpTest(s *VethsSuite) { - s.testXEchoVclServer("udp") + testXEchoVclServer(s, "udp") } func XEchoVclServerTcpTest(s *VethsSuite) { - s.testXEchoVclServer("tcp") + testXEchoVclServer(s, "tcp") } -func (s *VethsSuite) testXEchoVclServer(proto string) { +func testXEchoVclServer(s *VethsSuite, proto string) { port := "12345" - srvVppCont := s.getContainerByName("server-vpp") - srvAppCont := s.getContainerByName("server-app") + srvVppCont := s.GetContainerByName("server-vpp") + srvAppCont := s.GetContainerByName("server-app") - srvAppCont.createFile("/vcl.conf", getVclConfig(srvVppCont)) - srvAppCont.addEnvVar("VCL_CONFIG", "/vcl.conf") + srvAppCont.CreateFile("/vcl.conf", getVclConfig(srvVppCont)) + srvAppCont.AddEnvVar("VCL_CONFIG", "/vcl.conf") vclSrvCmd := fmt.Sprintf("vcl_test_server -p %s %s", proto, port) - srvAppCont.execServer(vclSrvCmd) + srvAppCont.ExecServer(vclSrvCmd) - serverVeth := s.getInterfaceByName(serverInterfaceName) - serverVethAddress := serverVeth.ip4AddressString() + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + serverVethAddress := serverVeth.Ip4AddressString() - clientVpp := s.getContainerByName("client-vpp").vppInstance - o := clientVpp.vppctl("test echo client uri %s://%s/%s fifo-size 64k verbose mbytes 2", proto, serverVethAddress, port) - s.log(o) - s.assertContains(o, "Test finished at") + clientVpp := s.GetContainerByName("client-vpp").VppInstance + o := clientVpp.Vppctl("test echo client uri %s://%s/%s fifo-size 64k verbose mbytes 2", proto, serverVethAddress, port) + s.Log(o) + s.AssertContains(o, "Test finished at") } -func (s *VethsSuite) testVclEcho(proto string) { +func testVclEcho(s *VethsSuite, proto string) { port := "12345" - srvVppCont := s.getContainerByName("server-vpp") - srvAppCont := s.getContainerByName("server-app") + srvVppCont := s.GetContainerByName("server-vpp") + srvAppCont := s.GetContainerByName("server-app") - srvAppCont.createFile("/vcl.conf", getVclConfig(srvVppCont)) - srvAppCont.addEnvVar("VCL_CONFIG", "/vcl.conf") - srvAppCont.execServer("vcl_test_server " + port) + srvAppCont.CreateFile("/vcl.conf", getVclConfig(srvVppCont)) + srvAppCont.AddEnvVar("VCL_CONFIG", "/vcl.conf") + srvAppCont.ExecServer("vcl_test_server " + port) - serverVeth := s.getInterfaceByName(serverInterfaceName) - serverVethAddress := serverVeth.ip4AddressString() + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + serverVethAddress := serverVeth.Ip4AddressString() - echoClnContainer := s.getTransientContainerByName("client-app") - echoClnContainer.createFile("/vcl.conf", getVclConfig(echoClnContainer)) + echoClnContainer := s.GetTransientContainerByName("client-app") + echoClnContainer.CreateFile("/vcl.conf", getVclConfig(echoClnContainer)) testClientCommand := "vcl_test_client -p " + proto + " " + serverVethAddress + " " + port - echoClnContainer.addEnvVar("VCL_CONFIG", "/vcl.conf") - o := echoClnContainer.exec(testClientCommand) - s.log(o) + echoClnContainer.AddEnvVar("VCL_CONFIG", "/vcl.conf") + o := echoClnContainer.Exec(testClientCommand) + s.Log(o) } func VclEchoTcpTest(s *VethsSuite) { - s.testVclEcho("tcp") + testVclEcho(s, "tcp") } func VclEchoUdpTest(s *VethsSuite) { - s.testVclEcho("udp") + testVclEcho(s, "udp") } func VclRetryAttachTest(s *VethsSuite) { - s.testRetryAttach("tcp") + testRetryAttach(s, "tcp") } -func (s *VethsSuite) testRetryAttach(proto string) { - srvVppContainer := s.getTransientContainerByName("server-vpp") +func testRetryAttach(s *VethsSuite, proto string) { + srvVppContainer := s.GetTransientContainerByName("server-vpp") - echoSrvContainer := s.getContainerByName("server-app") + echoSrvContainer := s.GetContainerByName("server-app") - echoSrvContainer.createFile("/vcl.conf", getVclConfig(echoSrvContainer)) + echoSrvContainer.CreateFile("/vcl.conf", getVclConfig(echoSrvContainer)) - echoSrvContainer.addEnvVar("VCL_CONFIG", "/vcl.conf") - echoSrvContainer.execServer("vcl_test_server -p " + proto + " 12346") + echoSrvContainer.AddEnvVar("VCL_CONFIG", "/vcl.conf") + echoSrvContainer.ExecServer("vcl_test_server -p " + proto + " 12346") - s.log("This whole test case can take around 3 minutes to run. Please be patient.") - s.log("... Running first echo client test, before disconnect.") + s.Log("This whole test case can take around 3 minutes to run. Please be patient.") + s.Log("... Running first echo client test, before disconnect.") - serverVeth := s.getInterfaceByName(serverInterfaceName) - serverVethAddress := serverVeth.ip4AddressString() + serverVeth := s.GetInterfaceByName(ServerInterfaceName) + serverVethAddress := serverVeth.Ip4AddressString() - echoClnContainer := s.getTransientContainerByName("client-app") - echoClnContainer.createFile("/vcl.conf", getVclConfig(echoClnContainer)) + echoClnContainer := s.GetTransientContainerByName("client-app") + echoClnContainer.CreateFile("/vcl.conf", getVclConfig(echoClnContainer)) testClientCommand := "vcl_test_client -U -p " + proto + " " + serverVethAddress + " 12346" - echoClnContainer.addEnvVar("VCL_CONFIG", "/vcl.conf") - o := echoClnContainer.exec(testClientCommand) - s.log(o) - s.log("... First test ended. Stopping VPP server now.") + echoClnContainer.AddEnvVar("VCL_CONFIG", "/vcl.conf") + o := echoClnContainer.Exec(testClientCommand) + s.Log(o) + s.Log("... First test ended. Stopping VPP server now.") // Stop server-vpp-instance, start it again and then run vcl-test-client once more - srvVppContainer.vppInstance.disconnect() + srvVppContainer.VppInstance.Disconnect() stopVppCommand := "/bin/bash -c 'ps -C vpp_main -o pid= | xargs kill -9'" - srvVppContainer.exec(stopVppCommand) + srvVppContainer.Exec(stopVppCommand) - s.setupServerVpp() + s.SetupServerVpp() - s.log("... VPP server is starting again, so waiting for a bit.") + s.Log("... VPP server is starting again, so waiting for a bit.") time.Sleep(30 * time.Second) // Wait a moment for the re-attachment to happen - s.log("... Running second echo client test, after disconnect and re-attachment.") - o = echoClnContainer.exec(testClientCommand) - s.log(o) - s.log("Done.") + s.Log("... Running second echo client test, after disconnect and re-attachment.") + o = echoClnContainer.Exec(testClientCommand) + s.Log(o) + s.Log("Done.") } diff --git a/extras/hs-test/vppinstance.go b/extras/hs-test/vppinstance.go deleted file mode 100644 index 4b2c7551034..00000000000 --- a/extras/hs-test/vppinstance.go +++ /dev/null @@ -1,499 +0,0 @@ -package main - -import ( - "context" - "fmt" - "go.fd.io/govpp/binapi/ethernet_types" - "io" - "net" - "os" - "os/exec" - "os/signal" - "strconv" - "strings" - "syscall" - "time" - - "github.com/edwarnicke/exechelper" - . "github.com/onsi/ginkgo/v2" - "github.com/sirupsen/logrus" - - "go.fd.io/govpp" - "go.fd.io/govpp/api" - "go.fd.io/govpp/binapi/af_packet" - interfaces "go.fd.io/govpp/binapi/interface" - "go.fd.io/govpp/binapi/interface_types" - "go.fd.io/govpp/binapi/session" - "go.fd.io/govpp/binapi/tapv2" - "go.fd.io/govpp/core" -) - -const vppConfigTemplate = `unix { - nodaemon - log %[1]s%[4]s - full-coredump - cli-listen %[1]s%[2]s - runtime-dir %[1]s/var/run - gid vpp -} - -api-trace { - on -} - -api-segment { - gid vpp -} - -socksvr { - socket-name %[1]s%[3]s -} - -statseg { - socket-name %[1]s/var/run/vpp/stats.sock -} - -plugins { - plugin default { disable } - - plugin unittest_plugin.so { enable } - plugin quic_plugin.so { enable } - plugin af_packet_plugin.so { enable } - plugin hs_apps_plugin.so { enable } - plugin http_plugin.so { enable } - plugin http_static_plugin.so { enable } - plugin prom_plugin.so { enable } - plugin tlsopenssl_plugin.so { enable } - plugin ping_plugin.so { enable } - plugin nsim_plugin.so { enable } - plugin mactime_plugin.so { enable } -} - -logging { - default-log-level debug - default-syslog-log-level debug -} - -` - -const ( - defaultCliSocketFilePath = "/var/run/vpp/cli.sock" - defaultApiSocketFilePath = "/var/run/vpp/api.sock" - defaultLogFilePath = "/var/log/vpp/vpp.log" -) - -type VppInstance struct { - container *Container - additionalConfig []Stanza - connection *core.Connection - apiStream api.Stream - cpus []int -} - -func (vpp *VppInstance) getSuite() *HstSuite { - return vpp.container.suite -} - -func (vpp *VppInstance) getCliSocket() string { - return fmt.Sprintf("%s%s", vpp.container.getContainerWorkDir(), defaultCliSocketFilePath) -} - -func (vpp *VppInstance) getRunDir() string { - return vpp.container.getContainerWorkDir() + "/var/run/vpp" -} - -func (vpp *VppInstance) getLogDir() string { - return vpp.container.getContainerWorkDir() + "/var/log/vpp" -} - -func (vpp *VppInstance) getEtcDir() string { - return vpp.container.getContainerWorkDir() + "/etc/vpp" -} - -func (vpp *VppInstance) start() error { - maxReconnectAttempts := 3 - // Replace default logger in govpp with our own - govppLogger := logrus.New() - govppLogger.SetOutput(io.MultiWriter(vpp.getSuite().logger.Writer(), GinkgoWriter)) - core.SetLogger(govppLogger) - // Create folders - containerWorkDir := vpp.container.getContainerWorkDir() - - vpp.container.exec("mkdir --mode=0700 -p " + vpp.getRunDir()) - vpp.container.exec("mkdir --mode=0700 -p " + vpp.getLogDir()) - vpp.container.exec("mkdir --mode=0700 -p " + vpp.getEtcDir()) - - // Create startup.conf inside the container - configContent := fmt.Sprintf( - vppConfigTemplate, - containerWorkDir, - defaultCliSocketFilePath, - defaultApiSocketFilePath, - defaultLogFilePath, - ) - configContent += vpp.generateCpuConfig() - for _, c := range vpp.additionalConfig { - configContent += c.toString() - } - startupFileName := vpp.getEtcDir() + "/startup.conf" - vpp.container.createFile(startupFileName, configContent) - - // create wrapper script for vppctl with proper CLI socket path - cliContent := "#!/usr/bin/bash\nvppctl -s " + vpp.getRunDir() + "/cli.sock" - vppcliFileName := "/usr/bin/vppcli" - vpp.container.createFile(vppcliFileName, cliContent) - vpp.container.exec("chmod 0755 " + vppcliFileName) - - vpp.getSuite().log("starting vpp") - if *isVppDebug { - // default = 3; VPP will timeout while debugging if there are not enough attempts - maxReconnectAttempts = 5000 - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGQUIT) - cont := make(chan bool, 1) - go func() { - <-sig - cont <- true - }() - - vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"") - fmt.Println("run following command in different terminal:") - fmt.Println("docker exec -it " + vpp.container.name + " gdb -ex \"attach $(docker exec " + vpp.container.name + " pidof vpp)\"") - fmt.Println("Afterwards press CTRL+\\ to continue") - <-cont - fmt.Println("continuing...") - } else { - // Start VPP - vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"") - } - - vpp.getSuite().log("connecting to vpp") - // Connect to VPP and store the connection - sockAddress := vpp.container.getHostWorkDir() + defaultApiSocketFilePath - conn, connEv, err := govpp.AsyncConnect( - sockAddress, - maxReconnectAttempts, - core.DefaultReconnectInterval) - if err != nil { - vpp.getSuite().log("async connect error: " + fmt.Sprint(err)) - return err - } - vpp.connection = conn - - // ... wait for Connected event - e := <-connEv - if e.State != core.Connected { - vpp.getSuite().log("connecting to VPP failed: " + fmt.Sprint(e.Error)) - } - - ch, err := conn.NewStream( - context.Background(), - core.WithRequestSize(50), - core.WithReplySize(50), - core.WithReplyTimeout(time.Second*5)) - if err != nil { - vpp.getSuite().log("creating stream failed: " + fmt.Sprint(err)) - return err - } - vpp.apiStream = ch - - return nil -} - -func (vpp *VppInstance) vppctl(command string, arguments ...any) string { - vppCliCommand := fmt.Sprintf(command, arguments...) - containerExecCommand := fmt.Sprintf("docker exec --detach=false %[1]s vppctl -s %[2]s %[3]s", - vpp.container.name, vpp.getCliSocket(), vppCliCommand) - vpp.getSuite().log(containerExecCommand) - output, err := exechelper.CombinedOutput(containerExecCommand) - vpp.getSuite().assertNil(err) - - return string(output) -} - -func (vpp *VppInstance) GetSessionStat(stat string) int { - o := vpp.vppctl("show session stats") - vpp.getSuite().log(o) - for _, line := range strings.Split(o, "\n") { - if strings.Contains(line, stat) { - tokens := strings.Split(strings.TrimSpace(line), " ") - val, err := strconv.Atoi(tokens[0]) - if err != nil { - Fail("failed to parse stat value %s" + fmt.Sprint(err)) - return 0 - } - return val - } - } - return 0 -} - -func (vpp *VppInstance) waitForApp(appName string, timeout int) { - vpp.getSuite().log("waiting for app " + appName) - for i := 0; i < timeout; i++ { - o := vpp.vppctl("show app") - if strings.Contains(o, appName) { - return - } - time.Sleep(1 * time.Second) - } - vpp.getSuite().assertNil(1, "Timeout while waiting for app '%s'", appName) -} - -func (vpp *VppInstance) createAfPacket( - veth *NetInterface, -) (interface_types.InterfaceIndex, error) { - createReq := &af_packet.AfPacketCreateV3{ - Mode: 1, - UseRandomHwAddr: true, - HostIfName: veth.Name(), - Flags: af_packet.AfPacketFlags(11), - } - if veth.hwAddress != (MacAddress{}) { - createReq.UseRandomHwAddr = false - createReq.HwAddr = veth.hwAddress - } - - vpp.getSuite().log("create af-packet interface " + veth.Name()) - if err := vpp.apiStream.SendMsg(createReq); err != nil { - vpp.getSuite().hstFail() - return 0, err - } - replymsg, err := vpp.apiStream.RecvMsg() - if err != nil { - return 0, err - } - reply := replymsg.(*af_packet.AfPacketCreateV3Reply) - err = api.RetvalToVPPApiError(reply.Retval) - if err != nil { - return 0, err - } - - veth.index = reply.SwIfIndex - - // Set to up - upReq := &interfaces.SwInterfaceSetFlags{ - SwIfIndex: veth.index, - Flags: interface_types.IF_STATUS_API_FLAG_ADMIN_UP, - } - - vpp.getSuite().log("set af-packet interface " + veth.Name() + " up") - if err := vpp.apiStream.SendMsg(upReq); err != nil { - return 0, err - } - replymsg, err = vpp.apiStream.RecvMsg() - if err != nil { - return 0, err - } - reply2 := replymsg.(*interfaces.SwInterfaceSetFlagsReply) - if err = api.RetvalToVPPApiError(reply2.Retval); err != nil { - return 0, err - } - - // Add address - if veth.addressWithPrefix() == (AddressWithPrefix{}) { - var err error - var ip4Address string - if ip4Address, err = veth.ip4AddrAllocator.NewIp4InterfaceAddress(veth.peer.networkNumber); err == nil { - veth.ip4Address = ip4Address - } else { - return 0, err - } - } - addressReq := &interfaces.SwInterfaceAddDelAddress{ - IsAdd: true, - SwIfIndex: veth.index, - Prefix: veth.addressWithPrefix(), - } - - vpp.getSuite().log("af-packet interface " + veth.Name() + " add address " + veth.ip4Address) - if err := vpp.apiStream.SendMsg(addressReq); err != nil { - return 0, err - } - replymsg, err = vpp.apiStream.RecvMsg() - if err != nil { - return 0, err - } - reply3 := replymsg.(*interfaces.SwInterfaceAddDelAddressReply) - err = api.RetvalToVPPApiError(reply3.Retval) - if err != nil { - return 0, err - } - - return veth.index, nil -} - -func (vpp *VppInstance) addAppNamespace( - secret uint64, - ifx interface_types.InterfaceIndex, - namespaceId string, -) error { - req := &session.AppNamespaceAddDelV4{ - IsAdd: true, - Secret: secret, - SwIfIndex: ifx, - NamespaceID: namespaceId, - SockName: defaultApiSocketFilePath, - } - - vpp.getSuite().log("add app namespace " + namespaceId) - if err := vpp.apiStream.SendMsg(req); err != nil { - return err - } - replymsg, err := vpp.apiStream.RecvMsg() - if err != nil { - return err - } - reply := replymsg.(*session.AppNamespaceAddDelV4Reply) - if err = api.RetvalToVPPApiError(reply.Retval); err != nil { - return err - } - - sessionReq := &session.SessionEnableDisable{ - IsEnable: true, - } - - vpp.getSuite().log("enable app namespace " + namespaceId) - if err := vpp.apiStream.SendMsg(sessionReq); err != nil { - return err - } - replymsg, err = vpp.apiStream.RecvMsg() - if err != nil { - return err - } - reply2 := replymsg.(*session.SessionEnableDisableReply) - if err = api.RetvalToVPPApiError(reply2.Retval); err != nil { - return err - } - - return nil -} - -func (vpp *VppInstance) createTap( - tap *NetInterface, - tapId ...uint32, -) error { - var id uint32 = 1 - if len(tapId) > 0 { - id = tapId[0] - } - createTapReq := &tapv2.TapCreateV3{ - ID: id, - HostIfNameSet: true, - HostIfName: tap.Name(), - HostIP4PrefixSet: true, - HostIP4Prefix: tap.ip4AddressWithPrefix(), - } - - vpp.getSuite().log("create tap interface " + tap.Name()) - // Create tap interface - if err := vpp.apiStream.SendMsg(createTapReq); err != nil { - return err - } - replymsg, err := vpp.apiStream.RecvMsg() - if err != nil { - return err - } - reply := replymsg.(*tapv2.TapCreateV3Reply) - if err = api.RetvalToVPPApiError(reply.Retval); err != nil { - return err - } - tap.peer.index = reply.SwIfIndex - - // Get name and mac - if err := vpp.apiStream.SendMsg(&interfaces.SwInterfaceDump{ - SwIfIndex: reply.SwIfIndex, - }); err != nil { - return err - } - replymsg, err = vpp.apiStream.RecvMsg() - if err != nil { - return err - } - ifDetails := replymsg.(*interfaces.SwInterfaceDetails) - tap.peer.name = ifDetails.InterfaceName - tap.peer.hwAddress = ifDetails.L2Address - - // Add address - addAddressReq := &interfaces.SwInterfaceAddDelAddress{ - IsAdd: true, - SwIfIndex: reply.SwIfIndex, - Prefix: tap.peer.addressWithPrefix(), - } - - vpp.getSuite().log("tap interface " + tap.Name() + " add address " + tap.peer.ip4Address) - if err := vpp.apiStream.SendMsg(addAddressReq); err != nil { - return err - } - replymsg, err = vpp.apiStream.RecvMsg() - if err != nil { - return err - } - reply2 := replymsg.(*interfaces.SwInterfaceAddDelAddressReply) - if err = api.RetvalToVPPApiError(reply2.Retval); err != nil { - return err - } - - // Set interface to up - upReq := &interfaces.SwInterfaceSetFlags{ - SwIfIndex: reply.SwIfIndex, - Flags: interface_types.IF_STATUS_API_FLAG_ADMIN_UP, - } - vpp.getSuite().log("set tap interface " + tap.Name() + " up") - if err := vpp.apiStream.SendMsg(upReq); err != nil { - return err - } - replymsg, err = vpp.apiStream.RecvMsg() - if err != nil { - return err - } - reply3 := replymsg.(*interfaces.SwInterfaceSetFlagsReply) - if err = api.RetvalToVPPApiError(reply3.Retval); err != nil { - return err - } - - // Get host mac - netIntf, err := net.InterfaceByName(tap.Name()) - if err == nil { - tap.hwAddress, _ = ethernet_types.ParseMacAddress(netIntf.HardwareAddr.String()) - } - - return nil -} - -func (vpp *VppInstance) saveLogs() { - logTarget := vpp.container.getLogDirPath() + "vppinstance-" + vpp.container.name + ".log" - logSource := vpp.container.getHostWorkDir() + defaultLogFilePath - cmd := exec.Command("cp", logSource, logTarget) - vpp.getSuite().log(cmd.String()) - cmd.Run() -} - -func (vpp *VppInstance) disconnect() { - vpp.connection.Disconnect() - vpp.apiStream.Close() -} - -func (vpp *VppInstance) generateCpuConfig() string { - var c Stanza - var s string - if len(vpp.cpus) < 1 { - return "" - } - c.newStanza("cpu"). - append(fmt.Sprintf("main-core %d", vpp.cpus[0])) - vpp.getSuite().log(fmt.Sprintf("main-core %d", vpp.cpus[0])) - workers := vpp.cpus[1:] - - if len(workers) > 0 { - for i := 0; i < len(workers); i++ { - if i != 0 { - s = s + ", " - } - s = s + fmt.Sprintf("%d", workers[i]) - } - c.append(fmt.Sprintf("corelist-workers %s", s)) - vpp.getSuite().log("corelist-workers " + s) - } - return c.close().toString() -} -- cgit 1.2.3-korg