diff options
Diffstat (limited to 'extras/hs-test')
60 files changed, 4927 insertions, 0 deletions
diff --git a/extras/hs-test/Makefile b/extras/hs-test/Makefile new file mode 100644 index 00000000000..b809b170e00 --- /dev/null +++ b/extras/hs-test/Makefile @@ -0,0 +1,174 @@ +export HS_ROOT=$(CURDIR) + +ifeq ($(VERBOSE),) +VERBOSE=false +endif + +ifeq ($(PERSIST),) +PERSIST=false +endif + +ifeq ($(UNCONFIGURE),) +UNCONFIGURE=false +endif + +ifeq ($(TEST),) +TEST=all +endif + +ifeq ($(TEST-HS),) +TEST-HS=all +endif + +ifeq ($(DEBUG),) +DEBUG=false +endif + +ifeq ($(CPUS),) +CPUS=1 +endif + +ifeq ($(PARALLEL),) +PARALLEL=1 +endif + +ifeq ($(REPEAT),) +REPEAT=0 +endif + +ifeq ($(VPPSRC),) +VPPSRC=$(shell pwd)/../.. +endif + +ifeq ($(UBUNTU_CODENAME),) +UBUNTU_CODENAME=$(shell grep '^UBUNTU_CODENAME=' /etc/os-release | cut -f2- -d=) +endif + +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 " fixstyle - format .go source files" + @echo " list-tests - list all tests" + @echo + @echo "make build arguments:" + @echo " UBUNTU_VERSION - ubuntu version for docker image" + @echo " HST_EXTENDED_TESTS - build extended tests" + @echo + @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" + @echo " DEBUG=[true|false] - attach VPP to GDB" + @echo " TEST=[test-name] - specific test to run" + @echo " CPUS=[n-cpus] - number of cpus to allocate to VPP and containers" + @echo " VPPSRC=[path-to-vpp-src] - path to vpp source files (for gdb)" + @echo " PARALLEL=[n-cpus] - number of test processes to spawn to run in parallel" + @echo " REPEAT=[n] - repeat tests up to N times or until a failure occurs" + @echo + @echo "List of all tests:" + $(call list_tests) + +.PHONY: list-tests +list-tests: + $(call list_tests) + +.PHONY: build-vpp-release +build-vpp-release: + @make -C ../.. build-release + +.PHONY: build-vpp-debug +build-vpp-debug: + @make -C ../.. build + +.PHONY: build-vpp-gcov +build-vpp-gcov: + @make -C ../.. build-vpp-gcov + +.build.ok: build + @touch .build.ok + +.build_debug.ok: build-debug + @touch .build.ok + +.PHONY: test +test: .deps.ok .build.ok + # '-' ignores the exit status, it is set in compress.sh + # necessary so gmake won't skip executing the bash script + -bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \ + --unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST) --cpus=$(CPUS) \ + --vppsrc=$(VPPSRC) --parallel=$(PARALLEL) --repeat=$(REPEAT) + @bash ./script/compress.sh + +.PHONY: test-debug +test-debug: .deps.ok .build_debug.ok + # '-' ignores the exit status, it is set in compress.sh + # necessary so gmake won't skip executing the bash script + -bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \ + --unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST) --cpus=$(CPUS) \ + --vppsrc=$(VPPSRC) --parallel=$(PARALLEL) --repeat=$(REPEAT) + @bash ./script/compress.sh + +.PHONY: test-cov +test-cov: .deps.ok .build.ok + -bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \ + --unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST-HS) --cpus=$(CPUS) \ + --vppsrc=$(VPPSRC) + @make -C ../.. test-cov-post HS_TEST=1 + @bash ./script/compress.sh + +.PHONY: build-go +build-go: + go build ./tools/http_server + +.PHONY: build +build: .deps.ok build-vpp-release build-go + @rm -f .build.ok + bash ./script/build_hst.sh release + @touch .build.ok + +.PHONY: build-cov +build-cov: .deps.ok build-vpp-gcov build-go + @rm -f .build.vpp + bash ./script/build_hst.sh gcov + @touch .build.vpp + +.PHONY: build-debug +build-debug: .deps.ok build-vpp-debug build-go + @rm -f .build.ok + bash ./script/build_hst.sh debug + @touch .build.ok + +.deps.ok: + @sudo make install-deps + +.PHONY: install-deps +install-deps: + @rm -f .deps.ok + @apt-get update \ + && apt-get install -y apt-transport-https ca-certificates curl software-properties-common \ + apache2-utils wrk bridge-utils + @if [ ! -f /usr/share/keyrings/docker-archive-keyring.gpg ] ; then \ + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; \ + echo "deb [arch=$(ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(UBUNTU_CODENAME) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null ; \ + apt-get update; \ + fi + @touch .deps.ok + +.PHONY: fixstyle +fixstyle: + @gofmt -w . + @go mod tidy diff --git a/extras/hs-test/README.rst b/extras/hs-test/README.rst new file mode 100644 index 00000000000..1dc1039b33f --- /dev/null +++ b/extras/hs-test/README.rst @@ -0,0 +1,319 @@ +Host stack test framework +========================= + +Overview +-------- + +The goal of the Host stack test framework (**hs-test**) is to ease writing and running end-to-end tests for VPP's Host Stack. +End-to-end tests often want multiple VPP instances, network namespaces, different types of interfaces +and to execute external tools or commands. With such requirements the existing VPP test framework is not sufficient. +For this, ``Go`` was chosen as a high level language, allowing rapid development, with ``Docker`` and ``ip`` being the tools for creating required topology. + +`Ginkgo`_ forms the base framework upon which the *hs-test* is built and run. +All tests are technically in a single suite because we are only using ``package main``. We simulate suite behavior by grouping tests by the topology they require. +This allows us to run those mentioned groups in parallel, but not individual tests in parallel. + + +Anatomy of a test case +---------------------- + +**Prerequisites**: + +* Install hs-test dependencies with ``make install-deps`` +* Tests use *hs-test*'s own docker image, so building it before starting tests is a prerequisite. Run ``make build[-debug]`` to do so +* Docker has to be installed and Go has to be in path of both the running user and root +* Root privileges are required to run tests as it uses Linux ``ip`` command for configuring topology + +**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), + TEST=<test-name> 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 +#. 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]) + #. 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 + and clean-up of test topology + +Adding a test case +------------------ + +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``) +#. Implement test behaviour inside the test method. This typically includes the following: + + #. 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 + #. 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. + +#. Create an ``init()`` function and register the test using ``register*SuiteTests(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``. + +:: + + package main + + import ( + "fmt" + ) + + func init(){ + registerMySuiteTest(MyTest) + } + + func MyTest(s *MySuite) { + clientVpp := s.getContainerByName("client-vpp").vppInstance + + serverVethAddress := s.netInterfaces["server-iface"].AddressString() + + result := clientVpp.vppctl("ping " + serverVethAddress) + s.assertNotNil(result) + s.log(result) + } + +Modifying the framework +----------------------- + +**Adding a test suite** + +.. _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 + +#. 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 + + :: + + 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: + + :: + + func registerMySuiteTests(tests ...func(s *MySuite)) { + nginxTests = append(myTests, 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, + 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 + 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 + + :: + + func (s *MySuite) SetupSuite() { + s.HstSuite.SetupSuite() + + // Add custom setup code here + + s.configureNetworkTopology("myTopology") + s.loadContainerTopology("2peerVeth") + } + +#. In suite file, implement ``SetupTest`` method which gets executed before each test. Starting containers and + configuring VPP is usually placed here + + :: + + func (s *MySuite) SetupTest() { + s.HstSuite.setupTest() + s.SetupVolumes() + s.SetupContainers() + } + +#. In order for ``Ginkgo`` to run this suite, we need to create a ``Describe`` container node with setup nodes and an ``It`` subject node. + Place them at the end of the suite file + + * Declare a suite struct variable before anything else + * To use ``BeforeAll()`` and ``AfterAll()``, the container has to be marked as ``Ordered`` + * Because the container is now marked as Ordered, if a test fails, all the subsequent tests are skipped. + To override this behavior, decorate the container node with ``ContinueOnFailure`` + + :: + + 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() + }) + 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)) + } + }) + +#. 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``). + +#. To run certain tests solo, create a new slice that will only contain tests that have to run solo and a new register function. + 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) + }, SpecTimeout(time.Minute*5)) + }) + +#. Next step is to add test cases to the suite. For that, see section `Adding a test case`_ above + +**Adding a topology element** + +Topology configuration exists as ``yaml`` files in the ``extras/hs-test/topo-network`` and +``extras/hs-test/topo-containers`` folders. Processing of a network topology file for a particular test suite +is started by the ``configureNetworkTopology`` method depending on which file's name is passed to it. +Specified file is loaded and converted into internal data structures which represent various elements of the topology. +After parsing the configuration, framework loops over the elements and configures them one by one on the host system. + +These are currently supported types of network elements. + +* ``netns`` - network namespace +* ``veth`` - veth network interface, optionally with target network namespace or IPv4 address +* ``bridge`` - ethernet bridge to connect created interfaces, optionally with target network namespace +* ``tap`` - tap network interface with IP address + +Similarly, container topology is started by ``loadContainerTopology()``, configuration file is processed +so that test suite retains map of defined containers and uses that to start them at the beginning +of each test case and stop containers after the test finishes. Container configuration can specify +also volumes which allow to share data between containers or between host system and containers. + +Supporting a new type of topology element requires adding code to recognize the new element type during loading. +And adding code to set up the element in the host system with some Linux tool, such as *ip*. +This should be implemented in ``netconfig.go`` for network and in ``container.go`` for containers and volumes. + +**Communicating between containers** + +When two VPP instances or other applications, each in its own Docker container, +want to communicate there are typically two ways this can be done within *hs-test*. + +* Network interfaces. Containers are being created with ``-d --network host`` options, + so they are connected with interfaces created in host system +* Shared folders. Containers are being created with ``-v`` option to create shared `volumes`_ between host system and containers + or just between containers + +Host system connects to VPP instances running in containers using a shared folder +where binary API socket is accessible by both sides. + +**Adding an external tool** + +If an external program should be executed as part of a test case, it might be useful to wrap its execution in its own function. +These types of functions are placed in the ``utils.go`` file. If the external program is not available by default in Docker image, +add its installation to ``extras/hs-test/Dockerfile.vpp`` in ``apt-get install`` command. +Alternatively copy the executable from host system to the Docker image, similarly how the VPP executables and libraries are being copied. + +**Skipping tests** + +``HstSuite`` provides several methods that can be called in tests for skipping it conditionally or unconditionally such as: +``skip()``, ``SkipIfMultiWorker()``, ``SkipUnlessExtendedTestsBuilt()``. You can also use Ginkgo's ``Skip()``. +However the tests currently run under test suites which set up topology and containers before actual test is run. For the reason of saving +test run time it is not advisable to use aforementioned skip methods and instead, just don't register the test. + +**Debugging a test** + +It is possible to debug VPP by attaching ``gdb`` before test execution by adding ``DEBUG=true`` like follows: + +:: + + $ make test TEST=LDPreloadIperfVppTest DEBUG=true + ... + run following command in different terminal: + docker exec -it server-vpp2456109 gdb -ex "attach $(docker exec server-vpp2456109 pidof vpp)" + Afterwards press CTRL+\ to continue + +If a test consists of more VPP instances then this is done for each of them. + + +**Eternal dependencies** + +* Linux tools ``ip``, ``brctl`` +* Standalone programs ``wget``, ``iperf3`` - since these are downloaded when Docker image is made, + they are reasonably up-to-date automatically +* Programs in Docker images - ``envoyproxy/envoy-contrib`` and ``nginx`` +* ``http_server`` - homegrown application that listens on specified port and sends a test file in response +* Non-standard Go libraries - see ``extras/hs-test/go.mod`` + +Generally, these will be updated on a per-need basis, for example when a bug is discovered +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 new file mode 100644 index 00000000000..e05ea76b9bb --- /dev/null +++ b/extras/hs-test/address_allocator.go @@ -0,0 +1,98 @@ +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 new file mode 100644 index 00000000000..44d84d5fe65 --- /dev/null +++ b/extras/hs-test/container.go @@ -0,0 +1,383 @@ +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 + CurrentSpecReport().LeafNodeText + 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 --rm" + 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" + } + + c.allocateCpus() + cmd += fmt.Sprintf(" --cpuset-cpus=\"%d-%d\"", c.allocatedCpus[0], c.allocatedCpus[len(c.allocatedCpus)-1]) + 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.pid) + 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 := CurrentSpecReport().LeafNodeText + 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() { + cmd := exec.Command("docker", "inspect", "--format='{{.State.Status}}'", c.name) + if output, _ := cmd.CombinedOutput(); !strings.Contains(string(output), "running") { + return + } + + testLogFilePath := c.getLogDirPath() + "container-" + c.name + ".log" + + cmd = exec.Command("docker", "logs", "--details", "-t", c.name) + output, err := cmd.CombinedOutput() + if err != nil { + Fail("fetching logs error: " + fmt.Sprint(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() + 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 new file mode 100644 index 00000000000..49a7dfb02f8 --- /dev/null +++ b/extras/hs-test/cpu.go @@ -0,0 +1,100 @@ +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/docker/Dockerfile.build b/extras/hs-test/docker/Dockerfile.build new file mode 100644 index 00000000000..8b2652e93fc --- /dev/null +++ b/extras/hs-test/docker/Dockerfile.build @@ -0,0 +1,8 @@ +ARG UBUNTU_VERSION + +FROM ubuntu:${UBUNTU_VERSION} + +RUN apt-get update \ + && apt-get install -y gcc git make autoconf libtool pkg-config cmake ninja-build golang \ + && rm -rf /var/lib/apt/lists/* + diff --git a/extras/hs-test/docker/Dockerfile.curl b/extras/hs-test/docker/Dockerfile.curl new file mode 100644 index 00000000000..81d15e86c82 --- /dev/null +++ b/extras/hs-test/docker/Dockerfile.curl @@ -0,0 +1,7 @@ +FROM hs-test/build + +COPY script/build_curl.sh /build_curl.sh +RUN apt-get update && apt-get install wget +RUN /build_curl.sh + +CMD ["/bin/sh"] diff --git a/extras/hs-test/docker/Dockerfile.nginx b/extras/hs-test/docker/Dockerfile.nginx new file mode 100644 index 00000000000..11ec6af156d --- /dev/null +++ b/extras/hs-test/docker/Dockerfile.nginx @@ -0,0 +1,20 @@ +ARG UBUNTU_VERSION + +FROM ubuntu:${UBUNTU_VERSION} + +RUN apt-get update \ + && apt-get install -y nginx gdb less \ + && rm -rf /var/lib/apt/lists/* + +COPY vpp-data/lib/* /usr/lib/ +COPY resources/nginx/vcl.conf /vcl.conf +COPY resources/nginx/nginx.conf /nginx.conf +COPY script/nginx_ldp.sh /usr/bin/nginx_ldp.sh + +ENV VCL_CONFIG=/vcl.conf +ENV LDP=/usr/lib/libvcl_ldpreload.so +ENV LDP_DEBUG=0 +ENV VCL_DEBUG=0 +ENV LDP_SID_BIT=8 + +ENTRYPOINT ["nginx_ldp.sh", "nginx", "-c", "/nginx.conf"] diff --git a/extras/hs-test/docker/Dockerfile.nginx-http3 b/extras/hs-test/docker/Dockerfile.nginx-http3 new file mode 100644 index 00000000000..5d66a2528a6 --- /dev/null +++ b/extras/hs-test/docker/Dockerfile.nginx-http3 @@ -0,0 +1,24 @@ +FROM hs-test/build + +COPY script/build_boringssl.sh /build_boringssl.sh +RUN git clone https://boringssl.googlesource.com/boringssl +RUN ./build_boringssl.sh + +COPY script/build_nginx.sh /build_nginx.sh +RUN git clone https://github.com/nginx/nginx +RUN ./build_nginx.sh + +COPY vpp-data/lib/* /usr/lib/ +COPY resources/nginx/vcl.conf /vcl.conf +COPY resources/nginx/nginx_http3.conf /nginx.conf +COPY script/nginx_ldp.sh /usr/bin/nginx_ldp.sh + +COPY resources/nginx/html/index.html /usr/share/nginx/index.html + +ENV VCL_CONFIG=/vcl.conf +ENV LDP=/usr/lib/libvcl_ldpreload.so +ENV LDP_DEBUG=0 +ENV VCL_DEBUG=0 +ENV LDP_SID_BIT=8 + +ENTRYPOINT ["nginx_ldp.sh", "/usr/local/nginx/sbin/nginx", "-c", "/nginx.conf"] diff --git a/extras/hs-test/docker/Dockerfile.nginx-server b/extras/hs-test/docker/Dockerfile.nginx-server new file mode 100644 index 00000000000..1971158131b --- /dev/null +++ b/extras/hs-test/docker/Dockerfile.nginx-server @@ -0,0 +1,12 @@ +ARG UBUNTU_VERSION + +FROM ubuntu:${UBUNTU_VERSION} + +RUN apt-get update \ + && apt-get install -y nginx \ + && rm -rf /var/lib/apt/lists/* + +COPY resources/nginx/nginx_server_mirroring.conf /nginx.conf + + +ENTRYPOINT ["nginx", "-c", "/nginx.conf"] diff --git a/extras/hs-test/docker/Dockerfile.vpp b/extras/hs-test/docker/Dockerfile.vpp new file mode 100644 index 00000000000..9d900e86772 --- /dev/null +++ b/extras/hs-test/docker/Dockerfile.vpp @@ -0,0 +1,33 @@ +ARG UBUNTU_VERSION + +FROM ubuntu:${UBUNTU_VERSION} + +RUN apt-get update \ + && apt-get install -y openssl libapr1 libnuma1 libsubunit0 \ + iproute2 libnl-3-dev libnl-route-3-dev python3 iputils-ping \ + vim gdb libunwind-dev \ + && rm -rf /var/lib/apt/lists/* + +ENV DIR=vpp-data/lib/vpp_plugins +COPY \ + $DIR/af_packet_plugin.so \ + $DIR/hs_apps_plugin.so \ + $DIR/http_plugin.so \ + $DIR/unittest_plugin.so \ + $DIR/quic_plugin.so \ + $DIR/http_static_plugin.so \ + $DIR/ping_plugin.so \ + $DIR/nsim_plugin.so \ + $DIR/prom_plugin.so \ + $DIR/tlsopenssl_plugin.so \ + /usr/lib/x86_64-linux-gnu/vpp_plugins/ + +COPY vpp-data/bin/vpp /usr/bin/ +COPY vpp-data/bin/vppctl /usr/bin/ +COPY vpp-data/bin/vpp_echo /usr/bin/ +COPY vpp-data/bin/vcl_* /usr/bin/ +COPY vpp-data/lib/*.so /usr/lib/ + +RUN addgroup vpp + +ENTRYPOINT ["tail", "-f", "/dev/null"] diff --git a/extras/hs-test/echo_test.go b/extras/hs-test/echo_test.go new file mode 100644 index 00000000000..ce852bea3e0 --- /dev/null +++ b/extras/hs-test/echo_test.go @@ -0,0 +1,51 @@ +package main + +func init() { + registerVethTests(EchoBuiltinTest) + registerSoloVethTests(TcpWithLossTest) +} + +func EchoBuiltinTest(s *VethsSuite) { + serverVpp := s.getContainerByName("server-vpp").vppInstance + serverVeth := s.getInterfaceByName(serverInterfaceName) + + serverVpp.vppctl("test echo server " + + " uri tcp://" + serverVeth.ip4AddressString() + "/1234") + + clientVpp := s.getContainerByName("client-vpp").vppInstance + + 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:") +} + +// unstable with multiple workers +func TcpWithLossTest(s *VethsSuite) { + s.SkipIfMultiWorker() + serverVpp := s.getContainerByName("server-vpp").vppInstance + + serverVeth := s.getInterfaceByName(serverInterfaceName) + serverVpp.vppctl("test echo server uri tcp://%s/20022", + serverVeth.ip4AddressString()) + + 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()) + + // Add loss of packets with Network Delay Simulator + 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) + + // 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) +} diff --git a/extras/hs-test/framework_test.go b/extras/hs-test/framework_test.go new file mode 100644 index 00000000000..8cbf936f026 --- /dev/null +++ b/extras/hs-test/framework_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var suiteTimeout time.Duration + +func TestHst(t *testing.T) { + if *isVppDebug { + // 30 minute timeout so that the framework won't timeout while debugging + suiteTimeout = time.Minute * 30 + } else { + suiteTimeout = time.Minute * 5 + } + RegisterFailHandler(Fail) + RunSpecs(t, "HST") +} diff --git a/extras/hs-test/go.mod b/extras/hs-test/go.mod new file mode 100644 index 00000000000..3be9ba20a86 --- /dev/null +++ b/extras/hs-test/go.mod @@ -0,0 +1,33 @@ +module fd.io/hs-test + +go 1.21 + +require ( + github.com/edwarnicke/exechelper v1.0.3 + go.fd.io/govpp v0.10.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect + github.com/onsi/ginkgo/v2 v2.16.0 + github.com/onsi/gomega v1.32.0 + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.3 + github.com/vishvananda/netns v0.0.4 // indirect + golang.org/x/sys v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/extras/hs-test/go.sum b/extras/hs-test/go.sum new file mode 100644 index 00000000000..479b0289814 --- /dev/null +++ b/extras/hs-test/go.sum @@ -0,0 +1,70 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/edwarnicke/exechelper v1.0.3 h1:OY2ocGAITTqnEDvZk0dRQSeMIQvyH0SyL/4ncz+5GeQ= +github.com/edwarnicke/exechelper v1.0.3/go.mod h1:R65OUPKns4bgeHkCmfSHbmqLBU8aHZxTgLmEyUBUk4U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM= +github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go.fd.io/govpp v0.10.0 h1:lL93SbqOILjON2pMvazrlHRekGYTRy0Qmj57RuAkxR0= +go.fd.io/govpp v0.10.0/go.mod h1:5m3bZM9ck+2EGC2O3ASmSSJAaoouyOlVWtiwj5BdCv0= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/extras/hs-test/hst_suite.go b/extras/hs-test/hst_suite.go new file mode 100644 index 00000000000..26dcf4e5a2b --- /dev/null +++ b/extras/hs-test/hst_suite.go @@ -0,0 +1,502 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "time" + + "github.com/edwarnicke/exechelper" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" +) + +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 + pid 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.pid = fmt.Sprint(os.Getpid()) + 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 := CurrentSpecReport().ContainerHierarchyTexts[0] + 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.containers { + container.stop() + } +} + +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 name + s.pid +} + +func (s *HstSuite) getInterfaceByName(name string) *NetInterface { + return s.netInterfaces[name+s.pid] +} + +func (s *HstSuite) getContainerByName(name string) *Container { + return s.containers[name+s.pid] +} + +/* + * 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[name+s.pid] + 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 + CurrentSpecReport().LeafNodeText + 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.pid + 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"] = elem["name"].(string) + s.pid + } + + if peer, ok := elem["peer"].(NetDevConfig); ok { + if peer["name"].(string) != "" { + peer["name"] = peer["name"].(string) + s.pid + } + if _, ok := peer["netns"]; ok { + peer["netns"] = peer["netns"].(string) + s.pid + } + } + + if _, ok := elem["netns"]; ok { + elem["netns"] = elem["netns"].(string) + s.pid + } + + if _, ok := elem["interfaces"]; ok { + interfaceCount := len(elem["interfaces"].([]interface{})) + for i := 0; i < interfaceCount; i++ { + elem["interfaces"].([]interface{})[i] = elem["interfaces"].([]interface{})[i].(string) + s.pid + } + } + + 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 { + 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 := CurrentSpecReport().LeafNodeText + + 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] +} + +// Returns last 4 digits of PID +func (s *HstSuite) getPortFromPid() string { + port := s.pid + for len(port) < 4 { + port += "0" + } + return port[len(port)-4:] +} + +func (s *HstSuite) startServerApp(running chan error, done chan struct{}, env []string) { + cmd := exec.Command("iperf3", "-4", "-s", "-p", s.getPortFromPid()) + 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.getPortFromPid()) + 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.pid}, 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 +} diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go new file mode 100644 index 00000000000..0bb6a43e851 --- /dev/null +++ b/extras/hs-test/http_test.go @@ -0,0 +1,295 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" +) + +func init() { + registerNsTests(HttpTpsTest) + registerVethTests(HttpCliTest, HttpCliConnectErrorTest) + registerNoTopoTests(NginxHttp3Test, NginxAsServerTest, + NginxPerfCpsTest, NginxPerfRpsTest, NginxPerfWrkTest, HeaderServerTest, + HttpStaticMovedTest, HttpStaticNotFoundTest, HttpCliMethodNotAllowedTest, + HttpCliBadRequestTest, HttpStaticPathTraversalTest) + registerNoTopoSoloTests(HttpStaticPromTest) +} + +const wwwRootPath = "/tmp/www_root" + +func HttpTpsTest(s *NsSuite) { + iface := s.getInterfaceByName(clientInterface) + client_ip := iface.ip4AddressString() + port := "8080" + finished := make(chan error, 1) + clientNetns := s.getNetNamespaceByName("cln") + + container := s.getContainerByName("vpp") + + // configure vpp in the container + container.vppInstance.vppctl("http tps uri tcp://0.0.0.0/8080") + + go func() { + defer GinkgoRecover() + s.startWget(finished, client_ip, port, "test_file_10M", clientNetns) + }() + // wait for client + err := <-finished + s.assertNil(err, fmt.Sprint(err)) +} + +func HttpCliTest(s *VethsSuite) { + serverContainer := s.getContainerByName("server-vpp") + clientContainer := s.getContainerByName("client-vpp") + + serverVeth := s.getInterfaceByName(serverInterfaceName) + + serverContainer.vppInstance.vppctl("http cli server") + + uri := "http://" + serverVeth.ip4AddressString() + "/80" + + o := clientContainer.vppInstance.vppctl("http cli client" + + " uri " + uri + " query /show/vlib/graph") + + s.log(o) + s.assertContains(o, "<html>", "<html> not found in the result!") +} + +func HttpCliConnectErrorTest(s *VethsSuite) { + clientContainer := s.getContainerByName("client-vpp") + + serverVeth := s.getInterfaceByName(serverInterfaceName) + + uri := "http://" + serverVeth.ip4AddressString() + "/80" + + o := clientContainer.vppInstance.vppctl("http cli client" + + " uri " + uri + " query /show/vlib/graph") + + 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()) + + vpp := s.getContainerByName("vpp").vppInstance + vpp.waitForApp("nginx-", 5) + serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + + defer func() { os.Remove(query) }() + 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.assertNil(err, fmt.Sprint(err)) + s.assertContains(o, "<http>", "<http> 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")) + time.Sleep(time.Second * 5) + go func() { + defer GinkgoRecover() + s.startWget(finished, serverAddress, "80", query, "") + }() + err := <-finished + 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() + req, err := http.NewRequest("GET", "http://"+serverAddress+":80/../secret_folder/secret_file.txt", nil) + s.assertNil(err, fmt.Sprint(err)) + resp, err := client.Do(req) + s.assertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + 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", "<http><body><p>Hello</p></body></http>") + 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/tmp.aaa", nil) + s.assertNil(err, fmt.Sprint(err)) + resp, err := client.Do(req) + s.assertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + 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")) + + client := newHttpClient() + req, err := http.NewRequest("GET", "http://"+serverAddress+":80/notfound.html", nil) + s.assertNil(err, fmt.Sprint(err)) + resp, err := client.Do(req) + s.assertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + 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") + + client := newHttpClient() + req, err := http.NewRequest("POST", "http://"+serverAddress+":80/test", nil) + s.assertNil(err, fmt.Sprint(err)) + resp, err := client.Do(req) + s.assertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + s.assertEqual(405, resp.StatusCode) + // TODO: need to be fixed in http code + //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") + + client := newHttpClient() + req, err := http.NewRequest("GET", "http://"+serverAddress+":80", nil) + s.assertNil(err, fmt.Sprint(err)) + resp, err := client.Do(req) + s.assertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + s.assertEqual(400, resp.StatusCode) +} + +func HeaderServerTest(s *NoTopoSuite) { + vpp := s.getContainerByName("vpp").vppInstance + serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + vpp.vppctl("http cli server") + + client := newHttpClient() + req, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil) + s.assertNil(err, fmt.Sprint(err)) + resp, err := client.Do(req) + s.assertNil(err, fmt.Sprint(err)) + defer resp.Body.Close() + s.assertEqual("http_cli_server", resp.Header.Get("Server")) +} + +func NginxAsServerTest(s *NoTopoSuite) { + s.skip("Broken in the CI") + query := "return_ok" + finished := make(chan error, 1) + + nginxCont := s.getContainerByName("nginx") + s.assertNil(nginxCont.run()) + + vpp := s.getContainerByName("vpp").vppInstance + vpp.waitForApp("nginx-", 5) + + serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + + defer func() { os.Remove(query) }() + go func() { + defer GinkgoRecover() + s.startWget(finished, serverAddress, "80", query, "") + }() + s.assertNil(<-finished) +} + +func parseString(s, pattern string) string { + temp := strings.Split(s, "\n") + for _, item := range temp { + if strings.Contains(item, pattern) { + return item + } + } + return "" +} + +func runNginxPerf(s *NoTopoSuite, mode, ab_or_wrk string) error { + nRequests := 1000000 + nClients := 1000 + + serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString() + + vpp := s.getContainerByName("vpp").vppInstance + + nginxCont := s.getContainerByName(singleTopoContainerNginx) + s.assertNil(nginxCont.run()) + vpp.waitForApp("nginx-", 5) + + if ab_or_wrk == "ab" { + abCont := s.getContainerByName("ab") + args := fmt.Sprintf("-n %d -c %d", nRequests, nClients) + if mode == "rps" { + args += " -k" + } else if mode != "cps" { + return fmt.Errorf("invalid mode %s; expected cps/rps", mode) + } + // don't exit on socket receive errors + args += " -r" + args += " http://" + serverAddress + ":80/64B.json" + 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) + } else { + 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() + rps := parseString(o, "requests") + s.log(rps) + s.log(err) + s.assertNil(err, "err: '%s', output: '%s'", err, o) + } + return nil +} + +// unstable with multiple workers +func NginxPerfCpsTest(s *NoTopoSuite) { + s.skip("Broken in the CI") + s.SkipIfMultiWorker() + s.assertNil(runNginxPerf(s, "cps", "ab")) +} + +func NginxPerfRpsTest(s *NoTopoSuite) { + s.skip("Broken in the CI") + s.assertNil(runNginxPerf(s, "rps", "ab")) +} + +func NginxPerfWrkTest(s *NoTopoSuite) { + s.skip("Broken in the CI") + s.assertNil(runNginxPerf(s, "", "wrk")) +} diff --git a/extras/hs-test/ldp_test.go b/extras/hs-test/ldp_test.go new file mode 100644 index 00000000000..24d2de39485 --- /dev/null +++ b/extras/hs-test/ldp_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "os" + + . "github.com/onsi/ginkgo/v2" +) + +func init() { + registerVethTests(LDPreloadIperfVppTest) +} + +func LDPreloadIperfVppTest(s *VethsSuite) { + var clnVclConf, srvVclConf Stanza + + serverContainer := s.getContainerByName("server-vpp") + serverVclFileName := serverContainer.getHostWorkDir() + "/vcl_srv.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" + + stopServerCh := make(chan struct{}, 1) + srvCh := make(chan error, 1) + clnCh := make(chan error) + + s.log("starting VPPs") + + clientAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default", + 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)) + + serverAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default", + 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") + + srvEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+serverVclFileName) + go func() { + defer GinkgoRecover() + s.startServerApp(srvCh, stopServerCh, srvEnv) + }() + + err = <-srvCh + s.assertNil(err, fmt.Sprint(err)) + + 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() + go func() { + defer GinkgoRecover() + s.startClientApp(serverVethAddress, clnEnv, clnCh, clnRes) + }() + s.log(<-clnRes) + + // wait for client's result + err = <-clnCh + 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 new file mode 100644 index 00000000000..e323f7fb721 --- /dev/null +++ b/extras/hs-test/linux_iperf_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" +) + +func init() { + registerTapTests(LinuxIperfTest) +} + +func LinuxIperfTest(s *TapSuite) { + clnCh := make(chan error) + stopServerCh := make(chan struct{}) + srvCh := make(chan error, 1) + clnRes := make(chan string, 1) + defer func() { + stopServerCh <- struct{}{} + }() + + go func() { + defer GinkgoRecover() + s.startServerApp(srvCh, stopServerCh, nil) + }() + err := <-srvCh + s.assertNil(err, fmt.Sprint(err)) + s.log("server running") + + ipAddress := s.getInterfaceByName(tapInterfaceName).ip4AddressString() + go func() { + defer GinkgoRecover() + s.startClientApp(ipAddress, nil, clnCh, clnRes) + }() + s.log("client running") + s.log(<-clnRes) + err = <-clnCh + 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 new file mode 100644 index 00000000000..daf3fc913e0 --- /dev/null +++ b/extras/hs-test/mirroring_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/edwarnicke/exechelper" +) + +func init() { + // registerNginxTests(MirroringTest) +} + +// broken when CPUS > 1 +func MirroringTest(s *NginxSuite) { + s.skip("Broken in the CI") + s.SkipIfMultiWorker() + proxyAddress := s.getInterfaceByName(mirroringClientInterfaceName).peer.ip4AddressString() + + path := "/64B.json" + + testCommand := "wrk -c 20 -t 10 -d 10 http://" + proxyAddress + ":80" + path + s.log(testCommand) + o, _ := exechelper.Output(testCommand) + s.log(string(o)) + s.assertNotEmpty(o) + + 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 new file mode 100644 index 00000000000..c76a0fda5f5 --- /dev/null +++ b/extras/hs-test/netconfig.go @@ -0,0 +1,383 @@ +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 new file mode 100644 index 00000000000..ac5f94c8535 --- /dev/null +++ b/extras/hs-test/proxy_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "os" + + "github.com/edwarnicke/exechelper" + . "github.com/onsi/ginkgo/v2" +) + +func init() { + registerNsTests(VppProxyHttpTcpTest, VppProxyHttpTlsTest, EnvoyProxyHttpTcpTest) +} + +func testProxyHttpTcp(s *NsSuite, proto string) error { + var outputFile string = "test" + s.pid + ".data" + var srcFilePid string = "httpTestFile" + s.pid + const srcFileNoPid = "httpTestFile" + const fileSize string = "10M" + stopServer := make(chan struct{}, 1) + serverRunning := make(chan struct{}, 1) + 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, srcFilePid)) + s.assertNil(err, "failed to run truncate command: "+fmt.Sprint(err)) + defer func() { os.Remove(srcFilePid) }() + + s.log("test file created...") + + go func() { + defer GinkgoRecover() + s.startHttpServer(serverRunning, stopServer, ":666", serverNetns) + }() + // TODO better error handling and recovery + <-serverRunning + + defer func(chan struct{}) { + stopServer <- struct{}{} + }(stopServer) + + s.log("http server started...") + + 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(), srcFileNoPid) + s.log(c) + _, err = exechelper.CombinedOutput(c) + + defer func() { os.Remove(outputFile) }() + + s.assertNil(err, "failed to run wget: '%s', cmd: %s", err, c) + stopServer <- struct{}{} + + s.assertNil(assertFileSize(outputFile, srcFilePid)) + return nil +} + +func configureVppProxy(s *NsSuite, proto string) { + serverVeth := s.getInterfaceByName(serverInterface) + clientVeth := s.getInterfaceByName(clientInterface) + + 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(), + ) + s.log("proxy configured: " + output) +} + +func VppProxyHttpTcpTest(s *NsSuite) { + proto := "tcp" + configureVppProxy(s, proto) + err := testProxyHttpTcp(s, proto) + 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)) +} + +func configureEnvoyProxy(s *NsSuite) { + envoyContainer := s.getContainerByName("envoy") + err := envoyContainer.create() + s.assertNil(err, "Error creating envoy container: %s", err) + + serverVeth := s.getInterfaceByName(serverInterface) + address := struct { + Server string + }{ + Server: serverVeth.peer.ip4AddressString(), + } + envoyContainer.createConfig( + "/etc/envoy/envoy.yaml", + "resources/envoy/proxy.yaml", + address, + ) + s.assertNil(envoyContainer.start()) +} + +func EnvoyProxyHttpTcpTest(s *NsSuite) { + configureEnvoyProxy(s) + err := testProxyHttpTcp(s, "tcp") + s.assertNil(err, fmt.Sprint(err)) +} diff --git a/extras/hs-test/raw_session_test.go b/extras/hs-test/raw_session_test.go new file mode 100644 index 00000000000..5c66df0b1ce --- /dev/null +++ b/extras/hs-test/raw_session_test.go @@ -0,0 +1,40 @@ +package main + +func init() { + registerVethTests(VppEchoQuicTest, VppEchoTcpTest) +} + +func VppEchoQuicTest(s *VethsSuite) { + s.testVppEcho("quic") +} + +// TODO: udp echo currently broken in vpp +func VppEchoUdpTest(s *VethsSuite) { + s.testVppEcho("udp") +} + +func VppEchoTcpTest(s *VethsSuite) { + s.testVppEcho("tcp") +} + +func (s *VethsSuite) testVppEcho(proto string) { + serverVethAddress := s.getInterfaceByName(serverInterfaceName).ip4AddressString() + uri := proto + "://" + serverVethAddress + "/12344" + + echoSrvContainer := s.getContainerByName("server-app") + serverCommand := "vpp_echo server TX=RX" + + " socket-name " + echoSrvContainer.getContainerWorkDir() + "/var/run/app_ns_sockets/default" + + " use-app-socket-api" + + " uri " + uri + s.log(serverCommand) + echoSrvContainer.execServer(serverCommand) + + echoClnContainer := s.getContainerByName("client-app") + + clientCommand := "vpp_echo client" + + " 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) +} diff --git a/extras/hs-test/resources/cert/localhost.crt b/extras/hs-test/resources/cert/localhost.crt new file mode 100644 index 00000000000..b21fb48906e --- /dev/null +++ b/extras/hs-test/resources/cert/localhost.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIUF116CAipHqQBCyAEvNesV0u4u0swDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCU0sxEDAOBgNVBAgMB1ZwcExhbmQxITAfBgNVBAoMGElu +dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MjkxMDI0MjhaFw0yNDA1Mjgx +MDI0MjhaMEIxCzAJBgNVBAYTAlNLMRAwDgYDVQQIDAdWcHBMYW5kMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCy40rDzrrHPGIyhP24hOBQefEgKD5uUGgSUyJTCur4yB/r2PGt +LlfipKwDmNArmZuFOgKh8evipu2jYaxf4GHQmi7PGLddvPkqo5FWtVW8oAVJMcp+ +fwfs7OgkqtYD6Y7qjmjfXb9+rMpPN8WZ7cKbJwZpF3lf8GGaLqRmPiQg2j8qzcVy +nz8cIwBZP8BJVclA9GIagijY7Zcmz0HnTPrPoLMeyLJOTqPMfkUYA2H2eHeISkQP +BeoFoiwCI5eM35UiWiLyiv9Kojn4BHx6MLrfKBjV13WtcRMgYm5VftsWOZ92lmHm +bpj9mGgtd84JWtWxs33oG4mNRSAeujf9AE5VAgMBAAGjUzBRMB0GA1UdDgQWBBTj +s+A5M/Cao+0Phgg6xFBKIPxLqjAfBgNVHSMEGDAWgBTjs+A5M/Cao+0Phgg6xFBK +IPxLqjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB3EcGDby5u +cEGjgAFR18kH4ztnYUdZUrPI72sOFfjRLtJpx00n759SBawqNW1Y2a1QRd+GgUBK +YpYd2gzWYFjf/4c5BN4SrjeZGnQ8N0YomqqGKvOQO0YdYK4i/lWJjLRaLiVBn9EX +Z+odYhGqQgoAJHnm5Mmqhx9ts8qxZLbdsh+T93mKvj+/yuai2Is+AJfLgZpdKPQN +bCoZemRm+nghRvEP8aX/469wiz7SOLqUzxrTOtXV48wTU5LWLDCs1lF9ZdGHR9/r +vj8unnEHIZiH3ZjN7OgaAoNHZE26Ywbmllc/a0vPw8iHdrLe7+Wtp4zXe2rcxhW7 +b+X1/yRCZ+Wg +-----END CERTIFICATE----- diff --git a/extras/hs-test/resources/cert/localhost.key b/extras/hs-test/resources/cert/localhost.key new file mode 100644 index 00000000000..2d65db50900 --- /dev/null +++ b/extras/hs-test/resources/cert/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy40rDzrrHPGIy +hP24hOBQefEgKD5uUGgSUyJTCur4yB/r2PGtLlfipKwDmNArmZuFOgKh8evipu2j +Yaxf4GHQmi7PGLddvPkqo5FWtVW8oAVJMcp+fwfs7OgkqtYD6Y7qjmjfXb9+rMpP +N8WZ7cKbJwZpF3lf8GGaLqRmPiQg2j8qzcVynz8cIwBZP8BJVclA9GIagijY7Zcm +z0HnTPrPoLMeyLJOTqPMfkUYA2H2eHeISkQPBeoFoiwCI5eM35UiWiLyiv9Kojn4 +BHx6MLrfKBjV13WtcRMgYm5VftsWOZ92lmHmbpj9mGgtd84JWtWxs33oG4mNRSAe +ujf9AE5VAgMBAAECggEANwiZ/bdh2t2G0Ef9zoCCif+Z4OzAmCuAePK+gpG/TB41 +Q9eQMlkpjH5gtRKUKHWvVMNOAAhvK2FzhmoMH8rmDMkCUZAnCV2TwjxkACr1X3xT +Y/s/cr8d7xPLL0ynXrjB0QNS3DT5Lr111/0ue3acAiN1Y2tnWc6YGFj1FsdTUg+O +zRysrpNUp3LAK+MXIhAXMCGKOOLxpjeyrcnUokH0I8e06of1AfAHX8jTn65MG5Ex +n9wBYPl+u2J3SjILHoqBKjcSoNILUfBN9mQGeXhoqCzwcnygDtOxIu9xgu2nCcJr +C1R/WXoQ8Jr6wa1n0aEVXDJeOEK9kKXLTt2/I4HB2QKBgQDemLy+o2/tbFwlU2Xy +8/tZa30kfLCAZ+kq+lE3Kkfqt3pPYzH+lfO7u/UWtavKRQRdsKsNKbpe/EdGq7c4 +YN3L1KG5JiIo3TxilUPilYacGHklfMMbEK7cs8Jebsl6rL7BgnKuqlXGY0HEEx8L +XqIKN1RdzL04WLOiA8qDGwYp7wKBgQDNu3DECCTkTa+mZdNDRntoffkgyd0AnwPA +PEf43BHORpKcfGwFIrf8QWRXcLdh72Yrc9o3D53GCq+NSYGPL2OiY+/3HoAy1mH1 +EBgS08qfkZBKr6+VGjWuVAlD2m2jW+AhGXMS+Lu4yzK3V+0EzlAu4WZVBUngg1// +6ZtyvXLf+wKBgQCozmO0nvUutFJc7BYQXP5sHZvVo8mmVyb4NMSKdUH8ug/DTJKJ +YuZnpG6FPlh9GEHrWyMc5Fw11FOpQGe+FZeeEC5k3ophOwWkLVZB6useTWDyEN9V +Ex3IuXnZa2LX6VDwJyEZXIuX24XwUB/m22k/Hh6Y079bj8kKQJ2/NytBeQKBgQCZ +RGMmJ8sUKqwJEyLoo8GcfvzyaHC03cI1nLMhuxGo0vq2ihsPWGYpD65pVhfIZkl/ +ZbfT/VZVC/DtGS3kNjHL8Rf8ykRHm18u6uaEYDQ73H3apjfwpK4JSaH9YuT7Jp87 +CXKpV5TCft8xp9d0FR+3TUSnYmE/WaBTTv335RuHsQKBgCFLyxzs0hM/MhCLHJ6b +AqyNPz36Xcwsgit1Svhwm1IC6FqkSJl3cRKhp1AP5w6ktUfUGNpF/TYI3x2jCg/m +c0nwmqi/3Cha64XKJcI4iT2+lyuE8jXovMdNiJEEKCDalpyYJbhzRaLsoSFSbiD1 +mFDl8/aNVaQKDDboSuj9AkKs +-----END PRIVATE KEY----- diff --git a/extras/hs-test/resources/envoy/envoy.log b/extras/hs-test/resources/envoy/envoy.log new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/extras/hs-test/resources/envoy/envoy.log diff --git a/extras/hs-test/resources/envoy/proxy.yaml b/extras/hs-test/resources/envoy/proxy.yaml new file mode 100644 index 00000000000..77da80d934d --- /dev/null +++ b/extras/hs-test/resources/envoy/proxy.yaml @@ -0,0 +1,53 @@ +admin: + access_log_path: /tmp/envoy.log + address: + socket_address: + address: 0.0.0.0 + port_value: 8081 +static_resources: + listeners: + # define a reverse proxy on :10001 that always uses :80 as an origin. + - address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 555 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: proxy_service + http_filters: + - name: envoy.filters.http.router + clusters: + - name: proxy_service + connect_timeout: 0.25s + type: LOGICAL_DNS + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: proxy_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + # following address will be generated by Addresser during test run + address: {{.Server}} + port_value: 666 +bootstrap_extensions: + - name: envoy.extensions.vcl.vcl_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.vcl.v3alpha.VclSocketInterface +default_socket_interface: "envoy.extensions.vcl.vcl_socket_interface" diff --git a/extras/hs-test/resources/envoy/vcl.conf b/extras/hs-test/resources/envoy/vcl.conf new file mode 100644 index 00000000000..164435a6ae5 --- /dev/null +++ b/extras/hs-test/resources/envoy/vcl.conf @@ -0,0 +1,7 @@ +vcl { + rx-fifo-size 400000 + tx-fifo-size 400000 + app-scope-global + use-mq-eventfd + app-socket-api /tmp/vpp-envoy/var/run/app_ns_sockets/default +} diff --git a/extras/hs-test/resources/nginx/html/index.html b/extras/hs-test/resources/nginx/html/index.html new file mode 100644 index 00000000000..6b7c97d7542 --- /dev/null +++ b/extras/hs-test/resources/nginx/html/index.html @@ -0,0 +1,6 @@ +<http> + <title>nginx docker with quic</title> +<body> + <p>Greetings!</p> +</body> +</http> diff --git a/extras/hs-test/resources/nginx/nginx.conf b/extras/hs-test/resources/nginx/nginx.conf new file mode 100644 index 00000000000..99073aab1ab --- /dev/null +++ b/extras/hs-test/resources/nginx/nginx.conf @@ -0,0 +1,30 @@ +master_process on; +worker_rlimit_nofile 10240; +worker_processes 2; +daemon off; + +events { + use epoll; + worker_connections 10240; + accept_mutex off; + multi_accept off; +} + +http { + keepalive_timeout 300s; + keepalive_requests 1000000; + sendfile on; + server { + listen 80; + root /usr/share/nginx; + index index.html index.htm; + location /return_ok + { + return 200 ''; + } + location /64B.json + { + return 200 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; + } + } +} diff --git a/extras/hs-test/resources/nginx/nginx_http3.conf b/extras/hs-test/resources/nginx/nginx_http3.conf new file mode 100644 index 00000000000..2a01f714111 --- /dev/null +++ b/extras/hs-test/resources/nginx/nginx_http3.conf @@ -0,0 +1,26 @@ +master_process on; +worker_processes 2; +daemon off; + +events { + use epoll; + accept_mutex off; + multi_accept off; +} + +http { + quic_gso on; + quic_retry on; + + access_log logs/access.log; + keepalive_timeout 300s; + sendfile on; + server { + listen 0.0.0.0:8443 quic; + #listen 0.0.0.0:8443 ssl; + root /usr/share/nginx; + ssl_certificate /etc/nginx/ssl/localhost.crt; + ssl_certificate_key /etc/nginx/ssl/localhost.key; + index index.html index.htm; + } +} diff --git a/extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf b/extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf new file mode 100644 index 00000000000..56debf5c290 --- /dev/null +++ b/extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf @@ -0,0 +1,84 @@ +master_process on; +worker_processes 4; +worker_rlimit_nofile 102400; +daemon off; + +error_log /tmp/nginx/error.log; + +events { + use epoll; + worker_connections 102400; + accept_mutex off; +} + +http { + include mime.types; + default_type application/octet-stream; + + access_log off; + + keepalive_timeout 300; + keepalive_requests 1000000; + + proxy_connect_timeout 300; + large_client_header_buffers 4 512k; + client_max_body_size 3000m; + client_header_buffer_size 2048m; + client_body_buffer_size 1024m; + proxy_buffers 16 10240k; + proxy_buffer_size 10240k; + + gzip on; + + upstream bk { + server {{.Server}}:8091; + keepalive 30000; + } + upstream bk1 { + server {{.Server}}:8092; + keepalive 30000; + } + upstream bk2 { + server {{.Server}}:8093; + keepalive 30000; + } + + server { + listen 80; + server_name {{.Proxy}}; + + server_tokens off; + + proxy_redirect off; + + location / { + root html; + index index.html index.htm; + proxy_pass http://bk; + proxy_set_header Connection ""; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Host $host:$server_port; + chunked_transfer_encoding on; + proxy_http_version 1.1; + mirror /mimic1; + mirror /mimic2; + mirror_request_body on; + } + location /mimic1 { + proxy_pass http://bk1$request_uri; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Connection ""; + chunked_transfer_encoding on; + proxy_http_version 1.1; + proxy_set_header Host $host:$server_port; + } + location /mimic2 { + proxy_pass http://bk2$request_uri; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Host $host:$server_port; + proxy_set_header Connection ""; + proxy_http_version 1.1; + chunked_transfer_encoding on; + } + } +} diff --git a/extras/hs-test/resources/nginx/nginx_server_mirroring.conf b/extras/hs-test/resources/nginx/nginx_server_mirroring.conf new file mode 100644 index 00000000000..4056801ea13 --- /dev/null +++ b/extras/hs-test/resources/nginx/nginx_server_mirroring.conf @@ -0,0 +1,32 @@ +master_process on; +worker_rlimit_nofile 10240; +worker_processes 2; +daemon off; + +events { + use epoll; + worker_connections 10240; + accept_mutex off; + multi_accept off; +} + +http { + keepalive_timeout 300s; + keepalive_requests 1000000; + sendfile on; + server { + listen 8091; + listen 8092; + listen 8093; + root /usr/share/nginx; + index index.html index.htm; + location /return_ok + { + return 200 ''; + } + location /64B.json + { + return 200 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; + } + } +} diff --git a/extras/hs-test/resources/nginx/vcl.conf b/extras/hs-test/resources/nginx/vcl.conf new file mode 100644 index 00000000000..cfcd5d2e959 --- /dev/null +++ b/extras/hs-test/resources/nginx/vcl.conf @@ -0,0 +1,11 @@ +vcl { + heapsize 64M + segment-size 4000000000 + add-segment-size 4000000000 + rx-fifo-size 4000000 + tx-fifo-size 4000000 + event-queue-size 100000 + + use-mq-eventfd + app-socket-api /tmp/nginx/var/run/app_ns_sockets/default +} diff --git a/extras/hs-test/script/build_boringssl.sh b/extras/hs-test/script/build_boringssl.sh new file mode 100755 index 00000000000..441878a77ca --- /dev/null +++ b/extras/hs-test/script/build_boringssl.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd boringssl +cmake -GNinja -B build +ninja -C build diff --git a/extras/hs-test/script/build_curl.sh b/extras/hs-test/script/build_curl.sh new file mode 100755 index 00000000000..e4258946f9e --- /dev/null +++ b/extras/hs-test/script/build_curl.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +wget https://github.com/stunnel/static-curl/releases/download/8.5.0/curl-static-amd64-8.5.0.tar.xz +tar -xvf ./curl-static-amd64-8.5.0.tar.xz +cp curl /usr/bin/curl
\ No newline at end of file diff --git a/extras/hs-test/script/build_hst.sh b/extras/hs-test/script/build_hst.sh new file mode 100755 index 00000000000..3b4bc28e275 --- /dev/null +++ b/extras/hs-test/script/build_hst.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +if [ $(lsb_release -is) != Ubuntu ]; then + echo "Host stack test framework is supported only on Ubuntu" + exit 1 +fi + +if [ -z $(which ab) ]; then + echo "Host stack test framework requires apache2-utils to be installed" + echo "It is recommended to run 'sudo make install-dep'" + exit 1 +fi + +if [ -z $(which wrk) ]; then + echo "Host stack test framework requires wrk to be installed" + echo "It is recommended to run 'sudo make install-dep'" + exit 1 +fi + +export VPP_WS=../.. + +if [ "$1" == "debug" ]; then + VPP_BUILD_ROOT=${VPP_WS}/build-root/build-vpp_debug-native/vpp +elif [ "$1" == "gcov" ]; then + VPP_BUILD_ROOT=${VPP_WS}/build-root/build-vpp_gcov-native/vpp +else + VPP_BUILD_ROOT=${VPP_WS}/build-root/build-vpp-native/vpp +fi +echo "Taking build objects from ${VPP_BUILD_ROOT}" + +if [ -z "$UBUNTU_VERSION" ] ; then + export UBUNTU_VERSION=$(lsb_release -rs) +fi +echo "Ubuntu version is set to ${UBUNTU_VERSION}" + +export HST_LDPRELOAD=${VPP_BUILD_ROOT}/lib/x86_64-linux-gnu/libvcl_ldpreload.so +echo "HST_LDPRELOAD is set to ${HST_LDPRELOAD}" + +export PATH=${VPP_BUILD_ROOT}/bin:$PATH + +bin=vpp-data/bin +lib=vpp-data/lib + +mkdir -p ${bin} ${lib} || true +rm -rf vpp-data/bin/* || true +rm -rf vpp-data/lib/* || true + +cp ${VPP_BUILD_ROOT}/bin/* ${bin} +res+=$? +cp -r ${VPP_BUILD_ROOT}/lib/x86_64-linux-gnu/* ${lib} +res+=$? +if [ $res -ne 0 ]; then + echo "Failed to copy VPP files. Is VPP built? Try running 'make build' in VPP directory." + exit 1 +fi + +docker_build () { + tag=$1 + dockername=$2 + docker build --build-arg UBUNTU_VERSION \ + --build-arg http_proxy=$HTTP_PROXY \ + --build-arg https_proxy=$HTTP_PROXY \ + --build-arg HTTP_PROXY=$HTTP_PROXY \ + --build-arg HTTPS_PROXY=$HTTP_PROXY \ + -t $tag -f docker/Dockerfile.$dockername . +} + +docker_build hs-test/vpp vpp +docker_build hs-test/nginx-ldp nginx +docker_build hs-test/nginx-server nginx-server +docker_build hs-test/build build +if [ "$HST_EXTENDED_TESTS" = true ] ; then + docker_build hs-test/nginx-http3 nginx-http3 + docker_build hs-test/curl curl +fi + +# cleanup detached images +images=$(docker images --filter "dangling=true" -q --no-trunc) +if [ "$images" != "" ]; then + docker rmi $images +fi diff --git a/extras/hs-test/script/build_nginx.sh b/extras/hs-test/script/build_nginx.sh new file mode 100755 index 00000000000..69d366aab0e --- /dev/null +++ b/extras/hs-test/script/build_nginx.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd nginx +./auto/configure --with-debug --with-http_v3_module --with-cc-opt="-I../boringssl/include" --with-ld-opt="-L../boringssl/build/ssl -L../boringssl/build/crypto" --without-http_rewrite_module --without-http_gzip_module +make +make install diff --git a/extras/hs-test/script/compress.sh b/extras/hs-test/script/compress.sh new file mode 100644 index 00000000000..1f0205c1efb --- /dev/null +++ b/extras/hs-test/script/compress.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# if failed-summary.log is not empty, exit status = 1 +if [ -s "${HS_ROOT}/summary/failed-summary.log" ] +then + if [ -n "${WORKSPACE}" ] + then + echo -n "Copying docker logs..." + dirs=$(jq -r '.[0] | .SpecReports[] | select(.State == "failed") | .LeafNodeText' ${HS_ROOT}/summary/report.json) + for dirName in $dirs; do + logDir=/tmp/hs-test/$dirName + if [ -d "$logDir" ]; then + mkdir -p ${WORKSPACE}/archives/summary + cp -r $logDir ${WORKSPACE}/archives/summary/ + fi + done + echo "Done." + + echo -n "Copying failed test logs into build log archive directory (${WORKSPACE}/archives)... " + mkdir -p ${WORKSPACE}/archives/summary + cp -a ${HS_ROOT}/summary/* ${WORKSPACE}/archives/summary + echo "Done." + + echo -n "Compressing files in ${WORKSPACE}/archives from test runs... " + cd ${WORKSPACE}/archives + find . -type f \( -name "*.json" -o -name "*.log" \) -exec gzip {} \; + echo "Done." + + else + echo "Not compressing files in temporary directories from test runs." + fi + exit 1 +fi diff --git a/extras/hs-test/script/nginx_ldp.sh b/extras/hs-test/script/nginx_ldp.sh new file mode 100755 index 00000000000..4a22e14aaf7 --- /dev/null +++ b/extras/hs-test/script/nginx_ldp.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +LD_PRELOAD=$LDP $@ 2>&1 > /proc/1/fd/1 diff --git a/extras/hs-test/suite_nginx_test.go b/extras/hs-test/suite_nginx_test.go new file mode 100644 index 00000000000..4c6e9dbb309 --- /dev/null +++ b/extras/hs-test/suite_nginx_test.go @@ -0,0 +1,132 @@ +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 = []func(s *NginxSuite){} +var nginxSoloTests = []func(s *NginxSuite){} + +type NginxSuite struct { + HstSuite +} + +func registerNginxTests(tests ...func(s *NginxSuite)) { + nginxTests = append(nginxTests, tests...) +} +func registerNginxSoloTests(tests ...func(s *NginxSuite)) { + nginxSoloTests = append(nginxSoloTests, 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 _, test := range nginxTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 _, test := range nginxSoloTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 new file mode 100644 index 00000000000..260fc1d681d --- /dev/null +++ b/extras/hs-test/suite_no_topo_test.go @@ -0,0 +1,108 @@ +package main + +import ( + "reflect" + "runtime" + "strings" + + . "github.com/onsi/ginkgo/v2" +) + +const ( + singleTopoContainerVpp = "vpp" + singleTopoContainerNginx = "nginx" + tapInterfaceName = "htaphost" +) + +var noTopoTests = []func(s *NoTopoSuite){} +var noTopoSoloTests = []func(s *NoTopoSuite){} + +type NoTopoSuite struct { + HstSuite +} + +func registerNoTopoTests(tests ...func(s *NoTopoSuite)) { + noTopoTests = append(noTopoTests, tests...) +} +func registerNoTopoSoloTests(tests ...func(s *NoTopoSuite)) { + noTopoSoloTests = append(noTopoSoloTests, 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 _, test := range noTopoTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 _, test := range noTopoSoloTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 new file mode 100644 index 00000000000..7bdb90b42ae --- /dev/null +++ b/extras/hs-test/suite_ns_test.go @@ -0,0 +1,117 @@ +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 = []func(s *NsSuite){} +var nsSoloTests = []func(s *NsSuite){} + +type NsSuite struct { + HstSuite +} + +func registerNsTests(tests ...func(s *NsSuite)) { + nsTests = append(nsTests, tests...) +} +func registerNsSoloTests(tests ...func(s *NsSuite)) { + nsSoloTests = append(nsSoloTests, 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 _, test := range nsTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 _, test := range nsSoloTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 new file mode 100644 index 00000000000..cb0653304c3 --- /dev/null +++ b/extras/hs-test/suite_tap_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "reflect" + "runtime" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" +) + +type TapSuite struct { + HstSuite +} + +var tapTests = []func(s *TapSuite){} +var tapSoloTests = []func(s *TapSuite){} + +func registerTapTests(tests ...func(s *TapSuite)) { + tapTests = append(tapTests, tests...) +} +func registerTapSoloTests(tests ...func(s *TapSuite)) { + tapSoloTests = append(tapSoloTests, 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 _, test := range tapTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 _, test := range tapSoloTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 new file mode 100644 index 00000000000..13ef5ef268d --- /dev/null +++ b/extras/hs-test/suite_veth_test.go @@ -0,0 +1,142 @@ +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 = []func(s *VethsSuite){} +var vethSoloTests = []func(s *VethsSuite){} + +type VethsSuite struct { + HstSuite +} + +func registerVethTests(tests ...func(s *VethsSuite)) { + vethTests = append(vethTests, tests...) +} +func registerSoloVethTests(tests ...func(s *VethsSuite)) { + vethSoloTests = append(vethSoloTests, 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 _, test := range vethTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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 _, test := range vethSoloTests { + test := test + pc := reflect.ValueOf(test).Pointer() + funcValue := runtime.FuncForPC(pc) + testName := 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/test b/extras/hs-test/test new file mode 100644 index 00000000000..398e2b39edb --- /dev/null +++ b/extras/hs-test/test @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +source vars + +args= +single_test=0 +persist_set=0 +unconfigure_set=0 +debug_set=0 +vppsrc= +ginkgo_args= +parallel= + +for i in "$@" +do +case "${i}" in + --persist=*) + persist="${i#*=}" + if [ $persist = "true" ]; then + args="$args -persist" + persist_set=1 + fi + ;; + --debug=*) + debug="${i#*=}" + if [ $debug = "true" ]; then + args="$args -debug" + debug_set=1 + fi + ;; + --verbose=*) + verbose="${i#*=}" + if [ $verbose = "true" ]; then + args="$args -verbose" + fi + ;; + --unconfigure=*) + unconfigure="${i#*=}" + if [ $unconfigure = "true" ]; then + args="$args -unconfigure" + unconfigure_set=1 + fi + ;; + --cpus=*) + args="$args -cpus ${i#*=}" + ;; + --vppsrc=*) + args="$args -vppsrc ${i#*=}" + ;; + --test=*) + tc_name="${i#*=}" + if [ $tc_name != "all" ]; then + single_test=1 + ginkgo_args="$ginkgo_args --focus $tc_name -vv" + args="$args -verbose" + else + ginkgo_args="$ginkgo_args -v" + fi + ;; + --parallel=*) + ginkgo_args="$ginkgo_args -procs=${i#*=}" + ;; + --repeat=*) + ginkgo_args="$ginkgo_args --repeat=${i#*=}" + ;; +esac +done + +if [ $single_test -eq 0 ] && [ $persist_set -eq 1 ]; then + echo "persist flag is not supported while running all tests!" + exit 1 +fi + +if [ $unconfigure_set -eq 1 ] && [ $single_test -eq 0 ]; then + echo "a single test has to be specified when unconfigure is set" + exit 1 +fi + +if [ $persist_set -eq 1 ] && [ $unconfigure_set -eq 1 ]; then + echo "setting persist flag and unconfigure flag is not allowed" + exit 1 +fi + +if [ $single_test -eq 0 ] && [ $debug_set -eq 1 ]; then + echo "VPP debug flag is not supperted while running all tests!" + exit 1 +fi + +mkdir -p summary + +sudo -E go run github.com/onsi/ginkgo/v2/ginkgo --no-color --trace --json-report=summary/report.json $ginkgo_args -- $args + +jq -r '.[0] | .SpecReports[] | select((.State == "failed") or (.State == "timedout") or (.State == "panicked")) | select(.Failure != null) | "TestName: \(.LeafNodeText)\nSuite:\n\(.Failure.Location.FileName)\nMessage:\n\(.Failure.Message)\n Full Stack Trace:\n\(.Failure.Location.FullStackTrace)\n"' summary/report.json > summary/failed-summary.log \ + && echo "Summary generated -> summary/failed-summary.log"
\ No newline at end of file diff --git a/extras/hs-test/tools/http_server/http_server.go b/extras/hs-test/tools/http_server/http_server.go new file mode 100644 index 00000000000..d2ab3851643 --- /dev/null +++ b/extras/hs-test/tools/http_server/http_server.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func main() { + if len(os.Args) < 3 { + fmt.Println("arg expected") + os.Exit(1) + } + + http.HandleFunc("/httpTestFile", func(w http.ResponseWriter, r *http.Request) { + file, _ := os.Open("httpTestFile" + os.Args[2]) + defer file.Close() + io.Copy(w, file) + }) + err := http.ListenAndServe(os.Args[1], nil) + if err != nil { + fmt.Printf("%v\n", err) + os.Exit(1) + } +} diff --git a/extras/hs-test/topo-containers/2peerVeth.yaml b/extras/hs-test/topo-containers/2peerVeth.yaml new file mode 100644 index 00000000000..e1591fb9019 --- /dev/null +++ b/extras/hs-test/topo-containers/2peerVeth.yaml @@ -0,0 +1,25 @@ +--- +volumes: + - volume: &server-vol + host-dir: "$HST_VOLUME_DIR/server-share" + container-dir: "/tmp/server-share" + is-default-work-dir: true + - volume: &client-vol + host-dir: "$HST_VOLUME_DIR/client-share" + container-dir: "/tmp/client-share" + is-default-work-dir: true + +containers: + - name: "server-vpp" + volumes: + - <<: *server-vol + - name: "client-vpp" + volumes: + - <<: *client-vol + - name: "server-app" + volumes: + - <<: *server-vol + - name: "client-app" + volumes: + - <<: *client-vol + diff --git a/extras/hs-test/topo-containers/nginxProxyAndServer.yaml b/extras/hs-test/topo-containers/nginxProxyAndServer.yaml new file mode 100644 index 00000000000..cc6b780bafc --- /dev/null +++ b/extras/hs-test/topo-containers/nginxProxyAndServer.yaml @@ -0,0 +1,20 @@ +--- +volumes: + - volume: &shared-vol-proxy + host-dir: "$HST_VOLUME_DIR/shared-vol-proxy" + +containers: + - name: "vpp-proxy" + volumes: + - <<: *shared-vol-proxy + container-dir: "/tmp/vpp" + is-default-work-dir: true + - name: "nginx-proxy" + volumes: + - <<: *shared-vol-proxy + container-dir: "/tmp/nginx" + is-default-work-dir: true + image: "hs-test/nginx-ldp" + is-optional: true + - name: "nginx-server" + image: "hs-test/nginx-server" diff --git a/extras/hs-test/topo-containers/ns.yaml b/extras/hs-test/topo-containers/ns.yaml new file mode 100644 index 00000000000..2298ad232c2 --- /dev/null +++ b/extras/hs-test/topo-containers/ns.yaml @@ -0,0 +1,27 @@ +--- +volumes: + - volume: &shared-vol + host-dir: "$HST_VOLUME_DIR/shared-vol" + +# $HST_DIR will be replaced during runtime by path to hs-test directory +containers: + - name: "vpp" + volumes: + - <<: *shared-vol + container-dir: "/tmp/vpp" + is-default-work-dir: true + - name: "envoy" + volumes: + - <<: *shared-vol + container-dir: "/tmp/vpp-envoy" + is-default-work-dir: true + - host-dir: "$HST_DIR/resources/envoy" + container-dir: "/tmp" + vars: + - name: "ENVOY_UID" + value: "0" + - name: "VCL_CONFIG" + value: "/tmp/vcl.conf" + image: "envoyproxy/envoy-contrib:v1.21-latest" + extra-args: "--concurrency 2 -c /etc/envoy/envoy.yaml" + is-optional: true diff --git a/extras/hs-test/topo-containers/single.yaml b/extras/hs-test/topo-containers/single.yaml new file mode 100644 index 00000000000..b6970c517bd --- /dev/null +++ b/extras/hs-test/topo-containers/single.yaml @@ -0,0 +1,47 @@ +--- +volumes: + - volume: &shared-vol + host-dir: "$HST_VOLUME_DIR/shared-vol" + +containers: + - name: "vpp" + volumes: + - <<: *shared-vol + container-dir: "/tmp/vpp" + is-default-work-dir: true + + - name: "nginx" + volumes: + - <<: *shared-vol + container-dir: "/tmp/nginx" + is-default-work-dir: true + image: "hs-test/nginx-ldp" + is-optional: true + + - name: "nginx-http3" + volumes: + - <<: *shared-vol + container-dir: "/tmp/nginx" + is-default-work-dir: true + - host-dir: $HST_DIR/resources/cert + container-dir: "/etc/nginx/ssl" + image: "hs-test/nginx-http3" + is-optional: true + + - name: "ab" + image: "jordi/ab" + is-optional: true + run-detached: false + + - name: "wrk" + image: "skandyla/wrk" + is-optional: true + run-detached: false + + - name: "curl" + vars: + - name: LD_LIBRARY_PATH + value: "/usr/local/lib" + image: "hs-test/curl" + is-optional: true + run-detached: false
\ No newline at end of file diff --git a/extras/hs-test/topo-network/2peerVeth.yaml b/extras/hs-test/topo-network/2peerVeth.yaml new file mode 100644 index 00000000000..f991d8b3701 --- /dev/null +++ b/extras/hs-test/topo-network/2peerVeth.yaml @@ -0,0 +1,25 @@ +--- +devices: + - name: "hsns" + type: "netns" + + - name: "srv" + type: "veth" + preset-hw-address: "00:00:5e:00:53:01" + peer: + name: "srv_veth" + netns: "hsns" + + - name: "cln" + type: "veth" + peer: + name: "cln_veth" + netns: "hsns" + + - name: "br" + type: "bridge" + netns: "hsns" + interfaces: + - srv_veth + - cln_veth + diff --git a/extras/hs-test/topo-network/2taps.yaml b/extras/hs-test/topo-network/2taps.yaml new file mode 100644 index 00000000000..f5dd8e2adda --- /dev/null +++ b/extras/hs-test/topo-network/2taps.yaml @@ -0,0 +1,18 @@ +--- +devices: + - name: "hstcln" + type: "tap" + ip4: + network: 1 + peer: + name: "" + ip4: + network: 1 + - name: "hstsrv" + type: "tap" + ip4: + network: 2 + peer: + name: "" + ip4: + network: 2 diff --git a/extras/hs-test/topo-network/ns.yaml b/extras/hs-test/topo-network/ns.yaml new file mode 100644 index 00000000000..018c329f77e --- /dev/null +++ b/extras/hs-test/topo-network/ns.yaml @@ -0,0 +1,23 @@ +--- +devices: + - name: "cln" + type: "netns" + + - name: "srv" + type: "netns" + + - name: "hclnvpp" + type: "veth" + peer: + name: "cln" + netns: "cln" + ip4: + network: 1 + + - name: "hsrvvpp" + type: "veth" + peer: + name: "srv" + netns: "srv" + ip4: + network: 2 diff --git a/extras/hs-test/topo-network/tap.yaml b/extras/hs-test/topo-network/tap.yaml new file mode 100644 index 00000000000..acf14958ba3 --- /dev/null +++ b/extras/hs-test/topo-network/tap.yaml @@ -0,0 +1,10 @@ +--- +devices: + - name: "htaphost" + type: "tap" + ip4: + network: 1 + peer: + name: "" + ip4: + network: 1 diff --git a/extras/hs-test/topo.go b/extras/hs-test/topo.go new file mode 100644 index 00000000000..6cb294511b3 --- /dev/null +++ b/extras/hs-test/topo.go @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000000..d250dc64519 --- /dev/null +++ b/extras/hs-test/utils.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "io" + "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 +} diff --git a/extras/hs-test/vars b/extras/hs-test/vars new file mode 100644 index 00000000000..d1ca078fe21 --- /dev/null +++ b/extras/hs-test/vars @@ -0,0 +1,7 @@ +export VPP_WS=../../ + +export HST_LDPRELOAD=${VPP_WS}/build-root/build-vpp-native/vpp/lib/x86_64-linux-gnu/libvcl_ldpreload.so +export PATH=${VPP_WS}/build-root/build-vpp-native/vpp/bin:$PATH + +export UBUNTU_VERSION=$(lsb_release -rs) +export HST_EXTENDED_TESTS=false diff --git a/extras/hs-test/vcl_test.go b/extras/hs-test/vcl_test.go new file mode 100644 index 00000000000..fdcd60ad503 --- /dev/null +++ b/extras/hs-test/vcl_test.go @@ -0,0 +1,156 @@ +package main + +import ( + "fmt" + "time" +) + +func init() { + registerVethTests(XEchoVclClientUdpTest, XEchoVclClientTcpTest, XEchoVclServerUdpTest, + XEchoVclServerTcpTest, VclEchoTcpTest, VclEchoUdpTest, VclRetryAttachTest) +} + +func getVclConfig(c *Container, ns_id_optional ...string) string { + var s Stanza + ns_id := "default" + 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") + 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)) + } + return s.close().toString() +} + +func XEchoVclClientUdpTest(s *VethsSuite) { + s.testXEchoVclClient("udp") +} + +func XEchoVclClientTcpTest(s *VethsSuite) { + s.testXEchoVclClient("tcp") +} + +func (s *VethsSuite) testXEchoVclClient(proto string) { + port := "12345" + 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) + + 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") +} + +func XEchoVclServerUdpTest(s *VethsSuite) { + s.testXEchoVclServer("udp") +} + +func XEchoVclServerTcpTest(s *VethsSuite) { + s.testXEchoVclServer("tcp") +} + +func (s *VethsSuite) testXEchoVclServer(proto string) { + port := "12345" + srvVppCont := s.getContainerByName("server-vpp") + srvAppCont := s.getContainerByName("server-app") + + 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) + + 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") +} + +func (s *VethsSuite) testVclEcho(proto string) { + port := "12345" + 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) + + serverVeth := s.getInterfaceByName(serverInterfaceName) + serverVethAddress := serverVeth.ip4AddressString() + + 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) +} + +func VclEchoTcpTest(s *VethsSuite) { + s.testVclEcho("tcp") +} + +func VclEchoUdpTest(s *VethsSuite) { + s.testVclEcho("udp") +} + +func VclRetryAttachTest(s *VethsSuite) { + s.testRetryAttach("tcp") +} + +func (s *VethsSuite) testRetryAttach(proto string) { + srvVppContainer := s.getTransientContainerByName("server-vpp") + + echoSrvContainer := s.getContainerByName("server-app") + + echoSrvContainer.createFile("/vcl.conf", getVclConfig(echoSrvContainer)) + + 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.") + + serverVeth := s.getInterfaceByName(serverInterfaceName) + serverVethAddress := serverVeth.ip4AddressString() + + 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.") + + // Stop server-vpp-instance, start it again and then run vcl-test-client once more + srvVppContainer.vppInstance.disconnect() + stopVppCommand := "/bin/bash -c 'ps -C vpp_main -o pid= | xargs kill -9'" + srvVppContainer.exec(stopVppCommand) + + s.setupServerVpp() + + 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.") +} diff --git a/extras/hs-test/vppinstance.go b/extras/hs-test/vppinstance.go new file mode 100644 index 00000000000..8a92776894c --- /dev/null +++ b/extras/hs-test/vppinstance.go @@ -0,0 +1,476 @@ +package main + +import ( + "context" + "fmt" + "io" + "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 } +} + +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 + } + + // 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 + } + + 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() +} |