Add a release pipeline including tests for Bird2 and VPP

This commit is contained in:
2026-04-05 17:34:30 +02:00
parent a7fb7db978
commit 9fc41679fd
20 changed files with 806 additions and 160 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
clab-* clab-*
**/*.bak **/*.bak
tests/.venv/
tests/out/

View File

@@ -1,85 +1,20 @@
# Building vpp-containerlab # Building vpp-containerlab
This docker container creates a VPP instance based on the latest VPP release. It starts up as per This document describes how to build, test and release the `vpp-containerlab` Docker image.
normal, using /etc/vpp/startup.conf (which Containerlab might replace when it starts its The image is built natively on two machines and combined into a multi-arch manifest:
containers). Once started, it'll execute `/etc/vpp/bootstrap.vpp` within the dataplane. There are
two relevant files:
1. `clab.vpp` -- generated by `files/init-container.sh`. Its purpose is to bind the `veth` - `summer` — amd64, Linux (local machine)
interfaces that containerlab has added to the container into the VPP dataplane (see below). - `jessica-orb` — arm64, OrbStack VM on macOS, reachable via `ssh jessica-orb`
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.
For Containerlab users who wish to have more control over their VPP bootstrap, it's possible to The pipeline sideloads locally-built VPP `.deb` packages rather than pulling from packagecloud,
bind-mount `/etc/vpp/bootstrap.vpp`. 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 ### SSH access to jessica-orb
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:
```bash The Docker daemon on `jessica` runs inside OrbStack's Linux VM. OrbStack listens on
docker run --privileged --rm tonistiigi/binfmt --install all `127.0.0.1:32222`; add a jump-host entry to `~/.ssh/config` on `summer` to reach it:
```
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=<path>` 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`:
``` ```
Host jessica-orb Host jessica-orb
@@ -107,118 +42,127 @@ ssh jessica-orb 'uname -m && docker info | head -3'
# expected: aarch64 # expected: aarch64
``` ```
Create the multiarch builder (run once on `summer`): ### One-time setup
Install the Robot Framework venv for running tests:
```bash ```bash
docker buildx create --name multiarch --driver docker-container --platform linux/amd64 --node summer-amd64 make venv
docker buildx create --append --name multiarch --driver docker-container --platform linux/arm64 --node jessica-arm64 ssh://jessica-orb
docker buildx inspect multiarch --bootstrap
``` ```
#### 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 ### Before every release
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 Build VPP on both machines (`make pkg-deb` in your VPP source tree on both `summer` and the
the arm64 build (wrong arch). The solution is to build each platform separately on its native OrbStack VM on `jessica`), then verify both machines have a consistent set of `.deb` packages:
machine and combine them into a manifest.
```bash ```bash
IMG=git.ipng.ch/ipng/vpp-containerlab make preflight
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
``` ```
## 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 ```bash
docker network create --driver=bridge clab-network --subnet=192.0.2.0/24 \ make preflight VPPDEBS=~/src/vpp/other-build-root
--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
``` ```
### 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: Run everything in one shot:
```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`:
```bash ```bash
$ sudo modprobe igb_uio make all
$ sudo modprobe vfio_pci
$ sudo modprobe uio_pci_generic
``` ```
Particularly the VFIO driver needs to be present before one can attempt to bindmount Or step through it manually:
`/dev/vfio/vfio` into the container!
## 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 Convenience targets:
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:
```bash ```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
``` `:stable` is only promoted **after** a successful `make all` — meaning both amd64 and arm64
create host-interface v2 name eth1 have been built, tested, pushed and combined into `:latest`. Do not run `make stable` unless
set interface name host-eth1 eth1 the full pipeline completed without errors.
set interface mtu 1500 eth1
set interface ip address eth1 192.0.2.2/24 ```bash
set interface ip address eth1 2001:db8::2/64 make all && make stable
set interface state eth1 up
``` ```
Containerlab will attach these `veth` pairs to the container, and replace our Docker CMD with one `make stable` points `:stable` at the same manifest as the current `:latest-amd64` and
that waits for all of these interfaces to be added (typically called `if-wait.sh`). In our own CMD, `:latest-arm64`, so it is always in sync with a fully tested release.
we then generate a config file called `/etc/vpp/clab.vpp` which contains the necessary VPP commands
to take control over these `veth` pairs. ## 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.

131
Makefile Normal file
View File

@@ -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

118
scripts/check-vppdebs.py Executable file
View File

@@ -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()

View File

@@ -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

View File

@@ -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; };
};
}

View File

@@ -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]

View File

@@ -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; };
};
}

View File

@@ -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]

View File

@@ -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"]

View File

@@ -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

View File

@@ -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
!

View File

@@ -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]

View File

@@ -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
!

View File

@@ -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]

View File

@@ -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"]

2
tests/common.robot Normal file
View File

@@ -0,0 +1,2 @@
*** Variables ***
${CLAB_BIN} containerlab

2
tests/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
robotframework
robotframework-sshlibrary

45
tests/rf-run.sh Executable file
View File

@@ -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"

44
tests/ssh.robot Normal file
View File

@@ -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.