public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: David Gibson <david@gibson.dropbear.id.au>
To: passt-dev@passt.top, Stefano Brivio <sbrivio@redhat.com>
Cc: crosa@redhat.com, jarichte@redhat.com,
	David Gibson <david@gibson.dropbear.id.au>
Subject: [PATCH 21/27] tasst: Helpers to test transferring data between sites
Date: Tue, 27 Jun 2023 12:54:22 +1000	[thread overview]
Message-ID: <20230627025429.2209702-22-david@gibson.dropbear.id.au> (raw)
In-Reply-To: <20230627025429.2209702-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 meta tests 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/transfer.py | 175 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 176 insertions(+), 1 deletion(-)
 create mode 100644 test/tasst/transfer.py

diff --git a/test/Makefile b/test/Makefile
index da542d33..953eacf2 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -226,7 +226,7 @@ $(VENV):
 	$(VENV)/bin/pip install -e ./$(PLUGIN)
 
 .PHONY: avocado-assets
-avocado-assets: nstool
+avocado-assets: nstool small.bin medium.bin big.bin
 
 .PHONY: avocado
 avocado: avocado-assets $(VENV)
diff --git a/test/tasst/transfer.py b/test/tasst/transfer.py
new file mode 100644
index 00000000..788c1d52
--- /dev/null
+++ b/test/tasst/transfer.py
@@ -0,0 +1,175 @@
+#! /usr/bin/env avocado-runner-avocado-classless
+
+# 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 contextlib
+import ipaddress
+import time
+
+from avocado_classless.test import assert_eq, test_output
+
+from tasst.nstool import unshare_site
+
+
+# HACK: how long to wait for the server to be ready and listening (s)
+SERVER_READY_DELAY = 0.05  # 1/20th of a second
+
+
+# socat needs IPv6 addresses in square brackets
+def socat_fmt(ip):
+    if isinstance(ip, ipaddress.IPv6Address):
+        return f'[{ip}]'
+    if isinstance(ip, ipaddress.IPv4Address):
+        return f'{ip}'
+    raise TypeError
+
+
+def socat_upload(datafile, cs, ss, connect, listen):
+    cs.require_cmds('socat', 'cat')
+    ss.require_cmds('socat')
+
+    with ss.bg(f'socat -u {listen} STDOUT', verbose=False) as server:
+        time.sleep(SERVER_READY_DELAY)
+        cs.fg(f'socat -u OPEN:{datafile} {connect}')
+        res = server.run()
+    srcdata = cs.output(f'cat {datafile}', verbose=False)
+    assert_eq(srcdata, res.stdout)
+
+
+def socat_download(datafile, cs, ss, connect, listen):
+    cs.require_cmds('socat')
+    ss.require_cmds('socat', 'cat')
+
+    with ss.bg(f'socat -u OPEN:{datafile} {listen}'):
+        time.sleep(SERVER_READY_DELAY)
+        dstdata = cs.output(f'socat -u {connect} STDOUT', verbose=False)
+    srcdata = ss.output(f'cat {datafile}', verbose=False)
+    assert_eq(srcdata, dstdata)
+
+
+# pylint: disable=R0913
+def _tcp_socat(connectip, connectport, listenip, listenport, fromip):
+    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_fmt(listenip)}'
+    if fromip is not None:
+        connect += f',bind={socat_fmt(fromip)}'
+    return (connect, listen)
+
+
+def tcp_upload(datafile, cs, ss, connectip, connectport,
+               listenip=None, listenport=None, fromip=None):
+    connect, listen = _tcp_socat(connectip, connectport, listenip, listenport,
+                                 fromip)
+    socat_upload(datafile, cs, ss, connect, listen)
+
+
+def tcp_download(datafile, cs, ss, connectip, connectport,
+                 listenip=None, listenport=None, fromip=None):
+    connect, listen = _tcp_socat(connectip, connectport, listenip, listenport,
+                                 fromip)
+    socat_download(datafile, cs, ss, connect, listen)
+
+
+def udp_transfer(datafile, cs, ss, connectip, connectport,
+                 listenip=None, listenport=None, fromip=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_fmt(listenip)}'
+    if fromip is not None:
+        connect += f',bind={socat_fmt(fromip)}'
+
+    socat_upload(datafile, cs, ss, connect, listen)
+
+
+def test_tcp_uploads(datafile, ip4, ip6, port,
+                     listenip4=None, listenip6=None, listenport=None,
+                     fromip4=None, fromip6=None):
+    if listenport is None:
+        listenport = port
+
+    def dec(sitefn):
+        def tcp4_upload(sites):
+            with sites as (cs, ss):
+                tcp_upload(datafile, cs, ss, ip4, port,
+                           listenip=listenip4, listenport=listenport,
+                           fromip=fromip4)
+
+        def tcp6_upload(sites):
+            with sites as (cs, ss):
+                tcp_upload(datafile, cs, ss, ip6, port,
+                           listenip=listenip6, listenport=listenport,
+                           fromip=fromip6)
+
+        return test_output(tcp4_upload, tcp6_upload)(sitefn)
+
+    return dec
+
+
+def test_udp_transfers(datafile, ip4, ip6, port,
+                       listenip4=None, listenip6=None, listenport=None,
+                       fromip4=None, fromip6=None):
+    if listenport is None:
+        listenport = port
+
+    def dec(sitefn):
+        def udp4_transfer(sites):
+            with sites as (cs, ss):
+                udp_transfer(datafile, cs, ss, ip4, port,
+                             listenip=listenip4, listenport=listenport,
+                             fromip=fromip4)
+
+        def udp6_transfer(sites):
+            with sites as (cs, ss):
+                udp_transfer(datafile, cs, ss, ip6, port,
+                             listenip=listenip6, listenport=listenport,
+                             fromip=fromip6)
+        return test_output(udp4_transfer, udp6_transfer)(sitefn)
+
+    return dec
+
+
+def test_transfers(ip4, ip6, port, **kwargs):
+    def dec(sitefn):
+        sitefn = test_tcp_uploads('small.bin', ip4, ip6, port,
+                                  **kwargs)(sitefn)
+        sitefn = test_udp_transfers('medium.bin', ip4, ip6, port,
+                                    **kwargs)(sitefn)
+        return sitefn
+
+    return dec
+
+
+@test_transfers(ip4=ipaddress.ip_address('127.0.0.1'),
+                ip6=ipaddress.ip_address('::1'),
+                port=10000)
+@contextlib.contextmanager
+def local_only():
+    with unshare_site('ns', '-Un') as ns:
+        ns.ifup('lo')
+        yield (ns, ns)
-- 
@@ -0,0 +1,175 @@
+#! /usr/bin/env avocado-runner-avocado-classless
+
+# 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 contextlib
+import ipaddress
+import time
+
+from avocado_classless.test import assert_eq, test_output
+
+from tasst.nstool import unshare_site
+
+
+# HACK: how long to wait for the server to be ready and listening (s)
+SERVER_READY_DELAY = 0.05  # 1/20th of a second
+
+
+# socat needs IPv6 addresses in square brackets
+def socat_fmt(ip):
+    if isinstance(ip, ipaddress.IPv6Address):
+        return f'[{ip}]'
+    if isinstance(ip, ipaddress.IPv4Address):
+        return f'{ip}'
+    raise TypeError
+
+
+def socat_upload(datafile, cs, ss, connect, listen):
+    cs.require_cmds('socat', 'cat')
+    ss.require_cmds('socat')
+
+    with ss.bg(f'socat -u {listen} STDOUT', verbose=False) as server:
+        time.sleep(SERVER_READY_DELAY)
+        cs.fg(f'socat -u OPEN:{datafile} {connect}')
+        res = server.run()
+    srcdata = cs.output(f'cat {datafile}', verbose=False)
+    assert_eq(srcdata, res.stdout)
+
+
+def socat_download(datafile, cs, ss, connect, listen):
+    cs.require_cmds('socat')
+    ss.require_cmds('socat', 'cat')
+
+    with ss.bg(f'socat -u OPEN:{datafile} {listen}'):
+        time.sleep(SERVER_READY_DELAY)
+        dstdata = cs.output(f'socat -u {connect} STDOUT', verbose=False)
+    srcdata = ss.output(f'cat {datafile}', verbose=False)
+    assert_eq(srcdata, dstdata)
+
+
+# pylint: disable=R0913
+def _tcp_socat(connectip, connectport, listenip, listenport, fromip):
+    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_fmt(listenip)}'
+    if fromip is not None:
+        connect += f',bind={socat_fmt(fromip)}'
+    return (connect, listen)
+
+
+def tcp_upload(datafile, cs, ss, connectip, connectport,
+               listenip=None, listenport=None, fromip=None):
+    connect, listen = _tcp_socat(connectip, connectport, listenip, listenport,
+                                 fromip)
+    socat_upload(datafile, cs, ss, connect, listen)
+
+
+def tcp_download(datafile, cs, ss, connectip, connectport,
+                 listenip=None, listenport=None, fromip=None):
+    connect, listen = _tcp_socat(connectip, connectport, listenip, listenport,
+                                 fromip)
+    socat_download(datafile, cs, ss, connect, listen)
+
+
+def udp_transfer(datafile, cs, ss, connectip, connectport,
+                 listenip=None, listenport=None, fromip=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_fmt(listenip)}'
+    if fromip is not None:
+        connect += f',bind={socat_fmt(fromip)}'
+
+    socat_upload(datafile, cs, ss, connect, listen)
+
+
+def test_tcp_uploads(datafile, ip4, ip6, port,
+                     listenip4=None, listenip6=None, listenport=None,
+                     fromip4=None, fromip6=None):
+    if listenport is None:
+        listenport = port
+
+    def dec(sitefn):
+        def tcp4_upload(sites):
+            with sites as (cs, ss):
+                tcp_upload(datafile, cs, ss, ip4, port,
+                           listenip=listenip4, listenport=listenport,
+                           fromip=fromip4)
+
+        def tcp6_upload(sites):
+            with sites as (cs, ss):
+                tcp_upload(datafile, cs, ss, ip6, port,
+                           listenip=listenip6, listenport=listenport,
+                           fromip=fromip6)
+
+        return test_output(tcp4_upload, tcp6_upload)(sitefn)
+
+    return dec
+
+
+def test_udp_transfers(datafile, ip4, ip6, port,
+                       listenip4=None, listenip6=None, listenport=None,
+                       fromip4=None, fromip6=None):
+    if listenport is None:
+        listenport = port
+
+    def dec(sitefn):
+        def udp4_transfer(sites):
+            with sites as (cs, ss):
+                udp_transfer(datafile, cs, ss, ip4, port,
+                             listenip=listenip4, listenport=listenport,
+                             fromip=fromip4)
+
+        def udp6_transfer(sites):
+            with sites as (cs, ss):
+                udp_transfer(datafile, cs, ss, ip6, port,
+                             listenip=listenip6, listenport=listenport,
+                             fromip=fromip6)
+        return test_output(udp4_transfer, udp6_transfer)(sitefn)
+
+    return dec
+
+
+def test_transfers(ip4, ip6, port, **kwargs):
+    def dec(sitefn):
+        sitefn = test_tcp_uploads('small.bin', ip4, ip6, port,
+                                  **kwargs)(sitefn)
+        sitefn = test_udp_transfers('medium.bin', ip4, ip6, port,
+                                    **kwargs)(sitefn)
+        return sitefn
+
+    return dec
+
+
+@test_transfers(ip4=ipaddress.ip_address('127.0.0.1'),
+                ip6=ipaddress.ip_address('::1'),
+                port=10000)
+@contextlib.contextmanager
+def local_only():
+    with unshare_site('ns', '-Un') as ns:
+        ns.ifup('lo')
+        yield (ns, ns)
-- 
2.41.0


  parent reply	other threads:[~2023-06-27  2:54 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-06-27  2:54 [PATCH 00/27] RFC: Start converting passt & pasta tests to Avocado using special plugin David Gibson
2023-06-27  2:54 ` [PATCH 01/27] avocado: Make a duplicate copy of testsuite for comparison purposes David Gibson
2023-06-27  2:54 ` [PATCH 02/27] avocado: Don't double download assets for test/ and oldtest/ David Gibson
2023-06-27  2:54 ` [PATCH 03/27] avocado: Move static checkers to avocado David Gibson
2023-06-27  2:54 ` [PATCH 04/27] avocado: Introduce "avocado-classless" plugin, runner and outline David Gibson
2023-06-27  2:54 ` [PATCH 05/27] avocado, test: Add static checkers for Python code David Gibson
2023-06-27  2:54 ` [PATCH 06/27] avocado: Resolver implementation for avocado-classless plugin David Gibson
2023-06-27  2:54 ` [PATCH 07/27] avocado: Add basic assertion helpers to " David Gibson
2023-06-27  2:54 ` [PATCH 08/27] tasst, avocado: Introduce library of common test helpers David Gibson
2023-06-27  2:54 ` [PATCH 09/27] avocado-classless: Test matrices by composition David Gibson
2023-06-27  2:54 ` [PATCH 10/27] tasst: Helper functions for executing commands in different places David Gibson
2023-06-27  2:54 ` [PATCH 11/27] avocado-classless: Allow overriding default timeout David Gibson
2023-06-27  2:54 ` [PATCH 12/27] avocado: Convert build tests to avocado David Gibson
2023-06-27  2:54 ` [PATCH 13/27] tasst: Add helpers for running background commands on sites David Gibson
2023-06-27  2:54 ` [PATCH 14/27] tasst: Add helper to get network interface names for a site David Gibson
2023-06-27  2:54 ` [PATCH 15/27] tasst: Add helpers to run commands with nstool David Gibson
2023-06-27  2:54 ` [PATCH 16/27] tasst: Add ifup and network address helpers to Site David Gibson
2023-06-27  2:54 ` [PATCH 17/27] tasst: Helper for creating veth devices between namespaces David Gibson
2023-06-27  2:54 ` [PATCH 18/27] tasst: Add helper for getting MTU of a network interface David Gibson
2023-06-27  2:54 ` [PATCH 19/27] tasst: Add helper to wait for IP address to appear David Gibson
2023-06-27  2:54 ` [PATCH 20/27] tasst: Add helpers for getting a site's routes David Gibson
2023-06-27  2:54 ` David Gibson [this message]
2023-06-27  2:54 ` [PATCH 22/27] tasst: IP address allocation helpers David Gibson
2023-06-27  2:54 ` [PATCH 23/27] tasst: Helpers for running daemons with a pidfile David Gibson
2023-06-27  2:54 ` [PATCH 24/27] tasst: Helpers for testing NDP behaviour David Gibson
2023-06-27  2:54 ` [PATCH 25/27] tasst: Helpers for testing DHCP & DHCPv6 behaviour David Gibson
2023-06-27  2:54 ` [PATCH 26/27] tasst: Helpers to construct a simple network environment for tests David Gibson
2023-06-27  2:54 ` [PATCH 27/27] avocado: Convert basic pasta tests David Gibson
2023-07-05  0:30   ` Stefano Brivio
2023-07-05  3:27     ` David Gibson
2023-07-07 17:42       ` Stefano Brivio
2023-07-10  7:45         ` 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=20230627025429.2209702-22-david@gibson.dropbear.id.au \
    --to=david@gibson.dropbear.id.au \
    --cc=crosa@redhat.com \
    --cc=jarichte@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).