From 8218c12baf58f80455a746dcb40c348db9f648d1 Mon Sep 17 00:00:00 2001 From: Aritra Basu Date: Tue, 25 Nov 2025 18:20:05 -0800 Subject: [PATCH] test: add Ginkgo-based test framework for vpp-manager component Signed-off-by: Aritra Basu --- .golangci.yml | 1 + Makefile | 1 + go.mod | 6 + go.sum | 3 +- pkg/testutils/assertions.go | 54 ++++ pkg/testutils/fixtures.go | 56 +++++ pkg/testutils/helpers.go | 29 +++ pkg/testutils/logger.go | 40 +++ pkg/testutils/vpp_instance.go | 260 ++++++++++++++++++++ vpp-manager/vpp_manager_test.go | 423 ++++++++++++++++++++++++++++++++ 10 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 pkg/testutils/assertions.go create mode 100644 pkg/testutils/fixtures.go create mode 100644 pkg/testutils/helpers.go create mode 100644 pkg/testutils/logger.go create mode 100644 pkg/testutils/vpp_instance.go create mode 100644 vpp-manager/vpp_manager_test.go diff --git a/.golangci.yml b/.golangci.yml index f7b737704..4fdc459d6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ linters: staticcheck: dot-import-whitelist: - "github.com/onsi/ginkgo" + - "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega" checks: - all diff --git a/Makefile b/Makefile index b5f96d19f..850426321 100644 --- a/Makefile +++ b/Makefile @@ -379,6 +379,7 @@ ci-test: builder-image -v /tmp/services-tests-vpp:/tmp/services-tests-vpp \ -v /tmp/felix-tests-vpp:/tmp/felix-tests-vpp \ -v /tmp/prometheus-tests-vpp:/tmp/prometheus-tests-vpp \ + -v /tmp/vpp-test-vpp-manager-test:/tmp/vpp-test-vpp-manager-test \ -v /var/run/docker.sock:/var/run/docker.sock \ --env VPP_BINARY=/usr/bin/vpp \ --env VPP_IMAGE=calicovpp/vpp:$(TAG) \ diff --git a/go.mod b/go.mod index 5acdc0e40..d56f50097 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 github.com/onsi/ginkgo v1.16.5 + github.com/onsi/ginkgo/v2 v2.25.1 github.com/onsi/gomega v1.38.1 github.com/orijtech/prometheus-go-metrics-exporter v0.0.6 github.com/osrg/gobgp/v3 v3.35.0 @@ -35,6 +36,7 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/bennyscetbun/jsongo v1.1.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -56,12 +58,14 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect @@ -98,6 +102,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.opencensus.io v0.24.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect @@ -109,6 +114,7 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect diff --git a/go.sum b/go.sum index e30c56b8b..60f50f7ae 100644 --- a/go.sum +++ b/go.sum @@ -86,7 +86,6 @@ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/Nu github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -219,6 +218,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/projectcalico/api v0.0.0-20251022175904-f2ab03771208 h1:ltTWo/cCc/8pEpzmlzIjt6r2383x2XDPkuBcd/9kE3k= github.com/projectcalico/api v0.0.0-20251022175904-f2ab03771208/go.mod h1:0+91//9pw8kMOyWwG1egVY9a9y43kVTLZfiEF1l5IQc= github.com/projectcalico/calico v0.0.0-20251021213802-b1f3c43f8437 h1:PbLe6D0Yis8ce4F+o/Dmlrv4Her2ZN6ns8rmlZoFoEY= diff --git a/pkg/testutils/assertions.go b/pkg/testutils/assertions.go new file mode 100644 index 000000000..87f32272a --- /dev/null +++ b/pkg/testutils/assertions.go @@ -0,0 +1,54 @@ +// Copyright (C) 2025 Cisco Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + . "github.com/onsi/gomega" + + "github.com/projectcalico/vpp-dataplane/v3/vpplink" +) + +// VppAssertions provides assertion helpers for VPP state +type VppAssertions struct { + vpp *vpplink.VppLink +} + +// NewVppAssertions creates a new VppAssertions helper +func NewVppAssertions(vpp *vpplink.VppLink) *VppAssertions { + return &VppAssertions{vpp: vpp} +} + +// AssertInterfaceIsUp checks that an interface is administratively up +func (a *VppAssertions) AssertInterfaceIsUp(swIfIndex uint32) { + details, err := a.vpp.GetInterfaceDetails(swIfIndex) + Expect(err).ToNot(HaveOccurred(), "failed to dump interfaces") + Expect(details.IsUp).To(BeTrue(), "interface is not admin up") +} + +// AssertMemifInterfaceExists checks that a memif interface exists +func (a *VppAssertions) AssertMemifInterfaceExists(swIfIndex uint32) { + memifs, err := a.vpp.ListMemifInterfaces() + Expect(err).ToNot(HaveOccurred(), "failed to dump memif interfaces") + + found := false + for _, memif := range memifs { + if memif.SwIfIndex == swIfIndex { + found = true + break + } + } + Expect(found).To(BeTrue(), "memif interface %d not found", swIfIndex) +} diff --git a/pkg/testutils/fixtures.go b/pkg/testutils/fixtures.go new file mode 100644 index 000000000..7d4550173 --- /dev/null +++ b/pkg/testutils/fixtures.go @@ -0,0 +1,56 @@ +// Copyright (C) 2025 Cisco Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "fmt" + + "github.com/sirupsen/logrus" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// VppFixture is a Ginkgo fixture to setup and teardown VPP instances in tests +type VppFixture struct { + Instance *VppInstance + Config VppConfig +} + +// Setup initializes the VPP fixture +func (f *VppFixture) Setup(name, image, binary string, log *logrus.Logger) { + if f.Config.WorkersCount == 0 && len(f.Config.EnablePlugins) == 0 { + f.Config = DefaultVppConfig() + } + + f.Instance = NewVppInstance(name, image, binary, f.Config, log) + + By(fmt.Sprintf("Starting VPP instance: %s", name)) + err := f.Instance.Start() + Expect(err).ToNot(HaveOccurred(), "failed to start VPP") + + By(fmt.Sprintf("Connecting to VPP instance: %s", name)) + _, err = f.Instance.Connect() + Expect(err).ToNot(HaveOccurred(), "failed to connect to VPP") +} + +// Teardown cleans up the VPP fixture +func (f *VppFixture) Teardown() { + if f.Instance != nil { + By(fmt.Sprintf("Stopping VPP instance: %s", f.Instance.Name)) + _ = f.Instance.Stop() + } +} diff --git a/pkg/testutils/helpers.go b/pkg/testutils/helpers.go new file mode 100644 index 000000000..50592b7c8 --- /dev/null +++ b/pkg/testutils/helpers.go @@ -0,0 +1,29 @@ +// Copyright (C) 2025 Cisco Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "net" + + . "github.com/onsi/gomega" +) + +// ParseMAC is a helper to parse MAC address +func ParseMAC(macStr string) net.HardwareAddr { + mac, err := net.ParseMAC(macStr) + Expect(err).ToNot(HaveOccurred(), "invalid MAC address") + return mac +} diff --git a/pkg/testutils/logger.go b/pkg/testutils/logger.go new file mode 100644 index 000000000..f4544b2ca --- /dev/null +++ b/pkg/testutils/logger.go @@ -0,0 +1,40 @@ +// Copyright (C) 2025 Cisco Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + . "github.com/onsi/ginkgo/v2" +) + +// LogSection logs a section header. +func LogSection(title string) { + GinkgoWriter.Printf("\n=== %s ===\n", title) +} + +// LogStep logs a test step with arrow prefix. +func LogStep(format string, args ...interface{}) { + GinkgoWriter.Printf(" → "+format+"\n", args...) +} + +// LogSuccess logs a success message with checkmark prefix. +func LogSuccess(format string, args ...interface{}) { + GinkgoWriter.Printf(" ✓ "+format+"\n", args...) +} + +// LogInfo logs an info message. +func LogInfo(format string, args ...interface{}) { + GinkgoWriter.Printf(" "+format+"\n", args...) +} diff --git a/pkg/testutils/vpp_instance.go b/pkg/testutils/vpp_instance.go new file mode 100644 index 000000000..6d4d8cc72 --- /dev/null +++ b/pkg/testutils/vpp_instance.go @@ -0,0 +1,260 @@ +// Copyright (C) 2025 Cisco Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/projectcalico/vpp-dataplane/v3/calico-vpp-agent/common" + "github.com/projectcalico/vpp-dataplane/v3/vpplink" + "github.com/sirupsen/logrus" +) + +// VppInstance represents a VPP instance running in a Docker container +type VppInstance struct { + Name string + ContainerName string + Image string + Binary string + Config VppConfig + ExtraArgs []string + vppLink *vpplink.VppLink + log *logrus.Entry + socketPath string + isRunning bool +} + +// VppConfig holds VPP configuration parameters +type VppConfig struct { + WorkersCount int + BuffersPerNuma int + EnablePlugins []string + DisablePlugins []string + CustomConfig string +} + +// DefaultVppConfig returns a default VPP configuration +func DefaultVppConfig() VppConfig { + return VppConfig{ + WorkersCount: 0, + BuffersPerNuma: 131072, + EnablePlugins: []string{"dispatch_trace_plugin.so"}, + DisablePlugins: []string{"dpdk_plugin.so", "ping_plugin.so"}, + } +} + +// NewVppInstance creates a new VPP instance with the given name and configuration +func NewVppInstance(name string, image string, binary string, config VppConfig, log *logrus.Logger) *VppInstance { + if log == nil { + log = logrus.New() + } + + return &VppInstance{ + Name: name, + ContainerName: fmt.Sprintf("vpp-test-%s", name), + Image: image, + Binary: binary, + Config: config, + log: log.WithFields(logrus.Fields{"vpp-instance": name}), + socketPath: fmt.Sprintf("/tmp/vpp-test-%s", name), + } +} + +// generateVppConfig generates VPP startup configuration +func (v *VppInstance) generateVppConfig() string { + if v.Config.CustomConfig != "" { + return v.Config.CustomConfig + } + + var pluginsConfig strings.Builder + pluginsConfig.WriteString("plugins {\n") + pluginsConfig.WriteString(" plugin default { enable }\n") + for _, plugin := range v.Config.EnablePlugins { + pluginsConfig.WriteString(fmt.Sprintf(" plugin %s { enable }\n", plugin)) + } + for _, plugin := range v.Config.DisablePlugins { + pluginsConfig.WriteString(fmt.Sprintf(" plugin %s { disable }\n", plugin)) + } + pluginsConfig.WriteString("}\n") + + config := fmt.Sprintf(`unix { + nodaemon + full-coredump + cli-listen /var/run/vpp/cli.sock + pidfile /run/vpp/vpp.pid +} +api-trace { on } +cpu { + workers %d +} +socksvr { + socket-name /var/run/vpp/vpp-api-test.sock +} +%s +buffers { + buffers-per-numa %d +}`, v.Config.WorkersCount, pluginsConfig.String(), v.Config.BuffersPerNuma) + + return config +} + +// fixSocketPermissions changes socket file permissions to be accessible by the test process +func (v *VppInstance) fixSocketPermissions() error { + // Wait for socket file to be created + maxRetries := 30 + var socketExists bool + for i := 0; i < maxRetries; i++ { + cmd := exec.Command("docker", "exec", v.ContainerName, "test", "-e", "/var/run/vpp/vpp-api-test.sock") + if err := cmd.Run(); err == nil { + socketExists = true + break + } + if i < maxRetries-1 { + time.Sleep(200 * time.Millisecond) + } + } + + if !socketExists { + v.log.Warnf("Socket file not found after waiting, attempting chmod anyway") + } + + // Change permissions on the socket directory and files to allow access + cmd := exec.Command("docker", "exec", v.ContainerName, "chmod", "-R", "777", "/var/run/vpp/") + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "failed to change socket permissions") + } + + v.log.Debugf("Fixed socket permissions for VPP instance %s", v.Name) + return nil +} + +// Start starts the VPP instance in a Docker container +func (v *VppInstance) Start() error { + v.log.Infof("Starting VPP instance %s", v.Name) + + // Clean up any existing container + _ = v.Stop() + + // Create socket directory + if err := os.MkdirAll(v.socketPath, 0755); err != nil { + return errors.Wrapf(err, "failed to create socket directory %s", v.socketPath) + } + + // Build docker run command + cmdParams := []string{ + "run", "-d", "--privileged", + "--name", v.ContainerName, + "-v", fmt.Sprintf("%s:/var/run/vpp/", v.socketPath), + "-v", "/proc:/proc", + "--sysctl", "net.ipv6.conf.all.disable_ipv6=0", + "--pid=host", + "--env", fmt.Sprintf("LD_LIBRARY_PATH=%s", os.Getenv("LD_LIBRARY_PATH")), + } + + cmdParams = append(cmdParams, v.ExtraArgs...) + cmdParams = append(cmdParams, "--entrypoint", v.Binary, v.Image, v.generateVppConfig()) + + cmd := exec.Command("docker", cmdParams...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrapf(err, "failed to start VPP container %s", v.ContainerName) + } + + v.isRunning = true + v.log.Infof("VPP container %s started successfully", v.ContainerName) + + // Wait a moment for VPP to create socket files and fix permissions + time.Sleep(500 * time.Millisecond) + if err := v.fixSocketPermissions(); err != nil { + v.log.Warnf("Failed to fix socket permissions: %v", err) + } + + return nil +} + +// Stop stops and removes the VPP Docker container +func (v *VppInstance) Stop() error { + if v.vppLink != nil { + v.vppLink.Close() + v.vppLink = nil + } + + cmd := exec.Command("docker", "rm", "-f", "-v", v.ContainerName) + if err := cmd.Run(); err != nil { + // Container might not exist, which is fine + v.log.Debugf("Failed to remove container %s: %v", v.ContainerName, err) + } + + // Clean up socket directory + os.RemoveAll(v.socketPath) + + v.isRunning = false + v.log.Infof("VPP container %s stopped", v.ContainerName) + + return nil +} + +// Connect establishes a connection to the VPP API +func (v *VppInstance) Connect() (*vpplink.VppLink, error) { + if v.vppLink != nil { + return v.vppLink, nil + } + + apiSocket := filepath.Join(v.socketPath, "vpp-api-test.sock") + + vppLink, err := common.CreateVppLinkInRetryLoop( + apiSocket, + v.log, + 20*time.Second, + 100*time.Millisecond, + ) + if err != nil { + return nil, errors.Wrapf(err, "failed to connect to VPP API at %s", apiSocket) + } + + v.vppLink = vppLink + v.log.Infof("Connected to VPP instance %s", v.Name) + + return v.vppLink, nil +} + +// GetVppLink returns the VppLink connection (must call Connect first) +func (v *VppInstance) GetVppLink() *vpplink.VppLink { + return v.vppLink +} + +// RunCli executes a VPP CLI command +func (v *VppInstance) RunCli(command string) (string, error) { + if v.vppLink == nil { + return "", errors.New("VPP link not connected, call Connect() first") + } + return v.vppLink.RunCli(command) +} + +// Exec runs a command inside the VPP container +func (v *VppInstance) Exec(args ...string) ([]byte, error) { + cmdArgs := append([]string{"exec", v.ContainerName}, args...) + cmd := exec.Command("docker", cmdArgs...) + return cmd.CombinedOutput() +} diff --git a/vpp-manager/vpp_manager_test.go b/vpp-manager/vpp_manager_test.go new file mode 100644 index 000000000..9a7cc12d5 --- /dev/null +++ b/vpp-manager/vpp_manager_test.go @@ -0,0 +1,423 @@ +// Copyright (C) 2025 Cisco Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "net" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + + "github.com/projectcalico/vpp-dataplane/v3/config" + "github.com/projectcalico/vpp-dataplane/v3/pkg/testutils" + "github.com/projectcalico/vpp-dataplane/v3/vpplink/types" +) + +const ( + VppImageEnv = "VPP_IMAGE" + VppBinaryEnv = "VPP_BINARY" +) + +var ( + vppImage string + vppBinary string + testLog *logrus.Logger +) + +func TestVppManager(t *testing.T) { + // Skip if VPP_IMAGE is not set (needed for integration tests) + _, hasImage := os.LookupEnv(VppImageEnv) + if !hasImage { + t.Skip("Skipping vpp-manager integration tests (VPP_IMAGE not set)") + } + + RegisterFailHandler(Fail) + RunSpecs(t, "VPP Manager Test Suite") +} + +var _ = BeforeSuite(func() { + var ok bool + vppImage, ok = os.LookupEnv(VppImageEnv) + Expect(ok).To(BeTrue(), "VPP_IMAGE environment variable must be set") + Expect(vppImage).ToNot(BeEmpty(), "VPP_IMAGE cannot be empty") + + vppBinary, ok = os.LookupEnv(VppBinaryEnv) + if !ok || vppBinary == "" { + vppBinary = "/usr/bin/vpp" + } + + testLog = logrus.New() + testLog.SetLevel(logrus.InfoLevel) + + testutils.LogInfo("Using VPP image: %s", vppImage) + testutils.LogInfo("Using VPP binary: %s", vppBinary) +}) + +var _ = Describe("vpp-manager", func() { + var vppFixture *testutils.VppFixture + + BeforeEach(func() { + vppFixture = &testutils.VppFixture{ + Config: testutils.DefaultVppConfig(), + } + vppFixture.Setup("vpp-manager-test", vppImage, vppBinary, testLog) + }) + + AfterEach(func() { + if vppFixture != nil { + vppFixture.Teardown() + } + }) + + Context("VppRunner.allocateStaticVRFs", func() { + // This test calls the VppRunner.allocateStaticVRFs() method + // from vpp_runner.go to verify VRF allocation works correctly + + It("should create punt and pod VRFs for IPv4 and IPv6", func() { + testutils.LogSection("Testing VppRunner.allocateStaticVRFs()") + vpp := vppFixture.Instance.GetVppLink() + + // Create a VppRunner instance with the test VPP connection + runner := &VppRunner{ + params: &config.VppManagerParams{}, + conf: []*config.LinuxInterfaceState{}, + vpp: vpp, + } + + testutils.LogStep("Calling runner.allocateStaticVRFs()") + err := runner.allocateStaticVRFs() + Expect(err).ToNot(HaveOccurred(), "allocateStaticVRFs() failed") + testutils.LogSuccess("allocateStaticVRFs() completed successfully") + + // Verify the VRFs were created in VPP + testutils.LogStep("Verifying VRFs were created") + fibOutput, err := vpp.RunCli("show ip fib summary") + Expect(err).ToNot(HaveOccurred()) + testutils.LogInfo("VRF summary (IPv4):\n%s", fibOutput) + + // Check for punt table by name + Expect(fibOutput).To(ContainSubstring("punt-table-ip4"), + "punt VRF should exist") + testutils.LogSuccess("Punt table (punt-table-ip4) created") + + // Check for pod VRF by name + Expect(fibOutput).To(ContainSubstring("calico-pods-ip4"), + "pod VRF should exist") + testutils.LogSuccess("Pod VRF (calico-pods-ip4) created") + + // Check IPv6 VRFs + fib6Output, err := vpp.RunCli("show ip6 fib summary") + Expect(err).ToNot(HaveOccurred()) + testutils.LogInfo("VRF summary (IPv6):\n%s", fib6Output) + + Expect(fib6Output).To(ContainSubstring("punt-table-ip6"), + "punt VRF (v6) should exist") + testutils.LogSuccess("Punt table v6 (punt-table-ip6) created") + + Expect(fib6Output).To(ContainSubstring("calico-pods-ip6"), + "pod VRF (v6) should exist") + testutils.LogSuccess("Pod VRF v6 (calico-pods-ip6) created") + + testutils.LogSuccess("VppRunner.allocateStaticVRFs() test passed") + }) + }) + + Context("VppRunner.AllocatePhysicalNetworkVRFs", func() { + // This test calls the VppRunner.AllocatePhysicalNetworkVRFs() method + // from vpp_runner.go to verify physical network VRF allocation + + It("should create VRFs for a physical network", func() { + testutils.LogSection("Testing VppRunner.AllocatePhysicalNetworkVRFs()") + vpp := vppFixture.Instance.GetVppLink() + + // Reset physical networks map for clean test + config.Info.PhysicalNets = make(map[string]config.PhysicalNetwork) + + // Create a VppRunner instance with the test VPP connection + runner := &VppRunner{ + params: &config.VppManagerParams{}, + conf: []*config.LinuxInterfaceState{}, + vpp: vpp, + } + + testPhyNetName := "test-physical-net" + + testutils.LogStep("Calling runner.AllocatePhysicalNetworkVRFs(%q)", testPhyNetName) + err := runner.AllocatePhysicalNetworkVRFs(testPhyNetName) + Expect(err).ToNot(HaveOccurred(), "AllocatePhysicalNetworkVRFs() failed") + testutils.LogSuccess("AllocatePhysicalNetworkVRFs() completed successfully") + + // Verify the physical network was registered in config.Info + testutils.LogStep("Verifying physical network registered in config.Info") + phyNet, exists := config.Info.PhysicalNets[testPhyNetName] + Expect(exists).To(BeTrue(), "physical network should be registered") + Expect(phyNet.VrfID).ToNot(BeZero(), "VrfID should be allocated") + Expect(phyNet.PodVrfID).ToNot(BeZero(), "PodVrfID should be allocated") + testutils.LogSuccess("Physical network registered: VrfID=%d, PodVrfID=%d", + phyNet.VrfID, phyNet.PodVrfID) + + // Verify VRFs were created in VPP + testutils.LogStep("Verifying VRFs created in VPP") + fibOutput, err := vpp.RunCli("show ip fib summary") + Expect(err).ToNot(HaveOccurred()) + testutils.LogInfo("VRF summary:\n%s", fibOutput) + + Expect(fibOutput).To(ContainSubstring(fmt.Sprintf("physical-net-%s-ip4", testPhyNetName)), + "physical network VRF should exist in VPP") + testutils.LogSuccess("Physical network VRF (physical-net-%s-ip4) exists", testPhyNetName) + + Expect(fibOutput).To(ContainSubstring(fmt.Sprintf("calico-pods-%s-ip4", testPhyNetName)), + "pod VRF for physical network should exist in VPP") + testutils.LogSuccess("Pod VRF (calico-pods-%s-ip4) exists", testPhyNetName) + + testutils.LogSuccess("VppRunner.AllocatePhysicalNetworkVRFs() test passed") + }) + }) + + Context("VPP Configuration", func() { + It("should verify VPP buffer and plugin configuration", func() { + testutils.LogSection("Testing VPP buffer and plugin configuration") + vpp := vppFixture.Instance.GetVppLink() + + // Check buffer configuration via CLI + testutils.LogStep("Checking VPP buffer allocation via CLI") + bufferOutput, err := vpp.RunCli("show buffers") + Expect(err).ToNot(HaveOccurred(), "failed to get buffer info") + Expect(bufferOutput).ToNot(BeEmpty(), "empty buffer output") + testutils.LogInfo("Buffer summary:\n%s", bufferOutput) + + // Verify buffers are allocated + testutils.LogStep("Verifying buffers are allocated") + Expect(bufferOutput).To(MatchRegexp("(?i)avail"), "no available buffers") + testutils.LogSuccess("VPP buffers allocated correctly") + + // Check plugin configuration + testutils.LogStep("Checking loaded VPP plugins") + pluginOutput, err := vpp.RunCli("show plugins") + Expect(err).ToNot(HaveOccurred(), "failed to get plugin info") + + // Check that dispatch_trace_plugin plugin is loaded + testutils.LogStep("Verifying dispatch_trace_plugin is loaded") + Expect(pluginOutput).To(ContainSubstring("dispatch_trace_plugin.so"), + "dispatch_trace_plugin plugin not loaded") + testutils.LogSuccess("dispatch_trace_plugin.so loaded") + + // Check that DPDK plugin is disabled + testutils.LogStep("Verifying dpdk_plugin is disabled") + Expect(pluginOutput).ToNot(ContainSubstring("dpdk_plugin.so"), + "dpdk plugin should be disabled") + testutils.LogSuccess("dpdk_plugin.so disabled") + + testutils.LogSuccess("VPP configuration test passed") + }) + }) + + Context("Interface Configuration", func() { + // NOTE: We cannot directly test VppRunner.configureVppUplinkInterface() in a container + // environment because it creates a TAP with HostNamespace:"pid:1" that requires access + // to the host's PID 1 network namespace. In a Docker container, pid:1 refers to the + // container's init() process, not the host, causing the Linux-side tap setup to fail! + // + // This test verifies the VPP-side operations that configureVppUplinkInterface() performs: + // - VRF allocation (via allocateStaticVRFs) + // - Interface address configuration (AddInterfaceAddress) + // - Interface state management (InterfaceAdminUp) + It("should configure uplink interface addresses", func() { + testutils.LogSection("Testing uplink interface address configuration") + testutils.LogInfo("NOTE: This only tests a subset of VppRunner.configureVppUplinkInterface()") + vpp := vppFixture.Instance.GetVppLink() + + // Create VRFs using VppRunner method + testutils.LogStep("Setting up prerequisite VRFs via VppRunner.allocateStaticVRFs()") + runner := &VppRunner{ + params: &config.VppManagerParams{}, + conf: []*config.LinuxInterfaceState{}, + vpp: vpp, + } + err := runner.allocateStaticVRFs() + Expect(err).ToNot(HaveOccurred(), "allocateStaticVRFs() failed") + testutils.LogSuccess("Static VRFs created via VppRunner") + + // Create a TAP interface to simulate an uplink interface + testutils.LogStep("Creating TAP interface to simulate uplink") + swIfIndex, err := vpp.CreateTapV2(&types.TapV2{ + GenericVppInterface: types.GenericVppInterface{ + HostInterfaceName: "uplink-tap", + HardwareAddr: testutils.ParseMAC("aa:bb:cc:dd:ee:01"), + }, + Tag: "test-uplink", + Flags: types.TapFlagNone, + HostMtu: 1500, + HostMacAddress: testutils.ParseMAC("aa:bb:cc:dd:ee:02"), + }) + Expect(err).ToNot(HaveOccurred(), "failed to create TAP interface") + testutils.LogSuccess("TAP interface created (swIfIndex: %d)", swIfIndex) + + // Test address configuration on uplink interface + testutils.LogStep("Adding IPv4 address to uplink") + testIPv4 := &net.IPNet{IP: net.ParseIP("192.168.100.1"), Mask: net.CIDRMask(24, 32)} + err = vpp.AddInterfaceAddress(swIfIndex, testIPv4) + Expect(err).ToNot(HaveOccurred(), "failed to add IPv4 address") + + testutils.LogStep("Adding IPv6 address to uplink") + testIPv6 := &net.IPNet{IP: net.ParseIP("fd00:100::1"), Mask: net.CIDRMask(64, 128)} + err = vpp.AddInterfaceAddress(swIfIndex, testIPv6) + Expect(err).ToNot(HaveOccurred(), "failed to add IPv6 address") + + // Verify addresses were added + testutils.LogStep("Verifying IPv4 addresses") + ipv4Addrs, err := vpp.AddrList(swIfIndex, false) + Expect(err).ToNot(HaveOccurred()) + Expect(len(ipv4Addrs)).To(BeNumerically(">=", 1), "should have at least 1 IPv4 address") + testutils.LogSuccess("Found %d IPv4 addresses", len(ipv4Addrs)) + + testutils.LogStep("Verifying IPv6 addresses") + ipv6Addrs, err := vpp.AddrList(swIfIndex, true) + Expect(err).ToNot(HaveOccurred()) + Expect(len(ipv6Addrs)).To(BeNumerically(">=", 1), "should have at least 1 IPv6 address") + testutils.LogSuccess("Found %d IPv6 addresses", len(ipv6Addrs)) + + // Bring interface up + testutils.LogStep("Bringing interface up in VPP") + err = vpp.InterfaceAdminUp(swIfIndex) + Expect(err).ToNot(HaveOccurred()) + testutils.LogSuccess("Interface is up") + + // Verify interface state + testutils.LogStep("Verifying interface details") + details, err := vpp.GetInterfaceDetails(swIfIndex) + Expect(err).ToNot(HaveOccurred()) + Expect(details.IsUp).To(BeTrue(), "interface should be up") + testutils.LogSuccess("Interface %s is up (mtu: %v)", details.Name, details.Mtu) + + testutils.LogSuccess("Uplink interface address configuration test passed") + }) + + It("should create TAP interface", func() { + testutils.LogSection("Testing TAP interface creation") + vpp := vppFixture.Instance.GetVppLink() + + // Create TAP interface + testutils.LogStep("Creating TAP interface") + tapSwIfIndex, err := vpp.CreateTapV2(&types.TapV2{ + GenericVppInterface: types.GenericVppInterface{ + HostInterfaceName: "tap-test", + HardwareAddr: testutils.ParseMAC("aa:bb:cc:dd:ee:01"), + }, + Tag: "host-tap-test", + Flags: types.TapFlagNone, + HostMtu: 1500, + HostMacAddress: testutils.ParseMAC("aa:bb:cc:dd:ee:02"), + }) + Expect(err).ToNot(HaveOccurred(), "failed to create TAP interface") + testutils.LogSuccess("TAP interface created (swIfIndex: %d)", tapSwIfIndex) + + // Set interface up + testutils.LogStep("Setting TAP interface up") + err = vpp.InterfaceAdminUp(tapSwIfIndex) + Expect(err).ToNot(HaveOccurred()) + testutils.LogSuccess("TAP interface is up") + + // Verify interface exists + testutils.LogStep("Verifying TAP interface details") + details, err := vpp.GetInterfaceDetails(tapSwIfIndex) + Expect(err).ToNot(HaveOccurred()) + Expect(details.IsUp).To(BeTrue()) + testutils.LogSuccess("TAP interface %s verified", details.Name) + + testutils.LogSuccess("TAP interface creation test passed") + }) + + It("should create AF_PACKET interface", func() { + testutils.LogSection("Testing AF_PACKET interface creation") + vpp := vppFixture.Instance.GetVppLink() + + // Create a veth pair first + testutils.LogStep("Creating veth pair (veth0 <-> veth1)") + _, err := vppFixture.Instance.Exec("ip", "link", "add", "veth0", "type", "veth", "peer", "name", "veth1") + Expect(err).ToNot(HaveOccurred(), "failed to create veth pair") + + testutils.LogStep("Bringing veth0 up") + _, err = vppFixture.Instance.Exec("ip", "link", "set", "veth0", "up") + Expect(err).ToNot(HaveOccurred(), "failed to bring up veth0") + testutils.LogSuccess("Host veth pair created") + + // Create AF_PACKET interface in VPP + testutils.LogStep("Creating AF_PACKET interface in VPP for veth0") + swIfIndex, err := vpp.CreateAfPacket(&types.AfPacketInterface{ + GenericVppInterface: types.GenericVppInterface{ + HostInterfaceName: "veth0", + }, + }) + Expect(err).ToNot(HaveOccurred(), "failed to create AF_PACKET interface") + Expect(swIfIndex).ToNot(BeZero(), "invalid AF_PACKET interface index") + testutils.LogSuccess("AF_PACKET interface created (swIfIndex: %d)", swIfIndex) + + // Bring interface up + testutils.LogStep("Bringing AF_PACKET interface up in VPP") + err = vpp.InterfaceAdminUp(swIfIndex) + Expect(err).ToNot(HaveOccurred(), "failed to bring interface up") + + // Verify interface exists and is up + testutils.LogStep("Verifying interface is up") + assertions := testutils.NewVppAssertions(vpp) + assertions.AssertInterfaceIsUp(swIfIndex) + testutils.LogSuccess("AF_PACKET interface test passed") + }) + + It("should create memif interface", func() { + testutils.LogSection("Testing memif interface creation") + vpp := vppFixture.Instance.GetVppLink() + + // Create memif socket directory in the VPP container + testutils.LogStep("Creating memif socket directory (/run/vpp)") + _, err := vppFixture.Instance.Exec("mkdir", "-p", "/run/vpp") + Expect(err).ToNot(HaveOccurred(), "failed to create memif socket directory") + + // Register a memif socket filename + testutils.LogStep("Registering memif socket filename in VPP") + socketID, err := vpp.AddMemifSocketFileName("/run/vpp/memif-test.sock") + Expect(err).ToNot(HaveOccurred(), "failed to add memif socket filename") + testutils.LogSuccess("Memif socket registered (socketID: %d)", socketID) + + // Create memif interface + testutils.LogStep("Creating memif interface (master, ethernet mode)") + memif := &types.Memif{ + Role: types.MemifMaster, + Mode: types.MemifModeEthernet, + SocketID: socketID, + QueueSize: 1024, + NumRxQueues: 1, + NumTxQueues: 1, + } + + err = vpp.CreateMemif(memif) + Expect(err).ToNot(HaveOccurred(), "failed to create memif interface") + Expect(memif.SwIfIndex).ToNot(BeZero(), "invalid memif interface index") + testutils.LogSuccess("Memif interface created (swIfIndex: %d)", memif.SwIfIndex) + + // Verify memif interface + testutils.LogStep("Verifying memif interface exists") + assertions := testutils.NewVppAssertions(vpp) + assertions.AssertMemifInterfaceExists(memif.SwIfIndex) + testutils.LogSuccess("Memif interface test passed") + }) + }) +})