-
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.
[QPPB] Implement test suit with common setups, based on BCC
- Loading branch information
1 parent
db4ad91
commit 135da45
Showing
10 changed files
with
1,607 additions
and
15 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,132 @@ | ||
import os | ||
import sys | ||
import json | ||
|
||
from bcc import BPF, DEBUG_PREPROCESSOR, DEBUG_SOURCE, DEBUG_BPF, DEBUG_BTF | ||
from .ns import pushns, popns | ||
from lib.topolog import logger | ||
from lib.common_config import ( | ||
start_router_daemons, | ||
kill_router_daemons, | ||
) | ||
|
||
CWD = os.path.dirname(os.path.realpath(__file__)) | ||
sys.path.append(os.path.join(CWD, "../")) | ||
sys.path.append(os.path.join(CWD, "../lib/")) | ||
|
||
# from pyroute2.netns import pushns, popns | ||
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): | ||
" XXX: remove xdp hook ... " | ||
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, vtysh_cmd=None, cflags=None, debug_on=True): | ||
""" | ||
Initialize rnode XDP hooks and BPF mapping handlers | ||
- compile xdp handlers from `xdp_qppb.c` | ||
- load `xdp_qppb` and `xdp_tc_mark` hooks | ||
- restart router with QPPB plugin | ||
- execute optional vtysh configuration | ||
Parameters | ||
---------- | ||
* `tgen`: topogen object | ||
* `vtysh_cmd`: any extra configuration, typically not present in json | ||
* `cflags`: processing mode required, any other optional | ||
* `debug_on`: enable all BPF debug logs | ||
Usage | ||
--------- | ||
load_qppb_plugin(tgen, r1, SET_QPPB_TABLE_MAP, cflags=["-DMARK_SKB"]) | ||
Returns -> None (XXX) | ||
""" | ||
debug_flags = DEBUG_BPF | DEBUG_PREPROCESSOR | DEBUG_SOURCE | DEBUG_BTF | ||
debug = debug_flags if debug_on else 0 | ||
bpf_flags = [ | ||
'-DBPF_PIN_DIR="{}"'.format(rnode.bpfdir), | ||
"-DRESPECT_TOS", | ||
"-DLOG_QPPB", | ||
"-DLOG_TC", | ||
"-w", | ||
] | ||
|
||
bpf_flags.extend(cflags) | ||
for mode in ["MARK_SKB", "MARK_META"]: | ||
if ("-D%s" % mode) in bpf_flags: | ||
bpf_flags.append('-DMODE_STR="%s"' % mode) | ||
|
||
try: | ||
src_file = "%s/xdp_qppb.c" % CWD | ||
logger.info("Preparing the XDP src: " + src_file) | ||
b = BPF(src_file=src_file, 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: | ||
import pytest # XXX: proper error handling | ||
pytest.skip("Failed to configure XDP environment -- \n%s", 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}) | ||
if vtysh_cmd: | ||
rnode.vtysh_cmd(vtysh_cmd) | ||
|
||
|
||
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.debug("===================\nDump bpf log buffer:\n") | ||
line = b.trace_readline(nonblocking=True) | ||
while line: | ||
logger.debug(line) | ||
line = b.trace_readline(nonblocking=True) |
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,142 @@ | ||
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}, # FIXME: NABI32? | ||
'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, libc=None): | ||
''' | ||
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) | ||
libc = libc or ctypes.CDLL(ctypes.util.find_library('c'), 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, libc=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, libc=libc) | ||
|
||
def popns(libc=None): | ||
''' | ||
Restore the previously saved netns. | ||
''' | ||
global __saved_ns | ||
fd = __saved_ns.pop() | ||
try: | ||
setns(fd, libc=libc) | ||
except Exception: | ||
__saved_ns.append(fd) | ||
raise | ||
os.close(fd) | ||
|
Oops, something went wrong.