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 v2 11/22] tasst: Add helpers to run commands with nstool
Date: Mon,  5 Aug 2024 22:36:50 +1000	[thread overview]
Message-ID: <20240805123701.1720730-12-david@gibson.dropbear.id.au> (raw)
In-Reply-To: <20240805123701.1720730-1-david@gibson.dropbear.id.au>

Use our existing nstool C helper, add python wrappers to easily run
commands in various namespaces.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 test/Makefile          |   8 +-
 test/tasst/__main__.py |   2 +-
 test/tasst/nstool.py   | 170 +++++++++++++++++++++++++++++++++++++++++
 test/tasst/snh.py      |  16 ++++
 4 files changed, 192 insertions(+), 4 deletions(-)
 create mode 100644 test/tasst/nstool.py

diff --git a/test/Makefile b/test/Makefile
index 8373ae77..83725f59 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -64,13 +64,15 @@ LOCAL_ASSETS = mbuto.img mbuto.mem.img podman/bin/podman QEMU_EFI.fd \
 	$(TESTDATA_ASSETS)
 
 ASSETS = $(DOWNLOAD_ASSETS) $(LOCAL_ASSETS)
+AVOCADO_ASSETS =
+META_ASSETS = nstool
 
 EXETER_SH = build/static_checkers.sh
 EXETER_PY = build/build.py
 EXETER_JOBS = $(EXETER_SH:%.sh=%.json) $(EXETER_PY:%.py=%.json)
 AVOCADO_JOBS = $(EXETER_JOBS) avocado/static_checkers.json
 
-TASST_SRCS = __init__.py __main__.py snh.py
+TASST_SRCS = __init__.py __main__.py nstool.py snh.py
 
 EXETER_META = meta/lint.json meta/tasst.json
 META_JOBS = $(EXETER_META)
@@ -157,11 +159,11 @@ meta/tasst.json: $(TASST_SRCS:%=tasst/%) $(VENV) pull-exeter
 	cd ..; PYTHONPATH=$(PYPATH_BASE) $(PYTHON) -m tasst --avocado > test/$@
 
 .PHONY: avocado
-avocado: venv $(AVOCADO_JOBS)
+avocado: venv $(AVOCADO_ASSETS) $(AVOCADO_JOBS)
 	$(RUN_AVOCADO) all $(AVOCADO_JOBS)
 
 .PHONY: meta
-meta: venv $(META_JOBS)
+meta: venv $(META_ASSETS) $(META_JOBS)
 	$(RUN_AVOCADO) meta $(META_JOBS)
 
 flake8:
diff --git a/test/tasst/__main__.py b/test/tasst/__main__.py
index 91499128..9fd6174e 100644
--- a/test/tasst/__main__.py
+++ b/test/tasst/__main__.py
@@ -13,7 +13,7 @@ library of test helpers for passt & pasta
 import exeter
 
 # We import just to get the exeter tests, which flake8 can't see
-from . import snh  # noqa: F401
+from . import nstool, snh  # noqa: F401
 
 
 if __name__ == '__main__':
diff --git a/test/tasst/nstool.py b/test/tasst/nstool.py
new file mode 100644
index 00000000..0b23fbfb
--- /dev/null
+++ b/test/tasst/nstool.py
@@ -0,0 +1,170 @@
+#! /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
+
+nstool.py - Run commands in namespaces via 'nstool'
+"""
+
+import contextlib
+import os
+import subprocess
+import tempfile
+
+import exeter
+
+from .snh import RealHost, SimNetHost
+
+# FIXME: Can this be made more portable?
+UNIX_PATH_MAX = 108
+
+NSTOOL_BIN = 'test/nstool'
+
+
+class NsTool(SimNetHost):
+    """A bundle of Linux namespaces managed by nstool"""
+
+    def __init__(self, name, sockpath, parent=RealHost()):
+        if len(sockpath) > UNIX_PATH_MAX:
+            raise ValueError(
+                f'Unix domain socket path "{sockpath}" is too long'
+            )
+
+        super().__init__(name)
+        self.sockpath = sockpath
+        self.parent = parent
+        self._pid = None
+
+    def __enter__(self):
+        cmd = [f'{NSTOOL_BIN}', 'info', '-wp', f'{self.sockpath}']
+        pid = self.parent.output(*cmd, timeout=1)
+        self._pid = int(pid)
+        return self
+
+    def __exit__(self, *exc_details):
+        pass
+
+    # PID of the nstool hold process as seen by the parent snh
+    def pid(self):
+        return self._pid
+
+    # PID of the nstool hold process as seen by another snh which can
+    # see the nstool socket (important when using PID namespaces)
+    def relative_pid(self, relative_to):
+        cmd = [f'{NSTOOL_BIN}', 'info', '-p', f'{self.sockpath}']
+        relpid = relative_to.output(*cmd)
+        return int(relpid)
+
+    def hostify(self, *cmd, capable=False, **kwargs):
+        hostcmd = [f'{NSTOOL_BIN}', 'exec']
+        if capable:
+            hostcmd.append('--keep-caps')
+        hostcmd += [self.sockpath, '--']
+        hostcmd += list(cmd)
+        return hostcmd, kwargs
+
+
+@contextlib.contextmanager
+def unshare_snh(name, *opts, parent=RealHost(), capable=False):
+    # Create path for temporary nstool Unix socket
+    with tempfile.TemporaryDirectory() as tmpd:
+        sockpath = os.path.join(tmpd, name)
+        cmd = ['unshare'] + list(opts)
+        cmd += ['--', f'{NSTOOL_BIN}', 'hold', f'{sockpath}']
+        with parent.bg(*cmd, capable=capable) as holder:
+            try:
+                with NsTool(name, sockpath, parent=parent) as snh:
+                    yield snh
+            finally:
+                try:
+                    parent.fg(f'{NSTOOL_BIN}', 'stop', f'{sockpath}')
+                finally:
+                    try:
+                        holder.run(timeout=0.1)
+                        holder.kill()
+                    finally:
+                        try:
+                            os.remove(sockpath)
+                        except FileNotFoundError:
+                            pass
+
+
+TEST_EXC = ValueError
+
+
+def test_sockdir_cleanup(s):
+    def mess(sockpaths):
+        with s as snh:
+            ns = snh
+            while isinstance(ns, NsTool):
+                sockpaths.append(ns.sockpath)
+                ns = ns.parent
+            raise TEST_EXC
+
+    sockpaths = []
+    exeter.assert_raises(TEST_EXC, mess, sockpaths)
+    assert sockpaths
+    for path in sockpaths:
+        assert not os.path.exists(os.path.dirname(path))
+
+
+def userns_snh():
+    return unshare_snh('usernetns', '-Ucn')
+
+
+@exeter.test
+def test_userns():
+    cmd = ['capsh', '--has-p=CAP_SETUID']
+    with RealHost() as realhost:
+        status = realhost.fg(*cmd, check=False)
+        assert status.returncode != 0
+    with userns_snh() as ns:
+        ns.fg(*cmd, capable=True)
+
+
+@contextlib.contextmanager
+def nested_snh():
+    with unshare_snh('userns', '-Uc') as userns:
+        with unshare_snh('netns', '-n', parent=userns, capable=True) as netns:
+            yield netns
+
+
+def pidns_snh():
+    return unshare_snh('pidns', '-Upfn')
+
+
+@exeter.test
+def test_relative_pid():
+    with pidns_snh() as snh:
+        # The holder is init (pid 1) within its own pidns
+        exeter.assert_eq(snh.relative_pid(snh), 1)
+
+
+# General tests for all the nstool examples
+for setup in [userns_snh, nested_snh, pidns_snh]:
+    # Common snh tests
+    SimNetHost.selftest_isolated(setup)
+    exeter.register_pipe(f'{setup.__qualname__}|test_sockdir_cleanup',
+                         setup, test_sockdir_cleanup)
+
+
+@contextlib.contextmanager
+def connect_snh():
+    with tempfile.TemporaryDirectory() as tmpd:
+        sockpath = os.path.join(tmpd, 'nons')
+        holdcmd = [f'{NSTOOL_BIN}', 'hold', f'{sockpath}']
+        with subprocess.Popen(holdcmd) as holder:
+            try:
+                with NsTool("fakens", sockpath) as snh:
+                    yield snh
+            finally:
+                holder.kill()
+                os.remove(sockpath)
+
+
+SimNetHost.selftest(connect_snh)
diff --git a/test/tasst/snh.py b/test/tasst/snh.py
index 8ee9802a..598ea979 100644
--- a/test/tasst/snh.py
+++ b/test/tasst/snh.py
@@ -178,6 +178,22 @@ class SimNetHost(contextlib.AbstractContextManager):
             testid = f'{setup.__qualname__}|{t.__qualname__}'
             exeter.register_pipe(testid, setup, t)
 
+    # Additional tests only valid if the snh is isolated (no outside
+    # network connections)
+    def test_is_isolated(self):
+        with self as snh:
+            exeter.assert_eq(snh.ifs(), ['lo'])
+
+    ISOLATED_SELFTESTS = [test_is_isolated]
+
+    @classmethod
+    def selftest_isolated(cls, setup):
+        "Register self tests for an isolated snh example"
+        cls.selftest(setup)
+        for t in cls.ISOLATED_SELFTESTS:
+            testid = f'{setup.__qualname__}|{t.__qualname__}'
+            exeter.register_pipe(testid, setup, t)
+
 
 class RealHost(SimNetHost):
     """Represents the host on which the tests are running (as opposed
-- 
@@ -178,6 +178,22 @@ class SimNetHost(contextlib.AbstractContextManager):
             testid = f'{setup.__qualname__}|{t.__qualname__}'
             exeter.register_pipe(testid, setup, t)
 
+    # Additional tests only valid if the snh is isolated (no outside
+    # network connections)
+    def test_is_isolated(self):
+        with self as snh:
+            exeter.assert_eq(snh.ifs(), ['lo'])
+
+    ISOLATED_SELFTESTS = [test_is_isolated]
+
+    @classmethod
+    def selftest_isolated(cls, setup):
+        "Register self tests for an isolated snh example"
+        cls.selftest(setup)
+        for t in cls.ISOLATED_SELFTESTS:
+            testid = f'{setup.__qualname__}|{t.__qualname__}'
+            exeter.register_pipe(testid, setup, t)
+
 
 class RealHost(SimNetHost):
     """Represents the host on which the tests are running (as opposed
-- 
2.45.2


  parent reply	other threads:[~2024-08-05 12:37 UTC|newest]

Thread overview: 31+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-08-05 12:36 [PATCH v2 00/22] RFC: Proof-of-concept based exeter+Avocado tests David Gibson
2024-08-05 12:36 ` [PATCH v2 01/22] nstool: Fix some trivial typos David Gibson
2024-08-05 12:36 ` [PATCH v2 02/22] nstool: Propagate SIGTERM to processes executed in the namespace David Gibson
2024-08-07  7:23   ` Stefano Brivio
2024-08-05 12:36 ` [PATCH v2 03/22] test: run static checkers with Avocado and JSON definitions David Gibson
2024-08-05 12:36 ` [PATCH v2 04/22] test: Extend make targets to run Avocado tests David Gibson
2024-08-05 12:36 ` [PATCH v2 05/22] test: Exeter based static tests David Gibson
2024-08-05 12:36 ` [PATCH v2 06/22] test: Add exeter+Avocado based build tests David Gibson
2024-08-06 22:11   ` Stefano Brivio
2024-08-07 10:51     ` David Gibson
2024-08-07 13:06       ` Stefano Brivio
2024-08-08  1:28         ` David Gibson
2024-08-08 22:55           ` Stefano Brivio
2024-08-05 12:36 ` [PATCH v2 07/22] test: Add linters for Python code David Gibson
2024-08-05 12:36 ` [PATCH v2 08/22] tasst: Introduce library of common test helpers David Gibson
2024-08-05 12:36 ` [PATCH v2 09/22] tasst: "snh" module for simulated network hosts David Gibson
2024-08-05 12:36 ` [PATCH v2 10/22] tasst: Add helper to get network interface names for a site David Gibson
2024-08-05 12:36 ` David Gibson [this message]
2024-08-05 12:36 ` [PATCH v2 12/22] tasst: Add ifup and network address helpers to SimNetHost David Gibson
2024-08-05 12:36 ` [PATCH v2 13/22] tasst: Helper for creating veth devices between namespaces David Gibson
2024-08-05 12:36 ` [PATCH v2 14/22] tasst: Add helper for getting MTU of a network interface David Gibson
2024-08-05 12:36 ` [PATCH v2 15/22] tasst: Add helper to wait for IP address to appear David Gibson
2024-08-05 12:36 ` [PATCH v2 16/22] tasst: Add helpers for getting a SimNetHost's routes David Gibson
2024-08-05 12:36 ` [PATCH v2 17/22] tasst: Helpers to test transferring data between sites David Gibson
2024-08-05 12:36 ` [PATCH v2 18/22] tasst: IP address allocation helpers David Gibson
2024-08-05 12:36 ` [PATCH v2 19/22] tasst: Helpers for testing NDP behaviour David Gibson
2024-08-05 12:36 ` [PATCH v2 20/22] tasst: Helpers for testing DHCP & DHCPv6 behaviour David Gibson
2024-08-05 12:37 ` [PATCH v2 21/22] tasst: Helpers to construct a simple network environment for tests David Gibson
2024-08-05 12:37 ` [PATCH v2 22/22] avocado: Convert basic pasta tests David Gibson
2024-08-06 12:28 ` [PATCH v2 00/22] RFC: Proof-of-concept based exeter+Avocado tests David Gibson
2024-08-07  8:17   ` Stefano Brivio

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=20240805123701.1720730-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).