Merge pull request #13332 from LabNConsulting/chopps/new-opts

This commit is contained in:
Donald Sharp 2023-04-22 19:51:05 -04:00 committed by GitHub
commit 2da3174115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 370 additions and 241 deletions

View File

@ -402,6 +402,63 @@ environment.
.. _screen: https://www.gnu.org/software/screen/ .. _screen: https://www.gnu.org/software/screen/
.. _tmux: https://github.com/tmux/tmux/wiki .. _tmux: https://github.com/tmux/tmux/wiki
Capturing Packets
"""""""""""""""""
One can view and capture packets on any of the networks or interfaces defined by
the topotest by specifying the ``--pcap=NET|INTF|all[,NET|INTF,...]`` CLI option
as shown in the examples below.
.. code:: shell
# Capture on all networks in isis_topo1 test
sudo -E pytest isis_topo1 --pcap=all
# Capture on `sw1` network
sudo -E pytest isis_topo1 --pcap=sw1
# Capture on `sw1` network and on interface `eth0` on router `r2`
sudo -E pytest isis_topo1 --pcap=sw1,r2:r2-eth0
For each capture a window is opened displaying a live summary of the captured
packets. Additionally, the entire packet stream is captured in a pcap file in
the tests log directory e.g.,::
.. code:: console
$ sudo -E pytest isis_topo1 --pcap=sw1,r2:r2-eth0
...
$ ls -l /tmp/topotests/isis_topo1.test_isis_topo1/
-rw------- 1 root root 45172 Apr 19 05:30 capture-r2-r2-eth0.pcap
-rw------- 1 root root 48412 Apr 19 05:30 capture-sw1.pcap
...
-
Viewing Live Daemon Logs
""""""""""""""""""""""""
One can live view daemon or the frr logs in separate windows using the
``--logd`` CLI option as shown below.
.. code:: shell
# View `ripd` logs on all routers in test
sudo -E pytest rip_allow_ecmp --logd=ripd
# View `ripd` logs on all routers and `mgmtd` log on `r1`
sudo -E pytest rip_allow_ecmp --logd=ripd --logd=mgmtd,r1
For each capture a window is opened displaying a live summary of the captured
packets. Additionally, the entire packet stream is captured in a pcap file in
the tests log directory e.g.,::
When using a unified log file `frr.log` one substitutes `frr` for the daemon
name in the ``--logd`` CLI option, e.g.,
.. code:: shell
# View `frr` log on all routers in test
sudo -E pytest some_test_suite --logd=frr
Spawning Debugging CLI, ``vtysh`` or Shells on Routers on Test Failure Spawning Debugging CLI, ``vtysh`` or Shells on Routers on Test Failure
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
@ -421,12 +478,30 @@ the help command from within a CLI launched on error:
test_bgp_multiview_topo1/test_bgp_routingTable> help test_bgp_multiview_topo1/test_bgp_routingTable> help
Commands: Basic Commands:
help :: this help cli :: open a secondary CLI window
sh [hosts] <shell-command> :: execute <shell-command> on <host> help :: this help
term [hosts] :: open shell terminals for hosts hosts :: list hosts
vtysh [hosts] :: open vtysh terminals for hosts quit :: quit the cli
[hosts] <vtysh-command> :: execute vtysh-command on hosts
HOST can be a host or one of the following:
- '*' for all hosts
- '.' for the parent munet
- a regex specified between '/' (e.g., '/rtr.*/')
New Window Commands:
logd HOST [HOST ...] DAEMON :: tail -f on the logfile of the given DAEMON for the given HOST[S]
pcap NETWORK :: 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.
stderr HOST [HOST ...] DAEMON :: tail -f on the stderr of the given DAEMON for the given HOST[S]
stdlog HOST [HOST ...] :: tail -f on the `frr.log` for the given HOST[S]
stdout HOST [HOST ...] DAEMON :: tail -f on the stdout of the given DAEMON for the given HOST[S]
term HOST [HOST ...] :: open terminal[s] (TMUX or XTerm) on HOST[S], * for all
vtysh ROUTER [ROUTER ...] ::
xterm HOST [HOST ...] :: open XTerm[s] on HOST[S], * for all
Inline Commands:
[ROUTER ...] COMMAND :: execute vtysh COMMAND on the router[s]
[HOST ...] sh <SHELL-COMMAND> :: execute <SHELL-COMMAND> on hosts
[HOST ...] shi <INTERACTIVE-COMMAND> :: execute <INTERACTIVE-COMMAND> on HOST[s]
test_bgp_multiview_topo1/test_bgp_routingTable> r1 show int br test_bgp_multiview_topo1/test_bgp_routingTable> r1 show int br
------ Host: r1 ------ ------ Host: r1 ------

View File

@ -1159,7 +1159,7 @@ def test_BGP_GR_TC_31_2_p1(request):
reset_config_on_routers(tgen) reset_config_on_routers(tgen)
logger.info( logger.info(
"[Phase 1] : Test Setup " "[Disable Mode]R1-----R2[Restart Mode] initialized " "[Phase 1] : Test Setup " "[Disable Mode]R1-----R2[Helper Mode] initialized "
) )
# Configure graceful-restart # Configure graceful-restart
@ -1251,7 +1251,7 @@ def test_BGP_GR_TC_31_2_p1(request):
tc_name, result tc_name, result
) )
logger.info("[Phase 2] : R2 Goes from Disable to Restart Mode ") logger.info("[Phase 2] : R1 Goes from Disable to Restart Mode ")
# Configure graceful-restart # Configure graceful-restart
input_dict = { input_dict = {
@ -1356,31 +1356,7 @@ def test_BGP_GR_TC_31_2_p1(request):
}, },
} }
# here the verify_graceful_restart fro the neighbor would be logger.info("[Phase 4] : R1 is UP and GR state is correct ")
# "NotReceived" as the latest GR config is not yet applied.
for addr_type in ADDR_TYPES:
result = verify_graceful_restart(
tgen, topo, addr_type, input_dict, dut="r1", peer="r2"
)
assert result is True, "Testcase {} : Failed \n Error {}".format(
tc_name, result
)
for addr_type in ADDR_TYPES:
# Verifying RIB routes
next_hop = next_hop_per_address_family(
tgen, dut, peer, addr_type, NEXT_HOP_IP_2
)
input_topo = {key: topo["routers"][key] for key in ["r2"]}
result = verify_rib(tgen, addr_type, dut, input_topo, next_hop, protocol)
assert result is True, "Testcase {} : Failed \n Error {}".format(
tc_name, result
)
logger.info("[Phase 6] : R1 is about to come up now ")
start_router_daemons(tgen, "r1", ["bgpd"])
logger.info("[Phase 4] : R1 is UP now, so time to collect GR stats ")
for addr_type in ADDR_TYPES: for addr_type in ADDR_TYPES:
result = verify_graceful_restart( result = verify_graceful_restart(

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
""" """
Topotest conftest.py file. Topotest conftest.py file.
""" """
@ -5,25 +6,24 @@ Topotest conftest.py file.
import glob import glob
import os import os
import pdb
import re import re
import resource import resource
import subprocess import subprocess
import sys import sys
import time import time
import pytest
import lib.fixtures import lib.fixtures
from lib import topolog import pytest
from lib.micronet_compat import Mininet from lib.micronet_compat import ConfigOptionsProxy, Mininet
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 json_cmp_result from lib.topotest import json_cmp_result
from munet import cli
from munet.base import Commander, proc_error from munet.base import Commander, proc_error
from munet.cleanup import cleanup_current, cleanup_previous from munet.cleanup import cleanup_current, cleanup_previous
from munet import cli from munet.testing.util import pause_test
from lib import topolog, topotest
def pytest_addoption(parser): def pytest_addoption(parser):
@ -61,12 +61,28 @@ def pytest_addoption(parser):
help="Comma-separated list of routers to spawn gdb on, or 'all'", help="Comma-separated list of routers to spawn gdb on, or 'all'",
) )
parser.addoption(
"--logd",
action="append",
metavar="DAEMON[,ROUTER[,...]",
help=(
"Tail-F of DAEMON log file. Specify routers in comma-separated list after "
"daemon to limit to a subset of routers"
),
)
parser.addoption( parser.addoption(
"--pause", "--pause",
action="store_true", action="store_true",
help="Pause after each test", help="Pause after each test",
) )
parser.addoption(
"--pause-at-end",
action="store_true",
help="Pause before taking munet down",
)
parser.addoption( parser.addoption(
"--pause-on-error", "--pause-on-error",
action="store_true", action="store_true",
@ -80,6 +96,13 @@ def pytest_addoption(parser):
help="Do not pause after (disables default when --shell or -vtysh given)", help="Do not pause after (disables default when --shell or -vtysh given)",
) )
parser.addoption(
"--pcap",
default="",
metavar="NET[,NET...]",
help="Comma-separated list of networks to capture packets on, or 'all'",
)
rundir_help = "directory for running in and log files" rundir_help = "directory for running in and log files"
parser.addini("rundir", rundir_help, default="/tmp/topotests") parser.addini("rundir", rundir_help, default="/tmp/topotests")
parser.addoption("--rundir", metavar="DIR", help=rundir_help) parser.addoption("--rundir", metavar="DIR", help=rundir_help)
@ -135,7 +158,7 @@ def pytest_addoption(parser):
def check_for_memleaks(): def check_for_memleaks():
assert topotest_extra_config["valgrind_memleaks"] assert topotest.g_pytest_config.option.valgrind_memleaks
leaks = [] leaks = []
tgen = get_topogen() # pylint: disable=redefined-outer-name tgen = get_topogen() # pylint: disable=redefined-outer-name
@ -179,16 +202,15 @@ def check_for_memleaks():
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")
def module_check_memtest(request): def module_check_memtest(request):
del request # disable unused warning
yield yield
if topotest_extra_config["valgrind_memleaks"]: if request.config.option.valgrind_memleaks:
if get_topogen() is not None: if get_topogen() is not None:
check_for_memleaks() check_for_memleaks()
def pytest_runtest_logstart(nodeid, location): def pytest_runtest_logstart(nodeid, location):
# location is (filename, lineno, testname) # location is (filename, lineno, testname)
topolog.logstart(nodeid, location, topotest_extra_config["rundir"]) topolog.logstart(nodeid, location, topotest.g_pytest_config.option.rundir)
def pytest_runtest_logfinish(nodeid, location): def pytest_runtest_logfinish(nodeid, location):
@ -199,10 +221,9 @@ def pytest_runtest_logfinish(nodeid, location):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item: pytest.Item) -> None: def pytest_runtest_call(item: pytest.Item) -> None:
"Hook the function that is called to execute the test." "Hook the function that is called to execute the test."
del item # disable unused warning
# For topology only run the CLI then exit # For topology only run the CLI then exit
if topotest_extra_config["topology_only"]: if item.config.option.topology_only:
get_topogen().cli() get_topogen().cli()
pytest.exit("exiting after --topology-only") pytest.exit("exiting after --topology-only")
@ -210,7 +231,7 @@ def pytest_runtest_call(item: pytest.Item) -> None:
yield yield
# Check for leaks if requested # Check for leaks if requested
if topotest_extra_config["valgrind_memleaks"]: if item.config.option.valgrind_memleaks:
check_for_memleaks() check_for_memleaks()
@ -233,6 +254,7 @@ def pytest_configure(config):
""" """
Assert that the environment is correctly configured, and get extra config. Assert that the environment is correctly configured, and get extra config.
""" """
topotest.g_pytest_config = ConfigOptionsProxy(config)
if config.getoption("--collect-only"): if config.getoption("--collect-only"):
return return
@ -254,11 +276,13 @@ def pytest_configure(config):
# Set some defaults for the pytest.ini [pytest] section # Set some defaults for the pytest.ini [pytest] section
# --------------------------------------------------- # ---------------------------------------------------
rundir = config.getoption("--rundir") rundir = config.option.rundir
if not rundir: if not rundir:
rundir = config.getini("rundir") rundir = config.getini("rundir")
if not rundir: if not rundir:
rundir = "/tmp/topotests" rundir = "/tmp/topotests"
config.option.rundir = rundir
if not config.getoption("--junitxml"): if not config.getoption("--junitxml"):
config.option.xmlpath = os.path.join(rundir, "topotests.xml") config.option.xmlpath = os.path.join(rundir, "topotests.xml")
xmlpath = config.option.xmlpath xmlpath = config.option.xmlpath
@ -271,8 +295,6 @@ def pytest_configure(config):
mv_path = commander.get_exec_path("mv") mv_path = commander.get_exec_path("mv")
commander.cmd_status([mv_path, xmlpath, xmlpath + suffix]) commander.cmd_status([mv_path, xmlpath, xmlpath + suffix])
topotest_extra_config["rundir"] = rundir
# Set the log_file (exec) to inside the rundir if not specified # Set the log_file (exec) to inside the rundir if not specified
if not config.getoption("--log-file") and not config.getini("log_file"): if not config.getoption("--log-file") and not config.getini("log_file"):
config.option.log_file = os.path.join(rundir, "exec.log") config.option.log_file = os.path.join(rundir, "exec.log")
@ -302,69 +324,19 @@ def pytest_configure(config):
elif b and not is_xdist and not have_windows: elif b and not is_xdist and not have_windows:
pytest.exit("{} use requires byobu/TMUX/SCREEN/XTerm".format(feature)) pytest.exit("{} use requires byobu/TMUX/SCREEN/XTerm".format(feature))
# --------------------------------------- #
# Record our options in global dictionary # Check for window capability if given options that require window
# --------------------------------------- #
assert_feature_windows(config.option.gdb_routers, "GDB")
assert_feature_windows(config.option.gdb_daemons, "GDB")
assert_feature_windows(config.option.cli_on_error, "--cli-on-error")
assert_feature_windows(config.option.shell, "--shell")
assert_feature_windows(config.option.shell_on_error, "--shell-on-error")
assert_feature_windows(config.option.vtysh, "--vtysh")
assert_feature_windows(config.option.vtysh_on_error, "--vtysh-on-error")
topotest_extra_config["rundir"] = rundir if config.option.topology_only and is_xdist:
asan_abort = config.getoption("--asan-abort")
topotest_extra_config["asan_abort"] = asan_abort
gdb_routers = config.getoption("--gdb-routers")
gdb_routers = gdb_routers.split(",") if gdb_routers else []
topotest_extra_config["gdb_routers"] = gdb_routers
gdb_daemons = config.getoption("--gdb-daemons")
gdb_daemons = gdb_daemons.split(",") if gdb_daemons else []
topotest_extra_config["gdb_daemons"] = gdb_daemons
assert_feature_windows(gdb_routers or gdb_daemons, "GDB")
gdb_breakpoints = config.getoption("--gdb-breakpoints")
gdb_breakpoints = gdb_breakpoints.split(",") if gdb_breakpoints else []
topotest_extra_config["gdb_breakpoints"] = gdb_breakpoints
cli_on_error = config.getoption("--cli-on-error")
topotest_extra_config["cli_on_error"] = cli_on_error
assert_feature_windows(cli_on_error, "--cli-on-error")
shell = config.getoption("--shell")
topotest_extra_config["shell"] = shell.split(",") if shell else []
assert_feature_windows(shell, "--shell")
strace = config.getoption("--strace-daemons")
topotest_extra_config["strace_daemons"] = strace.split(",") if strace else []
shell_on_error = config.getoption("--shell-on-error")
topotest_extra_config["shell_on_error"] = shell_on_error
assert_feature_windows(shell_on_error, "--shell-on-error")
topotest_extra_config["valgrind_extra"] = config.getoption("--valgrind-extra")
topotest_extra_config["valgrind_memleaks"] = config.getoption("--valgrind-memleaks")
vtysh = config.getoption("--vtysh")
topotest_extra_config["vtysh"] = vtysh.split(",") if vtysh else []
assert_feature_windows(vtysh, "--vtysh")
vtysh_on_error = config.getoption("--vtysh-on-error")
topotest_extra_config["vtysh_on_error"] = vtysh_on_error
assert_feature_windows(vtysh_on_error, "--vtysh-on-error")
pause_on_error = vtysh or shell or config.getoption("--pause-on-error")
if config.getoption("--no-pause-on-error"):
pause_on_error = False
topotest_extra_config["pause_on_error"] = pause_on_error
assert_feature_windows(pause_on_error, "--pause-on-error")
pause = config.getoption("--pause")
topotest_extra_config["pause"] = pause
assert_feature_windows(pause, "--pause")
topology_only = config.getoption("--topology-only")
if topology_only and is_xdist:
pytest.exit("Cannot use --topology-only with distributed test mode") pytest.exit("Cannot use --topology-only with distributed test mode")
topotest_extra_config["topology_only"] = topology_only
# Check environment now that we have config # Check environment now that we have config
if not diagnose_env(rundir): if not diagnose_env(rundir):
@ -430,10 +402,7 @@ def pytest_runtest_makereport(item, call):
# We want to pause, if requested, on any error not just test cases # We want to pause, if requested, on any error not just test cases
# (e.g., call.when == "setup") # (e.g., call.when == "setup")
if not pause: if not pause:
pause = ( pause = item.config.option.pause_on_error or item.config.option.pause
topotest_extra_config["pause_on_error"]
or topotest_extra_config["pause"]
)
# (topogen) Set topology error to avoid advancing in the test. # (topogen) Set topology error to avoid advancing in the test.
tgen = get_topogen() # pylint: disable=redefined-outer-name tgen = get_topogen() # pylint: disable=redefined-outer-name
@ -445,9 +414,9 @@ def pytest_runtest_makereport(item, call):
isatty = sys.stdout.isatty() isatty = sys.stdout.isatty()
error_cmd = None error_cmd = None
if error and topotest_extra_config["vtysh_on_error"]: if error and item.config.option.vtysh_on_error:
error_cmd = commander.get_exec_path(["vtysh"]) error_cmd = commander.get_exec_path(["vtysh"])
elif error and topotest_extra_config["shell_on_error"]: elif error and item.config.option.shell_on_error:
error_cmd = os.getenv("SHELL", commander.get_exec_path(["bash"])) error_cmd = os.getenv("SHELL", commander.get_exec_path(["bash"]))
if error_cmd: if error_cmd:
@ -499,7 +468,7 @@ def pytest_runtest_makereport(item, call):
if p.wait(): if p.wait():
logger.warning("xterm proc failed: %s:", proc_error(p, o, e)) logger.warning("xterm proc failed: %s:", proc_error(p, o, e))
if error and topotest_extra_config["cli_on_error"]: if error and item.config.option.cli_on_error:
# 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:
@ -507,23 +476,8 @@ def pytest_runtest_makereport(item, call):
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")
while pause and isatty: if pause and isatty:
try: pause_test()
user = raw_input(
'PAUSED, "cli" for CLI, "pdb" to debug, "Enter" to continue: '
)
except NameError:
user = input('PAUSED, "cli" for CLI, "pdb" to debug, "Enter" to continue: ')
user = user.strip()
if user == "cli":
cli.cli(Mininet.g_mnet_inst)
elif user == "pdb":
pdb.set_trace() # pylint: disable=forgotten-debug-statement
elif user:
print('Unrecognized input: "%s"' % user)
else:
break
# #

View File

@ -12,6 +12,49 @@ from munet import cli
from munet.base import BaseMunet, LinuxNamespace from munet.base import BaseMunet, LinuxNamespace
def cli_opt_list(option_list):
if not option_list:
return []
if isinstance(option_list, str):
return [x for x in option_list.split(",") if x]
return [x for x in option_list if x]
def name_in_cli_opt_str(name, option_list):
ol = cli_opt_list(option_list)
return name in ol or "all" in ol
class ConfigOptionsProxy:
def __init__(self, pytestconfig=None):
if isinstance(pytestconfig, ConfigOptionsProxy):
self.config = pytestconfig.config
else:
self.config = pytestconfig
self.option = self.config.option
def getoption(self, opt, defval=None):
if not self.config:
return defval
value = self.config.getoption(opt)
if value is None:
return defval
return value
def get_option(self, opt, defval=None):
return self.getoption(opt, defval)
def get_option_list(self, opt):
value = self.get_option(opt, "")
return cli_opt_list(value)
def name_in_option_list(self, name, opt):
optlist = self.get_option_list(opt)
return "all" in optlist or name in optlist
class Node(LinuxNamespace): class Node(LinuxNamespace):
"""Node (mininet compat).""" """Node (mininet compat)."""
@ -23,6 +66,8 @@ class Node(LinuxNamespace):
nkwargs["unet"] = kwargs["unet"] nkwargs["unet"] = kwargs["unet"]
if "private_mounts" in kwargs: if "private_mounts" in kwargs:
nkwargs["private_mounts"] = kwargs["private_mounts"] nkwargs["private_mounts"] = kwargs["private_mounts"]
if "logger" in kwargs:
nkwargs["logger"] = kwargs["logger"]
# This is expected by newer munet CLI code # This is expected by newer munet CLI code
self.config_dirname = "" self.config_dirname = ""
@ -120,7 +165,7 @@ class Mininet(BaseMunet):
g_mnet_inst = None g_mnet_inst = None
def __init__(self, rundir=None): def __init__(self, rundir=None, pytestconfig=None):
""" """
Create a Micronet. Create a Micronet.
""" """
@ -132,6 +177,8 @@ class Mininet(BaseMunet):
self.host_params = {} self.host_params = {}
self.prefix_len = 8 self.prefix_len = 8
self.cfgopt = ConfigOptionsProxy(pytestconfig)
# SNMPd used to require this, which was set int he mininet shell # 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 # 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 # do it if we don't have to. The snmpd.conf files have been updated
@ -268,12 +315,9 @@ ff02::2\tip6-allrouters
cli.add_cli_config(self, cdict) cli.add_cli_config(self, cdict)
# shellopt = ( shellopt = self.cfgopt.get_option_list("--shell")
# self.pytest_config.getoption("--shell") if self.pytest_config else None if "all" in shellopt or "." in shellopt:
# ) self.run_in_window("bash")
# 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 # This is expected by newer munet CLI code
self.config_dirname = "" self.config_dirname = ""
@ -335,6 +379,24 @@ ff02::2\tip6-allrouters
def start(self): def start(self):
"""Start the micronet topology.""" """Start the micronet topology."""
pcapopt = self.cfgopt.get_option_list("--pcap")
if "all" in pcapopt:
pcapopt = self.switches.keys()
for pcap in pcapopt:
if ":" in pcap:
host, intf = pcap.split(":")
pcap = f"{host}-{intf}"
host = self.hosts[host]
else:
host = self
intf = pcap
pcapfile = f"{self.rundir}/capture-{pcap}.pcap"
host.run_in_window(
f"tshark -s 9200 -i {intf} -P -w {pcapfile}",
background=True,
title=f"cap:{pcap}",
)
self.logger.debug("%s: Starting (no-op).", self) self.logger.debug("%s: Starting (no-op).", self)
def stop(self): def stop(self):

View File

@ -25,6 +25,7 @@ Basic usage instructions:
* After running stop Mininet with: tgen.stop_topology() * After running stop Mininet with: tgen.stop_topology()
""" """
import configparser
import grp import grp
import inspect import inspect
import json import json
@ -38,16 +39,11 @@ import subprocess
import sys import sys
from collections import OrderedDict from collections import OrderedDict
if sys.version_info[0] > 2:
import configparser
else:
import ConfigParser as configparser
import lib.topolog as topolog import lib.topolog as topolog
from lib.micronet import Commander from lib.micronet import Commander
from lib.micronet_compat import Mininet from lib.micronet_compat import Mininet
from lib.topolog import logger from lib.topolog import logger
from lib.topotest import g_extra_config from munet.testing.util import pause_test
from lib import topotest from lib import topotest
@ -193,7 +189,7 @@ class Topogen(object):
self._load_config() self._load_config()
# Create new log directory # Create new log directory
self.logdir = topotest.get_logs_path(g_extra_config["rundir"]) self.logdir = topotest.get_logs_path(topotest.g_pytest_config.option.rundir)
subprocess.check_call( subprocess.check_call(
"mkdir -p {0} && chmod 1777 {0}".format(self.logdir), shell=True "mkdir -p {0} && chmod 1777 {0}".format(self.logdir), shell=True
) )
@ -213,7 +209,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(rundir=self.logdir) self.net = Mininet(rundir=self.logdir, pytestconfig=topotest.g_pytest_config)
# Adjust the parent namespace # Adjust the parent namespace
topotest.fix_netns_limits(self.net) topotest.fix_netns_limits(self.net)
@ -455,7 +451,18 @@ class Topogen(object):
first is a simple kill with no sleep, the second will sleep if not first is a simple kill with no sleep, the second will sleep if not
killed and try with a different signal. killed and try with a different signal.
""" """
pause = bool(self.net.cfgopt.get_option("--pause-at-end"))
pause = pause or bool(self.net.cfgopt.get_option("--pause"))
if pause:
try:
pause_test("Before MUNET delete")
except KeyboardInterrupt:
print("^C...continuing")
except Exception as error:
self.logger.error("\n...continuing after error: %s", error)
logger.info("stopping topology: {}".format(self.modname)) logger.info("stopping topology: {}".format(self.modname))
errors = "" errors = ""
for gear in self.gears.values(): for gear in self.gears.values():
errors += gear.stop() errors += gear.stop()

View File

@ -9,13 +9,13 @@
# Network Device Education Foundation, Inc. ("NetDEF") # Network Device Education Foundation, Inc. ("NetDEF")
# #
import configparser
import difflib import difflib
import errno import errno
import functools import functools
import glob import glob
import json import json
import os import os
import pdb
import platform import platform
import re import re
import resource import resource
@ -24,22 +24,17 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
from collections.abc import Mapping
from copy import deepcopy from copy import deepcopy
import lib.topolog as topolog import lib.topolog as topolog
from lib.micronet_compat import Node
from lib.topolog import logger from lib.topolog import logger
from munet.base import Timeout
if sys.version_info[0] > 2:
import configparser
from collections.abc import Mapping
else:
import ConfigParser as configparser
from collections import Mapping
from lib import micronet from lib import micronet
from lib.micronet_compat import Node
g_extra_config = {} g_pytest_config = None
def get_logs_path(rundir): def get_logs_path(rundir):
@ -474,32 +469,6 @@ def int2dpid(dpid):
) )
def pid_exists(pid):
"Check whether pid exists in the current process table."
if pid <= 0:
return False
try:
os.waitpid(pid, os.WNOHANG)
except:
pass
try:
os.kill(pid, 0)
except OSError as err:
if err.errno == errno.ESRCH:
# ESRCH == No such process
return False
elif err.errno == errno.EPERM:
# EPERM clearly means there's a process to deny access to
return True
else:
# According to "man 2 kill" possible error values are
# (EINVAL, EPERM, ESRCH)
raise
else:
return True
def get_textdiff(text1, text2, title1="", title2="", **opts): def get_textdiff(text1, text2, title1="", title2="", **opts):
"Returns empty string if same or formatted diff" "Returns empty string if same or formatted diff"
@ -1086,7 +1055,7 @@ def checkAddressSanitizerError(output, router, component, logdir=""):
# No Address Sanitizer Error in Output. Now check for AddressSanitizer daemon file # No Address Sanitizer Error in Output. Now check for AddressSanitizer daemon file
if logdir: if logdir:
filepattern = logdir + "/" + router + "/" + component + ".asan.*" filepattern = logdir + "/" + router + ".asan." + component + ".*"
logger.debug( logger.debug(
"Log check for %s on %s, pattern %s\n" % (component, router, filepattern) "Log check for %s on %s, pattern %s\n" % (component, router, filepattern)
) )
@ -1303,7 +1272,8 @@ def fix_host_limits():
def setup_node_tmpdir(logdir, name): def setup_node_tmpdir(logdir, name):
# Cleanup old log, valgrind, and core files. # Cleanup old log, valgrind, and core files.
subprocess.check_call( subprocess.check_call(
"rm -rf {0}/{1}.valgrind.* {1}.*.asan {0}/{1}/".format(logdir, name), shell=True "rm -rf {0}/{1}.valgrind.* {0}/{1}.asan.* {0}/{1}/".format(logdir, name),
shell=True,
) )
# Setup the per node directory. # Setup the per node directory.
@ -1339,7 +1309,7 @@ class Router(Node):
# specified, then attempt to generate an unique logdir. # specified, then attempt to generate an unique logdir.
self.logdir = params.get("logdir") self.logdir = params.get("logdir")
if self.logdir is None: if self.logdir is None:
self.logdir = get_logs_path(g_extra_config["rundir"]) self.logdir = get_logs_path(g_pytest_config.getoption("--rundir"))
if not params.get("logger"): if not params.get("logger"):
# If logger is present topogen has already set this up # If logger is present topogen has already set this up
@ -1532,7 +1502,7 @@ class Router(Node):
) )
except Exception as ex: except Exception as ex:
logger.error("%s can't remove IPs %s", self, str(ex)) logger.error("%s can't remove IPs %s", self, str(ex))
# pdb.set_trace() # breakpoint()
# assert False, "can't remove IPs %s" % str(ex) # assert False, "can't remove IPs %s" % str(ex)
def checkCapability(self, daemon, param): def checkCapability(self, daemon, param):
@ -1598,10 +1568,7 @@ class Router(Node):
if (daemon == "zebra") and (self.daemons["mgmtd"] == 0): if (daemon == "zebra") and (self.daemons["mgmtd"] == 0):
# Add mgmtd with zebra - if it exists # Add mgmtd with zebra - if it exists
try: mgmtd_path = os.path.join(self.daemondir, "mgmtd")
mgmtd_path = os.path.join(self.daemondir, "mgmtd")
except:
pdb.set_trace()
if os.path.isfile(mgmtd_path): if os.path.isfile(mgmtd_path):
self.daemons["mgmtd"] = 1 self.daemons["mgmtd"] = 1
self.daemons_options["mgmtd"] = "" self.daemons_options["mgmtd"] = ""
@ -1609,11 +1576,7 @@ class Router(Node):
if (daemon == "zebra") and (self.daemons["staticd"] == 0): if (daemon == "zebra") and (self.daemons["staticd"] == 0):
# Add staticd with zebra - if it exists # Add staticd with zebra - if it exists
try: staticd_path = os.path.join(self.daemondir, "staticd")
staticd_path = os.path.join(self.daemondir, "staticd")
except:
pdb.set_trace()
if os.path.isfile(staticd_path): if os.path.isfile(staticd_path):
self.daemons["staticd"] = 1 self.daemons["staticd"] = 1
self.daemons_options["staticd"] = "" self.daemons_options["staticd"] = ""
@ -1688,8 +1651,7 @@ class Router(Node):
# used # used
self.cmd("echo 100000 > /proc/sys/net/mpls/platform_labels") self.cmd("echo 100000 > /proc/sys/net/mpls/platform_labels")
shell_routers = g_extra_config["shell"] if g_pytest_config.name_in_option_list(self.name, "--shell"):
if "all" in shell_routers or self.name in shell_routers:
self.run_in_window(os.getenv("SHELL", "bash"), title="sh-%s" % self.name) self.run_in_window(os.getenv("SHELL", "bash"), title="sh-%s" % self.name)
if self.daemons["eigrpd"] == 1: if self.daemons["eigrpd"] == 1:
@ -1706,8 +1668,7 @@ class Router(Node):
status = self.startRouterDaemons(tgen=tgen) status = self.startRouterDaemons(tgen=tgen)
vtysh_routers = g_extra_config["vtysh"] if g_pytest_config.name_in_option_list(self.name, "--vtysh"):
if "all" in vtysh_routers or self.name in vtysh_routers:
self.run_in_window("vtysh", title="vt-%s" % self.name) self.run_in_window("vtysh", title="vt-%s" % self.name)
if self.unified_config: if self.unified_config:
@ -1727,13 +1688,13 @@ class Router(Node):
def startRouterDaemons(self, daemons=None, tgen=None): def startRouterDaemons(self, daemons=None, tgen=None):
"Starts FRR daemons for this router." "Starts FRR daemons for this router."
asan_abort = g_extra_config["asan_abort"] asan_abort = bool(g_pytest_config.option.asan_abort)
gdb_breakpoints = g_extra_config["gdb_breakpoints"] gdb_breakpoints = g_pytest_config.get_option_list("--gdb-breakpoints")
gdb_daemons = g_extra_config["gdb_daemons"] gdb_daemons = g_pytest_config.get_option_list("--gdb-daemons")
gdb_routers = g_extra_config["gdb_routers"] gdb_routers = g_pytest_config.get_option_list("--gdb-routers")
valgrind_extra = g_extra_config["valgrind_extra"] valgrind_extra = bool(g_pytest_config.option.valgrind_extra)
valgrind_memleaks = g_extra_config["valgrind_memleaks"] valgrind_memleaks = bool(g_pytest_config.option.valgrind_memleaks)
strace_daemons = g_extra_config["strace_daemons"] strace_daemons = g_pytest_config.get_option_list("--strace-daemons")
# Get global bundle data # Get global bundle data
if not self.path_exists("/etc/frr/support_bundle_commands.conf"): if not self.path_exists("/etc/frr/support_bundle_commands.conf"):
@ -1757,11 +1718,21 @@ class Router(Node):
self.reportCores = True self.reportCores = True
# XXX: glue code forward ported from removed function. # XXX: glue code forward ported from removed function.
if self.version == None: if self.version is None:
self.version = self.cmd( self.version = self.cmd(
os.path.join(self.daemondir, "bgpd") + " -v" os.path.join(self.daemondir, "bgpd") + " -v"
).split()[2] ).split()[2]
logger.info("{}: running version: {}".format(self.name, self.version)) logger.info("{}: running version: {}".format(self.name, self.version))
logd_options = {}
for logd in g_pytest_config.get_option("--logd", []):
if "," in logd:
daemon, routers = logd.split(",", 1)
logd_options[daemon] = routers.split(",")
else:
daemon = logd
logd_options[daemon] = ["all"]
# If `daemons` was specified then some upper API called us with # If `daemons` was specified then some upper API called us with
# specific daemons, otherwise just use our own configuration. # specific daemons, otherwise just use our own configuration.
daemons_list = [] daemons_list = []
@ -1773,22 +1744,36 @@ class Router(Node):
if self.daemons[daemon] == 1: if self.daemons[daemon] == 1:
daemons_list.append(daemon) daemons_list.append(daemon)
tail_log_files = []
check_daemon_files = []
def start_daemon(daemon, extra_opts=None): def start_daemon(daemon, extra_opts=None):
daemon_opts = self.daemons_options.get(daemon, "") daemon_opts = self.daemons_options.get(daemon, "")
# get pid and vty filenames and remove the files
m = re.match(r"(.* |^)-n (\d+)( ?.*|$)", daemon_opts)
dfname = daemon if not m else "{}-{}".format(daemon, m.group(2))
runbase = "/var/run/{}/{}".format(self.routertype, dfname)
# If this is a new system bring-up remove the pid/vty files, otherwise
# do not since apparently presence of the pidfile impacts BGP GR
self.cmd_status("rm -f {0}.pid {0}.vty".format(runbase))
rediropt = " > {0}.out 2> {0}.err".format(daemon) rediropt = " > {0}.out 2> {0}.err".format(daemon)
if daemon == "snmpd": if daemon == "snmpd":
binary = "/usr/sbin/snmpd" binary = "/usr/sbin/snmpd"
cmdenv = "" cmdenv = ""
cmdopt = "{} -C -c /etc/frr/snmpd.conf -p ".format( cmdopt = "{} -C -c /etc/frr/snmpd.conf -p ".format(
daemon_opts daemon_opts
) + "/var/run/{}/snmpd.pid -x /etc/frr/agentx".format(self.routertype) ) + "{}.pid -x /etc/frr/agentx".format(runbase)
# check_daemon_files.append(runbase + ".pid")
else: else:
binary = os.path.join(self.daemondir, daemon) binary = os.path.join(self.daemondir, daemon)
check_daemon_files.extend([runbase + ".pid", runbase + ".vty"])
cmdenv = "ASAN_OPTIONS=" cmdenv = "ASAN_OPTIONS="
if asan_abort: if asan_abort:
cmdenv = "abort_on_error=1:" cmdenv += "abort_on_error=1:"
cmdenv += "log_path={0}/{1}.{2}.asan ".format( cmdenv += "log_path={0}/{1}.asan.{2} ".format(
self.logdir, self.name, daemon self.logdir, self.name, daemon
) )
@ -1811,9 +1796,16 @@ class Router(Node):
daemon, self.logdir, self.name daemon, self.logdir, self.name
) )
cmdopt = "{} --command-log-always --log file:{}.log --log-level debug".format( cmdopt = "{} --command-log-always ".format(daemon_opts)
daemon_opts, daemon cmdopt += "--log file:{}.log --log-level debug".format(daemon)
)
if daemon in logd_options:
logdopt = logd_options[daemon]
if "all" in logdopt or self.name in logdopt:
tail_log_files.append(
"{}/{}/{}.log".format(self.logdir, self.name, daemon)
)
if extra_opts: if extra_opts:
cmdopt += " " + extra_opts cmdopt += " " + extra_opts
@ -1840,8 +1832,6 @@ class Router(Node):
logger.info( logger.info(
"%s: %s %s launched in gdb window", self, self.routertype, daemon "%s: %s %s launched in gdb window", self, self.routertype, daemon
) )
# Need better check for daemons running.
time.sleep(5)
else: else:
if daemon != "snmpd": if daemon != "snmpd":
cmdopt += " -d " cmdopt += " -d "
@ -1900,16 +1890,50 @@ class Router(Node):
start_daemon(daemon) start_daemon(daemon)
# Check if daemons are running. # Check if daemons are running.
rundaemons = self.cmd("ls -1 /var/run/%s/*.pid" % self.routertype) wait_time = 30 if (gdb_routers or gdb_daemons) else 10
if re.search(r"No such file or directory", rundaemons): timeout = Timeout(wait_time)
return "Daemons are not running" for remaining in timeout:
if not check_daemon_files:
break
check = check_daemon_files[0]
if self.path_exists(check):
check_daemon_files.pop(0)
continue
self.logger.debug("Waiting {}s for {} to appear".format(remaining, check))
time.sleep(0.5)
if check_daemon_files:
assert False, "Timeout({}) waiting for {} to appear on {}".format(
wait_time, check_daemon_files[0], self.name
)
# Update the permissions on the log files # Update the permissions on the log files
self.cmd("chown frr:frr -R {}/{}".format(self.logdir, self.name)) self.cmd("chown frr:frr -R {}/{}".format(self.logdir, self.name))
self.cmd("chmod ug+rwX,o+r -R {}/{}".format(self.logdir, self.name)) self.cmd("chmod ug+rwX,o+r -R {}/{}".format(self.logdir, self.name))
if "frr" in logd_options:
logdopt = logd_options["frr"]
if "all" in logdopt or self.name in logdopt:
tail_log_files.append("{}/{}/frr.log".format(self.logdir, self.name))
for tailf in tail_log_files:
self.run_in_window("tail -f " + tailf, title=tailf, background=True)
return "" return ""
def pid_exists(self, pid):
if pid <= 0:
return False
try:
# If we are not using PID namespaces then we will be a parent of the pid,
# otherwise the init process of the PID namespace will have reaped the proc.
os.waitpid(pid, os.WNOHANG)
except Exception:
pass
rc, o, e = self.cmd_status("kill -0 " + str(pid), warn=False)
return rc == 0 or "No such process" not in e
def killRouterDaemons( def killRouterDaemons(
self, daemons, wait=True, assertOnError=True, minErrorVersion="5.1" self, daemons, wait=True, assertOnError=True, minErrorVersion="5.1"
): ):
@ -1929,15 +1953,15 @@ class Router(Node):
if re.search(r"%s" % daemon, d): if re.search(r"%s" % daemon, d):
daemonpidfile = d.rstrip() daemonpidfile = d.rstrip()
daemonpid = self.cmd("cat %s" % daemonpidfile).rstrip() daemonpid = self.cmd("cat %s" % daemonpidfile).rstrip()
if daemonpid.isdigit() and pid_exists(int(daemonpid)): if daemonpid.isdigit() and self.pid_exists(int(daemonpid)):
logger.debug( logger.debug(
"{}: killing {}".format( "{}: killing {}".format(
self.name, self.name,
os.path.basename(daemonpidfile.rsplit(".", 1)[0]), os.path.basename(daemonpidfile.rsplit(".", 1)[0]),
) )
) )
os.kill(int(daemonpid), signal.SIGKILL) self.cmd_status("kill -KILL {}".format(daemonpid))
if pid_exists(int(daemonpid)): if self.pid_exists(int(daemonpid)):
numRunning += 1 numRunning += 1
while wait and numRunning > 0: while wait and numRunning > 0:
sleep( sleep(
@ -1951,7 +1975,7 @@ class Router(Node):
for d in dmns[:-1]: for d in dmns[:-1]:
if re.search(r"%s" % daemon, d): if re.search(r"%s" % daemon, d):
daemonpid = self.cmd("cat %s" % d.rstrip()).rstrip() daemonpid = self.cmd("cat %s" % d.rstrip()).rstrip()
if daemonpid.isdigit() and pid_exists( if daemonpid.isdigit() and self.pid_exists(
int(daemonpid) int(daemonpid)
): ):
logger.info( logger.info(
@ -1962,8 +1986,10 @@ class Router(Node):
), ),
) )
) )
os.kill(int(daemonpid), signal.SIGKILL) self.cmd_status(
if daemonpid.isdigit() and not pid_exists( "kill -KILL {}".format(daemonpid)
)
if daemonpid.isdigit() and not self.pid_exists(
int(daemonpid) int(daemonpid)
): ):
numRunning -= 1 numRunning -= 1

View File

@ -24,8 +24,17 @@ import tempfile
import termios import termios
import tty import tty
from . import linux
from .config import list_to_dict_with_key try:
from . import linux
from .config import list_to_dict_with_key
except ImportError:
# We cannot use relative imports and still run this module directly as a script, and
# there are some use cases where we want to run this file as a script.
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
import linux
from config import list_to_dict_with_key
ENDMARKER = b"\x00END\x00" ENDMARKER = b"\x00END\x00"
@ -609,7 +618,16 @@ async def doline(
return True return True
await run_command( await run_command(
unet, outf, nline, execfmt, banner, hosts, toplevel, kinds, ns_only, interactive unet,
outf,
nline,
execfmt,
banner,
hosts,
toplevel,
kinds,
ns_only,
interactive,
) )
return True return True
@ -657,10 +675,10 @@ async def cli_client(sockpath, prompt="munet> "):
async def local_cli(unet, outf, prompt, histfile, background): async def local_cli(unet, outf, prompt, histfile, background):
"""Implement the user-side CLI for local munet.""" """Implement the user-side CLI for local munet."""
if unet: assert unet is not None
completer = Completer(unet) completer = Completer(unet)
readline.parse_and_bind("tab: complete") readline.parse_and_bind("tab: complete")
readline.set_completer(completer.complete) readline.set_completer(completer.complete)
print("\n--- Munet CLI Starting ---\n\n") print("\n--- Munet CLI Starting ---\n\n")
while True: while True:
@ -669,8 +687,6 @@ async def local_cli(unet, outf, prompt, histfile, background):
if line is None: if line is None:
return return
assert unet is not None
if not await doline(unet, line, outf, background): if not await doline(unet, line, outf, background):
return return
except KeyboardInterrupt: except KeyboardInterrupt:
@ -706,10 +722,19 @@ async def cli_client_connected(unet, background, reader, writer):
break break
line = line.decode("utf-8").strip() line = line.decode("utf-8").strip()
# def writef(x): class EncodingFile:
# writer.write(x.encode("utf-8")) """Wrap a writer to encode in utf-8."""
if not await doline(unet, line, writer, background, notty=True): def __init__(self, writer):
self.writer = writer
def write(self, x):
self.writer.write(x.encode("utf-8"))
def flush(self):
self.writer.flush()
if not await doline(unet, line, EncodingFile(writer), background, notty=True):
logging.debug("server closing cli connection") logging.debug("server closing cli connection")
return return

View File

@ -296,8 +296,12 @@ def be_init(new_pg, exec_args):
# No exec so we are the "child". # No exec so we are the "child".
new_process_group() new_process_group()
# Reap children as init process
vdebug("installing local handler for SIGCHLD")
signal.signal(signal.SIGCHLD, sig_sigchld)
while True: while True:
logging.info("parent: waiting to reap zombies") logging.info("init: waiting to reap zombies")
linux.pause() linux.pause()
# NOTREACHED # NOTREACHED