On Thu, 2 Oct 2025 17:57:08 +1000
David Gibson
Convert the pasta NDP tests from shell and our own DSL to Python using the exeter test protocol and tunbridge network simulation library.
Signed-off-by: David Gibson
--- test/Makefile | 2 +- test/pasta/dhcp | 5 ++++ test/pasta/ndp.py | 59 ++++++++++++++++++++++++++++++++++++++++++ test/run | 6 +++-- test/tasst/__init__.py | 4 +++ test/tasst/pasta.py | 40 ++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 3 deletions(-) create mode 100755 test/pasta/ndp.py create mode 100644 test/tasst/pasta.py diff --git a/test/Makefile b/test/Makefile index f66c7e7e..95e3d75e 100644 --- a/test/Makefile +++ b/test/Makefile @@ -67,7 +67,7 @@ ASSETS = $(DOWNLOAD_ASSETS) $(LOCAL_ASSETS)
EXETER_SH = smoke/smoke.sh build/static_checkers.sh EXETER_PYPATH = exeter/py3:tunbridge/:. -EXETER_PYTHON = smoke/smoke.py build/build.py +EXETER_PYTHON = smoke/smoke.py build/build.py pasta/ndp.py EXETER_BATS = $(EXETER_SH:%=%.bats) $(EXETER_PYTHON:%=%.bats) BATS_FILES = $(EXETER_BATS) \ podman/test/system/505-networking-pasta.bats diff --git a/test/pasta/dhcp b/test/pasta/dhcp index e1c66be6..61279fbf 100644 --- a/test/pasta/dhcp +++ b/test/pasta/dhcp @@ -18,6 +18,11 @@ test Interface name nsout IFNAME ip -j link show | jq -rM '.[] | select(.link_type == "ether").ifname' check [ -n "__IFNAME__" ]
+# Bring up the interface +ns ip link set dev __IFNAME__ up +# Wait for SLAAC & DAD to complete +ns while ! ip -j -6 addr show dev __IFNAME__ | jq -e '.[].addr_info.[] | select(.protocol == "kernel_ra")'; do sleep 0.1; done + test DHCP: address ns /sbin/dhclient -4 --no-pid __IFNAME__ nsout ADDR ip -j -4 addr show|jq -rM '.[] | select(.ifname == "__IFNAME__").addr_info[0].local' diff --git a/test/pasta/ndp.py b/test/pasta/ndp.py new file mode 100755 index 00000000..8c7ce31e --- /dev/null +++ b/test/pasta/ndp.py @@ -0,0 +1,59 @@ +#! /usr/bin/env python3 +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# test/pasta/ndp.py - pasta NDP functionality +# +# Copyright Red Hat +# Author: David Gibson
+ +import contextlib +import dataclasses +from typing import Iterator + +import exeter +import tunbridge +import tasst + + +@dataclasses.dataclass +class UnconfiguredScenario(exeter.Scenario): + """Tests for a pasta instance without --config-net""" + + host: tunbridge.Site + guest: tunbridge.Site + ifname: str + addr6: tunbridge.ip.AddrMask6 + gw6: tunbridge.ip.Addr6
Until this point, it looks like stuff I can happily copy and paste, and grasp, even. But then:
+ @exeter.scenariotest + def test_ifname(self) -> None: + ifs = tunbridge.ip.ifs(self.guest) + exeter.assert_eq(set(ifs), {'lo', self.ifname})
...why does a "Scenario" have a .ifname?
+ + @tunbridge.ndp.NdpScenario.subscenario + def test_ndp(self) -> tunbridge.ndp.NdpScenario: + tunbridge.ip.ifup(self.guest, self.ifname)
This raises the question of how much of tunbridge one needs to know to be able to write a basic test. Why is ifup() in 'ip'? I thought it would be more of a "link" thing. I admit I haven't had time to browse tunbridge recently, I'm just looking at this series right now.
+ return tunbridge.ndp.NdpScenario(client=self.guest, + ifname=self.ifname, + network=self.addr6.network, + gateway=self.gw6)
This makes sense to me.
+ + +@UnconfiguredScenario.test +@contextlib.contextmanager +def simh_pasta_setup() -> Iterator[UnconfiguredScenario]: + with (tunbridge.sample.simple_host('host') as simh, + tunbridge.sample.isolated('guest', simh.site) as guest): + assert simh.ip6 is not None + assert simh.gw6_ll is not None + with tasst.pasta.pasta(simh.site, guest): + yield UnconfiguredScenario(host=simh.site, + guest=guest, + ifname=simh.ifname, + addr6=simh.ip6, + gw6=simh.gw6_ll)
...and this too. But there's one thing I'm missing: if it's a network simulator, why do you need to call a simple_host() method to *describe* the fact that you have a host / site? That looks rather unexpected. I mean, I would have expected a syntax, in pseudocode, expressing: 1. x := node (properties such as a list of interfaces a, b, c) 2. pasta implements/connects a ...I think this is mostly embedded in the sample.simple_host() thing, but I'm not sure how. Maybe it will become clearer once I actually look into tunbridge, though. Of course, I'm trying to push away my bias coming from the fact I was, several years ago, for kselftests, aiming at something like this instead: A veth B x=$(addr A veth) B ping -c1 $x A $x vxlan B $(addr B veth) ... (where 'veth', 'vxlan' were both reserved keywords). Maybe once non-trivial links are implemented in tunbridge it will all become more obvious.
+ + +if __name__ == '__main__': + exeter.main() diff --git a/test/run b/test/run index 3872a56e..4f09d767 100755 --- a/test/run +++ b/test/run @@ -43,8 +43,10 @@ KERNEL=${KERNEL:-"/boot/vmlinuz-$(uname -r)"}
COMMIT="$(git log --oneline --no-decorate -1)"
-# Let exeter tests written in Python find their modules +# Let exeter tests written in Python find their modules and binaries to run export PYTHONPATH=${BASEPATH}/exeter/py3:${BASEPATH}/tunbridge:${BASEPATH} +export PASTA=${PASTA:-${BASEPATH}/../pasta} +
. lib/util . lib/context @@ -75,8 +77,8 @@ run() { exeter build/build.py exeter build/static_checkers.sh
+ exeter pasta/ndp.py setup pasta - test pasta/ndp test pasta/dhcp test pasta/tcp test pasta/udp diff --git a/test/tasst/__init__.py b/test/tasst/__init__.py index fd4fe9a8..f5386b3a 100644 --- a/test/tasst/__init__.py +++ b/test/tasst/__init__.py @@ -8,3 +8,7 @@ # # Copyright Red Hat # Author: David Gibson
+ +from . import pasta + +__all__ = ['pasta'] diff --git a/test/tasst/pasta.py b/test/tasst/pasta.py new file mode 100644 index 00000000..91f59036 --- /dev/null +++ b/test/tasst/pasta.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python3 +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# TASST - Test A Simple Socket Transport +# +# test/tasst/pasta.py - Helpers for seeting up pasta instances +# +# Copyright Red Hat +# Author: David Gibson + +import contextlib +import os +from typing import Iterator + +import tunbridge + + +@contextlib.contextmanager +def pasta(host: tunbridge.Site, guest: tunbridge.Site, *opts: str) \ + -> Iterator[tunbridge.site.SiteProcess]: + if tunbridge.unshare.parent(guest) is not host: + raise ValueError("pasta guest must be a namespace under host site") + + # This implies guest is a namespace site + assert isinstance(guest, tunbridge.unshare.NsenterSite) + + exe = os.environ['PASTA'] + + with host.tempdir() as piddir: + pidfile = os.path.join(piddir, 'pasta.pid') + cmd = [exe, '-f', '-P', pidfile] + list(opts) + [f'{guest.pid}'] + with host.bg(*cmd, stop=True) as pasta: + # Wait for the PID file to be written + pidstr = None + while not pidstr: + pidstr = host.readfile(pidfile, check=False) + pid = int(pidstr) + print(f'pasta started, host: {host}, guest: {guest}, pid: {pid}') + yield pasta
...perhaps we could also print version and path. This part also looks quite readable and intuitive to me without having looked into tunbridge recently. -- Stefano