mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-06-15 12:55:21 +00:00
tests: NEW micronet replacement for mininet
Signed-off-by: Christian Hopps <chopps@labn.net>
This commit is contained in:
parent
bc51ce6810
commit
6a5433ef0b
949
tests/topotests/lib/micronet.py
Normal file
949
tests/topotests/lib/micronet.py
Normal file
@ -0,0 +1,949 @@
|
|||||||
|
# -*- 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 pdb
|
||||||
|
import re
|
||||||
|
import readline
|
||||||
|
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 doesnt' 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 not os.path.exists(
|
||||||
|
"/run/screen/S-{}/{}".format(os.environ["USER"], os.environ["STY"])
|
||||||
|
):
|
||||||
|
cmd = ["sudo", "-u", os.environ["SUDO_USER"]] + cmd
|
||||||
|
cmd.append(nscmd)
|
||||||
|
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.append(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,
|
||||||
|
# stdout=open("/dev/null", "w"),
|
||||||
|
# stderr=open("/dev/null", "w"),
|
||||||
|
)
|
||||||
|
time_mod.sleep(2)
|
||||||
|
if p.poll() is not None:
|
||||||
|
self.logger.error("%s: Failed to launch xterm: %s", self, comm_error(p))
|
||||||
|
return ""
|
||||||
|
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")
|
312
tests/topotests/lib/micronet_cli.py
Normal file
312
tests/topotests/lib/micronet_cli.py
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
|
||||||
|
#
|
||||||
|
# July 24 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 argparse
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pdb
|
||||||
|
import pty
|
||||||
|
import re
|
||||||
|
import readline
|
||||||
|
import select
|
||||||
|
import shlex
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import termios
|
||||||
|
import time as time_mod
|
||||||
|
import traceback
|
||||||
|
import tty
|
||||||
|
|
||||||
|
|
||||||
|
ENDMARKER = b"\x00END\x00"
|
||||||
|
|
||||||
|
|
||||||
|
def lineiter(sock):
|
||||||
|
s = ""
|
||||||
|
while True:
|
||||||
|
sb = sock.recv(256)
|
||||||
|
if not sb:
|
||||||
|
return
|
||||||
|
|
||||||
|
s += sb.decode("utf-8")
|
||||||
|
i = s.find("\n")
|
||||||
|
if i != -1:
|
||||||
|
yield s[:i]
|
||||||
|
s = s[i + 1 :]
|
||||||
|
|
||||||
|
|
||||||
|
def spawn(unet, host, cmd):
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
old_tty = termios.tcgetattr(sys.stdin)
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
try:
|
||||||
|
master_fd, slave_fd = pty.openpty()
|
||||||
|
|
||||||
|
# use os.setsid() make it run in a new process group, or bash job
|
||||||
|
# control will not be enabled
|
||||||
|
p = unet.hosts[host].popen(
|
||||||
|
cmd,
|
||||||
|
preexec_fn=os.setsid,
|
||||||
|
stdin=slave_fd,
|
||||||
|
stdout=slave_fd,
|
||||||
|
stderr=slave_fd,
|
||||||
|
universal_newlines=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
while p.poll() is None:
|
||||||
|
r, w, e = select.select([sys.stdin, master_fd], [], [], 0.25)
|
||||||
|
if sys.stdin in r:
|
||||||
|
d = os.read(sys.stdin.fileno(), 10240)
|
||||||
|
os.write(master_fd, d)
|
||||||
|
elif master_fd in r:
|
||||||
|
o = os.read(master_fd, 10240)
|
||||||
|
if o:
|
||||||
|
os.write(sys.stdout.fileno(), o)
|
||||||
|
finally:
|
||||||
|
# restore tty settings back
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
|
||||||
|
|
||||||
|
def doline(unet, line, writef):
|
||||||
|
def host_cmd_split(unet, cmd):
|
||||||
|
csplit = cmd.split()
|
||||||
|
for i, e in enumerate(csplit):
|
||||||
|
if e not in unet.hosts:
|
||||||
|
break
|
||||||
|
hosts = csplit[:i]
|
||||||
|
if not hosts:
|
||||||
|
hosts = sorted(unet.hosts.keys())
|
||||||
|
cmd = " ".join(csplit[i:])
|
||||||
|
return hosts, cmd
|
||||||
|
|
||||||
|
line = line.strip()
|
||||||
|
m = re.match(r"^(\S+)(?:\s+(.*))?$", line)
|
||||||
|
if not m:
|
||||||
|
return True
|
||||||
|
|
||||||
|
cmd = m.group(1)
|
||||||
|
oargs = m.group(2) if m.group(2) else ""
|
||||||
|
if cmd == "q" or cmd == "quit":
|
||||||
|
return False
|
||||||
|
if cmd == "hosts":
|
||||||
|
writef("%% hosts: %s\n" % " ".join(sorted(unet.hosts.keys())))
|
||||||
|
elif cmd in ["term", "vtysh", "xterm"]:
|
||||||
|
args = oargs.split()
|
||||||
|
if not args or (len(args) == 1 and args[0] == "*"):
|
||||||
|
args = sorted(unet.hosts.keys())
|
||||||
|
hosts = [unet.hosts[x] for x in args]
|
||||||
|
for host in hosts:
|
||||||
|
if cmd == "t" or cmd == "term":
|
||||||
|
host.run_in_window("bash")
|
||||||
|
elif cmd == "v" or cmd == "vtysh":
|
||||||
|
host.run_in_window("vtysh")
|
||||||
|
elif cmd == "x" or cmd == "xterm":
|
||||||
|
host.run_in_window("bash", forcex=True)
|
||||||
|
elif cmd == "sh":
|
||||||
|
hosts, cmd = host_cmd_split(unet, oargs)
|
||||||
|
for host in hosts:
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
spawn(unet, host, cmd)
|
||||||
|
else:
|
||||||
|
if len(hosts) > 1:
|
||||||
|
writef("------ Host: %s ------\n" % host)
|
||||||
|
output = unet.hosts[host].cmd_legacy(cmd)
|
||||||
|
writef(output)
|
||||||
|
if len(hosts) > 1:
|
||||||
|
writef("------- End: %s ------\n" % host)
|
||||||
|
writef("\n")
|
||||||
|
elif cmd == "h" or cmd == "help":
|
||||||
|
writef(
|
||||||
|
"""
|
||||||
|
Commands:
|
||||||
|
help :: this help
|
||||||
|
sh [hosts] <shell-command> :: execute <shell-command> on <host>
|
||||||
|
term [hosts] :: open shell terminals for hosts
|
||||||
|
vtysh [hosts] :: open vtysh terminals for hosts
|
||||||
|
[hosts] <vtysh-command> :: execute vtysh-command on hosts\n\n"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
hosts, cmd = host_cmd_split(unet, line)
|
||||||
|
for host in hosts:
|
||||||
|
if len(hosts) > 1:
|
||||||
|
writef("------ Host: %s ------\n" % host)
|
||||||
|
output = unet.hosts[host].cmd_legacy('vtysh -c "{}"'.format(cmd))
|
||||||
|
writef(output)
|
||||||
|
if len(hosts) > 1:
|
||||||
|
writef("------- End: %s ------\n" % host)
|
||||||
|
writef("\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def cli_server_setup(unet):
|
||||||
|
sockdir = tempfile.mkdtemp("-sockdir", "pyt")
|
||||||
|
sockpath = os.path.join(sockdir, "cli-server.sock")
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(10)
|
||||||
|
sock.bind(sockpath)
|
||||||
|
sock.listen(1)
|
||||||
|
return sock, sockdir, sockpath
|
||||||
|
except Exception:
|
||||||
|
unet.cmd_status("rm -rf " + sockdir)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def cli_server(unet, server_sock):
|
||||||
|
sock, addr = server_sock.accept()
|
||||||
|
|
||||||
|
# Go into full non-blocking mode now
|
||||||
|
sock.settimeout(None)
|
||||||
|
|
||||||
|
for line in lineiter(sock):
|
||||||
|
line = line.strip()
|
||||||
|
def writef(x):
|
||||||
|
xb = x.encode("utf-8")
|
||||||
|
sock.send(xb)
|
||||||
|
if not doline(unet, line, writef):
|
||||||
|
return
|
||||||
|
sock.send(ENDMARKER)
|
||||||
|
|
||||||
|
|
||||||
|
def cli_client(sockpath, prompt="unet> "):
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(10)
|
||||||
|
sock.connect(sockpath)
|
||||||
|
|
||||||
|
# Go into full non-blocking mode now
|
||||||
|
sock.settimeout(None)
|
||||||
|
|
||||||
|
print("\n--- Micronet CLI Starting ---\n\n")
|
||||||
|
while True:
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
line = raw_input(prompt) # pylint: disable=E0602
|
||||||
|
else:
|
||||||
|
line = input(prompt)
|
||||||
|
if line is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Need to put \n back
|
||||||
|
line += "\n"
|
||||||
|
|
||||||
|
# Send the CLI command
|
||||||
|
sock.send(line.encode("utf-8"))
|
||||||
|
|
||||||
|
def bendswith(b, sentinel):
|
||||||
|
slen = len(sentinel)
|
||||||
|
return len(b) >= slen and b[-slen:] == sentinel
|
||||||
|
|
||||||
|
# Collect the output
|
||||||
|
rb = b""
|
||||||
|
while not bendswith(rb, ENDMARKER):
|
||||||
|
lb = sock.recv(4096)
|
||||||
|
if not lb:
|
||||||
|
return
|
||||||
|
rb += lb
|
||||||
|
|
||||||
|
# Remove the marker
|
||||||
|
rb = rb[:-len(ENDMARKER)]
|
||||||
|
|
||||||
|
# Write the output
|
||||||
|
sys.stdout.write(rb.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def local_cli(unet, outf, prompt="unet> "):
|
||||||
|
print("\n--- Micronet CLI Starting ---\n\n")
|
||||||
|
while True:
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
line = raw_input(prompt) # pylint: disable=E0602
|
||||||
|
else:
|
||||||
|
line = input(prompt)
|
||||||
|
if line is None:
|
||||||
|
return
|
||||||
|
if not doline(unet, line, outf.write):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def cli(unet, histfile=None, sockpath=None, force_window=False, title=None, prompt=None, background=True):
|
||||||
|
if prompt is None:
|
||||||
|
prompt = "unet> "
|
||||||
|
|
||||||
|
if force_window or not sys.stdin.isatty():
|
||||||
|
# Run CLI in another window b/c we have no tty.
|
||||||
|
sock, sockdir, sockpath = cli_server_setup(unet)
|
||||||
|
|
||||||
|
python_path = unet.get_exec_path(["python3", "python"])
|
||||||
|
us = os.path.realpath(__file__)
|
||||||
|
cmd = "{} {}".format(python_path, us)
|
||||||
|
if histfile:
|
||||||
|
cmd += " --histfile=" + histfile
|
||||||
|
if title:
|
||||||
|
cmd += " --prompt={}".format(title)
|
||||||
|
cmd += " " + sockpath
|
||||||
|
|
||||||
|
try:
|
||||||
|
unet.run_in_window(cmd, new_window=True, title=title, background=background)
|
||||||
|
return cli_server(unet, sock)
|
||||||
|
finally:
|
||||||
|
unet.cmd_status("rm -rf " + sockdir)
|
||||||
|
|
||||||
|
if not unet:
|
||||||
|
logger.debug("client-cli using sockpath %s", sockpath)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if histfile is None:
|
||||||
|
histfile = os.path.expanduser("~/.micronet-history.txt")
|
||||||
|
if not os.path.exists(histfile):
|
||||||
|
if unet:
|
||||||
|
unet.cmd("touch " + histfile)
|
||||||
|
else:
|
||||||
|
subprocess.run("touch " + histfile)
|
||||||
|
if histfile:
|
||||||
|
readline.read_history_file(histfile)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sockpath:
|
||||||
|
cli_client(sockpath, prompt=prompt)
|
||||||
|
else:
|
||||||
|
local_cli(unet, sys.stdout, prompt=prompt)
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
except Exception as ex:
|
||||||
|
logger.critical("cli: got exception: %s", ex, exc_info=True)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
readline.write_history_file(histfile)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.DEBUG, filename="/tmp/topotests/cli-client.log")
|
||||||
|
logger = logging.getLogger("cli-client")
|
||||||
|
logger.info("Start logging cli-client")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--histfile", help="file to user for history")
|
||||||
|
parser.add_argument("--prompt-text", help="prompt string to use")
|
||||||
|
parser.add_argument("socket", help="path to pair of sockets to communicate over")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
prompt = "{}> ".format(args.prompt_text) if args.prompt_text else "unet> "
|
||||||
|
cli(None, args.histfile, args.socket, prompt=prompt)
|
379
tests/topotests/lib/micronet_compat.py
Normal file
379
tests/topotests/lib/micronet_compat.py
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
|
||||||
|
#
|
||||||
|
# July 11 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 logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
|
||||||
|
from lib.micronet import LinuxNamespace, Micronet
|
||||||
|
from lib.micronet_cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
def get_pids_with_env(has_var, has_val=None):
|
||||||
|
result = {}
|
||||||
|
for pidenv in glob.iglob("/proc/*/environ"):
|
||||||
|
pid = pidenv.split("/")[2]
|
||||||
|
with open(pidenv, "rb") as rfb:
|
||||||
|
envlist = [x.decode("utf-8").split("=", 1) for x in rfb.read().split(b"\0")]
|
||||||
|
envlist = [[x[0], ""] if len(x) == 1 else x for x in envlist]
|
||||||
|
envdict = dict(envlist)
|
||||||
|
if has_var not in envdict:
|
||||||
|
continue
|
||||||
|
if has_val is None:
|
||||||
|
result[pid] = envdict
|
||||||
|
elif envdict[has_var] == str(has_val):
|
||||||
|
result[pid] = envdict
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _kill_piddict(pids_by_upid, sig):
|
||||||
|
for upid, pids in pids_by_upid:
|
||||||
|
logging.info(
|
||||||
|
"Sending %s to (%s) of micronet pid %s", sig, ", ".join(pids), upid
|
||||||
|
)
|
||||||
|
for pid in pids:
|
||||||
|
try:
|
||||||
|
os.kill(int(pid), sig)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_our_pids():
|
||||||
|
ourpid = str(os.getpid())
|
||||||
|
piddict = get_pids_with_env("MICRONET_PID", ourpid)
|
||||||
|
pids = [x for x in piddict if x != ourpid]
|
||||||
|
if pids:
|
||||||
|
return {ourpid: pids}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_other_pids():
|
||||||
|
piddict = get_pids_with_env("MICRONET_PID")
|
||||||
|
unet_pids = {d["MICRONET_PID"] for d in piddict.values()}
|
||||||
|
pids_by_upid = {p: set() for p in unet_pids}
|
||||||
|
for pid, envdict in piddict.items():
|
||||||
|
pids_by_upid[envdict["MICRONET_PID"]].add(pid)
|
||||||
|
# Filter out any child pid sets whos micronet pid is still running
|
||||||
|
return {x: y for x, y in pids_by_upid.items() if x not in y}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pids_by_upid(ours):
|
||||||
|
if ours:
|
||||||
|
return _get_our_pids()
|
||||||
|
return _get_other_pids()
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_pids(ours):
|
||||||
|
pids_by_upid = _get_pids_by_upid(ours).items()
|
||||||
|
if not pids_by_upid:
|
||||||
|
return
|
||||||
|
|
||||||
|
_kill_piddict(pids_by_upid, signal.SIGTERM)
|
||||||
|
|
||||||
|
# Give them 5 second to exit cleanly
|
||||||
|
logging.info("Waiting up to 5s to allow for clean exit of abandon'd pids")
|
||||||
|
for _ in range(0, 5):
|
||||||
|
pids_by_upid = _get_pids_by_upid(ours).items()
|
||||||
|
if not pids_by_upid:
|
||||||
|
return
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
pids_by_upid = _get_pids_by_upid(ours).items()
|
||||||
|
_kill_piddict(pids_by_upid, signal.SIGKILL)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_current():
|
||||||
|
"""Attempt to cleanup preview runs.
|
||||||
|
|
||||||
|
Currently this only scans for old processes.
|
||||||
|
"""
|
||||||
|
logging.info("reaping current micronet processes")
|
||||||
|
_cleanup_pids(True)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_previous():
|
||||||
|
"""Attempt to cleanup preview runs.
|
||||||
|
|
||||||
|
Currently this only scans for old processes.
|
||||||
|
"""
|
||||||
|
logging.info("reaping past micronet processes")
|
||||||
|
_cleanup_pids(False)
|
||||||
|
|
||||||
|
|
||||||
|
class Node(LinuxNamespace):
|
||||||
|
"""Node (mininet compat)."""
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a Node.
|
||||||
|
"""
|
||||||
|
self.params = kwargs
|
||||||
|
|
||||||
|
if "private_mounts" in kwargs:
|
||||||
|
private_mounts = kwargs["private_mounts"]
|
||||||
|
else:
|
||||||
|
private_mounts = kwargs.get("privateDirs", [])
|
||||||
|
|
||||||
|
logger = kwargs.get("logger")
|
||||||
|
|
||||||
|
super(Node, self).__init__(name, logger=logger, private_mounts=private_mounts)
|
||||||
|
|
||||||
|
def cmd(self, cmd, **kwargs):
|
||||||
|
"""Execute a command, joins stdout, stderr, ignores exit status."""
|
||||||
|
|
||||||
|
return super(Node, self).cmd_legacy(cmd, **kwargs)
|
||||||
|
|
||||||
|
def config(self, lo="up", **params):
|
||||||
|
"""Called by Micronet when topology is built (but not started)."""
|
||||||
|
# mininet brings up loopback here.
|
||||||
|
del params
|
||||||
|
del lo
|
||||||
|
|
||||||
|
def intfNames(self):
|
||||||
|
return self.intfs
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class Topo(object): # pylint: disable=R0205
|
||||||
|
"""
|
||||||
|
Topology object passed to Micronet to build actual topology.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.params = kwargs
|
||||||
|
self.name = kwargs["name"] if "name" in kwargs else "unnamed"
|
||||||
|
self.tgen = kwargs["tgen"] if "tgen" in kwargs else None
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(__name__ + ".topo")
|
||||||
|
|
||||||
|
self.logger.debug("%s: Creating", self)
|
||||||
|
|
||||||
|
self.nodes = {}
|
||||||
|
self.hosts = {}
|
||||||
|
self.switches = {}
|
||||||
|
self.links = {}
|
||||||
|
|
||||||
|
# if "no_init_build" in kwargs and kwargs["no_init_build"]:
|
||||||
|
# return
|
||||||
|
|
||||||
|
# This needs to move outside of here. Current tests count on it being called on init;
|
||||||
|
# however, b/c of this there is lots of twisty logic to support topogen based tests where
|
||||||
|
# the build routine must get get_topogen() so topogen can then set it's topogen.topo to the
|
||||||
|
# class it's in the process of instantiating (this one) b/c build will use topogen before
|
||||||
|
# the instantiation completes.
|
||||||
|
self.build(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Topo({})".format(self.name)
|
||||||
|
|
||||||
|
def build(self, *args, **kwargs):
|
||||||
|
"Overriden by real class"
|
||||||
|
del args
|
||||||
|
del kwargs
|
||||||
|
raise NotImplementedError("Needs to be overriden")
|
||||||
|
|
||||||
|
def addHost(self, name, **kwargs):
|
||||||
|
self.logger.debug("%s: addHost %s", self, name)
|
||||||
|
self.nodes[name] = dict(**kwargs)
|
||||||
|
self.hosts[name] = self.nodes[name]
|
||||||
|
return name
|
||||||
|
|
||||||
|
addNode = addHost
|
||||||
|
|
||||||
|
def addSwitch(self, name, **kwargs):
|
||||||
|
self.logger.debug("%s: addSwitch %s", self, name)
|
||||||
|
self.nodes[name] = dict(**kwargs)
|
||||||
|
if "cls" in self.nodes[name]:
|
||||||
|
self.logger.warning("Overriding Bridge class with micronet.Bridge")
|
||||||
|
del self.nodes[name]["cls"]
|
||||||
|
self.switches[name] = self.nodes[name]
|
||||||
|
return name
|
||||||
|
|
||||||
|
def addLink(self, name1, name2, **kwargs):
|
||||||
|
"""Link up switch and a router.
|
||||||
|
|
||||||
|
possible kwargs:
|
||||||
|
- intfName1 :: switch-side interface name - sometimes missing
|
||||||
|
- intfName2 :: router-side interface name
|
||||||
|
- addr1 :: switch-side MAC used by test_ldp_topo1 only
|
||||||
|
- addr2 :: router-side MAC used by test_ldp_topo1 only
|
||||||
|
"""
|
||||||
|
if1 = (
|
||||||
|
kwargs["intfName1"]
|
||||||
|
if "intfName1" in kwargs
|
||||||
|
else "{}-{}".format(name1, name2)
|
||||||
|
)
|
||||||
|
if2 = (
|
||||||
|
kwargs["intfName2"]
|
||||||
|
if "intfName2" in kwargs
|
||||||
|
else "{}-{}".format(name2, name1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.debug("%s: addLink %s %s if1: %s if2: %s", self, name1, name2, if1, if2)
|
||||||
|
|
||||||
|
if name1 in self.switches:
|
||||||
|
assert name2 in self.hosts
|
||||||
|
swname, rname = name1, name2
|
||||||
|
elif name2 in self.switches:
|
||||||
|
assert name1 in self.hosts
|
||||||
|
swname, rname = name2, name1
|
||||||
|
if1, if2 = if2, if1
|
||||||
|
else:
|
||||||
|
# p2p link
|
||||||
|
assert name1 in self.hosts
|
||||||
|
assert name2 in self.hosts
|
||||||
|
swname, rname = name1, name2
|
||||||
|
|
||||||
|
if swname not in self.links:
|
||||||
|
self.links[swname] = {}
|
||||||
|
|
||||||
|
if rname not in self.links[swname]:
|
||||||
|
self.links[swname][rname] = set()
|
||||||
|
|
||||||
|
self.links[swname][rname].add((if1, if2))
|
||||||
|
|
||||||
|
|
||||||
|
class Mininet(Micronet):
|
||||||
|
"""
|
||||||
|
Mininet using Micronet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
g_mnet_inst = None
|
||||||
|
|
||||||
|
def __init__(self, controller=None, topo=None):
|
||||||
|
"""
|
||||||
|
Create a Micronet.
|
||||||
|
"""
|
||||||
|
assert not controller
|
||||||
|
|
||||||
|
if Mininet.g_mnet_inst is not None:
|
||||||
|
Mininet.g_mnet_inst.stop()
|
||||||
|
Mininet.g_mnet_inst = self
|
||||||
|
|
||||||
|
self.configured_hosts = set()
|
||||||
|
self.host_params = {}
|
||||||
|
self.prefix_len = 8
|
||||||
|
|
||||||
|
# SNMPd used to require this, which was set int he mininet shell
|
||||||
|
# that all commands executed from. This is goofy default so let's not
|
||||||
|
# do it if we don't have to. The snmpd.conf files have been updated
|
||||||
|
# to set permissions to root:frr 770 to make this unneeded in that case
|
||||||
|
# os.umask(0)
|
||||||
|
|
||||||
|
super(Mininet, self).__init__()
|
||||||
|
|
||||||
|
self.logger.debug("%s: Creating", self)
|
||||||
|
|
||||||
|
if topo and topo.hosts:
|
||||||
|
self.logger.debug("Adding hosts: %s", topo.hosts.keys())
|
||||||
|
for name in topo.hosts:
|
||||||
|
self.add_host(name, **topo.hosts[name])
|
||||||
|
|
||||||
|
if topo and topo.switches:
|
||||||
|
self.logger.debug("Adding switches: %s", topo.switches.keys())
|
||||||
|
for name in topo.switches:
|
||||||
|
self.add_switch(name, **topo.switches[name])
|
||||||
|
|
||||||
|
if topo and topo.links:
|
||||||
|
self.logger.debug("Adding links: ")
|
||||||
|
for swname in sorted(topo.links):
|
||||||
|
for rname in sorted(topo.links[swname]):
|
||||||
|
for link in topo.links[swname][rname]:
|
||||||
|
self.add_link(swname, rname, link[0], link[1])
|
||||||
|
|
||||||
|
if topo:
|
||||||
|
# Now that topology is built, configure hosts
|
||||||
|
self.configure_hosts()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Mininet()"
|
||||||
|
|
||||||
|
def configure_hosts(self):
|
||||||
|
"""
|
||||||
|
Configure hosts once the topology has been built.
|
||||||
|
|
||||||
|
This function can be called multiple times if routers are added to the topology
|
||||||
|
later.
|
||||||
|
"""
|
||||||
|
if not self.hosts:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.debug("Configuring hosts: %s", self.hosts.keys())
|
||||||
|
|
||||||
|
for name in sorted(self.hosts.keys()):
|
||||||
|
if name in self.configured_hosts:
|
||||||
|
continue
|
||||||
|
|
||||||
|
host = self.hosts[name]
|
||||||
|
first_intf = host.intfs[0] if host.intfs else None
|
||||||
|
params = self.host_params[name]
|
||||||
|
|
||||||
|
if first_intf and "ip" in params:
|
||||||
|
ip = params["ip"]
|
||||||
|
i = ip.find("/")
|
||||||
|
if i == -1:
|
||||||
|
plen = self.prefix_len
|
||||||
|
else:
|
||||||
|
plen = int(ip[i + 1 :])
|
||||||
|
ip = ip[:i]
|
||||||
|
|
||||||
|
host.cmd_raises("ip addr add {}/{} dev {}".format(ip, plen, first_intf))
|
||||||
|
|
||||||
|
if "defaultRoute" in params:
|
||||||
|
host.cmd_raises(
|
||||||
|
"ip route add default {}".format(params["defaultRoute"])
|
||||||
|
)
|
||||||
|
|
||||||
|
host.config()
|
||||||
|
|
||||||
|
self.configured_hosts.add(name)
|
||||||
|
|
||||||
|
def add_host(self, name, cls=Node, **kwargs):
|
||||||
|
"""Add a host to micronet."""
|
||||||
|
|
||||||
|
self.host_params[name] = kwargs
|
||||||
|
super(Mininet, self).add_host(name, cls=cls, **kwargs)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the micronet topology."""
|
||||||
|
self.logger.debug("%s: Starting (no-op).", self)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the mininet topology (deletes)."""
|
||||||
|
self.logger.debug("%s: Stopping (deleting).", self)
|
||||||
|
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
self.logger.debug("%s: Stopped (deleted).", self)
|
||||||
|
|
||||||
|
if Mininet.g_mnet_inst == self:
|
||||||
|
Mininet.g_mnet_inst = None
|
||||||
|
|
||||||
|
def cli(self):
|
||||||
|
cli(self)
|
Loading…
Reference in New Issue
Block a user