diff --git a/tests/topotests/conftest.py b/tests/topotests/conftest.py index 8e4d13df7d..31d796ccb8 100755 --- a/tests/topotests/conftest.py +++ b/tests/topotests/conftest.py @@ -5,6 +5,7 @@ Topotest conftest.py file. # pylint: disable=consider-using-f-string import glob +import logging import os import re import resource @@ -16,7 +17,7 @@ import lib.fixtures import pytest from lib.micronet_compat import ConfigOptionsProxy, Mininet from lib.topogen import diagnose_env, get_topogen -from lib.topolog import logger +from lib.topolog import get_test_logdir, logger from lib.topotest import json_cmp_result from munet import cli from munet.base import Commander, proc_error @@ -25,6 +26,19 @@ from munet.testing.util import pause_test from lib import topolog, topotest +try: + # Used by munet native tests + from munet.testing.fixtures import event_loop, unet # pylint: disable=all # noqa + + @pytest.fixture(scope="module") + def rundir_module(pytestconfig): + d = os.path.join(pytestconfig.option.rundir, get_test_logdir()) + logging.debug("rundir_module: test module rundir %s", d) + return d + +except (AttributeError, ImportError): + pass + def pytest_addoption(parser): """ diff --git a/tests/topotests/example_munet/munet.yaml b/tests/topotests/example_munet/munet.yaml new file mode 100644 index 0000000000..34e1470103 --- /dev/null +++ b/tests/topotests/example_munet/munet.yaml @@ -0,0 +1,17 @@ +version: 1 +topology: + ipv6-enable: true + networks-autonumber: true + networks: + - name: net1 + - name: net2 + nodes: + - name: r1 + kind: frr + connections: ["net1"] + - name: r2 + kind: frr + connections: ["net1", "net2"] + - name: r3 + kind: frr + connections: ["net2"] diff --git a/tests/topotests/example_munet/r1/daemons b/tests/topotests/example_munet/r1/daemons new file mode 100644 index 0000000000..a454c95923 --- /dev/null +++ b/tests/topotests/example_munet/r1/daemons @@ -0,0 +1,6 @@ +zebra=1 +staticd=1 +vtysh_enable=1 +watchfrr_enable=1 +zebra_options="-d -F traditional --log=file:/var/log/frr/zebra.log" +staticd_options="-d -F traditional --log=file:/var/log/frr/staticd.log" diff --git a/tests/topotests/example_munet/r1/frr.conf b/tests/topotests/example_munet/r1/frr.conf new file mode 100644 index 0000000000..468bda5e01 --- /dev/null +++ b/tests/topotests/example_munet/r1/frr.conf @@ -0,0 +1,7 @@ +log file /var/log/frr/frr.log +service integrated-vtysh-config + +interface eth0 + ip address 10.0.1.1/24 + +ip route 10.0.0.0/8 blackhole diff --git a/tests/topotests/example_munet/r1/vtysh.conf b/tests/topotests/example_munet/r1/vtysh.conf new file mode 100644 index 0000000000..f863f560f1 --- /dev/null +++ b/tests/topotests/example_munet/r1/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config \ No newline at end of file diff --git a/tests/topotests/example_munet/r2/daemons b/tests/topotests/example_munet/r2/daemons new file mode 100644 index 0000000000..a454c95923 --- /dev/null +++ b/tests/topotests/example_munet/r2/daemons @@ -0,0 +1,6 @@ +zebra=1 +staticd=1 +vtysh_enable=1 +watchfrr_enable=1 +zebra_options="-d -F traditional --log=file:/var/log/frr/zebra.log" +staticd_options="-d -F traditional --log=file:/var/log/frr/staticd.log" diff --git a/tests/topotests/example_munet/r2/frr.conf b/tests/topotests/example_munet/r2/frr.conf new file mode 100644 index 0000000000..77d9892485 --- /dev/null +++ b/tests/topotests/example_munet/r2/frr.conf @@ -0,0 +1,10 @@ +log file /var/log/frr/frr.log +service integrated-vtysh-config + +interface eth0 + ip address 10.0.1.2/24 + +interface eth1 + ip address 10.0.2.2/24 + +ip route 10.0.0.0/8 blackhole diff --git a/tests/topotests/example_munet/r2/vtysh.conf b/tests/topotests/example_munet/r2/vtysh.conf new file mode 100644 index 0000000000..f863f560f1 --- /dev/null +++ b/tests/topotests/example_munet/r2/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config \ No newline at end of file diff --git a/tests/topotests/example_munet/r3/daemons b/tests/topotests/example_munet/r3/daemons new file mode 100644 index 0000000000..a454c95923 --- /dev/null +++ b/tests/topotests/example_munet/r3/daemons @@ -0,0 +1,6 @@ +zebra=1 +staticd=1 +vtysh_enable=1 +watchfrr_enable=1 +zebra_options="-d -F traditional --log=file:/var/log/frr/zebra.log" +staticd_options="-d -F traditional --log=file:/var/log/frr/staticd.log" diff --git a/tests/topotests/example_munet/r3/frr.conf b/tests/topotests/example_munet/r3/frr.conf new file mode 100644 index 0000000000..e0839e6d8a --- /dev/null +++ b/tests/topotests/example_munet/r3/frr.conf @@ -0,0 +1,7 @@ +log file /var/log/frr/frr.log +service integrated-vtysh-config + +interface eth0 + ip address 10.0.2.3/24 + +ip route 10.0.0.0/8 blackhole diff --git a/tests/topotests/example_munet/r3/vtysh.conf b/tests/topotests/example_munet/r3/vtysh.conf new file mode 100644 index 0000000000..f863f560f1 --- /dev/null +++ b/tests/topotests/example_munet/r3/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config \ No newline at end of file diff --git a/tests/topotests/example_munet/test_munet.py b/tests/topotests/example_munet/test_munet.py new file mode 100644 index 0000000000..0d9599fa54 --- /dev/null +++ b/tests/topotests/example_munet/test_munet.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# SPDX-License-Identifier: GPL-2.0-or-later +# +# April 23 2023, Christian Hopps +# +# Copyright (c) 2023, LabN Consulting, L.L.C. +# +async def test_native_test(unet): + o = unet.hosts["r1"].cmd_nostatus("ip addr") + print(o) diff --git a/tests/topotests/kinds.yaml b/tests/topotests/kinds.yaml new file mode 100644 index 0000000000..127790ed07 --- /dev/null +++ b/tests/topotests/kinds.yaml @@ -0,0 +1,30 @@ +version: 1 +kinds: + - name: frr + cmd: | + chown frr:frr -R /var/run/frr + chown frr:frr -R /var/log/frr + /usr/lib/frr/frrinit.sh start + tail -F /var/log/frr/frr.log + cleanup-cmd: | + /usr/lib/frr/frrinit.sh stop + volumes: + - "./%NAME%:/etc/frr" + - "%RUNDIR%/var.log.frr:/var/log/frr" + - "%RUNDIR%/var.run.frr:/var/run/frr" + cap-add: + - SYS_ADMIN + - AUDIT_WRITE + merge: ["volumes"] +cli: + commands: + - name: "" + exec: "vtysh -c '{}'" + format: "[ROUTER ...] COMMAND" + help: "execute vtysh COMMAND on the router[s]" + kinds: ["frr"] + - name: "vtysh" + exec: "/usr/bin/vtysh" + format: "vtysh ROUTER [ROUTER ...]" + new-window: true + kinds: ["frr"] diff --git a/tests/topotests/lib/micronet_compat.py b/tests/topotests/lib/micronet_compat.py index ccb3e56b61..f49db805ba 100644 --- a/tests/topotests/lib/micronet_compat.py +++ b/tests/topotests/lib/micronet_compat.py @@ -59,7 +59,6 @@ class Node(LinuxNamespace): """Node (mininet compat).""" def __init__(self, name, rundir=None, **kwargs): - nkwargs = {} if "unet" in kwargs: @@ -177,8 +176,6 @@ class Mininet(BaseMunet): self.host_params = {} self.prefix_len = 8 - self.cfgopt = ConfigOptionsProxy(pytestconfig) - # 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 diff --git a/tests/topotests/munet/base.py b/tests/topotests/munet/base.py index 38da7faa01..eb4b088442 100644 --- a/tests/topotests/munet/base.py +++ b/tests/topotests/munet/base.py @@ -26,6 +26,7 @@ from collections import defaultdict from pathlib import Path from typing import Union +from . import config as munet_config from . import linux @@ -2493,7 +2494,15 @@ class Bridge(SharedNamespace, InterfaceMixin): class BaseMunet(LinuxNamespace): """Munet.""" - def __init__(self, name="munet", isolated=True, pid=True, rundir=None, **kwargs): + def __init__( + self, + name="munet", + isolated=True, + pid=True, + rundir=None, + pytestconfig=None, + **kwargs, + ): """Create a Munet.""" # logging.warning("BaseMunet: %s", name) @@ -2562,6 +2571,8 @@ class BaseMunet(LinuxNamespace): roothost = self.rootcmd + self.cfgopt = munet_config.ConfigOptionsProxy(pytestconfig) + super().__init__( name, mount=True, net=isolated, uts=isolated, pid=pid, unet=None, **kwargs ) diff --git a/tests/topotests/munet/compat.py b/tests/topotests/munet/compat.py index bf9092e53a..e82a7d5b77 100644 --- a/tests/topotests/munet/compat.py +++ b/tests/topotests/munet/compat.py @@ -11,8 +11,18 @@ class PytestConfig: """Pytest config duck-type-compatible object using argprase args.""" + class Namespace: + """A namespace defined by a dictionary of values.""" + + def __init__(self, args): + self.args = args + + def __getattr__(self, attr): + return self.args[attr] if attr in self.args else None + def __init__(self, args): self.args = vars(args) + self.option = PytestConfig.Namespace(self.args) def getoption(self, name, default=None, skip=False): assert not skip diff --git a/tests/topotests/munet/config.py b/tests/topotests/munet/config.py index b66e4d17da..1b02f2e8ff 100644 --- a/tests/topotests/munet/config.py +++ b/tests/topotests/munet/config.py @@ -156,3 +156,57 @@ def merge_kind_config(kconf, config): if k not in new: new[k] = config[k] return new + + +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: + """Proxy options object to fill in for any missing pytest config.""" + + class DefNoneObject: + """An object that returns None for any attribute access.""" + + def __getattr__(self, attr): + return None + + def __init__(self, pytestconfig=None): + if isinstance(pytestconfig, ConfigOptionsProxy): + self.config = pytestconfig.config + self.option = self.config.option + else: + self.config = pytestconfig + if self.config: + self.option = self.config.option + else: + self.option = ConfigOptionsProxy.DefNoneObject() + + def getoption(self, opt, defval=None): + if not self.config: + return defval + + try: + return self.config.getoption(opt, default=defval) + except ValueError: + return defval + + 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 diff --git a/tests/topotests/munet/native.py b/tests/topotests/munet/native.py index 5e79b04b8a..fecf709d1a 100644 --- a/tests/topotests/munet/native.py +++ b/tests/topotests/munet/native.py @@ -423,37 +423,37 @@ class NodeMixin: stdout: file-like object with a ``name`` attribute, or a path to a file. stderr: file-like object with a ``name`` attribute, or a path to a file. """ - if not self.unet or not self.unet.pytest_config: + if not self.unet: return - outopt = self.unet.pytest_config.getoption("--stdout") + outopt = self.unet.cfgopt.getoption("--stdout") outopt = outopt if outopt is not None else "" if outopt == "all" or self.name in outopt.split(","): outname = stdout.name if hasattr(stdout, "name") else stdout self.run_in_window(f"tail -F {outname}", title=f"O:{self.name}") if stderr: - erropt = self.unet.pytest_config.getoption("--stderr") + erropt = self.unet.cfgopt.getoption("--stderr") erropt = erropt if erropt is not None else "" if erropt == "all" or self.name in erropt.split(","): errname = stderr.name if hasattr(stderr, "name") else stderr self.run_in_window(f"tail -F {errname}", title=f"E:{self.name}") def pytest_hook_open_shell(self): - if not self.unet or not self.unet.pytest_config: + if not self.unet: return gdbcmd = self.config.get("gdb-cmd") - shellopt = self.unet.pytest_config.getoption("--gdb", "") + shellopt = self.unet.cfgopt.getoption("--gdb", "") should_gdb = gdbcmd and (shellopt == "all" or self.name in shellopt.split(",")) - use_emacs = self.unet.pytest_config.getoption("--gdb-use-emacs", False) + use_emacs = self.unet.cfgopt.getoption("--gdb-use-emacs", False) if should_gdb and not use_emacs: cmds = self.config.get("gdb-target-cmds", []) for cmd in cmds: gdbcmd += f" '-ex={cmd}'" - bps = self.unet.pytest_config.getoption("--gdb-breakpoints", "").split(",") + bps = self.unet.cfgopt.getoption("--gdb-breakpoints", "").split(",") for bp in bps: gdbcmd += f" '-ex=b {bp}'" @@ -497,7 +497,7 @@ class NodeMixin: ] ) - bps = self.unet.pytest_config.getoption("--gdb-breakpoints", "").split(",") + bps = self.unet.cfgopt.getoption("--gdb-breakpoints", "").split(",") for bp in bps: cmd = f"br {bp}" self.cmd_raises( @@ -520,8 +520,8 @@ class NodeMixin: ) gdbcmd += f" '-ex={cmd}'" - shellopt = self.unet.pytest_config.getoption("--shell") - shellopt = shellopt if shellopt is not None else "" + shellopt = self.unet.cfgopt.getoption("--shell") + shellopt = shellopt if shellopt else "" if shellopt == "all" or self.name in shellopt.split(","): self.run_in_window("bash") @@ -1968,7 +1968,7 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace): con.cmd_raises(f"ip -6 route add default via {switch.ip6_address}") con.cmd_raises("ip link set lo up") - if self.unet.pytest_config and self.unet.pytest_config.getoption("--coverage"): + if self.unet.cfgopt.getoption("--coverage"): con.cmd_raises("mount -t debugfs none /sys/kernel/debug") async def gather_coverage_data(self): @@ -2402,7 +2402,6 @@ class Munet(BaseMunet): self, rundir=None, config=None, - pytestconfig=None, pid=True, logger=None, **kwargs, @@ -2433,8 +2432,6 @@ class Munet(BaseMunet): self.config_pathname = "" self.config_dirname = "" - self.pytest_config = pytestconfig - # Done in BaseMunet now # # We need some way to actually get back to the root namespace # if not self.isolated: @@ -2573,10 +2570,8 @@ ff02::2\tip6-allrouters # # Let's hide podman details # self.tmpfs_mount("/var/lib/containers/storage/overlay-containers") - shellopt = ( - self.pytest_config.getoption("--shell") if self.pytest_config else None - ) - shellopt = shellopt if shellopt is not None else "" + shellopt = self.cfgopt.getoption("--shell") + shellopt = shellopt if shellopt else "" if shellopt == "all" or "." in shellopt.split(","): self.run_in_window("bash") @@ -2795,11 +2790,8 @@ ff02::2\tip6-allrouters x for x in hosts if hasattr(x, "has_ready_cmd") and x.has_ready_cmd() ] - if not self.pytest_config: - pcapopt = "" - else: - pcapopt = self.pytest_config.getoption("--pcap") - pcapopt = pcapopt if pcapopt else "" + pcapopt = self.cfgopt.getoption("--pcap") + pcapopt = pcapopt if pcapopt else "" if pcapopt == "all": pcapopt = self.switches.keys() if pcapopt: @@ -2868,7 +2860,7 @@ ff02::2\tip6-allrouters self.logger.debug("%s: deleting.", self) - if self.pytest_config and self.pytest_config.getoption("--coverage"): + if self.cfgopt.getoption("--coverage"): nodes = ( x for x in self.hosts.values() if hasattr(x, "gather_coverage_data") ) @@ -2877,11 +2869,8 @@ ff02::2\tip6-allrouters except Exception as error: logging.warning("Error gathering coverage data: %s", error) - if not self.pytest_config: - pause = False - else: - pause = bool(self.pytest_config.getoption("--pause-at-end")) - pause = pause or bool(self.pytest_config.getoption("--pause")) + pause = bool(self.cfgopt.getoption("--pause-at-end")) + pause = pause or bool(self.cfgopt.getoption("--pause")) if pause: try: await async_pause_test("Before MUNET delete") diff --git a/tests/topotests/munet/parser.py b/tests/topotests/munet/parser.py index f92138d7b8..4fc0c75a60 100644 --- a/tests/topotests/munet/parser.py +++ b/tests/topotests/munet/parser.py @@ -235,7 +235,7 @@ def load_kinds(args, search=None): if search is None: search = [cwd] with importlib.resources.path("munet", "kinds.yaml") as datapath: - search.append(str(datapath.parent)) + search.insert(0, str(datapath.parent)) configs = [] if args_config: diff --git a/tests/topotests/pytest.ini b/tests/topotests/pytest.ini index ccbc9d2a16..f779bf0a74 100644 --- a/tests/topotests/pytest.ini +++ b/tests/topotests/pytest.ini @@ -1,6 +1,8 @@ # Skip pytests example directory [pytest] +asyncio_mode = auto + # We always turn this on inside conftest.py, default shown # addopts = --junitxml=/topotests.xml @@ -24,7 +26,7 @@ log_file_date_format = %Y-%m-%d %H:%M:%S junit_logging = all junit_log_passing_tests = true -norecursedirs = .git example_test example_topojson_test lib munet docker +norecursedirs = .git example_munet example_test example_topojson_test lib munet docker # Directory to store test results and run logs in, default shown # rundir = /tmp/topotests