diff options
Diffstat (limited to 'tests/topotests/lib/micronet.py')
| -rw-r--r-- | tests/topotests/lib/micronet.py | 1018 |
1 files changed, 18 insertions, 1000 deletions
diff --git a/tests/topotests/lib/micronet.py b/tests/topotests/lib/micronet.py index 1381009168..f4aa8278f1 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, +) |
