-
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.
Signed-off-by: Christian Hopps <[email protected]>
- Loading branch information
Showing
3 changed files
with
340 additions
and
0 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,249 @@ | ||
# -*- coding: utf-8 eval: (blacken-mode 1) -*- | ||
# SPDX-License-Identifier: ISC | ||
# | ||
# October 29 2023, Christian Hopps <[email protected]> | ||
# | ||
# Copyright (c) 2023, LabN Consulting, L.L.C. | ||
# | ||
|
||
import datetime | ||
import ipaddress | ||
import json | ||
import logging | ||
import math | ||
import os | ||
import pprint | ||
import re | ||
|
||
from lib.common_config import retry, step | ||
from lib.topolog import logger | ||
from lib.topotest import json_cmp as tt_json_cmp | ||
|
||
try: | ||
from deepdiff import DeepDiff as dd_json_cmp | ||
except ImportError: | ||
dd_json_cmp = None | ||
|
||
|
||
def json_cmp(got, expect, exact_match): | ||
if dd_json_cmp: | ||
if exact_match: | ||
deep_diff = dd_json_cmp(expect, got) | ||
# Convert DeepDiff completely into dicts or lists at all levels | ||
json_diff = json.loads(deep_diff.to_json()) | ||
else: | ||
json_diff = dd_json_cmp(expect, got, ignore_order=True) | ||
# Convert DeepDiff completely into dicts or lists at all levels | ||
# json_diff = json.loads(deep_diff.to_json()) | ||
# Remove new fields in json object from diff | ||
if json_diff.get("dictionary_item_added") is not None: | ||
del json_diff["dictionary_item_added"] | ||
# Remove new json objects in json array from diff | ||
if (new_items := json_diff.get("iterable_item_added")) is not None: | ||
new_item_paths = list(new_items.keys()) | ||
for path in new_item_paths: | ||
if type(new_items[path]) is dict: | ||
del new_items[path] | ||
if len(new_items) == 0: | ||
del json_diff["iterable_item_added"] | ||
if not json_diff: | ||
json_diff = None | ||
else: | ||
json_diff = tt_json_cmp(got, expect, exact_match) | ||
json_diff = str(json_diff) | ||
return json_diff | ||
|
||
|
||
def enable_debug(router): | ||
router.vtysh_cmd("debug northbound callbacks configuration") | ||
|
||
|
||
def disable_debug(router): | ||
router.vtysh_cmd("no debug northbound callbacks configuration") | ||
|
||
|
||
def do_oper_test(tgen, query_results): | ||
r1 = tgen.gears["r1"].net | ||
|
||
qcmd = ( | ||
r"vtysh -c 'show mgmt get-data-tree {}' " | ||
r"""| sed -e 's/"uptime": ".*"/"uptime": "rubout"/'""" | ||
r"""| sed -e 's/"vrf": "[0-9]*"/"vrf": "rubout"/'""" | ||
r"""| sed -e 's/"id": [0-9]*/"id": "rubout"/'""" | ||
) | ||
|
||
doreset = True | ||
dd_json_cmp = None | ||
for qr in query_results: | ||
step(f"Perform query '{qr[0]}'", reset=doreset) | ||
if doreset: | ||
doreset = False | ||
expected = open(qr[1], encoding="ascii").read() | ||
output = r1.cmd_nostatus(qcmd.format(qr[0])) | ||
|
||
try: | ||
ojson = json.loads(output) | ||
except json.decoder.JSONDecodeError as error: | ||
logging.error("Error decoding json: %s\noutput:\n%s", error, output) | ||
raise | ||
|
||
ejson = json.loads(expected) | ||
if dd_json_cmp: | ||
cmpout = json_cmp(ojson, ejson, exact_match=True) | ||
if cmpout: | ||
logging.warning( | ||
"-------DIFF---------\n%s\n---------DIFF----------", | ||
pprint.pformat(cmpout), | ||
) | ||
else: | ||
cmpout = tt_json_cmp(ojson, ejson, exact=True) | ||
if cmpout: | ||
logging.warning( | ||
"-------EXPECT--------\n%s\n------END-EXPECT------", | ||
pprint.pformat(ejson), | ||
) | ||
logging.warning( | ||
"--------GOT----------\n%s\n-------END-GOT--------", | ||
pprint.pformat(ojson), | ||
) | ||
|
||
assert cmpout is None | ||
|
||
|
||
def get_ip_networks(super_prefix, count): | ||
count_log2 = math.log(count, 2) | ||
if count_log2 != int(count_log2): | ||
count_log2 = int(count_log2) + 1 | ||
else: | ||
count_log2 = int(count_log2) | ||
network = ipaddress.ip_network(super_prefix) | ||
return tuple(network.subnets(count_log2))[0:count] | ||
|
||
|
||
@retry(retry_timeout=30, initial_wait=0.1) | ||
def check_kernel(r1, super_prefix, count, add, is_blackhole, vrf, matchvia): | ||
network = ipaddress.ip_network(super_prefix) | ||
vrfstr = f" vrf {vrf}" if vrf else "" | ||
if network.version == 6: | ||
kernel = r1.cmd_raises(f"ip -6 route show{vrfstr}") | ||
else: | ||
kernel = r1.cmd_raises(f"ip -4 route show{vrfstr}") | ||
|
||
# logger.debug("checking kernel routing table%s:\n%s", vrfstr, kernel) | ||
|
||
for i, net in enumerate(get_ip_networks(super_prefix, count)): | ||
if not add: | ||
assert str(net) not in kernel | ||
continue | ||
|
||
if is_blackhole: | ||
route = f"blackhole {str(net)} proto (static|196) metric 20" | ||
else: | ||
route = ( | ||
f"{str(net)}(?: nhid [0-9]+)? {matchvia} " | ||
"proto (static|196) metric 20" | ||
) | ||
assert re.search(route, kernel), f"Failed to find \n'{route}'\n in \n'{kernel}'" | ||
|
||
|
||
def addrgen(a, count, step=1): | ||
for _ in range(0, count, step): | ||
yield a | ||
a += step | ||
|
||
|
||
@retry(retry_timeout=30, initial_wait=0.1) | ||
def check_kernel_32(r1, start_addr, count, vrf, step=1): | ||
start = ipaddress.ip_address(start_addr) | ||
vrfstr = f" vrf {vrf}" if vrf else "" | ||
if start.version == 6: | ||
kernel = r1.cmd_raises(f"ip -6 route show{vrfstr}") | ||
else: | ||
kernel = r1.cmd_raises(f"ip -4 route show{vrfstr}") | ||
|
||
nentries = len(re.findall("\n", kernel)) | ||
logging.info("checking kernel routing table%s: (%s entries)", vrfstr, nentries) | ||
|
||
for addr in addrgen(start, count, step): | ||
assert str(addr) in kernel, f"Failed to find '{addr}' in {nentries} entries" | ||
|
||
|
||
def do_config( | ||
r1, | ||
count, | ||
add=True, | ||
do_ipv6=False, | ||
via=None, | ||
vrf=None, | ||
use_cli=False, | ||
): | ||
optype = "adding" if add else "removing" | ||
iptype = "IPv6" if do_ipv6 else "IPv4" | ||
|
||
# | ||
# Set the route details | ||
# | ||
|
||
if vrf: | ||
super_prefix = "2111::/48" if do_ipv6 else "111.0.0.0/8" | ||
else: | ||
super_prefix = "2055::/48" if do_ipv6 else "55.0.0.0/8" | ||
|
||
matchvia = "" | ||
if via == "blackhole": | ||
pass | ||
elif via: | ||
matchvia = f"dev {via}" | ||
else: | ||
if vrf: | ||
via = "2102::2" if do_ipv6 else "3.3.3.2" | ||
matchvia = f"via {via} dev r1-eth1" | ||
else: | ||
via = "2101::2" if do_ipv6 else "1.1.1.2" | ||
matchvia = f"via {via} dev r1-eth0" | ||
|
||
vrfdbg = " in vrf {}".format(vrf) if vrf else "" | ||
logger.debug("{} {} static {} routes{}".format(optype, count, iptype, vrfdbg)) | ||
|
||
# | ||
# Generate config file in a retrievable place | ||
# | ||
|
||
config_file = os.path.join( | ||
r1.logdir, r1.name, "{}-routes-{}.conf".format(iptype.lower(), optype) | ||
) | ||
with open(config_file, "w") as f: | ||
if use_cli: | ||
f.write("configure terminal\n") | ||
if vrf: | ||
f.write("vrf {}\n".format(vrf)) | ||
|
||
for i, net in enumerate(get_ip_networks(super_prefix, count)): | ||
if add: | ||
f.write("ip route {} {}\n".format(net, via)) | ||
else: | ||
f.write("no ip route {} {}\n".format(net, via)) | ||
|
||
# | ||
# Load config file. | ||
# | ||
|
||
if use_cli: | ||
load_command = 'vtysh < "{}"'.format(config_file) | ||
else: | ||
load_command = 'vtysh -f "{}"'.format(config_file) | ||
tstamp = datetime.datetime.now() | ||
output = r1.cmd_raises(load_command) | ||
delta = (datetime.datetime.now() - tstamp).total_seconds() | ||
|
||
# | ||
# Verify the results are in the kernel | ||
# | ||
check_kernel(r1, super_prefix, count, add, via == "blackhole", vrf, matchvia) | ||
|
||
optyped = "added" if add else "removed" | ||
logger.debug( | ||
"{} {} {} static routes under {}{} in {}s".format( | ||
optyped, count, iptype.lower(), super_prefix, vrfdbg, delta | ||
) | ||
) |
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,25 @@ | ||
log timestamp precision 6 | ||
log file frr.log | ||
|
||
no debug memstats-at-exit | ||
|
||
! debug northbound libyang | ||
! debug northbound callbacks | ||
|
||
debug northbound notifications | ||
debug northbound events | ||
|
||
debug mgmt backend datastore frontend transaction | ||
debug mgmt client frontend | ||
debug mgmt client backend | ||
|
||
interface r1-eth0 | ||
ip address 1.1.1.1/24 | ||
exit | ||
|
||
interface r1-eth1 vrf red | ||
ip address 3.3.3.1/24 | ||
exit | ||
|
||
ip route 11.11.11.11/32 1.1.1.2 | ||
ip route 13.13.13.13/32 3.3.3.2 vrf red |
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,66 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 eval: (blacken-mode 1) -*- | ||
# SPDX-License-Identifier: ISC | ||
# | ||
# Copyright (c) 2021, LabN Consulting, L.L.C. | ||
# Copyright (c) 2019-2020 by | ||
# Donatas Abraitis <[email protected]> | ||
# | ||
# noqa: E501 | ||
# | ||
""" | ||
Test static route functionality | ||
""" | ||
import time | ||
|
||
import pytest | ||
from lib.common_config import step | ||
from lib.topogen import Topogen, TopoRouter | ||
from oper import check_kernel_32 | ||
|
||
pytestmark = [pytest.mark.staticd] | ||
|
||
|
||
@pytest.fixture(scope="module") | ||
def tgen(request): | ||
"Setup/Teardown the environment and provide tgen argument to tests" | ||
|
||
topodef = {"s1": ("r1",), "s2": ("r1",)} | ||
|
||
tgen = Topogen(topodef, request.module.__name__) | ||
tgen.start_topology() | ||
|
||
router_list = tgen.routers() | ||
for rname, router in router_list.items(): | ||
# Setup VRF red | ||
router.net.add_l3vrf("red", 10) | ||
router.net.add_loop("lo-red") | ||
router.net.attach_iface_to_l3vrf("lo-red", "red") | ||
router.net.attach_iface_to_l3vrf(rname + "-eth1", "red") | ||
router.load_frr_config("frr-scale.conf") | ||
router.load_config(TopoRouter.RD_SHARP, "") | ||
|
||
tgen.start_router() | ||
yield tgen | ||
tgen.stop_topology() | ||
|
||
|
||
def test_oper_simple(tgen): | ||
if tgen.routers_have_failure(): | ||
pytest.skip(tgen.errors) | ||
|
||
r1 = tgen.gears["r1"].net | ||
|
||
time.sleep(2) | ||
count = 30 * 1000 | ||
|
||
vrf = None # "red" | ||
check_kernel_32(r1, "11.11.11.11", 1, vrf) | ||
|
||
step("Found 11.11.11.11 in kernel adding sharpd routes") | ||
r1.cmd_raises(f"vtysh -c 'sharp install routes 20.0.0.0 nexthop 1.1.1.2 {count}'") | ||
check_kernel_32(r1, "20.0.0.0", count, vrf, 1000) | ||
|
||
step(f"All {count} routes installed in kernel, continuing") | ||
output = r1.cmd_raises("vtysh -c 'show mgmt get-data-tree /frr-vrf:lib'") | ||
step("Got output") |