diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:53:30 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:53:30 +0000 |
commit | 2c7cac91ed6e7db0f6937923d2b57f97dbdbc337 (patch) | |
tree | c05dc0f8e6aa3accc84e3e5cffc933ed94941383 /tests/topotests/lib/micronet.py | |
parent | Initial commit. (diff) | |
download | frr-2536d89cfd3a79f991a2c9774117e8ccbf7cb5df.tar.xz frr-2536d89cfd3a79f991a2c9774117e8ccbf7cb5df.zip |
Adding upstream version 8.4.4.upstream/8.4.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/topotests/lib/micronet.py')
-rw-r--r-- | tests/topotests/lib/micronet.py | 1005 |
1 files changed, 1005 insertions, 0 deletions
diff --git a/tests/topotests/lib/micronet.py b/tests/topotests/lib/micronet.py new file mode 100644 index 0000000..dfa10cc --- /dev/null +++ b/tests/topotests/lib/micronet.py @@ -0,0 +1,1005 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# July 9 2021, Christian Hopps <chopps@labn.net> +# +# Copyright (c) 2021, LabN Consulting, L.L.C. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +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.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 += "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") + cmd.append("--uts") + + cmd.append(flags) + 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"), + preexec_fn=os.setsid, # detach from pgid so signals don't propogate + 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", "-a", "-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, 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 = [] + + # Set pre-command based on our namespace proc + self.set_pre_cmd( + ["/usr/bin/nsenter", "-a", "-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", "-a", "-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, 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") |