public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: David Gibson <david@gibson.dropbear.id.au>
To: Stefano Brivio <sbrivio@redhat.com>, passt-dev@passt.top
Cc: Cleber Rosa <crosa@redhat.com>,
	David Gibson <david@gibson.dropbear.id.au>
Subject: [PATCH v3 11/15] tasst: Helpers to test transferring data between sites
Date: Mon, 26 Aug 2024 12:09:38 +1000	[thread overview]
Message-ID: <20240826020942.545155-12-david@gibson.dropbear.id.au> (raw)
In-Reply-To: <20240826020942.545155-1-david@gibson.dropbear.id.au>

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@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@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'
+
+
+@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)
-- 
@@ -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


  parent reply	other threads:[~2024-08-26  2:09 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-08-26  2:09 [PATCH v3 00/15] RFC: Proof-of-concept based exeter+Avocado tests David Gibson
2024-08-26  2:09 ` [PATCH v3 01/15] test: run static checkers with Avocado and JSON definitions David Gibson
2024-08-26  2:09 ` [PATCH v3 02/15] test: Adjust how we invoke tests with run_avocado David Gibson
2024-08-26  2:09 ` [PATCH v3 03/15] test: Extend make targets to run Avocado tests David Gibson
2024-08-26  2:09 ` [PATCH v3 04/15] test: Exeter based static tests David Gibson
2024-08-26  2:09 ` [PATCH v3 05/15] tasst: Support library and linters for tests in Python David Gibson
2024-08-26  2:09 ` [PATCH v3 06/15] tasst/cmdsite: Base helpers for running shell commands in various places David Gibson
2024-08-26  2:09 ` [PATCH v3 07/15] test: Add exeter+Avocado based build tests David Gibson
2024-08-26  2:09 ` [PATCH v3 08/15] tasst/unshare: Add helpers to run commands in Linux unshared namespaces David Gibson
2024-08-26  2:09 ` [PATCH v3 09/15] tasst/ip: Helpers for configuring IPv4 and IPv6 David Gibson
2024-08-26  2:09 ` [PATCH v3 10/15] tasst/veth: Helpers for constructing veth devices between namespaces David Gibson
2024-08-26  2:09 ` David Gibson [this message]
2024-08-26  2:09 ` [PATCH v3 12/15] tasst: Helpers for testing NDP behaviour David Gibson
2024-08-26  2:09 ` [PATCH v3 13/15] tasst: Helpers for testing DHCP & DHCPv6 behaviour David Gibson
2024-08-26  2:09 ` [PATCH v3 14/15] tasst: Helpers to construct a simple network environment for tests David Gibson
2024-08-26  2:09 ` [PATCH v3 15/15] avocado: Convert basic pasta tests David Gibson

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240826020942.545155-12-david@gibson.dropbear.id.au \
    --to=david@gibson.dropbear.id.au \
    --cc=crosa@redhat.com \
    --cc=passt-dev@passt.top \
    --cc=sbrivio@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).