From e00241ad9f15208b0aae62203d0856114bf55fec Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sat, 26 Feb 2022 13:44:33 -0500 Subject: [PATCH] tests: new grpc topotest Signed-off-by: Christian Hopps --- doc/developer/topotests.rst | 3 + tests/.gitignore | 2 + tests/topotests/grpc_basic/lib | 1 + tests/topotests/grpc_basic/r1/zebra.conf | 8 + tests/topotests/grpc_basic/r2/zebra.conf | 8 + tests/topotests/grpc_basic/test_basic_grpc.py | 179 ++++++++++++++++++ tests/topotests/lib/grpc-query.py | 155 +++++++++++++++ 7 files changed, 356 insertions(+) create mode 120000 tests/topotests/grpc_basic/lib create mode 100644 tests/topotests/grpc_basic/r1/zebra.conf create mode 100644 tests/topotests/grpc_basic/r2/zebra.conf create mode 100644 tests/topotests/grpc_basic/test_basic_grpc.py create mode 100755 tests/topotests/lib/grpc-query.py diff --git a/doc/developer/topotests.rst b/doc/developer/topotests.rst index b41181f4e9..6c1d9148d1 100644 --- a/doc/developer/topotests.rst +++ b/doc/developer/topotests.rst @@ -35,6 +35,9 @@ Installing Topotest Requirements python2 -m pip install 'exabgp<4.0.0' useradd -d /var/run/exabgp/ -s /bin/false exabgp + # To enable the gRPC topotest install: + python3 -m pip install grpcio grpcio-tools + Enable Coredumps """""""""""""""" diff --git a/tests/.gitignore b/tests/.gitignore index 70d0ef6e0a..f00177abd8 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,6 +1,8 @@ *.log *.sum *.xml +frr-northbound.proto +frr_northbound* .pytest_cache /bgpd/test_aspath /bgpd/test_bgp_table diff --git a/tests/topotests/grpc_basic/lib b/tests/topotests/grpc_basic/lib new file mode 120000 index 0000000000..dc598c56dc --- /dev/null +++ b/tests/topotests/grpc_basic/lib @@ -0,0 +1 @@ +../lib \ No newline at end of file diff --git a/tests/topotests/grpc_basic/r1/zebra.conf b/tests/topotests/grpc_basic/r1/zebra.conf new file mode 100644 index 0000000000..49a0911c53 --- /dev/null +++ b/tests/topotests/grpc_basic/r1/zebra.conf @@ -0,0 +1,8 @@ +log record-priority +log timestamp precision 6 +log extended extlog + destination file ext-log.txt create + timestamp precision 6 + structured-data code-location +interface r1-eth0 + ip address 192.168.1.1/24 \ No newline at end of file diff --git a/tests/topotests/grpc_basic/r2/zebra.conf b/tests/topotests/grpc_basic/r2/zebra.conf new file mode 100644 index 0000000000..20da1885d4 --- /dev/null +++ b/tests/topotests/grpc_basic/r2/zebra.conf @@ -0,0 +1,8 @@ +log record-priority +log timestamp precision 6 +log extended extlog + destination file ext-log.txt create + timestamp precision 6 + structured-data code-location +interface r2-eth0 + ip address 192.168.1.2/24 diff --git a/tests/topotests/grpc_basic/test_basic_grpc.py b/tests/topotests/grpc_basic/test_basic_grpc.py new file mode 100644 index 0000000000..b6812a5afc --- /dev/null +++ b/tests/topotests/grpc_basic/test_basic_grpc.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# February 21 2022, Christian Hopps +# +# Copyright (c) 2022, LabN Consulting, L.L.C. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +""" +test_basic_grpc.py: Test Basic gRPC. +""" + +import logging +import os +import sys + +import pytest + +from lib.common_config import step +from lib.micronet import commander +from lib.topogen import Topogen, TopoRouter +from lib.topolog import logger + +CWD = os.path.dirname(os.path.realpath(__file__)) + +GRPCP_ZEBRA = 50051 +GRPCP_STATICD = 50052 +GRPCP_BFDD = 50053 +GRPCP_ISISD = 50054 +GRPCP_OSPFD = 50055 +GRPCP_PIMD = 50056 + +pytestmark = [ + # pytest.mark.mgmtd -- Need a new non-protocol marker + # pytest.mark.bfdd, + # pytest.mark.isisd, + # pytest.mark.ospfd, + # pytest.mark.pimd, + pytest.mark.staticd, +] + +script_path = os.path.realpath(os.path.join(CWD, "../lib/grpc-query.py")) + +try: + commander.cmd_raises([script_path, "--check"]) +except Exception: + pytest.skip( + "skipping; cannot create or import gRPC proto modules", allow_module_level=True + ) + + +@pytest.fixture(scope="module") +def tgen(request): + "Setup/Teardown the environment and provide tgen argument to tests" + topodef = {"s1": ("r1", "r2")} + tgen = Topogen(topodef, request.module.__name__) + + tgen.start_topology() + router_list = tgen.routers() + + for rname, router in router_list.items(): + router.load_config(TopoRouter.RD_ZEBRA, "zebra.conf", f"-M grpc:{GRPCP_ZEBRA}") + router.load_config(TopoRouter.RD_STATIC, None, f"-M grpc:{GRPCP_STATICD}") + # router.load_config(TopoRouter.RD_BFD, None, f"-M grpc:{GRPCP_BFDD}") + # router.load_config(TopoRouter.RD_ISIS, None, f"-M grpc:{GRPCP_ISISD}") + # router.load_config(TopoRouter.RD_OSPF, None, f"-M grpc:{GRPCP_OSPFD}") + # router.load_config(TopoRouter.RD_PIM, None, f"-M grpc:{GRPCP_PIMD}") + + tgen.start_router() + yield tgen + + logging.info("Stopping all routers (no assert on error)") + tgen.stop_topology() + + +# Let's not do this so we catch errors +# Fixture that executes before each test +@pytest.fixture(autouse=True) +def skip_on_failure(tgen): + if tgen.routers_have_failure(): + pytest.skip("skipped because of previous test failure") + + +# =================== +# The tests functions +# =================== + + +def run_grpc_client(r, port, commands): + if not isinstance(commands, str): + commands = "\n".join(commands) + "\n" + if not commands.endswith("\n"): + commands += "\n" + return r.cmd_raises([script_path, f"--port={port}"], stdin=commands) + + +def test_connectivity(tgen): + r1 = tgen.gears["r1"] + output = r1.cmd_raises("ping -c1 192.168.1.2") + logging.info("ping output: %s", output) + + +def test_capabilities(tgen): + r1 = tgen.gears["r1"] + output = run_grpc_client(r1, GRPCP_ZEBRA, "GETCAP") + logging.info("grpc output: %s", output) + + +def test_get_config(tgen): + nrepeat = 5 + r1 = tgen.gears["r1"] + + step("'GET' inteface config 10 times, once per invocation") + + for i in range(0, nrepeat): + output = run_grpc_client(r1, GRPCP_ZEBRA, "GET,/frr-interface:lib") + logging.info("[iteration %s]: grpc GET output: %s", i, output) + + step(f"'GET' YANG {nrepeat} times in one invocation") + commands = ["GET,/frr-interface:lib" for _ in range(0, 10)] + output = run_grpc_client(r1, GRPCP_ZEBRA, commands) + logging.info("grpc GET*{%d} output: %s", nrepeat, output) + + +def test_get_vrf_config(tgen): + r1 = tgen.gears["r1"] + + step("'GET' get VRF config") + + output = run_grpc_client(r1, GRPCP_ZEBRA, "GET,/frr-vrf:lib") + logging.info("grpc GET /frr-vrf:lib output: %s", output) + + +def test_shutdown_checks(tgen): + # Start a process rnuning that will fetch bunches of data then shut the routers down + # and check for cores. + nrepeat = 100 + r1 = tgen.gears["r1"] + commands = ["GET,/frr-interface:lib" for _ in range(0, nrepeat)] + p = r1.popen([script_path, f"--port={GRPCP_ZEBRA}"] + commands) + import time + + time.sleep(1) + try: + for r in tgen.routers().values(): + r.net.stopRouter() + r.net.checkRouterCores() + finally: + if p: + p.terminate() + p.wait() + + +# Memory leak test template +# Not compatible with the shutdown check above +def _test_memory_leak(tgen): + "Run the memory leak test and report results." + + if not tgen.is_memleak_enabled(): + pytest.skip("Memory leak test/report is disabled") + + tgen.report_memory_leaks() + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/lib/grpc-query.py b/tests/topotests/lib/grpc-query.py new file mode 100755 index 0000000000..61f01c36bb --- /dev/null +++ b/tests/topotests/lib/grpc-query.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# February 22 2022, Christian Hopps +# +# Copyright (c) 2022, LabN Consulting, L.L.C. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import argparse +import logging +import os +import sys + +import pytest + +CWD = os.path.dirname(os.path.realpath(__file__)) + +# This is painful but works if you have installed grpc and grpc_tools would be *way* +# better if we actually built and installed these but ... python packaging. +try: + import grpc + import grpc_tools + + from micronet import commander + + commander.cmd_raises(f"cp {CWD}/../../../grpc/frr-northbound.proto .") + commander.cmd_raises( + f"python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I . frr-northbound.proto" + ) +except Exception as error: + logging.error("can't create proto definition modules %s", error) + raise + +try: + sys.path[0:0] = "." + import frr_northbound_pb2 + import frr_northbound_pb2_grpc + + # Would be nice if compiling the modules internally from the source worked + # # import grpc_tools.protoc + # # proto_include = pkg_resources.resource_filename("grpc_tools", "_proto") + # from grpc_tools.protoc import _proto_file_to_module_name, _protos_and_services + # try: + # frr_northbound_pb2, frr_northbound_pb2_grpc = _protos_and_services( + # "frr_northbound.proto" + # ) + # finally: + # os.chdir(CWD) +except Exception as error: + logging.error("can't import proto definition modules %s", error) + raise + + +class GRPCClient: + def __init__(self, server, port): + self.channel = grpc.insecure_channel("{}:{}".format(server, port)) + self.stub = frr_northbound_pb2_grpc.NorthboundStub(self.channel) + + def get_capabilities(self): + request = frr_northbound_pb2.GetCapabilitiesRequest() + response = "NONE" + try: + response = self.stub.GetCapabilities(request) + except Exception as error: + logging.error("Got exception from stub: %s", error) + + logging.debug("GRPC Capabilities: %s", response) + return response + + def get(self, xpath): + request = frr_northbound_pb2.GetRequest() + request.path.append(xpath) + request.type = frr_northbound_pb2.GetRequest.ALL + request.encoding = frr_northbound_pb2.XML + xml = "" + for r in self.stub.Get(request): + logging.info('GRPC Get path: "%s" value: %s', request.path, r) + xml += str(r.data.data) + return xml + + +def next_action(action_list=None): + "Get next action from list or STDIN" + if action_list: + for action in action_list: + yield action + else: + while True: + try: + action = input("") + if not action: + break + yield action.strip() + except EOFError: + break + + +def main(*args): + parser = argparse.ArgumentParser(description="gRPC Client") + parser.add_argument( + "-s", "--server", default="localhost", help="gRPC Server Address" + ) + parser.add_argument( + "-p", "--port", type=int, default=50051, help="gRPC Server TCP Port" + ) + parser.add_argument("-v", "--verbose", action="store_true", help="be verbose") + parser.add_argument("--check", action="store_true", help="check runable") + parser.add_argument("actions", nargs="*", help="GETCAP|GET,xpath") + args = parser.parse_args(*args) + + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)s: GRPC-CLI-CLIENT: %(name)s %(message)s", + ) + + if args.check: + sys.exit(0) + + c = GRPCClient(args.server, args.port) + + for action in next_action(args.actions): + action = action.casefold() + logging.info("GOT ACTION: %s", action) + if action == "getcap": + caps = c.get_capabilities() + print("Capabilities:", caps) + elif action.startswith("get,"): + # Print Interface State and Config + _, xpath = action.split(",", 1) + print("Get XPath: ", xpath) + xml = c.get(xpath) + print("{}: {}".format(xpath, xml)) + # for _ in range(0, 1): + + +if __name__ == "__main__": + main() -- 2.39.5