summaryrefslogtreecommitdiffstats
path: root/tests/topotests
diff options
context:
space:
mode:
authorChristian Hopps <chopps@gmail.com>2023-03-24 14:06:38 +0100
committerChristian Hopps <chopps@labn.net>2023-04-15 19:29:38 +0200
commit60e037780e8fb76a95028f2f1e3c56f83b6cc74e (patch)
tree4420b3165b38afa6b9eeb1d67e397989ff15b11a /tests/topotests
parenttests: import munet 0.12.12 (diff)
downloadfrr-60e037780e8fb76a95028f2f1e3c56f83b6cc74e.tar.xz
frr-60e037780e8fb76a95028f2f1e3c56f83b6cc74e.zip
tests: switch to munet
Signed-off-by: Christian Hopps <chopps@labn.net>
Diffstat (limited to 'tests/topotests')
-rwxr-xr-xtests/topotests/conftest.py14
-rwxr-xr-xtests/topotests/lib/grpc-query.py3
-rw-r--r--tests/topotests/lib/micronet.py1018
-rw-r--r--tests/topotests/lib/micronet_cli.py306
-rw-r--r--tests/topotests/lib/micronet_compat.py343
-rw-r--r--tests/topotests/lib/topogen.py18
-rw-r--r--tests/topotests/lib/topotest.py8
-rw-r--r--tests/topotests/pytest.ini2
8 files changed, 264 insertions, 1448 deletions
diff --git a/tests/topotests/conftest.py b/tests/topotests/conftest.py
index df5d06602..4224b037b 100755
--- a/tests/topotests/conftest.py
+++ b/tests/topotests/conftest.py
@@ -7,21 +7,23 @@ import glob
import os
import pdb
import re
+import resource
import subprocess
import sys
import time
-import resource
import pytest
+
import lib.fixtures
from lib import topolog
-from lib.micronet import Commander, proc_error
-from lib.micronet_cli import cli
-from lib.micronet_compat import Mininet, cleanup_current, cleanup_previous
+from lib.micronet_compat import Mininet
from lib.topogen import diagnose_env, get_topogen
from lib.topolog import logger
from lib.topotest import g_extra_config as topotest_extra_config
from lib.topotest import json_cmp_result
+from munet.base import Commander, proc_error
+from munet.cleanup import cleanup_current, cleanup_previous
+from munet import cli
def pytest_addoption(parser):
@@ -501,7 +503,7 @@ def pytest_runtest_makereport(item, call):
# Really would like something better than using this global here.
# Not all tests use topogen though so get_topogen() won't work.
if Mininet.g_mnet_inst:
- cli(Mininet.g_mnet_inst, title=title, background=False)
+ cli.cli(Mininet.g_mnet_inst, title=title, background=False)
else:
logger.error("Could not launch CLI b/c no mininet exists yet")
@@ -515,7 +517,7 @@ def pytest_runtest_makereport(item, call):
user = user.strip()
if user == "cli":
- cli(Mininet.g_mnet_inst)
+ cli.cli(Mininet.g_mnet_inst)
elif user == "pdb":
pdb.set_trace() # pylint: disable=forgotten-debug-statement
elif user:
diff --git a/tests/topotests/lib/grpc-query.py b/tests/topotests/lib/grpc-query.py
index 6457bbefd..5dd12d581 100755
--- a/tests/topotests/lib/grpc-query.py
+++ b/tests/topotests/lib/grpc-query.py
@@ -21,7 +21,8 @@ try:
import grpc
import grpc_tools
- from micronet import commander
+ sys.path.append(os.path.dirname(CWD))
+ from munet.base import commander
commander.cmd_raises(f"cp {CWD}/../../../grpc/frr-northbound.proto .")
commander.cmd_raises(
diff --git a/tests/topotests/lib/micronet.py b/tests/topotests/lib/micronet.py
index 138100916..f4aa8278f 100644
--- a/tests/topotests/lib/micronet.py
+++ b/tests/topotests/lib/micronet.py
@@ -3,1004 +3,22 @@
#
# July 9 2021, Christian Hopps <chopps@labn.net>
#
-# Copyright (c) 2021, LabN Consulting, L.L.C.
+# Copyright (c) 2021-2023, LabN Consulting, L.L.C.
#
-import datetime
-import logging
-import os
-import re
-import shlex
-import subprocess
-import sys
-import tempfile
-import time as time_mod
-import traceback
-
-root_hostname = subprocess.check_output("hostname")
-
-# This allows us to cleanup any leftovers later on
-os.environ["MICRONET_PID"] = str(os.getpid())
-
-
-class Timeout(object):
- def __init__(self, delta):
- self.started_on = datetime.datetime.now()
- self.expires_on = self.started_on + datetime.timedelta(seconds=delta)
-
- def elapsed(self):
- elapsed = datetime.datetime.now() - self.started_on
- return elapsed.total_seconds()
-
- def is_expired(self):
- return datetime.datetime.now() > self.expires_on
-
-
-def is_string(value):
- """Return True if value is a string."""
- try:
- return isinstance(value, basestring) # type: ignore
- except NameError:
- return isinstance(value, str)
-
-
-def shell_quote(command):
- """Return command wrapped in single quotes."""
- if sys.version_info[0] >= 3:
- return shlex.quote(command)
- return "'{}'".format(command.replace("'", "'\"'\"'")) # type: ignore
-
-
-def cmd_error(rc, o, e):
- s = "rc {}".format(rc)
- o = "\n\tstdout: " + o.strip() if o and o.strip() else ""
- e = "\n\tstderr: " + e.strip() if e and e.strip() else ""
- return s + o + e
-
-
-def proc_error(p, o, e):
- args = p.args if is_string(p.args) else " ".join(p.args)
- s = "rc {} pid {}\n\targs: {}".format(p.returncode, p.pid, args)
- o = "\n\tstdout: " + o.strip() if o and o.strip() else ""
- e = "\n\tstderr: " + e.strip() if e and e.strip() else ""
- return s + o + e
-
-
-def comm_error(p):
- rc = p.poll()
- assert rc is not None
- if not hasattr(p, "saved_output"):
- p.saved_output = p.communicate()
- return proc_error(p, *p.saved_output)
-
-
-class Commander(object): # pylint: disable=R0205
- """
- Commander.
-
- An object that can execute commands.
- """
-
- tmux_wait_gen = 0
-
- def __init__(self, name, logger=None):
- """Create a Commander."""
- self.name = name
- self.last = None
- self.exec_paths = {}
- self.pre_cmd = []
- self.pre_cmd_str = ""
-
- if not logger:
- self.logger = logging.getLogger(__name__ + ".commander." + name)
- else:
- self.logger = logger
-
- self.cwd = self.cmd_raises("pwd").strip()
-
- def set_logger(self, logfile):
- self.logger = logging.getLogger(__name__ + ".commander." + self.name)
- if is_string(logfile):
- handler = logging.FileHandler(logfile, mode="w")
- else:
- handler = logging.StreamHandler(logfile)
-
- fmtstr = "%(asctime)s.%(msecs)03d %(levelname)s: {}({}): %(message)s".format(
- self.__class__.__name__, self.name
- )
- handler.setFormatter(logging.Formatter(fmt=fmtstr))
- self.logger.addHandler(handler)
-
- def set_pre_cmd(self, pre_cmd=None):
- if not pre_cmd:
- self.pre_cmd = []
- self.pre_cmd_str = ""
- else:
- self.pre_cmd = pre_cmd
- self.pre_cmd_str = " ".join(self.pre_cmd) + " "
-
- def __str__(self):
- return "Commander({})".format(self.name)
-
- def get_exec_path(self, binary):
- """Return the full path to the binary executable.
-
- `binary` :: binary name or list of binary names
- """
- if is_string(binary):
- bins = [binary]
- else:
- bins = binary
- for b in bins:
- if b in self.exec_paths:
- return self.exec_paths[b]
-
- rc, output, _ = self.cmd_status("which " + b, warn=False)
- if not rc:
- return os.path.abspath(output.strip())
- return None
-
- def get_tmp_dir(self, uniq):
- return os.path.join(tempfile.mkdtemp(), uniq)
-
- def test(self, flags, arg):
- """Run test binary, with flags and arg"""
- test_path = self.get_exec_path(["test"])
- rc, output, _ = self.cmd_status([test_path, flags, arg], warn=False)
- return not rc
-
- def path_exists(self, path):
- """Check if path exists."""
- return self.test("-e", path)
-
- def _get_cmd_str(self, cmd):
- if is_string(cmd):
- return self.pre_cmd_str + cmd
- cmd = self.pre_cmd + cmd
- return " ".join(cmd)
-
- def _get_sub_args(self, cmd, defaults, **kwargs):
- if is_string(cmd):
- defaults["shell"] = True
- pre_cmd = self.pre_cmd_str
- else:
- defaults["shell"] = False
- pre_cmd = self.pre_cmd
- cmd = [str(x) for x in cmd]
- defaults.update(kwargs)
- return pre_cmd, cmd, defaults
-
- def _popen(self, method, cmd, skip_pre_cmd=False, **kwargs):
- if sys.version_info[0] >= 3:
- defaults = {
- "encoding": "utf-8",
- "stdout": subprocess.PIPE,
- "stderr": subprocess.PIPE,
- }
- else:
- defaults = {
- "stdout": subprocess.PIPE,
- "stderr": subprocess.PIPE,
- }
- pre_cmd, cmd, defaults = self._get_sub_args(cmd, defaults, **kwargs)
-
- self.logger.debug('%s: %s("%s", kwargs: %s)', self, method, cmd, defaults)
-
- actual_cmd = cmd if skip_pre_cmd else pre_cmd + cmd
- p = subprocess.Popen(actual_cmd, **defaults)
- if not hasattr(p, "args"):
- p.args = actual_cmd
- return p, actual_cmd
-
- def set_cwd(self, cwd):
- self.logger.warning("%s: 'cd' (%s) does not work outside namespaces", self, cwd)
- self.cwd = cwd
-
- def popen(self, cmd, **kwargs):
- """
- Creates a pipe with the given `command`.
-
- Args:
- command: `str` or `list` of command to open a pipe with.
- **kwargs: kwargs is eventually passed on to Popen. If `command` is a string
- then will be invoked with shell=True, otherwise `command` is a list and
- will be invoked with shell=False.
-
- Returns:
- a subprocess.Popen object.
- """
- p, _ = self._popen("popen", cmd, **kwargs)
- return p
-
- def cmd_status(self, cmd, raises=False, warn=True, stdin=None, **kwargs):
- """Execute a command."""
-
- # We are not a shell like mininet, so we need to intercept this
- chdir = False
- if not is_string(cmd):
- cmds = cmd
- else:
- # XXX we can drop this when the code stops assuming it works
- m = re.match(r"cd(\s*|\s+(\S+))$", cmd)
- if m and m.group(2):
- self.logger.warning(
- "Bad call to 'cd' (chdir) emulating, use self.set_cwd():\n%s",
- "".join(traceback.format_stack(limit=12)),
- )
- assert is_string(cmd)
- chdir = True
- cmd += " && pwd"
-
- # If we are going to run under bash then we don't need shell=True!
- cmds = ["/bin/bash", "-c", cmd]
-
- pinput = None
-
- if is_string(stdin) or isinstance(stdin, bytes):
- pinput = stdin
- stdin = subprocess.PIPE
-
- p, actual_cmd = self._popen("cmd_status", cmds, stdin=stdin, **kwargs)
- stdout, stderr = p.communicate(input=pinput)
- rc = p.wait()
-
- # For debugging purposes.
- self.last = (rc, actual_cmd, cmd, stdout, stderr)
-
- if rc:
- if warn:
- self.logger.warning(
- "%s: proc failed: %s:", self, proc_error(p, stdout, stderr)
- )
- if raises:
- # error = Exception("stderr: {}".format(stderr))
- # This annoyingly doesn't' show stderr when printed normally
- error = subprocess.CalledProcessError(rc, actual_cmd)
- error.stdout, error.stderr = stdout, stderr
- raise error
- elif chdir:
- self.set_cwd(stdout.strip())
-
- return rc, stdout, stderr
-
- def cmd_legacy(self, cmd, **kwargs):
- """Execute a command with stdout and stderr joined, *IGNORES ERROR*."""
-
- defaults = {"stderr": subprocess.STDOUT}
- defaults.update(kwargs)
- _, stdout, _ = self.cmd_status(cmd, raises=False, **defaults)
- return stdout
-
- def cmd_raises(self, cmd, **kwargs):
- """Execute a command. Raise an exception on errors"""
-
- rc, stdout, _ = self.cmd_status(cmd, raises=True, **kwargs)
- assert rc == 0
- return stdout
-
- # Run a command in a new window (gnome-terminal, screen, tmux, xterm)
- def run_in_window(
- self,
- cmd,
- wait_for=False,
- background=False,
- name=None,
- title=None,
- forcex=False,
- new_window=False,
- tmux_target=None,
- ):
- """
- Run a command in a new window (TMUX, Screen or XTerm).
-
- Args:
- wait_for: True to wait for exit from command or `str` as channel neme to signal on exit, otherwise False
- background: Do not change focus to new window.
- title: Title for new pane (tmux) or window (xterm).
- name: Name of the new window (tmux)
- forcex: Force use of X11.
- new_window: Open new window (instead of pane) in TMUX
- tmux_target: Target for tmux pane.
-
- Returns:
- the pane/window identifier from TMUX (depends on `new_window`)
- """
-
- channel = None
- if is_string(wait_for):
- channel = wait_for
- elif wait_for is True:
- channel = "{}-wait-{}".format(os.getpid(), Commander.tmux_wait_gen)
- Commander.tmux_wait_gen += 1
-
- sudo_path = self.get_exec_path(["sudo"])
- nscmd = sudo_path + " " + self.pre_cmd_str + cmd
- if "TMUX" in os.environ and not forcex:
- cmd = [self.get_exec_path("tmux")]
- if new_window:
- cmd.append("new-window")
- cmd.append("-P")
- if name:
- cmd.append("-n")
- cmd.append(name)
- if tmux_target:
- cmd.append("-t")
- cmd.append(tmux_target)
- else:
- cmd.append("split-window")
- cmd.append("-P")
- cmd.append("-h")
- if not tmux_target:
- tmux_target = os.getenv("TMUX_PANE", "")
- if background:
- cmd.append("-d")
- if tmux_target:
- cmd.append("-t")
- cmd.append(tmux_target)
- if title:
- nscmd = "printf '\033]2;{}\033\\'; {}".format(title, nscmd)
- if channel:
- nscmd = 'trap "tmux wait -S {}; exit 0" EXIT; {}'.format(channel, nscmd)
- cmd.append(nscmd)
- elif "STY" in os.environ and not forcex:
- # wait for not supported in screen for now
- channel = None
- cmd = [self.get_exec_path("screen")]
- if title:
- cmd.append("-t")
- cmd.append(title)
- if not os.path.exists(
- "/run/screen/S-{}/{}".format(os.environ["USER"], os.environ["STY"])
- ):
- cmd = ["sudo", "-u", os.environ["SUDO_USER"]] + cmd
- cmd.extend(nscmd.split(" "))
- elif "DISPLAY" in os.environ:
- # We need it broken up for xterm
- user_cmd = cmd
- cmd = [self.get_exec_path("xterm")]
- if "SUDO_USER" in os.environ:
- cmd = [self.get_exec_path("sudo"), "-u", os.environ["SUDO_USER"]] + cmd
- if title:
- cmd.append("-T")
- cmd.append(title)
- cmd.append("-e")
- cmd.append(sudo_path)
- cmd.extend(self.pre_cmd)
- cmd.extend(["bash", "-c", user_cmd])
- # if channel:
- # return self.cmd_raises(cmd, skip_pre_cmd=True)
- # else:
- p = self.popen(
- cmd,
- skip_pre_cmd=True,
- stdin=None,
- shell=False,
- )
- time_mod.sleep(2)
- if p.poll() is not None:
- self.logger.error("%s: Failed to launch xterm: %s", self, comm_error(p))
- return p
- else:
- self.logger.error(
- "DISPLAY, STY, and TMUX not in environment, can't open window"
- )
- raise Exception("Window requestd but TMUX, Screen and X11 not available")
-
- pane_info = self.cmd_raises(cmd, skip_pre_cmd=True).strip()
-
- # Re-adjust the layout
- if "TMUX" in os.environ:
- self.cmd_status(
- "tmux select-layout -t {} tiled".format(
- pane_info if not tmux_target else tmux_target
- ),
- skip_pre_cmd=True,
- )
-
- # Wait here if we weren't handed the channel to wait for
- if channel and wait_for is True:
- cmd = [self.get_exec_path("tmux"), "wait", channel]
- self.cmd_status(cmd, skip_pre_cmd=True)
-
- return pane_info
-
- def delete(self):
- pass
-
-
-class LinuxNamespace(Commander):
- """
- A linux Namespace.
-
- An object that creates and executes commands in a linux namespace
- """
-
- def __init__(
- self,
- name,
- net=True,
- mount=True,
- uts=True,
- cgroup=False,
- ipc=False,
- pid=False,
- time=False,
- user=False,
- set_hostname=True,
- private_mounts=None,
- logger=None,
- ):
- """
- Create a new linux namespace.
-
- Args:
- name: Internal name for the namespace.
- net: Create network namespace.
- mount: Create network namespace.
- uts: Create UTS (hostname) namespace.
- cgroup: Create cgroup namespace.
- ipc: Create IPC namespace.
- pid: Create PID namespace, also mounts new /proc.
- time: Create time namespace.
- user: Create user namespace, also keeps capabilities.
- set_hostname: Set the hostname to `name`, uts must also be True.
- private_mounts: List of strings of the form
- "[/external/path:]/internal/path. If no external path is specified a
- tmpfs is mounted on the internal path. Any paths specified are first
- passed to `mkdir -p`.
- logger: Passed to superclass.
- """
- super(LinuxNamespace, self).__init__(name, logger)
-
- self.logger.debug("%s: Creating", self)
-
- self.intfs = []
-
- nslist = []
- cmd = ["/usr/bin/unshare"]
- flags = ""
- self.a_flags = []
- self.ifnetns = {}
-
- if cgroup:
- nslist.append("cgroup")
- flags += "C"
- if ipc:
- nslist.append("ipc")
- flags += "i"
- if mount:
- nslist.append("mnt")
- flags += "m"
- if net:
- nslist.append("net")
- flags += "n"
- if pid:
- nslist.append("pid")
- flags += "f"
- flags += "p"
- cmd.append("--mount-proc")
- if time:
- # XXX this filename is probably wrong
- nslist.append("time")
- flags += "T"
- if user:
- nslist.append("user")
- flags += "U"
- cmd.append("--keep-caps")
- if uts:
- nslist.append("uts")
- flags += "u"
-
- if flags:
- aflags = flags.replace("f", "")
- if aflags:
- self.a_flags = ["-" + x for x in aflags]
- cmd.extend(["-" + x for x in flags])
-
- if pid:
- cmd.append(commander.get_exec_path("tini"))
- cmd.append("-vvv")
- cmd.append("/bin/cat")
-
- # Using cat and a stdin PIPE is nice as it will exit when we do. However, we
- # also detach it from the pgid so that signals do not propagate to it. This is
- # b/c it would exit early (e.g., ^C) then, at least the main micronet proc which
- # has no other processes like frr daemons running, will take the main network
- # namespace with it, which will remove the bridges and the veth pair (because
- # the bridge side veth is deleted).
- self.logger.debug("%s: creating namespace process: %s", self, cmd)
- p = subprocess.Popen(
- cmd,
- stdin=subprocess.PIPE,
- stdout=open("/dev/null", "w"),
- stderr=open("/dev/null", "w"),
- text=True,
- start_new_session=True, # detach from pgid so signals don't propagate
- shell=False,
- )
- self.p = p
- self.pid = p.pid
-
- self.logger.debug("%s: namespace pid: %d", self, self.pid)
-
- # -----------------------------------------------
- # Now let's wait until unshare completes it's job
- # -----------------------------------------------
- timeout = Timeout(30)
- while p.poll() is None and not timeout.is_expired():
- for fname in tuple(nslist):
- ours = os.readlink("/proc/self/ns/{}".format(fname))
- theirs = os.readlink("/proc/{}/ns/{}".format(self.pid, fname))
- # See if their namespace is different
- if ours != theirs:
- nslist.remove(fname)
- if not nslist:
- break
- elapsed = int(timeout.elapsed())
- if elapsed <= 3:
- time_mod.sleep(0.1)
- elif elapsed > 10:
- self.logger.warning("%s: unshare taking more than %ss", self, elapsed)
- time_mod.sleep(3)
- else:
- self.logger.info("%s: unshare taking more than %ss", self, elapsed)
- time_mod.sleep(1)
- assert p.poll() is None, "unshare unexpectedly exited!"
- assert not nslist, "unshare never unshared!"
-
- # Set pre-command based on our namespace proc
- self.base_pre_cmd = ["/usr/bin/nsenter", *self.a_flags, "-t", str(self.pid)]
- if not pid:
- self.base_pre_cmd.append("-F")
- self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + self.cwd])
-
- # Remount sysfs and cgroup to pickup any changes
- self.cmd_raises("mount -t sysfs sysfs /sys")
- self.cmd_raises(
- "mount -o rw,nosuid,nodev,noexec,relatime -t cgroup2 cgroup /sys/fs/cgroup"
- )
-
- # Set the hostname to the namespace name
- if uts and set_hostname:
- # Debugging get the root hostname
- self.cmd_raises("hostname " + self.name)
- nroot = subprocess.check_output("hostname")
- if root_hostname != nroot:
- result = self.p.poll()
- assert root_hostname == nroot, "STATE of namespace process {}".format(
- result
- )
-
- if private_mounts:
- if is_string(private_mounts):
- private_mounts = [private_mounts]
- for m in private_mounts:
- s = m.split(":", 1)
- if len(s) == 1:
- self.tmpfs_mount(s[0])
- else:
- self.bind_mount(s[0], s[1])
-
- o = self.cmd_legacy("ls -l /proc/{}/ns".format(self.pid))
- self.logger.debug("namespaces:\n %s", o)
-
- # Doing this here messes up all_protocols ipv6 check
- self.cmd_raises("ip link set lo up")
-
- def __str__(self):
- return "LinuxNamespace({})".format(self.name)
-
- def tmpfs_mount(self, inner):
- self.cmd_raises("mkdir -p " + inner)
- self.cmd_raises("mount -n -t tmpfs tmpfs " + inner)
-
- def bind_mount(self, outer, inner):
- self.cmd_raises("mkdir -p " + inner)
- self.cmd_raises("mount --rbind {} {} ".format(outer, inner))
-
- def add_vlan(self, vlanname, linkiface, vlanid):
- self.logger.debug("Adding VLAN interface: %s (%s)", vlanname, vlanid)
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- self.cmd_raises(
- [
- ip_path,
- "link",
- "add",
- "link",
- linkiface,
- "name",
- vlanname,
- "type",
- "vlan",
- "id",
- vlanid,
- ]
- )
- self.cmd_raises([ip_path, "link", "set", "dev", vlanname, "up"])
-
- def add_loop(self, loopname):
- self.logger.debug("Adding Linux iface: %s", loopname)
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- self.cmd_raises([ip_path, "link", "add", loopname, "type", "dummy"])
- self.cmd_raises([ip_path, "link", "set", "dev", loopname, "up"])
-
- def add_l3vrf(self, vrfname, tableid):
- self.logger.debug("Adding Linux VRF: %s", vrfname)
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- self.cmd_raises(
- [ip_path, "link", "add", vrfname, "type", "vrf", "table", tableid]
- )
- self.cmd_raises([ip_path, "link", "set", "dev", vrfname, "up"])
-
- def del_iface(self, iface):
- self.logger.debug("Removing Linux Iface: %s", iface)
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- self.cmd_raises([ip_path, "link", "del", iface])
-
- def attach_iface_to_l3vrf(self, ifacename, vrfname):
- self.logger.debug("Attaching Iface %s to Linux VRF %s", ifacename, vrfname)
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- if vrfname:
- self.cmd_raises(
- [ip_path, "link", "set", "dev", ifacename, "master", vrfname]
- )
- else:
- self.cmd_raises([ip_path, "link", "set", "dev", ifacename, "nomaster"])
-
- def add_netns(self, ns):
- self.logger.debug("Adding network namespace %s", ns)
-
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- if os.path.exists("/run/netns/{}".format(ns)):
- self.logger.warning("%s: Removing existing nsspace %s", self, ns)
- try:
- self.delete_netns(ns)
- except Exception as ex:
- self.logger.warning(
- "%s: Couldn't remove existing nsspace %s: %s",
- self,
- ns,
- str(ex),
- exc_info=True,
- )
- self.cmd_raises([ip_path, "netns", "add", ns])
-
- def delete_netns(self, ns):
- self.logger.debug("Deleting network namespace %s", ns)
-
- ip_path = self.get_exec_path("ip")
- assert ip_path, "XXX missing ip command!"
- self.cmd_raises([ip_path, "netns", "delete", ns])
-
- def set_intf_netns(self, intf, ns, up=False):
- # In case a user hard-codes 1 thinking it "resets"
- ns = str(ns)
- if ns == "1":
- ns = str(self.pid)
-
- self.logger.debug("Moving interface %s to namespace %s", intf, ns)
-
- cmd = "ip link set {} netns " + ns
- if up:
- cmd += " up"
- self.intf_ip_cmd(intf, cmd)
- if ns == str(self.pid):
- # If we are returning then remove from dict
- if intf in self.ifnetns:
- del self.ifnetns[intf]
- else:
- self.ifnetns[intf] = ns
-
- def reset_intf_netns(self, intf):
- self.logger.debug("Moving interface %s to default namespace", intf)
- self.set_intf_netns(intf, str(self.pid))
-
- def intf_ip_cmd(self, intf, cmd):
- """Run an ip command for considering an interfaces possible namespace.
-
- `cmd` - format is run using the interface name on the command
- """
- if intf in self.ifnetns:
- assert cmd.startswith("ip ")
- cmd = "ip -n " + self.ifnetns[intf] + cmd[2:]
- self.cmd_raises(cmd.format(intf))
-
- def set_cwd(self, cwd):
- # Set pre-command based on our namespace proc
- self.logger.debug("%s: new CWD %s", self, cwd)
- self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + cwd])
-
- def register_interface(self, ifname):
- if ifname not in self.intfs:
- self.intfs.append(ifname)
-
- def delete(self):
- if self.p and self.p.poll() is None:
- if sys.version_info[0] >= 3:
- try:
- self.p.terminate()
- self.p.communicate(timeout=10)
- except subprocess.TimeoutExpired:
- self.p.kill()
- self.p.communicate(timeout=2)
- else:
- self.p.kill()
- self.p.communicate()
- self.set_pre_cmd(["/bin/false"])
-
-
-class SharedNamespace(Commander):
- """
- Share another namespace.
-
- An object that executes commands in an existing pid's linux namespace
- """
-
- def __init__(self, name, pid, aflags=("-a",), logger=None):
- """
- Share a linux namespace.
-
- Args:
- name: Internal name for the namespace.
- pid: PID of the process to share with.
- """
- super(SharedNamespace, self).__init__(name, logger)
-
- self.logger.debug("%s: Creating", self)
-
- self.pid = pid
- self.intfs = []
- self.a_flags = aflags
-
- # Set pre-command based on our namespace proc
- self.set_pre_cmd(
- ["/usr/bin/nsenter", *self.a_flags, "-t", str(self.pid), "--wd=" + self.cwd]
- )
-
- def __str__(self):
- return "SharedNamespace({})".format(self.name)
-
- def set_cwd(self, cwd):
- # Set pre-command based on our namespace proc
- self.logger.debug("%s: new CWD %s", self, cwd)
- self.set_pre_cmd(
- ["/usr/bin/nsenter", *self.a_flags, "-t", str(self.pid), "--wd=" + cwd]
- )
-
- def register_interface(self, ifname):
- if ifname not in self.intfs:
- self.intfs.append(ifname)
-
-
-class Bridge(SharedNamespace):
- """
- A linux bridge.
- """
-
- next_brid_ord = 0
-
- @classmethod
- def _get_next_brid(cls):
- brid_ord = cls.next_brid_ord
- cls.next_brid_ord += 1
- return brid_ord
-
- def __init__(self, name=None, unet=None, logger=None):
- """Create a linux Bridge."""
-
- self.unet = unet
- self.brid_ord = self._get_next_brid()
- if name:
- self.brid = name
- else:
- self.brid = "br{}".format(self.brid_ord)
- name = self.brid
-
- super(Bridge, self).__init__(name, unet.pid, aflags=unet.a_flags, logger=logger)
-
- self.logger.debug("Bridge: Creating")
-
- assert len(self.brid) <= 16 # Make sure fits in IFNAMSIZE
- self.cmd_raises("ip link delete {} || true".format(self.brid))
- self.cmd_raises("ip link add {} type bridge".format(self.brid))
- self.cmd_raises("ip link set {} up".format(self.brid))
-
- self.logger.debug("%s: Created, Running", self)
-
- def __str__(self):
- return "Bridge({})".format(self.brid)
-
- def delete(self):
- """Stop the bridge (i.e., delete the linux resources)."""
-
- rc, o, e = self.cmd_status("ip link show {}".format(self.brid), warn=False)
- if not rc:
- rc, o, e = self.cmd_status(
- "ip link delete {}".format(self.brid), warn=False
- )
- if rc:
- self.logger.error(
- "%s: error deleting bridge %s: %s",
- self,
- self.brid,
- cmd_error(rc, o, e),
- )
- else:
- self.logger.debug("%s: Deleted.", self)
-
-
-class Micronet(LinuxNamespace): # pylint: disable=R0205
- """
- Micronet.
- """
-
- def __init__(self):
- """Create a Micronet."""
-
- self.hosts = {}
- self.switches = {}
- self.links = {}
- self.macs = {}
- self.rmacs = {}
-
- super(Micronet, self).__init__("micronet", mount=True, net=True, uts=True)
-
- self.logger.debug("%s: Creating", self)
-
- def __str__(self):
- return "Micronet()"
-
- def __getitem__(self, key):
- if key in self.switches:
- return self.switches[key]
- return self.hosts[key]
-
- def add_host(self, name, cls=LinuxNamespace, **kwargs):
- """Add a host to micronet."""
-
- self.logger.debug("%s: add_host %s", self, name)
-
- self.hosts[name] = cls(name, **kwargs)
- # Create a new mounted FS for tracking nested network namespaces creatd by the
- # user with `ip netns add`
- self.hosts[name].tmpfs_mount("/run/netns")
-
- def add_link(self, name1, name2, if1, if2):
- """Add a link between switch and host to micronet."""
- isp2p = False
- if name1 in self.switches:
- assert name2 in self.hosts
- elif name2 in self.switches:
- assert name1 in self.hosts
- name1, name2 = name2, name1
- if1, if2 = if2, if1
- else:
- # p2p link
- assert name1 in self.hosts
- assert name2 in self.hosts
- isp2p = True
-
- lname = "{}:{}-{}:{}".format(name1, if1, name2, if2)
- self.logger.debug("%s: add_link %s%s", self, lname, " p2p" if isp2p else "")
- self.links[lname] = (name1, if1, name2, if2)
-
- # And create the veth now.
- if isp2p:
- lhost, rhost = self.hosts[name1], self.hosts[name2]
- lifname = "i1{:x}".format(lhost.pid)
- rifname = "i2{:x}".format(rhost.pid)
- self.cmd_raises(
- "ip link add {} type veth peer name {}".format(lifname, rifname)
- )
-
- self.cmd_raises("ip link set {} netns {}".format(lifname, lhost.pid))
- lhost.cmd_raises("ip link set {} name {}".format(lifname, if1))
- lhost.cmd_raises("ip link set {} up".format(if1))
- lhost.register_interface(if1)
-
- self.cmd_raises("ip link set {} netns {}".format(rifname, rhost.pid))
- rhost.cmd_raises("ip link set {} name {}".format(rifname, if2))
- rhost.cmd_raises("ip link set {} up".format(if2))
- rhost.register_interface(if2)
- else:
- switch = self.switches[name1]
- host = self.hosts[name2]
-
- assert len(if1) <= 16 and len(if2) <= 16 # Make sure fits in IFNAMSIZE
-
- self.logger.debug("%s: Creating veth pair for link %s", self, lname)
- self.cmd_raises(
- "ip link add {} type veth peer name {} netns {}".format(
- if1, if2, host.pid
- )
- )
- self.cmd_raises("ip link set {} netns {}".format(if1, switch.pid))
- switch.register_interface(if1)
- host.register_interface(if2)
- self.cmd_raises("ip link set {} master {}".format(if1, switch.brid))
- self.cmd_raises("ip link set {} up".format(if1))
- host.cmd_raises("ip link set {} up".format(if2))
-
- # Cache the MAC values, and reverse mapping
- self.get_mac(name1, if1)
- self.get_mac(name2, if2)
-
- def add_switch(self, name):
- """Add a switch to micronet."""
-
- self.logger.debug("%s: add_switch %s", self, name)
- self.switches[name] = Bridge(name, self)
-
- def get_mac(self, name, ifname):
- if name in self.hosts:
- dev = self.hosts[name]
- else:
- dev = self.switches[name]
-
- if (name, ifname) not in self.macs:
- _, output, _ = dev.cmd_status("ip -o link show " + ifname)
- m = re.match(".*link/(loopback|ether) ([0-9a-fA-F:]+) .*", output)
- mac = m.group(2)
- self.macs[(name, ifname)] = mac
- self.rmacs[mac] = (name, ifname)
-
- return self.macs[(name, ifname)]
-
- def delete(self):
- """Delete the micronet topology."""
-
- self.logger.debug("%s: Deleting.", self)
-
- for lname, (_, _, rname, rif) in self.links.items():
- host = self.hosts[rname]
-
- self.logger.debug("%s: Deleting veth pair for link %s", self, lname)
-
- rc, o, e = host.cmd_status("ip link delete {}".format(rif), warn=False)
- if rc:
- self.logger.error(
- "Error deleting veth pair %s: %s", lname, cmd_error(rc, o, e)
- )
-
- self.links = {}
-
- for host in self.hosts.values():
- try:
- host.delete()
- except Exception as error:
- self.logger.error(
- "%s: error while deleting host %s: %s", self, host, error
- )
-
- self.hosts = {}
-
- for switch in self.switches.values():
- try:
- switch.delete()
- except Exception as error:
- self.logger.error(
- "%s: error while deleting switch %s: %s", self, switch, error
- )
- self.switches = {}
-
- self.logger.debug("%s: Deleted.", self)
-
- super(Micronet, self).delete()
-
-
-# ---------------------------
-# Root level utility function
-# ---------------------------
-
-
-def get_exec_path(binary):
- base = Commander("base")
- return base.get_exec_path(binary)
-
-
-commander = Commander("micronet")
+# flake8: noqa
+
+from munet.base import BaseMunet as Micronet
+from munet.base import (
+ Bridge,
+ Commander,
+ LinuxNamespace,
+ SharedNamespace,
+ Timeout,
+ cmd_error,
+ comm_error,
+ commander,
+ get_exec_path,
+ proc_error,
+ root_hostname,
+ shell_quote,
+)
diff --git a/tests/topotests/lib/micronet_cli.py b/tests/topotests/lib/micronet_cli.py
deleted file mode 100644
index e54b75f71..000000000
--- a/tests/topotests/lib/micronet_cli.py
+++ /dev/null
@@ -1,306 +0,0 @@
-# -*- coding: utf-8 eval: (blacken-mode 1) -*-
-# SPDX-License-Identifier: GPL-2.0-or-later
-#
-# July 24 2021, Christian Hopps <chopps@labn.net>
-#
-# Copyright (c) 2021, LabN Consulting, L.L.C.
-#
-import argparse
-import logging
-import os
-import pty
-import re
-import readline
-import select
-import socket
-import subprocess
-import sys
-import tempfile
-import termios
-import tty
-
-
-ENDMARKER = b"\x00END\x00"
-
-
-def lineiter(sock):
- s = ""
- while True:
- sb = sock.recv(256)
- if not sb:
- return
-
- s += sb.decode("utf-8")
- i = s.find("\n")
- if i != -1:
- yield s[:i]
- s = s[i + 1 :]
-
-
-def spawn(unet, host, cmd):
- if sys.stdin.isatty():
- old_tty = termios.tcgetattr(sys.stdin)
- tty.setraw(sys.stdin.fileno())
- try:
- master_fd, slave_fd = pty.openpty()
-
- # use os.setsid() make it run in a new process group, or bash job
- # control will not be enabled
- p = unet.hosts[host].popen(
- cmd,
- preexec_fn=os.setsid,
- stdin=slave_fd,
- stdout=slave_fd,
- stderr=slave_fd,
- universal_newlines=True,
- )
-
- while p.poll() is None:
- r, w, e = select.select([sys.stdin, master_fd], [], [], 0.25)
- if sys.stdin in r:
- d = os.read(sys.stdin.fileno(), 10240)
- os.write(master_fd, d)
- elif master_fd in r:
- o = os.read(master_fd, 10240)
- if o:
- os.write(sys.stdout.fileno(), o)
- finally:
- # restore tty settings back
- if sys.stdin.isatty():
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
-
-
-def doline(unet, line, writef):
- def host_cmd_split(unet, cmd):
- csplit = cmd.split()
- for i, e in enumerate(csplit):
- if e not in unet.hosts:
- break
- hosts = csplit[:i]
- if not hosts:
- hosts = sorted(unet.hosts.keys())
- cmd = " ".join(csplit[i:])
- return hosts, cmd
-
- line = line.strip()
- m = re.match(r"^(\S+)(?:\s+(.*))?$", line)
- if not m:
- return True
-
- cmd = m.group(1)
- oargs = m.group(2) if m.group(2) else ""
- if cmd == "q" or cmd == "quit":
- return False
- if cmd == "hosts":
- writef("%% hosts: %s\n" % " ".join(sorted(unet.hosts.keys())))
- elif cmd in ["term", "vtysh", "xterm"]:
- args = oargs.split()
- if not args or (len(args) == 1 and args[0] == "*"):
- args = sorted(unet.hosts.keys())
- hosts = [unet.hosts[x] for x in args if x in unet.hosts]
- for host in hosts:
- if cmd == "t" or cmd == "term":
- host.run_in_window("bash", title="sh-%s" % host)
- elif cmd == "v" or cmd == "vtysh":
- host.run_in_window("vtysh", title="vt-%s" % host)
- elif cmd == "x" or cmd == "xterm":
- host.run_in_window("bash", title="sh-%s" % host, forcex=True)
- elif cmd == "sh":
- hosts, cmd = host_cmd_split(unet, oargs)
- for host in hosts:
- if sys.stdin.isatty():
- spawn(unet, host, cmd)
- else:
- if len(hosts) > 1:
- writef("------ Host: %s ------\n" % host)
- output = unet.hosts[host].cmd_legacy(cmd)
- writef(output)
- if len(hosts) > 1:
- writef("------- End: %s ------\n" % host)
- writef("\n")
- elif cmd == "h" or cmd == "help":
- writef(
- """
-Commands:
- help :: this help
- sh [hosts] <shell-command> :: execute <shell-command> on <host>
- term [hosts] :: open shell terminals for hosts
- vtysh [hosts] :: open vtysh terminals for hosts
- [hosts] <vtysh-command> :: execute vtysh-command on hosts\n\n"""
- )
- else:
- hosts, cmd = host_cmd_split(unet, line)
- for host in hosts:
- if len(hosts) > 1:
- writef("------ Host: %s ------\n" % host)
- output = unet.hosts[host].cmd_legacy('vtysh -c "{}"'.format(cmd))
- writef(output)
- if len(hosts) > 1:
- writef("------- End: %s ------\n" % host)
- writef("\n")
- return True
-
-
-def cli_server_setup(unet):
- sockdir = tempfile.mkdtemp("-sockdir", "pyt")
- sockpath = os.path.join(sockdir, "cli-server.sock")
- try:
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.settimeout(10)
- sock.bind(sockpath)
- sock.listen(1)
- return sock, sockdir, sockpath
- except Exception:
- unet.cmd_status("rm -rf " + sockdir)
- raise
-
-
-def cli_server(unet, server_sock):
- sock, addr = server_sock.accept()
-
- # Go into full non-blocking mode now
- sock.settimeout(None)
-
- for line in lineiter(sock):
- line = line.strip()
-
- def writef(x):
- xb = x.encode("utf-8")
- sock.send(xb)
-
- if not doline(unet, line, writef):
- return
- sock.send(ENDMARKER)
-
-
-def cli_client(sockpath, prompt="unet> "):
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.settimeout(10)
- sock.connect(sockpath)
-
- # Go into full non-blocking mode now
- sock.settimeout(None)
-
- print("\n--- Micronet CLI Starting ---\n\n")
- while True:
- if sys.version_info[0] == 2:
- line = raw_input(prompt) # pylint: disable=E0602
- else:
- line = input(prompt)
- if line is None:
- return
-
- # Need to put \n back
- line += "\n"
-
- # Send the CLI command
- sock.send(line.encode("utf-8"))
-
- def bendswith(b, sentinel):
- slen = len(sentinel)
- return len(b) >= slen and b[-slen:] == sentinel
-
- # Collect the output
- rb = b""
- while not bendswith(rb, ENDMARKER):
- lb = sock.recv(4096)
- if not lb:
- return
- rb += lb
-
- # Remove the marker
- rb = rb[: -len(ENDMARKER)]
-
- # Write the output
- sys.stdout.write(rb.decode("utf-8"))
-
-
-def local_cli(unet, outf, prompt="unet> "):
- print("\n--- Micronet CLI Starting ---\n\n")
- while True:
- if sys.version_info[0] == 2:
- line = raw_input(prompt) # pylint: disable=E0602
- else:
- line = input(prompt)
- if line is None:
- return
- if not doline(unet, line, outf.write):
- return
-
-
-def cli(
- unet,
- histfile=None,
- sockpath=None,
- force_window=False,
- title=None,
- prompt=None,
- background=True,
-):
- logger = logging.getLogger("cli-client")
-
- if prompt is None:
- prompt = "unet> "
-
- if force_window or not sys.stdin.isatty():
- # Run CLI in another window b/c we have no tty.
- sock, sockdir, sockpath = cli_server_setup(unet)
-
- python_path = unet.get_exec_path(["python3", "python"])
- us = os.path.realpath(__file__)
- cmd = "{} {}".format(python_path, us)
- if histfile:
- cmd += " --histfile=" + histfile
- if title:
- cmd += " --prompt={}".format(title)
- cmd += " " + sockpath
-
- try:
- unet.run_in_window(cmd, new_window=True, title=title, background=background)
- return cli_server(unet, sock)
- finally:
- unet.cmd_status("rm -rf " + sockdir)
-
- if not unet:
- logger.debug("client-cli using sockpath %s", sockpath)
-
- try:
- if histfile is None:
- histfile = os.path.expanduser("~/.micronet-history.txt")
- if not os.path.exists(histfile):
- if unet:
- unet.cmd("touch " + histfile)
- else:
- subprocess.run("touch " + histfile)
- if histfile:
- readline.read_history_file(histfile)
- except Exception:
- pass
-
- try:
- if sockpath:
- cli_client(sockpath, prompt=prompt)
- else:
- local_cli(unet, sys.stdout, prompt=prompt)
- except EOFError:
- pass
- except Exception as ex:
- logger.critical("cli: got exception: %s", ex, exc_info=True)
- raise
- finally:
- readline.write_history_file(histfile)
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.DEBUG, filename="/tmp/topotests/cli-client.log")
- logger = logging.getLogger("cli-client")
- logger.info("Start logging cli-client")
-
- parser = argparse.ArgumentParser()
- parser.add_argument("--histfile", help="file to user for history")
- parser.add_argument("--prompt-text", help="prompt string to use")
- parser.add_argument("socket", help="path to pair of sockets to communicate over")
- args = parser.parse_args()
-
- prompt = "{}> ".format(args.prompt_text) if args.prompt_text else "unet> "
- cli(None, args.histfile, args.socket, prompt=prompt)
diff --git a/tests/topotests/lib/micronet_compat.py b/tests/topotests/lib/micronet_compat.py
index edbd36008..211d61fb6 100644
--- a/tests/topotests/lib/micronet_compat.py
+++ b/tests/topotests/lib/micronet_compat.py
@@ -3,140 +3,43 @@
#
# July 11 2021, Christian Hopps <chopps@labn.net>
#
-# Copyright (c) 2021, LabN Consulting, L.L.C
+# Copyright (c) 2021-2023, LabN Consulting, L.L.C
#
-
-import glob
-import logging
+import ipaddress
import os
-import signal
-import time
-
-from lib.micronet import LinuxNamespace, Micronet
-from lib.micronet_cli import cli
-
-
-def get_pids_with_env(has_var, has_val=None):
- result = {}
- for pidenv in glob.iglob("/proc/*/environ"):
- pid = pidenv.split("/")[2]
- try:
- with open(pidenv, "rb") as rfb:
- envlist = [
- x.decode("utf-8").split("=", 1) for x in rfb.read().split(b"\0")
- ]
- envlist = [[x[0], ""] if len(x) == 1 else x for x in envlist]
- envdict = dict(envlist)
- if has_var not in envdict:
- continue
- if has_val is None:
- result[pid] = envdict
- elif envdict[has_var] == str(has_val):
- result[pid] = envdict
- except Exception:
- # E.g., process exited and files are gone
- pass
- return result
-
-
-def _kill_piddict(pids_by_upid, sig):
- for upid, pids in pids_by_upid:
- logging.info(
- "Sending %s to (%s) of micronet pid %s", sig, ", ".join(pids), upid
- )
- for pid in pids:
- try:
- os.kill(int(pid), sig)
- except Exception:
- pass
-
-
-def _get_our_pids():
- ourpid = str(os.getpid())
- piddict = get_pids_with_env("MICRONET_PID", ourpid)
- pids = [x for x in piddict if x != ourpid]
- if pids:
- return {ourpid: pids}
- return {}
-
-def _get_other_pids():
- piddict = get_pids_with_env("MICRONET_PID")
- unet_pids = {d["MICRONET_PID"] for d in piddict.values()}
- pids_by_upid = {p: set() for p in unet_pids}
- for pid, envdict in piddict.items():
- pids_by_upid[envdict["MICRONET_PID"]].add(pid)
- # Filter out any child pid sets whos micronet pid is still running
- return {x: y for x, y in pids_by_upid.items() if x not in y}
-
-
-def _get_pids_by_upid(ours):
- if ours:
- return _get_our_pids()
- return _get_other_pids()
-
-
-def _cleanup_pids(ours):
- pids_by_upid = _get_pids_by_upid(ours).items()
- if not pids_by_upid:
- return
-
- _kill_piddict(pids_by_upid, signal.SIGTERM)
-
- # Give them 5 second to exit cleanly
- logging.info("Waiting up to 5s to allow for clean exit of abandon'd pids")
- for _ in range(0, 5):
- pids_by_upid = _get_pids_by_upid(ours).items()
- if not pids_by_upid:
- return
- time.sleep(1)
-
- pids_by_upid = _get_pids_by_upid(ours).items()
- _kill_piddict(pids_by_upid, signal.SIGKILL)
-
-
-def cleanup_current():
- """Attempt to cleanup preview runs.
-
- Currently this only scans for old processes.
- """
- logging.info("reaping current micronet processes")
- _cleanup_pids(True)
-
-
-def cleanup_previous():
- """Attempt to cleanup preview runs.
-
- Currently this only scans for old processes.
- """
- logging.info("reaping past micronet processes")
- _cleanup_pids(False)
+from munet import cli
+from munet.base import BaseMunet, LinuxNamespace
class Node(LinuxNamespace):
"""Node (mininet compat)."""
- def __init__(self, name, **kwargs):
- """
- Create a Node.
- """
- self.params = kwargs
+ def __init__(self, name, rundir=None, **kwargs):
+
+ nkwargs = {}
+ if "unet" in kwargs:
+ nkwargs["unet"] = kwargs["unet"]
if "private_mounts" in kwargs:
- private_mounts = kwargs["private_mounts"]
- else:
- private_mounts = kwargs.get("privateDirs", [])
+ nkwargs["private_mounts"] = kwargs["private_mounts"]
+
+ # This is expected by newer munet CLI code
+ self.config_dirname = ""
+ self.config = {"kind": "frr"}
+ self.mgmt_ip = None
+ self.mgmt_ip6 = None
- logger = kwargs.get("logger")
+ super().__init__(name, **nkwargs)
- super(Node, self).__init__(name, logger=logger, private_mounts=private_mounts)
+ self.rundir = self.unet.rundir.joinpath(self.name)
def cmd(self, cmd, **kwargs):
"""Execute a command, joins stdout, stderr, ignores exit status."""
return super(Node, self).cmd_legacy(cmd, **kwargs)
- def config(self, lo="up", **params):
+ def config_host(self, lo="up", **params):
"""Called by Micronet when topology is built (but not started)."""
# mininet brings up loopback here.
del params
@@ -148,20 +51,76 @@ class Node(LinuxNamespace):
def terminate(self):
return
+ def add_vlan(self, vlanname, linkiface, vlanid):
+ self.logger.debug("Adding VLAN interface: %s (%s)", vlanname, vlanid)
+ ip_path = self.get_exec_path("ip")
+ assert ip_path, "XXX missing ip command!"
+ self.cmd_raises(
+ [
+ ip_path,
+ "link",
+ "add",
+ "link",
+ linkiface,
+ "name",
+ vlanname,
+ "type",
+ "vlan",
+ "id",
+ vlanid,
+ ]
+ )
+ self.cmd_raises([ip_path, "link", "set", "dev", vlanname, "up"])
+
+ def add_loop(self, loopname):
+ self.logger.debug("Adding Linux iface: %s", loopname)
+ ip_path = self.get_exec_path("ip")
+ assert ip_path, "XXX missing ip command!"
+ self.cmd_raises([ip_path, "link", "add", loopname, "type", "dummy"])
+ self.cmd_raises([ip_path, "link", "set", "dev", loopname, "up"])
+
+ def add_l3vrf(self, vrfname, tableid):
+ self.logger.debug("Adding Linux VRF: %s", vrfname)
+ ip_path = self.get_exec_path("ip")
+ assert ip_path, "XXX missing ip command!"
+ self.cmd_raises(
+ [ip_path, "link", "add", vrfname, "type", "vrf", "table", tableid]
+ )
+ self.cmd_raises([ip_path, "link", "set", "dev", vrfname, "up"])
+
+ def del_iface(self, iface):
+ self.logger.debug("Removing Linux Iface: %s", iface)
+ ip_path = self.get_exec_path("ip")
+ assert ip_path, "XXX missing ip command!"
+ self.cmd_raises([ip_path, "link", "del", iface])
+
+ def attach_iface_to_l3vrf(self, ifacename, vrfname):
+ self.logger.debug("Attaching Iface %s to Linux VRF %s", ifacename, vrfname)
+ ip_path = self.get_exec_path("ip")
+ assert ip_path, "XXX missing ip command!"
+ if vrfname:
+ self.cmd_raises(
+ [ip_path, "link", "set", "dev", ifacename, "master", vrfname]
+ )
+ else:
+ self.cmd_raises([ip_path, "link", "set", "dev", ifacename, "nomaster"])
+
+ set_cwd = LinuxNamespace.set_ns_cwd
+
class Topo(object): # pylint: disable=R0205
def __init__(self, *args, **kwargs):
raise Exception("Remove Me")
-class Mininet(Micronet):
+class Mininet(BaseMunet):
"""
Mininet using Micronet.
"""
g_mnet_inst = None
- def __init__(self):
+ def __init__(self, rundir=None):
"""
Create a Micronet.
"""
@@ -179,7 +138,146 @@ class Mininet(Micronet):
# to set permissions to root:frr 770 to make this unneeded in that case
# os.umask(0)
- super(Mininet, self).__init__()
+ super(Mininet, self).__init__(pid=False, rundir=rundir)
+
+ # From munet/munet/native.py
+ with open(os.path.join(self.rundir, "nspid"), "w", encoding="ascii") as f:
+ f.write(f"{self.pid}\n")
+
+ with open(os.path.join(self.rundir, "nspids"), "w", encoding="ascii") as f:
+ f.write(f'{" ".join([str(x) for x in self.pids])}\n')
+
+ hosts_file = os.path.join(self.rundir, "hosts.txt")
+ with open(hosts_file, "w", encoding="ascii") as hf:
+ hf.write(
+ f"""127.0.0.1\tlocalhost {self.name}
+::1\tip6-localhost ip6-loopback
+fe00::0\tip6-localnet
+ff00::0\tip6-mcastprefix
+ff02::1\tip6-allnodes
+ff02::2\tip6-allrouters
+"""
+ )
+ self.bind_mount(hosts_file, "/etc/hosts")
+
+ # Common CLI commands for any topology
+ cdict = {
+ "commands": [
+ #
+ # Window commands.
+ #
+ {
+ "name": "pcap",
+ "format": "pcap NETWORK",
+ "help": (
+ "capture packets from NETWORK into file capture-NETWORK.pcap"
+ " the command is run within a new window which also shows"
+ " packet summaries. NETWORK can also be an interface specified"
+ " as HOST:INTF. To capture inside the host namespace."
+ ),
+ "exec": "tshark -s 9200 -i {0} -P -w capture-{0}.pcap",
+ "top-level": True,
+ "new-window": {"background": True},
+ },
+ {
+ "name": "term",
+ "format": "term HOST [HOST ...]",
+ "help": "open terminal[s] (TMUX or XTerm) on HOST[S], * for all",
+ "exec": "bash",
+ "new-window": True,
+ },
+ {
+ "name": "vtysh",
+ "exec": "/usr/bin/vtysh",
+ "format": "vtysh ROUTER [ROUTER ...]",
+ "new-window": True,
+ "kinds": ["frr"],
+ },
+ {
+ "name": "xterm",
+ "format": "xterm HOST [HOST ...]",
+ "help": "open XTerm[s] on HOST[S], * for all",
+ "exec": "bash",
+ "new-window": {
+ "forcex": True,
+ },
+ },
+ {
+ "name": "logd",
+ "exec": "tail -F %RUNDIR%/{}.log",
+ "format": "logd HOST [HOST ...] DAEMON",
+ "help": (
+ "tail -f on the logfile of the given "
+ "DAEMON for the given HOST[S]"
+ ),
+ "new-window": True,
+ },
+ {
+ "name": "stdlog",
+ "exec": (
+ "[ -e %RUNDIR%/frr.log ] && tail -F %RUNDIR%/frr.log "
+ "|| tail -F /var/log/frr.log"
+ ),
+ "format": "stdlog HOST [HOST ...]",
+ "help": "tail -f on the `frr.log` for the given HOST[S]",
+ "new-window": True,
+ },
+ {
+ "name": "stdout",
+ "exec": "tail -F %RUNDIR%/{0}.err",
+ "format": "stdout HOST [HOST ...] DAEMON",
+ "help": (
+ "tail -f on the stdout of the given DAEMON for the given HOST[S]"
+ ),
+ "new-window": True,
+ },
+ {
+ "name": "stderr",
+ "exec": "tail -F %RUNDIR%/{0}.out",
+ "format": "stderr HOST [HOST ...] DAEMON",
+ "help": (
+ "tail -f on the stderr of the given DAEMON for the given HOST[S]"
+ ),
+ "new-window": True,
+ },
+ #
+ # Non-window commands.
+ #
+ {
+ "name": "",
+ "exec": "vtysh -c '{}'",
+ "format": "[ROUTER ...] COMMAND",
+ "help": "execute vtysh COMMAND on the router[s]",
+ "kinds": ["frr"],
+ },
+ {
+ "name": "sh",
+ "format": "[HOST ...] sh <SHELL-COMMAND>",
+ "help": "execute <SHELL-COMMAND> on hosts",
+ "exec": "{}",
+ },
+ {
+ "name": "shi",
+ "format": "[HOST ...] shi <INTERACTIVE-COMMAND>",
+ "help": "execute <INTERACTIVE-COMMAND> on HOST[s]",
+ "exec": "{}",
+ "interactive": True,
+ },
+ ]
+ }
+
+ cli.add_cli_config(self, cdict)
+
+ # shellopt = (
+ # self.pytest_config.getoption("--shell") if self.pytest_config else None
+ # )
+ # shellopt = shellopt if shellopt is not None else ""
+ # if shellopt == "all" or "." in shellopt.split(","):
+ # self.run_in_window("bash")
+
+ # This is expected by newer munet CLI code
+ self.config_dirname = ""
+ self.config = {}
self.logger.debug("%s: Creating", self)
@@ -217,12 +315,15 @@ class Mininet(Micronet):
host.cmd_raises("ip addr add {}/{} dev {}".format(ip, plen, first_intf))
+ # can be used by munet cli
+ host.mgmt_ip = ipaddress.ip_address(ip)
+
if "defaultRoute" in params:
host.cmd_raises(
"ip route add default {}".format(params["defaultRoute"])
)
- host.config()
+ host.config_host()
self.configured_hosts.add(name)
@@ -248,4 +349,4 @@ class Mininet(Micronet):
Mininet.g_mnet_inst = None
def cli(self):
- cli(self)
+ cli.cli(self)
diff --git a/tests/topotests/lib/topogen.py b/tests/topotests/lib/topogen.py
index f5b3ad06d..474b7ec37 100644
--- a/tests/topotests/lib/topogen.py
+++ b/tests/topotests/lib/topogen.py
@@ -212,7 +212,7 @@ class Topogen(object):
# Mininet(Micronet) to build the actual topology.
assert not inspect.isclass(topodef)
- self.net = Mininet()
+ self.net = Mininet(rundir=self.logdir)
# Adjust the parent namespace
topotest.fix_netns_limits(self.net)
@@ -752,8 +752,8 @@ class TopoRouter(TopoGear):
"""
super(TopoRouter, self).__init__(tgen, name, **params)
self.routertype = params.get("routertype", "frr")
- if "privateDirs" not in params:
- params["privateDirs"] = self.PRIVATE_DIRS
+ if "private_mounts" not in params:
+ params["private_mounts"] = self.PRIVATE_DIRS
# Propagate the router log directory
logfile = self._setup_tmpdir()
@@ -1100,7 +1100,7 @@ class TopoHost(TopoGear):
* `ip`: the IP address (string) for the host interface
* `defaultRoute`: the default route that will be installed
(e.g. 'via 10.0.0.1')
- * `privateDirs`: directories that will be mounted on a different domain
+ * `private_mounts`: directories that will be mounted on a different domain
(e.g. '/etc/important_dir').
"""
super(TopoHost, self).__init__(tgen, name, **params)
@@ -1120,10 +1120,10 @@ class TopoHost(TopoGear):
def __str__(self):
gear = super(TopoHost, self).__str__()
- gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
+ gear += ' TopoHost<ip="{}",defaultRoute="{}",private_mounts="{}">'.format(
self.params["ip"],
self.params["defaultRoute"],
- str(self.params["privateDirs"]),
+ str(self.params["private_mounts"]),
)
return gear
@@ -1146,10 +1146,10 @@ class TopoExaBGP(TopoHost):
(e.g. 'via 10.0.0.1')
Note: the different between a host and a ExaBGP peer is that this class
- has a privateDirs already defined and contains functions to handle ExaBGP
- things.
+ has a private_mounts already defined and contains functions to handle
+ ExaBGP things.
"""
- params["privateDirs"] = self.PRIVATE_DIRS
+ params["private_mounts"] = self.PRIVATE_DIRS
super(TopoExaBGP, self).__init__(tgen, name, **params)
def __str__(self):
diff --git a/tests/topotests/lib/topotest.py b/tests/topotests/lib/topotest.py
index 86a7f2000..05fbecc54 100644
--- a/tests/topotests/lib/topotest.py
+++ b/tests/topotests/lib/topotest.py
@@ -1318,7 +1318,7 @@ def setup_node_tmpdir(logdir, name):
class Router(Node):
"A Node with IPv4/IPv6 forwarding enabled"
- def __init__(self, name, **params):
+ def __init__(self, name, *posargs, **params):
# Backward compatibility:
# Load configuration defaults like topogen.
@@ -1347,7 +1347,7 @@ class Router(Node):
l = topolog.get_logger(name, log_level="debug", target=logfile)
params["logger"] = l
- super(Router, self).__init__(name, **params)
+ super(Router, self).__init__(name, *posargs, **params)
self.daemondir = None
self.hasmpls = False
@@ -1407,8 +1407,8 @@ class Router(Node):
# pylint: disable=W0221
# Some params are only meaningful for the parent class.
- def config(self, **params):
- super(Router, self).config(**params)
+ def config_host(self, **params):
+ super(Router, self).config_host(**params)
# User did not specify the daemons directory, try to autodetect it.
self.daemondir = params.get("daemondir")
diff --git a/tests/topotests/pytest.ini b/tests/topotests/pytest.ini
index 6986e3051..ccbc9d2a1 100644
--- a/tests/topotests/pytest.ini
+++ b/tests/topotests/pytest.ini
@@ -24,7 +24,7 @@ log_file_date_format = %Y-%m-%d %H:%M:%S
junit_logging = all
junit_log_passing_tests = true
-norecursedirs = .git example_test example_topojson_test lib docker
+norecursedirs = .git example_test example_topojson_test lib munet docker
# Directory to store test results and run logs in, default shown
# rundir = /tmp/topotests