mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-04-29 21:37:08 +00:00
949 lines
31 KiB
Python
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")
|