Merge pull request #13361 from LabNConsulting/chopps/munet-cfgopt-and-native

cfgopt in munet and native config support and example
This commit is contained in:
Donatas Abraitis 2023-04-24 13:50:36 +03:00 committed by GitHub
commit 766fcb6056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 215 additions and 36 deletions

View File

@ -5,6 +5,7 @@ Topotest conftest.py file.
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
import glob import glob
import logging
import os import os
import re import re
import resource import resource
@ -16,7 +17,7 @@ import lib.fixtures
import pytest import pytest
from lib.micronet_compat import ConfigOptionsProxy, 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 get_test_logdir, logger
from lib.topotest import json_cmp_result from lib.topotest import json_cmp_result
from munet import cli from munet import cli
from munet.base import Commander, proc_error from munet.base import Commander, proc_error
@ -25,6 +26,19 @@ from munet.testing.util import pause_test
from lib import topolog, topotest 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): def pytest_addoption(parser):
""" """

View File

@ -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"]

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1 @@
service integrated-vtysh-config

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1 @@
service integrated-vtysh-config

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1 @@
service integrated-vtysh-config

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
# SPDX-License-Identifier: GPL-2.0-or-later
#
# April 23 2023, Christian Hopps <chopps@labn.net>
#
# Copyright (c) 2023, LabN Consulting, L.L.C.
#
async def test_native_test(unet):
o = unet.hosts["r1"].cmd_nostatus("ip addr")
print(o)

View File

@ -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"]

View File

@ -59,7 +59,6 @@ class Node(LinuxNamespace):
"""Node (mininet compat).""" """Node (mininet compat)."""
def __init__(self, name, rundir=None, **kwargs): def __init__(self, name, rundir=None, **kwargs):
nkwargs = {} nkwargs = {}
if "unet" in kwargs: if "unet" in kwargs:
@ -177,8 +176,6 @@ 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

View File

@ -26,6 +26,7 @@ from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union
from . import config as munet_config
from . import linux from . import linux
@ -2493,7 +2494,15 @@ class Bridge(SharedNamespace, InterfaceMixin):
class BaseMunet(LinuxNamespace): class BaseMunet(LinuxNamespace):
"""Munet.""" """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.""" """Create a Munet."""
# logging.warning("BaseMunet: %s", name) # logging.warning("BaseMunet: %s", name)
@ -2562,6 +2571,8 @@ class BaseMunet(LinuxNamespace):
roothost = self.rootcmd roothost = self.rootcmd
self.cfgopt = munet_config.ConfigOptionsProxy(pytestconfig)
super().__init__( super().__init__(
name, mount=True, net=isolated, uts=isolated, pid=pid, unet=None, **kwargs name, mount=True, net=isolated, uts=isolated, pid=pid, unet=None, **kwargs
) )

View File

@ -11,8 +11,18 @@
class PytestConfig: class PytestConfig:
"""Pytest config duck-type-compatible object using argprase args.""" """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): def __init__(self, args):
self.args = vars(args) self.args = vars(args)
self.option = PytestConfig.Namespace(self.args)
def getoption(self, name, default=None, skip=False): def getoption(self, name, default=None, skip=False):
assert not skip assert not skip

View File

@ -156,3 +156,57 @@ def merge_kind_config(kconf, config):
if k not in new: if k not in new:
new[k] = config[k] new[k] = config[k]
return new 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

View File

@ -423,37 +423,37 @@ class NodeMixin:
stdout: file-like object with a ``name`` attribute, or a path to a file. 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. 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 return
outopt = self.unet.pytest_config.getoption("--stdout") outopt = self.unet.cfgopt.getoption("--stdout")
outopt = outopt if outopt is not None else "" outopt = outopt if outopt is not None else ""
if outopt == "all" or self.name in outopt.split(","): if outopt == "all" or self.name in outopt.split(","):
outname = stdout.name if hasattr(stdout, "name") else stdout outname = stdout.name if hasattr(stdout, "name") else stdout
self.run_in_window(f"tail -F {outname}", title=f"O:{self.name}") self.run_in_window(f"tail -F {outname}", title=f"O:{self.name}")
if stderr: if stderr:
erropt = self.unet.pytest_config.getoption("--stderr") erropt = self.unet.cfgopt.getoption("--stderr")
erropt = erropt if erropt is not None else "" erropt = erropt if erropt is not None else ""
if erropt == "all" or self.name in erropt.split(","): if erropt == "all" or self.name in erropt.split(","):
errname = stderr.name if hasattr(stderr, "name") else stderr errname = stderr.name if hasattr(stderr, "name") else stderr
self.run_in_window(f"tail -F {errname}", title=f"E:{self.name}") self.run_in_window(f"tail -F {errname}", title=f"E:{self.name}")
def pytest_hook_open_shell(self): def pytest_hook_open_shell(self):
if not self.unet or not self.unet.pytest_config: if not self.unet:
return return
gdbcmd = self.config.get("gdb-cmd") 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(",")) 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: if should_gdb and not use_emacs:
cmds = self.config.get("gdb-target-cmds", []) cmds = self.config.get("gdb-target-cmds", [])
for cmd in cmds: for cmd in cmds:
gdbcmd += f" '-ex={cmd}'" 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: for bp in bps:
gdbcmd += f" '-ex=b {bp}'" 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: for bp in bps:
cmd = f"br {bp}" cmd = f"br {bp}"
self.cmd_raises( self.cmd_raises(
@ -520,8 +520,8 @@ class NodeMixin:
) )
gdbcmd += f" '-ex={cmd}'" gdbcmd += f" '-ex={cmd}'"
shellopt = self.unet.pytest_config.getoption("--shell") shellopt = self.unet.cfgopt.getoption("--shell")
shellopt = shellopt if shellopt is not None else "" shellopt = shellopt if shellopt else ""
if shellopt == "all" or self.name in shellopt.split(","): if shellopt == "all" or self.name in shellopt.split(","):
self.run_in_window("bash") 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(f"ip -6 route add default via {switch.ip6_address}")
con.cmd_raises("ip link set lo up") 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") con.cmd_raises("mount -t debugfs none /sys/kernel/debug")
async def gather_coverage_data(self): async def gather_coverage_data(self):
@ -2402,7 +2402,6 @@ class Munet(BaseMunet):
self, self,
rundir=None, rundir=None,
config=None, config=None,
pytestconfig=None,
pid=True, pid=True,
logger=None, logger=None,
**kwargs, **kwargs,
@ -2433,8 +2432,6 @@ class Munet(BaseMunet):
self.config_pathname = "" self.config_pathname = ""
self.config_dirname = "" self.config_dirname = ""
self.pytest_config = pytestconfig
# Done in BaseMunet now # Done in BaseMunet now
# # We need some way to actually get back to the root namespace # # We need some way to actually get back to the root namespace
# if not self.isolated: # if not self.isolated:
@ -2573,10 +2570,8 @@ ff02::2\tip6-allrouters
# # Let's hide podman details # # Let's hide podman details
# self.tmpfs_mount("/var/lib/containers/storage/overlay-containers") # self.tmpfs_mount("/var/lib/containers/storage/overlay-containers")
shellopt = ( shellopt = self.cfgopt.getoption("--shell")
self.pytest_config.getoption("--shell") if self.pytest_config else None shellopt = shellopt if shellopt else ""
)
shellopt = shellopt if shellopt is not None else ""
if shellopt == "all" or "." in shellopt.split(","): if shellopt == "all" or "." in shellopt.split(","):
self.run_in_window("bash") self.run_in_window("bash")
@ -2795,10 +2790,7 @@ ff02::2\tip6-allrouters
x for x in hosts if hasattr(x, "has_ready_cmd") and x.has_ready_cmd() x for x in hosts if hasattr(x, "has_ready_cmd") and x.has_ready_cmd()
] ]
if not self.pytest_config: pcapopt = self.cfgopt.getoption("--pcap")
pcapopt = ""
else:
pcapopt = self.pytest_config.getoption("--pcap")
pcapopt = pcapopt if pcapopt else "" pcapopt = pcapopt if pcapopt else ""
if pcapopt == "all": if pcapopt == "all":
pcapopt = self.switches.keys() pcapopt = self.switches.keys()
@ -2868,7 +2860,7 @@ ff02::2\tip6-allrouters
self.logger.debug("%s: deleting.", self) self.logger.debug("%s: deleting.", self)
if self.pytest_config and self.pytest_config.getoption("--coverage"): if self.cfgopt.getoption("--coverage"):
nodes = ( nodes = (
x for x in self.hosts.values() if hasattr(x, "gather_coverage_data") 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: except Exception as error:
logging.warning("Error gathering coverage data: %s", error) logging.warning("Error gathering coverage data: %s", error)
if not self.pytest_config: pause = bool(self.cfgopt.getoption("--pause-at-end"))
pause = False pause = pause or bool(self.cfgopt.getoption("--pause"))
else:
pause = bool(self.pytest_config.getoption("--pause-at-end"))
pause = pause or bool(self.pytest_config.getoption("--pause"))
if pause: if pause:
try: try:
await async_pause_test("Before MUNET delete") await async_pause_test("Before MUNET delete")

View File

@ -235,7 +235,7 @@ def load_kinds(args, search=None):
if search is None: if search is None:
search = [cwd] search = [cwd]
with importlib.resources.path("munet", "kinds.yaml") as datapath: with importlib.resources.path("munet", "kinds.yaml") as datapath:
search.append(str(datapath.parent)) search.insert(0, str(datapath.parent))
configs = [] configs = []
if args_config: if args_config:

View File

@ -1,6 +1,8 @@
# Skip pytests example directory # Skip pytests example directory
[pytest] [pytest]
asyncio_mode = auto
# We always turn this on inside conftest.py, default shown # We always turn this on inside conftest.py, default shown
# addopts = --junitxml=<rundir>/topotests.xml # addopts = --junitxml=<rundir>/topotests.xml
@ -24,7 +26,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 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 # Directory to store test results and run logs in, default shown
# rundir = /tmp/topotests # rundir = /tmp/topotests