diff --git a/.gitignore b/.gitignore index 894a9f8..f143ff9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ clab-* **/*.bak +tests/.venv/ +tests/out/ diff --git a/BUILDING.md b/BUILDING.md index f1ea3de..847f0d6 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,85 +1,20 @@ # Building vpp-containerlab -This docker container creates a VPP instance based on the latest VPP release. It starts up as per -normal, using /etc/vpp/startup.conf (which Containerlab might replace when it starts its -containers). Once started, it'll execute `/etc/vpp/bootstrap.vpp` within the dataplane. There are -two relevant files: +This document describes how to build, test and release the `vpp-containerlab` Docker image. +The image is built natively on two machines and combined into a multi-arch manifest: -1. `clab.vpp` -- generated by `files/init-container.sh`. Its purpose is to bind the `veth` - interfaces that containerlab has added to the container into the VPP dataplane (see below). -1. `vppcfg.vpp` -- generated by `files/init-container.sh`. Its purpose is to read the user - specified `vppcfg.yaml` file and convert it into VPP CLI commands. If no YAML file is - specified, or if it is not syntactically valid, an empty file is generated instead. +- `summer` — amd64, Linux (local machine) +- `jessica-orb` — arm64, OrbStack VM on macOS, reachable via `ssh jessica-orb` -For Containerlab users who wish to have more control over their VPP bootstrap, it's possible to -bind-mount `/etc/vpp/bootstrap.vpp`. +The pipeline sideloads locally-built VPP `.deb` packages rather than pulling from packagecloud, +so VPP must be compiled on both machines before building the image. -## Building +## Prerequisites -To build, this container uses Docker's `buildx`, for which on Debian Bookworm it's required to use -the upstream (docker.com) packages described [[here](https://docs.docker.com/engine/install/debian/)]. -To allow the buildx to build for multi-arch, it's also required to install the Qemu `binfmt` -emulators, with: +### SSH access to jessica-orb -```bash -docker run --privileged --rm tonistiigi/binfmt --install all -``` - -Then, ongoing builds can be cross-platform and take about 1500 seconds on an AMD64 i7-12700T -The buildx invocation will build 'latest' and then tag it with the current VPP package release, -which you can get from `vppcfg show version`, like so: - -```bash -IMG=git.ipng.ch/ipng/vpp-containerlab -ARCH=linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -TAG=latest -docker buildx build --load --platform $ARCH \ - --tag $IMG:$TAG -f docker/Dockerfile docker/ - -TAG=v25.10-release -docker buildx build --load --build-arg REPO=2510 --platform $ARCH \ - --tag $IMG:$TAG -f docker/Dockerfile docker/ -``` - -### Sideloading locally built VPP packages - -Instead of pulling VPP from packagecloud, you can sideload locally built `.deb` packages using -Docker buildx's `--build-context` flag. This is useful for testing unreleased VPP builds or -working around version-specific issues (for example, VPP 25.10 fails to start on kernels that -do not expose NUMA topology via sysfs, such as OrbStack on Apple Silicon; VPP 26.06+ fixes this). - -Point `--build-context vppdebs=` at a directory containing `libvppinfra_*.deb`, -`vpp_*.deb`, and `vpp-plugin-core_*.deb`. If the context is not provided, the build falls back -to packagecloud as normal. The `.deb` files are bind-mounted during the build and never stored -in an image layer. **Note:** the directory must contain `.deb` files for exactly one VPP version; -if multiple versions are present the glob patterns will match ambiguously and the build will fail. - -```bash -# Build from locally compiled VPP packages (e.g. from ~/src/vpp after make pkg-deb): -IMG=git.ipng.ch/ipng/vpp-containerlab -ARCH=linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -VPPDEBS=~/src/vpp/build-root -docker buildx build --load --platform $ARCH \ - --build-context vppdebs=$VPPDEBS \ - --tag $IMG:latest -f docker/Dockerfile docker/ - -# Build from packagecloud as normal (no --build-context needed): -docker buildx build --load --platform $ARCH \ - --tag $IMG:latest -f docker/Dockerfile docker/ -``` - -### Multiarch - -Building a combined `linux/amd64` + `linux/arm64` manifest requires two machines building natively -— one per architecture. The setup below uses `summer` (amd64, Linux) and `jessica` (arm64, macOS -running OrbStack). **VPP must be compiled on each machine before building the Docker image**, because -the sideloader mounts locally built `.deb` files that are architecture-specific. - -#### Setup - -On `jessica`, the Docker daemon runs inside OrbStack's Linux VM. Expose its SSH port so `summer` -can reach it. OrbStack listens on `127.0.0.1:32222`; add a jump-host entry to `~/.ssh/config` on -`summer`: +The Docker daemon on `jessica` runs inside OrbStack's Linux VM. OrbStack listens on +`127.0.0.1:32222`; add a jump-host entry to `~/.ssh/config` on `summer` to reach it: ``` Host jessica-orb @@ -107,118 +42,127 @@ ssh jessica-orb 'uname -m && docker info | head -3' # expected: aarch64 ``` -Create the multiarch builder (run once on `summer`): +### One-time setup + +Install the Robot Framework venv for running tests: ```bash -docker buildx create --name multiarch --driver docker-container --platform linux/amd64 --node summer-amd64 -docker buildx create --append --name multiarch --driver docker-container --platform linux/arm64 --node jessica-arm64 ssh://jessica-orb -docker buildx inspect multiarch --bootstrap +make venv ``` -#### Build +This only needs to be re-run if `tests/requirements.txt` changes. -Build VPP on both machines first (`make pkg-deb` in your VPP source tree on both `summer` and the -OrbStack VM on `jessica`). When sideloading `.deb` files, Docker sends the build context from the -client to every builder node — meaning `summer`'s amd64 debs would be sent to `jessica-orb` for -the arm64 build (wrong arch). The solution is to build each platform separately on its native -machine and combine them into a manifest. +### Before every release + +Build VPP on both machines (`make pkg-deb` in your VPP source tree on both `summer` and the +OrbStack VM on `jessica`), then verify both machines have a consistent set of `.deb` packages: ```bash -IMG=git.ipng.ch/ipng/vpp-containerlab -VPPDEBS=~/src/vpp/build-root - -# Step 1: build amd64 on summer, push with platform tag -docker buildx build --platform linux/amd64 \ - --no-cache --build-context vppdebs=$VPPDEBS \ - --push --tag $IMG:latest-amd64 \ - -f docker/Dockerfile docker/ - -# Step 2: build arm64 natively on jessica-orb, push with platform tag -# (repo and VPP debs must be present on jessica-orb at the same paths) -# Note: $IMG and $VPPDEBS expand on summer before being sent over SSH -- set them first. -ssh jessica-orb "cd ~/src/vpp-containerlab && \ - docker buildx build --platform linux/arm64 \ - --no-cache --build-context vppdebs=$VPPDEBS \ - --push --tag $IMG:latest-arm64 \ - -f docker/Dockerfile docker/" - -# Step 3: combine into a single multi-arch manifest and push in one step -# (docker buildx build --push produces manifest lists, so use imagetools, not docker manifest) -docker buildx imagetools create \ - --tag $IMG:latest \ - $IMG:latest-amd64 \ - $IMG:latest-arm64 +make preflight ``` -## Testing standalone container +This checks that `~/src/vpp/build-root` on each machine contains exactly one version of each +required package and that the version on `summer` matches the version on `jessica-orb`. +Override the path if your build root is elsewhere: ```bash -docker network create --driver=bridge clab-network --subnet=192.0.2.0/24 \ - --ipv6 --subnet=2001:db8::/64 -docker rm clab-pim -docker run --cap-add=NET_ADMIN --cap-add=SYS_NICE --cap-add=SYS_PTRACE \ - --device=/dev/net/tun:/dev/net/tun \ - --device=/dev/vhost-net:/dev/vhost-net \ - --privileged --name clab-pim \ - git.ipng.ch/ipng/vpp-containerlab:latest -docker network connect clab-network clab-pim +make preflight VPPDEBS=~/src/vpp/other-build-root ``` -### A note on DPDK +## Release pipeline + +The full pipeline runs in this order: -DPDK will be disabled by default as it requires hugepages and VFIO and/or UIO to use physical -network cards. If DPDK at some future point is desired, mapping VFIO can be done by adding this: ``` - --device=/dev/vfio/vfio:/dev/vfio/vfio +preflight → build → test → push → release ``` -or in Containerlab, using the `devices` feature: - -```yaml -my-node: - image: git.ipng.ch/ipng/vpp-containerlab:latest - kind: fdio_vpp - devices: - - /dev/vfio/vfio - - /dev/net/tun - - /dev/vhost-net -``` - -If using DPDK in a container, one of the userspace IO kernel drivers must be loaded in the host -kernel. Options are `igb_uio`, `vfio_pci`, or `uio_pci_generic`: +Run everything in one shot: ```bash -$ sudo modprobe igb_uio -$ sudo modprobe vfio_pci -$ sudo modprobe uio_pci_generic +make all ``` -Particularly the VFIO driver needs to be present before one can attempt to bindmount -`/dev/vfio/vfio` into the container! +Or step through it manually: -## Configuring VPP +| Step | Command | What it does | +|------|---------|--------------| +| 1 | `make preflight` | Validate VPP debs on summer and jessica-orb | +| 2 | `make build-amd64` | Build image locally for amd64 | +| 3 | `make test-amd64` | Run e2e tests against the amd64 image | +| 4 | `make sync-arm64` | Rsync working tree to jessica-orb | +| 5 | `make build-arm64` | Build image on jessica-orb for arm64 | +| 6 | `make test-arm64` | Run e2e tests on jessica-orb against the arm64 image | +| 7 | `make push-amd64` | Tag and push `:latest-amd64` to the registry | +| 8 | `make push-arm64` | Tag and push `:latest-arm64` to the registry | +| 9 | `make release` | Combine into a single `:latest` multi-arch manifest | -When Containerlab starts the docker containers, it'll offer one or more `veth` point to point -network links, which will show up as `eth1` and further. `eth0` is the default NIC that belongs to -the management plane in Containerlab (the one which you'll see with `containerlab inspect`). Before -VPP can use these `veth` interfaces, it needs to bind them, like so: +Convenience targets: ```bash -docker exec -it clab-pim vppctl +make build # steps 2+4+5 (both platforms) +make test # steps 3+6 (both platforms) +make push # steps 7+8 (both platforms) ``` -and then within the VPP control shell: +### Promoting to :stable -``` -create host-interface v2 name eth1 -set interface name host-eth1 eth1 -set interface mtu 1500 eth1 -set interface ip address eth1 192.0.2.2/24 -set interface ip address eth1 2001:db8::2/64 -set interface state eth1 up +`:stable` is only promoted **after** a successful `make all` — meaning both amd64 and arm64 +have been built, tested, pushed and combined into `:latest`. Do not run `make stable` unless +the full pipeline completed without errors. + +```bash +make all && make stable ``` -Containerlab will attach these `veth` pairs to the container, and replace our Docker CMD with one -that waits for all of these interfaces to be added (typically called `if-wait.sh`). In our own CMD, -we then generate a config file called `/etc/vpp/clab.vpp` which contains the necessary VPP commands -to take control over these `veth` pairs. +`make stable` points `:stable` at the same manifest as the current `:latest-amd64` and +`:latest-arm64`, so it is always in sync with a fully tested release. + +## Running a single test suite + +Pass `TEST=` to restrict which suite is run: + +```bash +make test-amd64 TEST=tests/01-vpp-ospf +make test TEST=tests/02-vpp-frr +``` + +The default is `tests/` (all suites). + +## Debugging test failures + +**Read the HTML log** — written after every run regardless of outcome: + +```bash +xdg-open tests/out/tests-docker-log.html +``` + +**Deploy the topology manually** to keep containers running for inspection: + +```bash +IMAGE=git.ipng.ch/ipng/vpp-containerlab:latest-amd64-test \ + containerlab deploy -t tests/01-vpp-ospf/e2e-lab/vpp.clab.yml +``` + +Then inspect live state: + +```bash +# OSPF neighbour state +containerlab exec -t tests/01-vpp-ospf/e2e-lab/vpp.clab.yml \ + --label clab-node-name=vpp1 --cmd "birdc show ospf neighbor" + +# Manual ping +containerlab exec -t tests/01-vpp-ospf/e2e-lab/vpp.clab.yml \ + --label clab-node-name=client1 --cmd "ping -c 5 10.82.98.82" + +# Tear down when done +containerlab destroy -t tests/01-vpp-ospf/e2e-lab/vpp.clab.yml --cleanup +``` + +**Common cause — OSPF convergence time:** 100% ping loss usually means routing is not up yet. +Tune the `Sleep` duration in the relevant `.robot` file by deploying manually and watching +`birdc show ospf neighbor` (or `vtysh -c "show ip ospf neighbor"` for FRR) until all +neighbours reach state `Full`. + +**Increase robot verbosity:** add `--loglevel DEBUG` to the `robot` invocation in +`tests/rf-run.sh` temporarily. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73928a8 --- /dev/null +++ b/Makefile @@ -0,0 +1,131 @@ +IMG := git.ipng.ch/ipng/vpp-containerlab +BUILDHOST := jessica-orb +BUILDDIR := ~/src/.vpp-containerlab-build +VPPDEBS := $(HOME)/src/vpp/build-root +TEST ?= tests/ + +.PHONY: all help preflight build build-amd64 build-arm64 sync-arm64 test test-amd64 test-arm64 \ + push push-amd64 push-arm64 release stable venv + +help: + @echo "vpp-containerlab build, test and release" + @echo "" + @echo "Typical workflow:" + @echo " 1. make venv Set up local Robot Framework venv (once, or after requirements change)" + @echo " 2. make preflight Verify VPP debs in VPPDEBS= on summer and jessica-orb" + @echo " 3. make build-amd64 Build image locally for amd64 (sideloading VPPDEBS)" + @echo " 4. make test-amd64 Run e2e tests locally against the amd64 image" + @echo " 5. make sync-arm64 Rsync working tree to jessica-orb and set up venv there" + @echo " 6. make build-arm64 Build image on jessica-orb for arm64 (sideloading VPPDEBS)" + @echo " 7. make test-arm64 Run e2e tests on jessica-orb against the arm64 image" + @echo " 8. make push-amd64 Tag and push :latest-amd64 to the registry" + @echo " 9. make push-arm64 Tag and push :latest-arm64 to the registry (runs on jessica-orb)" + @echo " 10. make release Combine into a single :latest multi-arch manifest" + @echo "" + @echo " make all Run steps 2-10 in one go (venv must already exist)" + @echo " make build Run steps 3+5+6 (sync-arm64 + both builds)" + @echo " make test Run steps 4+7 (both test suites)" + @echo " make push Run steps 8+9 (both pushes)" + @echo " make release Run step 10 (publish into a multi-arch manifest)" + @echo " make stable When all tests pass on amd64+arm64, push this ':latest' as :stable'" + @echo "" + @echo "Variables (override on command line):" + @echo " VPPDEBS=~/src/vpp/build-root Directory of locally-built VPP .deb packages" + @echo " TEST=tests/02-vpp-frr Run only a specific test suite (default: all)" + +# Build both platforms, test both, push both, then combine into :latest. +all: preflight build test push release + +build: build-amd64 sync-arm64 build-arm64 + +test: test-amd64 test-arm64 + +push: push-amd64 push-arm64 + +# ------------------------------------------------------------------------- +# Preflight — validate VPP debs on summer and jessica-orb +# ------------------------------------------------------------------------- + +# Check locally, then pipe the same script to jessica-orb over SSH, +# passing the local version so jessica-orb can assert both machines match. +preflight: + python3 scripts/check-vppdebs.py $(VPPDEBS) + $(eval VPP_VERSION := $(shell python3 scripts/check-vppdebs.py --print-version $(VPPDEBS))) + ssh $(BUILDHOST) python3 - --assert-version $(VPP_VERSION) $(VPPDEBS) < scripts/check-vppdebs.py + +# ------------------------------------------------------------------------- +# Local venv (summer) +# ------------------------------------------------------------------------- +tests/.venv: tests/requirements.txt + python3 -m venv tests/.venv + tests/.venv/bin/pip install -q -r tests/requirements.txt + +venv: tests/.venv + +# ------------------------------------------------------------------------- +# amd64 — runs locally on summer +# ------------------------------------------------------------------------- +build-amd64: + docker buildx build --no-cache --load --platform linux/amd64 \ + --build-context vppdebs=$(VPPDEBS) \ + --tag $(IMG):latest-amd64-test \ + -f docker/Dockerfile docker/ + +test-amd64: tests/.venv + IMAGE=$(IMG):latest-amd64-test tests/rf-run.sh docker $(TEST) + +push-amd64: + docker tag $(IMG):latest-amd64-test $(IMG):latest-amd64 + docker push $(IMG):latest-amd64 + +# ------------------------------------------------------------------------- +# arm64 — runs on jessica-orb via rsync + SSH +# ------------------------------------------------------------------------- + +# Wipe and re-sync summer's working tree to a stable directory on jessica-orb, +# then set up the robot venv there. +sync-arm64: + @case "$(BUILDDIR)" in \ + .*|/*) echo "ERROR: BUILDDIR '$(BUILDDIR)' must not start with '.' or '/'" >&2; exit 1;; \ + esac + ssh $(BUILDHOST) "rm -rf $(BUILDDIR) && mkdir -p $(BUILDDIR)" + rsync -a --exclude='.git' --exclude='tests/.venv' --exclude='tests/out' \ + ./ $(BUILDHOST):$(BUILDDIR)/ + ssh $(BUILDHOST) "cd $(BUILDDIR) && \ + python3 -m venv tests/.venv && \ + tests/.venv/bin/pip install -q -r tests/requirements.txt" + +build-arm64: + ssh $(BUILDHOST) "cd $(BUILDDIR) && \ + docker buildx build --no-cache --load --platform linux/arm64 \ + --build-context vppdebs=$(VPPDEBS) \ + --tag $(IMG):latest-arm64-test \ + -f docker/Dockerfile docker/" + +test-arm64: + ssh $(BUILDHOST) "cd $(BUILDDIR) && \ + IMAGE=$(IMG):latest-arm64-test \ + tests/rf-run.sh docker $(TEST)" + +push-arm64: + ssh $(BUILDHOST) "docker tag $(IMG):latest-arm64-test $(IMG):latest-arm64 && \ + docker push $(IMG):latest-arm64" + +# ------------------------------------------------------------------------- +# Release — combine amd64 + arm64 into a single :latest manifest +# ------------------------------------------------------------------------- +release: + docker buildx imagetools create \ + --tag $(IMG):latest \ + $(IMG):latest-amd64 \ + $(IMG):latest-arm64 + + +# ------------------------------------------------------------------------- +# Stable — mark latest release as stable. Only do this if all tests pass +# ------------------------------------------------------------------------- +stable: + docker buildx imagetools create \ + --tag $(IMG):stable \ + $(IMG):latest-amd64 \ + $(IMG):latest-arm64 diff --git a/scripts/check-vppdebs.py b/scripts/check-vppdebs.py new file mode 100755 index 0000000..c69730d --- /dev/null +++ b/scripts/check-vppdebs.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +Validate a directory of locally-built VPP .deb packages before a Docker build. + +Checks: + - The directory exists + - Exactly one *.changes file + - Exactly one *.buildinfo file + - Exactly one of each required .deb package + - All packages carry the same version string + - Optionally: version matches an expected value (for cross-machine consistency) + +Usage: + check-vppdebs.py [--print-version] [--assert-version VERSION] [directory] + + --print-version Print only the detected version string and exit (no other output). + --assert-version VER After all checks pass, assert the detected version equals VER. + Use this to verify summer and jessica-orb have the same build. + directory Path to check (default: ~/src/vpp/build-root). +""" + +import sys +import glob +import argparse +from pathlib import Path + +REQUIRED_DEBS = [ + "libvppinfra_*.deb", + "python3-vpp-api_*.deb", + "vpp_*.deb", + "vpp-crypto-engines_*.deb", + "vpp-plugin-core_*.deb", +] + +def find(directory, pattern): + return sorted(glob.glob(str(directory / pattern))) + +def version_from_deb(path): + """Extract the version field from a deb filename: name_VERSION_arch.deb""" + return Path(path).stem.split("_")[1] + +def main(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--print-version", action="store_true") + parser.add_argument("--assert-version", metavar="VERSION", default=None) + parser.add_argument("directory", nargs="?", default="~/src/vpp/build-root") + args = parser.parse_args() + + directory = Path(args.directory).expanduser() + errors = [] + versions = [] + + if not args.print_version: + print(f"Checking VPP debs in: {directory}") + + if not directory.is_dir(): + print(f" ERROR: directory does not exist: {directory}") + sys.exit(1) + + # *.changes + changes = find(directory, "*.changes") + if len(changes) == 1: + if not args.print_version: + print(f" OK changes : {Path(changes[0]).name}") + else: + errors.append(f"expected exactly 1 *.changes, found {len(changes)}: {[Path(f).name for f in changes]}") + + # *.buildinfo + buildinfo = find(directory, "*.buildinfo") + if len(buildinfo) == 1: + if not args.print_version: + print(f" OK buildinfo : {Path(buildinfo[0]).name}") + else: + errors.append(f"expected exactly 1 *.buildinfo, found {len(buildinfo)}: {[Path(f).name for f in buildinfo]}") + + # required debs + for pattern in REQUIRED_DEBS: + matches = find(directory, pattern) + if len(matches) == 1: + ver = version_from_deb(matches[0]) + versions.append(ver) + if not args.print_version: + print(f" OK {pattern:<30s}: {Path(matches[0]).name}") + else: + errors.append(f"expected exactly 1 {pattern}, found {len(matches)}: {[Path(f).name for f in matches]}") + + # version consistency within this directory + if versions and len(set(versions)) > 1: + errors.append(f"debs carry mixed versions: {sorted(set(versions))}") + + if errors: + if not args.print_version: + print() + for e in errors: + print(f" ERROR: {e}") + sys.exit(1) + + detected = versions[0] if versions else None + + if args.print_version: + print(detected or "") + sys.exit(0) + + print(f" OK version : {detected}") + + # cross-machine version assertion + if args.assert_version: + if detected == args.assert_version: + print(f" OK matches summer : {detected}") + else: + print(f" ERROR: version mismatch: this machine={detected}, summer={args.assert_version}") + sys.exit(1) + + print() + print("Preflight OK.") + +if __name__ == "__main__": + main() diff --git a/tests/01-vpp-bird/01-e2e-lab.robot b/tests/01-vpp-bird/01-e2e-lab.robot new file mode 100644 index 0000000..797b5d0 --- /dev/null +++ b/tests/01-vpp-bird/01-e2e-lab.robot @@ -0,0 +1,57 @@ +*** Settings *** +Library OperatingSystem +Resource ../ssh.robot +Resource ../common.robot + +Suite Teardown Run Keyword Cleanup + + +*** Variables *** +${lab-name} e2e-vpp +${lab-file-name} e2e-lab/vpp.clab.yml +${runtime} docker + + +*** Test Cases *** +Deploy ${lab-name} lab + Log ${CURDIR} + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file-name} + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Pause to let OSPF converge + Sleep 20s + +Check BFD Adjacencies + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=vpp2 --cmd "birdc show bfd ses" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Match Regexp ${output} (?m)fe80::.*eth2.*Up + Should Match Regexp ${output} (?m)10\.82\.98\..*eth2.*Up + +Check OSPF IPv4 Adjacency + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=vpp1 --cmd "birdc show ospf nei ospf4" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Match Regexp ${output} (?m)Full/PtP.*eth2 + +Check OSPF IPv6 Adjacency + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=vpp2 --cmd "birdc show ospf nei ospf6" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Match Regexp ${output} (?m)Full/PtP.*eth2 + +Ensure client1 can ping client2 + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=client1 --cmd "ping -c 5 10.82.98.82" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 5 packets transmitted, 4 packets received, 20% packet loss + +*** Keywords *** +Cleanup + Run ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file-name} --cleanup diff --git a/tests/01-vpp-bird/e2e-lab/config/vpp1/bird-local.conf b/tests/01-vpp-bird/e2e-lab/config/vpp1/bird-local.conf new file mode 100644 index 0000000..e7eae3a --- /dev/null +++ b/tests/01-vpp-bird/e2e-lab/config/vpp1/bird-local.conf @@ -0,0 +1,19 @@ +protocol bfd bfd1 { + interface "eth2" { interval 100 ms; multiplier 30; }; +} + +protocol ospf v2 ospf4 { + ipv4 { import all; export all; }; + area 0 { + interface "loop0" { stub yes; }; + interface "eth2" { type pointopoint; cost 10; bfd on; }; + }; +} + +protocol ospf v3 ospf6 { + ipv6 { import all; export all; }; + area 0 { + interface "loop0" { stub yes; }; + interface "eth2" { type pointopoint; cost 10; bfd on; }; + }; +} diff --git a/tests/01-vpp-bird/e2e-lab/config/vpp1/vppcfg.yaml b/tests/01-vpp-bird/e2e-lab/config/vpp1/vppcfg.yaml new file mode 100644 index 0000000..ee4c7f3 --- /dev/null +++ b/tests/01-vpp-bird/e2e-lab/config/vpp1/vppcfg.yaml @@ -0,0 +1,16 @@ +interfaces: + eth1: + description: "To client1" + mtu: 1500 + lcp: eth1 + addresses: [10.82.98.65/28, 2001:db8:8298:101::1/64] + eth2: + description: "To vpp2" + mtu: 9216 + lcp: eth2 + addresses: [10.82.98.16/31, 2001:db8:8298:1::1/64] +loopbacks: + loop0: + description: "vpp1" + lcp: loop0 + addresses: [10.82.98.0/32, 2001:db8:8298::/128] diff --git a/tests/01-vpp-bird/e2e-lab/config/vpp2/bird-local.conf b/tests/01-vpp-bird/e2e-lab/config/vpp2/bird-local.conf new file mode 100644 index 0000000..e7eae3a --- /dev/null +++ b/tests/01-vpp-bird/e2e-lab/config/vpp2/bird-local.conf @@ -0,0 +1,19 @@ +protocol bfd bfd1 { + interface "eth2" { interval 100 ms; multiplier 30; }; +} + +protocol ospf v2 ospf4 { + ipv4 { import all; export all; }; + area 0 { + interface "loop0" { stub yes; }; + interface "eth2" { type pointopoint; cost 10; bfd on; }; + }; +} + +protocol ospf v3 ospf6 { + ipv6 { import all; export all; }; + area 0 { + interface "loop0" { stub yes; }; + interface "eth2" { type pointopoint; cost 10; bfd on; }; + }; +} diff --git a/tests/01-vpp-bird/e2e-lab/config/vpp2/vppcfg.yaml b/tests/01-vpp-bird/e2e-lab/config/vpp2/vppcfg.yaml new file mode 100644 index 0000000..fc74fc7 --- /dev/null +++ b/tests/01-vpp-bird/e2e-lab/config/vpp2/vppcfg.yaml @@ -0,0 +1,16 @@ +interfaces: + eth1: + description: "To client2" + mtu: 1500 + lcp: eth1 + addresses: [10.82.98.81/28, 2001:db8:8298:102::1/64] + eth2: + description: "To vpp1" + mtu: 9216 + lcp: eth2 + addresses: [10.82.98.17/31, 2001:db8:8298:1::2/64] +loopbacks: + loop0: + description: "vpp2" + lcp: loop0 + addresses: [10.82.98.1/32, 2001:db8:8298::1/128] diff --git a/tests/01-vpp-bird/e2e-lab/vpp.clab.yml b/tests/01-vpp-bird/e2e-lab/vpp.clab.yml new file mode 100644 index 0000000..76fe543 --- /dev/null +++ b/tests/01-vpp-bird/e2e-lab/vpp.clab.yml @@ -0,0 +1,38 @@ +name: e2e-vpp + +topology: + kinds: + fdio_vpp: + image: ${IMAGE} + startup-config: config/__clabNodeName__/vppcfg.yaml + binds: + - config/__clabNodeName__/bird-local.conf:/config/bird/bird-local.conf:ro + linux: + image: alpine:latest + + nodes: + vpp1: + kind: fdio_vpp + vpp2: + kind: fdio_vpp + client1: + kind: linux + exec: + - ip link set address 00:c1:ab:00:00:01 dev eth1 + - ip addr add 10.82.98.66/28 dev eth1 + - ip route add 10.82.98.0/24 via 10.82.98.65 + - ip addr add 2001:db8:8298:101::2/64 dev eth1 + - ip route add 2001:db8:8298::/48 via 2001:db8:8298:101::1 + client2: + kind: linux + exec: + - ip link set address 00:c1:ab:00:00:02 dev eth1 + - ip addr add 10.82.98.82/28 dev eth1 + - ip route add 10.82.98.0/24 via 10.82.98.81 + - ip addr add 2001:db8:8298:102::2/64 dev eth1 + - ip route add 2001:db8:8298::/48 via 2001:db8:8298:102::1 + + links: + - endpoints: ["vpp1:eth2", "vpp2:eth2"] + - endpoints: ["client1:eth1", "vpp1:eth1"] + - endpoints: ["client2:eth1", "vpp2:eth1"] diff --git a/tests/02-vpp-frr/01-e2e-lab.robot b/tests/02-vpp-frr/01-e2e-lab.robot new file mode 100644 index 0000000..a2626e3 --- /dev/null +++ b/tests/02-vpp-frr/01-e2e-lab.robot @@ -0,0 +1,58 @@ +*** Settings *** +Library OperatingSystem +Resource ../ssh.robot +Resource ../common.robot + +Suite Teardown Run Keyword Cleanup + + +*** Variables *** +${lab-name} e2e-vpp +${lab-file-name} e2e-lab/vpp.clab.yml +${runtime} docker + + +*** Test Cases *** +Deploy ${lab-name} lab + Log ${CURDIR} + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file-name} + Log ${output} + Should Be Equal As Integers ${rc} 0 + +Pause to let OSPF converge + Sleep 20s + +Check BFD Adjacencies + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=vpp1 --cmd "vtysh -c 'show bfd peers brief'" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Match Regexp ${output} (?m)10\.82\.98\..*10\.82\.98\..*up + Should Match Regexp ${output} (?m)fe80::.*fe80::.*up + +Check OSPF IPv4 Adjacency + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=vpp1 --cmd "vtysh -c 'show ip ospf nei'" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Match Regexp ${output} (?m)Full/.*eth2 + +Check OSPF IPv6 Adjacency + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=vpp2 --cmd "vtysh -c 'show ipv6 ospf nei'" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Match Regexp ${output} (?m)Full/.*eth2 + +Ensure client1 can ping client2 + ${rc} ${output} = Run And Return Rc And Output + ... ${CLAB_BIN} --runtime ${runtime} exec -t ${CURDIR}/${lab-file-name} --label clab-node-name\=client1 --cmd "ping -c 5 10.82.98.82" + Log ${output} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 5 packets transmitted, 4 packets received, 20% packet loss + + +*** Keywords *** +Cleanup + Run ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file-name} --cleanup diff --git a/tests/02-vpp-frr/e2e-lab/config/vpp1/frr.conf b/tests/02-vpp-frr/e2e-lab/config/vpp1/frr.conf new file mode 100644 index 0000000..2123dcf --- /dev/null +++ b/tests/02-vpp-frr/e2e-lab/config/vpp1/frr.conf @@ -0,0 +1,31 @@ +frr version 10.3 +frr defaults traditional +hostname vpp1 +log syslog informational +service integrated-vtysh-config +! +ip router-id 10.82.98.0 +! +interface eth2 + ip ospf area 0 + ip ospf bfd + ip ospf cost 10 + ip ospf network point-to-point + ipv6 ospf6 area 0 + ipv6 ospf6 bfd + ipv6 ospf6 cost 10 + ipv6 ospf6 network point-to-point +exit +! +interface loop0 + ip ospf passive +exit +! +router ospf + redistribute connected +exit +! +router ospf6 + redistribute connected +exit +! diff --git a/tests/02-vpp-frr/e2e-lab/config/vpp1/vppcfg.yaml b/tests/02-vpp-frr/e2e-lab/config/vpp1/vppcfg.yaml new file mode 100644 index 0000000..ee4c7f3 --- /dev/null +++ b/tests/02-vpp-frr/e2e-lab/config/vpp1/vppcfg.yaml @@ -0,0 +1,16 @@ +interfaces: + eth1: + description: "To client1" + mtu: 1500 + lcp: eth1 + addresses: [10.82.98.65/28, 2001:db8:8298:101::1/64] + eth2: + description: "To vpp2" + mtu: 9216 + lcp: eth2 + addresses: [10.82.98.16/31, 2001:db8:8298:1::1/64] +loopbacks: + loop0: + description: "vpp1" + lcp: loop0 + addresses: [10.82.98.0/32, 2001:db8:8298::/128] diff --git a/tests/02-vpp-frr/e2e-lab/config/vpp2/frr.conf b/tests/02-vpp-frr/e2e-lab/config/vpp2/frr.conf new file mode 100644 index 0000000..83e26d6 --- /dev/null +++ b/tests/02-vpp-frr/e2e-lab/config/vpp2/frr.conf @@ -0,0 +1,31 @@ +frr version 10.3 +frr defaults traditional +hostname vpp2 +log syslog informational +service integrated-vtysh-config +! +ip router-id 10.82.98.1 +! +interface eth2 + ip ospf area 0 + ip ospf bfd + ip ospf cost 10 + ip ospf network point-to-point + ipv6 ospf6 area 0 + ipv6 ospf6 bfd + ipv6 ospf6 cost 10 + ipv6 ospf6 network point-to-point +exit +! +interface loop0 + ip ospf passive +exit +! +router ospf + redistribute connected +exit +! +router ospf6 + redistribute connected +exit +! diff --git a/tests/02-vpp-frr/e2e-lab/config/vpp2/vppcfg.yaml b/tests/02-vpp-frr/e2e-lab/config/vpp2/vppcfg.yaml new file mode 100644 index 0000000..fc74fc7 --- /dev/null +++ b/tests/02-vpp-frr/e2e-lab/config/vpp2/vppcfg.yaml @@ -0,0 +1,16 @@ +interfaces: + eth1: + description: "To client2" + mtu: 1500 + lcp: eth1 + addresses: [10.82.98.81/28, 2001:db8:8298:102::1/64] + eth2: + description: "To vpp1" + mtu: 9216 + lcp: eth2 + addresses: [10.82.98.17/31, 2001:db8:8298:1::2/64] +loopbacks: + loop0: + description: "vpp2" + lcp: loop0 + addresses: [10.82.98.1/32, 2001:db8:8298::1/128] diff --git a/tests/02-vpp-frr/e2e-lab/vpp.clab.yml b/tests/02-vpp-frr/e2e-lab/vpp.clab.yml new file mode 100644 index 0000000..8cb18bd --- /dev/null +++ b/tests/02-vpp-frr/e2e-lab/vpp.clab.yml @@ -0,0 +1,41 @@ +name: e2e-vpp + +topology: + kinds: + fdio_vpp: + image: ${IMAGE} + startup-config: config/__clabNodeName__/vppcfg.yaml + binds: + - config/__clabNodeName__/frr.conf:/config/frr/frr.conf:ro + env: + BIRD_ENABLED: false + FRR_ENABLED: true + linux: + image: alpine:latest + + nodes: + vpp1: + kind: fdio_vpp + vpp2: + kind: fdio_vpp + client1: + kind: linux + exec: + - ip link set address 00:c1:ab:00:00:01 dev eth1 + - ip addr add 10.82.98.66/28 dev eth1 + - ip route add 10.82.98.0/24 via 10.82.98.65 + - ip addr add 2001:db8:8298:101::2/64 dev eth1 + - ip route add 2001:db8:8298::/48 via 2001:db8:8298:101::1 + client2: + kind: linux + exec: + - ip link set address 00:c1:ab:00:00:02 dev eth1 + - ip addr add 10.82.98.82/28 dev eth1 + - ip route add 10.82.98.0/24 via 10.82.98.81 + - ip addr add 2001:db8:8298:102::2/64 dev eth1 + - ip route add 2001:db8:8298::/48 via 2001:db8:8298:102::1 + + links: + - endpoints: ["vpp1:eth2", "vpp2:eth2"] + - endpoints: ["client1:eth1", "vpp1:eth1"] + - endpoints: ["client2:eth1", "vpp2:eth1"] diff --git a/tests/common.robot b/tests/common.robot new file mode 100644 index 0000000..a4b9e34 --- /dev/null +++ b/tests/common.robot @@ -0,0 +1,2 @@ +*** Variables *** +${CLAB_BIN} containerlab diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..af1d9bf --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +robotframework +robotframework-sshlibrary diff --git a/tests/rf-run.sh b/tests/rf-run.sh new file mode 100755 index 0000000..0faf93a --- /dev/null +++ b/tests/rf-run.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Run Robot Framework tests for vpp-containerlab. +# Arguments: +# $1 - container runtime: [docker, podman] +# $2 - test suite path (directory or .robot file) +# +# Environment variables: +# CLAB_BIN - path to containerlab binary (default: containerlab) +# IMAGE - docker image to use in topology (must be set) + +set -e + +if [ -z "${CLAB_BIN}" ]; then + CLAB_BIN=containerlab +fi + +if [ -z "${IMAGE}" ]; then + echo "ERROR: IMAGE environment variable must be set" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +mkdir -p "${SCRIPT_DIR}/out" + +source "${SCRIPT_DIR}/.venv/bin/activate" + +function get_logname() { + path=$1 + filename=$(basename "$path") + if [[ "$filename" == *.* ]]; then + dirname=$(dirname "$path") + basename=$(basename "$path" | cut -d. -f1) + echo "${dirname##*/}-${basename}" + else + echo "${filename}" + fi +} + +robot --consolecolors on -r none \ + --variable CLAB_BIN:"${CLAB_BIN}" \ + --variable runtime:"$1" \ + -l "${SCRIPT_DIR}/out/$(get_logname $2)-$1-log" \ + --output "${SCRIPT_DIR}/out/$(get_logname $2)-$1-out.xml" \ + "$2" diff --git a/tests/ssh.robot b/tests/ssh.robot new file mode 100644 index 0000000..08a2316 --- /dev/null +++ b/tests/ssh.robot @@ -0,0 +1,44 @@ +*** Settings *** +Library SSHLibrary + + +*** Keywords *** +Login via SSH with username and password + [Arguments] + ... ${address}=${None} + ... ${port}=22 + ... ${username}=${None} + ... ${password}=${None} + # seconds to try and successfully login + ... ${try_for}=4 + ... ${conn_timeout}=3 + FOR ${i} IN RANGE ${try_for} + SSHLibrary.Open Connection ${address} timeout=${conn_timeout} + ${status}= Run Keyword And Return Status SSHLibrary.Login ${username} ${password} + IF ${status} BREAK + Sleep 1s + END + IF $status!=True + Fail Unable to connect to ${address} via SSH in ${try_for} attempts + END + Log Exited the loop. + +Login via SSH with public key + [Arguments] + ... ${address}=${None} + ... ${port}=22 + ... ${username}=${None} + ... ${keyfile}=${None} + ... ${try_for}=4 + ... ${conn_timeout}=3 + FOR ${i} IN RANGE ${try_for} + SSHLibrary.Open Connection ${address} timeout=${conn_timeout} + ${status}= Run Keyword And Return Status SSHLibrary.Login With Public Key + ... ${username} ${keyfile} + IF ${status} BREAK + Sleep 1s + END + IF $status!=True + Fail Unable to connect to ${address} via SSH in ${try_for} attempts + END + Log Exited the loop.