diff options
Diffstat (limited to 'tests')
27 files changed, 1630 insertions, 30 deletions
diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-update-loc-rib-step1.json b/tests/topotests/bgp_bmp/bmp1import/bmp-update-loc-rib-step1.json new file mode 100644 index 0000000000..3542f4e495 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-update-loc-rib-step1.json @@ -0,0 +1,34 @@ +{ + "loc-rib": { + "update": { + "172.31.0.77/32": { + "as_path": "", + "bgp_nexthop": "192.168.1.3", + "bmp_log_type": "update", + "ip_prefix": "172.31.0.77/32", + "is_filtered": false, + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "444:1", + "peer_type": "loc-rib instance", + "policy": "loc-rib" + }, + "2001::1125/128": { + "afi": 2, + "as_path": "", + "bmp_log_type": "update", + "ip_prefix": "2001::1125/128", + "is_filtered": false, + "nxhp_ip": "192:167::3", + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "555:1", + "peer_type": "loc-rib instance", + "policy": "loc-rib", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-update-loc-rib-step2.json b/tests/topotests/bgp_bmp/bmp1import/bmp-update-loc-rib-step2.json new file mode 100644 index 0000000000..60066d502c --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-update-loc-rib-step2.json @@ -0,0 +1,34 @@ +{ + "loc-rib": { + "update": { + "172.31.0.77/32": { + "as_path": "", + "bgp_nexthop": "192.168.1.3", + "bmp_log_type": "update", + "ip_prefix": "172.31.0.77/32", + "is_filtered": false, + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "666:22", + "peer_type": "loc-rib instance", + "policy": "loc-rib" + }, + "2001::1125/128": { + "afi": 2, + "as_path": "", + "bmp_log_type": "update", + "ip_prefix": "2001::1125/128", + "is_filtered": false, + "nxhp_ip": "192:167::3", + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "666:22", + "peer_type": "loc-rib instance", + "policy": "loc-rib", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-update-post-policy-step1.json b/tests/topotests/bgp_bmp/bmp1import/bmp-update-post-policy-step1.json new file mode 100644 index 0000000000..cf71f20485 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-update-post-policy-step1.json @@ -0,0 +1,36 @@ +{ + "post-policy": { + "update": { + "172.31.0.77/32": { + "as_path": "", + "bgp_nexthop": "192.168.1.3", + "bmp_log_type": "update", + "ip_prefix": "172.31.0.77/32", + "ipv6": false, + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "444:1", + "peer_ip": "192.168.1.3", + "peer_type": "route distinguisher instance", + "policy": "post-policy" + }, + "2001::1125/128": { + "afi": 2, + "as_path": "", + "bmp_log_type": "update", + "ip_prefix": "2001::1125/128", + "ipv6": true, + "nxhp_ip": "192:167::3", + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "555:1", + "peer_ip": "192:167::3", + "peer_type": "route distinguisher instance", + "policy": "post-policy", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-update-post-policy-step2.json b/tests/topotests/bgp_bmp/bmp1import/bmp-update-post-policy-step2.json new file mode 100644 index 0000000000..b555c2a371 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-update-post-policy-step2.json @@ -0,0 +1,36 @@ +{ + "post-policy": { + "update": { + "172.31.0.77/32": { + "as_path": "", + "bgp_nexthop": "192.168.1.3", + "bmp_log_type": "update", + "ip_prefix": "172.31.0.77/32", + "ipv6": false, + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "666:22", + "peer_ip": "192.168.1.3", + "peer_type": "route distinguisher instance", + "policy": "post-policy" + }, + "2001::1125/128": { + "afi": 2, + "as_path": "", + "bmp_log_type": "update", + "ip_prefix": "2001::1125/128", + "ipv6": true, + "nxhp_ip": "192:167::3", + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "666:22", + "peer_ip": "192:167::3", + "peer_type": "route distinguisher instance", + "policy": "post-policy", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-update-pre-policy-step1.json b/tests/topotests/bgp_bmp/bmp1import/bmp-update-pre-policy-step1.json new file mode 100644 index 0000000000..43273cc93a --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-update-pre-policy-step1.json @@ -0,0 +1,36 @@ +{ + "pre-policy": { + "update": { + "172.31.0.77/32": { + "as_path": "", + "bgp_nexthop": "192.168.1.3", + "bmp_log_type": "update", + "ip_prefix": "172.31.0.77/32", + "ipv6": false, + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "444:1", + "peer_ip": "192.168.1.3", + "peer_type": "route distinguisher instance", + "policy": "pre-policy" + }, + "2001::1125/128": { + "afi": 2, + "as_path": "", + "bmp_log_type": "update", + "ip_prefix": "2001::1125/128", + "ipv6": true, + "nxhp_ip": "192:167::3", + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "555:1", + "peer_ip": "192:167::3", + "peer_type": "route distinguisher instance", + "policy": "pre-policy", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-update-pre-policy-step2.json b/tests/topotests/bgp_bmp/bmp1import/bmp-update-pre-policy-step2.json new file mode 100644 index 0000000000..20549926d5 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-update-pre-policy-step2.json @@ -0,0 +1,36 @@ +{ + "pre-policy": { + "update": { + "172.31.0.77/32": { + "as_path": "", + "bgp_nexthop": "192.168.1.3", + "bmp_log_type": "update", + "ip_prefix": "172.31.0.77/32", + "ipv6": false, + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "666:22", + "peer_ip": "192.168.1.3", + "peer_type": "route distinguisher instance", + "policy": "pre-policy" + }, + "2001::1125/128": { + "afi": 2, + "as_path": "", + "bmp_log_type": "update", + "ip_prefix": "2001::1125/128", + "ipv6": true, + "nxhp_ip": "192:167::3", + "origin": "IGP", + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "666:22", + "peer_ip": "192:167::3", + "peer_type": "route distinguisher instance", + "policy": "pre-policy", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-loc-rib-step1.json b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-loc-rib-step1.json new file mode 100644 index 0000000000..fcf518390d --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-loc-rib-step1.json @@ -0,0 +1,28 @@ +{ + "loc-rib": { + "withdraw": { + "172.31.0.77/32": { + "bmp_log_type": "withdraw", + "ip_prefix": "172.31.0.77/32", + "is_filtered": false, + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "444:1", + "peer_type": "loc-rib instance", + "policy": "loc-rib" + }, + "2001::1125/128": { + "afi": 2, + "bmp_log_type": "withdraw", + "ip_prefix": "2001::1125/128", + "is_filtered": false, + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "555:1", + "peer_type": "loc-rib instance", + "policy": "loc-rib", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-loc-rib-step2.json b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-loc-rib-step2.json new file mode 100644 index 0000000000..1e5040ba60 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-loc-rib-step2.json @@ -0,0 +1,34 @@ +{ + "loc-rib": { + "withdraw": { + "172.31.0.15/32": { + "afi": 1, + "bmp_log_type": "withdraw", + "ip_prefix": "172.31.0.15/32", + "is_filtered": false, + "label": 0, + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "0:0", + "peer_type": "loc-rib instance", + "policy": "loc-rib", + "rd": "444:2", + "safi": 128 + }, + "2001::1111/128": { + "afi": 2, + "bmp_log_type": "withdraw", + "ip_prefix": "2001::1111/128", + "is_filtered": false, + "label": 0, + "peer_asn": 65501, + "peer_bgp_id": "192.168.0.1", + "peer_distinguisher": "0:0", + "peer_type": "loc-rib instance", + "policy": "loc-rib", + "rd": "555:2", + "safi": 128 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-post-policy-step1.json b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-post-policy-step1.json new file mode 100644 index 0000000000..6626e91361 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-post-policy-step1.json @@ -0,0 +1,30 @@ +{ + "post-policy": { + "withdraw": { + "172.31.0.77/32": { + "bmp_log_type": "withdraw", + "ip_prefix": "172.31.0.77/32", + "ipv6": false, + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "444:1", + "peer_ip": "192.168.1.3", + "peer_type": "route distinguisher instance", + "policy": "post-policy" + }, + "2001::1125/128": { + "afi": 2, + "bmp_log_type": "withdraw", + "ip_prefix": "2001::1125/128", + "ipv6": true, + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "555:1", + "peer_ip": "192:167::3", + "peer_type": "route distinguisher instance", + "policy": "post-policy", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-pre-policy-step1.json b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-pre-policy-step1.json new file mode 100644 index 0000000000..d3fb1b7ba1 --- /dev/null +++ b/tests/topotests/bgp_bmp/bmp1import/bmp-withdraw-pre-policy-step1.json @@ -0,0 +1,30 @@ +{ + "pre-policy": { + "withdraw": { + "172.31.0.77/32": { + "bmp_log_type": "withdraw", + "ip_prefix": "172.31.0.77/32", + "ipv6": false, + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "444:1", + "peer_ip": "192.168.1.3", + "peer_type": "route distinguisher instance", + "policy": "pre-policy" + }, + "2001::1125/128": { + "afi": 2, + "bmp_log_type": "withdraw", + "ip_prefix": "2001::1125/128", + "ipv6": true, + "peer_asn": 65501, + "peer_bgp_id": "192.168.1.3", + "peer_distinguisher": "555:1", + "peer_ip": "192:167::3", + "peer_type": "route distinguisher instance", + "policy": "pre-policy", + "safi": 1 + } + } + } +} diff --git a/tests/topotests/bgp_bmp/r1import/frr.conf b/tests/topotests/bgp_bmp/r1import/frr.conf new file mode 100644 index 0000000000..bec4eb01c7 --- /dev/null +++ b/tests/topotests/bgp_bmp/r1import/frr.conf @@ -0,0 +1,73 @@ +interface r1import-eth0 + ip address 192.0.2.1/24 +! +interface r1import-eth1 + ip address 192.168.0.1/24 + ipv6 address 192:168::1/64 +! +interface r1import-eth2 + ip address 192.168.1.1/24 + ipv6 address 192:167::1/64 +! +router bgp 65501 + bgp router-id 192.168.0.1 + bgp log-neighbor-changes + no bgp ebgp-requires-policy + neighbor 192.168.0.2 remote-as 65502 + neighbor 192:168::2 remote-as 65502 +! + bmp targets bmp1 + bmp connect 192.0.2.10 port 1789 min-retry 100 max-retry 10000 + bmp monitor ipv4 unicast pre-policy + bmp monitor ipv6 unicast pre-policy + bmp monitor ipv4 unicast post-policy + bmp monitor ipv6 unicast post-policy + bmp monitor ipv4 unicast loc-rib + bmp monitor ipv6 unicast loc-rib + bmp import-vrf-view vrf1 + exit +! + address-family ipv4 vpn + neighbor 192.168.0.2 activate + neighbor 192.168.0.2 soft-reconfiguration inbound + exit-address-family + address-family ipv6 vpn + neighbor 192:168::2 activate + neighbor 192:168::2 soft-reconfiguration inbound + exit-address-family + address-family ipv4 unicast + neighbor 192.168.0.2 activate + neighbor 192.168.0.2 soft-reconfiguration inbound + no neighbor 192:168::2 activate + exit-address-family +! + address-family ipv6 unicast + neighbor 192:168::2 activate + neighbor 192:168::2 soft-reconfiguration inbound + exit-address-family +! +router bgp 65501 vrf vrf1 + bgp router-id 192.168.0.1 + bgp log-neighbor-changes + neighbor 192.168.1.3 remote-as 65501 + neighbor 192:167::3 remote-as 65501 + address-family ipv4 unicast + neighbor 192.168.1.3 activate + neighbor 192.168.1.3 soft-reconfiguration inbound + no neighbor 192:167::3 activate + label vpn export 101 + rd vpn export 444:1 + rt vpn both 52:100 + export vpn + import vpn + exit-address-family + address-family ipv6 unicast + neighbor 192:167::3 activate + neighbor 192:167::3 soft-reconfiguration inbound + label vpn export 103 + rd vpn export 555:1 + rt vpn both 54:200 + export vpn + import vpn + exit-address-family +exit diff --git a/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv4-update-step1.json b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv4-update-step1.json new file mode 100644 index 0000000000..c21a586c3b --- /dev/null +++ b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv4-update-step1.json @@ -0,0 +1,21 @@ +{ + "routes": { + "172.31.0.77/32": [ + { + "bestpath": true, + "pathFrom": "internal", + "path": "", + "origin": "IGP", + "nexthops": [ + { + "ip": "192.168.1.3", + "hostname": "r3", + "afi": "ipv4", + "used": true + } + ] + } + ] + } +} + diff --git a/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv4-withdraw-step1.json b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv4-withdraw-step1.json new file mode 100644 index 0000000000..154bef7995 --- /dev/null +++ b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv4-withdraw-step1.json @@ -0,0 +1,6 @@ +{ + "routes": { + "172.31.0.77/32": null + } +} + diff --git a/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv6-update-step1.json b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv6-update-step1.json new file mode 100644 index 0000000000..14df5ec931 --- /dev/null +++ b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv6-update-step1.json @@ -0,0 +1,27 @@ +{ + "routes": { + "2001::1125/128": [ + { + "bestpath": true, + "pathFrom": "internal", + "path": "", + "origin": "IGP", + "nexthops": [ + { + "ip": "192:167::3", + "hostname": "r3", + "afi": "ipv6", + "scope": "global" + }, + { + "hostname": "r3", + "afi": "ipv6", + "scope": "link-local", + "used": true + } + ] + } + ] + } +} + diff --git a/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv6-withdraw-step1.json b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv6-withdraw-step1.json new file mode 100644 index 0000000000..7c7a95e33e --- /dev/null +++ b/tests/topotests/bgp_bmp/r1import/show-bgp-vrf1-ipv6-withdraw-step1.json @@ -0,0 +1,6 @@ +{ + "routes": { + "2001::1125/128": null + } +} + diff --git a/tests/topotests/bgp_bmp/r3/frr.conf b/tests/topotests/bgp_bmp/r3/frr.conf new file mode 100644 index 0000000000..145e156b11 --- /dev/null +++ b/tests/topotests/bgp_bmp/r3/frr.conf @@ -0,0 +1,18 @@ +interface r3-eth0 + ip address 192.168.1.3/24 + ipv6 address 192:167::3/64 +! +router bgp 65501 + bgp router-id 192.168.1.3 + bgp log-neighbor-changes + no bgp network import-check + neighbor 192.168.1.1 remote-as 65501 + neighbor 192:167::1 remote-as 65501 + address-family ipv4 unicast + neighbor 192.168.1.1 activate + no neighbor 192:167::1 activate + exit-address-family + address-family ipv6 unicast + neighbor 192:167::1 activate + exit-address-family +exit diff --git a/tests/topotests/bgp_bmp/test_bgp_bmp_3.py b/tests/topotests/bgp_bmp/test_bgp_bmp_3.py new file mode 100644 index 0000000000..212cf9e696 --- /dev/null +++ b/tests/topotests/bgp_bmp/test_bgp_bmp_3.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC + +# Copyright 2024 6WIND S.A. +# + +""" +test_bgp_bmp.py_3: Test BGP BMP functionalities + + +------+ +------+ +------+ + | | | | | | + | BMP1 |------------| R1 |---------------| R2 | + | | | | | | + +------+ +--+---+ +------+ + | + +--+---+ + | | + | R3 | + | | + +------+ + +Setup two routers R1 and R2 with one link configured with IPv4 and +IPv6 addresses. +Configure BGP in R1 and R2 to exchange prefixes from +the latter to the first router. +Setup a link between R1 and the BMP server, activate the BMP feature in R1 +and ensure the monitored BGP sessions logs are well present on the BMP server. +""" + +from functools import partial +import json +import os +import pytest +import sys + +# Save the Current Working Directory to find configuration files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join("../")) +sys.path.append(os.path.join("../lib/")) + +# pylint: disable=C0413 +# Import topogen and topotest helpers +from lib import topotest +from lib.bgp import verify_bgp_convergence_from_running_config +from lib.bgp import bgp_configure_prefixes +from .bgpbmp import ( + bmp_check_for_prefixes, + bmp_check_for_peer_message, + bmp_update_seq, + bmp_reset_seq, +) +from lib.topogen import Topogen, TopoRouter, get_topogen +from lib.topolog import logger + +pytestmark = [pytest.mark.bgpd] + +PRE_POLICY = "pre-policy" +POST_POLICY = "post-policy" +LOC_RIB = "loc-rib" + +UPDATE_EXPECTED_JSON = False +DEBUG_PCAP = False + + +def build_topo(tgen): + tgen.add_router("r1import") + tgen.add_router("r2") + tgen.add_router("r3") # CPE behind r1 + + tgen.add_bmp_server("bmp1import", ip="192.0.2.10", defaultRoute="via 192.0.2.1") + + switch = tgen.add_switch("s1") + switch.add_link(tgen.gears["r1import"]) + switch.add_link(tgen.gears["bmp1import"]) + + tgen.add_link(tgen.gears["r1import"], tgen.gears["r2"], "r1import-eth1", "r2-eth0") + tgen.add_link(tgen.gears["r1import"], tgen.gears["r3"], "r1import-eth2", "r3-eth0") + + +def setup_module(mod): + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + tgen.net["r1import"].cmd( + """ +ip link add vrf1 type vrf table 10 +ip link set vrf1 up +ip link set r1import-eth2 master vrf1 + """ + ) + + bmp_reset_seq() + if DEBUG_PCAP: + tgen.gears["r1import"].run("rm /tmp/bmp.pcap") + tgen.gears["r1import"].run( + "tcpdump -nni r1import-eth0 -s 0 -w /tmp/bmp.pcap &", stdout=None + ) + + for rname, router in tgen.routers().items(): + logger.info("Loading router %s" % rname) + router.load_frr_config( + os.path.join(CWD, "{}/frr.conf".format(rname)), + [(TopoRouter.RD_ZEBRA, None), (TopoRouter.RD_BGP, "-M bmp")], + ) + + tgen.start_router() + + logger.info("starting BMP servers") + for bmp_name, server in tgen.get_bmp_servers().items(): + server.start(log_file=os.path.join(tgen.logdir, bmp_name, "bmp.log")) + + +def teardown_module(_mod): + tgen = get_topogen() + tgen.stop_topology() + + +def test_bgp_convergence(): + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + result = verify_bgp_convergence_from_running_config(tgen, dut="r1import") + assert result is True, "BGP is not converging" + + +def _test_prefixes_syncro(policy, vrf=None, step=1): + """ + Check that the given policy has syncronised the previously received BGP + updates. + """ + tgen = get_topogen() + + prefixes = ["172.31.0.77/32", "2001::1125/128"] + # check + test_func = partial( + bmp_check_for_prefixes, + prefixes, + "update", + policy, + step, + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import"), + tgen.gears["r1import"], + f"{CWD}/bmp1import", + UPDATE_EXPECTED_JSON, + LOC_RIB, + ) + success, res = topotest.run_and_expect(test_func, None, count=30, wait=1) + assert success, "Checking the updated prefixes has failed ! %s" % res + + +def _test_prefixes(policy, vrf=None, step=0): + """ + Setup the BMP monitor policy, Add and withdraw ipv4/v6 prefixes. + Check if the previous actions are logged in the BMP server with the right + message type and the right policy. + """ + tgen = get_topogen() + + safi = "vpn" if vrf else "unicast" + + prefixes = ["172.31.0.77/32", "2001::1125/128"] + + for type in ("update", "withdraw"): + bmp_update_seq( + tgen.gears["bmp1import"], os.path.join(tgen.logdir, "bmp1import", "bmp.log") + ) + + bgp_configure_prefixes( + tgen.gears["r3"], + 65501, + "unicast", + prefixes, + vrf=None, + update=(type == "update"), + ) + + logger.info(f"checking for prefixes {type}") + + for ipver in [4, 6]: + if UPDATE_EXPECTED_JSON: + continue + ref_file = "{}/r1import/show-bgp-{}-ipv{}-{}-step{}.json".format( + CWD, vrf, ipver, type, step + ) + expected = json.loads(open(ref_file).read()) + + test_func = partial( + topotest.router_json_cmp, + tgen.gears["r1import"], + f"show bgp vrf {vrf} ipv{ipver} json", + expected, + ) + _, res = topotest.run_and_expect(test_func, None, count=30, wait=1) + assertmsg = f"r1: BGP IPv{ipver} convergence failed" + assert res is None, assertmsg + + # check + test_func = partial( + bmp_check_for_prefixes, + prefixes, + type, + policy, + step, + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import"), + tgen.gears["r1import"], + f"{CWD}/bmp1import", + UPDATE_EXPECTED_JSON, + LOC_RIB, + ) + success, res = topotest.run_and_expect(test_func, None, count=30, wait=1) + assert success, "Checking the updated prefixes has failed ! %s" % res + + +def _test_peer_up(check_locrib=True): + """ + Checking for BMP peers up messages + """ + + tgen = get_topogen() + if check_locrib: + peers = ["0.0.0.0", "192.168.1.3", "192:167::3"] + else: + peers = ["192.168.1.3", "192:167::3"] + + logger.info("checking for BMP peers up messages") + + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer up", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert success, "Checking the updated prefixes has been failed !." + + +def test_bmp_server_logging(): + """ + Assert the logging of the bmp server. + """ + + def check_for_log_file(): + tgen = get_topogen() + output = tgen.gears["bmp1import"].run( + "ls {}".format(os.path.join(tgen.logdir, "bmp1import")) + ) + if "bmp.log" not in output: + return False + return True + + success, _ = topotest.run_and_expect(check_for_log_file, True, count=30, wait=1) + assert success, "The BMP server is not logging" + + +def test_bmp_peer_up_start(): + _test_peer_up() + + +def test_bmp_bgp_unicast(): + """ + Add/withdraw bgp unicast prefixes and check the bmp logs. + """ + logger.info("*** Unicast prefixes pre-policy logging ***") + _test_prefixes(PRE_POLICY, vrf="vrf1", step=1) + logger.info("*** Unicast prefixes post-policy logging ***") + _test_prefixes(POST_POLICY, vrf="vrf1", step=1) + logger.info("*** Unicast prefixes loc-rib logging ***") + _test_prefixes(LOC_RIB, vrf="vrf1", step=1) + + +def test_peer_down(): + """ + Checking for BMP peers down messages + """ + tgen = get_topogen() + + tgen.gears["r3"].vtysh_cmd("clear bgp *") + + peers = ["192.168.1.3", "192:167::3"] + + logger.info("checking for BMP peers down messages") + + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer down", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert success, "Checking the updated prefixes has been failed !." + + +def test_reconfigure_prefixes(): + """ + Reconfigured BGP networks from R3. Check for BGP VRF update messages + """ + + tgen = get_topogen() + + prefixes = ["172.31.0.77/32", "2001::1125/128"] + bgp_configure_prefixes( + tgen.gears["r3"], + 65501, + "unicast", + prefixes, + vrf=None, + update=True, + ) + + for ipver in [4, 6]: + ref_file = "{}/r1import/show-bgp-{}-ipv{}-{}-step{}.json".format( + CWD, "vrf1", ipver, "update", 1 + ) + expected = json.loads(open(ref_file).read()) + + test_func = partial( + topotest.router_json_cmp, + tgen.gears["r1import"], + f"show bgp vrf vrf1 ipv{ipver} json", + expected, + ) + _, res = topotest.run_and_expect(test_func, None, count=30, wait=1) + assertmsg = f"r1: BGP IPv{ipver} convergence failed" + assert res is None, assertmsg + + +def test_monitor_syncro(): + """ + Checking for BMP peers down messages + """ + tgen = get_topogen() + + tgen.gears["r1import"].vtysh_cmd( + """ + configure terminal + router bgp 65501 + bmp targets bmp1 + bmp import-vrf-view vrf1 + """ + ) + + logger.info("*** Unicast prefixes pre-policy logging ***") + _test_prefixes_syncro(PRE_POLICY, vrf="vrf1") + logger.info("*** Unicast prefixes post-policy logging ***") + _test_prefixes_syncro(POST_POLICY, vrf="vrf1") + logger.info("*** Unicast prefixes loc-rib logging ***") + _test_prefixes_syncro(LOC_RIB, vrf="vrf1") + + +def test_reconfigure_route_distinguisher_vrf1(): + """ + Checking for BMP peers down messages + """ + tgen = get_topogen() + + bmp_update_seq( + tgen.gears["bmp1import"], os.path.join(tgen.logdir, "bmp1import", "bmp.log") + ) + peers = ["0.0.0.0"] + + tgen.gears["r1import"].vtysh_cmd( + """ + configure terminal + router bgp 65501 vrf vrf1 + address-family ipv4 unicast + rd vpn export 666:22 + exit-address-family + address-family ipv6 unicast + rd vpn export 666:22 + """ + ) + logger.info( + "Checking for BMP peer down LOC-RIB message with route-distinguisher set to 444:1" + ) + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer down", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + peer_distinguisher="444:1", + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert ( + success + ), "Checking the BMP peer down LOC-RIB message with route-distinguisher set to 444:1 failed !." + + logger.info( + "Checking for BMP peer up LOC-RIB messages with route-distinguisher set to 666:22" + ) + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer up", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + peer_distinguisher="666:22", + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert ( + success + ), "Checking the BMP peer up LOC-RIB message with route-distinguisher set to 666:22 failed !." + + logger.info( + "Checking for BMP peer up messages with route-distinguisher set to 666:22" + ) + peers = ["192.168.1.3", "192:167::3"] + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer up", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + peer_distinguisher="666:22", + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert ( + success + ), "Checking the BMP peer up messages with route-distinguisher set to 666:22 failed !." + + logger.info("*** Unicast prefixes pre-policy logging ***") + _test_prefixes_syncro(PRE_POLICY, vrf="vrf1", step=2) + logger.info("*** Unicast prefixes post-policy logging ***") + _test_prefixes_syncro(POST_POLICY, vrf="vrf1", step=2) + logger.info("*** Unicast prefixes loc-rib logging ***") + _test_prefixes_syncro(LOC_RIB, vrf="vrf1", step=2) + + +def test_bgp_routerid_changed(): + """ + Checking for BGP loc-rib up messages with new router-id + """ + tgen = get_topogen() + + tgen.gears["r1import"].vtysh_cmd( + """ + configure terminal + router bgp 65501 vrf vrf1 + bgp router-id 192.168.1.77 + """ + ) + + peers = ["0.0.0.0"] + + logger.info( + "checking for BMP peer down LOC-RIB message with router-id set to 192.168.0.1." + ) + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer down", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + peer_bgp_id="192.168.0.1", + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert ( + success + ), "Checking the BMP peer down LOC-RIB message with router-id set to 192.168.0.1 failed !." + + logger.info( + "checking for BMP peer up LOC-RIB message with router-id set to 192.168.1.77." + ) + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer up", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + peer_bgp_id="192.168.1.77", + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert ( + success + ), "Checking the BMP peer up LOC-RIB message with router-id set to 192.168.1.77 failed !." + + +def test_bgp_instance_flapping(): + """ + Checking for BGP loc-rib up messages + """ + tgen = get_topogen() + + # create flapping at BMP + # note: only peer up are handled at BMP level today + tgen.net["r1import"].cmd("ip link set dev vrf1 down") + + peers = ["0.0.0.0"] + + logger.info("checking for BMP peer down LOC-RIB message.") + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer down", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert success, "Checking the BMP peer down LOC-RIB message failed !." + + tgen.net["r1import"].cmd("ip link set dev vrf1 up") + + logger.info("checking for BMP peer up LOC-RIB message.") + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer up", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + is_rd_instance=True, + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert success, "Checking the BMP peer up LOC-RIB message failed !." + + +def test_peer_up_after_flush(): + """ + Checking for BMP peers down messages + """ + _test_peer_up(check_locrib=False) + + +def test_peer_down_locrib(): + """ + Checking for BMP peers down loc-rib messages + """ + tgen = get_topogen() + + tgen.gears["r1import"].vtysh_cmd( + """ + configure terminal + router bgp 65501 + bmp targets bmp1 + no bmp import-vrf-view vrf1 + """ + ) + + peers = ["0.0.0.0"] + + logger.info("checking for BMP peers down messages") + + test_func = partial( + bmp_check_for_peer_message, + peers, + "peer down", + tgen.gears["bmp1import"], + os.path.join(tgen.logdir, "bmp1import", "bmp.log"), + ) + success, _ = topotest.run_and_expect(test_func, True, count=30, wait=1) + assert success, "Checking the BMP peer down message has failed !." + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/__init__.py b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/__init__.py diff --git a/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/r1/frr.conf b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/r1/frr.conf new file mode 100644 index 0000000000..428b1d992f --- /dev/null +++ b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/r1/frr.conf @@ -0,0 +1,35 @@ +! +interface r1-eth0 + ip address 192.168.179.4/24 +exit +! +router bgp 65001 +! +router bgp 65001 vrf CUSTOMER-A + bgp router-id 192.168.179.4 + no bgp ebgp-requires-policy + no bgp network import-check + neighbor 192.168.179.5 remote-as external +! + address-family ipv4 unicast + neighbor 192.168.179.5 next-hop-self + neighbor 192.168.179.5 allowas-in 10 + label vpn export auto + rd vpn export 100:1 + rt vpn both 100:1 100:2 + export vpn + import vpn + exit-address-family +! +router bgp 65001 vrf CUSTOMER-B + bgp router-id 192.168.0.1 + no bgp ebgp-requires-policy + no bgp network import-check +! + address-family ipv4 unicast + label vpn export auto + rd vpn export 100:2 + rt vpn import 100:1 100:2 + export vpn + import vpn + exit-address-family diff --git a/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/r2/frr.conf b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/r2/frr.conf new file mode 100644 index 0000000000..58e63d6cf0 --- /dev/null +++ b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/r2/frr.conf @@ -0,0 +1,48 @@ +! +interface lo + ip address 10.10.10.10/32 +! +interface r2-eth0 + ip address 192.168.179.5/24 +exit +! +interface r2-eth1 + ip address 192.168.2.2/24 +exit +! +router bgp 65002 +! +router bgp 65002 vrf CUSTOMER-A + bgp router-id 192.168.179.5 + no bgp ebgp-requires-policy + no bgp network import-check + neighbor 192.168.179.4 remote-as external +! + address-family ipv4 unicast + neighbor 192.168.179.4 next-hop-self + neighbor 192.168.179.4 route-map r1 out + label vpn export auto + rd vpn export 100:1 + rt vpn import 100:1 100:2 + export vpn + import vpn + exit-address-family +! +router bgp 65002 vrf CUSTOMER-B + bgp router-id 192.168.0.2 + no bgp ebgp-requires-policy + no bgp network import-check +! + address-family ipv4 unicast + redistribute connected + network 10.10.10.10/32 + label vpn export auto + rd vpn export 100:2 + rt vpn both 100:2 + export vpn + import vpn + exit-address-family +! +route-map r1 permit 10 + set as-path prepend 65001 +! diff --git a/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/test_bgp_vpnv4_import_allowas_in_between_vrf.py b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/test_bgp_vpnv4_import_allowas_in_between_vrf.py new file mode 100644 index 0000000000..23325c7a17 --- /dev/null +++ b/tests/topotests/bgp_vpnv4_import_allowas_in_between_vrf/test_bgp_vpnv4_import_allowas_in_between_vrf.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC + +# +# Copyright (c) 2024 by +# Donatas Abraitis <donatas@opensourcerouting.org> +# + +import os +import sys +import json +import pytest +import functools + +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# pylint: disable=C0413 +from lib import topotest +from lib.topogen import Topogen, get_topogen + +pytestmark = [pytest.mark.bgpd] + + +def build_topo(tgen): + tgen.add_router("r1") + tgen.add_router("r2") + + switch = tgen.add_switch("s1") + switch.add_link(tgen.gears["r1"]) + switch.add_link(tgen.gears["r2"]) + + switch = tgen.add_switch("s2") + switch.add_link(tgen.gears["r1"]) + + switch = tgen.add_switch("s3") + switch.add_link(tgen.gears["r2"]) + + +def setup_module(mod): + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + r1 = tgen.gears["r1"] + r2 = tgen.gears["r2"] + + r1.run("ip link add CUSTOMER-A type vrf table 1001") + r1.run("ip link set up dev CUSTOMER-A") + r1.run("ip link set r1-eth0 master CUSTOMER-A") + + r1.run("ip link add CUSTOMER-B type vrf table 1002") + r1.run("ip link set up dev CUSTOMER-B") + r1.run("ip link set r1-eth1 master CUSTOMER-B") + + r2.run("ip link add CUSTOMER-A type vrf table 1001") + r2.run("ip link set up dev CUSTOMER-A") + r2.run("ip link set r2-eth0 master CUSTOMER-A") + + r2.run("ip link add CUSTOMER-B type vrf table 1002") + r2.run("ip link set up dev CUSTOMER-B") + r2.run("ip link set r2-eth1 master CUSTOMER-B") + + router_list = tgen.routers() + + for _, (rname, router) in enumerate(router_list.items(), 1): + router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname))) + + tgen.start_router() + + +def teardown_module(mod): + tgen = get_topogen() + tgen.stop_topology() + + +def test_bgp_vpnv4_import_allowas_in_between_vrf(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r1 = tgen.gears["r1"] + + def _bgp_converge(): + output = json.loads( + r1.vtysh_cmd("show bgp vrf CUSTOMER-A ipv4 unicast 10.10.10.10/32 json") + ) + expected = { + "paths": [ + { + "aspath": { + "string": "65002 65001", + }, + "valid": True, + } + ] + } + return topotest.json_cmp(output, expected) + + test_func = functools.partial(_bgp_converge) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) + assert result is None, "Failed to see 10.10.10.10/32 with a valid next-hop" + + def _vrf_route_imported_to_vrf(): + output = json.loads( + r1.vtysh_cmd("show ip route vrf CUSTOMER-B 10.10.10.10/32 json") + ) + expected = { + "10.10.10.10/32": [ + { + "protocol": "bgp", + "vrfName": "CUSTOMER-B", + "selected": True, + "installed": True, + "table": 1002, + "internalNextHopNum": 1, + "internalNextHopActiveNum": 1, + "nexthops": [ + { + "fib": True, + "ip": "192.168.179.5", + "afi": "ipv4", + "interfaceName": "r1-eth0", + "vrf": "CUSTOMER-A", + "active": True, + } + ], + } + ] + } + return topotest.json_cmp(output, expected) + + test_func = functools.partial(_vrf_route_imported_to_vrf) + _, result = topotest.run_and_expect(test_func, None, count=30, wait=1) + assert ( + result is None + ), "Failed to see 10.10.10.10/32 to be imported into CUSTOMER-B VRF (Zebra)" + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/munet/base.py b/tests/topotests/munet/base.py index e77eb15dc8..e9410d442d 100644 --- a/tests/topotests/munet/base.py +++ b/tests/topotests/munet/base.py @@ -332,6 +332,10 @@ class Commander: # pylint: disable=R0904 self.last = None self.exec_paths = {} + # For running commands one time only (deals with asyncio) + self.cmd_once_done = {} + self.cmd_once_locks = {} + if not logger: logname = f"munet.{self.__class__.__name__.lower()}.{name}" self.logger = logging.getLogger(logname) @@ -1189,7 +1193,7 @@ class Commander: # pylint: disable=R0904 return stdout # Run a command in a new window (gnome-terminal, screen, tmux, xterm) - def run_in_window( + def run_in_window( # pylint: disable=too-many-positional-arguments self, cmd, wait_for=False, @@ -1205,7 +1209,7 @@ class Commander: # pylint: disable=R0904 Args: cmd: string to execute. - wait_for: True to wait for exit from command or `str` as channel neme to + wait_for: True to wait for exit from command or `str` as channel name to signal on exit, otherwise False background: Do not change focus to new window. title: Title for new pane (tmux) or window (xterm). @@ -1405,6 +1409,26 @@ class Commander: # pylint: disable=R0904 return pane_info + async def async_cmd_raises_once(self, cmd, **kwargs): + if cmd in self.cmd_once_done: + return self.cmd_once_done[cmd] + + if cmd not in self.cmd_once_locks: + self.cmd_once_locks[cmd] = asyncio.Lock() + + async with self.cmd_once_locks[cmd]: + if cmd not in self.cmd_once_done: + self.logger.info("Running command once: %s", cmd) + self.cmd_once_done[cmd] = await commander.async_cmd_raises( + cmd, **kwargs + ) + return self.cmd_once_done[cmd] + + def cmd_raises_once(self, cmd, **kwargs): + if cmd not in self.cmd_once_done: + self.cmd_once_done[cmd] = commander.cmd_raises(cmd, **kwargs) + return self.cmd_once_done[cmd] + def delete(self): """Calls self.async_delete within an exec loop.""" asyncio.run(self.async_delete()) diff --git a/tests/topotests/munet/munet-schema.json b/tests/topotests/munet/munet-schema.json index 6ebc368dcb..44453cb44f 100644 --- a/tests/topotests/munet/munet-schema.json +++ b/tests/topotests/munet/munet-schema.json @@ -117,6 +117,12 @@ "bios": { "type": "string" }, + "cloud-init": { + "type": "boolean" + }, + "cloud-init-disk": { + "type": "string" + }, "disk": { "type": "string" }, @@ -129,7 +135,7 @@ "initial-cmd": { "type": "string" }, - "kerenel": { + "kernel": { "type": "string" }, "initrd": { @@ -373,6 +379,12 @@ "networks-autonumber": { "type": "boolean" }, + "initial-setup-cmd": { + "type": "string" + }, + "initial-setup-host-cmd": { + "type": "string" + }, "networks": { "type": "array", "items": { @@ -452,6 +464,12 @@ "bios": { "type": "string" }, + "cloud-init": { + "type": "boolean" + }, + "cloud-init-disk": { + "type": "string" + }, "disk": { "type": "string" }, @@ -464,7 +482,7 @@ "initial-cmd": { "type": "string" }, - "kerenel": { + "kernel": { "type": "string" }, "initrd": { diff --git a/tests/topotests/munet/mutest/userapi.py b/tests/topotests/munet/mutest/userapi.py index abc63af365..e367e65a15 100644 --- a/tests/topotests/munet/mutest/userapi.py +++ b/tests/topotests/munet/mutest/userapi.py @@ -180,7 +180,7 @@ class TestCase: # sum_hfmt = "{:5.5s} {:4.4s} {:>6.6s} {}" # sum_dfmt = "{:5s} {:4.4s} {:^6.6s} {}" - sum_fmt = "%-8.8s %4.4s %{}s %6s %s" + sum_fmt = "%-10s %4.4s %{}s %6s %s" def __init__( self, diff --git a/tests/topotests/munet/native.py b/tests/topotests/munet/native.py index e3b782396e..4e29fe91b1 100644 --- a/tests/topotests/munet/native.py +++ b/tests/topotests/munet/native.py @@ -24,6 +24,13 @@ import time from pathlib import Path + +try: + # We only want to require yaml for the gen cloud image feature + import yaml +except ImportError: + pass + from . import cli from .base import BaseMunet from .base import Bridge @@ -749,9 +756,11 @@ class L3NodeMixin(NodeMixin): # Disable IPv6 self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=0") self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=1") + self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=0") else: self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=1") self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=0") + self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=1") self.next_p2p_network = ipaddress.ip_network(f"10.254.{self.id}.0/31") self.next_p2p_network6 = ipaddress.ip_network(f"fcff:ffff:{self.id:02x}::/127") @@ -2265,6 +2274,164 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace): tid = self.cpu_thread_map[i] self.cmd_raises_nsonly(f"taskset -cp {aff} {tid}") + def _gen_network_config(self): + intfs = sorted(self.intfs) + if not intfs: + return "" + + self.logger.debug("Generating cloud-init interface config") + config = {} + config["version"] = 2 + enets = config["ethernets"] = {} + + for ifname in sorted(self.intfs): + self.logger.debug("Interface %s", ifname) + conn = find_with_kv(self.config["connections"], "name", ifname) + + index = self.config["connections"].index(conn) + to = conn["to"] + switch = self.unet.switches.get(to) + mtu = conn.get("mtu") + if not mtu and switch: + mtu = switch.config.get("mtu") + + devaddr = conn.get("physical", "") + # Eventually we should get the MAC from /sys + if not devaddr: + mac = self.tapmacs.get(ifname, f"02:aa:aa:aa:{index:02x}:{self.id:02x}") + nic = { + "match": {"macaddress": str(mac)}, + "set-name": ifname, + } + if mtu: + nic["mtu"] = str(mtu) + enets[f"nic-{ifname}"] = nic + + ifaddr4 = self.get_intf_addr(ifname, ipv6=False) + ifaddr6 = self.get_intf_addr(ifname, ipv6=True) + if not ifaddr4 and not ifaddr6: + continue + net = { + "dhcp4": False, + "dhcp6": False, + "accept-ra": False, + "addresses": [], + } + if ifaddr4: + net["addresses"].append(str(ifaddr4)) + if ifaddr6: + net["addresses"].append(str(ifaddr6)) + if switch and hasattr(switch, "is_nat") and switch.is_nat: + net["nameservers"] = {"addresses": []} + nameservers = net["nameservers"]["addresses"] + if hasattr(switch, "ip6_address"): + net["gateway6"] = str(switch.ip6_address) + nameservers.append("2001:4860:4860::8888") + if switch.ip_address: + net["gateway4"] = str(switch.ip_address) + nameservers.append("8.8.8.8") + enets[ifname] = net + + return yaml.safe_dump(config) + + def _gen_cloud_init(self): + qc = self.qemu_config + cc = qc.get("console", {}) + cipath = self.rundir.joinpath("cloud-init.img") + + geniso = get_exec_path_host("genisoimage") + if not geniso: + mfbin = get_exec_path_host("mkfs.vfat") + mcbin = get_exec_path_host("mcopy") + assert ( + mfbin and mcbin + ), "genisoimage or mkfs.vfat,mcopy needed to gen cloud-init disk" + + # + # cloud-init: meta-data + # + mdata = f""" +instance-id: "munet-{self.id}" +local-hostname: "{self.name}" +""" + # + # cloud-init: user-data + # + ssh_auth_s = "" + if bool(self.ssh_keyfile): + pubkey = commander.cmd_raises(f"ssh-keygen -y -f {self.ssh_keyfile}") + assert pubkey, f"Can't extract public key from {self.ssh_keyfile}" + pubkey = pubkey.strip() + ssh_auth_s = f'ssh_authorized_keys: ["{pubkey}"]' + + user = cc.get("user", "root") + password = cc.get("password", "admin") + if user != "root": + root_password = "admin" + else: + root_password = password + + udata = f"""#cloud-config +disable_root: 0 +ssh_pwauth: 1 +hostname: {self.name} +runcmd: + - systemctl enable serial-getty@ttyS1.service + - systemctl start serial-getty@ttyS1.service + - systemctl enable serial-getty@ttyS2.service + - systemctl start serial-getty@ttyS2.service + - systemctl enable serial-getty@hvc0.service + - systemctl start serial-getty@hvc0.service + - systemctl enable serial-getty@hvc1.service + - systemctl start serial-getty@hvc1.service +users: + - name: root + lock_passwd: false + plain_text_passwd: "{root_password}" + {ssh_auth_s} +""" + if user != "root": + udata += """ + - name: {user} + lock_passwd: false + plain_text_passwd: "{password}" + {ssh_auth_s} +""" + # + # cloud-init: network-config + # + ndata = self._gen_network_config() + + # + # Generate cloud-init files + # + cidir = self.rundir.joinpath("ci-data") + commander.cmd_raises(f"mkdir -p {cidir}") + + with open(cidir.joinpath("meta-data"), "w+", encoding="utf-8") as f: + f.write(mdata) + with open(cidir.joinpath("user-data"), "w+", encoding="utf-8") as f: + f.write(udata) + files = "meta-data user-data" + if ndata: + files += " network-config" + with open(cidir.joinpath("network-config"), "w+", encoding="utf-8") as f: + f.write(ndata) + if geniso: + commander.cmd_raises( + f"cd {cidir} && " + f'genisoimage -output "{cipath}" -volid cidata' + f" -joliet -rock {files}" + ) + else: + commander.cmd_raises(f'cd {cidir} && mkfs.vfat -n cidata "{cipath}"') + commander.cmd_raises(f'cd {cidir} && mcopy -oi "{cipath}" {files}') + + # + # Generate cloud-init disk + # + return cipath + async def launch(self): """Launch qemu.""" self.logger.info("%s: Launch Qemu", self) @@ -2367,11 +2534,21 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace): diskpath = os.path.join(self.unet.config_dirname, diskpath) if dtpl and (not disk or not os.path.exists(diskpath)): + basename = os.path.basename(dtpl) + confdir = self.unet.config_dirname + if re.match("(https|http|ftp|tftp):.*", dtpl): + await self.unet.async_cmd_raises_once( + f"cd {confdir} && (test -e {basename} || curl -fLO {dtpl})" + ) + dtplpath = os.path.join(confdir, basename) + if not disk: - disk = qc["disk"] = f"{self.name}-{os.path.basename(dtpl)}" + disk = qc["disk"] = f"{self.name}-{basename}" diskpath = os.path.join(self.rundir, disk) + if self.path_exists(diskpath): logging.debug("Disk '%s' file exists, using.", diskpath) + else: if dtplpath[0] != "/": dtplpath = os.path.join(self.unet.config_dirname, dtpl) @@ -2392,11 +2569,15 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace): args.extend(["-device", "ahci,id=ahci"]) args.extend(["-device", "ide-hd,bus=ahci.0,drive=sata-disk0"]) - cidiskpath = qc.get("cloud-init-disk") - if cidiskpath: - if cidiskpath[0] != "/": - cidiskpath = os.path.join(self.unet.config_dirname, cidiskpath) - args.extend(["-drive", f"file={cidiskpath},if=virtio,format=qcow2"]) + if qc.get("cloud-init"): + cidiskpath = qc.get("cloud-init-disk") + if cidiskpath: + if cidiskpath[0] != "/": + cidiskpath = os.path.join(self.unet.config_dirname, cidiskpath) + else: + cidiskpath = self._gen_cloud_init() + diskfmt = "qcow2" if str(cidiskpath).endswith("qcow2") else "raw" + args.extend(["-drive", f"file={cidiskpath},if=virtio,format={diskfmt}"]) # args.extend(["-display", "vnc=0.0.0.0:40"]) @@ -2488,7 +2669,7 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace): if use_cmdcon: confiles.append("_cmdcon") - password = cc.get("password", "") + password = cc.get("password", "admin") if self.disk_created: password = cc.get("initial-password", password) @@ -2764,9 +2945,11 @@ ff02::2\tip6-allrouters # Disable IPv6 self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=0") self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=1") + self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=0") else: self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=1") self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=0") + self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=1") # we really need overlay, but overlay-layers (used by overlay-images) # counts on things being present in overlay so this temp stuff doesn't work. @@ -2774,6 +2957,24 @@ ff02::2\tip6-allrouters # # Let's hide podman details # self.tmpfs_mount("/var/lib/containers/storage/overlay-containers") + def run_init_cmds(unet, key, on_host): + cmds = unet.topoconf.get(key, "") + cmds = cmds.replace("%CONFIGDIR%", str(unet.config_dirname)) + cmds = cmds.replace("%RUNDIR%", str(unet.rundir)) + cmds = cmds.strip() + if not cmds: + return + + cmds += "\n" + c = commander if on_host else unet + o = c.cmd_raises(cmds) + self.logger.debug( + "run_init_cmds (on-host: %s): %s", on_host, cmd_error(0, o, "") + ) + + run_init_cmds(self, "initial-setup-host-cmd", True) + run_init_cmds(self, "initial-setup-cmd", False) + shellopt = self.cfgopt.getoption("--shell") shellopt = shellopt if shellopt else "" if shellopt == "all" or "." in shellopt.split(","): @@ -3061,7 +3262,8 @@ done""" if not rc: continue logging.info("Pulling missing image %s", image) - aw = self.rootcmd.async_cmd_raises(f"podman pull {image}") + + aw = self.rootcmd.async_cmd_raises_once(f"podman pull {image}") tasks.append(asyncio.create_task(aw)) if not tasks: return diff --git a/tests/topotests/munet/testing/util.py b/tests/topotests/munet/testing/util.py index 99687c0a83..02ff9bd69e 100644 --- a/tests/topotests/munet/testing/util.py +++ b/tests/topotests/munet/testing/util.py @@ -8,12 +8,17 @@ """Utility functions useful when using munet testing functionailty in pytest.""" import asyncio import datetime +import fcntl import functools import logging +import os +import re +import select import sys import time from ..base import BaseMunet +from ..base import Timeout from ..cli import async_cli @@ -23,6 +28,7 @@ from ..cli import async_cli async def async_pause_test(desc=""): + """Pause the running of a test offering options for CLI or PDB.""" isatty = sys.stdout.isatty() if not isatty: desc = f" for {desc}" if desc else "" @@ -49,11 +55,12 @@ async def async_pause_test(desc=""): def pause_test(desc=""): + """Pause the running of a test offering options for CLI or PDB.""" asyncio.run(async_pause_test(desc)) def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True): - """decorator: retry while functions return is not None or raises an exception. + """Retry decorated function until it returns None, raises an exception, or timeout. * `retry_timeout`: Retry for at least this many seconds; after waiting initial_wait seconds @@ -116,3 +123,91 @@ def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True): return func_retry return _retry + + +def readline(f, timeout=None): + """Read a line or timeout. + + This function will take over the file object, the file object should not be used + outside of calling this function once you begin. + + Return: A line, remaining buffer if EOF (subsequent calls will return ""), or None + for timeout. + """ + fd = f.fileno() + if not hasattr(f, "munet_non_block_set"): + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + f.munet_non_block_set = True + f.munet_lines = [] + f.munet_buf = "" + + if f.munet_lines: + return f.munet_lines.pop(0) + + timeout = Timeout(timeout) + remaining = timeout.remaining() + while remaining > 0: + ready, _, _ = select.select([fd], [], [], remaining) + if not ready: + return None + + c = f.read() + if c is None: + logging.error("munet readline: unexpected None during read") + return None + + if not c: + logging.debug("munet readline: got eof") + c = f.munet_buf + f.munet_buf = "" + return c + + f.munet_buf += c + while "\n" in f.munet_buf: + a, f.munet_buf = f.munet_buf.split("\n", 1) + f.munet_lines.append(a + "\n") + + if f.munet_lines: + return f.munet_lines.pop(0) + + remaining = timeout.remaining() + return None + + +def waitline(f, regex, timeout=120): + """Match a regex within lines from a file with a timeout. + + This function will take over the file object (by calling `readline` above), the file + object should not be used outside of calling these functions once you begin. + + Return: the match object or None. + """ + timeo = Timeout(timeout) + while not timeo.is_expired(): + line = readline(f, timeo.remaining()) + if line is None: + break + + if line == "": + logging.warning("waitline: got eof while matching '%s'", regex) + return None + + assert line[-1] == "\n" + line = line[:-1] + if not line: + continue + + logging.debug("waitline: searching: '%s' for '%s'", line, regex) + m = re.search(regex, line) + if m: + logging.debug("waitline: matched '%s'", m.group(0)) + return m + + logging.warning( + "Timeout while getting output matching '%s' within %ss (actual %ss)", + regex, + timeout, + timeo.elapsed(), + ) + return None diff --git a/tests/topotests/pim_boundary_acl/test_pim_boundary_acl.py b/tests/topotests/pim_boundary_acl/test_pim_boundary_acl.py index 1488e610c8..2a77c3b223 100644 --- a/tests/topotests/pim_boundary_acl/test_pim_boundary_acl.py +++ b/tests/topotests/pim_boundary_acl/test_pim_boundary_acl.py @@ -135,12 +135,10 @@ def test_pim_asm_igmp_join_acl(): expected = { "r1-eth0":{ "name":"r1-eth0", - "224.0.1.40":"*", "229.1.1.1":None }, "r1-eth2":{ "name":"r1-eth2", - "224.0.1.40":"*", "229.1.1.1":None } } @@ -166,9 +164,7 @@ def test_pim_asm_igmp_join_acl(): "sources":[ { "source":"*", - "timer":"--:--", "forwarded":False, - "uptime":"*" } ] } @@ -227,8 +223,6 @@ def test_pim_asm_igmp_join_acl(): "source":"*", "group":"229.1.1.1", "primaryAddr":"10.0.20.2", - "sockFd":"*", - "upTime":"*" } ] } @@ -286,13 +280,11 @@ def test_pim_ssm_igmp_join_acl(): expected = { "r1-eth0":{ "name":"r1-eth0", - "224.0.1.40":"*", "229.1.1.1":None, "232.1.1.1":None }, "r1-eth2":{ "name":"r1-eth2", - "224.0.1.40":"*", "229.1.1.1":None, "232.1.1.1":None } @@ -319,9 +311,7 @@ def test_pim_ssm_igmp_join_acl(): "sources":[ { "source":"10.0.20.2", - "timer":"*", "forwarded":False, - "uptime":"*" } ] } @@ -397,9 +387,7 @@ def test_pim_ssm_igmp_join_acl(): "sources":[ { "source":"10.0.20.2", - "timer":"*", "forwarded":False, - "uptime":"*" } ] } @@ -422,8 +410,6 @@ def test_pim_ssm_igmp_join_acl(): "source":"10.0.20.2", "group":"232.1.1.1", "primaryAddr":"10.0.20.2", - "sockFd":"*", - "upTime":"*" } ] } @@ -491,9 +477,7 @@ def test_pim_ssm_igmp_join_acl(): "sources":[ { "source":"10.0.40.4", - "timer":"*", "forwarded":False, - "uptime":"*" } ] } |
