From nobody Fri Jan 13 21:25:31 2023 X-Original-To: dev-commits-src-branches@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4NtvbJ0Hr3z2qlB4; Fri, 13 Jan 2023 21:25:32 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4NtvbH6G4Jz434G; Fri, 13 Jan 2023 21:25:31 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1673645131; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=KqZzH1sms7tr5baE71GYrv8BEt4fIY95RVwiy1HUmDg=; b=cpmQ0JyDYP2nEVCYGIbkZeaZFGcLSBsqS7nBsmjK3RDqk0nhmrNAGYVUEBRwbCajzZkXbL ybIuWqJiHVTod+IKzg/acb4FBWQ9VHAs47/+SbCi4O6JvwrnJOJY07Cf0JSk5JyX2MUe6h Dvxg5JylYBkR6Hy+LHDmDAaT+9RxUvYp0rMCQoQhKExWeTwK2h8gk/4sbJI+Tdcy0NuV4f BXS/W8cDV8rLKwWQzhLnTcD/m6rt/hu8HhusqPc2F6MipLfK5Cglex0MFwFGFtHu2R5eWW Uou/hN4/w1acFCPSWoP4bmOvCNhEqdq8OGfC4H2czVEIU3BE4AwcCdWarTbovQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1673645131; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=KqZzH1sms7tr5baE71GYrv8BEt4fIY95RVwiy1HUmDg=; b=RV3lVy1Zqy8f/4WAr4qVOCoHmOFP/fFfMqRYS3atRk7GqjIFsIhX8NIDHruyxuC5u0k4eq HO5M/F/RyZQXU93SNF4hFWR0HALbpuu59+SPBJ2rHO2Non4ZsC6ydCx0HHCg033QXT8cOA g8kN4NfUnmfpKh4MOOM0Ai9DwryNdoKg4mCnkY2H+9QtyTphUa7OddVXX/ZKLq6IQoBjgD IlRNqutnkFeRGBoTf8AMOWZZqYZOOFOP2gNI5vhcOO+7GlAQWZpj+mYmVilUZvhspqSCyr tauk54sToF4V6sCmwL0shY/uBIPKJtv2a0PZcyprOOn1/wapzUdhkTiu13NTlw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1673645131; a=rsa-sha256; cv=none; b=wuMOQuJqNHlBSGxSfQSf9H/j7BDBm/eX10+88VRqiKrTxqaOjZuNOTJd8kBJxTkoANFfFH E/dCYWzep24wGfTZob8AvVrP9b3ncVcAz1NY4652xJwGhppsL15Fo3SUR4zHDloULgHqbE Vjq33wBSQF5g85hr4EfqnOuOY31FKUwCVeJESI+mKf5M5HeyzyElt2LwtoqQ/nYjmqfOOv Hg9rvGAO2bfpjOw7RnY7/89p5YOrQx7HQ7KSe4oq8PN/F/smH+uBoI6sVyTcz2iSvZeqEo +llcNLN8qs46Q6Iz1EP84ipPUkfqhL56irRmUmjUNunHUlp6ELth0VHVkHGlTA== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4NtvbH4wCVzNBb; Fri, 13 Jan 2023 21:25:31 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.16.1/8.16.1) with ESMTP id 30DLPVAP041967; Fri, 13 Jan 2023 21:25:31 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.16.1/8.16.1/Submit) id 30DLPVFq041966; Fri, 13 Jan 2023 21:25:31 GMT (envelope-from git) Date: Fri, 13 Jan 2023 21:25:31 GMT Message-Id: <202301132125.30DLPVFq041966@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org From: "Alexander V. Chernikov" Subject: git: bfa3b9fee7db - stable/13 - testing: add ability to specify multi-vnet topologies in the pytest framework. List-Id: Commits to the stable branches of the FreeBSD src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-branches List-Help: List-Post: List-Subscribe: List-Unsubscribe: Sender: owner-dev-commits-src-branches@freebsd.org X-BeenThere: dev-commits-src-branches@freebsd.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: melifaro X-Git-Repository: src X-Git-Refname: refs/heads/stable/13 X-Git-Reftype: branch X-Git-Commit: bfa3b9fee7db620a37647f58355ef87faad2530d Auto-Submitted: auto-generated X-ThisMailContainsUnwantedMimeParts: N The branch stable/13 has been updated by melifaro: URL: https://cgit.FreeBSD.org/src/commit/?id=bfa3b9fee7db620a37647f58355ef87faad2530d commit bfa3b9fee7db620a37647f58355ef87faad2530d Author: Alexander V. Chernikov AuthorDate: 2022-07-07 10:05:06 +0000 Commit: Alexander V. Chernikov CommitDate: 2023-01-13 21:24:11 +0000 testing: add ability to specify multi-vnet topologies in the pytest framework. Notable amount of tests related to the packet IO require two VNET jails for proper testing and avoiding side effects for the host system. Additionally, it is often required to run actions in the jails seme-sequentially - waiting for the listener initialisation can be an example of such dependency. This change extends pytest vnet framework to allow defining multi-vnet multi-epair topologies in declarative style, without any need to bother about jail or repair names. All jail creation/teardown, interface creation/teardown and address assignments are handled automatically. Example: TOPOLOGY = { "vnet1": {"ifaces": ["if1", "if2", "if3"]}, "vnet2": {"ifaces": ["if1", "if2", "if3"]}, "if1": {"prefixes6": [("2001:db8:a::1/64", "2001:db8:a::2/64")]}, "if2": {"prefixes6": [("2001:db8:b::1/64", "2001:db8:b::2/64")]}, "if3": {"prefixes6": [("2001:db8:c::1/64", "2001:db8:c::2/64")]}, } def vnet2_handler(self, vnet, obj_map, pipe): ss = VerboseSocketServer("::", self.DEFAULT_PORT) pipe.send("READY") def test_output6_base(self): self.wait_object(second_vnet.pipe) The definitions above will create 2 vnets ("jail_test_output6_base", "jail_test_output6_base_2"), 3 epairs, attached to both first and second jails, set up the IP addresses for each epair, spawn another process for vnet2_handler and pass control to vnet2_handler and test_output6_base. Both processes can pass objects between each other using pre-created pipes. Differential Revision: https://reviews.freebsd.org/D35708 (cherry picked from commit cfc9cf9baf474618daad9f5d5f7c74e66acafbd3) --- tests/atf_python/sys/net/tools.py | 40 ++++ tests/atf_python/sys/net/vnet.py | 457 +++++++++++++++++++++++++++++--------- 2 files changed, 397 insertions(+), 100 deletions(-) diff --git a/tests/atf_python/sys/net/tools.py b/tests/atf_python/sys/net/tools.py index 9f44872c2c37..c67941b414fc 100644 --- a/tests/atf_python/sys/net/tools.py +++ b/tests/atf_python/sys/net/tools.py @@ -12,6 +12,7 @@ from typing import Optional class ToolsHelper(object): NETSTAT_PATH = "/usr/bin/netstat" + IFCONFIG_PATH = "/sbin/ifconfig" @classmethod def get_output(cls, cmd: str, verbose=False) -> str: @@ -19,6 +20,23 @@ class ToolsHelper(object): print("run: '{}'".format(cmd)) return os.popen(cmd).read() + @classmethod + def print_output(cls, cmd: str, verbose=True): + if verbose: + print("======= {} =====".format(cmd)) + print(cls.get_output(cmd)) + if verbose: + print() + + @classmethod + def print_net_debug(cls): + cls.print_output("ifconfig") + cls.print_output("netstat -rnW") + + @classmethod + def set_sysctl(cls, oid, val): + cls.get_output("sysctl {}={}".format(oid, val)) + @classmethod def get_routes(cls, family: str, fibnum: int = 0): family_key = {"inet": "-4", "inet6": "-6"}.get(family) @@ -31,3 +49,25 @@ class ToolsHelper(object): return js[0]["rt-entry"] else: return [] + + @classmethod + def get_linklocals(cls): + ret = {} + ifname = None + ips = [] + for line in cls.get_output(cls.IFCONFIG_PATH).splitlines(): + if line[0].isalnum(): + if ifname: + ret[ifname] = ips + ips = [] + ifname = line.split(":")[0] + else: + words = line.split() + if words[0] == "inet6" and words[1].startswith("fe80"): + # inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2 + ip = words[1].split("%")[0] + scopeid = int(words[words.index("scopeid") + 1], 16) + ips.append((ip, scopeid)) + if ifname: + ret[ifname] = ips + return ret diff --git a/tests/atf_python/sys/net/vnet.py b/tests/atf_python/sys/net/vnet.py index 0957364f627c..663f7695a0cc 100644 --- a/tests/atf_python/sys/net/vnet.py +++ b/tests/atf_python/sys/net/vnet.py @@ -1,30 +1,52 @@ #!/usr/local/bin/python3 +import copy +import ipaddress import os import socket +import sys import time from ctypes import cdll from ctypes import get_errno from ctypes.util import find_library +from multiprocessing import Pipe +from multiprocessing import Process +from typing import Dict from typing import List +from typing import NamedTuple from typing import Optional +from atf_python.sys.net.tools import ToolsHelper -def run_cmd(cmd: str) -> str: + +def run_cmd(cmd: str, verbose=True) -> str: print("run: '{}'".format(cmd)) return os.popen(cmd).read() -class VnetInterface(object): - INTERFACES_FNAME = "created_interfaces.lst" +def convert_test_name(test_name: str) -> str: + """Convert test name to a string that can be used in the file/jail names""" + ret = "" + for char in test_name: + if char.isalnum() or char in ("_", "-"): + ret += char + elif char in ("["): + ret += "_" + return ret + +class VnetInterface(object): # defines from net/if_types.h IFT_LOOP = 0x18 IFT_ETHER = 0x06 - def __init__(self, iface_name: str): + def __init__(self, iface_alias: str, iface_name: str): self.name = iface_name + self.alias = iface_alias self.vnet_name = "" self.jailed = False + self.addr_map: Dict[str, Dict] = {"inet6": {}, "inet": {}} + self.prefixes4: List[List[str]] = [] + self.prefixes6: List[List[str]] = [] if iface_name.startswith("lo"): self.iftype = self.IFT_LOOP else: @@ -34,56 +56,67 @@ class VnetInterface(object): def ifindex(self): return socket.if_nametoindex(self.name) + @property + def first_ipv6(self): + d = self.addr_map["inet6"] + return d[next(iter(d))] + + @property + def first_ipv4(self): + d = self.addr_map["inet"] + return d[next(iter(d))] + def set_vnet(self, vnet_name: str): self.vnet_name = vnet_name def set_jailed(self, jailed: bool): self.jailed = jailed - def run_cmd(self, cmd): + def run_cmd( + self, + cmd, + verbose=False, + ): if self.vnet_name and not self.jailed: cmd = "jexec {} {}".format(self.vnet_name, cmd) - run_cmd(cmd) + return run_cmd(cmd, verbose) - @staticmethod - def file_append_line(line): - with open(VnetInterface.INTERFACES_FNAME, "a") as f: - f.write(line + "\n") + @classmethod + def setup_loopback(cls, vnet_name: str): + lo = VnetInterface("", "lo0") + lo.set_vnet(vnet_name) + lo.turn_up() @classmethod - def create_iface(cls, iface_name: str): + def create_iface(cls, alias_name: str, iface_name: str) -> List["VnetInterface"]: name = run_cmd("/sbin/ifconfig {} create".format(iface_name)).rstrip() if not name: raise Exception("Unable to create iface {}".format(iface_name)) - cls.file_append_line(name) + ret = [cls(alias_name, name)] if name.startswith("epair"): - cls.file_append_line(name[:-1] + "b") - return cls(name) + ret.append(cls(alias_name, name[:-1] + "b")) + return ret - @staticmethod - def cleanup_ifaces(): - try: - with open(VnetInterface.INTERFACES_FNAME, "r") as f: - for line in f: - run_cmd("/sbin/ifconfig {} destroy".format(line.strip())) - os.unlink(VnetInterface.INTERFACES_FNAME) - except Exception: - pass - - def setup_addr(self, addr: str): - if ":" in addr: + def setup_addr(self, _addr: str): + addr = ipaddress.ip_interface(_addr) + if addr.version == 6: family = "inet6" else: family = "inet" cmd = "/sbin/ifconfig {} {} {}".format(self.name, family, addr) self.run_cmd(cmd) + self.addr_map[family][str(addr)] = addr - def delete_addr(self, addr: str): - if ":" in addr: + def delete_addr(self, _addr: str): + addr = ipaddress.ip_address(_addr) + if addr.version == 6: + family = "inet6" cmd = "/sbin/ifconfig {} inet6 {} delete".format(self.name, addr) else: + family = "inet" cmd = "/sbin/ifconfig {} -alias {}".format(self.name, addr) self.run_cmd(cmd) + del self.addr_map[family][str(addr)] def turn_up(self): cmd = "/sbin/ifconfig {} up".format(self.name) @@ -93,50 +126,127 @@ class VnetInterface(object): cmd = "/usr/sbin/ndp -i {} -disabled".format(self.name) self.run_cmd(cmd) + def has_tentative(self) -> bool: + """True if an interface has some addresses in tenative state""" + cmd = "/sbin/ifconfig {} inet6".format(self.name) + out = self.run_cmd(cmd, verbose=False) + for line in out.splitlines(): + if "tentative" in line: + return True + return False -class VnetInstance(object): - JAILS_FNAME = "created_jails.lst" - def __init__(self, vnet_name: str, jid: int, ifaces: List[VnetInterface]): +class IfaceFactory(object): + INTERFACES_FNAME = "created_ifaces.lst" + + def __init__(self, test_name: str): + self.test_name = test_name + test_id = convert_test_name(test_name) + self.file_name = self.INTERFACES_FNAME + + def _register_iface(self, iface_name: str): + with open(self.file_name, "a") as f: + f.write(iface_name + "\n") + + def create_iface(self, alias_name: str, iface_name: str) -> List[VnetInterface]: + ifaces = VnetInterface.create_iface(alias_name, iface_name) + for iface in ifaces: + self._register_iface(iface.name) + return ifaces + + def cleanup(self): + try: + with open(self.file_name, "r") as f: + for line in f: + run_cmd("/sbin/ifconfig {} destroy".format(line.strip())) + os.unlink(self.INTERFACES_FNAME) + except Exception: + pass + + +class VnetInstance(object): + def __init__( + self, vnet_alias: str, vnet_name: str, jid: int, ifaces: List[VnetInterface] + ): self.name = vnet_name + self.alias = vnet_alias # reference in the test topology self.jid = jid self.ifaces = ifaces + self.iface_alias_map = {} # iface.alias: iface + self.iface_map = {} # iface.name: iface for iface in ifaces: iface.set_vnet(vnet_name) iface.set_jailed(True) + self.iface_alias_map[iface.alias] = iface + self.iface_map[iface.name] = iface + self.need_dad = False # Disable duplicate address detection by default + self.attached = False + self.pipe = None + self.subprocess = None def run_vnet_cmd(self, cmd): - if self.vnet_name: - cmd = "jexec {} {}".format(self.vnet_name, cmd) + if not self.attached: + cmd = "jexec {} {}".format(self.name, cmd) return run_cmd(cmd) - @staticmethod - def wait_interface(vnet_name: str, iface_name: str): - cmd = "jexec {} /sbin/ifconfig -l".format(vnet_name) - for i in range(50): - ifaces = run_cmd(cmd).strip().split(" ") - if iface_name in ifaces: - return True - time.sleep(0.1) - return False + def disable_dad(self): + self.run_vnet_cmd("/sbin/sysctl net.inet6.ip6.dad_count=0") + + def set_pipe(self, pipe): + self.pipe = pipe + + def set_subprocess(self, p): + self.subprocess = p @staticmethod - def file_append_line(line): - with open(VnetInstance.JAILS_FNAME, "a") as f: - f.write(line + "\n") + def attach_jid(jid: int): + _path: Optional[str] = find_library("c") + if _path is None: + raise Exception("libc not found") + path: str = _path + libc = cdll.LoadLibrary(path) + if libc.jail_attach(jid) != 0: + raise Exception("jail_attach() failed: errno {}".format(get_errno())) + + def attach(self): + self.attach_jid(self.jid) + self.attached = True + + +class VnetFactory(object): + JAILS_FNAME = "created_jails.lst" + + def __init__(self, test_name: str): + self.test_name = test_name + self.test_id = convert_test_name(test_name) + self.file_name = self.JAILS_FNAME + self._vnets: List[str] = [] + + def _register_vnet(self, vnet_name: str): + self._vnets.append(vnet_name) + with open(self.file_name, "a") as f: + f.write(vnet_name + "\n") @staticmethod - def cleanup_vnets(): - try: - with open(VnetInstance.JAILS_FNAME) as f: - for line in f: - run_cmd("/usr/sbin/jail -r {}".format(line.strip())) - os.unlink(VnetInstance.JAILS_FNAME) - except Exception: - pass + def _wait_interfaces(vnet_name: str, ifaces: List[str]) -> List[str]: + cmd = "jexec {} /sbin/ifconfig -l".format(vnet_name) + not_matched: List[str] = [] + for i in range(50): + vnet_ifaces = run_cmd(cmd).strip().split(" ") + not_matched = [] + for iface_name in ifaces: + if iface_name not in vnet_ifaces: + not_matched.append(iface_name) + if len(not_matched) == 0: + return [] + time.sleep(0.1) + return not_matched - @classmethod - def create_with_interfaces(cls, vnet_name: str, ifaces: List[VnetInterface]): + def create_vnet(self, vnet_alias: str, ifaces: List[VnetInterface]): + vnet_name = "jail_{}".format(self.test_id) + if self._vnets: + # add number to distinguish jails + vnet_name = "{}_{}".format(vnet_name, len(self._vnets) + 1) iface_cmds = " ".join(["vnet.interface={}".format(i.name) for i in ifaces]) cmd = "/usr/sbin/jail -i -c name={} persist vnet {}".format( vnet_name, iface_cmds @@ -145,59 +255,206 @@ class VnetInstance(object): jid = int(jid_str) if jid <= 0: raise Exception("Jail creation failed, output: {}".format(jid)) - cls.file_append_line(vnet_name) + self._register_vnet(vnet_name) - for iface in ifaces: - if cls.wait_interface(vnet_name, iface.name): - continue + # Run expedited version of routing + VnetInterface.setup_loopback(vnet_name) + + not_found = self._wait_interfaces(vnet_name, [i.name for i in ifaces]) + if not_found: raise Exception( - "Interface {} has not appeared in vnet {}".format(iface.name, vnet_name) + "Interfaces {} has not appeared in vnet {}".format(not_found, vnet_name) ) - return cls(vnet_name, jid, ifaces) - - @staticmethod - def attach_jid(jid: int): - _path: Optional[str] = find_library("c") - if _path is None: - raise Exception("libc not found") - path: str = _path - libc = cdll.LoadLibrary(path) - if libc.jail_attach(jid) != 0: - raise Exception("jail_attach() failed: errno {}".format(get_errno())) + return VnetInstance(vnet_alias, vnet_name, jid, ifaces) - def attach(self): - self.attach_jid(self.jid) + def cleanup(self): + try: + with open(self.file_name) as f: + for line in f: + jail_name = line.strip() + ToolsHelper.print_output( + "/usr/sbin/jexec {} ifconfig -l".format(jail_name) + ) + run_cmd("/usr/sbin/jail -r {}".format(line.strip())) + os.unlink(self.JAILS_FNAME) + except OSError: + pass -class SingleVnetTestTemplate(object): - num_epairs = 1 - IPV6_PREFIXES: List[str] = [] - IPV4_PREFIXES: List[str] = [] +class SingleInterfaceMap(NamedTuple): + ifaces: List[VnetInterface] + vnet_aliases: List[str] + + +class VnetTestTemplate(object): + TOPOLOGY = {} + + def _get_vnet_handler(self, vnet_alias: str): + handler_name = "{}_handler".format(vnet_alias) + return getattr(self, handler_name, None) + + def _setup_vnet(self, vnet: VnetInstance, obj_map: Dict, pipe): + """Base Handler to setup given VNET. + Can be run in a subprocess. If so, passes control to the special + vnetX_handler() after setting up interface addresses + """ + vnet.attach() + print("# setup_vnet({})".format(vnet.name)) + + topo = obj_map["topo_map"] + ipv6_ifaces = [] + # Disable DAD + if not vnet.need_dad: + vnet.disable_dad() + for iface in vnet.ifaces: + # check index of vnet within an interface + # as we have prefixes for both ends of the interface + iface_map = obj_map["iface_map"][iface.alias] + idx = iface_map.vnet_aliases.index(vnet.alias) + prefixes6 = topo[iface.alias].get("prefixes6", []) + prefixes4 = topo[iface.alias].get("prefixes4", []) + if prefixes6 or prefixes4: + ipv6_ifaces.append(iface) + iface.turn_up() + if prefixes6: + iface.enable_ipv6() + for prefix in prefixes6 + prefixes4: + iface.setup_addr(prefix[idx]) + for iface in ipv6_ifaces: + while iface.has_tentative(): + time.sleep(0.1) + + # Run actual handler + handler = self._get_vnet_handler(vnet.alias) + if handler: + # Do unbuffered stdout for children + # so the logs are present if the child hangs + sys.stdout.reconfigure(line_buffering=True) + handler(vnet, obj_map, pipe) + + def setup_topology(self, topo: Dict, test_name: str): + """Creates jails & interfaces for the provided topology""" + iface_map: Dict[str, SingleInterfaceMap] = {} + vnet_map = {} + iface_factory = IfaceFactory(test_name) + vnet_factory = VnetFactory(test_name) + for obj_name, obj_data in topo.items(): + if obj_name.startswith("if"): + epair_ifaces = iface_factory.create_iface(obj_name, "epair") + smap = SingleInterfaceMap(epair_ifaces, []) + iface_map[obj_name] = smap + for obj_name, obj_data in topo.items(): + if obj_name.startswith("vnet"): + vnet_ifaces = [] + for iface_alias in obj_data["ifaces"]: + # epair creates 2 interfaces, grab first _available_ + # and map it to the VNET being created + idx = len(iface_map[iface_alias].vnet_aliases) + iface_map[iface_alias].vnet_aliases.append(obj_name) + vnet_ifaces.append(iface_map[iface_alias].ifaces[idx]) + vnet = vnet_factory.create_vnet(obj_name, vnet_ifaces) + vnet_map[obj_name] = vnet + # Debug output + print("============= TEST TOPOLOGY =============") + for vnet_alias, vnet in vnet_map.items(): + print("# vnet {} -> {}".format(vnet.alias, vnet.name), end="") + handler = self._get_vnet_handler(vnet.alias) + if handler: + print(" handler: {}".format(handler.__name__), end="") + print() + for iface_alias, iface_data in iface_map.items(): + vnets = iface_data.vnet_aliases + ifaces: List[VnetInterface] = iface_data.ifaces + if len(vnets) == 1 and len(ifaces) == 2: + print( + "# iface {}: {}::{} -> main::{}".format( + iface_alias, vnets[0], ifaces[0].name, ifaces[1].name + ) + ) + elif len(vnets) == 2 and len(ifaces) == 2: + print( + "# iface {}: {}::{} -> {}::{}".format( + iface_alias, vnets[0], ifaces[0].name, vnets[1], ifaces[1].name + ) + ) + else: + print( + "# iface {}: ifaces: {} vnets: {}".format( + iface_alias, vnets, [i.name for i in ifaces] + ) + ) + print() + return {"iface_map": iface_map, "vnet_map": vnet_map, "topo_map": topo} def setup_method(self, method): - test_name = method.__name__ - vnet_name = "jail_{}".format(test_name) - ifaces = [] - for i in range(self.num_epairs): - ifaces.append(VnetInterface.create_iface("epair")) - self.vnet = VnetInstance.create_with_interfaces(vnet_name, ifaces) - self.vnet.attach() - for i, addr in enumerate(self.IPV6_PREFIXES): - if addr: - iface = self.vnet.ifaces[i] - iface.turn_up() - iface.enable_ipv6() - iface.setup_addr(addr) - for i, addr in enumerate(self.IPV4_PREFIXES): - if addr: - iface = self.vnet.ifaces[i] - iface.turn_up() - iface.setup_addr(addr) + """Sets up all the required topology and handlers for the given test""" + # 'test_ip6_output.py::TestIP6Output::test_output6_pktinfo[ipandif] (setup)' + test_id = os.environ.get("PYTEST_CURRENT_TEST").split(" ")[0] + test_name = test_id.split("::")[-1] + topology = self.TOPOLOGY + # First, setup kernel objects - interfaces & vnets + obj_map = self.setup_topology(topology, test_name) + main_vnet = None # one without subprocess handler + for vnet_alias, vnet in obj_map["vnet_map"].items(): + if self._get_vnet_handler(vnet_alias): + # Need subprocess to run + parent_pipe, child_pipe = Pipe() + p = Process( + target=self._setup_vnet, + args=( + vnet, + obj_map, + child_pipe, + ), + ) + vnet.set_pipe(parent_pipe) + vnet.set_subprocess(p) + p.start() + else: + if main_vnet is not None: + raise Exception("there can be only 1 VNET w/o handler") + main_vnet = vnet + # Main vnet needs to be the last, so all the other subprocesses + # are started & their pipe handles collected + self.vnet = main_vnet + self._setup_vnet(main_vnet, obj_map, None) + # Save state for the main handler + self.iface_map = obj_map["iface_map"] + self.vnet_map = obj_map["vnet_map"] + + def cleanup(self, test_id: str): + # pytest test id: file::class::test_name + test_name = test_id.split("::")[-1] - def cleanup(self, nodeid: str): print("==== vnet cleanup ===") - VnetInstance.cleanup_vnets() - VnetInterface.cleanup_ifaces() + print("# test_name: '{}'".format(test_name)) + VnetFactory(test_name).cleanup() + IfaceFactory(test_name).cleanup() + + def wait_object(self, pipe, timeout=5): + if pipe.poll(timeout): + return pipe.recv() + raise TimeoutError + + @property + def curvnet(self): + pass - def run_cmd(self, cmd: str) -> str: - return os.popen(cmd).read() + +class SingleVnetTestTemplate(VnetTestTemplate): + IPV6_PREFIXES: List[str] = [] + IPV4_PREFIXES: List[str] = [] + + def setup_method(self, method): + topology = copy.deepcopy( + { + "vnet1": {"ifaces": ["if1"]}, + "if1": {"prefixes4": [], "prefixes6": []}, + } + ) + for prefix in self.IPV6_PREFIXES: + topology["if1"]["prefixes6"].append((prefix,)) + for prefix in self.IPV4_PREFIXES: + topology["if1"]["prefixes4"].append((prefix,)) + self.TOPOLOGY = topology + super().setup_method(method)