Many of our existing tests are based on using socat to transfer between various locations connected via pasta or passt. Add helpers to make avocado tests performing similar transfers. Add selftests to verify those work as expected when we don't have pasta or passt involved yet. Signed-off-by: David Gibson <david(a)gibson.dropbear.id.au> --- test/Makefile | 2 +- test/tasst/__main__.py | 1 + test/tasst/transfer.py | 193 +++++++++++++++++++++++++++++++++++++++++ test/tasst/veth.py | 26 +++++- 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 test/tasst/transfer.py diff --git a/test/Makefile b/test/Makefile index 5cd5c781..3ac67b66 100644 --- a/test/Makefile +++ b/test/Makefile @@ -65,7 +65,7 @@ LOCAL_ASSETS = mbuto.img mbuto.mem.img podman/bin/podman QEMU_EFI.fd \ ASSETS = $(DOWNLOAD_ASSETS) $(LOCAL_ASSETS) AVOCADO_ASSETS = -META_ASSETS = nstool +META_ASSETS = nstool small.bin medium.bin big.bin EXETER_SH = build/static_checkers.sh EXETER_PY = build/build.py diff --git a/test/tasst/__main__.py b/test/tasst/__main__.py index 4eab9157..251edae5 100644 --- a/test/tasst/__main__.py +++ b/test/tasst/__main__.py @@ -18,6 +18,7 @@ import exeter MODULES = [ 'cmdsite', 'ip', + 'transfer', 'unshare', 'veth', ] diff --git a/test/tasst/transfer.py b/test/tasst/transfer.py new file mode 100644 index 00000000..6654b6da --- /dev/null +++ b/test/tasst/transfer.py @@ -0,0 +1,193 @@ +#! /usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright Red Hat +# Author: David Gibson <david(a)gibson.dropbear.id.au> + +""" +Test A Simple Socket Transport + +transfer.py - Helpers for testing data transfers +""" + +import dataclasses +import ipaddress +import time +from typing import Iterator, Optional + +import exeter + +from . import cmdsite, ip, unshare + +# HACK: how long to wait for the server to be ready and listening (s) +SERVER_READY_DELAY = 0.1 # 1/10th of a second + + +# socat needs IPv6 addresses in square brackets +def socat_ip(ip: ip.Addr) -> str: + if isinstance(ip, ipaddress.IPv6Address): + return f'[{ip}]' + elif isinstance(ip, ipaddress.IPv4Address): + return f'{ip}' + raise TypeError + + +def socat_upload(datafile: str, csite: cmdsite.CmdSite, + ssite: cmdsite.CmdSite, connect: str, listen: str) -> None: + srcdata = csite.output('cat', f'{datafile}') + with ssite.bg('socat', '-u', f'{listen}', 'STDOUT', + capture=cmdsite.Capture.STDOUT) as server: + time.sleep(SERVER_READY_DELAY) + + # Can't use csite.fg() here, because while we wait for the + # client to complete we won't be reading from the output pipe + # of the server, meaning it will freeze once the buffers fill + with csite.bg('socat', '-u', f'OPEN:{datafile}', f'{connect}') \ + as client: + res = server.run() + client.run() + exeter.assert_eq(srcdata, res.stdout) + + +def socat_download(datafile: str, csite: cmdsite.CmdSite, + ssite: cmdsite.CmdSite, + connect: str, listen: str) -> None: + srcdata = ssite.output('cat', f'{datafile}') + with ssite.bg('socat', '-u', f'OPEN:{datafile}', f'{listen}'): + time.sleep(SERVER_READY_DELAY) + dstdata = csite.output('socat', '-u', f'{connect}', 'STDOUT') + exeter.assert_eq(srcdata, dstdata) + + +def _tcp_socat(connectip: ip.Addr, connectport: int, + listenip: Optional[ip.Addr], listenport: Optional[int], + fromip: Optional[ip.Addr]) -> tuple[str, str]: + v6 = isinstance(connectip, ipaddress.IPv6Address) + if listenport is None: + listenport = connectport + if v6: + connect = f'TCP6:[{connectip}]:{connectport},ipv6only' + listen = f'TCP6-LISTEN:{listenport},ipv6only' + else: + connect = f'TCP4:{connectip}:{connectport}' + listen = f'TCP4-LISTEN:{listenport}' + if listenip is not None: + listen += f',bind={socat_ip(listenip)}' + if fromip is not None: + connect += f',bind={socat_ip(fromip)}' + return (connect, listen) + + +def tcp_upload(datafile: str, cs: cmdsite.CmdSite, ss: cmdsite.CmdSite, + connectip: ip.Addr, connectport: int, + listenip: Optional[ip.Addr] = None, + listenport: Optional[int] = None, + fromip: Optional[ip.Addr] = None) -> None: + connect, listen = _tcp_socat(connectip, connectport, listenip, listenport, + fromip) + socat_upload(datafile, cs, ss, connect, listen) + + +def tcp_download(datafile: str, cs: cmdsite.CmdSite, ss: cmdsite.CmdSite, + connectip: ip.Addr, connectport: int, + listenip: Optional[ip.Addr] = None, + listenport: Optional[int] = None, + fromip: Optional[ip.Addr] = None) -> None: + connect, listen = _tcp_socat(connectip, connectport, listenip, listenport, + fromip) + socat_download(datafile, cs, ss, connect, listen) + + +def udp_transfer(datafile: str, cs: cmdsite.CmdSite, ss: cmdsite.CmdSite, + connectip: ip.Addr, connectport: int, + listenip: Optional[ip.Addr] = None, + listenport: Optional[int] = None, + fromip: Optional[ip.Addr] = None) -> None: + v6 = isinstance(connectip, ipaddress.IPv6Address) + if listenport is None: + listenport = connectport + if v6: + connect = f'UDP6:[{connectip}]:{connectport},ipv6only,shut-null' + listen = f'UDP6-LISTEN:{listenport},ipv6only,null-eof' + else: + connect = f'UDP4:{connectip}:{connectport},shut-null' + listen = f'UDP4-LISTEN:{listenport},null-eof' + if listenip is not None: + listen += f',bind={socat_ip(listenip)}' + if fromip is not None: + connect += f',bind={socat_ip(fromip)}' + + socat_upload(datafile, cs, ss, connect, listen) + + +SMALL_DATA = 'small.bin' +BIG_DATA = 'big.bin' +UDP_DATA = 'medium.bin' + + +(a)dataclasses.dataclass +class TransferScenario(exeter.Scenario): + client: cmdsite.CmdSite + server: cmdsite.CmdSite + connect_ip: ip.Addr + connect_port: int + listen_ip: Optional[ip.Addr] = None + from_ip: Optional[ip.Addr] = None + listen_port: Optional[int] = None + + def tcp_upload(self, datafile: str) -> None: + tcp_upload(datafile, self.client, self.server, + self.connect_ip, self.connect_port, + listenip=self.listen_ip, listenport=self.listen_port, + fromip=self.from_ip) + + @exeter.scenariotest + def tcp_small_upload(self) -> None: + self.tcp_upload(SMALL_DATA) + + @exeter.scenariotest + def tcp_big_upload(self) -> None: + self.tcp_upload(BIG_DATA) + + def tcp_download(self, datafile: str) -> None: + tcp_download(datafile, self.client, self.server, + self.connect_ip, self.connect_port, + listenip=self.listen_ip, listenport=self.listen_port, + fromip=self.from_ip) + + @exeter.scenariotest + def tcp_small_download(self) -> None: + self.tcp_download(SMALL_DATA) + + @exeter.scenariotest + def tcp_big_download(self) -> None: + self.tcp_download(BIG_DATA) + + @exeter.scenariotest + def udp_transfer(self, datafile: str = UDP_DATA) -> None: + udp_transfer(datafile, self.client, self.server, + self.connect_ip, self.connect_port, + listenip=self.listen_ip, listenport=self.listen_port, + fromip=self.from_ip) + + +def local4() -> Iterator[TransferScenario]: + with unshare.unshare('ns', '-Un') as ns: + ip.ifup(ns, 'lo') + yield TransferScenario(client=ns, server=ns, + connect_ip=ip.LOOPBACK4, + connect_port=10000) + + +def local6() -> Iterator[TransferScenario]: + with unshare.unshare('ns', '-Un') as ns: + ip.ifup(ns, 'lo') + yield TransferScenario(client=ns, server=ns, + connect_ip=ip.LOOPBACK6, + connect_port=10000) + + +def selftests() -> None: + TransferScenario.test(local4) + TransferScenario.test(local6) diff --git a/test/tasst/veth.py b/test/tasst/veth.py index 7fa5cbb5..3a9c123b 100644 --- a/test/tasst/veth.py +++ b/test/tasst/veth.py @@ -17,7 +17,7 @@ import ipaddress import exeter -from . import cmdsite, ip, unshare +from . import cmdsite, ip, transfer, unshare @contextlib.contextmanager @@ -78,3 +78,27 @@ def selftests() -> None: @exeter.test def test_no_dad() -> None: test_slaac(dad='disable') + + def veth_transfer(ip1: ip.AddrMask, ip2: ip.AddrMask) \ + -> Iterator[transfer.TransferScenario]: + with veth_setup() as (ns1, ns2): + ip.ifup(ns1, 'lo') + ip.ifup(ns1, 'vetha', ip1, dad='disable') + ip.ifup(ns2, 'lo') + ip.ifup(ns2, 'vethb', ip2, dad='disable') + + yield transfer.TransferScenario(client=ns1, server=ns2, + connect_ip=ip2.ip, + connect_port=10000) + + ipa = ip.IpiAllocator() + ns1_ip4, ns1_ip6 = ipa.next_ipis() + ns2_ip4, ns2_ip6 = ipa.next_ipis() + + @transfer.TransferScenario.test + def veth_transfer4() -> Iterator[transfer.TransferScenario]: + yield from veth_transfer(ns1_ip4, ns2_ip4) + + @transfer.TransferScenario.test + def veth_transfer6() -> Iterator[transfer.TransferScenario]: + yield from veth_transfer(ns1_ip6, ns2_ip6) -- 2.46.0