# -*- coding: utf-8 eval: (blacken-mode 1) -*- # # July 9 2021, Christian Hopps # # 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")