summaryrefslogtreecommitdiffstats
path: root/tests/topotests/lib
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 04:24:32 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 04:24:32 +0000
commit35cadacd2bb9383686753731e31bd7e145fb2506 (patch)
tree4489adbde75a837989533837185b2b8369a0bf68 /tests/topotests/lib
parentAdding debian version 9.1-0.1. (diff)
downloadfrr-35cadacd2bb9383686753731e31bd7e145fb2506.tar.xz
frr-35cadacd2bb9383686753731e31bd7e145fb2506.zip
Merging upstream version 10.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/topotests/lib')
-rw-r--r--tests/topotests/lib/bgpcheck.py63
-rw-r--r--tests/topotests/lib/bmp_collector/bmp.py3
-rw-r--r--tests/topotests/lib/common_config.py7
-rwxr-xr-xtests/topotests/lib/fe_client.py420
-rw-r--r--tests/topotests/lib/snmptest.py156
-rw-r--r--tests/topotests/lib/topogen.py47
-rw-r--r--tests/topotests/lib/topotest.py226
7 files changed, 874 insertions, 48 deletions
diff --git a/tests/topotests/lib/bgpcheck.py b/tests/topotests/lib/bgpcheck.py
new file mode 100644
index 0000000..5ca35a5
--- /dev/null
+++ b/tests/topotests/lib/bgpcheck.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Copyright 2023, 6wind
+import json
+
+from lib import topotest
+
+
+def check_show_bgp_vpn_prefix_not_found(router, ipversion, prefix, rd, label=None):
+ """
+ Check if a given vpn prefix is not present in the BGP RIB
+ * 'router': the router to check BGP VPN RIB
+ * 'ipversion': The ip version to check: ipv4 or ipv6
+ * 'prefix': the IP prefix to check
+ * 'rd': the route distinguisher to check
+ * 'label: the label to check
+ """
+ output = json.loads(
+ router.vtysh_cmd("show bgp {} vpn {} json".format(ipversion, prefix))
+ )
+ if label:
+ expected = {rd: {"prefix": prefix, "paths": [{"remoteLabel": label}]}}
+ else:
+ expected = {rd: {"prefix": prefix}}
+ ret = topotest.json_cmp(output, expected)
+ if ret is None:
+ return "not good"
+ return None
+
+
+def check_show_bgp_vpn_prefix_found(
+ router, ipversion, prefix, rd, label=None, nexthop=None
+):
+ """
+ Check if a given vpn prefix is present in the BGP RIB
+ * 'router': the router to check BGP VPN RIB
+ * 'ipversion': The ip version to check: ipv4 or ipv6
+ * 'prefix': the IP prefix to check
+ * 'rd': the route distinguisher to check
+ * 'label: the label to check
+ """
+ output = json.loads(
+ router.vtysh_cmd("show bgp {} vpn {} json".format(ipversion, prefix))
+ )
+ if label:
+ if nexthop:
+ expected = {
+ rd: {
+ "prefix": prefix,
+ "paths": [{"remoteLabel": label, "nexthops": [{"ip": nexthop}]}],
+ }
+ }
+ else:
+ expected = {rd: {"prefix": prefix, "paths": [{"remoteLabel": label}]}}
+ else:
+ if nexthop:
+ expected = {
+ rd: {"prefix": prefix, "paths": [{"nexthops": [{"ip": nexthop}]}]}
+ }
+ else:
+ expected = {rd: {"prefix": prefix}}
+ return topotest.json_cmp(output, expected)
diff --git a/tests/topotests/lib/bmp_collector/bmp.py b/tests/topotests/lib/bmp_collector/bmp.py
index b07329c..57f642a 100644
--- a/tests/topotests/lib/bmp_collector/bmp.py
+++ b/tests/topotests/lib/bmp_collector/bmp.py
@@ -252,6 +252,7 @@ class BMPPerPeerMessage:
if peer_type == 0x03:
msg['is_filtered'] = bool(peer_flags & IS_FILTERED)
+ msg['policy'] = 'loc-rib'
else:
# peer_flags = 0x0000 0000
# ipv6, post-policy, as-path, adj-rib-out, reserverdx4
@@ -259,7 +260,7 @@ class BMPPerPeerMessage:
is_as_path = bool(peer_flags & IS_AS_PATH)
is_post_policy = bool(peer_flags & IS_POST_POLICY)
is_ipv6 = bool(peer_flags & IS_IPV6)
- msg['post_policy'] = is_post_policy
+ msg['policy'] = 'post-policy' if is_post_policy else 'pre-policy'
msg['ipv6'] = is_ipv6
msg['peer_ip'] = bin2str_ipaddress(peer_address, is_ipv6)
diff --git a/tests/topotests/lib/common_config.py b/tests/topotests/lib/common_config.py
index e19d96f..598db84 100644
--- a/tests/topotests/lib/common_config.py
+++ b/tests/topotests/lib/common_config.py
@@ -14,6 +14,7 @@ import socket
import subprocess
import sys
import traceback
+import configparser
from collections import OrderedDict
from copy import deepcopy
from datetime import datetime, timedelta
@@ -21,12 +22,6 @@ from functools import wraps
from re import search as re_search
from time import sleep
-try:
- # Imports from python2
- import ConfigParser as configparser
-except ImportError:
- # Imports from python3
- import configparser
from lib.micronet import comm_error
from lib.topogen import TopoRouter, get_topogen
diff --git a/tests/topotests/lib/fe_client.py b/tests/topotests/lib/fe_client.py
new file mode 100755
index 0000000..07059cc
--- /dev/null
+++ b/tests/topotests/lib/fe_client.py
@@ -0,0 +1,420 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 eval: (blacken-mode 1) -*-
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# November 27 2023, Christian Hopps <chopps@labn.net>
+#
+# Copyright (c) 2023, LabN Consulting, L.L.C.
+#
+# noqa: E501
+#
+import argparse
+import json
+import logging
+import os
+import socket
+import struct
+import sys
+import time
+from pathlib import Path
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+
+# This is painful but works if you have installed protobuf would be better if we
+# actually built and installed these but ... python packaging.
+try:
+ sys.path.append(os.path.dirname(CWD))
+ from munet.base import commander
+
+ commander.cmd_raises(f"protoc --python_out={CWD} -I {CWD}/../../../lib mgmt.proto")
+except Exception as error:
+ logging.error("can't create protobuf definition modules %s", error)
+ raise
+
+try:
+ sys.path[0:0] = "."
+ import mgmt_pb2
+except Exception as error:
+ logging.error("can't import proto definition modules %s", error)
+ raise
+
+CANDIDATE_DS = mgmt_pb2.DatastoreId.CANDIDATE_DS
+OPERATIONAL_DS = mgmt_pb2.DatastoreId.OPERATIONAL_DS
+RUNNING_DS = mgmt_pb2.DatastoreId.RUNNING_DS
+STARTUP_DS = mgmt_pb2.DatastoreId.STARTUP_DS
+
+# =====================
+# Native message values
+# =====================
+
+MGMT_MSG_MARKER_PROTOBUF = b"\000###"
+MGMT_MSG_MARKER_NATIVE = b"\001###"
+
+#
+# Native message formats
+#
+MSG_HDR_FMT = "=H2xIQQ"
+HDR_FIELD_CODE = 0
+HDR_FIELD_VSPLIT = 1
+HDR_FIELD_SESS_ID = 2
+HDR_FIELD_REQ_ID = 3
+
+MSG_ERROR_FMT = "=h6x"
+ERROR_FIELD_ERROR = 0
+
+# MSG_GET_TREE_FMT = "=B7x"
+# GET_TREE_FIELD_RESULT_TYPE = 0
+
+MSG_TREE_DATA_FMT = "=bBB5x"
+TREE_DATA_FIELD_PARTIAL_ERROR = 0
+TREE_DATA_FIELD_RESULT_TYPE = 1
+TREE_DATA_FIELD_MORE = 2
+
+MSG_GET_DATA_FMT = "=BB6x"
+GET_DATA_FIELD_RESULT_TYPE = 0
+GET_DATA_FIELD_FLAGS = 1
+GET_DATA_FLAG_STATE = 0x1
+GET_DATA_FLAG_CONFIG = 0x2
+GET_DATA_FLAG_EXACT = 0x4
+
+MSG_NOTIFY_FMT = "=B7x"
+NOTIFY_FIELD_RESULT_TYPE = 0
+
+#
+# Native message codes
+#
+MSG_CODE_ERROR = 0
+# MSG_CODE_GET_TREE = 1
+MSG_CODE_TREE_DATA = 2
+MSG_CODE_GET_DATA = 3
+MSG_CODE_NOTIFY = 4
+
+msg_native_formats = {
+ MSG_CODE_ERROR: MSG_ERROR_FMT,
+ # MSG_CODE_GET_TREE: MSG_GET_TREE_FMT,
+ MSG_CODE_TREE_DATA: MSG_TREE_DATA_FMT,
+ MSG_CODE_GET_DATA: MSG_GET_DATA_FMT,
+ MSG_CODE_NOTIFY: MSG_NOTIFY_FMT,
+}
+
+
+# Result formats
+MSG_FORMAT_XML = 1
+MSG_FORMAT_JSON = 2
+MSG_FORMAT_LYB = 3
+
+
+def cstr(mdata):
+ assert mdata[-1] == 0
+ return mdata[:-1]
+
+
+class FEClientError(Exception):
+ pass
+
+
+class PBMessageError(FEClientError):
+ def __init__(self, msg, errstr):
+ self.msg = msg
+ # self.sess_id = mhdr[HDR_FIELD_SESS_ID]
+ # self.req_id = mhdr[HDR_FIELD_REQ_ID]
+ self.error = -1
+ self.errstr = errstr
+ super().__init__(f"PBMessageError: {self.errstr}: {msg}")
+
+
+class NativeMessageError(FEClientError):
+ def __init__(self, mhdr, mfixed, mdata):
+ self.mhdr = mhdr
+ self.sess_id = mhdr[HDR_FIELD_SESS_ID]
+ self.req_id = mhdr[HDR_FIELD_REQ_ID]
+ self.error = mfixed[0]
+ self.errstr = cstr(mdata)
+ super().__init__(
+ "NativeMessageError: "
+ f"session {self.sess_id} reqid {self.req_id} "
+ f"error {self.error}: {self.errstr}"
+ )
+
+
+#
+# Low-level socket functions
+#
+
+
+def recv_wait(sock, size):
+ """Receive a fixed number of bytes from a stream socket."""
+ data = b""
+ while len(data) < size:
+ newdata = sock.recv(size - len(data))
+ if not newdata:
+ raise Exception("Socket closed")
+ data += newdata
+ return data
+
+
+def recv_msg(sock):
+ marker = recv_wait(sock, 4)
+ assert marker in (MGMT_MSG_MARKER_PROTOBUF, MGMT_MSG_MARKER_NATIVE)
+
+ msize = int.from_bytes(recv_wait(sock, 4), byteorder=sys.byteorder)
+ assert msize >= 8
+ mdata = recv_wait(sock, msize - 8) if msize > 8 else b""
+
+ return mdata, marker == MGMT_MSG_MARKER_NATIVE
+
+
+def send_msg(sock, marker, mdata):
+ """Send a mgmtd native message to a stream socket."""
+ msize = int.to_bytes(len(mdata) + 8, byteorder=sys.byteorder, length=4)
+ sock.send(marker)
+ sock.send(msize)
+ sock.send(mdata)
+
+
+class Session:
+ """A session to the mgmtd server."""
+
+ client_id = 1
+
+ def __init__(self, sock):
+ self.sock = sock
+ self.next_req_id = 1
+
+ req = mgmt_pb2.FeMessage()
+ req.register_req.client_name = "test-client"
+ self.send_pb_msg(req)
+ logging.debug("Sent FeRegisterReq: %s", req)
+
+ req = mgmt_pb2.FeMessage()
+ req.session_req.create = 1
+ req.session_req.client_conn_id = Session.client_id
+ Session.client_id += 1
+ self.send_pb_msg(req)
+ logging.debug("Sent FeSessionReq: %s", req)
+
+ reply = self.recv_pb_msg(mgmt_pb2.FeMessage())
+ logging.debug("Received FeSessionReply: %s", repr(reply))
+
+ assert reply.session_reply.success
+ self.sess_id = reply.session_reply.session_id
+
+ def close(self, clean=True):
+ if clean:
+ req = mgmt_pb2.FeMessage()
+ req.session_req.create = 0
+ req.session_req.sess_id = self.sess_id
+ self.send_pb_msg(req)
+ self.sock.close()
+ self.sock = None
+
+ def get_next_req_id(self):
+ req_id = self.next_req_id
+ self.next_req_id += 1
+ return req_id
+
+ # --------------------------
+ # Protobuf message functions
+ # --------------------------
+
+ def recv_pb_msg(self, msg):
+ """Receive a protobuf message."""
+ mdata, native = recv_msg(self.sock)
+ assert not native
+
+ msg.ParseFromString(mdata)
+
+ req = getattr(msg, msg.WhichOneof("message"))
+ if req.HasField("success"):
+ if not req.success:
+ raise PBMessageError(msg, req.error_if_any)
+
+ return msg
+
+ def send_pb_msg(self, msg):
+ """Send a protobuf message."""
+ mdata = msg.SerializeToString()
+ return send_msg(self.sock, MGMT_MSG_MARKER_PROTOBUF, mdata)
+
+ # ------------------------
+ # Native message functions
+ # ------------------------
+
+ def recv_native_msg(self):
+ """Send a native message."""
+ mdata, native = recv_msg(self.sock)
+ assert native
+
+ hlen = struct.calcsize(MSG_HDR_FMT)
+ hdata = mdata[:hlen]
+ mhdr = struct.unpack(MSG_HDR_FMT, hdata)
+ code = mhdr[0]
+
+ if code not in msg_native_formats:
+ raise Exception(f"Unknown native msg code {code} rcvd")
+
+ mfmt = msg_native_formats[code]
+ flen = struct.calcsize(mfmt)
+ fdata = mdata[hlen : hlen + flen]
+ mfixed = struct.unpack(mfmt, fdata)
+ mdata = mdata[hlen + flen :]
+
+ if code == MSG_ERROR_FMT:
+ raise NativeMessageError(mhdr, mfixed, mdata)
+
+ return mhdr, mfixed, mdata
+
+ def send_native_msg(self, mdata):
+ """Send a native message."""
+ return send_msg(self.sock, MGMT_MSG_MARKER_NATIVE, mdata)
+
+ def get_native_msg_header(self, msg_code):
+ req_id = self.get_next_req_id()
+ hdata = struct.pack(MSG_HDR_FMT, msg_code, 0, self.sess_id, req_id)
+ return hdata, req_id
+
+ # -----------------------
+ # Front-end API Fountains
+ # -----------------------
+
+ def lock(self, lock=True, ds_id=mgmt_pb2.CANDIDATE_DS):
+ req = mgmt_pb2.FeMessage()
+ req.lockds_req.session_id = self.sess_id
+ req.lockds_req.req_id = self.get_next_req_id()
+ req.lockds_req.ds_id = ds_id
+ req.lockds_req.lock = lock
+ self.send_pb_msg(req)
+ logging.debug("Sent LockDsReq: %s", req)
+
+ reply = self.recv_pb_msg(mgmt_pb2.FeMessage())
+ logging.debug("Received Reply: %s", repr(reply))
+ assert reply.lockds_reply.success
+
+ def get_data(self, query, data=True, config=False):
+ # Create the message
+ mdata, req_id = self.get_native_msg_header(MSG_CODE_GET_DATA)
+ flags = GET_DATA_FLAG_STATE if data else 0
+ flags |= GET_DATA_FLAG_CONFIG if config else 0
+ mdata += struct.pack(MSG_GET_DATA_FMT, MSG_FORMAT_JSON, flags)
+ mdata += query.encode("utf-8") + b"\x00"
+
+ self.send_native_msg(mdata)
+ logging.debug("Sent GET-TREE")
+
+ mhdr, mfixed, mdata = self.recv_native_msg()
+ assert mdata[-1] == 0
+ result = mdata[:-1].decode("utf-8")
+
+ logging.debug("Received GET: %s: %s", mfixed, mdata)
+ return result
+
+ # def subscribe(self, notif_xpath):
+ # # Create the message
+ # mdata, req_id = self.get_native_msg_header(MSG_CODE_SUBSCRIBE)
+ # mdata += struct.pack(MSG_SUBSCRIBE_FMT, MSG_FORMAT_JSON)
+ # mdata += notif_xpath.encode("utf-8") + b"\x00"
+
+ # self.send_native_msg(mdata)
+ # logging.debug("Sent SUBSCRIBE")
+
+ def recv_notify(self, xpaths=None):
+ while True:
+ logging.debug("Waiting for Notify Message")
+ mhdr, mfixed, mdata = self.recv_native_msg()
+ if mhdr[HDR_FIELD_CODE] == MSG_CODE_NOTIFY:
+ logging.debug("Received Notify Message: %s: %s", mfixed, mdata)
+ else:
+ raise Exception(f"Received NON-NOTIFY Message: {mfixed}: {mdata}")
+
+ vsplit = mhdr[HDR_FIELD_VSPLIT]
+ assert mdata[vsplit - 1] == 0
+ xpath = mdata[: vsplit - 1].decode("utf-8")
+
+ assert mdata[-1] == 0
+ result = mdata[vsplit:-1].decode("utf-8")
+
+ if not xpaths:
+ return result
+ js = json.loads(result)
+ key = [x for x in js.keys()][0]
+ for xpath in xpaths:
+ if key.startswith(xpath):
+ return result
+ logging.debug("'%s' didn't match xpath filters", key)
+
+
+def __parse_args():
+ MPATH = "/var/run/frr/mgmtd_fe.sock"
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-l", "--listen", nargs="*", metavar="XPATH", help="xpath[s] to listen for"
+ )
+ parser.add_argument(
+ "--notify-count",
+ type=int,
+ default=1,
+ help="Number of notifications to listen for 0 for infinite",
+ )
+ parser.add_argument(
+ "-b", "--both", action="store_true", help="return both config and data"
+ )
+ parser.add_argument(
+ "-c", "--config-only", action="store_true", help="return config only"
+ )
+ parser.add_argument(
+ "-q", "--query", nargs="+", metavar="XPATH", help="xpath[s] to query"
+ )
+ parser.add_argument("-s", "--server", default=MPATH, help="path to server socket")
+ parser.add_argument("-v", "--verbose", action="store_true", help="Be verbose")
+ args = parser.parse_args()
+
+ level = logging.DEBUG if args.verbose else logging.INFO
+ logging.basicConfig(level=level, format="%(asctime)s %(levelname)s: %(message)s")
+
+ return args
+
+
+def __server_connect(spath):
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ logging.debug("Connecting to server on %s", spath)
+ while ec := sock.connect_ex(str(spath)):
+ logging.warn("retry server connection in .5s (%s)", os.strerror(ec))
+ time.sleep(0.5)
+ logging.info("Connected to server on %s", spath)
+ return sock
+
+
+def __main():
+ args = __parse_args()
+ sock = __server_connect(Path(args.server))
+ sess = Session(sock)
+
+ if args.query:
+ # Performa an xpath query
+ # query = "/frr-interface:lib/interface/state/mtu"
+ for query in args.query:
+ logging.info("Sending query: %s", query)
+ result = sess.get_data(
+ query, data=not args.config_only, config=(args.both or args.config_only)
+ )
+ print(result)
+
+ if args.listen is not None:
+ i = args.notify_count
+ while i > 0 or args.notify_count == 0:
+ notif = sess.recv_notify(args.listen)
+ print(notif)
+ i -= 1
+
+
+def main():
+ try:
+ __main()
+ except KeyboardInterrupt:
+ logging.info("Exiting")
+ except Exception as error:
+ logging.error("Unexpected error exiting: %s", error, exc_info=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/topotests/lib/snmptest.py b/tests/topotests/lib/snmptest.py
index e7cd657..814813f 100644
--- a/tests/topotests/lib/snmptest.py
+++ b/tests/topotests/lib/snmptest.py
@@ -18,6 +18,7 @@ Basic usage instructions:
"""
from lib.topolog import logger
+import re
class SnmpTester(object):
@@ -72,16 +73,6 @@ class SnmpTester(object):
# third token onwards is the value of the object
return tokens[0].split(".", 1)[1]
- @staticmethod
- def _get_snmp_oid(snmp_output):
- tokens = snmp_output.strip().split()
-
- # if len(tokens) > 5:
- # return None
-
- # third token is the value of the object
- return tokens[0].split(".", 1)[1]
-
def _parse_multiline(self, snmp_output):
results = snmp_output.strip().split("\n")
@@ -116,6 +107,151 @@ class SnmpTester(object):
result = self.router.cmd(cmd)
return self._parse_multiline(result)
+ def parse_notif_ipv4(self, notif):
+ # normalise values
+ notif = re.sub(":", "", notif)
+ notif = re.sub('"([0-9]{2}) ([0-9]{2}) "', r"\1\2", notif)
+ notif = re.sub('"([0-9]{2}) "', r"\1", notif)
+ elems = re.findall(r"([0-9,\.]+) = ([0-9,\.]+)", notif)
+
+ # remove common part
+ elems = elems[1:]
+ return elems
+
+ def is_notif_bgp4_valid(self, output_list, address):
+ oid_notif_type = ".1.3.6.1.6.3.1.1.4.1.0"
+ peer_notif_established = ".1.3.6.1.2.1.15.0.1"
+ peer_notif_backward = ".1.3.6.1.2.1.15.0.2"
+ oid_peer_last_error = ".1.3.6.1.2.1.15.3.1.14"
+ oid_peer_remote_addr = ".1.3.6.1.2.1.15.3.1.7"
+ oid_peer_state = ".1.3.6.1.2.1.15.3.1.2"
+
+ nb_notif = len(output_list)
+ for nb in range(0, nb_notif - 1):
+ # identify type of notification
+ # established or BackwardTransition
+
+ if output_list[nb][0][0] != "{}".format(oid_notif_type):
+ return False
+
+ if output_list[nb][0][1] == "{}".format(peer_notif_established):
+ logger.info("Established notification")
+ elif output_list[nb][0][1] == "{}".format(peer_notif_backward):
+ logger.info("Backward transition notification")
+ else:
+ return False
+
+ # same behavior for 2 notification type in bgp4
+ if output_list[nb][1][0] != "{}.{}".format(oid_peer_remote_addr, address):
+ return False
+
+ if output_list[nb][2][0] != "{}.{}".format(oid_peer_last_error, address):
+ return False
+ if output_list[nb][3][0] != "{}.{}".format(oid_peer_state, address):
+ return False
+
+ return True
+
+ def is_notif_bgp4v2_valid(self, output_list, address, type_requested):
+ oid_notif_type = ".1.3.6.1.6.3.1.1.4.1.0"
+ peer_notif_established = ".1.3.6.1.3.5.1.0.1"
+ peer_notif_backward = ".1.3.6.1.3.5.1.0.2"
+ oid_peer_state = ".1.3.6.1.3.5.1.1.2.1.13"
+ oid_peer_local_port = ".1.3.6.1.3.5.1.1.2.1.6"
+ oid_peer_remote_port = ".1.3.6.1.3.5.1.1.2.1.9"
+ oid_peer_err_code_recv = ".1.3.6.1.3.5.1.1.3.1.1"
+ oid_peer_err_sub_code_recv = ".1.3.6.1.3.5.1.1.3.1.2"
+ oid_peer_err_recv_text = ".1.3.6.1.3.5.1.1.3.1.4"
+
+ nb_notif = len(output_list)
+ for nb in range(nb_notif):
+ if output_list[nb][0][0] != "{}".format(oid_notif_type):
+ return False
+
+ if output_list[nb][0][1] == "{}".format(peer_notif_established):
+ logger.info("Established notification")
+ notif_type = "Estab"
+
+ elif output_list[nb][0][1] == "{}".format(peer_notif_backward):
+ logger.info("Backward transition notification")
+ notif_type = "Backward"
+ else:
+ return False
+
+ if notif_type != type_requested:
+ continue
+
+ if output_list[nb][1][0] != "{}.1.{}".format(oid_peer_state, address):
+ continue
+
+ if output_list[nb][2][0] != "{}.1.{}".format(oid_peer_local_port, address):
+ return False
+
+ if output_list[nb][3][0] != "{}.1.{}".format(oid_peer_remote_port, address):
+ return False
+
+ if notif_type == "Estab":
+ return True
+
+ if output_list[nb][4][0] != "{}.1.{}".format(
+ oid_peer_err_code_recv, address
+ ):
+ return False
+
+ if output_list[nb][5][0] != "{}.1.{}".format(
+ oid_peer_err_sub_code_recv, address
+ ):
+ return False
+
+ if output_list[nb][6][0] != "{}.1.{}".format(
+ oid_peer_err_recv_text, address
+ ):
+ return False
+
+ return True
+
+ return False
+
+ def get_notif_bgp4(self, output_file):
+ notifs = []
+ notif_list = []
+ whitecleanfile = re.sub("\t", " ", output_file)
+ results = whitecleanfile.strip().split("\n")
+
+ # don't consider additional SNMP or application messages
+ for result in results:
+ if re.search(r"(\.([0-9]+))+\s", result):
+ notifs.append(result)
+
+ oid_v4 = r"1\.3\.6\.1\.2\.1\.15"
+ for one_notif in notifs:
+ is_ipv4_notif = re.search(oid_v4, one_notif)
+ if is_ipv4_notif != None:
+ formated_notif = self.parse_notif_ipv4(one_notif)
+ notif_list.append(formated_notif)
+
+ return notif_list
+
+ def get_notif_bgp4v2(self, output_file):
+ notifs = []
+ notif_list = []
+ whitecleanfile = re.sub("\t", " ", output_file)
+ results = whitecleanfile.strip().split("\n")
+
+ # don't consider additional SNMP or application messages
+ for result in results:
+ if re.search(r"(\.([0-9]+))+\s", result):
+ notifs.append(result)
+
+ oid_v6 = r"1\.3\.6\.1\.3\.5\.1"
+ for one_notif in notifs:
+ is_ipv6_notif = re.search(oid_v6, one_notif)
+ if is_ipv6_notif != None:
+ formated_notif = self.parse_notif_ipv4(one_notif)
+ notif_list.append(formated_notif)
+
+ return notif_list
+
def test_oid(self, oid, value):
print("oid: {}".format(self.get_next(oid)))
return self.get_next(oid) == value
diff --git a/tests/topotests/lib/topogen.py b/tests/topotests/lib/topogen.py
index 4d935b9..155d2d0 100644
--- a/tests/topotests/lib/topogen.py
+++ b/tests/topotests/lib/topogen.py
@@ -81,20 +81,20 @@ def is_string(value):
def get_exabgp_cmd(commander=None):
- """Return the command to use for ExaBGP version < 4."""
+ """Return the command to use for ExaBGP version >= 4.2.11"""
if commander is None:
commander = Commander("exabgp", logger=logging.getLogger("exabgp"))
def exacmd_version_ok(exacmd):
- logger.debug("checking %s for exabgp < version 4", exacmd)
+ logger.debug("checking %s for exabgp version >= 4.2.11", exacmd)
_, stdout, _ = commander.cmd_status(exacmd + " -v", warn=False)
m = re.search(r"ExaBGP\s*:\s*((\d+)\.(\d+)(?:\.(\d+))?)", stdout)
if not m:
return False
version = m.group(1)
- if topotest.version_cmp(version, "4") >= 0:
- logging.debug("found exabgp version >= 4 in %s will keep looking", exacmd)
+ if topotest.version_cmp(version, "4.2.11") < 0:
+ logging.debug("found exabgp version < 4.2.11 in %s will keep looking", exacmd)
return False
logger.info("Using ExaBGP version %s in %s", version, exacmd)
return True
@@ -102,14 +102,14 @@ def get_exabgp_cmd(commander=None):
exacmd = commander.get_exec_path("exabgp")
if exacmd and exacmd_version_ok(exacmd):
return exacmd
- py2_path = commander.get_exec_path("python2")
- if py2_path:
- exacmd = py2_path + " -m exabgp"
+ py3_path = commander.get_exec_path("python3")
+ if py3_path:
+ exacmd = py3_path + " -m exabgp"
if exacmd_version_ok(exacmd):
return exacmd
- py2_path = commander.get_exec_path("python")
- if py2_path:
- exacmd = py2_path + " -m exabgp"
+ py3_path = commander.get_exec_path("python")
+ if py3_path:
+ exacmd = py3_path + " -m exabgp"
if exacmd_version_ok(exacmd):
return exacmd
return None
@@ -719,6 +719,7 @@ class TopoRouter(TopoGear):
"/etc/frr",
"/etc/snmp",
"/var/run/frr",
+ "/var/lib/frr",
"/var/log",
]
@@ -744,6 +745,7 @@ class TopoRouter(TopoGear):
RD_SNMP = 18
RD_PIM6 = 19
RD_MGMTD = 20
+ RD_TRAP = 21
RD = {
RD_FRR: "frr",
RD_ZEBRA: "zebra",
@@ -766,6 +768,7 @@ class TopoRouter(TopoGear):
RD_PATH: "pathd",
RD_SNMP: "snmpd",
RD_MGMTD: "mgmtd",
+ RD_TRAP: "snmptrapd",
}
def __init__(self, tgen, cls, name, **params):
@@ -842,7 +845,7 @@ class TopoRouter(TopoGear):
TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
TopoRouter.RD_PIM, TopoRouter.RD_PIM6, TopoRouter.RD_PBR,
- TopoRouter.RD_SNMP, TopoRouter.RD_MGMTD.
+ TopoRouter.RD_SNMP, TopoRouter.RD_MGMTD, TopoRouter.RD_TRAP.
Possible `source` values are `None` for an empty config file, a path name which is
used directly, or a file name with no path components which is first looked for
@@ -880,7 +883,7 @@ class TopoRouter(TopoGear):
# Enable all daemon command logging, logging files
# and set them to the start dir.
for daemon, enabled in nrouter.daemons.items():
- if enabled and daemon != "snmpd":
+ if enabled and daemon != "snmpd" and daemon != "snmptrapd":
self.vtysh_cmd(
"\n".join(
[
@@ -1196,7 +1199,7 @@ class TopoExaBGP(TopoHost):
* Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
"""
exacmd = self.tgen.get_exabgp_cmd()
- assert exacmd, "Can't find a usabel ExaBGP (must be < version 4)"
+ assert exacmd, "Can't find a usable ExaBGP (must be version >= 4.2.11)"
self.run("mkdir -p /etc/exabgp")
self.run("chmod 755 /etc/exabgp")
@@ -1207,8 +1210,22 @@ class TopoExaBGP(TopoHost):
self.run("chmod 644 /etc/exabgp/*")
self.run("chmod a+x /etc/exabgp/*.py")
self.run("chown -R exabgp:exabgp /etc/exabgp")
+ self.run("[ -p /var/run/exabgp.in ] || mkfifo /var/run/exabgp.in")
+ self.run("[ -p /var/run/exabgp.out ] || mkfifo /var/run/exabgp.out")
+ self.run("chown exabgp:exabgp /var/run/exabgp.{in,out}")
+ self.run("chmod 600 /var/run/exabgp.{in,out}")
+
+ log_dir = os.path.join(self.logdir, self.name)
+ self.run("chmod 777 {}".format(log_dir))
+
+ log_file = os.path.join(log_dir, "exabgp.log")
- output = self.run(exacmd + " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
+ env_cmd = "env exabgp.log.level=INFO "
+ env_cmd += "exabgp.log.destination={} ".format(log_file)
+
+ output = self.run(
+ env_cmd + exacmd + " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg "
+ )
if output is None or len(output) == 0:
output = "<none>"
@@ -1369,7 +1386,7 @@ def diagnose_env_linux(rundir):
logger.info("LDPd tests will not run (missing mpls-iptunnel kernel module)")
if not get_exabgp_cmd():
- logger.warning("Failed to find exabgp < 4")
+ logger.warning("Failed to find exabgp >= 4.2.11")
logger.removeHandler(fhandler)
fhandler.close()
diff --git a/tests/topotests/lib/topotest.py b/tests/topotests/lib/topotest.py
index c220bcf..2bb8923 100644
--- a/tests/topotests/lib/topotest.py
+++ b/tests/topotests/lib/topotest.py
@@ -31,7 +31,8 @@ from copy import deepcopy
import lib.topolog as topolog
from lib.micronet_compat import Node
from lib.topolog import logger
-from munet.base import Timeout
+from munet.base import commander, get_exec_path_host, Timeout
+from munet.testing.util import retry
from lib import micronet
@@ -1261,8 +1262,8 @@ def rlimit_atleast(rname, min_value, raises=False):
def fix_netns_limits(ns):
# Maximum read and write socket buffer sizes
- sysctl_atleast(ns, "net.ipv4.tcp_rmem", [10 * 1024, 87380, 16 * 2 ** 20])
- sysctl_atleast(ns, "net.ipv4.tcp_wmem", [10 * 1024, 87380, 16 * 2 ** 20])
+ sysctl_atleast(ns, "net.ipv4.tcp_rmem", [10 * 1024, 87380, 16 * 2**20])
+ sysctl_atleast(ns, "net.ipv4.tcp_wmem", [10 * 1024, 87380, 16 * 2**20])
sysctl_assure(ns, "net.ipv4.conf.all.rp_filter", 0)
sysctl_assure(ns, "net.ipv4.conf.default.rp_filter", 0)
@@ -1321,8 +1322,8 @@ def fix_host_limits():
sysctl_atleast(None, "net.core.netdev_max_backlog", 4 * 1024)
# Maximum read and write socket buffer sizes
- sysctl_atleast(None, "net.core.rmem_max", 16 * 2 ** 20)
- sysctl_atleast(None, "net.core.wmem_max", 16 * 2 ** 20)
+ sysctl_atleast(None, "net.core.rmem_max", 16 * 2**20)
+ sysctl_atleast(None, "net.core.wmem_max", 16 * 2**20)
# Garbage Collection Settings for ARP and Neighbors
sysctl_atleast(None, "net.ipv4.neigh.default.gc_thresh2", 4 * 1024)
@@ -1363,6 +1364,8 @@ def setup_node_tmpdir(logdir, name):
class Router(Node):
"A Node with IPv4/IPv6 forwarding enabled"
+ gdb_emacs_router = None
+
def __init__(self, name, *posargs, **params):
# Backward compatibility:
# Load configuration defaults like topogen.
@@ -1380,6 +1383,8 @@ class Router(Node):
)
self.perf_daemons = {}
+ self.rr_daemons = {}
+ self.valgrind_gdb_daemons = {}
# If this topology is using old API and doesn't have logdir
# specified, then attempt to generate an unique logdir.
@@ -1420,6 +1425,7 @@ class Router(Node):
"pathd": 0,
"snmpd": 0,
"mgmtd": 0,
+ "snmptrapd": 0,
}
self.daemons_options = {"zebra": ""}
self.reportCores = True
@@ -1799,7 +1805,12 @@ class Router(Node):
gdb_breakpoints = g_pytest_config.get_option_list("--gdb-breakpoints")
gdb_daemons = g_pytest_config.get_option_list("--gdb-daemons")
gdb_routers = g_pytest_config.get_option_list("--gdb-routers")
+ gdb_use_emacs = bool(g_pytest_config.option.gdb_use_emacs)
+ rr_daemons = g_pytest_config.get_option_list("--rr-daemons")
+ rr_routers = g_pytest_config.get_option_list("--rr-routers")
+ rr_options = g_pytest_config.get_option("--rr-options", "")
valgrind_extra = bool(g_pytest_config.option.valgrind_extra)
+ valgrind_leak_kinds = g_pytest_config.option.valgrind_leak_kinds
valgrind_memleaks = bool(g_pytest_config.option.valgrind_memleaks)
strace_daemons = g_pytest_config.get_option_list("--strace-daemons")
@@ -1875,6 +1886,15 @@ class Router(Node):
# do not since apparently presence of the pidfile impacts BGP GR
self.cmd_status("rm -f {0}.pid {0}.vty".format(runbase))
+ def do_gdb_or_rr(gdb):
+ routers = gdb_routers if gdb else rr_routers
+ daemons = gdb_daemons if gdb else rr_daemons
+ return (
+ (routers or daemons)
+ and (not routers or self.name in routers or "all" in routers)
+ and (not daemons or daemon in daemons or "all" in daemons)
+ )
+
rediropt = " > {0}.out 2> {0}.err".format(daemon)
if daemon == "snmpd":
binary = "/usr/sbin/snmpd"
@@ -1883,6 +1903,15 @@ class Router(Node):
daemon_opts
) + "{}.pid -x /etc/frr/agentx".format(runbase)
# check_daemon_files.append(runbase + ".pid")
+ elif daemon == "snmptrapd":
+ binary = "/usr/sbin/snmptrapd"
+ cmdenv = ""
+ cmdopt = (
+ "{} ".format(daemon_opts)
+ + "-C -c /etc/{}/snmptrapd.conf".format(self.routertype)
+ + " -p {}.pid".format(runbase)
+ + " -LF 6-7 {}/{}/snmptrapd.log".format(self.logdir, self.name)
+ )
else:
binary = os.path.join(self.daemondir, daemon)
check_daemon_files.extend([runbase + ".pid", runbase + ".vty"])
@@ -1901,13 +1930,23 @@ class Router(Node):
supp_file = os.path.abspath(
os.path.join(this_dir, "../../../tools/valgrind.supp")
)
- cmdenv += " /usr/bin/valgrind --num-callers=50 --log-file={1}/{2}.valgrind.{0}.%p --leak-check=full --suppressions={3}".format(
- daemon, self.logdir, self.name, supp_file
+
+ valgrind_logbase = f"{self.logdir}/{self.name}.valgrind.{daemon}"
+ if do_gdb_or_rr(True):
+ cmdenv += " exec"
+ cmdenv += (
+ " /usr/bin/valgrind --num-callers=50"
+ f" --log-file={valgrind_logbase}.%p"
+ f" --leak-check=full --suppressions={supp_file}"
)
+ if valgrind_leak_kinds:
+ cmdenv += f" --show-leak-kinds={valgrind_leak_kinds}"
if valgrind_extra:
cmdenv += (
" --gen-suppressions=all --expensive-definedness-checks=yes"
)
+ if do_gdb_or_rr(True):
+ cmdenv += " --vgdb-error=0"
elif daemon in strace_daemons or "all" in strace_daemons:
cmdenv = "strace -f -D -o {1}/{2}.strace.{0} ".format(
daemon, self.logdir, self.name
@@ -1922,16 +1961,22 @@ class Router(Node):
tail_log_files.append(
"{}/{}/{}.log".format(self.logdir, self.name, daemon)
)
+
if extra_opts:
cmdopt += " " + extra_opts
+ if do_gdb_or_rr(True) and do_gdb_or_rr(False):
+ logger.warning("cant' use gdb and rr at same time")
+
if (
- (gdb_routers or gdb_daemons)
- and (
- not gdb_routers or self.name in gdb_routers or "all" in gdb_routers
- )
- and (not gdb_daemons or daemon in gdb_daemons or "all" in gdb_daemons)
- ):
+ not gdb_use_emacs or Router.gdb_emacs_router or valgrind_memleaks
+ ) and do_gdb_or_rr(True):
+ if Router.gdb_emacs_router is not None:
+ logger.warning(
+ "--gdb-use-emacs can only run a single router and daemon, using"
+ " new window"
+ )
+
if daemon == "snmpd":
cmdopt += " -f "
@@ -1941,9 +1986,133 @@ class Router(Node):
gdbcmd += " -ex 'set breakpoint pending on'"
for bp in gdb_breakpoints:
gdbcmd += " -ex 'b {}'".format(bp)
- gdbcmd += " -ex 'run {}'".format(cmdopt)
- self.run_in_window(gdbcmd, daemon)
+ if not valgrind_memleaks:
+ gdbcmd += " -ex 'run {}'".format(cmdopt)
+ self.run_in_window(gdbcmd, daemon)
+
+ logger.info(
+ "%s: %s %s launched in gdb window",
+ self,
+ self.routertype,
+ daemon,
+ )
+
+ else:
+ cmd = " ".join([cmdenv, binary, cmdopt])
+ p = self.popen(cmd)
+ self.valgrind_gdb_daemons[daemon] = p
+ if p.poll() and p.returncode:
+ self.logger.error(
+ '%s: Failed to launch "%s" (%s) with perf using: %s',
+ self,
+ daemon,
+ p.returncode,
+ cmd,
+ )
+ assert False, "Faled to launch valgrind with gdb"
+ logger.debug(
+ "%s: %s %s started with perf", self, self.routertype, daemon
+ )
+ # Now read the erorr log file until we ae given launch priority
+ timeout = Timeout(30)
+ vpid = None
+ for remaining in timeout:
+ try:
+ fname = f"{valgrind_logbase}.{p.pid}"
+ logging.info("Checking %s for valgrind launch info", fname)
+ o = open(fname, encoding="ascii").read()
+ except FileNotFoundError:
+ logging.info("%s not present yet", fname)
+ else:
+ m = re.search(r"target remote \| (.*vgdb) --pid=(\d+)", o)
+ if m:
+ vgdb_cmd = m.group(0)
+ break
+ time.sleep(1)
+ else:
+ assert False, "Faled to get launch info for valgrind with gdb"
+
+ gdbcmd += f" -ex '{vgdb_cmd}'"
+ gdbcmd += " -ex 'c'"
+ self.run_in_window(gdbcmd, daemon)
+
+ logger.info(
+ "%s: %s %s launched in gdb window",
+ self,
+ self.routertype,
+ daemon,
+ )
+ elif gdb_use_emacs and do_gdb_or_rr(True):
+ assert Router.gdb_emacs_router is None
+ Router.gdb_emacs_router = self
+
+ assert not valgrind_memleaks, "vagrind gdb in emacs not supported yet"
+
+ if daemon == "snmpd":
+ cmdopt += " -f "
+ cmdopt += rediropt
+
+ sudo_path = get_exec_path_host("sudo")
+ ecbin = [
+ sudo_path,
+ "-Eu",
+ os.environ["SUDO_USER"],
+ get_exec_path_host("emacsclient"),
+ ]
+ pre_cmd = self._get_pre_cmd(True, False, ns_only=True, root_level=True)
+ # why fail:? gdb -i=mi -iex='set debuginfod enabled off' {binary} "
+ gdbcmd = f"{sudo_path} {pre_cmd} gdb -i=mi {binary} "
+
+ commander.cmd_raises(
+ ecbin
+ + [
+ "--eval",
+ f'(gdb "{gdbcmd}"))',
+ ]
+ )
+
+ elcheck = (
+ '(ignore-errors (with-current-buffer "*gud-nsenter*"'
+ " (and (string-match-p"
+ ' "(gdb) "'
+ " (buffer-substring-no-properties "
+ ' (- (point-max) 10) (point-max))) "ready")))'
+ )
+
+ @retry(10)
+ def emacs_gdb_ready():
+ check = commander.cmd_nostatus(ecbin + ["--eval", elcheck])
+ return None if "ready" in check else False
+
+ emacs_gdb_ready()
+
+ # target gdb commands
+ cmd = "set breakpoint pending on"
+ self.cmd_raises(
+ ecbin
+ + [
+ "--eval",
+ f'(gud-gdb-run-command-fetch-lines "{cmd}" "*gud-gdb*")',
+ ]
+ )
+ # gdb breakpoints
+ for bp in gdb_breakpoints:
+ self.cmd_raises(
+ ecbin
+ + [
+ "--eval",
+ f'(gud-gdb-run-command-fetch-lines "br {bp}" "*gud-gdb*")',
+ ]
+ )
+
+ self.cmd_raises(
+ ecbin
+ + [
+ "--eval",
+ f'(gud-gdb-run-command-fetch-lines "run {cmdopt}" "*gud-gdb*")',
+ ]
+ )
logger.info(
"%s: %s %s launched in gdb window", self, self.routertype, daemon
@@ -1969,8 +2138,31 @@ class Router(Node):
logger.debug(
"%s: %s %s started with perf", self, self.routertype, daemon
)
+ elif do_gdb_or_rr(False):
+ cmdopt += rediropt
+ cmd = " ".join(
+ [
+ "rr record -o {} {} --".format(self.rundir / "rr", rr_options),
+ binary,
+ cmdopt,
+ ]
+ )
+ p = self.popen(cmd)
+ self.rr_daemons[daemon] = p
+ if p.poll() and p.returncode:
+ self.logger.error(
+ '%s: Failed to launch "%s" (%s) with rr using: %s',
+ self,
+ daemon,
+ p.returncode,
+ cmd,
+ )
+ else:
+ logger.debug(
+ "%s: %s %s started with rr", self, self.routertype, daemon
+ )
else:
- if daemon != "snmpd":
+ if daemon != "snmpd" and daemon != "snmptrapd":
cmdopt += " -d "
cmdopt += rediropt
@@ -2213,6 +2405,8 @@ class Router(Node):
for daemon in self.daemons:
if daemon == "snmpd":
continue
+ if daemon == "snmptrapd":
+ continue
if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning):
sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon))
if daemon == "staticd":