From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=none (p=none dis=none) header.from=gibson.dropbear.id.au Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=gibson.dropbear.id.au header.i=@gibson.dropbear.id.au header.a=rsa-sha256 header.s=202408 header.b=MW/TMnZM; dkim-atps=neutral Received: from mail.ozlabs.org (gandalf.ozlabs.org [150.107.74.76]) by passt.top (Postfix) with ESMTPS id A2C645A0276 for ; Mon, 26 Aug 2024 04:09:56 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gibson.dropbear.id.au; s=202408; t=1724638184; bh=5lmOeAiE5kH72NrxaEPdw9NVR2HcGbzEvhoLnRXiXp0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=MW/TMnZMjktG5H2aQ6d73QK6QvYoGUXrqrn2NmjS/H1dY/Ydc1rJOW3dK28JkTWtZ HtlIqSh9guNuvmMPoMAHi8leNzCm6FtV/j/uZOybBlKY2Ay6k4jV2lXEiu0BONkOxv SiFTUMWHMZ/sWz5/ufh7XCQV7RYlc2NDvaofOnRIVSBOXkijt202E0RHk3Yt/e68ku diXfTjhk+8M5IoGVSNSxDonYkvMHFXGCJc35bl1272Lv/sibhzGBc3Pv5RR0PnZLsS J1b9gpgu3kbwCQZHBqHQOyI6etbK5O0fPfa3sHMOWd1aISWl9SNnkskDxD6aa2qkKw R4Qrd52lrKw2A== Received: by gandalf.ozlabs.org (Postfix, from userid 1007) id 4WsYyw5y4Mz4x8x; Mon, 26 Aug 2024 12:09:44 +1000 (AEST) From: David Gibson To: Stefano Brivio , passt-dev@passt.top Subject: [PATCH v3 11/15] tasst: Helpers to test transferring data between sites Date: Mon, 26 Aug 2024 12:09:38 +1000 Message-ID: <20240826020942.545155-12-david@gibson.dropbear.id.au> X-Mailer: git-send-email 2.46.0 In-Reply-To: <20240826020942.545155-1-david@gibson.dropbear.id.au> References: <20240826020942.545155-1-david@gibson.dropbear.id.au> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Message-ID-Hash: GLKRPAX3DYIGY27QRRGAQFMWSQAWSEZG X-Message-ID-Hash: GLKRPAX3DYIGY27QRRGAQFMWSQAWSEZG X-MailFrom: dgibson@gandalf.ozlabs.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Cleber Rosa , David Gibson X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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 + +""" +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' + + +@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