-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tests: Implement test suit with common setups, based on BCC
Check the documentation for more details: https://phabricator.vyos.net/T4180 Signed-off-by: Volodymyr Huti <[email protected]>
- Loading branch information
1 parent
d15652d
commit e247740
Showing
10 changed files
with
1,615 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
#!/usr/bin/env python | ||
# | ||
# SPDX-License-Identifier: ISC | ||
# Copyright (c) 2023 VyOS Inc. | ||
# Volodymyr Huti <[email protected]> | ||
# | ||
|
||
import os | ||
import sys | ||
import json | ||
import pytest | ||
|
||
from bcc import BPF, DEBUG_PREPROCESSOR, DEBUG_SOURCE, DEBUG_BPF, DEBUG_BTF | ||
from lib.topolog import logger | ||
from lib.common_config import ( | ||
start_router_daemons, | ||
kill_router_daemons, | ||
) | ||
|
||
# from pyroute2.netns import pushns, popns | ||
from .ns import pushns, popns | ||
|
||
DEV_DEBUG = True | ||
os.environ["PYTHONBREAKPOINT"] = "pudb.set_trace" | ||
|
||
CWD = os.path.dirname(os.path.realpath(__file__)) | ||
sys.path.append(os.path.join(CWD, "../")) | ||
sys.path.append(os.path.join(CWD, "../lib/")) | ||
|
||
from ctypes import Structure, c_int, c_uint, c_ubyte | ||
from enum import Enum | ||
|
||
|
||
class BgpPolicy(Enum): | ||
NONE = c_int(0) | ||
Dst = c_int(1) | ||
Src = c_int(2) | ||
|
||
|
||
class XdpMode(str, Enum): | ||
META = "MARK_META" | ||
SKB = "MARK_SKB" | ||
|
||
|
||
class KeyV4(Structure): | ||
_fields_ = [("prefixlen", c_uint), ("data", c_ubyte * 4)] | ||
|
||
|
||
def router_attach_xdp(rnode, iface): | ||
""" | ||
- swap netns to rnode, | ||
- attach `xdp_qppb` to `iface` | ||
- switch back to root ns | ||
""" | ||
ns = "/proc/%d/ns/net" % rnode.net.pid | ||
qppb_fn = rnode.bpf.funcs[b"xdp_qppb"] | ||
|
||
pushns(ns) | ||
logger.debug("Attach XDP handler '{}'\nNetNS --> {})".format(iface, ns)) | ||
rnode.bpf.attach_xdp(iface, qppb_fn, BPF.XDP_FLAGS_DRV_MODE) | ||
popns() | ||
|
||
|
||
def router_remove_xdp(rnode, iface): | ||
pushns("/proc/%d/ns/net" % rnode.net.pid) | ||
logger.debug("Removing XDP handler for {}:{}".format(rnode.name, iface)) | ||
rnode.bpf.remove_xdp(iface) | ||
popns() | ||
|
||
|
||
def load_qppb_plugin(tgen, rnode, mode=XdpMode.SKB, debug_on=True): | ||
""" | ||
Initialize rnode XDP hooks and BPF mapping handlers | ||
- compile xdp handlers from `xdp_qppb.c` in specified `mode` | ||
- load `xdp_qppb` and `xdp_tc_mark` hooks | ||
- restart router with QPPB plugin | ||
Parameters | ||
---------- | ||
* `tgen`: topogen object | ||
* `rnode`: router object | ||
* `mode`: xdp processing mode required | ||
* `debug_on`: enable debug logs for bpf compilation / xdp handlers | ||
Usage | ||
--------- | ||
load_qppb_plugin(tgen, r1, mode=XdpMode.META) | ||
Returns -> None (XXX) | ||
""" | ||
debug_flags = DEBUG_BPF | DEBUG_PREPROCESSOR | DEBUG_SOURCE | DEBUG_BTF | ||
debug = debug_flags if debug_on else 0 | ||
src_file = CWD + "/xdp_qppb.c" | ||
bpf_flags = [ | ||
'-DBPF_PIN_DIR="{}"'.format(rnode.bpfdir), | ||
'-DMODE_STR="{}"'.format(mode), | ||
"-D{}".format(mode.value), | ||
"-DRESPECT_TOS", | ||
"-w", | ||
] | ||
if debug_on: | ||
bpf_flags.append("-DLOG_QPPB") | ||
bpf_flags.append("-DLOG_TC") | ||
|
||
try: | ||
logger.info("Preparing the XDP src: " + src_file) | ||
b = BPF(src_file=src_file.encode(), cflags=bpf_flags, debug=debug) | ||
|
||
logger.info("Loading XDP hooks -- xdp_qppb, xdp_tc_mark") | ||
b.load_func(b"xdp_qppb", BPF.XDP) | ||
b.load_func(b"xdp_tc_mark", BPF.SCHED_CLS) | ||
rnode.bpf = b | ||
except Exception as e: | ||
pytest.skip("Failed to configure XDP environment -- \n" + str(e)) | ||
|
||
qppb_module = "-M vyos_qppb:" + rnode.bpfdir | ||
logger.info( | ||
"Restart {}, XDP hooks loading...\nPlugin args:: {}".format( | ||
rnode.name, qppb_module | ||
) | ||
) | ||
kill_router_daemons(tgen, rnode.name, ["bgpd"]) | ||
start_router_daemons(tgen, rnode.name, ["bgpd"], {"bgpd": qppb_module}) | ||
|
||
|
||
def tc_bpf_filter(rnode, ifid): | ||
"Attach tc bpf filter, depends on pyroute2 package" | ||
import pyroute2 | ||
from pyroute2.netns import pushns, popns | ||
|
||
tc_fn = rnode.bpf.funcs[b"xdp_tc_mark"] | ||
rnode_ns = "/proc/{}/ns/net".format(rnode.net.pid) | ||
|
||
logger.debug("Attach TC-BPF handler '{}'\nNetNS --> {})".format(ifid, rnode_ns)) | ||
# ip.tc("add", "clsact", ifid, "1:") | ||
pushns(rnode_ns) | ||
ip = pyroute2.IPRoute() | ||
ip.tc( | ||
"add-filter", | ||
"bpf", | ||
ifid, | ||
20, # XXX: | ||
fd=tc_fn.fd, | ||
name=tc_fn.name, | ||
parent=0x10000, | ||
classid=0x10030, # XXX: should be default? default is taken from htb | ||
direct_action=True, | ||
) | ||
popns() | ||
|
||
|
||
def assert_bw(out, bw_target, tolerance, time=10): | ||
"Assert that connection matches BW in Mbits +- %tolerance" | ||
_min = bw_target * (1 - tolerance) | ||
_max = bw_target * (1 + tolerance) | ||
half_samples = time / 2 | ||
data = json.loads(out) | ||
bws = [] | ||
|
||
for sample in data["intervals"]: | ||
bits = int(sample["sum"]["bits_per_second"]) | ||
mbits = bits / 1024 / 1024 | ||
bw = mbits / 8 | ||
logger.debug("BW sample [{} <= {} <= {}]".format(_min, bw, _max)) | ||
if _min <= bw <= _max: | ||
bws.append(bw) | ||
|
||
_len = len(bws) | ||
assert ( | ||
_len >= half_samples | ||
), "Only {} samples are within targeted BW [{}:{}%]".format( | ||
_len, bw_target, tolerance * 100 | ||
) | ||
|
||
|
||
def bpf_print_trace(b): | ||
"XXX: Call this from debugger, to avoid blocking / IO collissions" | ||
logger.info("=" * 40) | ||
logger.debug("Dump bpf log buffer:\n") | ||
line = b.trace_readline(nonblocking=True) | ||
while line: | ||
logger.debug(line) | ||
line = b.trace_readline(nonblocking=True) | ||
|
||
|
||
def tc(rnode, cmd): | ||
logger.debug("TC cmd: " + cmd) | ||
return rnode.cmd_raises("tc " + cmd) | ||
|
||
|
||
def tc_check(host, cmds): | ||
tcoutputs = [tc(host, cmd) for cmd in cmds] | ||
for output in tcoutputs: | ||
if output != "": | ||
logger.debug("TC: " + output) | ||
|
||
|
||
def tc_log_stats(host, iface): | ||
if not DEV_DEBUG: | ||
return | ||
tc_flags = "-g -s -d -p -col" | ||
tc_check( | ||
host, | ||
[ | ||
tc_flags + " filter ls dev " + iface, | ||
tc_flags + " class ls dev " + iface, | ||
tc_flags + " qdisc ls ", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
BGP routing table entry for 10.61.0.1/32, version XX | ||
Paths: (1 available, best #1, table default) | ||
Advertised to non peer-group peers: | ||
10.0.0.2 | ||
10 60 | ||
10.0.0.2 from 10.0.0.2 (1.0.2.17) | ||
Origin incomplete, valid, external, best (First path received) | ||
Community: 60:1 | ||
QOS: Precedence af11 (10) | ||
Last update: XXXX |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
#!/usr/bin/env python | ||
# | ||
# SPDX-License-Identifier: ISC | ||
# Copyright (c) 2023 VyOS Inc. | ||
# Volodymyr Huti <[email protected]> | ||
# | ||
|
||
import platform | ||
import ctypes | ||
import os | ||
import io | ||
|
||
""" | ||
XXX: | ||
The `xdp_attach` method expects target dev to be visible (to be in local netns). | ||
It is possible to run some helper via `rnode.popen("bcc_script.py", ...)`, | ||
but we would still need to initialized a separate BPF handler in the root ns. | ||
The functionality is provided by `pyroute2` package - `setns/pushns/popns` | ||
Either we can specify pyroute2 as an required dependencies, or just | ||
adopt the relevant methods, avoiding any extra packages. | ||
/usr/lib/python3/dist-packages/pyroute2/netns/__init__.py | ||
/usr/lib/python3/dist-packages/pyroute2/netns/nslink.py | ||
from pyroute2 import IPRoute, NetNS, IPDB, NSPopen | ||
""" | ||
|
||
file = io.IOBase | ||
CLONE_NEWNET = 0x40000000 | ||
NETNS_RUN_DIR = "/var/run/netns" | ||
__saved_ns = [] | ||
|
||
machine = platform.machine() | ||
arch = platform.architecture()[0] | ||
__NR = { | ||
"x86_": {"64bit": 308}, | ||
"i386": {"32bit": 346}, | ||
"i686": {"32bit": 346}, | ||
"mips": {"32bit": 4344, "64bit": 5303}, | ||
"armv": {"32bit": 375}, | ||
"aarc": {"32bit": 375, "64bit": 268}, # FIXME: EABI vs. OABI? | ||
"ppc6": {"64bit": 350}, | ||
"s390": {"64bit": 339}, | ||
} | ||
__NR_setns = __NR.get(machine[:4], {}).get(arch, 308) | ||
|
||
|
||
def setns(netns, flags=os.O_CREAT): | ||
""" | ||
Set netns for the current process. | ||
The flags semantics is the same as for the `open(2)` | ||
call: | ||
- O_CREAT -- create netns, if doesn't exist | ||
- O_CREAT | O_EXCL -- create only if doesn't exist | ||
Note that "main" netns has no name. But you can access it with:: | ||
setns('foo') # move to netns foo | ||
setns('/proc/1/ns/net') # go back to default netns | ||
See also `pushns()`/`popns()`/`dropns()` | ||
Changed in 0.5.1: the routine closes the ns fd if it's | ||
not provided via arguments. | ||
""" | ||
newfd = False | ||
basestring = (str, bytes) | ||
lib_name = "libc.so.6" | ||
# lib_name = ctypes.util.find_library('c') <- bugged for me | ||
libc = ctypes.CDLL(lib_name, use_errno=True) | ||
if isinstance(netns, basestring): | ||
netnspath = _get_netnspath(netns) | ||
if os.path.basename(netns) in listnetns(os.path.dirname(netns)): | ||
if flags & (os.O_CREAT | os.O_EXCL) == (os.O_CREAT | os.O_EXCL): | ||
raise OSError(errno.EEXIST, "netns exists", netns) | ||
else: | ||
if flags & os.O_CREAT: | ||
create(netns, libc=libc) | ||
nsfd = os.open(netnspath, os.O_RDONLY) | ||
newfd = True | ||
elif isinstance(netns, file): | ||
nsfd = netns.fileno() | ||
elif isinstance(netns, int): | ||
nsfd = netns | ||
else: | ||
raise RuntimeError("netns should be a string or an open fd") | ||
error = libc.syscall(__NR_setns, nsfd, CLONE_NEWNET) | ||
if newfd: | ||
os.close(nsfd) | ||
if error != 0: | ||
raise OSError(ctypes.get_errno(), "failed to open netns", netns) | ||
|
||
|
||
def _get_netnspath(name): | ||
netnspath = name | ||
dirname = os.path.dirname(name) | ||
if not dirname: | ||
netnspath = "%s/%s" % (NETNS_RUN_DIR, name) | ||
if hasattr(netnspath, "encode"): | ||
netnspath = netnspath.encode("ascii") | ||
return netnspath | ||
|
||
|
||
def listnetns(nspath=None): | ||
""" | ||
List available network namespaces. | ||
""" | ||
if nspath: | ||
nsdir = nspath | ||
else: | ||
nsdir = NETNS_RUN_DIR | ||
try: | ||
return os.listdir(nsdir) | ||
except OSError as e: | ||
if e.errno == errno.ENOENT: | ||
return [] | ||
else: | ||
raise | ||
|
||
|
||
def pushns(newns=None): | ||
""" | ||
Save the current netns in order to return to it later. If newns is | ||
specified, change to it:: | ||
# --> the script in the "main" netns | ||
netns.pushns("test") | ||
# --> changed to "test", the "main" is saved | ||
netns.popns() | ||
# --> "test" is dropped, back to the "main" | ||
""" | ||
global __saved_ns | ||
__saved_ns.append(os.open("/proc/self/ns/net", os.O_RDONLY)) | ||
if newns is not None: | ||
setns(newns) | ||
|
||
|
||
def popns(): | ||
""" | ||
Restore the previously saved netns. | ||
""" | ||
global __saved_ns | ||
fd = __saved_ns.pop() | ||
try: | ||
setns(fd) | ||
except Exception: | ||
__saved_ns.append(fd) | ||
raise | ||
os.close(fd) |
Oops, something went wrong.