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 15/27] tasst: Add helpers to run commands with nstool
Date: Tue, 27 Jun 2023 12:54:16 +1000 [thread overview]
Message-ID: <20230627025429.2209702-16-david@gibson.dropbear.id.au> (raw)
In-Reply-To: <20230627025429.2209702-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 | 2 +-
test/tasst/exesite.py | 8 ++
test/tasst/nstool.py | 168 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 177 insertions(+), 1 deletion(-)
create mode 100644 test/tasst/nstool.py
diff --git a/test/Makefile b/test/Makefile
index 58159c83..da542d33 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -226,7 +226,7 @@ $(VENV):
$(VENV)/bin/pip install -e ./$(PLUGIN)
.PHONY: avocado-assets
-avocado-assets:
+avocado-assets: nstool
.PHONY: avocado
avocado: avocado-assets $(VENV)
diff --git a/test/tasst/exesite.py b/test/tasst/exesite.py
index 811b670e..2e15129f 100644
--- a/test/tasst/exesite.py
+++ b/test/tasst/exesite.py
@@ -161,6 +161,14 @@ def test_site(sitefn):
test_has_lo)(sitefn)
+def test_isolated_site(sitefn):
+ def test_isolated_net(s):
+ with s as site:
+ assert_eq(site.ifs(), ['lo'])
+
+ return test_output(test_isolated_net)(test_site(sitefn))
+
+
class RealHost(Site):
"""Represents the host on which the tests are running (as opposed
to some simulated host created by the tests)
diff --git a/test/tasst/nstool.py b/test/tasst/nstool.py
new file mode 100644
index 00000000..f05c420d
--- /dev/null
+++ b/test/tasst/nstool.py
@@ -0,0 +1,168 @@
+#! /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
+
+nstool.py - Run commands in namespaces via 'nstool'
+"""
+
+import contextlib
+import os
+import tempfile
+
+from avocado.utils.process import CmdError
+from avocado_classless.test import assert_eq, assert_raises, test_output
+
+from tasst.exesite import Site, REAL_HOST, test_isolated_site, test_site
+from tasst.typecheck import typecheck
+
+# FIXME: Can this be made more portable? # pylint: disable=W0511
+UNIX_PATH_MAX = 108
+
+NSTOOL_BIN = './nstool'
+
+
+class NsToolSite(Site):
+ """A bundle of Linux namespaces managed by nstool"""
+
+ def __init__(self, name, sockpath, parent=REAL_HOST):
+ if len(sockpath) > UNIX_PATH_MAX:
+ raise ValueError(
+ f'Unix domain socket path "{sockpath}" is too long'
+ )
+
+ super().__init__(name)
+ self.sockpath = typecheck(sockpath, str)
+ self.parent = typecheck(parent, Site)
+ self._pid = None
+
+ def __enter__(self):
+ pid = self.parent.output(f'{NSTOOL_BIN} info -wp {self.sockpath}',
+ verbose=False, timeout=1)
+ self._pid = int(pid)
+ return self
+
+ def __exit__(self, *exc_details):
+ pass
+
+ # PID of the nstool hold process as seen by the test host
+ def pid(self):
+ return self._pid
+
+ # PID of the nstool hold process as seen by another site
+ # (important when using PID namespaces)
+ def relative_pid(self, relative_to):
+ relpid = relative_to.output(f'{NSTOOL_BIN} info -p {self.sockpath}')
+ return int(relpid)
+
+ def hostify(self, cmd, *, sudo=False, **kwargs):
+ nst_args = self.sockpath
+ if sudo:
+ nst_args = '--keep-caps ' + nst_args
+ return f'{NSTOOL_BIN} exec {nst_args} -- {cmd}', kwargs
+
+
+@contextlib.contextmanager
+def unshare_site(nsname, unshare_opts, parent=REAL_HOST, sudo=False):
+ unshare_opts = typecheck(unshare_opts, str)
+ parent = typecheck(parent, Site)
+ sudo = typecheck(sudo, bool)
+ parent.require_cmds('unshare', NSTOOL_BIN)
+
+ # Create path for temporary nstool Unix socket
+ #
+ # Using Avocado's workdir often gives paths that are too lonhg for
+ # Unix sockets
+ with tempfile.TemporaryDirectory() as tmpd:
+ sockpath = os.path.join(tmpd, nsname)
+ holdcmd = f'unshare {unshare_opts} -- {NSTOOL_BIN} hold {sockpath}'
+ with parent.bg(holdcmd, sudo=sudo) as holder:
+ try:
+ with NsToolSite(nsname, sockpath, parent=parent) as site:
+ yield site
+ finally:
+ try:
+ parent.fg(f'{NSTOOL_BIN} stop {sockpath}')
+ finally:
+ try:
+ holder.run(timeout=0.1)
+ finally:
+ try:
+ os.remove(sockpath)
+ except FileNotFoundError:
+ pass
+
+
+TEST_EXC = ValueError
+
+
+def test_sockdir_cleanup(s):
+ def mess(sockpaths):
+ with s as site:
+ ns = site
+ while isinstance(ns, NsToolSite):
+ sockpaths.append(ns.sockpath)
+ ns = ns.parent
+ raise TEST_EXC
+
+ sockpaths = []
+ assert_raises(TEST_EXC, mess, sockpaths)
+ assert sockpaths
+ for path in sockpaths:
+ assert not os.path.exists(os.path.dirname(path))
+
+
+def test_userns(nstool_site):
+ REAL_HOST.require_cmds('capsh')
+ with nstool_site as ns:
+ ns.require_cmds('capsh')
+ capcmd = 'capsh --has-p=CAP_SETUID'
+ assert_raises(CmdError, REAL_HOST.fg, capcmd)
+ ns.fg(capcmd, sudo=True)
+
+
+@test_output(test_userns, test_sockdir_cleanup)
+@test_isolated_site
+def userns_site():
+ return unshare_site('usernetns', '-Ucn')
+
+
+@test_output(test_sockdir_cleanup)
+@test_isolated_site
+@contextlib.contextmanager
+def nested_site():
+ with unshare_site('userns', '-Uc') as userns:
+ with unshare_site('netns', '-n', parent=userns, sudo=True) as netns:
+ yield netns
+
+
+def test_relative_pid(s):
+ with s as site:
+ # The holder is init (pid 1) within its own pidns
+ assert_eq(site.relative_pid(site), 1)
+
+
+@test_output(test_relative_pid, test_sockdir_cleanup)
+@test_isolated_site
+def pidns_site():
+ return unshare_site('pidns', '-Upfn')
+
+
+@test_site
+@contextlib.contextmanager
+def connect_site():
+ with tempfile.TemporaryDirectory() as tmpd:
+ sockpath = os.path.join(tmpd, 'nons')
+ holdcmd = f'{NSTOOL_BIN} hold {sockpath}'
+ try:
+ with REAL_HOST.bg(holdcmd, ignore_status=True,
+ context_timeout=0.1):
+ with NsToolSite("fake ns", sockpath) as site:
+ yield site
+ finally:
+ os.remove(sockpath)
--
@@ -0,0 +1,168 @@
+#! /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
+
+nstool.py - Run commands in namespaces via 'nstool'
+"""
+
+import contextlib
+import os
+import tempfile
+
+from avocado.utils.process import CmdError
+from avocado_classless.test import assert_eq, assert_raises, test_output
+
+from tasst.exesite import Site, REAL_HOST, test_isolated_site, test_site
+from tasst.typecheck import typecheck
+
+# FIXME: Can this be made more portable? # pylint: disable=W0511
+UNIX_PATH_MAX = 108
+
+NSTOOL_BIN = './nstool'
+
+
+class NsToolSite(Site):
+ """A bundle of Linux namespaces managed by nstool"""
+
+ def __init__(self, name, sockpath, parent=REAL_HOST):
+ if len(sockpath) > UNIX_PATH_MAX:
+ raise ValueError(
+ f'Unix domain socket path "{sockpath}" is too long'
+ )
+
+ super().__init__(name)
+ self.sockpath = typecheck(sockpath, str)
+ self.parent = typecheck(parent, Site)
+ self._pid = None
+
+ def __enter__(self):
+ pid = self.parent.output(f'{NSTOOL_BIN} info -wp {self.sockpath}',
+ verbose=False, timeout=1)
+ self._pid = int(pid)
+ return self
+
+ def __exit__(self, *exc_details):
+ pass
+
+ # PID of the nstool hold process as seen by the test host
+ def pid(self):
+ return self._pid
+
+ # PID of the nstool hold process as seen by another site
+ # (important when using PID namespaces)
+ def relative_pid(self, relative_to):
+ relpid = relative_to.output(f'{NSTOOL_BIN} info -p {self.sockpath}')
+ return int(relpid)
+
+ def hostify(self, cmd, *, sudo=False, **kwargs):
+ nst_args = self.sockpath
+ if sudo:
+ nst_args = '--keep-caps ' + nst_args
+ return f'{NSTOOL_BIN} exec {nst_args} -- {cmd}', kwargs
+
+
+@contextlib.contextmanager
+def unshare_site(nsname, unshare_opts, parent=REAL_HOST, sudo=False):
+ unshare_opts = typecheck(unshare_opts, str)
+ parent = typecheck(parent, Site)
+ sudo = typecheck(sudo, bool)
+ parent.require_cmds('unshare', NSTOOL_BIN)
+
+ # Create path for temporary nstool Unix socket
+ #
+ # Using Avocado's workdir often gives paths that are too lonhg for
+ # Unix sockets
+ with tempfile.TemporaryDirectory() as tmpd:
+ sockpath = os.path.join(tmpd, nsname)
+ holdcmd = f'unshare {unshare_opts} -- {NSTOOL_BIN} hold {sockpath}'
+ with parent.bg(holdcmd, sudo=sudo) as holder:
+ try:
+ with NsToolSite(nsname, sockpath, parent=parent) as site:
+ yield site
+ finally:
+ try:
+ parent.fg(f'{NSTOOL_BIN} stop {sockpath}')
+ finally:
+ try:
+ holder.run(timeout=0.1)
+ finally:
+ try:
+ os.remove(sockpath)
+ except FileNotFoundError:
+ pass
+
+
+TEST_EXC = ValueError
+
+
+def test_sockdir_cleanup(s):
+ def mess(sockpaths):
+ with s as site:
+ ns = site
+ while isinstance(ns, NsToolSite):
+ sockpaths.append(ns.sockpath)
+ ns = ns.parent
+ raise TEST_EXC
+
+ sockpaths = []
+ assert_raises(TEST_EXC, mess, sockpaths)
+ assert sockpaths
+ for path in sockpaths:
+ assert not os.path.exists(os.path.dirname(path))
+
+
+def test_userns(nstool_site):
+ REAL_HOST.require_cmds('capsh')
+ with nstool_site as ns:
+ ns.require_cmds('capsh')
+ capcmd = 'capsh --has-p=CAP_SETUID'
+ assert_raises(CmdError, REAL_HOST.fg, capcmd)
+ ns.fg(capcmd, sudo=True)
+
+
+@test_output(test_userns, test_sockdir_cleanup)
+@test_isolated_site
+def userns_site():
+ return unshare_site('usernetns', '-Ucn')
+
+
+@test_output(test_sockdir_cleanup)
+@test_isolated_site
+@contextlib.contextmanager
+def nested_site():
+ with unshare_site('userns', '-Uc') as userns:
+ with unshare_site('netns', '-n', parent=userns, sudo=True) as netns:
+ yield netns
+
+
+def test_relative_pid(s):
+ with s as site:
+ # The holder is init (pid 1) within its own pidns
+ assert_eq(site.relative_pid(site), 1)
+
+
+@test_output(test_relative_pid, test_sockdir_cleanup)
+@test_isolated_site
+def pidns_site():
+ return unshare_site('pidns', '-Upfn')
+
+
+@test_site
+@contextlib.contextmanager
+def connect_site():
+ with tempfile.TemporaryDirectory() as tmpd:
+ sockpath = os.path.join(tmpd, 'nons')
+ holdcmd = f'{NSTOOL_BIN} hold {sockpath}'
+ try:
+ with REAL_HOST.bg(holdcmd, ignore_status=True,
+ context_timeout=0.1):
+ with NsToolSite("fake ns", sockpath) as site:
+ yield site
+ finally:
+ os.remove(sockpath)
--
2.41.0
next prev 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 ` David Gibson [this message]
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 ` [PATCH 21/27] tasst: Helpers to test transferring data between sites David Gibson
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-16-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).