mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-08-08 14:04:32 +00:00
tests: switch to munet
Signed-off-by: Christian Hopps <chopps@labn.net>
This commit is contained in:
parent
352ddc72b7
commit
60e037780e
@ -7,21 +7,23 @@ import glob
|
|||||||
import os
|
import os
|
||||||
import pdb
|
import pdb
|
||||||
import re
|
import re
|
||||||
|
import resource
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import resource
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import lib.fixtures
|
import lib.fixtures
|
||||||
from lib import topolog
|
from lib import topolog
|
||||||
from lib.micronet import Commander, proc_error
|
from lib.micronet_compat import Mininet
|
||||||
from lib.micronet_cli import cli
|
|
||||||
from lib.micronet_compat import Mininet, cleanup_current, cleanup_previous
|
|
||||||
from lib.topogen import diagnose_env, get_topogen
|
from lib.topogen import diagnose_env, get_topogen
|
||||||
from lib.topolog import logger
|
from lib.topolog import logger
|
||||||
from lib.topotest import g_extra_config as topotest_extra_config
|
from lib.topotest import g_extra_config as topotest_extra_config
|
||||||
from lib.topotest import json_cmp_result
|
from lib.topotest import json_cmp_result
|
||||||
|
from munet.base import Commander, proc_error
|
||||||
|
from munet.cleanup import cleanup_current, cleanup_previous
|
||||||
|
from munet import cli
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
@ -501,7 +503,7 @@ def pytest_runtest_makereport(item, call):
|
|||||||
# Really would like something better than using this global here.
|
# Really would like something better than using this global here.
|
||||||
# Not all tests use topogen though so get_topogen() won't work.
|
# Not all tests use topogen though so get_topogen() won't work.
|
||||||
if Mininet.g_mnet_inst:
|
if Mininet.g_mnet_inst:
|
||||||
cli(Mininet.g_mnet_inst, title=title, background=False)
|
cli.cli(Mininet.g_mnet_inst, title=title, background=False)
|
||||||
else:
|
else:
|
||||||
logger.error("Could not launch CLI b/c no mininet exists yet")
|
logger.error("Could not launch CLI b/c no mininet exists yet")
|
||||||
|
|
||||||
@ -515,7 +517,7 @@ def pytest_runtest_makereport(item, call):
|
|||||||
user = user.strip()
|
user = user.strip()
|
||||||
|
|
||||||
if user == "cli":
|
if user == "cli":
|
||||||
cli(Mininet.g_mnet_inst)
|
cli.cli(Mininet.g_mnet_inst)
|
||||||
elif user == "pdb":
|
elif user == "pdb":
|
||||||
pdb.set_trace() # pylint: disable=forgotten-debug-statement
|
pdb.set_trace() # pylint: disable=forgotten-debug-statement
|
||||||
elif user:
|
elif user:
|
||||||
|
@ -21,7 +21,8 @@ try:
|
|||||||
import grpc
|
import grpc
|
||||||
import grpc_tools
|
import grpc_tools
|
||||||
|
|
||||||
from micronet import commander
|
sys.path.append(os.path.dirname(CWD))
|
||||||
|
from munet.base import commander
|
||||||
|
|
||||||
commander.cmd_raises(f"cp {CWD}/../../../grpc/frr-northbound.proto .")
|
commander.cmd_raises(f"cp {CWD}/../../../grpc/frr-northbound.proto .")
|
||||||
commander.cmd_raises(
|
commander.cmd_raises(
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,306 +0,0 @@
|
|||||||
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
|
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
#
|
|
||||||
# July 24 2021, Christian Hopps <chopps@labn.net>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2021, LabN Consulting, L.L.C.
|
|
||||||
#
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import pty
|
|
||||||
import re
|
|
||||||
import readline
|
|
||||||
import select
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import termios
|
|
||||||
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 if x in unet.hosts]
|
|
||||||
for host in hosts:
|
|
||||||
if cmd == "t" or cmd == "term":
|
|
||||||
host.run_in_window("bash", title="sh-%s" % host)
|
|
||||||
elif cmd == "v" or cmd == "vtysh":
|
|
||||||
host.run_in_window("vtysh", title="vt-%s" % host)
|
|
||||||
elif cmd == "x" or cmd == "xterm":
|
|
||||||
host.run_in_window("bash", title="sh-%s" % host, 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,
|
|
||||||
):
|
|
||||||
logger = logging.getLogger("cli-client")
|
|
||||||
|
|
||||||
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)
|
|
@ -3,140 +3,43 @@
|
|||||||
#
|
#
|
||||||
# July 11 2021, Christian Hopps <chopps@labn.net>
|
# July 11 2021, Christian Hopps <chopps@labn.net>
|
||||||
#
|
#
|
||||||
# Copyright (c) 2021, LabN Consulting, L.L.C
|
# Copyright (c) 2021-2023, LabN Consulting, L.L.C
|
||||||
#
|
#
|
||||||
|
import ipaddress
|
||||||
import glob
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import time
|
|
||||||
|
|
||||||
from lib.micronet import LinuxNamespace, Micronet
|
from munet import cli
|
||||||
from lib.micronet_cli import cli
|
from munet.base import BaseMunet, LinuxNamespace
|
||||||
|
|
||||||
|
|
||||||
def get_pids_with_env(has_var, has_val=None):
|
|
||||||
result = {}
|
|
||||||
for pidenv in glob.iglob("/proc/*/environ"):
|
|
||||||
pid = pidenv.split("/")[2]
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
except Exception:
|
|
||||||
# E.g., process exited and files are gone
|
|
||||||
pass
|
|
||||||
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):
|
class Node(LinuxNamespace):
|
||||||
"""Node (mininet compat)."""
|
"""Node (mininet compat)."""
|
||||||
|
|
||||||
def __init__(self, name, **kwargs):
|
def __init__(self, name, rundir=None, **kwargs):
|
||||||
"""
|
|
||||||
Create a Node.
|
|
||||||
"""
|
|
||||||
self.params = kwargs
|
|
||||||
|
|
||||||
|
nkwargs = {}
|
||||||
|
|
||||||
|
if "unet" in kwargs:
|
||||||
|
nkwargs["unet"] = kwargs["unet"]
|
||||||
if "private_mounts" in kwargs:
|
if "private_mounts" in kwargs:
|
||||||
private_mounts = kwargs["private_mounts"]
|
nkwargs["private_mounts"] = kwargs["private_mounts"]
|
||||||
else:
|
|
||||||
private_mounts = kwargs.get("privateDirs", [])
|
|
||||||
|
|
||||||
logger = kwargs.get("logger")
|
# This is expected by newer munet CLI code
|
||||||
|
self.config_dirname = ""
|
||||||
|
self.config = {"kind": "frr"}
|
||||||
|
self.mgmt_ip = None
|
||||||
|
self.mgmt_ip6 = None
|
||||||
|
|
||||||
super(Node, self).__init__(name, logger=logger, private_mounts=private_mounts)
|
super().__init__(name, **nkwargs)
|
||||||
|
|
||||||
|
self.rundir = self.unet.rundir.joinpath(self.name)
|
||||||
|
|
||||||
def cmd(self, cmd, **kwargs):
|
def cmd(self, cmd, **kwargs):
|
||||||
"""Execute a command, joins stdout, stderr, ignores exit status."""
|
"""Execute a command, joins stdout, stderr, ignores exit status."""
|
||||||
|
|
||||||
return super(Node, self).cmd_legacy(cmd, **kwargs)
|
return super(Node, self).cmd_legacy(cmd, **kwargs)
|
||||||
|
|
||||||
def config(self, lo="up", **params):
|
def config_host(self, lo="up", **params):
|
||||||
"""Called by Micronet when topology is built (but not started)."""
|
"""Called by Micronet when topology is built (but not started)."""
|
||||||
# mininet brings up loopback here.
|
# mininet brings up loopback here.
|
||||||
del params
|
del params
|
||||||
@ -148,20 +51,76 @@ class Node(LinuxNamespace):
|
|||||||
def terminate(self):
|
def terminate(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def add_vlan(self, vlanname, linkiface, vlanid):
|
||||||
|
self.logger.debug("Adding VLAN interface: %s (%s)", vlanname, vlanid)
|
||||||
|
ip_path = self.get_exec_path("ip")
|
||||||
|
assert ip_path, "XXX missing ip command!"
|
||||||
|
self.cmd_raises(
|
||||||
|
[
|
||||||
|
ip_path,
|
||||||
|
"link",
|
||||||
|
"add",
|
||||||
|
"link",
|
||||||
|
linkiface,
|
||||||
|
"name",
|
||||||
|
vlanname,
|
||||||
|
"type",
|
||||||
|
"vlan",
|
||||||
|
"id",
|
||||||
|
vlanid,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.cmd_raises([ip_path, "link", "set", "dev", vlanname, "up"])
|
||||||
|
|
||||||
|
def add_loop(self, loopname):
|
||||||
|
self.logger.debug("Adding Linux iface: %s", loopname)
|
||||||
|
ip_path = self.get_exec_path("ip")
|
||||||
|
assert ip_path, "XXX missing ip command!"
|
||||||
|
self.cmd_raises([ip_path, "link", "add", loopname, "type", "dummy"])
|
||||||
|
self.cmd_raises([ip_path, "link", "set", "dev", loopname, "up"])
|
||||||
|
|
||||||
|
def add_l3vrf(self, vrfname, tableid):
|
||||||
|
self.logger.debug("Adding Linux VRF: %s", vrfname)
|
||||||
|
ip_path = self.get_exec_path("ip")
|
||||||
|
assert ip_path, "XXX missing ip command!"
|
||||||
|
self.cmd_raises(
|
||||||
|
[ip_path, "link", "add", vrfname, "type", "vrf", "table", tableid]
|
||||||
|
)
|
||||||
|
self.cmd_raises([ip_path, "link", "set", "dev", vrfname, "up"])
|
||||||
|
|
||||||
|
def del_iface(self, iface):
|
||||||
|
self.logger.debug("Removing Linux Iface: %s", iface)
|
||||||
|
ip_path = self.get_exec_path("ip")
|
||||||
|
assert ip_path, "XXX missing ip command!"
|
||||||
|
self.cmd_raises([ip_path, "link", "del", iface])
|
||||||
|
|
||||||
|
def attach_iface_to_l3vrf(self, ifacename, vrfname):
|
||||||
|
self.logger.debug("Attaching Iface %s to Linux VRF %s", ifacename, vrfname)
|
||||||
|
ip_path = self.get_exec_path("ip")
|
||||||
|
assert ip_path, "XXX missing ip command!"
|
||||||
|
if vrfname:
|
||||||
|
self.cmd_raises(
|
||||||
|
[ip_path, "link", "set", "dev", ifacename, "master", vrfname]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.cmd_raises([ip_path, "link", "set", "dev", ifacename, "nomaster"])
|
||||||
|
|
||||||
|
set_cwd = LinuxNamespace.set_ns_cwd
|
||||||
|
|
||||||
|
|
||||||
class Topo(object): # pylint: disable=R0205
|
class Topo(object): # pylint: disable=R0205
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
raise Exception("Remove Me")
|
raise Exception("Remove Me")
|
||||||
|
|
||||||
|
|
||||||
class Mininet(Micronet):
|
class Mininet(BaseMunet):
|
||||||
"""
|
"""
|
||||||
Mininet using Micronet.
|
Mininet using Micronet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
g_mnet_inst = None
|
g_mnet_inst = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, rundir=None):
|
||||||
"""
|
"""
|
||||||
Create a Micronet.
|
Create a Micronet.
|
||||||
"""
|
"""
|
||||||
@ -179,7 +138,146 @@ class Mininet(Micronet):
|
|||||||
# to set permissions to root:frr 770 to make this unneeded in that case
|
# to set permissions to root:frr 770 to make this unneeded in that case
|
||||||
# os.umask(0)
|
# os.umask(0)
|
||||||
|
|
||||||
super(Mininet, self).__init__()
|
super(Mininet, self).__init__(pid=False, rundir=rundir)
|
||||||
|
|
||||||
|
# From munet/munet/native.py
|
||||||
|
with open(os.path.join(self.rundir, "nspid"), "w", encoding="ascii") as f:
|
||||||
|
f.write(f"{self.pid}\n")
|
||||||
|
|
||||||
|
with open(os.path.join(self.rundir, "nspids"), "w", encoding="ascii") as f:
|
||||||
|
f.write(f'{" ".join([str(x) for x in self.pids])}\n')
|
||||||
|
|
||||||
|
hosts_file = os.path.join(self.rundir, "hosts.txt")
|
||||||
|
with open(hosts_file, "w", encoding="ascii") as hf:
|
||||||
|
hf.write(
|
||||||
|
f"""127.0.0.1\tlocalhost {self.name}
|
||||||
|
::1\tip6-localhost ip6-loopback
|
||||||
|
fe00::0\tip6-localnet
|
||||||
|
ff00::0\tip6-mcastprefix
|
||||||
|
ff02::1\tip6-allnodes
|
||||||
|
ff02::2\tip6-allrouters
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.bind_mount(hosts_file, "/etc/hosts")
|
||||||
|
|
||||||
|
# Common CLI commands for any topology
|
||||||
|
cdict = {
|
||||||
|
"commands": [
|
||||||
|
#
|
||||||
|
# Window commands.
|
||||||
|
#
|
||||||
|
{
|
||||||
|
"name": "pcap",
|
||||||
|
"format": "pcap NETWORK",
|
||||||
|
"help": (
|
||||||
|
"capture packets from NETWORK into file capture-NETWORK.pcap"
|
||||||
|
" the command is run within a new window which also shows"
|
||||||
|
" packet summaries. NETWORK can also be an interface specified"
|
||||||
|
" as HOST:INTF. To capture inside the host namespace."
|
||||||
|
),
|
||||||
|
"exec": "tshark -s 9200 -i {0} -P -w capture-{0}.pcap",
|
||||||
|
"top-level": True,
|
||||||
|
"new-window": {"background": True},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "term",
|
||||||
|
"format": "term HOST [HOST ...]",
|
||||||
|
"help": "open terminal[s] (TMUX or XTerm) on HOST[S], * for all",
|
||||||
|
"exec": "bash",
|
||||||
|
"new-window": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtysh",
|
||||||
|
"exec": "/usr/bin/vtysh",
|
||||||
|
"format": "vtysh ROUTER [ROUTER ...]",
|
||||||
|
"new-window": True,
|
||||||
|
"kinds": ["frr"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xterm",
|
||||||
|
"format": "xterm HOST [HOST ...]",
|
||||||
|
"help": "open XTerm[s] on HOST[S], * for all",
|
||||||
|
"exec": "bash",
|
||||||
|
"new-window": {
|
||||||
|
"forcex": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "logd",
|
||||||
|
"exec": "tail -F %RUNDIR%/{}.log",
|
||||||
|
"format": "logd HOST [HOST ...] DAEMON",
|
||||||
|
"help": (
|
||||||
|
"tail -f on the logfile of the given "
|
||||||
|
"DAEMON for the given HOST[S]"
|
||||||
|
),
|
||||||
|
"new-window": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stdlog",
|
||||||
|
"exec": (
|
||||||
|
"[ -e %RUNDIR%/frr.log ] && tail -F %RUNDIR%/frr.log "
|
||||||
|
"|| tail -F /var/log/frr.log"
|
||||||
|
),
|
||||||
|
"format": "stdlog HOST [HOST ...]",
|
||||||
|
"help": "tail -f on the `frr.log` for the given HOST[S]",
|
||||||
|
"new-window": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"exec": "tail -F %RUNDIR%/{0}.err",
|
||||||
|
"format": "stdout HOST [HOST ...] DAEMON",
|
||||||
|
"help": (
|
||||||
|
"tail -f on the stdout of the given DAEMON for the given HOST[S]"
|
||||||
|
),
|
||||||
|
"new-window": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stderr",
|
||||||
|
"exec": "tail -F %RUNDIR%/{0}.out",
|
||||||
|
"format": "stderr HOST [HOST ...] DAEMON",
|
||||||
|
"help": (
|
||||||
|
"tail -f on the stderr of the given DAEMON for the given HOST[S]"
|
||||||
|
),
|
||||||
|
"new-window": True,
|
||||||
|
},
|
||||||
|
#
|
||||||
|
# Non-window commands.
|
||||||
|
#
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"exec": "vtysh -c '{}'",
|
||||||
|
"format": "[ROUTER ...] COMMAND",
|
||||||
|
"help": "execute vtysh COMMAND on the router[s]",
|
||||||
|
"kinds": ["frr"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sh",
|
||||||
|
"format": "[HOST ...] sh <SHELL-COMMAND>",
|
||||||
|
"help": "execute <SHELL-COMMAND> on hosts",
|
||||||
|
"exec": "{}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shi",
|
||||||
|
"format": "[HOST ...] shi <INTERACTIVE-COMMAND>",
|
||||||
|
"help": "execute <INTERACTIVE-COMMAND> on HOST[s]",
|
||||||
|
"exec": "{}",
|
||||||
|
"interactive": True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.add_cli_config(self, cdict)
|
||||||
|
|
||||||
|
# shellopt = (
|
||||||
|
# self.pytest_config.getoption("--shell") if self.pytest_config else None
|
||||||
|
# )
|
||||||
|
# shellopt = shellopt if shellopt is not None else ""
|
||||||
|
# if shellopt == "all" or "." in shellopt.split(","):
|
||||||
|
# self.run_in_window("bash")
|
||||||
|
|
||||||
|
# This is expected by newer munet CLI code
|
||||||
|
self.config_dirname = ""
|
||||||
|
self.config = {}
|
||||||
|
|
||||||
self.logger.debug("%s: Creating", self)
|
self.logger.debug("%s: Creating", self)
|
||||||
|
|
||||||
@ -217,12 +315,15 @@ class Mininet(Micronet):
|
|||||||
|
|
||||||
host.cmd_raises("ip addr add {}/{} dev {}".format(ip, plen, first_intf))
|
host.cmd_raises("ip addr add {}/{} dev {}".format(ip, plen, first_intf))
|
||||||
|
|
||||||
|
# can be used by munet cli
|
||||||
|
host.mgmt_ip = ipaddress.ip_address(ip)
|
||||||
|
|
||||||
if "defaultRoute" in params:
|
if "defaultRoute" in params:
|
||||||
host.cmd_raises(
|
host.cmd_raises(
|
||||||
"ip route add default {}".format(params["defaultRoute"])
|
"ip route add default {}".format(params["defaultRoute"])
|
||||||
)
|
)
|
||||||
|
|
||||||
host.config()
|
host.config_host()
|
||||||
|
|
||||||
self.configured_hosts.add(name)
|
self.configured_hosts.add(name)
|
||||||
|
|
||||||
@ -248,4 +349,4 @@ class Mininet(Micronet):
|
|||||||
Mininet.g_mnet_inst = None
|
Mininet.g_mnet_inst = None
|
||||||
|
|
||||||
def cli(self):
|
def cli(self):
|
||||||
cli(self)
|
cli.cli(self)
|
||||||
|
@ -212,7 +212,7 @@ class Topogen(object):
|
|||||||
# Mininet(Micronet) to build the actual topology.
|
# Mininet(Micronet) to build the actual topology.
|
||||||
assert not inspect.isclass(topodef)
|
assert not inspect.isclass(topodef)
|
||||||
|
|
||||||
self.net = Mininet()
|
self.net = Mininet(rundir=self.logdir)
|
||||||
|
|
||||||
# Adjust the parent namespace
|
# Adjust the parent namespace
|
||||||
topotest.fix_netns_limits(self.net)
|
topotest.fix_netns_limits(self.net)
|
||||||
@ -752,8 +752,8 @@ class TopoRouter(TopoGear):
|
|||||||
"""
|
"""
|
||||||
super(TopoRouter, self).__init__(tgen, name, **params)
|
super(TopoRouter, self).__init__(tgen, name, **params)
|
||||||
self.routertype = params.get("routertype", "frr")
|
self.routertype = params.get("routertype", "frr")
|
||||||
if "privateDirs" not in params:
|
if "private_mounts" not in params:
|
||||||
params["privateDirs"] = self.PRIVATE_DIRS
|
params["private_mounts"] = self.PRIVATE_DIRS
|
||||||
|
|
||||||
# Propagate the router log directory
|
# Propagate the router log directory
|
||||||
logfile = self._setup_tmpdir()
|
logfile = self._setup_tmpdir()
|
||||||
@ -1100,7 +1100,7 @@ class TopoHost(TopoGear):
|
|||||||
* `ip`: the IP address (string) for the host interface
|
* `ip`: the IP address (string) for the host interface
|
||||||
* `defaultRoute`: the default route that will be installed
|
* `defaultRoute`: the default route that will be installed
|
||||||
(e.g. 'via 10.0.0.1')
|
(e.g. 'via 10.0.0.1')
|
||||||
* `privateDirs`: directories that will be mounted on a different domain
|
* `private_mounts`: directories that will be mounted on a different domain
|
||||||
(e.g. '/etc/important_dir').
|
(e.g. '/etc/important_dir').
|
||||||
"""
|
"""
|
||||||
super(TopoHost, self).__init__(tgen, name, **params)
|
super(TopoHost, self).__init__(tgen, name, **params)
|
||||||
@ -1120,10 +1120,10 @@ class TopoHost(TopoGear):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
gear = super(TopoHost, self).__str__()
|
gear = super(TopoHost, self).__str__()
|
||||||
gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
|
gear += ' TopoHost<ip="{}",defaultRoute="{}",private_mounts="{}">'.format(
|
||||||
self.params["ip"],
|
self.params["ip"],
|
||||||
self.params["defaultRoute"],
|
self.params["defaultRoute"],
|
||||||
str(self.params["privateDirs"]),
|
str(self.params["private_mounts"]),
|
||||||
)
|
)
|
||||||
return gear
|
return gear
|
||||||
|
|
||||||
@ -1146,10 +1146,10 @@ class TopoExaBGP(TopoHost):
|
|||||||
(e.g. 'via 10.0.0.1')
|
(e.g. 'via 10.0.0.1')
|
||||||
|
|
||||||
Note: the different between a host and a ExaBGP peer is that this class
|
Note: the different between a host and a ExaBGP peer is that this class
|
||||||
has a privateDirs already defined and contains functions to handle ExaBGP
|
has a private_mounts already defined and contains functions to handle
|
||||||
things.
|
ExaBGP things.
|
||||||
"""
|
"""
|
||||||
params["privateDirs"] = self.PRIVATE_DIRS
|
params["private_mounts"] = self.PRIVATE_DIRS
|
||||||
super(TopoExaBGP, self).__init__(tgen, name, **params)
|
super(TopoExaBGP, self).__init__(tgen, name, **params)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -1318,7 +1318,7 @@ def setup_node_tmpdir(logdir, name):
|
|||||||
class Router(Node):
|
class Router(Node):
|
||||||
"A Node with IPv4/IPv6 forwarding enabled"
|
"A Node with IPv4/IPv6 forwarding enabled"
|
||||||
|
|
||||||
def __init__(self, name, **params):
|
def __init__(self, name, *posargs, **params):
|
||||||
|
|
||||||
# Backward compatibility:
|
# Backward compatibility:
|
||||||
# Load configuration defaults like topogen.
|
# Load configuration defaults like topogen.
|
||||||
@ -1347,7 +1347,7 @@ class Router(Node):
|
|||||||
l = topolog.get_logger(name, log_level="debug", target=logfile)
|
l = topolog.get_logger(name, log_level="debug", target=logfile)
|
||||||
params["logger"] = l
|
params["logger"] = l
|
||||||
|
|
||||||
super(Router, self).__init__(name, **params)
|
super(Router, self).__init__(name, *posargs, **params)
|
||||||
|
|
||||||
self.daemondir = None
|
self.daemondir = None
|
||||||
self.hasmpls = False
|
self.hasmpls = False
|
||||||
@ -1407,8 +1407,8 @@ class Router(Node):
|
|||||||
|
|
||||||
# pylint: disable=W0221
|
# pylint: disable=W0221
|
||||||
# Some params are only meaningful for the parent class.
|
# Some params are only meaningful for the parent class.
|
||||||
def config(self, **params):
|
def config_host(self, **params):
|
||||||
super(Router, self).config(**params)
|
super(Router, self).config_host(**params)
|
||||||
|
|
||||||
# User did not specify the daemons directory, try to autodetect it.
|
# User did not specify the daemons directory, try to autodetect it.
|
||||||
self.daemondir = params.get("daemondir")
|
self.daemondir = params.get("daemondir")
|
||||||
|
@ -24,7 +24,7 @@ log_file_date_format = %Y-%m-%d %H:%M:%S
|
|||||||
junit_logging = all
|
junit_logging = all
|
||||||
junit_log_passing_tests = true
|
junit_log_passing_tests = true
|
||||||
|
|
||||||
norecursedirs = .git example_test example_topojson_test lib docker
|
norecursedirs = .git example_test example_topojson_test lib munet docker
|
||||||
|
|
||||||
# Directory to store test results and run logs in, default shown
|
# Directory to store test results and run logs in, default shown
|
||||||
# rundir = /tmp/topotests
|
# rundir = /tmp/topotests
|
||||||
|
Loading…
Reference in New Issue
Block a user