diff options
Diffstat (limited to 'doc/developer')
| -rw-r--r-- | doc/developer/cli.rst | 171 | ||||
| -rw-r--r-- | doc/developer/logging.rst | 4 | ||||
| -rw-r--r-- | doc/developer/topotests-jsontopo.rst | 143 | ||||
| -rw-r--r-- | doc/developer/topotests.rst | 42 |
4 files changed, 259 insertions, 101 deletions
diff --git a/doc/developer/cli.rst b/doc/developer/cli.rst index edabe61d92..9254eb4739 100644 --- a/doc/developer/cli.rst +++ b/doc/developer/cli.rst @@ -139,6 +139,7 @@ by the parser. selector: "<" `selector_seq_seq` ">" `varname_token` : "{" `selector_seq_seq` "}" `varname_token` : "[" `selector_seq_seq` "]" `varname_token` + : "![" `selector_seq_seq` "]" `varname_token` selector_seq_seq: `selector_seq_seq` "|" `selector_token_seq` : `selector_token_seq` selector_token_seq: `selector_token_seq` `selector_token` @@ -218,6 +219,10 @@ one-or-more selection and repetition. provide mutual exclusion. User input matches at most one option. - ``[square brackets]`` -- Contains sequences of tokens that can be omitted. ``[<a|b>]`` can be shortened to ``[a|b]``. +- ``![exclamation square brackets]`` -- same as ``[square brackets]``, but + only allow skipping the contents if the command input starts with ``no``. + (For cases where the positive command needs a parameter, but the parameter + is optional for the negative case.) - ``{curly|braces}`` -- similar to angle brackets, but instead of mutual exclusion, curly braces indicate that one or more of the pipe-separated sequences may be provided in any order. @@ -767,6 +772,172 @@ User input: ``ip`` partially matches ``ipv6`` but exactly matches ``ip``, so ``ip`` will win. +Adding a CLI Node +----------------- + +To add a new CLI node, you should: + +- define a new numerical node constant +- define a node structure in the relevant daemon +- call ``install_node()`` in the relevant daemon +- define and install the new node in vtysh +- define corresponding node entry commands in daemon and vtysh +- add a new entry to the ``ctx_keywords`` dictionary in ``tools/frr-reload.py`` + +Defining the numerical node constant +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Add your new node value to the enum before ``NODE_TYPE_MAX`` in +``lib/command.h``: + +.. code-block:: c + + enum node_type { + AUTH_NODE, // Authentication mode of vty interface. + VIEW_NODE, // View node. Default mode of vty interface. + [...] + MY_NEW_NODE, + NODE_TYPE_MAX, // maximum + }; + +Defining a node structure +^^^^^^^^^^^^^^^^^^^^^^^^^ +In your daemon-specific code where you define your new commands that +attach to the new node, add a node definition: + +.. code-block:: c + + static struct cmd_node my_new_node = { + .name = "my new node name", + .node = MY_NEW_NODE, // enum node_type lib/command.h + .parent_node = CONFIG_NODE, + .prompt = "%s(my-new-node-prompt)# ", + .config_write = my_new_node_config_write, + }; + +You will need to define ``my_new_node_config_write(struct vty \*vty)`` +(or omit this field if you have no relevant configuration to save). + +Calling ``install_node()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ +In the daemon's initialization function, before installing your new commands +with ``install_element()``, add a call ``install_node(&my_new_node)``. + +Defining and installing the new node in vtysh +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The build tools automatically collect command definitions for vtysh. +However, new nodes must be coded in vtysh specifically. + +In ``vtysh/vtysh.c``, define a stripped-down node structure and +call ``install_node()``: + +.. code-block:: c + + static struct cmd_node my_new_node = { + .name = "my new node name", + .node = MY_NEW_NODE, /* enum node_type lib/command.h */ + .parent_node = CONFIG_NODE, + .prompt = "%s(my-new-node-prompt)# ", + }; + [...] + void vtysh_init_vty(void) + { + [...] + install_node(&my_new_node) + [...] + } + +Defining corresponding node entry commands in daemon and vtysh +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The command that descends into the new node is typically programmed +with ``VTY_PUSH_CONTEXT`` or equivalent in the daemon's CLI handler function. +(If the CLI has been updated to use the new northbound architecture, +``VTY_PUSH_XPATH`` is used instead.) + +In vtysh, you must implement a corresponding node change so that vtysh +tracks the daemon's movement through the node tree. + +Although the build tools typically scan daemon code for CLI definitions +to replicate their parsing in vtysh, the node-descent function in the +daemon must be blocked from this replication so that a hand-coded +skeleton can be written in ``vtysh.c``. + +Accordingly, use one of the ``*_NOSH`` macros such as ``DEFUN_NOSH``, +``DEFPY_NOSH``, or ``DEFUN_YANG_NOSH`` for the daemon's node-descent +CLI definition, and use ``DEFUNSH`` in ``vtysh.c`` for the vtysh equivalent. + +.. seealso:: :ref:`vtysh-special-defuns` + +Examples: + +``zebra_whatever.c`` + +.. code-block:: c + + DEFPY_NOSH(my_new_node, + my_new_node_cmd, + "my-new-node foo", + "New Thing\n" + "A foo\n") + { + [...] + VTY_PUSH_CONTEXT(MY_NEW_NODE, bar); + [...] + } + + +``ripd_whatever.c`` + +.. code-block:: c + + DEFPY_YANG_NOSH(my_new_node, + my_new_node_cmd, + "my-new-node foo", + "New Thing\n" + "A foo\n") + { + [...] + VTY_PUSH_XPATH(MY_NEW_NODE, xbar); + [...] + } + + +``vtysh.c`` + +.. code-block:: c + + DEFUNSH(VTYSH_ZEBRA, my_new_node, + my_new_node_cmd, + "my-new-node foo", + "New Thing\n" + "A foo\n") + { + vty->node = MY_NEW_NODE; + return CMD_SUCCESS; + } + [...] + install_element(CONFIG_NODE, &my_new_node_cmd); + + +Adding a new entry to the ``ctx_keywords`` dictionary +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In file ``tools/frr-reload.py``, the ``ctx_keywords`` dictionary +describes the various node relationships. +Add a new node entry at the appropriate level in this dictionary. + +.. code-block:: python + + ctx_keywords = { + [...] + "key chain ": { + "key ": {} + }, + [...] + "my-new-node": {}, + [...] + } + + + Inspection & Debugging ---------------------- diff --git a/doc/developer/logging.rst b/doc/developer/logging.rst index b827afd6cc..681fc1173c 100644 --- a/doc/developer/logging.rst +++ b/doc/developer/logging.rst @@ -191,6 +191,10 @@ Networking data types ``%pNHs``: :frrfmtout:`1.2.3.4 if 15` — same as :c:func:`nexthop2str()` + ``%pNHcg``: :frrfmtout:`1.2.3.4` — compact gateway only + + ``%pNHci``: :frrfmtout:`eth0` — compact interface only + .. frrfmt:: %pBD (struct bgp_dest *) :frrfmtout:`fe80::1234/64` diff --git a/doc/developer/topotests-jsontopo.rst b/doc/developer/topotests-jsontopo.rst index 866f27337f..e2cc72cc56 100644 --- a/doc/developer/topotests-jsontopo.rst +++ b/doc/developer/topotests-jsontopo.rst @@ -23,19 +23,18 @@ On top of current topotests framework following enhancements are done: Logging of test case executions ------------------------------- -* The user can enable logging of testcases execution messages into log file by - adding ``frrtest_log_dir = /tmp/topotests/`` in :file:`pytest.ini`. -* Router's current configuration can be displyed on console or sent to logs by - adding ``show_router_config = True`` in :file:`pytest.ini`. +* The execution log for each test is saved in the test specific directory create + under `/tmp/topotests` (e.g., + `/tmp/topotests/<testdirname.testfilename>/exec.log`) -Log file name will be displayed when we start execution: +* Additionally all test logs are captured in the `topotest.xml` results file. + This file will be saved in `/tmp/topotests/topotests.xml`. In order to extract + the logs for a particular test one can use the `analyze.py` utility found in + the topotests base directory. -.. code-block:: console - - root@test:# python ./test_topo_json_single_link.py - - Logs will be sent to logfile: - /tmp/topotests/test_topo_json_single_link_11:57:01.353797 +* Router's current configuration, as it is changed during the test, can be + displayed on console or sent to logs by adding ``show_router_config = True`` in + :file:`pytest.ini`. Note: directory "/tmp/topotests/" is created by topotests by default, making use of same directory to save execution logs. @@ -51,18 +50,18 @@ topology test. This is the recommended test writing routine: -* Create a json file , which will have routers and protocol configurations -* Create topology from json -* Create configuration from json -* Write the tests +* Create a json file which will have routers and protocol configurations +* Write and debug the tests * Format the new code using `black <https://github.com/psf/black>`_ * Create a Pull Request .. Note:: - BGP tests MUST use generous convergence timeouts - you must ensure - that any test involving BGP uses a convergence timeout of at least - 130 seconds. + BGP tests MUST use generous convergence timeouts - you must ensure that any + test involving BGP uses a convergence timeout that is proportional to the + configured BGP timers. If the timers are not reduced from their defaults this + means 130 seconds; however, it is highly recommended that timers be reduced + from the default values unless the test requires they not be. File Hierarchy ^^^^^^^^^^^^^^ @@ -72,21 +71,17 @@ repository hierarchy looks like this: .. code-block:: console - $ cd path/to/topotests + $ cd frr/tests/topotests $ find ./* ... - ./example-topojson-test # the basic example test topology-1 - ./example-topojson-test/test_example_topojson.json # input json file, having - topology, interfaces, bgp and other configuration - ./example-topojson-test/test_example_topojson.py # test script to write and - execute testcases + ./example_test/ + ./example_test/test_template_json.json # input json file, having topology, interfaces, bgp and other configuration + ./example_test/test_template_json.py # test script to write and execute testcases ... ./lib # shared test/topology functions - ./lib/topojson.py # library to create topology and configurations dynamically - from json file - ./lib/common_config.py # library to create protocol's common configurations ex- - static_routes, prefix_lists, route_maps etc. - ./lib/bgp.py # library to create only bgp configurations + ./lib/topojson.py # library to create topology and configurations dynamically from json file + ./lib/common_config.py # library to create protocol's common configurations ex- static_routes, prefix_lists, route_maps etc. + ./lib/bgp.py # library to create and test bgp configurations Defining the Topology and initial configuration in JSON file ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -370,34 +365,32 @@ Optional keywords/options in JSON: Building topology and configurations """""""""""""""""""""""""""""""""""" -Topology and initial configuration will be created in setup_module(). Following -is the sample code:: +Topology and initial configuration as well as teardown are invoked through the +use of a pytest fixture:: - def setup_module(mod): - json_file = "{}/my_test_name.json".format(CWD) - tgen = Topogen(json_file, mod.__name__) - # json topo object is now available in tgen.json_topo + from lib import fixtures - # Starting topology, create tmp files which are loaded to routers - # to start deamons and then start routers - start_topology(tgen) + tgen = pytest.fixture(fixtures.tgen_json, scope="module") - # Creating configuration from JSON - build_config_from_json(tgen) - def teardown_module(mod): - tgen = get_topogen() + # tgen is defined above + # topo is a fixture defined in ../conftest.py and automatically available + def test_bgp_convergence(tgen, topo): + bgp_convergence = bgp.verify_bgp_convergence(tgen, topo) + assert bgp_convergence - # Stop toplogy and Remove tmp files - stop_topology(tgen) +The `fixtures.topo_json` function calls `topojson.setup_module_from_json()` to +create and return a new `topogen.Topogen()` object using the JSON config file +with the same base filename as the test (i.e., `test_file.py` -> +`test_file.json`). Additionally, the fixture calls `tgen.stop_topology()` after +all the tests have run to cleanup. The function is only invoked once per +file/module (scope="module"), but the resulting object is passed to each +function that has `tgen` as an argument. +For more info on the powerful pytest fixtures feature please see `FIXTURES`_. -* Note: Topology will be created in setup module but routers will not be - started until we load zebra.conf and bgpd.conf to routers. For all routers - dirs will be created in /tmp/topotests/<test_folder_name>/<router_name> - zebra.conf and bgpd.conf empty files will be created and laoded to routers. - All folder and files are deleted in teardown module.. +.. _FIXTURES: https://docs.pytest.org/en/6.2.x/fixture.html Creating configuration files """""""""""""""""""""""""""" @@ -425,49 +418,37 @@ Writing Tests """"""""""""" Test topologies should always be bootstrapped from the -example_test/test_template_json.py, because it contains important boilerplate -code that can't be avoided, like: - -imports: os, sys, pytest, and topotest/topogen. - -The global variable CWD (Current Working directory): which is most likely going -to be used to reference the routers configuration file location +`example_test/test_template_json.py` when possible in order to take advantage of +the most recent infrastructure support code. Example: -* The topology class that inherits from Mininet Topo class; - - .. code-block:: python - - class TemplateTopo(Topo): - def build(self, *_args, **_opts): - tgen = get_topogen(self) - # topology build code - - -* pytest setup_module() and teardown_module() to start the topology: +* Define a module scoped fixture to setup/teardown and supply the tests with the + `Topogen` object. - .. code-block:: python +.. code-block:: python - def setup_module(_m): - tgen = Topogen(TemplateTopo) + import pytest + from lib import fixtures - # Starting topology, create tmp files which are loaded to routers - # to start deamons and then start routers - start_topology(tgen, CWD) + tgen = pytest.fixture(fixtures.tgen_json, scope="module") - def teardown_module(_m): - tgen = get_topogen() - # Stop toplogy and Remove tmp files - stop_topology(tgen, CWD) +* Define test functions using pytest fixtures +.. code-block:: python -* ``__main__`` initialization code (to support running the script directly) + from lib import bgp - .. code-block:: python + # tgen is defined above + # topo is a global available fixture defined in ../conftest.py + def test_bgp_convergence(tgen, topo): + "Test for BGP convergence." - if **name** == '\ **main**\ ': - sys.exit(pytest.main(["-s"])) + # Don't run this test if we have any failure. + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + bgp_convergence = bgp.verify_bgp_convergence(tgen, topo) + assert bgp_convergence diff --git a/doc/developer/topotests.rst b/doc/developer/topotests.rst index fa6a1ba660..c52d210ee5 100644 --- a/doc/developer/topotests.rst +++ b/doc/developer/topotests.rst @@ -983,22 +983,20 @@ Writing Tests """"""""""""" Test topologies should always be bootstrapped from -:file:`tests/topotests/example-test/test_template.py` because it contains +:file:`tests/topotests/example_test/test_template.py` because it contains important boilerplate code that can't be avoided, like: Example: .. code:: py - # For all registered routers, load the zebra configuration file - CWD = os.path.dirname(os.path.realpath(__file__)) - for rname, router in router_list.items(): - router.load_config( - TopoRouter.RD_ZEBRA, - os.path.join(CWD, '{}/zebra.conf'.format(rname)) - ) - # os.path.join() joins the CWD string with arguments adding the necessary - # slashes ('/'). Arguments must not begin with '/'. + # For all routers arrange for: + # - starting zebra using config file from <rtrname>/zebra.conf + # - starting ospfd using an empty config file. + for rname, router in router_list.items(): + router.load_config(TopoRouter.RD_ZEBRA, "zebra.conf") + router.load_config(TopoRouter.RD_OSPF) + - The topology definition or build function @@ -1013,27 +1011,31 @@ Example: # topology build code ... -- pytest ``setup_module()`` and ``teardown_module()`` to start the topology +- pytest setup/teardown fixture to start the topology and supply `tgen` argument + to tests. .. code:: py - def setup_module(module): + + @pytest.fixture(scope="module") + def tgen(request): + "Setup/Teardown the environment and provide tgen argument to tests" + tgen = Topogen(topodef, module.__name__) # or tgen = Topogen(build_topo, module.__name__) - tgen.start_topology('debug') + ... - def teardown_module(_m): - tgen = get_topogen() - tgen.stop_topology() + # Start and configure the router daemons + tgen.start_router() -- ``__main__`` initialization code (to support running the script directly) + # Provide tgen as argument to each test function + yield tgen -.. code:: py + # Teardown after last test runs + tgen.stop_topology() - if __name__ == '__main__': - sys.exit(pytest.main(["-s"])) Requirements: |
