mirror_frr/tests/topotests/lib/micronet.py
Donald Sharp 4c98b89e5c *: Fix doesnt spelling mistakes
Signed-off-by: Donald Sharp <sharpd@nvidia.com>
2022-05-06 20:38:40 -04:00

949 lines
31 KiB
Python

# -*- 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 /sys to pickup any changes
self.cmd_raises("mount -t sysfs sysfs /sys")
# 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_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")