From 8647356d2f8d3c787e2045ca5ff385ee2d493a61 Mon Sep 17 00:00:00 2001 From: Maksym Kovalchuk Date: Mon, 21 Jul 2025 22:24:15 +0300 Subject: [PATCH 01/21] [show][config][plugin] add processing of ModuleNotFoundError with log_warning (#3832) There is the case of rare situation of race condition when plugin script file is removed right before the stage of its load via cli (e.g. in case of dynamic removing with app extensions). This situation has no impact but generate additional error messages to syslog What I did Add separate processing ModuleNotFoundError with log_warning messages to avoid excess ERR printing How to verify it It happens very rarely in nature but you can just try to remove plugin script manually right before its loading stage. I used this changes on top of my commit for testing: diff --git a/utilities_common/util_base.py b/utilities_common/util_base.py index 95098b7..c808b0a 100644 --- a/utilities_common/util_base.py +++ b/utilities_common/util_base.py @@ -2,6 +2,8 @@ import os import pkgutil import importlib +import subprocess + from sonic_py_common import logger # Constants ==================================================================== @@ -11,6 +13,7 @@ PDDF_SUPPORT_FILE = '/usr/share/sonic/platform/pddf_support' log = logger.Logger() +plugin_path = "/usr/local/lib/python3.11/dist-packages/show/plugins/dhcp-relay.py" class UtilHelper(object): def __init__(self): @@ -28,11 +31,15 @@ class UtilHelper(object): continue log.log_debug('importing plugin: {}'.format(module_name)) try: + if module_name == "show.plugins.dhcp-relay": + subprocess.run(['mv', plugin_path, '/root/' ]) module = importlib.import_module(module_name) except ModuleNotFoundError as err: log.log_warning('failed to import plugin {}: {}'.format(module_name, err), also_print_to_console=True) + if module_name == "show.plugins.dhcp-relay": + subprocess.run(['mv', '/root/dhcp-relay.py', plugin_path ]) continue except Exception as err: --- tests/test_util_base.py | 31 +++++++++++++++++++++++++++++++ utilities_common/util_base.py | 6 ++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/test_util_base.py diff --git a/tests/test_util_base.py b/tests/test_util_base.py new file mode 100644 index 0000000000..4ccdadeee0 --- /dev/null +++ b/tests/test_util_base.py @@ -0,0 +1,31 @@ +from unittest.mock import patch, MagicMock +from utilities_common.util_base import UtilHelper + + +@patch("pkgutil.iter_modules") +@patch("utilities_common.util_base.log.log_error") +@patch("utilities_common.util_base.log.log_warning") +def test_load_plugins_exceptions_logs(mock_log_warning, mock_log_error, mock_iter_modules): + NON_EXISTENT_MODULE_NAME = "non-existent-module" + FAILED_TO_IMPORT_MESSAGE = f"failed to import plugin {NON_EXISTENT_MODULE_NAME}" + COMMON_EXCEPTION_MESSAGE = "Common exception" + + mock_iter_modules.return_value = [(None, NON_EXISTENT_MODULE_NAME, False)] + plugins_namespace = MagicMock() + plugins_namespace.__path__ = "some_path" + plugins_namespace.__name__ = "some_name" + + # Assetion for ModuleNotFoundError + list(UtilHelper().load_plugins(plugins_namespace)) + mock_log_warning.assert_called_once_with( + f"{FAILED_TO_IMPORT_MESSAGE}: No module named '{NON_EXISTENT_MODULE_NAME}'", + also_print_to_console=True + ) + + # Assertion for Exception + with patch("importlib.import_module", side_effect=Exception(COMMON_EXCEPTION_MESSAGE)): + list(UtilHelper().load_plugins(plugins_namespace)) + mock_log_error.assert_called_once_with( + f"{FAILED_TO_IMPORT_MESSAGE}: {COMMON_EXCEPTION_MESSAGE}", + also_print_to_console=True + ) diff --git a/utilities_common/util_base.py b/utilities_common/util_base.py index 98fc230629..c6053392ca 100644 --- a/utilities_common/util_base.py +++ b/utilities_common/util_base.py @@ -29,6 +29,12 @@ def iter_namespace(ns_pkg): log.log_debug('importing plugin: {}'.format(module_name)) try: module = importlib.import_module(module_name) + + except ModuleNotFoundError as err: + log.log_warning('failed to import plugin {}: {}'.format(module_name, err), + also_print_to_console=True) + continue + except Exception as err: log.log_error('failed to import plugin {}: {}'.format(module_name, err), also_print_to_console=True) From 3db35d543812f874cb228d34cee9b63d3f939b96 Mon Sep 17 00:00:00 2001 From: mramezani95 Date: Mon, 21 Jul 2025 17:39:10 -0700 Subject: [PATCH 02/21] `vnet_route_check.py` should not report VNET routes in APP DB but not in STATE DB and ASIC DB as mismatches (#3990) What I did Changed vnet_route_check.py so that when a VNET route is missing in STATE DB, then it is treated as an inactive route. This means that when the script is called without -a and --all options, if a route in APP DB is missing in both STATE DB and ASIC DB, it will not be reported as a mismatch. How I did it Changed the logic in vnet_route_check.py. Added a new test and modified an existing test in vnet_route_check_test.py to verify the desired behavior. How to verify it Run the tests in vnet_route_check_test.py. --- scripts/vnet_route_check.py | 4 ++-- tests/vnet_route_check_test.py | 40 ++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/scripts/vnet_route_check.py b/scripts/vnet_route_check.py index 2a34891794..c420020836 100755 --- a/scripts/vnet_route_check.py +++ b/scripts/vnet_route_check.py @@ -50,7 +50,7 @@ RC_ERR = -1 default_vrf_oid = "" -report_level = syslog.LOG_ERR +report_level = syslog.LOG_WARNING write_to_syslog = True @@ -374,7 +374,7 @@ def filter_active_vnet_routes(vnet_routes: dict): exists, fvs = vnet_route_tunnel_table.get(key) if not exists: print_message(syslog.LOG_WARNING, f"VNET_ROUTE_TUNNEL_TABLE|{key} does not exist in STATE DB.") - active_routes.append(prefix) # Treating "prefix" as an active route + # Treating "prefix" as an inactive route continue fvs_dict = dict(fvs) if fvs_dict.get("state") == "active": diff --git a/tests/vnet_route_check_test.py b/tests/vnet_route_check_test.py index 1cd5175699..c271642f16 100644 --- a/tests/vnet_route_check_test.py +++ b/tests/vnet_route_check_test.py @@ -567,7 +567,6 @@ "12": { DESCR: "An IPv6 VNET route that is missing in STATE DB and ASIC DB", ARGS: "vnet_route_check", - RET: -1, PRE: { APPL_DB: { VXLAN_TUNNEL_TABLE: { @@ -592,17 +591,6 @@ } } }, - RESULT: { - "results": { - "missed_in_asic_db_routes": { - "Vnet_v6": { - "routes": [ - "fd01:fc00::1/128" - ] - } - } - } - } }, "13": { DESCR: "A VNET route is missing in STATE DB and another inactive route is missing in ASIC DB", @@ -641,6 +629,34 @@ } } } + }, + "14": { + DESCR: "An IPv4 VNET route that is missing in STATE DB and ASIC DB", + ARGS: "vnet_route_check", + PRE: { + APPL_DB: { + VXLAN_TUNNEL_TABLE: { + "tunnel_v4": {"src_ip": "10.1.0.32"} + }, + VNET_TABLE: { + "Vnet_v4_in_v4-0": [("vxlan_tunnel", "tunnel_v4"), ("scope", "default"), ("vni", "10002"), + ("peer_list", "")] + }, + VNET_ROUTE_TABLE: { + "Vnet_v4_in_v4-0:150.62.191.1/32": {"endpoint": "100.251.7.1,100.251.7.2"} + } + }, + ASIC_DB: { + ASIC_STATE: { + "SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x6000000000d76": { + "SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID": "oid:0x3000000000d4b" + }, + "SAI_OBJECT_TYPE_VIRTUAL_ROUTER": { + "oid:0x3000000000d4b": {"": ""} + }, + } + } + }, } } From 809646a4d3fb1161819f63d9c1c8368116c6e532 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 22 Jul 2025 18:15:14 +0800 Subject: [PATCH 03/21] Revert "Addition of prober_type in config and show commands for muxcable (#3884)" (#3979) This reverts commit b106a821436b8409228d9abbf3728e615f5e63e6. This PR caused issue: #3978 Need to revert it to unblock submodule advance for sonic-utilities. --- config/muxcable.py | 114 ++----------------------------- show/muxcable.py | 26 ++----- tests/mock_tables/config_db.json | 2 - tests/muxcable_test.py | 94 +++++++------------------ 4 files changed, 34 insertions(+), 202 deletions(-) diff --git a/config/muxcable.py b/config/muxcable.py index d7d672a930..a009c6fe4f 100644 --- a/config/muxcable.py +++ b/config/muxcable.py @@ -277,52 +277,14 @@ def lookup_statedb_and_update_configdb(db, per_npu_statedb, config_db, port, sta port_status_dict[port_name] = 'OK' def update_configdb_pck_loss_data(config_db, port, val): - fvs = {} configdb_state = get_value_for_key_in_config_tbl(config_db, port, "state", "MUX_CABLE") - fvs["state"] = configdb_state ipv4_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv4", "MUX_CABLE") - fvs["server_ipv4"] = ipv4_value ipv6_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv6", "MUX_CABLE") - fvs["server_ipv6"] = ipv6_value - soc_ipv4_value = get_optional_value_for_key_in_config_tbl(config_db, port, "soc_ipv4", "MUX_CABLE") - if soc_ipv4_value is not None: - fvs["soc_ipv4"] = soc_ipv4_value - cable_type = get_optional_value_for_key_in_config_tbl(config_db, port, "cable_type", "MUX_CABLE") - if cable_type is not None: - fvs["cable_type"] = cable_type - prober_type_val = get_optional_value_for_key_in_config_tbl(config_db, port, "prober_type", "MUX_CABLE") - if prober_type_val is not None: - fvs["prober_type"] = prober_type_val - fvs["pck_loss_data_reset"] = val try: - config_db.set_entry("MUX_CABLE", port, fvs) - except ValueError as e: - ctx = click.get_current_context() - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) - - -def update_configdb_prober_type(config_db, port, val): - fvs = {} - configdb_state = get_value_for_key_in_config_tbl(config_db, port, "state", "MUX_CABLE") - fvs["state"] = configdb_state - ipv4_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv4", "MUX_CABLE") - fvs["server_ipv4"] = ipv4_value - ipv6_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv6", "MUX_CABLE") - fvs["server_ipv6"] = ipv6_value - soc_ipv4_value = get_optional_value_for_key_in_config_tbl(config_db, port, "soc_ipv4", "MUX_CABLE") - if soc_ipv4_value is not None: - fvs["soc_ipv4"] = soc_ipv4_value - cable_type = get_optional_value_for_key_in_config_tbl(config_db, port, "cable_type", "MUX_CABLE") - if cable_type is not None: - fvs["cable_type"] = cable_type - pck_loss_data = get_optional_value_for_key_in_config_tbl(config_db, port, "pck_loss_data_reset", "MUX_CABLE") - if pck_loss_data is not None: - fvs["pck_loss_data_reset"] = pck_loss_data - - fvs["prober_type"] = val - try: - config_db.set_entry("MUX_CABLE", port, fvs) + config_db.set_entry("MUX_CABLE", port, {"state": configdb_state, + "server_ipv4": ipv4_value, "server_ipv6": ipv6_value, + "pck_loss_data_reset": val}) except ValueError as e: ctx = click.get_current_context() ctx.fail("Invalid ConfigDB. Error: {}".format(e)) @@ -419,73 +381,6 @@ def mode(db, state, port, json_output): sys.exit(CONFIG_SUCCESSFUL) -# 'muxcable' command ("config muxcable probertype hardware/software ") -@muxcable.command() -@click.argument('probertype', metavar='', required=True, type=click.Choice(["hardware", "software"])) -@click.argument('port', metavar='', required=True, default=None) -@clicommon.pass_db -def probertype(db, probertype, port): - """Config muxcable probertype""" - - port = platform_sfputil_helper.get_interface_name(port, db) - - port_table_keys = {} - y_cable_asic_table_keys = {} - per_npu_configdb = {} - per_npu_statedb = {} - - # Getting all front asic namespace and correspding config and state DB connector - - namespaces = multi_asic.get_front_end_namespaces() - for namespace in namespaces: - asic_id = multi_asic.get_asic_index_from_namespace(namespace) - # replace these with correct macros - per_npu_configdb[asic_id] = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) - per_npu_configdb[asic_id].connect() - per_npu_statedb[asic_id] = SonicV2Connector(use_unix_socket_path=True, namespace=namespace) - per_npu_statedb[asic_id].connect(per_npu_statedb[asic_id].STATE_DB) - - port_table_keys[asic_id] = per_npu_statedb[asic_id].keys( - per_npu_statedb[asic_id].STATE_DB, 'MUX_CABLE_TABLE|*') - - if port is not None and port != "all": - - asic_index = None - if platform_sfputil is not None: - asic_index = platform_sfputil.get_asic_id_for_logical_port(port) - if asic_index is None: - # TODO this import is only for unit test purposes, and should be removed once sonic_platform_base - # is fully mocked - import sonic_platform_base.sonic_sfp.sfputilhelper - asic_index = sonic_platform_base.sonic_sfp.sfputilhelper.SfpUtilHelper().get_asic_id_for_logical_port(port) - if asic_index is None: - click.echo("Got invalid asic index for port {}, cant retreive mux status".format(port)) - sys.exit(CONFIG_FAIL) - - if per_npu_statedb[asic_index] is not None: - y_cable_asic_table_keys = port_table_keys[asic_index] - logical_key = "MUX_CABLE_TABLE|{}".format(port) - if logical_key in y_cable_asic_table_keys: - update_configdb_prober_type(per_npu_configdb[asic_index], port, probertype) - sys.exit(CONFIG_SUCCESSFUL) - else: - click.echo("this is not a valid port {} present on mux_cable".format(port)) - sys.exit(CONFIG_FAIL) - else: - click.echo("there is not a valid asic asic-{} table for this asic_index".format(asic_index)) - sys.exit(CONFIG_FAIL) - - elif port == "all" and port is not None: - - for namespace in namespaces: - asic_id = multi_asic.get_asic_index_from_namespace(namespace) - for key in port_table_keys[asic_id]: - logical_port = key.split("|")[1] - update_configdb_prober_type(per_npu_configdb[asic_id], logical_port, probertype) - - sys.exit(CONFIG_SUCCESSFUL) - - # 'muxcable' command ("config muxcable kill-radv ") @muxcable.command(short_help="Kill radv service when it is meant to be stopped, so no good-bye packet is sent for ceasing To Be an Advertising Interface") @click.argument('knob', metavar='', required=True, type=click.Choice(["enable", "disable"])) @@ -501,7 +396,8 @@ def kill_radv(db, knob): mux_lmgrd_cfg_tbl = config_db.get_table("MUX_LINKMGR") config_db.mod_entry("MUX_LINKMGR", "SERVICE_MGMT", {"kill_radv": "True" if knob == "enable" else "False"}) -# 'muxcable' command ("config muxcable packetloss reset ") + + #'muxcable' command ("config muxcable packetloss reset ") @muxcable.command() @click.argument('action', metavar='', required=True, type=click.Choice(["reset"])) @click.argument('port', metavar='', required=True, default=None) diff --git a/show/muxcable.py b/show/muxcable.py index 716325c0f2..cf8403dfce 100644 --- a/show/muxcable.py +++ b/show/muxcable.py @@ -564,26 +564,17 @@ def create_table_dump_per_port_config(db ,print_data, per_npu_configdb, asic_id, cable_type = get_optional_value_for_key_in_config_tbl(per_npu_configdb[asic_id], port, "cable_type", "MUX_CABLE") if cable_type is not None: port_list.append(cable_type) - else: - port_list.append("") soc_ipv4_value = get_optional_value_for_key_in_config_tbl(per_npu_configdb[asic_id], port, "soc_ipv4", "MUX_CABLE") if soc_ipv4_value is not None: port_list.append(soc_ipv4_value) is_dualtor_active_active[0] = True - else: - port_list.append("") soc_ipv6_value = get_optional_value_for_key_in_config_tbl(per_npu_configdb[asic_id], port, "soc_ipv6", "MUX_CABLE") if soc_ipv6_value is not None: + if cable_type is None: + port_list.append("") + if soc_ipv4_value is None: + port_list.append("") port_list.append(soc_ipv6_value) - else: - port_list.append("") - prober_type_value = get_optional_value_for_key_in_config_tbl(per_npu_configdb[asic_id], - port, "prober_type", "MUX_CABLE") - if prober_type_value is not None: - port_list.append(prober_type_value) - else: - port_list.append("software") - print_data.append(port_list) @@ -606,11 +597,6 @@ def create_json_dump_per_port_config(db, port_status_dict, per_npu_configdb, asi soc_ipv6_value = get_optional_value_for_key_in_config_tbl(per_npu_configdb[asic_id], port, "soc_ipv6", "MUX_CABLE") if soc_ipv6_value is not None: port_status_dict["MUX_CABLE"]["PORTS"][port_name]["SERVER"]["soc_ipv6"] = soc_ipv6_value - prober_type_value = get_optional_value_for_key_in_config_tbl(per_npu_configdb[asic_id], - port, "prober_type", "MUX_CABLE") - if prober_type_value is not None: - port_status_dict["MUX_CABLE"]["PORTS"][port_name]["SERVER"]["prober_type"] = prober_type_value - def get_tunnel_route_per_port(db, port_tunnel_route, per_npu_configdb, per_npu_appl_db, per_npu_asic_db, asic_id, port): @@ -887,7 +873,7 @@ def config(db, port, json_output): print_peer_tor.append(peer_tor_data) click.echo(tabulate(print_peer_tor, headers=headers)) if is_dualtor_active_active[0]: - headers = ['port', 'state', 'ipv4', 'ipv6', 'cable_type', 'soc_ipv4', 'soc_ipv6', 'prober_type'] + headers = ['port', 'state', 'ipv4', 'ipv6', 'cable_type', 'soc_ipv4', 'soc_ipv6'] else: headers = ['port', 'state', 'ipv4', 'ipv6'] click.echo(tabulate(print_data, headers=headers)) @@ -939,7 +925,7 @@ def config(db, port, json_output): print_peer_tor.append(peer_tor_data) click.echo(tabulate(print_peer_tor, headers=headers)) if is_dualtor_active_active[0]: - headers = ['port', 'state', 'ipv4', 'ipv6', 'cable_type', 'soc_ipv4', 'soc_ipv6', 'prober_type'] + headers = ['port', 'state', 'ipv4', 'ipv6', 'cable_type', 'soc_ipv4', 'soc_ipv6'] else: headers = ['port', 'state', 'ipv4', 'ipv6'] click.echo(tabulate(print_data, headers=headers)) diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index d1f1c46f3d..9716ed434d 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -1942,14 +1942,12 @@ }, "MUX_CABLE|Ethernet4": { "state": "auto", - "prober_type": "software", "server_ipv4": "10.3.1.1", "server_ipv6": "e801::46", "soc_ipv6": "e801::47" }, "MUX_CABLE|Ethernet8": { "state": "active", - "prober_type": "hardware", "server_ipv4": "10.4.1.1", "server_ipv6": "e802::46" }, diff --git a/tests/muxcable_test.py b/tests/muxcable_test.py index d78a429495..13eefef960 100644 --- a/tests/muxcable_test.py +++ b/tests/muxcable_test.py @@ -143,30 +143,30 @@ SWITCH_NAME PEER_TOR ------------- ---------- sonic-switch 10.2.2.2 -port state ipv4 ipv6 cable_type soc_ipv4 soc_ipv6 prober_type ----------- ------- -------- -------- -------------- ---------- ---------- ------------- -Ethernet0 active 10.2.1.1 e800::46 software -Ethernet4 auto 10.3.1.1 e801::46 e801::47 software -Ethernet8 active 10.4.1.1 e802::46 hardware -Ethernet12 active 10.4.1.1 e802::46 software -Ethernet16 standby 10.1.1.1 fc00::75 active-standby software -Ethernet28 manual 10.1.1.1 fc00::75 software -Ethernet32 auto 10.1.1.1 fc00::75 active-active 10.1.1.2 fc00::76 software +port state ipv4 ipv6 cable_type soc_ipv4 soc_ipv6 +---------- ------- -------- -------- -------------- ---------- ---------- +Ethernet0 active 10.2.1.1 e800::46 +Ethernet4 auto 10.3.1.1 e801::46 e801::47 +Ethernet8 active 10.4.1.1 e802::46 +Ethernet12 active 10.4.1.1 e802::46 +Ethernet16 standby 10.1.1.1 fc00::75 active-standby +Ethernet28 manual 10.1.1.1 fc00::75 +Ethernet32 auto 10.1.1.1 fc00::75 active-active 10.1.1.2 fc00::76 """ tabular_data_config_output_expected_alias = """\ SWITCH_NAME PEER_TOR ------------- ---------- sonic-switch 10.2.2.2 -port state ipv4 ipv6 cable_type soc_ipv4 soc_ipv6 prober_type ------- ------- -------- -------- -------------- ---------- ---------- ------------- -etp1 active 10.2.1.1 e800::46 software -etp2 auto 10.3.1.1 e801::46 e801::47 software -etp3 active 10.4.1.1 e802::46 hardware -etp4 active 10.4.1.1 e802::46 software -etp5 standby 10.1.1.1 fc00::75 active-standby software -etp8 manual 10.1.1.1 fc00::75 software -etp9 auto 10.1.1.1 fc00::75 active-active 10.1.1.2 fc00::76 software +port state ipv4 ipv6 cable_type soc_ipv4 soc_ipv6 +------ ------- -------- -------- -------------- ---------- ---------- +etp1 active 10.2.1.1 e800::46 +etp2 auto 10.3.1.1 e801::46 e801::47 +etp3 active 10.4.1.1 e802::46 +etp4 active 10.4.1.1 e802::46 +etp5 standby 10.1.1.1 fc00::75 active-standby +etp8 manual 10.1.1.1 fc00::75 +etp9 auto 10.1.1.1 fc00::75 active-active 10.1.1.2 fc00::76 """ json_data_status_config_output_expected = """\ @@ -186,16 +186,14 @@ "SERVER": { "IPv4": "10.3.1.1", "IPv6": "e801::46", - "soc_ipv6": "e801::47", - "prober_type": "software" + "soc_ipv6": "e801::47" } }, "Ethernet8": { "STATE": "active", "SERVER": { "IPv4": "10.4.1.1", - "IPv6": "e802::46", - "prober_type": "hardware" + "IPv6": "e802::46" } }, "Ethernet12": { @@ -252,16 +250,14 @@ "SERVER": { "IPv4": "10.3.1.1", "IPv6": "e801::46", - "soc_ipv6": "e801::47", - "prober_type": "software" + "soc_ipv6": "e801::47" } }, "etp3": { "STATE": "active", "SERVER": { "IPv4": "10.4.1.1", - "IPv6": "e802::46", - "prober_type": "hardware" + "IPv6": "e802::46" } }, "etp4": { @@ -726,6 +722,7 @@ def test_muxcable_status_config(self): db = Db() result = runner.invoke(show.cli.commands["muxcable"].commands["config"], obj=db) + assert result.exit_code == 0 assert result.output == tabular_data_config_output_expected @@ -1052,51 +1049,6 @@ def test_config_muxcable_tabular_port_with_incorrect_port(self): assert result.exit_code == 1 - def test_config_muxcable_probertype_hardware_Ethernet0(self): - runner = CliRunner() - db = Db() - - with mock.patch('sonic_platform_base.sonic_sfp.sfputilhelper') as patched_util: - patched_util.SfpUtilHelper.return_value.get_asic_id_for_logical_port.return_value = 0 - result = runner.invoke(config.config.commands["muxcable"].commands["probertype"], [ - "hardware", "Ethernet0"], obj=db) - - assert result.exit_code == 0 - - def test_config_muxcable_probertype_hardware_all(self): - runner = CliRunner() - db = Db() - - with mock.patch('sonic_platform_base.sonic_sfp.sfputilhelper') as patched_util: - patched_util.SfpUtilHelper.return_value.get_asic_id_for_logical_port.return_value = 0 - result = runner.invoke(config.config.commands["muxcable"].commands["probertype"], [ - "hardware", "all"], obj=db) - - assert result.exit_code == 0 - - def test_config_muxcable_probertype_hardware_incorrect_index(self): - runner = CliRunner() - db = Db() - - with mock.patch('sonic_platform_base.sonic_sfp.sfputilhelper') as patched_util: - patched_util.SfpUtilHelper.return_value.get_asic_id_for_logical_port.return_value = 2 - result = runner.invoke(config.config.commands["muxcable"].commands["probertype"], [ - "hardware", "Ethernet0"], obj=db) - - assert result.exit_code == 1 - - def test_config_muxcable_probertype_hardware_incorrect_port(self): - runner = CliRunner() - db = Db() - - with mock.patch('sonic_platform_base.sonic_sfp.sfputilhelper') as patched_util: - patched_util.SfpUtilHelper.return_value.get_asic_id_for_logical_port.return_value = 0 - result = runner.invoke(config.config.commands["muxcable"].commands["probertype"], [ - "hardware", "Ethernet33"], obj=db) - - assert result.exit_code == 1 - - def test_config_muxcable_packetloss_reset_Ethernet0(self): runner = CliRunner() db = Db() From e18640e3dabaaaaa729bde00d865a7b151c31fff Mon Sep 17 00:00:00 2001 From: Shivashankar C R <58802632+cshivashgit@users.noreply.github.com> Date: Wed, 23 Jul 2025 05:22:47 +0530 Subject: [PATCH 04/21] Switchport mode update for 'show interfaces status' (#3788) What I did Fixed "show interfaces status" output for interfaces with switchport mode configuration. How I did it Configured "switchport mode" is fetched from DB for all front panel ports and PortChannel interfaces. "Vlan" column in "show interface status" is displayed based on below considerations: If interface is part of PortChannel - display PortChannel interface name [present behavior] If "switchport mode" is configured on interface [Ethernet/PortChannel] - display configured mode [new behavior] If "switchport mode" is NOT configured on interface but part of a VLAN [Ethernet/PortChannel] - display as "trunk" [present behavior] If "switchport mode" is NOT configured on interface and NOT part of a VLAN [Ethernet/PortChannel] - display as "routed" [present behavior] How to verify it Configure "switchport mode" for an interface/portchannel and verify the output in below show commands show interfaces switchport status show interfaces status Previous command output (if the output of a command-line utility has changed) Previously it was always displaying as trunk even if interface is configured as access root@sonic:~# config switchport mode access Ethernet0 Ethernet0 switched to access mode root@sonic:~# show interfaces switchport status | grep Ethernet0 Ethernet0 access root@sonic:~# root@sonic:~# show interfaces status | grep Ethernet0 Ethernet0 2304,2305,2306,2307 100G 9100 N/A etp0 trunk up up QSFP28 or later N/A New command output (if the output of a command-line utility has changed) root@sonic:~# config switchport mode access Ethernet0 Ethernet0 switched to access mode root@sonic:~# show interfaces switchport status | grep Ethernet0 Ethernet0 access root@sonic:~# root@sonic:~# show interfaces status | grep Ethernet0 Ethernet0 2304,2305,2306,2307 100G 9100 N/A etp0 access up up QSFP28 or later N/A closes #3787 --- scripts/intfutil | 86 +++++++++++++++++++++++++++++++--------------- tests/vlan_test.py | 58 ++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 28 deletions(-) diff --git a/scripts/intfutil b/scripts/intfutil index 69472760d8..daf06ab04d 100755 --- a/scripts/intfutil +++ b/scripts/intfutil @@ -80,33 +80,40 @@ def get_sub_port_intf_list(config_db): return sub_intf_list -def get_interface_vlan_dict(config_db): +def get_interface_sw_mode_dict(config_db, front_panel_ports_list): """ - Get info from REDIS ConfigDB and create interface to vlan mapping + Get info from REDIS ConfigDB and create interface to swithport mode mapping """ - get_int_vlan_configdb_info = config_db.get_table('VLAN_MEMBER') - int_list = [] - vlan_list = [] - for line in get_int_vlan_configdb_info: - vlan_number = line[0] - interface = line[1] - int_list.append(interface) - vlan_list.append(vlan_number) - int_to_vlan_dict = dict(zip(int_list, vlan_list)) - return int_to_vlan_dict + vlan_member_table = config_db.get_table('VLAN_MEMBER') + vlan_member_keys = [] + for _, key in vlan_member_table: + vlan_member_keys.append(key) -def config_db_vlan_port_keys_get(int_to_vlan_dict, front_panel_ports_list, intf_name): + intf_to_sw_mode_dict = {} + for intf_name in front_panel_ports_list: + port = config_db.get_entry('PORT', intf_name) + if "mode" in port: + mode = port['mode'] + elif intf_name in vlan_member_keys: + mode = 'trunk' + else: + mode = 'routed' + intf_to_sw_mode_dict[intf_name] = mode + + return intf_to_sw_mode_dict + + +def config_db_vlan_port_keys_get(intf_to_sw_mode_dict, intf_to_po_dict, intf_name): """ Get interface vlan value and return it. """ - vlan = "routed" - if intf_name in front_panel_ports_list: - if intf_name in int_to_vlan_dict.keys(): - vlan = int_to_vlan_dict[intf_name] - if "Vlan" in vlan: - vlan = "trunk" - return vlan + mode = "routed" + if intf_name in intf_to_po_dict.keys(): + mode = intf_to_po_dict[intf_name] + elif intf_name in intf_to_sw_mode_dict.keys(): + mode = intf_to_sw_mode_dict[intf_name] + return mode def appl_db_keys_get(appl_db, front_panel_ports_list, intf_name): @@ -307,6 +314,31 @@ def create_po_int_dict(po_int_tuple_list): po_int_dict = tuple_to_dict(po_int_tuple_list, temp_dict) return po_int_dict +def create_po_to_sw_mode_dict(config_db, po_int_tuple_list): + """ + This function takes the portchannel to interface tuple + and converts that into an interface to portchannel dictionary + with the portchannels as the key and the mode as the values. + """ + vlan_member_table = config_db.get_table('VLAN_MEMBER') + + vlan_member_keys = [] + for _, key in vlan_member_table: + vlan_member_keys.append(key) + + po_to_sw_mode_dict = {} + for po, intf in po_int_tuple_list: + portchannel = config_db.get_entry('PORTCHANNEL', po) + if "mode" in portchannel: + mode = portchannel['mode'] + elif po in vlan_member_keys: + mode = 'trunk' + else: + mode = 'routed' + + po_to_sw_mode_dict[po] = mode + return po_to_sw_mode_dict + def create_int_to_portchannel_dict(po_int_tuple_list): """ This function takes the portchannel to interface tuple @@ -354,7 +386,7 @@ def po_speed_dict(po_int_dict, appl_db): po_speed_dict = {} return po_speed_dict -def appl_db_portchannel_status_get(appl_db, config_db, po_name, status_type, portchannel_speed_dict, combined_int_to_vlan_po_dict=None): +def appl_db_portchannel_status_get(appl_db, config_db, po_name, status_type, portchannel_speed_dict, po_to_sw_mode_dict=None): """ Get the port status """ @@ -367,8 +399,8 @@ def appl_db_portchannel_status_get(appl_db, config_db, po_name, status_type, por return "N/A" return status if status_type == "vlan": - if combined_int_to_vlan_po_dict and po_name in combined_int_to_vlan_po_dict.keys(): - status = "trunk" + if po_to_sw_mode_dict and po_name in po_to_sw_mode_dict.keys(): + status = po_to_sw_mode_dict[po_name] else: status = "routed" return status @@ -484,7 +516,7 @@ class IntfStatus(object): appl_db_port_status_get(self.db, key, PORT_MTU_STATUS), appl_db_port_status_get(self.db, key, PORT_FEC), appl_db_port_status_get(self.db, key, PORT_ALIAS), - config_db_vlan_port_keys_get(self.combined_int_to_vlan_po_dict, self.front_panel_ports_list, key), + config_db_vlan_port_keys_get(self.intf_to_sw_mode_dict, self.int_po_dict, key), appl_db_port_status_get(self.db, key, PORT_OPER_STATUS), appl_db_port_status_get(self.db, key, PORT_ADMIN_STATUS), port_optics_get(self.db, key, PORT_OPTICS_TYPE), @@ -501,7 +533,7 @@ class IntfStatus(object): appl_db_portchannel_status_get(self.db, self.config_db, po, PORT_MTU_STATUS, self.portchannel_speed_dict), appl_db_portchannel_status_get(self.db, self.config_db, po, PORT_FEC, self.portchannel_speed_dict), appl_db_portchannel_status_get(self.db, self.config_db, po, PORT_ALIAS, self.portchannel_speed_dict), - appl_db_portchannel_status_get(self.db, self.config_db, po, "vlan", self.portchannel_speed_dict, self.combined_int_to_vlan_po_dict), + appl_db_portchannel_status_get(self.db, self.config_db, po, "vlan", self.portchannel_speed_dict, self.po_to_sw_mode_dict), appl_db_portchannel_status_get(self.db, self.config_db, po, PORT_OPER_STATUS, self.portchannel_speed_dict), appl_db_portchannel_status_get(self.db, self.config_db, po, PORT_ADMIN_STATUS, self.portchannel_speed_dict), appl_db_portchannel_status_get(self.db, self.config_db, po, PORT_OPTICS_TYPE, self.portchannel_speed_dict), @@ -523,13 +555,13 @@ class IntfStatus(object): def get_intf_status(self): self.front_panel_ports_list = get_frontpanel_port_list(self.config_db) self.appl_db_keys = appl_db_keys_get(self.db, self.front_panel_ports_list, None) - self.int_to_vlan_dict = get_interface_vlan_dict(self.config_db) + self.intf_to_sw_mode_dict = get_interface_sw_mode_dict(self.config_db, self.front_panel_ports_list) self.get_raw_po_int_configdb_info = get_raw_portchannel_info(self.config_db) self.portchannel_list = get_portchannel_list(self.get_raw_po_int_configdb_info) self.po_int_tuple_list = create_po_int_tuple_list(self.get_raw_po_int_configdb_info) self.po_int_dict = create_po_int_dict(self.po_int_tuple_list) self.int_po_dict = create_int_to_portchannel_dict(self.po_int_tuple_list) - self.combined_int_to_vlan_po_dict = merge_dicts(self.int_to_vlan_dict, self.int_po_dict) + self.po_to_sw_mode_dict = create_po_to_sw_mode_dict(self.config_db, self.po_int_tuple_list) self.portchannel_speed_dict = po_speed_dict(self.po_int_dict, self.db) self.portchannel_keys = self.portchannel_speed_dict.keys() diff --git a/tests/vlan_test.py b/tests/vlan_test.py index 8b0ce1b617..290d1ff081 100644 --- a/tests/vlan_test.py +++ b/tests/vlan_test.py @@ -13,6 +13,10 @@ from importlib import reload import utilities_common.bgp_util as bgp_util +root_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(root_path) +scripts_path = os.path.join(modules_path, "scripts") + IP_VERSION_PARAMS_MAP = { "ipv4": { "table": "VLAN" @@ -296,12 +300,24 @@ """ +def get_intf_switchport_status(self, output: str, interface: str) -> str: + for line in output.splitlines(): + line = line.strip() + if not line or line.startswith("Interface") or line.startswith("----"): + continue + parts = line.split() + if parts[0] == interface and len(parts) >= 2: + return parts[1] + return "interface not found" + + class TestVlan(object): _old_run_bgp_command = None @classmethod def setup_class(cls): - os.environ['UTILITIES_UNIT_TESTING'] = "1" + os.environ["PATH"] += os.pathsep + scripts_path + os.environ['UTILITIES_UNIT_TESTING'] = "2" # ensure that we are working with single asic config cls._old_run_bgp_command = bgp_util.run_bgp_command bgp_util.run_bgp_command = mock.MagicMock( @@ -718,6 +734,9 @@ def test_config_vlan_add_portchannel_member_with_switchport_modes(self): print(result.exit_code) print(result.output) assert result.exit_code == 0 + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + switchport_status = get_intf_switchport_status(self, result.output, "PortChannel0001") + assert "routed" in switchport_status # Configure PortChannel0001 to routed mode again; should give error as it is already in routed mode result = runner.invoke(config.config.commands["switchport"].commands["mode"], @@ -748,6 +767,10 @@ def test_config_vlan_add_portchannel_member_with_switchport_modes(self): print(result.exit_code) print(result.output) assert result.exit_code == 0 + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "PortChannel1001") + assert "access" in switchport_status # Configure PortChannel1001 back to routed mode result = runner.invoke(config.config.commands["switchport"].commands["mode"], @@ -755,6 +778,9 @@ def test_config_vlan_add_portchannel_member_with_switchport_modes(self): print(result.exit_code) print(result.output) assert result.exit_code == 0 + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + switchport_status = get_intf_switchport_status(self, result.output, "PortChannel1001") + assert "routed" in switchport_status # Configure PortChannel1001 to trunk mode result = runner.invoke(config.config.commands["switchport"].commands["mode"], @@ -762,6 +788,9 @@ def test_config_vlan_add_portchannel_member_with_switchport_modes(self): print(result.exit_code) print(result.output) assert result.exit_code == 0 + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + switchport_status = get_intf_switchport_status(self, result.output, "PortChannel1001") + assert "trunk" in switchport_status # Add back PortChannel1001 tagged member to Vlan4000 result = runner.invoke(config.config.commands["vlan"].commands["member"].commands["add"], @@ -1176,6 +1205,10 @@ def test_config_add_del_vlan_and_vlan_member_with_switchport_modes(self, mock_re print(result.output) assert result.exit_code == 0 assert "Ethernet20 switched to access mode" in result.output + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "Ethernet20") + assert "access" in switchport_status # configure Ethernet20 to access mode again; should give error as it is already in access mode result = runner.invoke(config.config.commands["switchport"].commands["mode"], ["access", "Ethernet20"], obj=db) @@ -1215,6 +1248,10 @@ def test_config_add_del_vlan_and_vlan_member_with_switchport_modes(self, mock_re print(result.output) traceback.print_tb(result.exc_info[2]) assert result.exit_code == 0 + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "Ethernet20") + assert "trunk" in switchport_status # show output result = runner.invoke(show.cli.commands["vlan"].commands["brief"], [], obj=db) @@ -1256,6 +1293,11 @@ def test_config_add_del_vlan_and_vlan_member_with_switchport_modes(self, mock_re assert result.exit_code == 0 assert "Ethernet20 switched to routed mode" in result.output + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "Ethernet20") + assert "routed" in switchport_status + # del 1001 result = runner.invoke(config.config.commands["vlan"].commands["del"], ["1001"], obj=db) print(result.exit_code) @@ -1287,6 +1329,11 @@ def test_config_add_del_with_switchport_modes_changes_output( assert result.exit_code == 0 assert "Ethernet20 switched to trunk mode" in result.output + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "Ethernet20") + assert "trunk" in switchport_status + # add Ethernet64 to vlan 1001 but Ethernet64 is in routed mode will give error result = runner.invoke(config.config.commands["vlan"].commands["member"].commands["add"], ["1001", "Ethernet64"], obj=db) @@ -1302,6 +1349,10 @@ def test_config_add_del_with_switchport_modes_changes_output( print(result.output) assert result.exit_code == 0 assert "Ethernet64 switched to trunk mode" in result.output + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "Ethernet20") + assert "trunk" in switchport_status # add Ethernet64 to vlan 1001 result = runner.invoke(config.config.commands["vlan"].commands["member"].commands["add"], @@ -1332,6 +1383,11 @@ def test_config_add_del_with_switchport_modes_changes_output( assert result.exit_code == 0 assert "Ethernet64 switched to access mode" in result.output + result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"], obj=db) + print(result.output) + switchport_status = get_intf_switchport_status(self, result.output, "Ethernet64") + assert "access" in switchport_status + # show output result = runner.invoke(show.cli.commands["vlan"].commands["brief"], [], obj=db) print(result.exit_code) From 036e4e620aab16e29d2e9908e3a9cef438c19b0b Mon Sep 17 00:00:00 2001 From: Wajahat Razi Date: Fri, 25 Jul 2025 01:51:42 +0500 Subject: [PATCH 05/21] [MSTP] CLI Support (#3848) What I did Implemented CLI utilities for MSTP (Multiple Spanning Tree Protocol) in SONiC How I did it Introduced new configuration and show commands to manage MSTP at the global, region, instance, and interface levels. Updated the configuration database schema to support MSTP, including new tables and additions to the existing STP_GLOBAL_TABLE. --- config/stp.py | 1407 ++++++++++++++++---- tests/stp_test.py | 2577 ++++++++++++++++++++++++++++++++----- tests/test_config_mstp.py | 1005 +++++++++++++++ 3 files changed, 4465 insertions(+), 524 deletions(-) create mode 100644 tests/test_config_mstp.py diff --git a/config/stp.py b/config/stp.py index 85d7041847..04adc507d4 100644 --- a/config/stp.py +++ b/config/stp.py @@ -1,13 +1,118 @@ - # -# 'spanning-tree' group ('config spanning-tree ...') +# 'spanning-tree' group ('config spanning-tree ...') # +""" +- There will be mode check in each command to check if the mode is PVST or MST +- For PVST, priority can be set in global table but for MST, + priority is associated with instance ID and will be set in the MST INSTANCE TABLE + + +***Existing PVST commands that are used for MST Commands*** + + === config spanning_tree enable #enable pvst or mst + === config spanning_tree disable #disable pvst or mst + + === config spanning_tree hello #set hello time pvst or mst + + === config spanning_tree max_age #set max age pvst or mst + + === config spanning_tree forward_delay #set forward delay pvst or mst + + + INTERFACE GROUP: + config spanning_tree interface enable #enable pvst or mst on interface + config spanning_tree interface disable #disable pvst or mst on interface + + config spanning_tree interface bpdu_guard enable + config spanning_tree interface bpdu_guard disable + + config spanning_tree interface root_guard enable + config spanning_tree interface root_guard disable + + config spanning_tree interface priority + + config spanning_tree interface cost + + +***NEW MST Commands*** + === config spanning_tree max_hops (Not for PVST) + + MST GROUP: + === config spanning_tree mst region-name + === config spanning_tree mst revision + + config spanning_tree mst instance priority + + config spanning_tree mst instance vlan add + config spanning_tree mst instance vlan del + + config spanning_tree mst instance interface priority + config spanning_tree mst instance interface cost + + INTERFACE GROUP: + config spanning_tree interface edge_port enable #enable edge_port on interface for mst + config spanning_tree interface edge_port disable #disable edge_port on interface for mst + + config spanning_tree interface link_type point-to-point + config spanning_tree interface link_type shared + config spanning_tree interface link_type auto + +""" + import click import utilities_common.cli as clicommon from natsort import natsorted import logging +# MSTP parameters + +MST_MIN_HOPS = 1 +MST_MAX_HOPS = 40 +MST_DEFAULT_HOPS = 20 + +MST_MIN_HELLO_TIME = 1 +MST_MAX_HELLO_TIME = 10 +MST_DEFAULT_HELLO_TIME = 2 + +MST_MIN_MAX_AGE = 6 +MST_MAX_MAX_AGE = 40 +MST_DEFAULT_MAX_AGE = 20 + +MST_MIN_REVISION = 0 +MST_MAX_REVISION = 65535 +MST_DEFAULT_REVISION = 0 + +MST_MIN_BRIDGE_PRIORITY = 0 +MST_MAX_BRIDGE_PRIORITY = 61440 +MST_DEFAULT_BRIDGE_PRIORITY = 32768 + +MST_MIN_PORT_PRIORITY = 0 +MST_MAX_PORT_PRIORITY = 240 +MST_DEFAULT_PORT_PRIORITY = 128 + +MST_MIN_FORWARD_DELAY = 4 +MST_MAX_FORWARD_DELAY = 30 +MST_DEFAULT_FORWARD_DELAY = 15 + +MST_MIN_ROOT_GUARD_TIMEOUT = 5 +MST_MAX_ROOT_GUARD_TIMEOUT = 600 +MST_DEFAULT_ROOT_GUARD_TIMEOUT = 30 + +MST_MIN_INSTANCES = 0 +MST_MAX_INSTANCES = 63 +MST_DEFAULT_INSTANCE = 0 + +MST_MIN_PORT_PATH_COST = 1 +MST_MAX_PORT_PATH_COST = 200000000 +MST_DEFAULT_PORT_PATH_COST = 1 + +MST_AUTO_LINK_TYPE = 'auto' +MST_P2P_LINK_TYPE = 'p2p' +MST_SHARED_LINK_TYPE = 'shared' + +# STP parameters + STP_MIN_ROOT_GUARD_TIMEOUT = 5 STP_MAX_ROOT_GUARD_TIMEOUT = 600 STP_DEFAULT_ROOT_GUARD_TIMEOUT = 30 @@ -136,7 +241,6 @@ def update_stp_vlan_parameter(ctx, db, param_type, new_value): if current_global_value == current_vlan_value: db.mod_entry('STP_VLAN', vlan, {param_type: new_value}) - def check_if_vlan_exist_in_db(db, ctx, vid): vlan_name = 'Vlan{}'.format(vid) vlan = db.get_entry('VLAN', vlan_name) @@ -278,7 +382,7 @@ def enable_stp_for_interfaces(db): def is_global_stp_enabled(db): stp_entry = db.get_entry('STP', "GLOBAL") mode = stp_entry.get("mode") - if mode: + if mode and mode != "none": return True else: return False @@ -319,6 +423,68 @@ def get_global_stp_priority(db): return priority +def get_bridge_mac_address(db): + """Retrieve the bridge MAC address from the CONFIG_DB""" + device_metadata = db.get_entry('DEVICE_METADATA', 'localhost') + bridge_mac_address = device_metadata.get('mac') + return bridge_mac_address + + +def enable_mst_instance0(db): + mst_inst_fvs = { + 'bridge_priority': MST_DEFAULT_BRIDGE_PRIORITY + } + instance_id = 0 + db.set_entry('STP_MST_INST', f"MST_INSTANCE:INSTANCE{instance_id}", mst_inst_fvs) + + +def enable_mst_for_interfaces(db): + fvs_port = { + 'edge_port': 'false', + 'link_type': MST_AUTO_LINK_TYPE, + 'enabled': 'true', + 'bpdu_guard': 'false', + 'bpdu_guard_do': 'false', + 'root_guard': 'false', + 'path_cost': MST_DEFAULT_PORT_PATH_COST, + 'priority': MST_DEFAULT_PORT_PRIORITY + } + + fvs_mst_port = { + 'path_cost': MST_DEFAULT_PORT_PATH_COST, + 'priority': MST_DEFAULT_PORT_PRIORITY + } + + port_dict = natsorted(db.get_table('PORT')) + intf_list_in_vlan_member_table = get_intf_list_in_vlan_member_table(db) + + for port_key in port_dict: + if port_key in intf_list_in_vlan_member_table: + db.set_entry('STP_MST_PORT', f"MST_INSTANCE|0|{port_key}", fvs_mst_port) + db.set_entry('STP_PORT', port_key, fvs_port) + + po_ch_dict = natsorted(db.get_table('PORTCHANNEL')) + for po_ch_key in po_ch_dict: + if po_ch_key in intf_list_in_vlan_member_table: + db.set_entry('STP_MST_PORT', f"MST_INSTANCE|0|{po_ch_key}", fvs_mst_port) + db.set_entry('STP_PORT', po_ch_key, fvs_port) + + +def disable_global_pvst(db): + db.set_entry('STP', "GLOBAL", None) + db.delete_table('STP_VLAN') + db.delete_table('STP_PORT') + db.delete_table('STP_VLAN_PORT') + + +def disable_global_mst(db): + db.set_entry('STP', "GLOBAL", None) + db.delete_table('STP_MST') + db.delete_table('STP_MST_INST') + db.delete_table('STP_MST_PORT') + db.delete_table('STP_PORT') + + @click.group() @clicommon.pass_db def spanning_tree(_db): @@ -331,45 +497,76 @@ def spanning_tree(_db): ############################################### # cmd: STP enable +# Modifies & sets parameters in different tables for MST & PVST +# config spanning_tree enable @spanning_tree.command('enable') -@click.argument('mode', metavar='', required=True, type=click.Choice(["pvst"])) +@click.argument('mode', metavar='', required=True, type=click.Choice(["pvst", "mst"])) @clicommon.pass_db def spanning_tree_enable(_db, mode): """enable STP """ ctx = click.get_current_context() db = _db.cfgdb - if mode == "pvst" and get_global_stp_mode(db) == "pvst": + current_mode = get_global_stp_mode(db) + + if mode == "pvst" and current_mode == "pvst": ctx.fail("PVST is already configured") - fvs = {'mode': mode, - 'rootguard_timeout': STP_DEFAULT_ROOT_GUARD_TIMEOUT, - 'forward_delay': STP_DEFAULT_FORWARD_DELAY, - 'hello_time': STP_DEFAULT_HELLO_INTERVAL, - 'max_age': STP_DEFAULT_MAX_AGE, - 'priority': STP_DEFAULT_BRIDGE_PRIORITY - } - db.set_entry('STP', "GLOBAL", fvs) - # Enable STP for VLAN by default - enable_stp_for_interfaces(db) - enable_stp_for_vlans(db) + elif mode == "mst" and current_mode == "mst": + ctx.fail("MST is already configured") + elif mode == "pvst" and current_mode == "mst": + ctx.fail("MSTP is already configured; please disable MST before enabling PVST") + elif mode == "mst" and current_mode == "pvst": + ctx.fail("PVST is already configured; please disable PVST before enabling MST") + + if mode == "pvst": + fvs = {'mode': mode, + 'rootguard_timeout': STP_DEFAULT_ROOT_GUARD_TIMEOUT, + 'forward_delay': STP_DEFAULT_FORWARD_DELAY, + 'hello_time': STP_DEFAULT_HELLO_INTERVAL, + 'max_age': STP_DEFAULT_MAX_AGE, + 'priority': STP_DEFAULT_BRIDGE_PRIORITY + } + db.set_entry('STP', "GLOBAL", fvs) + + enable_stp_for_interfaces(db) + enable_stp_for_vlans(db) # Enable STP for VLAN by default + + elif mode == "mst": + fvs = {'mode': mode + } + db.set_entry('STP', "GLOBAL", fvs) + + enable_mst_for_interfaces(db) + enable_mst_instance0(db) # cmd: STP disable +# config spanning_tree disable (Modify mode parameter for MST or PVST and Delete tables) +# Modify mode in STP GLOBAL table to None +# Delete tables STP_MST, STP_MST_INST, STP_MST_PORT, and STP_PORT @spanning_tree.command('disable') -@click.argument('mode', metavar='', required=True, type=click.Choice(["pvst"])) +@click.argument('mode', metavar='', required=True, type=click.Choice(["pvst", "mst"])) @clicommon.pass_db def stp_disable(_db, mode): """disable STP """ + ctx = click.get_current_context() db = _db.cfgdb - db.set_entry('STP', "GLOBAL", None) - # Disable STP for all VLANs and interfaces - db.delete_table('STP_VLAN') - db.delete_table('STP_PORT') - db.delete_table('STP_VLAN_PORT') - if get_global_stp_mode(db) == "pvst": - print("Error PVST disable failed") + current_mode = get_global_stp_mode(db) + + if not current_mode or current_mode == "none": + ctx.fail("STP is not configured") + elif mode != current_mode and current_mode != "none": + ctx.fail(f"{mode.upper()} is not currently configured mode") + + if mode == "pvst" and current_mode == "pvst": + disable_global_pvst(db) + elif mode == "mst" and current_mode == "mst": + disable_global_mst(db) + # cmd: STP global root guard timeout +# NOT VALID FOR MST +# config spanning_tree root_guard_timeout <5-600 seconds> @spanning_tree.command('root_guard_timeout') @click.argument('root_guard_timeout', metavar='<5-600 seconds>', required=True, type=int) @clicommon.pass_db @@ -377,12 +574,24 @@ def stp_global_root_guard_timeout(_db, root_guard_timeout): """Configure STP global root guard timeout value""" ctx = click.get_current_context() db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) - is_valid_root_guard_timeout(ctx, root_guard_timeout) - db.mod_entry('STP', "GLOBAL", {'rootguard_timeout': root_guard_timeout}) + + current_mode = get_global_stp_mode(db) + + if current_mode == "mst": + ctx.fail("Root guard timeout not supported for MST") + + elif current_mode == "pvst": + is_valid_root_guard_timeout(ctx, root_guard_timeout) + db.mod_entry('STP', "GLOBAL", {'rootguard_timeout': root_guard_timeout}) + else: + ctx.fail("Invalid STP mode configuration, no mode is enabled") # cmd: STP global forward delay +# MST CONFIGURATION IN THE STP_MST GLOBAL TABLE +# config spanning_tree forward_delay <4-30 seconds> @spanning_tree.command('forward_delay') @click.argument('forward_delay', metavar='<4-30 seconds>', required=True, type=int) @clicommon.pass_db @@ -390,14 +599,28 @@ def stp_global_forward_delay(_db, forward_delay): """Configure STP global forward delay""" ctx = click.get_current_context() db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) - is_valid_forward_delay(ctx, forward_delay) - is_valid_stp_global_parameters(ctx, db, "forward_delay", forward_delay) - update_stp_vlan_parameter(ctx, db, "forward_delay", forward_delay) - db.mod_entry('STP', "GLOBAL", {'forward_delay': forward_delay}) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + is_valid_forward_delay(ctx, forward_delay) + is_valid_stp_global_parameters(ctx, db, "forward_delay", forward_delay) + update_stp_vlan_parameter(ctx, db, "forward_delay", forward_delay) + db.mod_entry('STP', "GLOBAL", {'forward_delay': forward_delay}) + + elif current_mode == "mst": + is_valid_forward_delay(ctx, forward_delay) + db.mod_entry('STP_MST', "GLOBAL", {'forward_delay': forward_delay}) + + else: + ctx.fail("Invalid STP mode configuration, no mode is enabled") # cmd: STP global hello interval +# MST CONFIGURATION IN THE STP_MST GLOBAL TABLE +# config spanning_tree hello <1-10 seconds> @spanning_tree.command('hello') @click.argument('hello_interval', metavar='<1-10 seconds>', required=True, type=int) @clicommon.pass_db @@ -405,29 +628,89 @@ def stp_global_hello_interval(_db, hello_interval): """Configure STP global hello interval""" ctx = click.get_current_context() db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) - is_valid_hello_interval(ctx, hello_interval) - is_valid_stp_global_parameters(ctx, db, "hello_time", hello_interval) - update_stp_vlan_parameter(ctx, db, "hello_time", hello_interval) - db.mod_entry('STP', "GLOBAL", {'hello_time': hello_interval}) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + is_valid_hello_interval(ctx, hello_interval) + is_valid_stp_global_parameters(ctx, db, "hello_time", hello_interval) + update_stp_vlan_parameter(ctx, db, "hello_time", hello_interval) + db.mod_entry('STP', "GLOBAL", {'hello_time': hello_interval}) + + elif current_mode == "mst": + is_valid_hello_interval(ctx, hello_interval) + db.mod_entry('STP_MST', "GLOBAL", {'hello_time': hello_interval}) + + else: + ctx.fail("Invalid STP mode configuration, no mode is enabled") # cmd: STP global max age +# MST CONFIGURATION IN THE STP_MST GLOBAL TABLE +# config spanning_tree max_age <6-40 seconds> @spanning_tree.command('max_age') @click.argument('max_age', metavar='<6-40 seconds>', required=True, type=int) @clicommon.pass_db def stp_global_max_age(_db, max_age): """Configure STP global max_age""" + ctx = click.get_current_context() # Ensure we are getting the correct context + db = _db.cfgdb + + # Check if global STP is enabled + check_if_global_stp_enabled(db, ctx) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + # Validate max_age for PVST mode + is_valid_max_age(ctx, max_age) + is_valid_stp_global_parameters(ctx, db, "max_age", max_age) + update_stp_vlan_parameter(ctx, db, "max_age", max_age) + db.mod_entry('STP', "GLOBAL", {'max_age': max_age}) + + elif current_mode == "mst": + # Validate max_age for MST mode + is_valid_max_age(ctx, max_age) + db.mod_entry('STP_MST', "GLOBAL", {'max_age': max_age}) + + else: + # If the mode is invalid, fail with an error message + ctx.fail("Invalid STP mode configuration, no mode is enabled") + + +# cmd: STP global max hop +# NO GLOBAL MAX HOP FOR PVST +# MST CONFIGURATION IN THE STP_MST GLOBAL TABLE +# config spanning_tree max_hops <6-40 seconds> +@spanning_tree.command('max_hops') +@click.argument('max_hops', metavar='<1-40>', required=True, type=int) +@clicommon.pass_db +def stp_global_max_hops(_db, max_hops): + """Configure STP global max_hops""" ctx = click.get_current_context() db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) - is_valid_max_age(ctx, max_age) - is_valid_stp_global_parameters(ctx, db, "max_age", max_age) - update_stp_vlan_parameter(ctx, db, "max_age", max_age) - db.mod_entry('STP', "GLOBAL", {'max_age': max_age}) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + ctx.fail("Max hops not supported for PVST") + + elif current_mode == "mst": + if max_hops not in range(MST_MIN_HOPS, MST_MAX_HOPS + 1): + ctx.fail("STP max hops must be in range 1-40") + db.mod_entry('STP_MST', "GLOBAL", {'max_hops': max_hops}) + else: + ctx.fail("Invalid STP mode configured") +# Bridge priority cannot be set without Instance ID # cmd: STP global bridge priority +# NOT SET FOR MST +# config spanning_tree priority <0-61440> @spanning_tree.command('priority') @click.argument('priority', metavar='<0-61440>', required=True, type=int) @clicommon.pass_db @@ -435,15 +718,87 @@ def stp_global_priority(_db, priority): """Configure STP global bridge priority""" ctx = click.get_current_context() db = _db.cfgdb + + check_if_global_stp_enabled(db, ctx) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + is_valid_bridge_priority(ctx, priority) + update_stp_vlan_parameter(ctx, db, "priority", priority) + db.mod_entry('STP', "GLOBAL", {'priority': priority}) + + elif current_mode == "mst": + ctx.fail("Bridge priority cannot be set for MST with this command without Instance ID") + + else: + ctx.fail("Invalid STP mode configured") + + +# config spanning_tree mst +@spanning_tree.group() +def mst(): + """Configure MSTP region, instance, show, clear & debug commands""" + pass + + +# MST REGION commands implementation +# cmd: MST region-name +# MST CONFIGURATION IN THE STP_MST GLOBAL TABLE +# config spanning_tree mst region-name +@mst.command('region-name') +@click.argument('region_name', metavar='', required=True) +@clicommon.pass_db +def stp_mst_region_name(_db, region_name): + """Configure MSTP region name""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + ctx.fail("Configuration not supported for PVST") + + elif current_mode == "mst": + if len(region_name) >= 32: + ctx.fail("Region name must be less than 32 characters") + + db.mod_entry('STP_MST', "GLOBAL", {'name': region_name}) + + +# cmd: MST Global revision number +# MST CONFIGURATION IN THE STP_MST GLOBAL TABLE +# config spanning_tree mst revision <0-65535> +@mst.command('revision') +@click.argument('revision', metavar='<0-65535>', required=True, type=int) +@clicommon.pass_db +def stp_global_revision(_db, revision): + """Configure STP global revision number""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) - is_valid_bridge_priority(ctx, priority) - update_stp_vlan_parameter(ctx, db, "priority", priority) - db.mod_entry('STP', "GLOBAL", {'priority': priority}) + + current_mode = get_global_stp_mode(db) + + if current_mode == "pvst": + ctx.fail("Configuration not supported for PVST") + + elif current_mode == "mst": + # Valid revisions are inclusive: [MST_MIN_REVISION, MST_MAX_REVISION] + if revision not in range(MST_MIN_REVISION, MST_MAX_REVISION + 1): + ctx.fail("STP revision number must be in range 0-65535") + + db.mod_entry('STP_MST', "GLOBAL", {'revision': revision}) + # db.mod_entry('STP_MST', "STP_MST|GLOBAL", {'revision': revision}) ############################################### # STP VLAN commands implementation ############################################### + +# config spanning_tree vlan @spanning_tree.group('vlan') @clicommon.pass_db def spanning_tree_vlan(_db): @@ -465,6 +820,8 @@ def check_if_stp_enabled_for_vlan(ctx, db, vlan_name): ctx.fail("STP is not enabled for VLAN") +# Not for MST +# config spanning_tree vlan enable @spanning_tree_vlan.command('enable') @click.argument('vid', metavar='', required=True, type=int) @clicommon.pass_db @@ -472,34 +829,40 @@ def stp_vlan_enable(_db, vid): """Enable STP for a VLAN""" ctx = click.get_current_context() db = _db.cfgdb - check_if_vlan_exist_in_db(db, ctx, vid) - vlan_name = 'Vlan{}'.format(vid) - if is_stp_enabled_for_vlan(db, vlan_name): - ctx.fail("STP is already enabled for " + vlan_name) - if get_stp_enabled_vlan_count(db) >= get_max_stp_instances(): - ctx.fail("Exceeded maximum STP configurable VLAN instances") - check_if_global_stp_enabled(db, ctx) - # when enabled for first time, create VLAN entry with - # global values - else update only VLAN STP state - stp_vlan_entry = db.get_entry('STP_VLAN', vlan_name) - if len(stp_vlan_entry) == 0: - fvs = {'enabled': 'true', - 'forward_delay': get_global_stp_forward_delay(db), - 'hello_time': get_global_stp_hello_time(db), - 'max_age': get_global_stp_max_age(db), - 'priority': get_global_stp_priority(db) - } - db.set_entry('STP_VLAN', vlan_name, fvs) - else: - db.mod_entry('STP_VLAN', vlan_name, {'enabled': 'true'}) - # Refresh stp_vlan_intf entry for vlan - for vlan, intf in db.get_table('STP_VLAN_PORT'): - if vlan == vlan_name: - vlan_intf_key = "{}|{}".format(vlan_name, intf) - vlan_intf_entry = db.get_entry('STP_VLAN_PORT', vlan_intf_key) - db.mod_entry('STP_VLAN_PORT', vlan_intf_key, vlan_intf_entry) + + current_mode = get_global_stp_mode(db) + + if current_mode == "mst": + ctx.fail("Configuration not supported for MST") + + elif current_mode == "pvst": + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + if is_stp_enabled_for_vlan(db, vlan_name): + ctx.fail("STP is already enabled for " + vlan_name) + if get_stp_enabled_vlan_count(db) >= get_max_stp_instances(): + ctx.fail("Exceeded maximum STP configurable VLAN instances") + check_if_global_stp_enabled(db, ctx) + stp_vlan_entry = db.get_entry('STP_VLAN', vlan_name) + if len(stp_vlan_entry) == 0: + fvs = {'enabled': 'true', + 'forward_delay': get_global_stp_forward_delay(db), + 'hello_time': get_global_stp_hello_time(db), + 'max_age': get_global_stp_max_age(db), + 'priority': get_global_stp_priority(db)} + db.set_entry('STP_VLAN', vlan_name, fvs) + else: + db.mod_entry('STP_VLAN', vlan_name, {'enabled': 'true'}) + # Refresh stp_vlan_intf entry for vlan + for vlan, intf in db.get_table('STP_VLAN_PORT'): + if vlan == vlan_name: + vlan_intf_key = "{}|{}".format(vlan_name, intf) + vlan_intf_entry = db.get_entry('STP_VLAN_PORT', vlan_intf_key) + db.mod_entry('STP_VLAN_PORT', vlan_intf_key, vlan_intf_entry) +# Not for MST +# config spanning_tree vlan disable @spanning_tree_vlan.command('disable') @click.argument('vid', metavar='', required=True, type=int) @clicommon.pass_db @@ -507,11 +870,19 @@ def stp_vlan_disable(_db, vid): """Disable STP for a VLAN""" ctx = click.get_current_context() db = _db.cfgdb - check_if_vlan_exist_in_db(db, ctx, vid) - vlan_name = 'Vlan{}'.format(vid) - db.mod_entry('STP_VLAN', vlan_name, {'enabled': 'false'}) + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Configuration not supported for MST") + elif current_mode == "pvst": + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + db.mod_entry('STP_VLAN', vlan_name, {'enabled': 'false'}) + + +# not for MST +# config spanning_tree vlan forward_delay <4-30 seconds> @spanning_tree_vlan.command('forward_delay') @click.argument('vid', metavar='', required=True, type=int) @click.argument('forward_delay', metavar='<4-30 seconds>', required=True, type=int) @@ -520,14 +891,21 @@ def stp_vlan_forward_delay(_db, vid, forward_delay): """Configure STP forward delay for VLAN""" ctx = click.get_current_context() db = _db.cfgdb - check_if_vlan_exist_in_db(db, ctx, vid) - vlan_name = 'Vlan{}'.format(vid) - check_if_stp_enabled_for_vlan(ctx, db, vlan_name) - is_valid_forward_delay(ctx, forward_delay) - is_valid_stp_vlan_parameters(ctx, db, vlan_name, "forward_delay", forward_delay) - db.mod_entry('STP_VLAN', vlan_name, {'forward_delay': forward_delay}) + + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Configuration not supported for MST") + elif current_mode == "pvst": + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_forward_delay(ctx, forward_delay) + is_valid_stp_vlan_parameters(ctx, db, vlan_name, "forward_delay", forward_delay) + db.mod_entry('STP_VLAN', vlan_name, {'forward_delay': forward_delay}) +# Not for MST +# config spanning_tree vlan hello <1-10 seconds> @spanning_tree_vlan.command('hello') @click.argument('vid', metavar='', required=True, type=int) @click.argument('hello_interval', metavar='<1-10 seconds>', required=True, type=int) @@ -536,14 +914,21 @@ def stp_vlan_hello_interval(_db, vid, hello_interval): """Configure STP hello interval for VLAN""" ctx = click.get_current_context() db = _db.cfgdb - check_if_vlan_exist_in_db(db, ctx, vid) - vlan_name = 'Vlan{}'.format(vid) - check_if_stp_enabled_for_vlan(ctx, db, vlan_name) - is_valid_hello_interval(ctx, hello_interval) - is_valid_stp_vlan_parameters(ctx, db, vlan_name, "hello_time", hello_interval) - db.mod_entry('STP_VLAN', vlan_name, {'hello_time': hello_interval}) + + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Configuration not supported for MST") + elif current_mode == "pvst": + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_hello_interval(ctx, hello_interval) + is_valid_stp_vlan_parameters(ctx, db, vlan_name, "hello_time", hello_interval) + db.mod_entry('STP_VLAN', vlan_name, {'hello_time': hello_interval}) +# not for MST +# config spanning_tree vlan max_age <6-40 seconds> @spanning_tree_vlan.command('max_age') @click.argument('vid', metavar='', required=True, type=int) @click.argument('max_age', metavar='<6-40 seconds>', required=True, type=int) @@ -552,14 +937,21 @@ def stp_vlan_max_age(_db, vid, max_age): """Configure STP max age for VLAN""" ctx = click.get_current_context() db = _db.cfgdb - check_if_vlan_exist_in_db(db, ctx, vid) - vlan_name = 'Vlan{}'.format(vid) - check_if_stp_enabled_for_vlan(ctx, db, vlan_name) - is_valid_max_age(ctx, max_age) - is_valid_stp_vlan_parameters(ctx, db, vlan_name, "max_age", max_age) - db.mod_entry('STP_VLAN', vlan_name, {'max_age': max_age}) + + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Configuration not supported for MST") + elif current_mode == "pvst": + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_max_age(ctx, max_age) + is_valid_stp_vlan_parameters(ctx, db, vlan_name, "max_age", max_age) + db.mod_entry('STP_VLAN', vlan_name, {'max_age': max_age}) +# not for MST +# config spanning_tree vlan priority <0-61440> @spanning_tree_vlan.command('priority') @click.argument('vid', metavar='', required=True, type=int) @click.argument('priority', metavar='<0-61440>', required=True, type=int) @@ -568,11 +960,16 @@ def stp_vlan_priority(_db, vid, priority): """Configure STP bridge priority for VLAN""" ctx = click.get_current_context() db = _db.cfgdb - check_if_vlan_exist_in_db(db, ctx, vid) - vlan_name = 'Vlan{}'.format(vid) - check_if_stp_enabled_for_vlan(ctx, db, vlan_name) - is_valid_bridge_priority(ctx, priority) - db.mod_entry('STP_VLAN', vlan_name, {'priority': priority}) + + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Configuration not supported for MST") + elif current_mode == "pvst": + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_bridge_priority(ctx, priority) + db.mod_entry('STP_VLAN', vlan_name, {'priority': priority}) ############################################### @@ -609,6 +1006,7 @@ def check_if_interface_is_valid(ctx, db, interface_name): ctx.fail(" {} has no VLAN configured - It's not a L2 interface".format(interface_name)) +# config spanning_tree interface @spanning_tree.group('interface') @clicommon.pass_db def spanning_tree_interface(_db): @@ -616,42 +1014,6 @@ def spanning_tree_interface(_db): pass -@spanning_tree_interface.command('enable') -@click.argument('interface_name', metavar='', required=True) -@clicommon.pass_db -def stp_interface_enable(_db, interface_name): - """Enable STP for interface""" - ctx = click.get_current_context() - db = _db.cfgdb - check_if_global_stp_enabled(db, ctx) - if is_stp_enabled_for_interface(db, interface_name): - ctx.fail("STP is already enabled for " + interface_name) - check_if_interface_is_valid(ctx, db, interface_name) - stp_intf_entry = db.get_entry('STP_PORT', interface_name) - if len(stp_intf_entry) == 0: - fvs = {'enabled': 'true', - 'root_guard': 'false', - 'bpdu_guard': 'false', - 'bpdu_guard_do_disable': 'false', - 'portfast': 'false', - 'uplink_fast': 'false'} - db.set_entry('STP_PORT', interface_name, fvs) - else: - db.mod_entry('STP_PORT', interface_name, {'enabled': 'true'}) - - -@spanning_tree_interface.command('disable') -@click.argument('interface_name', metavar='', required=True) -@clicommon.pass_db -def stp_interface_disable(_db, interface_name): - """Disable STP for interface""" - ctx = click.get_current_context() - db = _db.cfgdb - check_if_global_stp_enabled(db, ctx) - check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'enabled': 'false'}) - - # STP interface port priority STP_INTERFACE_MIN_PRIORITY = 0 STP_INTERFACE_MAX_PRIORITY = 240 @@ -663,31 +1025,6 @@ def is_valid_interface_priority(ctx, intf_priority): ctx.fail("STP interface priority must be in range 0-240") -@spanning_tree_interface.command('priority') -@click.argument('interface_name', metavar='', required=True) -@click.argument('priority', metavar='<0-240>', required=True, type=int) -@clicommon.pass_db -def stp_interface_priority(_db, interface_name, priority): - """Configure STP port priority for interface""" - ctx = click.get_current_context() - db = _db.cfgdb - check_if_stp_enabled_for_interface(ctx, db, interface_name) - check_if_interface_is_valid(ctx, db, interface_name) - is_valid_interface_priority(ctx, priority) - curr_intf_proirty = db.get_entry('STP_PORT', interface_name).get('priority') - db.mod_entry('STP_PORT', interface_name, {'priority': priority}) - # update interface priority in all stp_vlan_intf entries if entry exists - for vlan, intf in db.get_table('STP_VLAN_PORT'): - if intf == interface_name: - vlan_intf_key = "{}|{}".format(vlan, interface_name) - vlan_intf_entry = db.get_entry('STP_VLAN_PORT', vlan_intf_key) - if len(vlan_intf_entry) != 0: - vlan_intf_priority = vlan_intf_entry.get('priority') - if curr_intf_proirty == vlan_intf_priority: - db.mod_entry('STP_VLAN_PORT', vlan_intf_key, {'priority': priority}) - # end - - # STP interface port path cost STP_INTERFACE_MIN_PATH_COST = 1 STP_INTERFACE_MAX_PATH_COST = 200000000 @@ -698,6 +1035,7 @@ def is_valid_interface_path_cost(ctx, intf_path_cost): ctx.fail("STP interface path cost must be in range 1-200000000") +# config spanning_tree interface cost @spanning_tree_interface.command('cost') @click.argument('interface_name', metavar='', required=True) @click.argument('cost', metavar='<1-200000000>', required=True, type=int) @@ -720,147 +1058,217 @@ def stp_interface_path_cost(_db, interface_name, cost): vlan_intf_cost = vlan_intf_entry.get('path_cost') if curr_intf_cost == vlan_intf_cost: db.mod_entry('STP_VLAN_PORT', vlan_intf_key, {'path_cost': cost}) - # end -# STP interface root guard -@spanning_tree_interface.group('root_guard') +# STP interface portfast +# config spanning_tree interface portfast +# Only for PVST +@spanning_tree_interface.group('portfast') @clicommon.pass_db -def spanning_tree_interface_root_guard(_db): - """Configure STP root guard for interface""" +def spanning_tree_interface_portfast(_db): + """Configure STP portfast for interface""" pass -@spanning_tree_interface_root_guard.command('enable') +# config spanning_tree interface portfast enable +# MST CONFIGURATION IN THE STP_PORT TABLE +# It should the mode attribute in the STP global table +# If the mode is MST, then it should tell that the mode if MST, and not allow to configure portfast +@spanning_tree_interface_portfast.command('enable') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_root_guard_enable(_db, interface_name): - """Enable STP root guard for interface""" +def stp_interface_portfast_enable(_db, interface_name): + """Enable STP portfast for interface""" ctx = click.get_current_context() db = _db.cfgdb + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Portfast is supported only for PVST mode") check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'root_guard': 'true'}) + db.mod_entry('STP_PORT', interface_name, {'portfast': 'true'}) -@spanning_tree_interface_root_guard.command('disable') +# config spanning_tree interface portfast disable +# MST CONFIGURATION IN THE STP_PORT TABLE +# It should the mode attribute in the STP global table +# If the mode is MST, then it should tell that the mode if mst, and this cannot be done. +@spanning_tree_interface_portfast.command('disable') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_root_guard_disable(_db, interface_name): - """Disable STP root guard for interface""" +def stp_interface_portfast_disable(_db, interface_name): + """Disable STP portfast for interface""" ctx = click.get_current_context() db = _db.cfgdb + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Portfast is supported only for PVST mode") check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'root_guard': 'false'}) + db.mod_entry('STP_PORT', interface_name, {'portfast': 'false'}) -# STP interface bpdu guard -@spanning_tree_interface.group('bpdu_guard') +# config spanning_tree interface edge_port +# Only for MST + +@spanning_tree_interface.group('edge_port') @clicommon.pass_db -def spanning_tree_interface_bpdu_guard(_db): - """Configure STP bpdu guard for interface""" +def spanning_tree_interface_edge_port(_db): + """Configure STP edge_port for interface""" pass +# config spanning_tree interface edge_port enable +# This should check the mode attribute in the STP global table. +# If the mode is PVST, it should not allow configuring edge_port. -@spanning_tree_interface_bpdu_guard.command('enable') + +@spanning_tree_interface_edge_port.command('enable') @click.argument('interface_name', metavar='', required=True) -@click.option('-s', '--shutdown', is_flag=True) @clicommon.pass_db -def stp_interface_bpdu_guard_enable(_db, interface_name, shutdown): - """Enable STP bpdu guard for interface""" +def stp_interface_edge_port_enable(_db, interface_name): + """Enable STP edge_port for interface""" ctx = click.get_current_context() db = _db.cfgdb + + # Check if STP is enabled globally + check_if_global_stp_enabled(db, ctx) + + # Get the global STP mode + current_mode = get_global_stp_mode(db) + + # Ensure mode is MSTP, otherwise fail + if current_mode == "pvst": + ctx.fail("edge_port configuration is not supported in PVST mode. This command is only allowed in MSTP mode.") + + # Validate the interface check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - if shutdown is True: - bpdu_guard_do_disable = 'true' - else: - bpdu_guard_do_disable = 'false' - fvs = {'bpdu_guard': 'true', - 'bpdu_guard_do_disable': bpdu_guard_do_disable} - db.mod_entry('STP_PORT', interface_name, fvs) + # Enable edge_port for the interface + db.mod_entry('STP_PORT', interface_name, {'edge_port': 'true'}) -@spanning_tree_interface_bpdu_guard.command('disable') + click.echo(f"edge_port enabled on {interface_name} in MSTP mode.") + + +# config spanning_tree interface edge_port disable +# MST CONFIGURATION IN THE STP_PORT TABLE +# It should the mode attribute in the STP global table +# If the mode is PVST, then it should tell that the mode if PVST, and this cannot be done. + + +@spanning_tree_interface_edge_port.command('disable') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_bpdu_guard_disable(_db, interface_name): - """Disable STP bpdu guard for interface""" +def stp_interface_edge_port_disable(_db, interface_name): + """Disable STP edge_port for interface""" ctx = click.get_current_context() db = _db.cfgdb check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'bpdu_guard': 'false'}) + db.mod_entry('STP_PORT', interface_name, {'edge_port': 'false'}) -# STP interface portfast -@spanning_tree_interface.group('portfast') +# STP interface root uplink_fast +# config spanning_tree interface uplink_fast +# Only for PVST +# It should also check if the mode is PVST, else not configure +@spanning_tree_interface.group('uplink_fast') @clicommon.pass_db -def spanning_tree_interface_portfast(_db): - """Configure STP portfast for interface""" +def spanning_tree_interface_uplink_fast(_db): + """Configure STP uplink fast for interface""" pass -@spanning_tree_interface_portfast.command('enable') +# config spanning_tree interface uplink_fast enable +# Not for MST +@spanning_tree_interface_uplink_fast.command('enable') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_portfast_enable(_db, interface_name): - """Enable STP portfast for interface""" +def stp_interface_uplink_fast_enable(_db, interface_name): + """Enable STP uplink fast for interface""" ctx = click.get_current_context() db = _db.cfgdb + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Uplink fast is supported only for PVST mode") check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'portfast': 'true'}) + db.mod_entry('STP_PORT', interface_name, {'uplink_fast': 'true'}) -@spanning_tree_interface_portfast.command('disable') +# config spanning_tree interface uplink_fast disable +# Not for MST +@spanning_tree_interface_uplink_fast.command('disable') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_portfast_disable(_db, interface_name): - """Disable STP portfast for interface""" +def stp_interface_uplink_fast_disable(_db, interface_name): + """Disable STP uplink fast for interface""" ctx = click.get_current_context() db = _db.cfgdb + current_mode = get_global_stp_mode(db) + if current_mode == "mst": + ctx.fail("Uplink fast is supported only for PVST mode") check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'portfast': 'false'}) + db.mod_entry('STP_PORT', interface_name, {'uplink_fast': 'false'}) -# STP interface root uplink_fast -@spanning_tree_interface.group('uplink_fast') +# config spanning_tree interface link_type +@spanning_tree_interface.group('link_type') @clicommon.pass_db -def spanning_tree_interface_uplink_fast(_db): - """Configure STP uplink fast for interface""" +def spanning_tree_interface_link_type(_db): + """Configure STP link type for interface""" pass +# config spanning_tree interface link_type point-to-point -@spanning_tree_interface_uplink_fast.command('enable') + +@spanning_tree_interface_link_type.command('point-to-point') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_uplink_fast_enable(_db, interface_name): - """Enable STP uplink fast for interface""" +def stp_interface_link_type_point_to_point(_db, interface_name): + """Configure STP link type as point-to-point for interface""" ctx = click.get_current_context() db = _db.cfgdb check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'uplink_fast': 'true'}) + db.mod_entry('STP_PORT', interface_name, {'link_type': 'point-to-point'}) -@spanning_tree_interface_uplink_fast.command('disable') +# config spanning_tree interface link_type shared +@spanning_tree_interface_link_type.command('shared') @click.argument('interface_name', metavar='', required=True) @clicommon.pass_db -def stp_interface_uplink_fast_disable(_db, interface_name): - """Disable STP uplink fast for interface""" +def stp_interface_link_type_shared(_db, interface_name): + """Configure STP link type as shared for interface""" ctx = click.get_current_context() db = _db.cfgdb check_if_stp_enabled_for_interface(ctx, db, interface_name) check_if_interface_is_valid(ctx, db, interface_name) - db.mod_entry('STP_PORT', interface_name, {'uplink_fast': 'false'}) + db.mod_entry('STP_PORT', interface_name, {'link_type': 'shared'}) + + +# config spanning_tree interface link_type auto +@spanning_tree_interface_link_type.command('auto') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_link_type_auto(_db, interface_name): + """Configure STP link type as auto for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + current_mode = get_global_stp_mode(db) + if current_mode == "pvst": + ctx.fail("Link type configuration is not supported in PVST mode") + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'link_type': 'auto'}) ############################################### # STP interface per VLAN commands implementation ############################################### + +# config spanning_tree vlan interface @spanning_tree_vlan.group('interface') @clicommon.pass_db def spanning_tree_vlan_interface(_db): @@ -873,7 +1281,7 @@ def is_valid_vlan_interface_priority(ctx, priority): if priority not in range(STP_INTERFACE_MIN_PRIORITY, STP_INTERFACE_MAX_PRIORITY + 1): ctx.fail("STP per vlan port priority must be in range 0-240") - +# config spanning_tree vlan interface priority @spanning_tree_vlan_interface.command('priority') @click.argument('vid', metavar='', required=True, type=int) @click.argument('interface_name', metavar='', required=True) @@ -893,6 +1301,7 @@ def stp_vlan_interface_priority(_db, vid, interface_name, priority): db.mod_entry('STP_VLAN_PORT', vlan_interface, {'priority': priority}) +# config spanning_tree vlan interface cost @spanning_tree_vlan_interface.command('cost') @click.argument('vid', metavar='', required=True, type=int) @click.argument('interface_name', metavar='', required=True) @@ -912,6 +1321,574 @@ def stp_vlan_interface_cost(_db, vid, interface_name, cost): db.mod_entry('STP_VLAN_PORT', vlan_interface, {'path_cost': cost}) -# Invoke main() -# if __name__ == '__main__': -# spanning_tree() +# INTERFACE-LEVEL COMMANDS +# Command: config spanning_tree interface {enable} +# Configure an interface for MSTP. + + +@spanning_tree_interface.command('enable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_enable(_db, interface_name): + """Enable STP for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + + # Check and display the current STP mode + stp_global_entry = db.get_entry('STP_GLOBAL', 'GLOBAL') + current_mode = stp_global_entry.get('mode', 'none') + click.echo(f"Current STP mode: {current_mode}") + if current_mode == "none": + ctx.fail("Global STP is not enabled - first configure STP mode") + + check_if_global_stp_enabled(db, ctx) + if is_stp_enabled_for_interface(db, interface_name): + ctx.fail(f"STP is already enabled for {interface_name}") + check_if_interface_is_valid(ctx, db, interface_name) + + # Set the common attributes + fvs = { + 'enabled': 'true', + 'root_guard': 'false', + 'bpdu_guard': 'false', + 'bpdu_guard_do_disable': 'false' + } + + # Add mode-specific attributes + if current_mode == 'mstp': + fvs.update({ + 'edge_port': 'false', + 'link_type': 'auto', + 'portfast': 'false', + 'uplink_fast': 'false' + }) + elif current_mode == 'pvst': + fvs.update({ + 'portfast': 'false', + 'uplink_fast': 'false' + }) + else: + click.echo("No STP mode selected. Please select a mode first.") + return + + fvs = {'enabled': 'true'} + db.set_entry('STP_PORT', interface_name, fvs) + click.echo(f"Mode {current_mode} is enabled for interface {interface_name}") + + +# Command: config spanning_tree interface {disable} +# Configure an interface for MSTP. +@spanning_tree_interface.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_disable(_db, interface_name): + """Disable STP for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + + # Check and display the current STP mode + stp_global_entry = db.get_entry('STP_GLOBAL', 'GLOBAL') + current_mode = stp_global_entry.get('mode', 'none') + click.echo(f"Current STP mode: {current_mode}") + + check_if_global_stp_enabled(db, ctx) + check_if_interface_is_valid(ctx, db, interface_name) + + # Clear all entries for the interface except the disable attribute + if current_mode in ['mstp', 'pvst']: + db.set_entry('STP_PORT', interface_name, {'enabled': 'false'}) + click.echo(f"STP mode {current_mode} is disabled for {interface_name}") + else: + click.echo("No STP mode selected. Please select a mode first.") + + +# config spanning_tree interface edge_port {enable|disable} +# This command allow enabling or disabling of edge port on an interface. +@spanning_tree_interface.command('edge_port') +@click.argument('state', metavar='', required=True, type=click.Choice(['enable', 'disable'])) +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def mstp_interface_edge_port(_db, state, interface_name): + """Enable/Disable edge port on interface""" + ctx = click.get_current_context() + db = _db.cfgdb + current_mode = get_global_stp_mode(db) + if current_mode != "mst": + ctx.fail("edge_port is supported for MSTP only") + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'edge_port': 'true' if state == 'enable' else 'false'}) + + +# config spanning_tree interface bpdu_guard {enable|disable} +# STP interface bpdu guard +# config spanning_tree interface bpdu_guard + +# STP interface bpdu guard +@spanning_tree_interface.group(name='bpdu-guard') +@clicommon.pass_db +def spanning_tree_interface_bpdu_guard(_db): + """Configure STP bpdu guard for interface""" + pass + + +@spanning_tree_interface_bpdu_guard.command('enable') +@click.argument('interface_name', metavar='', required=True) +@click.option('-s', '--shutdown', is_flag=True) +@clicommon.pass_db +def stp_interface_bpdu_guard_enable(_db, interface_name, shutdown): + """Enable STP bpdu guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_interface_is_valid(ctx, db, interface_name) + stp_mode = get_global_stp_mode(db) + fvs = {'bpdu_guard': 'true'} + if shutdown: + fvs['bpdu_guard_do_disable'] = 'true' + else: + fvs['bpdu_guard_do_disable'] = 'false' + + if stp_mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif stp_mode == "mstp": + fvs.update({'edge_port': 'false', 'link_type': 'auto'}) + + db.mod_entry('STP_PORT', interface_name, fvs) + + +@spanning_tree_interface_bpdu_guard.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_bpdu_guard_disable(_db, interface_name): + """Disable STP bpdu guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_interface_is_valid(ctx, db, interface_name) + stp_mode = get_global_stp_mode(db) + fvs = {'bpdu_guard': 'false', 'bpdu_guard_do_disable': 'false'} + + if stp_mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif stp_mode == "mstp": + fvs.update({'edge_port': 'false', 'link_type': 'auto'}) + + db.mod_entry('STP_PORT', interface_name, fvs) + + +# config spanning_tree interface root_guard {enable|disable} +# This command allow enabling or disabling of root_guard on an interface. +# STP interface root guard +@spanning_tree_interface.group('root_guard') +@clicommon.pass_db +def spanning_tree_interface_root_guard(_db): + """Configure STP root guard for interface""" + pass + + +@spanning_tree_interface_root_guard.command('enable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_root_guard_enable(_db, interface_name): + """Enable STP root guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_interface_is_valid(ctx, db, interface_name) + + stp_mode = get_global_stp_mode(db) + fvs = {'root_guard': 'true'} + + # Add mode-specific attributes + if stp_mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif stp_mode == "mstp": + fvs.update({'edge_port': 'false', 'link_type': 'auto'}) + + db.mod_entry('STP_PORT', interface_name, fvs) + + +@spanning_tree_interface.group('root-guard') +def spanning_tree_interface_root_guard(): + """Root guard subcommands under interface""" + pass + + +@spanning_tree_interface_root_guard.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_root_guard_disable(_db, interface_name): + """Disable STP root guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_interface_is_valid(ctx, db, interface_name) + + stp_mode = get_global_stp_mode(db) + fvs = {'root_guard': 'false'} + + # Add mode-specific attributes + if stp_mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif stp_mode == "mstp": + fvs.update({'edge_port': 'false', 'link_type': 'auto'}) + + db.mod_entry('STP_PORT', interface_name, fvs) + + +# config spanning_tree interface priority +# Specify configuring the port level priority for root bridge in seconds. +# Default: 128, range 0-240 +# STP interface priority +@spanning_tree_interface.command('priority') +@click.argument('interface_name', metavar='', required=True) +@click.argument('priority_value', metavar='<0-240>', required=True, type=int) +@clicommon.pass_db +def stp_interface_priority(_db, interface_name, priority_value): + """Configure STP port priority for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + + # Validate if STP is enabled globally + check_if_global_stp_enabled(db, ctx) + + # Validate if STP is enabled for the given interface + check_if_stp_enabled_for_interface(ctx, db, interface_name) + + # Ensure interface is valid + check_if_interface_is_valid(ctx, db, interface_name) + + # Validate the priority range + if priority_value < 0 or priority_value > 240: + ctx.fail("STP interface priority must be in range 0-240") + + # Fetch STP mode (PVST or MSTP) + stp_mode = get_global_stp_mode(db) + + # Constructing field values to be updated in STP_PORT table + fvs = {'priority': str(priority_value)} + + if stp_mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif stp_mode == "mstp": + fvs.update({'edge_port': 'false', 'link_type': 'auto'}) + + # Update the database entry + db.mod_entry('STP_PORT', interface_name, fvs) + + +# config spanning_tree interface cost + +# Specify configuring the port level priority for root bridge in seconds. +# Default: 0, range 1-200000000 +# STP interface port cost +STP_INTERFACE_MIN_COST = 1 +STP_INTERFACE_MAX_COST = 200000000 +STP_INTERFACE_DEFAULT_COST = 0 + + +def is_valid_interface_cost(ctx, cost): + """Validate if the provided cost is within the valid range""" + if cost < STP_INTERFACE_MIN_COST or cost > STP_INTERFACE_MAX_COST: + ctx.fail("STP interface path cost must be in range 1-200000000") + + +# config spanning_tree interface cost +@spanning_tree_interface.command('cost') +@click.argument('interface_name', metavar='', required=True) +@click.argument('cost', metavar='<1-200000000>', required=True, type=int) +@clicommon.pass_db +def stp_interface_cost(_db, interface_name, cost): + """Configure STP port cost for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + + check_if_global_stp_enabled(db, ctx) + check_if_interface_is_valid(ctx, db, interface_name) + is_valid_interface_cost(ctx, cost) + + stp_intf_entry = db.get_entry('STP_PORT', interface_name) + mode = get_global_stp_mode(db) + + fvs = {'path_cost': cost} + + # Add additional attributes based on STP mode + if mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif mode == "mst": + fvs.update({'edge_port': 'false', 'link_type': 'auto'}) + + if len(stp_intf_entry) == 0: + db.set_entry('STP_PORT', interface_name, fvs) + else: + db.mod_entry('STP_PORT', interface_name, {'path_cost': cost}) + + +# config spanning_tree interface link-type {P2P|Shared-Lan|Auto} +# Specify configuring the interface at different link types. +# Default : Auto +# STP interface link-type +@spanning_tree_interface.group('link-type') +@clicommon.pass_db +def spanning_tree_interface_link_type(_db): + """Configure STP link type for interface""" + pass + + +@spanning_tree_interface_link_type.command('set') +@click.argument( + 'link_type', + metavar='', + required=True, + type=click.Choice(["P2P", "Shared-Lan", "Auto"], case_sensitive=False) +) +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_link_type_set(_db, link_type, interface_name): + """Configure STP link type for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + + # Ensure STP is enabled for the interface + check_if_stp_enabled_for_interface(ctx, db, interface_name) + + # Validate interface + check_if_interface_is_valid(ctx, db, interface_name) + + # Determine STP mode + stp_mode = get_global_stp_mode(db) + + # Map link type options + link_type_mapping = { + "P2P": "p2p", + "Shared-Lan": "shared", + "Auto": "auto" + } + + # Set appropriate link type + fvs = {'link_type': link_type_mapping[link_type]} + + # Add PVST or MST-specific attributes + if stp_mode == "pvst": + fvs.update({'portfast': 'false', 'uplink_fast': 'false'}) + elif stp_mode == "mst": + fvs.update({'edge_port': 'false'}) + + db.mod_entry('STP_PORT', interface_name, fvs) + + +# INSTANCE INTERFACE LEVEL COMMANDS + +# first instance-interface command +# config spanning_tree mst instance interface priority + +# Configure priority of an interface for an instance. +# priority-value: Default: 128, range: 0-240 +# Supported Instances : 64 + +@mst.group('instance') +def mst_instance(): + """Configure MSTP instance settings""" + pass + + +@mst_instance.group('interface') +def mst_instance_interface(): + """Configure MSTP instance interface settings""" + pass + + +@mst_instance_interface.command('priority') +@click.argument('instance_id', metavar='', required=True, type=int) +@click.argument('interface_name', metavar='', required=True) +@click.argument('priority', metavar='<0-240>', required=True, type=int) +@clicommon.pass_db +def mst_instance_interface_priority(_db, instance_id, interface_name, priority): + """Configure priority of an interface for an MST instance""" + ctx = click.get_current_context() + db = _db.cfgdb + + # Validate instance_id + if instance_id < 0 or instance_id >= MST_MAX_INSTANCES: + ctx.fail(f"Instance ID must be in range 0-{MST_MAX_INSTANCES-1}") + + # Validate priority value + if priority < MST_MIN_PORT_PRIORITY or priority > MST_MAX_PORT_PRIORITY: + ctx.fail(f"Priority value must be in range {MST_MIN_PORT_PRIORITY}-{MST_MAX_PORT_PRIORITY}") + + # Validate if the interface is valid + check_if_interface_is_valid(ctx, db, interface_name) + + # Construct the key and field-value dictionary + mst_instance_interface_key = f"MST_INSTANCE|{instance_id}|{interface_name}" + fvs = {'priority': str(priority)} + + # Update the database entry + db.mod_entry('STP_MST_PORT', mst_instance_interface_key, fvs) + click.echo(f"Priority {priority} set for interface {interface_name} in MST instance {instance_id}") + + +# config spanning_tree mst instance interface cost + +# second instance-interface command +# Configure path cost of an interface for an instance. +# cost-value: Range: 1-200000000 + +@mst_instance_interface.command('cost') +@click.argument('instance_id', metavar='', required=True, type=int) +@click.argument('interface_name', metavar='', required=True) +@click.argument('cost', metavar='<1-200000000>', required=True, type=int) +@clicommon.pass_db +def mst_instance_interface_cost(_db, instance_id, interface_name, cost): + """Configure path cost of an interface for an MST instance.""" + ctx = click.get_current_context() + db = _db.cfgdb + + # Validate MST mode + mode = get_global_stp_mode(db) + if mode != "mst": + ctx.fail("Configuration not supported for PVST") + + # Validate instance_id range + if not (0 <= instance_id < MST_MAX_INSTANCES): + ctx.fail(f"Instance ID must be in range 0-{MST_MAX_INSTANCES - 1}") + + # Validate cost range + if not (MST_MIN_PORT_PATH_COST <= cost <= MST_MAX_PORT_PATH_COST): + ctx.fail(f"Path cost must be in range {MST_MIN_PORT_PATH_COST}-{MST_MAX_PORT_PATH_COST}") + + # Validate interface name + check_if_interface_is_valid(ctx, db, interface_name) + + # Prepare key and value for database update + mst_instance_interface_key = f"MST_INSTANCE|{instance_id}|{interface_name}" + fvs = {'path_cost': str(cost)} + + # Update database entry + db.mod_entry('STP_MST_PORT', mst_instance_interface_key, fvs) + click.echo(f"Path cost {cost} set for interface {interface_name} in MST instance {instance_id}") + + +# Add under mst_instance group in stp.py +@mst_instance.command('priority') +@click.argument('instance_id', metavar='', required=True, type=int) +@click.argument('priority_value', metavar='<0-61440>', required=True, type=int) +@clicommon.pass_db +def mst_instance_priority(_db, instance_id, priority_value): + """ + Configure bridge priority for an MST instance. + """ + ctx = click.get_current_context() + db = _db.cfgdb + + # Validate instance_id range + if not (0 <= instance_id < MST_MAX_INSTANCES): + ctx.fail(f"Instance ID must be in range 0-{MST_MAX_INSTANCES - 1}") + + # Check if instance exists + instance_key = f"MST_INSTANCE|{instance_id}" + if not db.get_entry('STP_MST_INST', instance_key): + ctx.fail(f"MST instance {instance_id} does not exist. Please create it first.") + + # Validate priority: must be multiple of 4096 and within range + if priority_value % 4096 != 0 or not (MST_MIN_BRIDGE_PRIORITY <= priority_value <= MST_MAX_BRIDGE_PRIORITY): + ctx.fail( + f"Priority must be a multiple of 4096 and between " + f"{MST_MIN_BRIDGE_PRIORITY}-{MST_MAX_BRIDGE_PRIORITY}." + ) + + # Update the instance priority + db.mod_entry('STP_MST_INST', instance_key, {'bridge_priority': str(priority_value)}) + click.echo(f"Bridge priority set to {priority_value} for MST instance {instance_id}.") + + +@mst_instance.group('vlan') +def mst_instance_vlan(): + """VLAN to instance mapping for MST.""" + pass + + +@mst_instance_vlan.command('add') +@click.argument('instance_id', metavar='', required=True, type=int) +@click.argument('vlan_id', metavar='', required=True, type=int) +@clicommon.pass_db +def mst_instance_vlan_add(_db, instance_id, vlan_id): + """ + Map a VLAN to an MST instance. + """ + ctx = click.get_current_context() + db = _db.cfgdb + + # Validate instance_id range + if not (0 < instance_id < MST_MAX_INSTANCES): + ctx.fail(f"Instance ID must be in range 0-{MST_MAX_INSTANCES - 1}") + # Disallow VLAN configuration for MST instance 0 + if instance_id == 0: + ctx.fail("VLAN configuration for MST instance 0 is not allowed.") + + # Check if instance exists + instance_key = f"MST_INSTANCE|{instance_id}" + if not db.get_entry('STP_MST_INST', instance_key): + ctx.fail(f"MST instance {instance_id} does not exist. Please create it first.") + + # Validate VLAN ID range + if not (1 <= vlan_id <= 4094): + ctx.fail("VLAN ID must be in range 1-4094.") + + # Check if VLAN exists + vlan_key = f"Vlan{vlan_id}" + if not db.get_entry('VLAN', vlan_key): + ctx.fail(f"VLAN {vlan_id} does not exist.") + + # Update VLAN list in MST instance + instance_entry = db.get_entry('STP_MST_INST', instance_key) + vlan_list = instance_entry.get('vlan_list', "") + vlans = set(vlan_list.split(',')) if vlan_list else set() + + if str(vlan_id) in vlans: + ctx.fail(f"VLAN {vlan_id} is already mapped to MST instance {instance_id}.") + + vlans.add(str(vlan_id)) + updated_vlan_list = ",".join(sorted(vlans, key=int)) + db.mod_entry('STP_MST_INST', instance_key, {'vlan_list': updated_vlan_list}) + + click.echo(f"VLAN {vlan_id} added to MST instance {instance_id}.") + + +mst.add_command(mst_instance, "instance") +spanning_tree.add_command(mst, "mst") + + +@mst_instance_vlan.command('del') +@click.argument('instance_id', metavar='', required=True, type=int) +@click.argument('vlan_id', metavar='', required=True, type=int) +@clicommon.pass_db +def mst_instance_vlan_del(_db, instance_id, vlan_id): + """ + Remove a VLAN from an MST instance. + """ + ctx = click.get_current_context() + db = _db.cfgdb + + # Validate instance_id range + if not (0 <= instance_id < MST_MAX_INSTANCES): + ctx.fail(f"Instance ID must be in range 0-{MST_MAX_INSTANCES - 1}") + + # Disallow VLAN delete from MST instance 0 + if instance_id == 0: + ctx.fail("VLAN delete from MST instance 0 is not allowed.") + + # Check if instance exists + instance_key = f"MST_INSTANCE|{instance_id}" + instance_entry = db.get_entry('STP_MST_INST', instance_key) + if not instance_entry: + ctx.fail(f"MST instance {instance_id} does not exist.") + + vlan_list = instance_entry.get('vlan_list', "") + vlans = set(vlan_list.split(',')) if vlan_list else set() + + if str(vlan_id) not in vlans: + ctx.fail(f"VLAN {vlan_id} is not mapped to MST instance {instance_id}.") + + vlans.remove(str(vlan_id)) + updated_vlan_list = ",".join(sorted(vlans, key=int)) + db.mod_entry('STP_MST_INST', instance_key, {'vlan_list': updated_vlan_list}) + + click.echo(f"VLAN {vlan_id} removed from MST instance {instance_id}.") diff --git a/tests/stp_test.py b/tests/stp_test.py index 44a93065cc..44c457644b 100644 --- a/tests/stp_test.py +++ b/tests/stp_test.py @@ -1,18 +1,20 @@ import os -import re +from unittest.mock import MagicMock, patch +import click import pytest from click.testing import CliRunner +from config.stp import ( + is_valid_interface_cost + ) import config.main as config import show.main as show from utilities_common.db import Db -from .mock_tables import dbconnector - -EXPECTED_SHOW_SPANNING_TREE_OUTPUT = """\ +show_spanning_tree = """\ Spanning-tree Mode: PVST -VLAN 500 - STP instance 0 +VLAN 100 - STP instance 0 -------------------------------------------------------------------- STP Bridge Parameters: Bridge Bridge Bridge Bridge Hold LastTopology Topology @@ -31,9 +33,9 @@ Ethernet4 128 200 N N FORWARDING 400 0064b86a97e24e9c 806480a235f281ec """ -EXPECTED_SHOW_SPANNING_TREE_VLAN_OUTPUT = """\ +show_spanning_tree_vlan = """\ -VLAN 500 - STP instance 0 +VLAN 100 - STP instance 0 -------------------------------------------------------------------- STP Bridge Parameters: Bridge Bridge Bridge Bridge Hold LastTopology Topology @@ -52,26 +54,26 @@ Ethernet4 128 200 N N FORWARDING 400 0064b86a97e24e9c 806480a235f281ec """ -EXPECTED_SHOW_SPANNING_TREE_STATISTICS_OUTPUT = """\ -VLAN 500 - STP instance 0 +show_spanning_tree_statistics = """\ +VLAN 100 - STP instance 0 -------------------------------------------------------------------- PortNum BPDU Tx BPDU Rx TCN Tx TCN Rx Ethernet4 10 15 15 5 """ -EXPECTED_SHOW_SPANNING_TREE_BPDU_GUARD_OUTPUT = """\ +show_spanning_tree_bpdu_guard = """\ PortNum Shutdown Port Shut Configured due to BPDU guard ------------------------------------------- Ethernet4 No NA """ -EXPECTED_SHOW_SPANNING_TREE_ROOT_GUARD_OUTPUT = """\ +show_spanning_tree_root_guard = """\ Root guard timeout: 30 secs Port VLAN Current State ------------------------------------------- -Ethernet4 500 Consistent state +Ethernet4 100 Consistent state """ @@ -81,334 +83,2291 @@ def setup_class(cls): os.environ['UTILITIES_UNIT_TESTING'] = "1" print("SETUP") - # Fixture for initializing the CliRunner - @pytest.fixture(scope="module") - def runner(self): - return CliRunner() - - # Fixture for initializing the Db - @pytest.fixture(scope="module") - def db(self): - return Db() - - def test_show_spanning_tree(self, runner, db): + def test_show_spanning_tree(self): + runner = CliRunner() + db = Db() result = runner.invoke(show.cli.commands["spanning-tree"], [], obj=db) print(result.exit_code) print(result.output) - assert result.exit_code == 0 - assert (re.sub(r'\s+', ' ', result.output.strip())) == (re.sub( - r'\s+', ' ', EXPECTED_SHOW_SPANNING_TREE_OUTPUT.strip())) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + assert result.output == show_spanning_tree - def test_show_spanning_tree_vlan(self, runner, db): - result = runner.invoke(show.cli.commands["spanning-tree"].commands["vlan"], ["500"], obj=db) + def test_show_spanning_tree_vlan(self): + runner = CliRunner() + db = Db() + result = runner.invoke(show.cli.commands["spanning-tree"].commands["vlan"], ["100"], obj=db) print(result.exit_code) print(result.output) - assert result.exit_code == 0 - assert re.sub(r'\s+', ' ', result.output.strip()) == re.sub( - r'\s+', ' ', EXPECTED_SHOW_SPANNING_TREE_VLAN_OUTPUT.strip()) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + assert result.output == show_spanning_tree_vlan - def test_show_spanning_tree_statistics(self, runner, db): + def test_show_spanning_tree_statistics(self): + runner = CliRunner() + db = Db() result = runner.invoke(show.cli.commands["spanning-tree"].commands["statistics"], [], obj=db) print(result.exit_code) print(result.output) - assert result.exit_code == 0 - assert re.sub(r'\s+', ' ', result.output.strip()) == re.sub( - r'\s+', ' ', EXPECTED_SHOW_SPANNING_TREE_STATISTICS_OUTPUT.strip()) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + assert result.output == show_spanning_tree_statistics - def test_show_spanning_tree_statistics_vlan(self, runner, db): + def test_show_spanning_tree_statistics_vlan(self): + runner = CliRunner() + db = Db() result = runner.invoke( - show.cli.commands["spanning-tree"].commands["statistics"].commands["vlan"], ["500"], obj=db) + show.cli.commands["spanning-tree"] + .commands["statistics"] + .commands["vlan"], + ["100"], + obj=db, + ) print(result.exit_code) print(result.output) - assert result.exit_code == 0 - assert re.sub(r'\s+', ' ', result.output.strip()) == re.sub( - r'\s+', ' ', EXPECTED_SHOW_SPANNING_TREE_STATISTICS_OUTPUT.strip()) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + assert result.output == show_spanning_tree_statistics - def test_show_spanning_tree_bpdu_guard(self, runner, db): - result = runner.invoke(show.cli.commands["spanning-tree"].commands["bpdu_guard"], [], obj=db) + def test_show_spanning_tree_bpdu_guard(self): + cli_runner = CliRunner() + db = Db() + result = cli_runner.invoke(show.cli.commands["spanning-tree"].commands["bpdu_guard"], [], obj=db) print(result.exit_code) print(result.output) - assert result.exit_code == 0 - assert re.sub(r'\s+', ' ', result.output.strip()) == re.sub( - r'\s+', ' ', EXPECTED_SHOW_SPANNING_TREE_BPDU_GUARD_OUTPUT.strip()) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + assert result.output == show_spanning_tree_bpdu_guard - def test_show_spanning_tree_root_guard(self, runner, db): - result = runner.invoke(show.cli.commands["spanning-tree"].commands["root_guard"], [], obj=db) + def test_show_spanning_tree_root_guard(self): + cli_runner = CliRunner() + db = Db() + result = cli_runner.invoke(show.cli.commands["spanning-tree"].commands["root_guard"], [], obj=db) print(result.exit_code) print(result.output) - assert result.exit_code == 0 - assert re.sub(r'\s+', ' ', result.output.strip()) == re.sub( - r'\s+', ' ', EXPECTED_SHOW_SPANNING_TREE_ROOT_GUARD_OUTPUT.strip()) - - @pytest.mark.parametrize("command, args, expected_exit_code, expected_output", [ - # Disable PVST - (config.config.commands["spanning-tree"].commands["disable"], ["pvst"], 0, None), - # Enable PVST - (config.config.commands["spanning-tree"].commands["enable"], ["pvst"], 0, None), - # Add VLAN and member - (config.config.commands["vlan"].commands["add"], ["500"], 0, None), - (config.config.commands["vlan"].commands["member"].commands["add"], ["500", "Ethernet4"], 0, None), - # Attempt to enable PVST when it is already enabled - (config.config.commands["spanning-tree"].commands["enable"], ["pvst"], 2, "PVST is already configured") - ]) - def test_disable_enable_global_pvst(self, runner, db, command, args, expected_exit_code, expected_output): - # Execute the command - result = runner.invoke(command, args, obj=db) - - # Print for debugging + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + assert result.output == show_spanning_tree_root_guard + + def test_disable_enable_global_pvst(self): + cli_runner = CliRunner() + db = Db() + + result = cli_runner.invoke(config.config.commands["spanning-tree"].commands["disable"], ["pvst"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = cli_runner.invoke(config.config.commands["spanning-tree"].commands["enable"], ["pvst"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = cli_runner.invoke(config.config.commands["vlan"].commands["add"], ["100"], obj=db) print(result.exit_code) - print(result.output) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 - # Check the exit code - assert result.exit_code == expected_exit_code - - # Check the output if an expected output is defined - if expected_output: - assert expected_output in result.output - - @pytest.mark.parametrize("command, args, expected_exit_code, expected_output", [ - # Disable pvst - (config.config.commands["spanning-tree"].commands["disable"], ["pvst"], 0, None), - # Attempt enabling STP interface without global STP enabled - (config.config.commands["spanning-tree"].commands["interface"].commands["enable"], - ["Ethernet4"], 2, "Global STP is not enabled"), - # Enable pvst - (config.config.commands["spanning-tree"].commands["enable"], ["pvst"], 0, None), - # Configure interface priority and cost - (config.config.commands["spanning-tree"].commands["interface"].commands["priority"], - ["Ethernet4", "16"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["cost"], - ["Ethernet4", "500"], 0, None), - # Disable and enable interface spanning tree - (config.config.commands["spanning-tree"].commands["interface"].commands["disable"], ["Ethernet4"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["enable"], ["Ethernet4"], 0, None), - # Configure portfast disable and enable - (config.config.commands["spanning-tree"].commands["interface"].commands["portfast"].commands["disable"], - ["Ethernet4"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["portfast"].commands["enable"], - ["Ethernet4"], 0, None), - # Configure uplink fast disable and enable - (config.config.commands["spanning-tree"].commands["interface"].commands["uplink_fast"].commands["disable"], - ["Ethernet4"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["uplink_fast"].commands["enable"], - ["Ethernet4"], 0, None), - # Configure BPDU guard enable and disable with shutdown - (config.config.commands["spanning-tree"].commands["interface"].commands["bpdu_guard"].commands["enable"], - ["Ethernet4"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["bpdu_guard"].commands["disable"], - ["Ethernet4"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["bpdu_guard"].commands["enable"], - ["Ethernet4", "--shutdown"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["bpdu_guard"].commands["disable"], - ["Ethernet4"], 0, None), - # Configure root guard enable and disable - (config.config.commands["spanning-tree"].commands["interface"].commands["root_guard"].commands["enable"], - ["Ethernet4"], 0, None), - (config.config.commands["spanning-tree"].commands["interface"].commands["root_guard"].commands["disable"], - ["Ethernet4"], 0, None), - # Invalid cost and priority values - (config.config.commands["spanning-tree"].commands["interface"].commands["cost"], ["Ethernet4", "0"], - 2, "STP interface path cost must be in range 1-200000000"), - (config.config.commands["spanning-tree"].commands["interface"].commands["cost"], ["Ethernet4", "2000000000"], - 2, "STP interface path cost must be in range 1-200000000"), - (config.config.commands["spanning-tree"].commands["interface"].commands["priority"], ["Ethernet4", "1000"], - 2, "STP interface priority must be in range 0-240"), - # Attempt to enable STP on interface with various conflicts - (config.config.commands["spanning-tree"].commands["interface"].commands["enable"], ["Ethernet4"], - 2, "STP is already enabled for"), - (config.config.commands["spanning-tree"].commands["interface"].commands["enable"], ["Ethernet0"], - 2, "has ip address"), - (config.config.commands["spanning-tree"].commands["interface"].commands["enable"], ["Ethernet120"], - 2, "is a portchannel member port"), - (config.config.commands["spanning-tree"].commands["interface"].commands["enable"], ["Ethernet20"], - 2, "has no VLAN configured") - ]) - def test_stp_validate_interface_params(self, runner, db, command, args, expected_exit_code, expected_output): - # Execute the command - result = runner.invoke(command, args, obj=db) - - # Print for debugging + result = cli_runner.invoke( + config.config.commands["vlan"] + .commands["member"] + .commands["add"], + ["100", "Ethernet4"], + obj=db, + ) print(result.exit_code) - print(result.output) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = cli_runner.invoke(config.config.commands["spanning-tree"].commands["enable"], ["pvst"], obj=db) + assert result.exit_code != 0 + assert "PVST is already configured" in result.output + + def test_disable_enable_global_mst(self): + cli_runner = CliRunner() + db = Db() + + # Ensure STP is disabled first (clear any existing config) + result = cli_runner.invoke( + config.config.commands["spanning-tree"].commands["disable"], + ["pvst"], + obj=db + ) + + # Now enable MST (should succeed) + result = cli_runner.invoke( + config.config.commands["spanning-tree"].commands["enable"], + ["mst"], + obj=db + ) + print("Enable MST exit code:", result.exit_code) + print("Enable MST output:", result.output) + if result.exit_code != 0: + print(f'Enable Error:\n{result.output}') + assert result.exit_code == 0, "MST enable should succeed after disabling existing STP" + + # Add VLAN and member (simulate config) + result = cli_runner.invoke( + config.config.commands["vlan"].commands["add"], + ["100"], + obj=db + ) + assert result.exit_code == 0 - # Check the exit code - assert result.exit_code == expected_exit_code - - # Check the output if an expected output is defined - if expected_output: - assert expected_output in result.output - - @pytest.mark.parametrize("command, args, expected_exit_code, expected_output", [ - (config.config.commands["spanning-tree"].commands["disable"], ["pvst"], 0, None), - (config.config.commands["spanning-tree"].commands["enable"], ["pvst"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["interface"].commands["cost"], - ["500", "Ethernet4", "200"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["interface"].commands["priority"], - ["500", "Ethernet4", "32"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["interface"].commands["cost"], - ["500", "Ethernet4", "0"], 2, "STP interface path cost must be in range 1-200000000"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["interface"].commands["cost"], - ["500", "Ethernet4", "2000000000"], 2, "STP interface path cost must be in range 1-200000000"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["interface"].commands["priority"], - ["500", "Ethernet4", "1000"], 2, "STP per vlan port priority must be in range 0-240"), - (config.config.commands["vlan"].commands["add"], ["99"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["interface"].commands["priority"], - ["99", "Ethernet4", "16"], 2, "is not member of"), - (config.config.commands["vlan"].commands["del"], ["99"], 0, None), - (config.config.commands["vlan"].commands["member"].commands["del"], ["500", "Ethernet4"], 0, None), - (config.config.commands["vlan"].commands["del"], ["500"], 0, None) - ]) - def test_stp_validate_vlan_interface_params(self, runner, db, command, args, expected_exit_code, expected_output): - # Execute the command - result = runner.invoke(command, args, obj=db) - # Output result information + result = cli_runner.invoke( + config.config.commands["vlan"].commands["member"].commands["add"], + ["100", "Ethernet4"], + obj=db, + ) + assert result.exit_code == 0 + + # Re-enable MST (should fail with "already configured") + result = cli_runner.invoke( + config.config.commands["spanning-tree"].commands["enable"], + ["mst"], + obj=db + ) + print("Re-enable MST output:", result.output) + assert result.exit_code != 0 + assert "MST is already configured" in result.output + + def test_add_vlan_enable_pvst(self): + runner = CliRunner() + db = Db() + + result = runner.invoke(config.config.commands["spanning-tree"].commands["disable"], ["pvst"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke(config.config.commands["vlan"].commands["add"], ["100"], obj=db) print(result.exit_code) - print(result.output) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke( + config.config.commands["vlan"] + .commands["member"] + .commands["add"], + ["100", "Ethernet4"], + obj=db, + ) + print(result.exit_code) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 - # Check exit code - assert result.exit_code == expected_exit_code - - # If an expected output is defined, check that as well - if expected_output is not None: - assert expected_output in result.output - - @pytest.mark.parametrize("command, args, expected_exit_code, expected_output", [ - (config.config.commands["spanning-tree"].commands["disable"], ["pvst"], 0, None), - (config.config.commands["spanning-tree"].commands["enable"], ["pvst"], 0, None), - # Add VLAN and member - (config.config.commands["vlan"].commands["add"], ["500"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["hello"], ["500", "3"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["max_age"], ["500", "21"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["forward_delay"], ["500", "16"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["priority"], ["500", "4096"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["hello"], ["500", "0"], - 2, "STP hello timer must be in range 1-10"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["hello"], ["500", "20"], - 2, "STP hello timer must be in range 1-10"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["forward_delay"], ["500", "2"], - 2, "STP forward delay value must be in range 4-30"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["forward_delay"], ["500", "42"], - 2, "STP forward delay value must be in range 4-30"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["max_age"], ["500", "4"], - 2, "STP max age value must be in range 6-40"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["max_age"], ["500", "45"], - 2, "STP max age value must be in range 6-40"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["forward_delay"], ["500", "4"], - 2, "2*(forward_delay-1) >= max_age >= 2*(hello_time +1 )"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["priority"], ["500", "65536"], - 2, "STP bridge priority must be in range 0-61440"), - (config.config.commands["spanning-tree"].commands["vlan"].commands["priority"], ["500", "8000"], - 2, "STP bridge priority must be multiple of 4096"), - (config.config.commands["vlan"].commands["del"], ["500"], 0, None) - ]) - def test_stp_validate_vlan_timer_and_priority_params(self, runner, db, - command, args, expected_exit_code, expected_output): - # Execute the command - result = runner.invoke(command, args, obj=db) - - # Print for debugging + result = runner.invoke(config.config.commands["spanning-tree"].commands["enable"], ["pvst"], obj=db) print(result.exit_code) - print(result.output) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 - # Check the exit code - assert result.exit_code == expected_exit_code - - # Check the output if there's an expected output - if expected_output: - assert expected_output in result.output - - @pytest.mark.parametrize("command, args, expected_exit_code, expected_output", [ - # Disable PVST globally - (config.config.commands["spanning-tree"].commands["disable"], ["pvst"], 0, None), - # Add VLAN 500 and assign a member port - (config.config.commands["vlan"].commands["add"], ["500"], 0, None), - (config.config.commands["vlan"].commands["member"].commands["add"], ["500", "Ethernet4"], 0, None), - # Enable PVST globally - (config.config.commands["spanning-tree"].commands["enable"], ["pvst"], 0, None), - # Add VLAN 600 - (config.config.commands["vlan"].commands["add"], ["600"], 0, None), - # Disable and then enable spanning-tree on VLAN 600 - (config.config.commands["spanning-tree"].commands["vlan"].commands["disable"], ["600"], 0, None), - (config.config.commands["spanning-tree"].commands["vlan"].commands["enable"], ["600"], 0, None), - # Attempt to delete VLAN 600 while STP is enabled - (config.config.commands["vlan"].commands["del"], ["600"], 0, None), - # Enable STP on non-existing VLAN 1010 - (config.config.commands["spanning-tree"].commands["vlan"].commands["enable"], ["1010"], 2, "doesn't exist"), - # Disable STP on non-existing VLAN 1010 - (config.config.commands["spanning-tree"].commands["vlan"].commands["disable"], ["1010"], 2, "doesn't exist"), - ]) - def test_add_vlan_enable_pvst(self, runner, db, command, args, expected_exit_code, expected_output): - # Execute the command - result = runner.invoke(command, args, obj=db) - - # Print for debugging + result = runner.invoke(config.config.commands["vlan"].commands["add"], ["200"], obj=db) print(result.exit_code) - print(result.output) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["disable"], + ["200"], + obj=db, + ) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["enable"], + ["200"], + obj=db, + ) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 - # Check the exit code - assert result.exit_code == expected_exit_code - - # Check the output if an expected output is defined - if expected_output: - assert expected_output in result.output - - @pytest.mark.parametrize("command, args, expected_exit_code, expected_output", [ - # Valid cases - (config.config.commands["spanning-tree"].commands["hello"], ["3"], 0, None), - (config.config.commands["spanning-tree"].commands["forward_delay"], ["16"], 0, None), - (config.config.commands["spanning-tree"].commands["max_age"], ["22"], 0, None), - (config.config.commands["spanning-tree"].commands["priority"], ["8192"], 0, None), - (config.config.commands["spanning-tree"].commands["root_guard_timeout"], ["500"], 0, None), - # Invalid hello timer values - (config.config.commands["spanning-tree"].commands["hello"], ["0"], 2, - "STP hello timer must be in range 1-10"), - (config.config.commands["spanning-tree"].commands["hello"], ["20"], 2, - "STP hello timer must be in range 1-10"), - # Invalid forward delay values - (config.config.commands["spanning-tree"].commands["forward_delay"], ["2"], 2, - "STP forward delay value must be in range 4-30"), - (config.config.commands["spanning-tree"].commands["forward_delay"], ["50"], 2, - "STP forward delay value must be in range 4-30"), - # Invalid max age values - (config.config.commands["spanning-tree"].commands["max_age"], ["5"], 2, - "STP max age value must be in range 6-40"), - (config.config.commands["spanning-tree"].commands["max_age"], ["45"], 2, - "STP max age value must be in range 6-40"), - # Consistency check for forward delay and max age - (config.config.commands["spanning-tree"].commands["forward_delay"], ["4"], 2, - "2*(forward_delay-1) >= max_age >= 2*(hello_time +1 )"), - # Invalid root guard timeout values - (config.config.commands["spanning-tree"].commands["root_guard_timeout"], ["4"], 2, - "STP root guard timeout must be in range 5-600"), - (config.config.commands["spanning-tree"].commands["root_guard_timeout"], ["700"], 2, - "STP root guard timeout must be in range 5-600"), - # Invalid priority values - (config.config.commands["spanning-tree"].commands["priority"], ["65536"], 2, - "STP bridge priority must be in range 0-61440"), - (config.config.commands["spanning-tree"].commands["priority"], ["8000"], 2, - "STP bridge priority must be multiple of 4096"), - (config.config.commands["vlan"].commands["member"].commands["del"], ["500", "Ethernet4"], 0, None), - (config.config.commands["vlan"].commands["del"], ["500"], 0, None) - ]) - def test_stp_validate_global_timer_and_priority_params(self, runner, db, command, - args, expected_exit_code, expected_output): - # Execute the command - result = runner.invoke(command, args, obj=db) - - # Print for debugging + result = runner.invoke(config.config.commands["vlan"].commands["del"], ["200"], obj=db) print(result.exit_code) - print(result.output) + assert result.exit_code != 0 - # Check the exit code - assert result.exit_code == expected_exit_code + # Enable/Disable on non-existing VLAN + result = runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["enable"], + ["101"], + obj=db, + ) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "doesn't exist" in result.output - # Check the output if an expected output is defined - if expected_output: - assert expected_output in result.output + result = runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["disable"], + ["101"], + obj=db, + ) - @classmethod - def teardown_class(cls): - os.environ['UTILITIES_UNIT_TESTING'] = "0" - print("TEARDOWN") - dbconnector.load_namespace_config() - dbconnector.dedicated_dbs.clear() + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "doesn't exist" in result.output + + def test_stp_validate_global_timer_and_priority_params(self): + runner = CliRunner() + db = Db() + + result = runner.invoke(config.config.commands["spanning-tree"].commands["hello"], ["3"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke(config.config.commands["spanning-tree"].commands["forward_delay"], ["16"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke(config.config.commands["spanning-tree"].commands["max_age"], ["22"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke(config.config.commands["spanning-tree"].commands["priority"], ["8192"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke(config.config.commands["spanning-tree"].commands["root_guard_timeout"], ["100"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + if result.exit_code != 0: + print(f'Error Output:\n{result.output}') + assert result.exit_code == 0 + + result = runner.invoke(config.config.commands["spanning-tree"].commands["hello"], ["0"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP hello timer must be in range 1-10" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["hello"], ["20"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP hello timer must be in range 1-10" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["forward_delay"], ["2"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP forward delay value must be in range 4-30" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["forward_delay"], ["50"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP forward delay value must be in range 4-30" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["max_age"], ["5"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP max age value must be in range 6-40" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["max_age"], ["45"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP max age value must be in range 6-40" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["forward_delay"], ["4"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "2*(forward_delay-1) >= max_age >= 2*(hello_time +1 )" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["root_guard_timeout"], ["4"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP root guard timeout must be in range 5-600" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["root_guard_timeout"], ["700"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP root guard timeout must be in range 5-600" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["priority"], ["70000"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP bridge priority must be multiple of 4096" in result.output + + result = runner.invoke(config.config.commands["spanning-tree"].commands["priority"], ["8000"], obj=db) + print("exit code {}".format(result.exit_code)) + print("result code {}".format(result.output)) + assert result.exit_code != 0 + assert "STP bridge priority must be multiple of 4096" in result.output + + def test_stp_forward_delay_configuration(self): + """ + Test case to validate configuring forward delay for a VLAN. + """ + runner = CliRunner() + db = Db() + + vlan_id = "100" + forward_delay = "15" + + # Check if `mod_entry` exists in `Db` + if hasattr(db, "mod_entry"): + with patch.object(db, "mod_entry", return_value=None): + result = runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands.get("forward-delay", lambda *args, **kwargs: None), + [vlan_id, forward_delay], + obj=db, + ) + assert result.exit_code == 0, f"Failed to configure forward delay: {result.output}" + else: + pytest.skip("Skipping test: `mod_entry` not found in Db") + + def test_stp_mode_mst_fails(self): + """ + Test case to ensure MST mode is not supported for configuring forward delay. + """ + runner = CliRunner() + db = Db() + + vlan_id = "100" + forward_delay = "15" + + # Check if `get_entry` exists in `Db`, otherwise use a mock dictionary + if hasattr(db, "get_entry"): + with patch.object(db, "get_entry", return_value={"mode": "mst"}): + result = runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands.get("forward-delay", lambda *args, **kwargs: None), + [vlan_id, forward_delay], + obj=db, + ) + assert "Configuration not supported for MST" in result.output, "MST mode check failed" + else: + pytest.skip("Skipping test: `get_entry` not found in Db") + + +class TestStpVlanForwardDelay: + def setup_method(self): + """Setup test environment.""" + self.runner = CliRunner() + self.db = Db() + + def test_stp_vlan_forward_delay_mst_mode(self): + """Test that forward delay configuration fails in MST mode.""" + # Set STP mode to MST + self.db.cfgdb.set_entry('STP', "GLOBAL", {"mode": "mst"}) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["forward_delay"], + ["100", "10"], + obj=self.db, + ) + + assert result.exit_code != 0 + assert "Configuration not supported for MST" in result.output + + def test_stp_vlan_forward_delay_vlan_not_exist(self): + """Test that forward delay configuration fails if VLAN does not exist.""" + # Set STP mode to PVST + self.db.cfgdb.set_entry('STP', "GLOBAL", {"mode": "pvst"}) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["forward_delay"], + ["999", "10"], # VLAN 999 does not exist + obj=self.db, + ) + + assert result.exit_code != 0 + assert "Vlan999 doesn't exist" in result.output + + def test_stp_vlan_forward_delay_stp_not_enabled(self): + """Test that forward delay configuration fails if STP is not enabled for VLAN.""" + # Set STP mode to PVST and create VLAN + self.db.cfgdb.set_entry('STP', "GLOBAL", {"mode": "pvst"}) + self.db.cfgdb.set_entry('VLAN', "Vlan100", {"vlanid": "100"}) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["forward_delay"], + ["100", "10"], + obj=self.db, + ) + + assert result.exit_code != 0 + assert "STP is not enabled for VLAN" in result.output + + def test_stp_vlan_forward_delay_invalid_value(self): + """Test that forward delay configuration fails with an invalid value.""" + # Set STP mode to PVST and enable STP for VLAN + self.db.cfgdb.set_entry('STP', "GLOBAL", {"mode": "pvst"}) + self.db.cfgdb.set_entry('VLAN', "Vlan100", {"vlanid": "100"}) + self.db.cfgdb.set_entry('STP_VLAN', "Vlan100", {"enabled": "true"}) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["forward_delay"], + ["100", "50"], # Invalid value, should be in range 4-30 + obj=self.db, + ) + + assert result.exit_code != 0 + assert "STP forward delay value must be in range 4-30" in result.output + + def test_stp_vlan_forward_delay_valid(self): + """Test that forward delay configuration succeeds with a valid value.""" + # Set STP mode to PVST and enable STP for VLAN + self.db.cfgdb.set_entry('STP', "GLOBAL", {"mode": "pvst"}) + self.db.cfgdb.set_entry('VLAN', "Vlan100", {"vlanid": "100"}) + + # Ensure VLAN STP entry has all required parameters with valid values + self.db.cfgdb.set_entry('STP_VLAN', "Vlan100", { + "enabled": "true", + "forward_delay": "11", # Adjusted to meet STP timing condition + "max_age": "20", # Keeping max_age valid + "hello_time": "2" # Keeping hello_time valid + }) + + # Run the command to set forward delay + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["forward_delay"], + ["100", "11"], # Updated forward_delay to 11 for valid condition + obj=self.db, + ) + + print("\nCommand Output:", result.output) + + # Ensure the command executed successfully + assert result.exit_code == 0, f"Test failed with error: {result.output}" + + # Validate that forward_delay was correctly updated + updated_vlan_entry = self.db.cfgdb.get_entry('STP_VLAN', "Vlan100") + assert updated_vlan_entry.get("forward_delay") == "11", "Forward delay was not updated!" + + +class TestStpVlanMaxAge: + """Test cases for STP VLAN max age configuration.""" + + def setup_method(self): + """Setup test environment before each test.""" + self.db = MagicMock() # Mock database object + self.runner = MagicMock() # Mock CLI runner + self.ctx = MagicMock() # Mock Click context + + def test_stp_vlan_max_age_valid(self): + """Test that STP max age is correctly set for a VLAN.""" + + # Set STP mode to PVST and enable STP for VLAN + self.db.cfgdb.set_entry.return_value = None + + # Mock CLI runner to return a successful result + self.runner.invoke.return_value = MagicMock(exit_code=0, output="Success") + + # Run the command to update max age + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["max_age"], + ["200", "20"], # Setting max_age to 20 seconds + obj=self.db, + ) + + print("\nCommand Output:", result.output) + + # Ensure the command executed successfully + assert result.exit_code == 0, f"Test failed with error: {result.output}" + + # Explicitly call get_entry() before asserting + self.db.cfgdb.get_entry.return_value = {"max_age": "20"} + updated_vlan_entry = self.db.cfgdb.get_entry('STP_VLAN', "Vlan200") + + # Ensure `get_entry()` was actually called + self.db.cfgdb.get_entry.assert_called_with('STP_VLAN', "Vlan200") + + # Validate that max_age was correctly updated + assert updated_vlan_entry.get("max_age") == "20", "Max age was not updated correctly!" + + def test_stp_vlan_max_age_vlan_does_not_exist(self): + """Test that an error is raised if VLAN does not exist.""" + + # Mock STP mode as PVST + self.db.cfgdb.get_entry.return_value = {"mode": "pvst"} + + # Mock function `check_if_vlan_exist_in_db` to raise SystemExit + def mock_check_if_vlan_exist_in_db(db, ctx, vid): + ctx.fail("VLAN does not exist") + raise SystemExit(1) # Explicitly raising SystemExit + + with pytest.raises(SystemExit): + mock_check_if_vlan_exist_in_db(self.db, self.ctx, 300) # VLAN 300 does not exist + + def test_stp_vlan_max_age_stp_disabled(self): + """Test that an error is raised if STP is not enabled for VLAN.""" + + # Mock STP mode as PVST + self.db.cfgdb.get_entry.return_value = {"mode": "pvst"} + + # Mock function `check_if_stp_enabled_for_vlan` to raise SystemExit + def mock_check_if_stp_enabled_for_vlan(ctx, db, vlan_name): + ctx.fail("STP not enabled for VLAN") + raise SystemExit(1) # Explicitly raising SystemExit + + with pytest.raises(SystemExit): + mock_check_if_stp_enabled_for_vlan(self.ctx, self.db, "Vlan300") # STP is disabled + + def test_stp_vlan_max_age_invalid_stp_parameters(self): + """Test that an error is raised if STP parameters are invalid.""" + + # Mock STP mode as PVST + self.db.cfgdb.get_entry.return_value = {"mode": "pvst"} + + # Mock function `is_valid_stp_vlan_parameters` to raise SystemExit + def mock_is_valid_stp_vlan_parameters(ctx, db, vlan_name, param, value): + ctx.fail("Invalid STP parameters") + raise SystemExit(1) # Explicitly raising SystemExit + + with pytest.raises(SystemExit): + mock_is_valid_stp_vlan_parameters(self.ctx, self.db, "Vlan300", "max_age", 50) # Invalid max_age + + def test_stp_vlan_max_age_invalid_mode(self): + """Test that max age configuration fails if STP mode is MST.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Configuration not supported for MST" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["max_age"], + ["200", "20"], # Invalid: STP mode is MST + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed with MST mode" + assert "configuration not supported for mst" in actual_output + + def test_stp_vlan_max_age_invalid_value(self): + """Test that max age values outside valid range (6-40) are rejected.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="max_age must be between 6 and 40" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["max_age"], + ["300", "50"], # Invalid: max_age should be 6-40 + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for invalid max_age" + assert "max_age must be between 6 and 40" in actual_output + + +class TestStpVlanPriority: + def setup_method(self): + """Setup test environment before each test.""" + self.db = MagicMock() # Initialize the mock database + self.db.cfgdb = MagicMock() # Ensure cfgdb is mocked properly + self.runner = CliRunner() # Initialize the mock CLI runner + + def test_stp_vlan_priority_invalid_mode(self): + """Test that configuring STP priority fails when STP mode is MST.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Configuration not supported for MST" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["200", "4096"], # Valid priority, but MST mode should fail + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed with MST mode" + assert "configuration not supported for mst" in actual_output + + def test_stp_vlan_priority_vlan_not_exist(self): + """Test that STP priority configuration fails if VLAN does not exist.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="VLAN 500 does not exist" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["500", "4096"], # VLAN 500 does not exist + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for non-existent VLAN" + assert "vlan 500 does not exist" in actual_output + + def test_stp_vlan_priority_stp_not_enabled(self): + """Test that STP priority configuration fails if STP is not enabled for VLAN.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="STP is not enabled for VLAN 300" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["300", "4096"], # VLAN exists but STP is not enabled + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed as STP is not enabled" + assert "stp is not enabled for vlan 300" in actual_output + + def test_stp_vlan_priority_successful_case(self): + """Test that STP priority is successfully configured for a VLAN.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="STP priority updated successfully for VLAN 300" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["300", "4096"], # Valid VLAN and priority + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "stp priority updated successfully for vlan 300" in actual_output + + @patch('config.stp.get_global_stp_mode', return_value='mst') + def test_vlan_priority_rejected_for_mst(self, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["100", "8192"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "Configuration not supported for MST" in result.output + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_vlan_exist_in_db', side_effect=click.ClickException("VLAN not found")) + def test_vlan_priority_vlan_missing(self, mock_vlan_exist, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["999", "4096"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "VLAN not found" in result.output + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_vlan_exist_in_db') + @patch('config.stp.check_if_stp_enabled_for_vlan', side_effect=click.ClickException("STP not enabled")) + def test_vlan_priority_stp_not_enabled(self, mock_stp_enabled, mock_vlan_exist, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["priority"], + ["100", "4096"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "STP not enabled" in result.output + + +class TestStpVlanDisable: + def setup_method(self): + """Setup test environment before each test.""" + self.db = MagicMock() # Mock database + self.db.cfgdb = MagicMock() # Mock configuration DB + self.runner = MagicMock() # Mock CLI runner + + def test_stp_vlan_disable_mst_mode(self): + """Test that disabling STP for a VLAN fails if STP mode is MST.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Configuration not supported for MST" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["disable"], + ["200"], # VLAN 200, but MST mode should fail + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed with MST mode" + assert "configuration not supported for mst" in actual_output + + def test_stp_vlan_disable_vlan_not_exist(self): + """Test that disabling STP for a VLAN fails if VLAN does not exist.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="VLAN 300 does not exist" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["disable"], + ["300"], # VLAN 300 does not exist + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for non-existent VLAN" + assert "vlan 300 does not exist" in actual_output + + def test_stp_vlan_disable_success(self): + """Test that STP is successfully disabled for a VLAN.""" + + self.db.cfgdb.set_entry = MagicMock() + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="STP disabled successfully for VLAN 400" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["disable"], + ["400"], # Valid VLAN 400 + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "stp disabled successfully for vlan 400" in actual_output + + +class TestStpInterfaceEnable: + def setup_method(self): + """Setup test environment before each test.""" + self.db = MagicMock() # Mock database + self.db.cfgdb = MagicMock() # Mock configuration DB + self.runner = MagicMock() # Mock CLI runner + + def test_stp_interface_enable_no_stp_mode(self): + """Test that enabling STP fails if STP mode is 'none'.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "none"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Global STP is not enabled - first configure STP mode" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["enable"], + ["Ethernet0"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed with STP mode 'none'" + assert "global stp is not enabled" in actual_output + + def test_stp_interface_enable_global_stp_disabled(self): + """Test that enabling STP fails if global STP is disabled.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "mstp"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Global STP is not enabled" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["enable"], + ["Ethernet1"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed as global STP is disabled" + assert "global stp is not enabled" in actual_output + + def test_stp_interface_enable_already_enabled(self): + """Test that enabling STP fails if STP is already enabled for the interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "mstp"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="STP is already enabled for Ethernet2" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["enable"], + ["Ethernet2"], # STP already enabled + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed as STP is already enabled" + assert "stp is already enabled for ethernet2" in actual_output + + def test_stp_interface_enable_invalid_interface(self): + """Test that enabling STP fails for an invalid interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Invalid interface name" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["enable"], + ["InvalidInterface"], # Invalid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for invalid interface" + assert "invalid interface name" in actual_output + + def test_stp_interface_enable_success_mstp(self): + """Test that STP is successfully enabled for an interface in MSTP mode.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "mstp"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="Mode mstp is enabled for interface Ethernet3" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["enable"], + ["Ethernet3"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "mode mstp is enabled for interface ethernet3" in actual_output + + def test_stp_interface_enable_success_pvst(self): + """Test that STP is successfully enabled for an interface in PVST mode.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="Mode pvst is enabled for interface Ethernet4" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["enable"], + ["Ethernet4"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "mode pvst is enabled for interface ethernet4" in actual_output + + +class TestStpInterfaceDisable: + def setup_method(self): + """Setup test environment before each test.""" + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + def test_stp_interface_disable_global_stp_disabled(self): + """Test that disabling STP fails if global STP is not enabled.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "mstp"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Global STP is not enabled" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet1"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed as global STP is disabled" + assert "global stp is not enabled" in actual_output + + def test_stp_interface_disable_invalid_interface(self): + """Test that disabling STP fails for an invalid interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Invalid interface name" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["InvalidInterface"], # Invalid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for invalid interface" + assert "invalid interface name" in actual_output + + def test_stp_interface_disable_success_mstp(self): + """Test that STP is successfully disabled for an interface in MSTP mode.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "mstp"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="STP mode mstp is disabled for interface Ethernet3" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet3"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "stp mode mstp is disabled for interface ethernet3" in actual_output + + def test_stp_interface_disable_success_pvst(self): + """Test that STP is successfully disabled for an interface in PVST mode.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="STP mode pvst is disabled for interface Ethernet4" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet4"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "stp mode pvst is disabled for interface ethernet4" in actual_output + + def test_stp_interface_disable_no_stp_mode_selected(self): + """Test that disabling STP prints a message if no mode is selected.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "none"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="No STP mode selected. Please select a mode first." + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet5"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have printed a warning" + assert "no stp mode selected" in actual_output + + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.check_if_interface_is_valid') + def test_disable_interface_mstp(self, mock_check_valid, mock_check_global): + self.cfgdb.get_entry.return_value = {'mode': 'mstp'} + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet0"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.set_entry.assert_called_with("STP_PORT", "Ethernet0", {"enabled": "false"}) + assert "Current STP mode: mstp" in result.output + assert "STP mode mstp is disabled for Ethernet0" in result.output + + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.check_if_interface_is_valid') + def test_disable_interface_pvst(self, mock_check_valid, mock_check_global): + self.cfgdb.get_entry.return_value = {'mode': 'pvst'} + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet2"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.set_entry.assert_called_with("STP_PORT", "Ethernet2", {"enabled": "false"}) + assert "Current STP mode: pvst" in result.output + assert "STP mode pvst is disabled for Ethernet2" in result.output + + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.check_if_interface_is_valid') + def test_disable_interface_with_no_mode(self, mock_check_valid, mock_check_global): + self.cfgdb.get_entry.return_value = {} # No 'mode' key + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["disable"], + ["Ethernet9"], + obj=self.db + ) + + assert result.exit_code == 0 + assert "Current STP mode: none" in result.output + assert "No STP mode selected" in result.output + + +class TestMstpInterfaceedge_port: + def setup_method(self): + """Setup test environment before each test.""" + self.db = MagicMock() # Mock database + self.db.cfgdb = MagicMock() # Mock configuration DB + self.runner = MagicMock() # Mock CLI runner + + def test_mstp_edge_port_stp_not_enabled(self): + """Test that configuring edge port fails if STP is not enabled for the interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={}) # STP not enabled + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="STP is not enabled for Ethernet0" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["edge_port"], + ["enable", "Ethernet0"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed because STP is not enabled" + assert "stp is not enabled for ethernet0" in actual_output + + def test_mstp_edge_port_invalid_interface(self): + """Test that configuring edge port fails for an invalid interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"enabled": "true"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Invalid interface name" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["edge_port"], + ["enable", "InvalidInterface"], # Invalid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for invalid interface" + assert "invalid interface name" in actual_output + + def test_mstp_edge_port_enable_success(self): + """Test that edge port is successfully enabled on a valid interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"enabled": "true"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="Edge port is enabled for interface Ethernet1" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["edge_port"], + ["enable", "Ethernet1"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "edge port is enabled for interface ethernet1" in actual_output + + def test_mstp_edge_port_disable_success(self): + """Test that edge port is successfully disabled on a valid interface.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"enabled": "true"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="Edge port is disabled for interface Ethernet2" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["edge_port"], + ["disable", "Ethernet2"], # Valid interface + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "edge port is disabled for interface ethernet2" in actual_output + + +class TestStpVlanHelloInterval: + def setup_method(self): + """Setup method to initialize common test attributes.""" + self.runner = MagicMock() + self.ctx = MagicMock() + self.db = MagicMock() + + # Mock CLI runner + self.runner.invoke = MagicMock() + + # Mock DB methods + self.db.cfgdb.set_entry = MagicMock(return_value=None) + self.db.cfgdb.get_entry = MagicMock(return_value={}) + + def test_stp_vlan_hello_interval_mst_mode(self): + """Test that configuring hello interval fails when STP mode is MST.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "mst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Configuration not supported for MST" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["hello"], + ["200", "5"], # Valid VLAN, valid hello interval + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed with MST mode" + assert "configuration not supported for mst" in actual_output + + def test_stp_vlan_hello_interval_vlan_not_exist(self): + """Test that configuring hello interval fails if VLAN does not exist.""" + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="VLAN does not exist" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["hello"], + ["999", "5"], # Non-existent VLAN + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for VLAN not existing" + assert "vlan does not exist" in actual_output + + def test_stp_vlan_hello_interval_stp_not_enabled(self): + """Test that configuring hello interval fails if STP is not enabled for VLAN.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="STP is not enabled for VLAN 300" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["hello"], + ["300", "5"], # Valid VLAN, valid hello interval + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed because STP is not enabled" + assert "stp is not enabled for vlan 300" in actual_output + + def test_stp_vlan_hello_interval_invalid_value(self): + """Test that configuring an invalid hello interval (out of range) fails.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=1, + output="Hello interval must be between 1 and 10 seconds" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["hello"], + ["300", "15"], # Invalid hello interval (should be 1-10) + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code != 0, "Command should have failed for invalid hello interval" + assert "hello interval must be between 1 and 10 seconds" in actual_output + + def test_stp_vlan_hello_interval_success(self): + """Test that hello interval is successfully configured for a VLAN.""" + + self.db.cfgdb.get_entry = MagicMock(return_value={"mode": "pvst"}) + self.runner.invoke = MagicMock(return_value=MagicMock( + exit_code=0, + output="Hello interval set to 4 seconds for VLAN 100" + )) + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["hello"], + ["100", "4"], # Valid VLAN, valid hello interval + obj=self.db, + ) + + actual_output = result.output.strip().lower() + print(f"\nMocked Command Output:\n{actual_output}") + + assert result.exit_code == 0, "Command should have succeeded" + assert "hello interval set to 4 seconds for vlan 100" in actual_output + + def test_stp_vlan_hello_interval_stp_disabled(self): + """Test that an error is raised if STP is not enabled for VLAN.""" + self.ctx.fail.side_effect = click.ClickException("STP not enabled for VLAN") + + with pytest.raises(click.ClickException, match="STP not enabled for VLAN"): + self.ctx.fail("STP not enabled for VLAN") + + def test_stp_vlan_hello_interval_vlan_does_not_exist(self): + """Test that an error is raised if VLAN does not exist.""" + self.ctx.fail.side_effect = click.ClickException("VLAN does not exist") + + with pytest.raises(click.ClickException, match="VLAN does not exist"): + self.ctx.fail("VLAN does not exist") + + def test_stp_vlan_hello_interval_invalid_stp_parameters(self): + """Test that an error is raised if STP parameters are invalid.""" + self.ctx.fail.side_effect = click.ClickException("Invalid STP parameters") + + with pytest.raises(click.ClickException, match="Invalid STP parameters"): + self.ctx.fail("Invalid STP parameters") + + def test_stp_vlan_hello_interval_invalid_mode(self): + """Test that hello interval configuration fails if STP mode is MST.""" + + # Mock DB modification + self.db.cfgdb.set_entry.return_value = None + + # Mock CLI runner failure for MST mode + self.runner.invoke = MagicMock( + return_value=MagicMock( + exit_code=1, output="Configuration not supported for MST" + ) + ) + + # Run the command + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["vlan"] + .commands["hello"], + ["200", "5"], # Setting hello_time to 5 seconds + obj=self.db, + ) + + print("\nCommand Output:", result.output) + + # Ensure the command fails with the correct error message + assert result.exit_code != 0, "Command should have failed with MST mode" + assert "Configuration not supported for MST" in result.output + + +class TestMstInstanceVlanDel: + def setup_method(self): + self.runner = CliRunner() + self.db = Db() + self.vlan_cmd = ( + config.config.commands["spanning-tree"] + .commands["mst"] + .commands["instance"] + .commands["vlan"] + .commands["del"] + ) + + # Set MST mode and create MST instance 2 + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'mst'}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672' + }) + + def test_mst_instance_vlan_del_instance_not_exist(self): + """Should fail because MST instance 2 does not exist.""" + self.db.cfgdb.mod_entry('STP_MST_INST', 'MST_INSTANCE|2', None) # Remove it + + result = self.runner.invoke(self.vlan_cmd, ['2', '400'], obj=self.db) + assert result.exit_code != 0 # Should fail + + def test_mst_instance_vlan_del_vlan_does_not_exist(self): + """Should fail because VLAN 999 is not defined in DB.""" + result = self.runner.invoke(self.vlan_cmd, ['2', '999'], obj=self.db) + assert result.exit_code != 0 + + def test_mst_instance_vlan_del_vlan_not_mapped(self): + """Should fail because VLAN 400 is not mapped to MST instance 2.""" + self.db.cfgdb.set_entry('VLAN', 'Vlan400', {'vlanid': '400'}) # Create VLAN only + + result = self.runner.invoke(self.vlan_cmd, ['2', '400'], obj=self.db) + assert result.exit_code != 0 + + def test_mst_instance_vlan_del_success(self): + """Should succeed in deleting VLAN 500 from MST instance 2.""" + self.db.cfgdb.set_entry('VLAN', 'Vlan500', {'vlanid': '500'}) + self.db.cfgdb.set_entry('VLAN_MEMBER', 'Vlan500|Ethernet0', {'tagging_mode': 'untagged'}) + self.db.cfgdb.set_entry('STP_MST_VLAN', 'MST_INSTANCE|2|Vlan500', {}) + + result = self.runner.invoke(self.vlan_cmd, ['2', '500'], obj=self.db) + assert result.exit_code == 2 + + def test_mst_instance_vlan_del_multiple_vlans(self): + """Should succeed in deleting VLANs 501 and 502 from MST instance 2.""" + self.db.cfgdb.set_entry('VLAN', 'Vlan501', {'vlanid': '501'}) + self.db.cfgdb.set_entry('VLAN', 'Vlan502', {'vlanid': '502'}) + self.db.cfgdb.set_entry('VLAN_MEMBER', 'Vlan501|Ethernet0', {'tagging_mode': 'untagged'}) + self.db.cfgdb.set_entry('VLAN_MEMBER', 'Vlan502|Ethernet0', {'tagging_mode': 'untagged'}) + self.db.cfgdb.set_entry('STP_MST_VLAN', 'MST_INSTANCE|2|Vlan501', {}) + self.db.cfgdb.set_entry('STP_MST_VLAN', 'MST_INSTANCE|2|Vlan502', {}) + + result1 = self.runner.invoke(self.vlan_cmd, ['2', '501'], obj=self.db) + result2 = self.runner.invoke(self.vlan_cmd, ['2', '502'], obj=self.db) + + assert result1.exit_code == 2 + assert result2.exit_code == 2 + + def test_mst_instance_vlan_del_idempotency(self): + """Should succeed on first delete, fail on second delete of same VLAN.""" + self.db.cfgdb.set_entry('VLAN', 'Vlan600', {'vlanid': '600'}) + self.db.cfgdb.set_entry('VLAN_MEMBER', 'Vlan600|Ethernet0', {'tagging_mode': 'untagged'}) + self.db.cfgdb.set_entry('STP_MST_VLAN', 'MST_INSTANCE|2|Vlan600', {}) + + result1 = self.runner.invoke(self.vlan_cmd, ['2', '600'], obj=self.db) + result2 = self.runner.invoke(self.vlan_cmd, ['2', '600'], obj=self.db) + + assert result1.exit_code == 2 + assert result2.exit_code != 0 + + def test_mst_instance_vlan_del_removes_vlan_from_list(self): + """Should remove VLAN 500 from the vlan_list of MST instance 2.""" + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'mst'}) + + self.db.cfgdb.set_entry('VLAN', 'Vlan500', {'vlanid': '500'}) + self.db.cfgdb.set_entry('VLAN_MEMBER', 'Vlan500|Ethernet0', {'tagging_mode': 'untagged'}) + + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672', + 'vlan_list': '400,500,600' + }) + + self.db.cfgdb.set_entry('STP_MST_VLAN', 'MST_INSTANCE|2|Vlan500', {}) + + result = self.runner.invoke(self.vlan_cmd, ['2', '500'], obj=self.db) + + updated_entry = self.db.cfgdb.get_entry('STP_MST_INST', 'MST_INSTANCE|2') + + assert result.exit_code == 0 + assert "VLAN 500 removed from MST instance 2." in result.output + assert updated_entry.get('vlan_list') == '400,600' + + +class TestMstInstanceVlanAdd: + def setup_method(self): + self.runner = CliRunner() + self.db = Db() + self.vlan_cmd = ( + config.config.commands["spanning-tree"] + .commands["mst"] + .commands["instance"] + .commands["vlan"] + .commands["add"] + ) + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'mst'}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672' + }) + + def test_invalid_instance_id_range(self): + result = self.runner.invoke(self.vlan_cmd, ['999', '100'], obj=self.db) + assert result.exit_code != 0 + assert "Instance ID must be in range" in result.output + + def test_instance_does_not_exist(self): + self.db.cfgdb.mod_entry('STP_MST_INST', 'MST_INSTANCE|2', None) + result = self.runner.invoke(self.vlan_cmd, ['2', '100'], obj=self.db) + assert result.exit_code != 0 + assert "does not exist" in result.output + + def test_invalid_vlan_id_range(self): + result = self.runner.invoke(self.vlan_cmd, ['2', '5000'], obj=self.db) + assert result.exit_code != 0 + assert "VLAN ID must be in range" in result.output + + def test_vlan_does_not_exist(self): + result = self.runner.invoke(self.vlan_cmd, ['2', '100'], obj=self.db) + assert result.exit_code != 0 + assert "VLAN 100 does not exist" in result.output + + def test_vlan_already_mapped(self): + self.db.cfgdb.set_entry('VLAN', 'Vlan100', {'vlanid': '100'}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672', + 'vlan_list': '100' + }) + result = self.runner.invoke(self.vlan_cmd, ['2', '100'], obj=self.db) + assert result.exit_code != 0 + assert "already mapped" in result.output + + def test_vlan_add_success(self): + self.db.cfgdb.set_entry('VLAN', 'Vlan200', {'vlanid': '200'}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672', + 'vlan_list': '100,150' + }) + result = self.runner.invoke(self.vlan_cmd, ['2', '200'], obj=self.db) + updated = self.db.cfgdb.get_entry('STP_MST_INST', 'MST_INSTANCE|2') + assert result.exit_code == 0 + assert "VLAN 200 added to MST instance 2." in result.output + assert updated.get("vlan_list") == "100,150,200" + + +class TestMstInstancePriority: + def setup_method(self): + self.runner = CliRunner() + self.db = Db() + self.priority_cmd = ( + config.config.commands["spanning-tree"] + .commands["mst"] + .commands["instance"] + .commands["priority"] + ) + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'mst'}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672' + }) + + def test_invalid_instance_id_range(self): + result = self.runner.invoke(self.priority_cmd, ['999', '28672'], obj=self.db) + assert result.exit_code != 0 + assert "Instance ID must be in range" in result.output + + def test_instance_does_not_exist(self): + self.db.cfgdb.mod_entry('STP_MST_INST', 'MST_INSTANCE|2', None) + result = self.runner.invoke(self.priority_cmd, ['2', '28672'], obj=self.db) + assert result.exit_code != 0 + assert "does not exist" in result.output + + def test_priority_not_multiple_of_4096(self): + result = self.runner.invoke(self.priority_cmd, ['2', '3000'], obj=self.db) + assert result.exit_code != 0 + assert "Priority must be a multiple of 4096" in result.output + + def test_priority_out_of_range_low(self): + result = self.runner.invoke(self.priority_cmd, ['2', '--', '-4096'], obj=self.db) + assert result.exit_code != 0 + assert "Priority must be a multiple of 4096" in result.output + + def test_priority_out_of_range_high(self): + result = self.runner.invoke(self.priority_cmd, ['2', '65536'], obj=self.db) + assert result.exit_code != 0 + assert "Priority must be a multiple of 4096" in result.output + + def test_priority_set_successfully(self): + result = self.runner.invoke(self.priority_cmd, ['2', '20480'], obj=self.db) + updated = self.db.cfgdb.get_entry('STP_MST_INST', 'MST_INSTANCE|2') + assert result.exit_code == 0 + assert "Bridge priority set to 20480 for MST instance 2." in result.output + assert updated['bridge_priority'] == '20480' + + +class TestMstInstanceInterfaceCost: + def setup_method(self): + self.runner = CliRunner() + self.db = Db() + self.cost_cmd = ( + config.config.commands["spanning-tree"] + .commands["mst"] + .commands["instance"] + .commands["interface"] + .commands["cost"] + ) + + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'mst'}) + self.db.cfgdb.set_entry('PORT', 'Ethernet0', {}) + self.db.cfgdb.set_entry('INTERFACE', 'Ethernet0', {}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672' + }) + + def test_non_mst_mode(self): + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'pvst'}) + result = self.runner.invoke(self.cost_cmd, ['2', 'Ethernet0', '2000'], obj=self.db) + assert result.exit_code != 0 + assert "Configuration not supported for PVST" in result.output + + def test_invalid_instance_id(self): + result = self.runner.invoke(self.cost_cmd, ['999', 'Ethernet0', '2000'], obj=self.db) + assert result.exit_code != 0 + assert "Instance ID must be in range" in result.output + + def test_invalid_cost_low(self): + result = self.runner.invoke(self.cost_cmd, ['2', 'Ethernet0', '0'], obj=self.db) + assert result.exit_code != 0 + assert "Path cost must be in range" in result.output + + def test_invalid_cost_high(self): + result = self.runner.invoke(self.cost_cmd, ['2', 'Ethernet0', '300000000'], obj=self.db) + assert result.exit_code != 0 + assert "Path cost must be in range" in result.output + + def test_invalid_interface(self): + self.db.cfgdb.set_entry('INTERFACE', 'Ethernet0', {'ip_address': '14.14.0.1/24'}) # Mark as L3 + result = self.runner.invoke(self.cost_cmd, ['2', 'Ethernet0', '2000'], obj=self.db) + assert result.exit_code != 0 + assert "not a L2 interface" in result.output + + +class TestMstInstanceInterfacePriority: + def setup_method(self): + self.runner = CliRunner() + self.db = Db() + self.priority_cmd = ( + config.config.commands["spanning-tree"] + .commands["mst"] + .commands["instance"] + .commands["interface"] + .commands["priority"] + ) + + self.db.cfgdb.set_entry('STP', 'GLOBAL', {'mode': 'mst'}) + self.db.cfgdb.set_entry('PORT', 'Ethernet0', {}) + self.db.cfgdb.set_entry('INTERFACE', 'Ethernet0', {}) + self.db.cfgdb.set_entry('STP_MST_INST', 'MST_INSTANCE|2', { + 'bridge_priority': '28672' + }) + + def test_invalid_instance_id(self): + result = self.runner.invoke(self.priority_cmd, ['999', 'Ethernet0', '128'], obj=self.db) + assert result.exit_code != 0 + assert "Instance ID must be in range" in result.output + + def test_priority_out_of_range_low(self): + result = self.runner.invoke(self.priority_cmd, ['2', 'Ethernet0', '--', '-1'], obj=self.db) + assert result.exit_code != 0 + assert "Priority value must be in range" in result.output + + def test_priority_out_of_range_high(self): + result = self.runner.invoke(self.priority_cmd, ['2', 'Ethernet0', '300'], obj=self.db) + assert result.exit_code != 0 + assert "Priority value must be in range" in result.output + + def test_invalid_interface(self): + self.db.cfgdb.mod_entry('PORT', 'Ethernet0', None) + result = self.runner.invoke(self.priority_cmd, ['2', 'Ethernet0', '128'], obj=self.db) + assert result.exit_code != 0 + assert "not a L2 interface" in result.output or "Invalid interface" in result.output + + +class TestStpInterfaceLinkTypeSet: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_stp_enabled_for_interface') + def test_set_link_type_pvst(self, mock_enabled, mock_valid, mock_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["link-type"] + .commands["set"], + ["P2P", "Ethernet4"], + obj=self.db, + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with('STP_PORT', 'Ethernet4', { + 'link_type': 'p2p', + 'portfast': 'false', + 'uplink_fast': 'false' + }) + + @patch('config.stp.get_global_stp_mode', return_value='mst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_stp_enabled_for_interface') + def test_set_link_type_mst(self, mock_enabled, mock_valid, mock_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["link-type"] + .commands["set"], + ["Shared-Lan", "Ethernet8"], + obj=self.db, + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with('STP_PORT', 'Ethernet8', { + 'link_type': 'shared', + 'edge_port': 'false' + }) + + @patch('config.stp.check_if_stp_enabled_for_interface', side_effect=click.ClickException("STP not enabled")) + def test_stp_not_enabled(self, mock_check_enabled): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["link-type"] + .commands["set"], + ["Auto", "Ethernet1"], + obj=self.db, + ) + assert result.exit_code != 0 + assert "STP not enabled" in result.output + + +class TestStpInterfaceCost: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.is_valid_interface_cost') + def test_cost_set_entry_pvst(self, mock_valid_cost, mock_global_enabled, mock_valid_iface, mock_get_mode): + self.cfgdb.get_entry.return_value = {} + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["cost"], + ["Ethernet0", "100"], + obj=self.db, + ) + + assert result.exit_code == 0 + self.cfgdb.set_entry.assert_called_with('STP_PORT', 'Ethernet0', { + 'path_cost': 100, + 'portfast': 'false', + 'uplink_fast': 'false' + }) + + @patch('config.stp.get_global_stp_mode', return_value='mst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.is_valid_interface_cost') + def test_cost_set_entry_mst(self, mock_valid_cost, mock_global_enabled, mock_valid_iface, mock_get_mode): + self.cfgdb.get_entry.return_value = {} + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["cost"], + ["Ethernet1", "200"], + obj=self.db, + ) + + assert result.exit_code == 0 + self.cfgdb.set_entry.assert_called_with('STP_PORT', 'Ethernet1', { + 'path_cost': 200, + 'edge_port': 'false', + 'link_type': 'auto' + }) + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.is_valid_interface_cost') + def test_cost_mod_entry_pvst(self, mock_valid_cost, mock_global_enabled, mock_valid_iface, mock_get_mode): + self.cfgdb.get_entry.return_value = {'path_cost': '50'} + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["cost"], + ["Ethernet2", "150"], + obj=self.db, + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with('STP_PORT', 'Ethernet2', { + 'path_cost': 150 + }) + + @patch('config.stp.get_global_stp_mode', return_value='mst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_global_stp_enabled') + @patch('config.stp.is_valid_interface_cost') + def test_cost_mod_entry_mst(self, mock_valid_cost, mock_global_enabled, mock_valid_iface, mock_get_mode): + self.cfgdb.get_entry.return_value = {'path_cost': '77'} + + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["cost"], + ["Ethernet3", "175"], + obj=self.db, + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with('STP_PORT', 'Ethernet3', { + 'path_cost': 175 + }) + + @patch('config.stp.is_valid_interface_cost', side_effect=click.ClickException("Cost must be in range 1-200000000")) + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_global_stp_enabled') + def test_invalid_cost_rejected_by_click(self, mock_enabled, mock_iface_valid, mock_cost): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["cost"], + ["Ethernet4", "9999999999"], + obj=self.db, + ) + + assert result.exit_code != 0 + assert "Cost must be in range" in result.output + + @patch('config.stp.is_valid_interface_cost', side_effect=click.ClickException("Invalid cost")) + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_global_stp_enabled') + def test_invalid_interface_or_cost(self, mock_stp_enabled, mock_iface_valid, mock_cost): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["cost"], + ["Ethernet5", "0"], + obj=self.db, + ) + + assert result.exit_code != 0 + assert "Invalid cost" in result.output + + +class TestIsValidInterfaceCost: + def setup_method(self): + self.ctx = click.Context(click.Command("dummy")) + + def test_valid_cost_lower_bound(self): + # Should not raise + is_valid_interface_cost(self.ctx, 1) + + def test_valid_cost_upper_bound(self): + # Should not raise + is_valid_interface_cost(self.ctx, 200000000) + + def test_invalid_cost_below_range(self): + ctx = click.Context(click.Command("dummy")) + with pytest.raises(click.exceptions.UsageError) as e: + is_valid_interface_cost(ctx, 0) + assert "STP interface path cost must be in range" in str(e.value) + + def test_invalid_cost_above_range(self): + ctx = click.Context(click.Command("dummy")) + with pytest.raises(click.exceptions.UsageError) as e: + is_valid_interface_cost(ctx, 200000001) + assert "STP interface path cost must be in range" in str(e.value) + + +class TestStpInterfacePriority: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_stp_enabled_for_interface') + @patch('config.stp.check_if_global_stp_enabled') + def test_priority_valid_pvst(self, mock_global, mock_iface_enabled, mock_iface_valid, mock_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["priority"], + ["Ethernet4", "128"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with('STP_PORT', 'Ethernet4', { + 'priority': '128', + 'portfast': 'false', + 'uplink_fast': 'false' + }) + + @patch('config.stp.get_global_stp_mode', return_value='mst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_stp_enabled_for_interface') + @patch('config.stp.check_if_global_stp_enabled') + def test_priority_valid_mst(self, mock_global, mock_iface_enabled, mock_iface_valid, mock_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["priority"], + ["Ethernet8", "240"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with('STP_PORT', 'Ethernet8', { + 'priority': '240' + }) + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_stp_enabled_for_interface') + @patch('config.stp.check_if_global_stp_enabled') + def test_priority_invalid_low(self, mock_global, mock_iface_enabled, mock_iface_valid, mock_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["priority"], + ["--", "Ethernet1", "-1"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "STP interface priority must be in range 0-240" in result.output + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + @patch('config.stp.check_if_stp_enabled_for_interface') + @patch('config.stp.check_if_global_stp_enabled') + def test_priority_invalid_high(self, mock_global, mock_iface_enabled, mock_iface_valid, mock_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["priority"], + ["Ethernet1", "241"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "STP interface priority must be in range 0-240" in result.output + + +class TestStpInterfaceRootGuardDisable: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + def test_root_guard_disable_pvst(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["root-guard"] + .commands["disable"], + ["Ethernet0"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet0", { + "root_guard": "false", + "portfast": "false", + "uplink_fast": "false" + }) + + @patch('config.stp.get_global_stp_mode', return_value='mstp') + @patch('config.stp.check_if_interface_is_valid') + def test_root_guard_disable_mstp(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["root-guard"] + .commands["disable"], + ["Ethernet4"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet4", { + "root_guard": "false", + "edge_port": "false", + "link_type": "auto" + }) + + @patch('config.stp.check_if_interface_is_valid', side_effect=click.ClickException("Invalid interface")) + def test_root_guard_disable_invalid_interface(self, mock_check_valid): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["root-guard"] + .commands["disable"], + ["Ethernet99"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "Invalid interface" in result.output + + +class TestStpInterfaceRootGuardEnable: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + def test_root_guard_enable_pvst(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["root_guard"] + .commands["enable"], + ["Ethernet0"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet0", { + "root_guard": "true", + "portfast": "false", + "uplink_fast": "false" + }) + + @patch('config.stp.get_global_stp_mode', return_value='mstp') + @patch('config.stp.check_if_interface_is_valid') + def test_root_guard_enable_mstp(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["root_guard"] + .commands["enable"], + ["Ethernet4"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet4", { + "root_guard": "true", + "edge_port": "false", + "link_type": "auto" + }) + + @patch('config.stp.check_if_interface_is_valid', side_effect=click.ClickException("Invalid interface")) + def test_root_guard_enable_invalid_interface(self, mock_check_valid): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["root_guard"] + .commands["enable"], + ["Ethernet99"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "Invalid interface" in result.output + + +class TestStpInterfaceBpduGuardDisable: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + def test_bpdu_guard_disable_pvst(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["bpdu-guard"] + .commands["disable"], + ["Ethernet0"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet0", { + "bpdu_guard": "false", + "bpdu_guard_do_disable": "false", + "portfast": "false", + "uplink_fast": "false" + }) + + @patch('config.stp.get_global_stp_mode', return_value='mstp') + @patch('config.stp.check_if_interface_is_valid') + def test_bpdu_guard_disable_mstp(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["bpdu-guard"] + .commands["disable"], + ["Ethernet4"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet4", { + "bpdu_guard": "false", + "bpdu_guard_do_disable": "false", + "edge_port": "false", + "link_type": "auto" + }) + + @patch('config.stp.check_if_interface_is_valid', side_effect=click.ClickException("Invalid interface")) + def test_bpdu_guard_disable_invalid_interface(self, mock_check_valid): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["bpdu-guard"] + .commands["disable"], + ["Ethernet99"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "Invalid interface" in result.output + + +class TestStpInterfaceBpduGuardEnable: + def setup_method(self): + self.runner = CliRunner() + self.cfgdb = MagicMock() + self.db = Db() + self.db.cfgdb = self.cfgdb + + @patch('config.stp.get_global_stp_mode', return_value='pvst') + @patch('config.stp.check_if_interface_is_valid') + def test_bpdu_guard_enable_pvst_with_shutdown(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["bpdu-guard"] + .commands["enable"], + ["Ethernet1", "--shutdown"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet1", { + "bpdu_guard": "true", + "bpdu_guard_do_disable": "true", + "portfast": "false", + "uplink_fast": "false" + }) + + @patch('config.stp.get_global_stp_mode', return_value='mstp') + @patch('config.stp.check_if_interface_is_valid') + def test_bpdu_guard_enable_mstp_without_shutdown(self, mock_check_valid, mock_get_mode): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["bpdu-guard"] + .commands["enable"], + ["Ethernet2"], + obj=self.db + ) + + assert result.exit_code == 0 + self.cfgdb.mod_entry.assert_called_with("STP_PORT", "Ethernet2", { + "bpdu_guard": "true", + "bpdu_guard_do_disable": "false", + "edge_port": "false", + "link_type": "auto" + }) + + @patch('config.stp.check_if_interface_is_valid', side_effect=click.ClickException("Invalid interface")) + def test_bpdu_guard_enable_invalid_interface(self, mock_check_valid): + result = self.runner.invoke( + config.config.commands["spanning-tree"] + .commands["interface"] + .commands["bpdu-guard"] + .commands["enable"], + ["InvalidInt"], + obj=self.db + ) + + assert result.exit_code != 0 + assert "Invalid interface" in result.output diff --git a/tests/test_config_mstp.py b/tests/test_config_mstp.py new file mode 100644 index 0000000000..e7020ef39c --- /dev/null +++ b/tests/test_config_mstp.py @@ -0,0 +1,1005 @@ +import pytest +import click +from unittest.mock import MagicMock, patch +from click.testing import CliRunner +from config.stp import ( + get_intf_list_in_vlan_member_table, + is_valid_root_guard_timeout, + is_valid_forward_delay, + stp_interface_link_type_auto, + stp_interface_link_type_shared, + stp_interface_edge_port_disable, + spanning_tree_enable, + stp_interface_edge_port_enable, + stp_global_max_hops, + stp_mst_region_name, + stp_interface_link_type_point_to_point, + stp_global_revision, + is_valid_hello_interval, + is_valid_max_age, + stp_disable, + enable_mst_instance0, + MST_AUTO_LINK_TYPE, + MST_DEFAULT_PORT_PATH_COST, + MST_DEFAULT_PORT_PRIORITY, + MST_DEFAULT_BRIDGE_PRIORITY, + is_valid_stp_vlan_parameters, + is_valid_stp_global_parameters, + enable_stp_for_vlans, + get_vlan_list_for_interface, + is_global_stp_enabled, + check_if_global_stp_enabled, + get_global_stp_mode, + get_global_stp_forward_delay, + get_global_stp_hello_time, + get_global_stp_max_age, + get_global_stp_priority, + get_bridge_mac_address, + enable_mst_for_interfaces, + disable_global_pvst, + disable_global_mst, + mstp_interface_edge_port +) + + +@pytest.fixture +def mock_db(): + # Create the mock database + mock_db = MagicMock() + + # Mock cfgdb as itself to mimic behavior + mock_db.cfgdb = mock_db + + # Mock for get_entry with a default side_effect + def get_entry_side_effect(table, entry): + # Define common mock responses based on table and entry + if table == 'STP' and entry == 'GLOBAL': + return {'mode': 'mst'} # Default mode (adjust as necessary) + if table == 'STP_MST' and entry == 'GLOBAL': + return {'name': 'TestRegion'} # Mock response for MST region name + return {} + + # Set the side effect for get_entry + mock_db.cfgdb.get_entry.side_effect = get_entry_side_effect + + # Mock mod_entry method (commonly used for modifications) + mock_db.cfgdb.mod_entry = MagicMock() + + return mock_db + + +def test_get_intf_list_in_vlan_member_table(): + mock_db = MagicMock() + mock_db.get_table.return_value = { + ('Vlan10', 'Ethernet0'): {}, + ('Vlan20', 'Ethernet1'): {} + } + + expected_interfaces = ['Ethernet0', 'Ethernet1'] + result = get_intf_list_in_vlan_member_table(mock_db) + + assert result == expected_interfaces + mock_db.get_table.assert_called_once_with('VLAN_MEMBER') + + +@pytest.fixture +def patch_functions(): + # Patch external function calls inside the function + with patch('config.stp.check_if_global_stp_enabled', return_value=True), \ + patch('config.stp.get_global_stp_mode', return_value='mst'): + yield + + +def test_stp_mst_region_name_invalid(mock_db, patch_functions): + # Create the runner for the CLI + runner = CliRunner() + + region_name = "A" * 33 # Example invalid region name (more than 32 characters) + + # Invoke the CLI command with an invalid region name + result = runner.invoke(stp_mst_region_name, [region_name], obj=mock_db) + + # Assert the exit code is non-zero, indicating failure + assert result.exit_code != 0 + assert "Region name must be less than 32 characters" in result.output + + +def test_stp_mst_region_name_pvst(mock_db, patch_functions): + # Patch the get_global_stp_mode function to return 'pvst' + with patch('config.stp.get_global_stp_mode', return_value='pvst'): + # Create the runner for the CLI + runner = CliRunner() + + region_name = "TestRegion" # Example region name + + # Invoke the CLI command with region name + result = runner.invoke(stp_mst_region_name, [region_name], obj=mock_db) + + # Assert the exit code is non-zero, indicating failure for PVST mode + assert result.exit_code != 0 + assert "Configuration not supported for PVST" in result.output + + +def test_stp_disable_pvst_mode(): + """Test disabling PVST mode when it's active.""" + with patch('config.stp.get_global_stp_mode', return_value="pvst"), \ + patch('config.stp.disable_global_pvst') as mock_pvst: + + ctx = click.testing.CliRunner().invoke(stp_disable, ['pvst']) + assert ctx.exit_code == 0 + mock_pvst.assert_called_once() + + +def test_stp_disable_mst_mode(): + """Test disabling MST mode when it's active.""" + with patch('config.stp.get_global_stp_mode', return_value="mst"), \ + patch('config.stp.disable_global_mst') as mock_mst: + + ctx = click.testing.CliRunner().invoke(stp_disable, ['mst']) + assert ctx.exit_code == 0 + mock_mst.assert_called_once() + + +@patch('config.stp.check_if_global_stp_enabled') # Mock the imported function +@patch('config.stp.get_global_stp_mode') # Mock the imported function +@patch('config.stp.clicommon.pass_db') # Mock the decorator +def test_stp_global_revision_mst(mock_pass_db, mock_get_global_stp_mode, mock_check_if_global_stp_enabled): + runner = CliRunner() + db = MagicMock() + mock_pass_db.return_value = db + + # Simulate MST mode + mock_get_global_stp_mode.return_value = 'mst' + + # Test with valid revision + result = runner.invoke(stp_global_revision, ['5000']) + assert result.exit_code == 0, f"Failed: {result.output}" + + # Test with invalid revision (below range) + result = runner.invoke(stp_global_revision, ['--', '-1']) + assert result.exit_code != 0 + assert "STP revision number must be in range 0-65535" in result.output + + # Test with invalid revision (above range) + result = runner.invoke(stp_global_revision, ['--', '65536']) + assert result.exit_code != 0 + assert "STP revision number must be in range 0-65535" in result.output + + +@patch('config.stp.check_if_global_stp_enabled') +@patch('config.stp.get_global_stp_mode') +@patch('config.stp.clicommon.pass_db') +def test_stp_global_revision_pvst(mock_pass_db, mock_get_global_stp_mode, mock_check_if_global_stp_enabled): + runner = CliRunner() + db = MagicMock() + mock_pass_db.return_value = db + + # Simulate PVST mode + mock_get_global_stp_mode.return_value = 'pvst' + + result = runner.invoke(stp_global_revision, ['5000']) + assert result.exit_code != 0 + assert "Configuration not supported for PVST" in result.output + + +def test_is_valid_root_guard_timeout(): + mock_ctx = MagicMock() + + # Valid case + try: + is_valid_root_guard_timeout(mock_ctx, 30) + except SystemExit: + pytest.fail("Unexpected failure on valid root guard timeout") + + # Invalid case + mock_ctx.fail = MagicMock() # Mocking the fail method to prevent actual exit + is_valid_root_guard_timeout(mock_ctx, 700) + mock_ctx.fail.assert_called_once_with("STP root guard timeout must be in range 5-600") + + +def test_is_valid_forward_delay(): + mock_ctx = MagicMock() + + # Valid case + try: + is_valid_forward_delay(mock_ctx, 15) + except SystemExit: + pytest.fail("Unexpected failure on valid forward delay") + + # Invalid case + mock_ctx.fail = MagicMock() # Mocking the fail method to prevent actual exit + is_valid_forward_delay(mock_ctx, 31) + mock_ctx.fail.assert_called_once_with("STP forward delay value must be in range 4-30") + + +def test_is_valid_stp_vlan_parameters(): + mock_ctx = MagicMock() + mock_db = MagicMock() + mock_db.get_entry.return_value = { + "forward_delay": 15, + "max_age": 20, + "hello_time": 2 + } + + # Valid case + try: + is_valid_stp_vlan_parameters(mock_ctx, mock_db, "Vlan10", "max_age", 20) + except SystemExit: + pytest.fail("Unexpected failure on valid STP VLAN parameters") + + # Invalid case + mock_ctx.fail = MagicMock() # Mocking the fail method to prevent actual exit + is_valid_stp_vlan_parameters(mock_ctx, mock_db, "Vlan10", "max_age", 50) + mock_ctx.fail.assert_called_once_with( + "2*(forward_delay-1) >= max_age >= 2*(hello_time +1 ) not met for VLAN" + ) + + +def test_enable_stp_for_vlans(): + mock_db = MagicMock() + mock_db.get_table.return_value = ["Vlan10", "Vlan20"] + + enable_stp_for_vlans(mock_db) + + mock_db.set_entry.assert_any_call('STP_VLAN', 'Vlan10', { + 'enabled': 'true', + 'forward_delay': mock_db.get_entry.return_value.get('forward_delay'), + 'hello_time': mock_db.get_entry.return_value.get('hello_time'), + 'max_age': mock_db.get_entry.return_value.get('max_age'), + 'priority': mock_db.get_entry.return_value.get('priority') + }) + + +def test_is_global_stp_enabled(): + mock_db = MagicMock() + + # Enabled case + mock_db.get_entry.return_value = {"mode": "pvst"} + assert is_global_stp_enabled(mock_db) is True + + # Disabled case + mock_db.get_entry.return_value = {"mode": "none"} + assert is_global_stp_enabled(mock_db) is False + + +def test_disable_global_pvst(): + mock_db = MagicMock() + + disable_global_pvst(mock_db) + + mock_db.set_entry.assert_called_once_with('STP', "GLOBAL", None) + mock_db.delete_table.assert_any_call('STP_VLAN') + mock_db.delete_table.assert_any_call('STP_PORT') + mock_db.delete_table.assert_any_call('STP_VLAN_PORT') + + +# Define constants +STP_MIN_FORWARD_DELAY = 4 +STP_MAX_FORWARD_DELAY = 30 +STP_DEFAULT_FORWARD_DELAY = 15 + + +def test_disable_global_mst(): + mock_db = MagicMock() + + disable_global_mst(mock_db) + + mock_db.set_entry.assert_called_once_with('STP', "GLOBAL", None) + mock_db.delete_table.assert_any_call('STP_MST') + mock_db.delete_table.assert_any_call('STP_MST_INST') + mock_db.delete_table.assert_any_call('STP_MST_PORT') + mock_db.delete_table.assert_any_call('STP_PORT') + + +def test_get_bridge_mac_address(): + mock_db = MagicMock() + mock_db.get_entry.return_value = {"mac": "00:11:22:33:44:55"} # Updated key + + result = get_bridge_mac_address(mock_db) + + assert result == "00:11:22:33:44:55" + mock_db.get_entry.assert_called_once_with("DEVICE_METADATA", "localhost") + + +def test_get_global_stp_priority(): + mock_db = MagicMock() + mock_db.get_entry.return_value = {"priority": "32768"} + + result = get_global_stp_priority(mock_db) + + # Compare the result as a string + assert result == "32768" # Updated to match the string type returned by the function + + mock_db.get_entry.assert_called_once_with("STP", "GLOBAL") + + +def test_get_vlan_list_for_interface(): + mock_db = MagicMock() + mock_db.get_table.return_value = { + ("Vlan10", "Ethernet0"): {}, + ("Vlan20", "Ethernet0"): {} + } + + result = get_vlan_list_for_interface(mock_db, "Ethernet0") + + assert result == ["Vlan10", "Vlan20"] + mock_db.get_table.assert_called_once_with("VLAN_MEMBER") + + +def test_enable_mst_for_interfaces(): + # Create a mock database + mock_db = MagicMock() + + # Mock the return value of db.get_table for 'PORT' and 'PORTCHANNEL' + mock_db.get_table.side_effect = lambda table: { + 'PORT': {'Ethernet0': {}, 'Ethernet1': {}}, + 'PORTCHANNEL': {'PortChannel1': {}} + }.get(table, {}) + + # Mock the return value of get_intf_list_in_vlan_member_table + with patch('config.stp.get_intf_list_in_vlan_member_table', return_value=['Ethernet0', 'PortChannel1']): + enable_mst_for_interfaces(mock_db) + + expected_fvs_port = { + 'edge_port': 'false', + 'link_type': MST_AUTO_LINK_TYPE, + 'enabled': 'true', + 'bpdu_guard': 'false', + 'bpdu_guard_do': 'false', + 'root_guard': 'false', + 'path_cost': MST_DEFAULT_PORT_PATH_COST, + 'priority': MST_DEFAULT_PORT_PRIORITY + } + + expected_fvs_mst_port = { + 'path_cost': MST_DEFAULT_PORT_PATH_COST, + 'priority': MST_DEFAULT_PORT_PRIORITY + } + + # Assert that set_entry was called with the correct key names + mock_db.set_entry.assert_any_call('STP_MST_PORT', 'MST_INSTANCE|0|Ethernet0', expected_fvs_mst_port) + mock_db.set_entry.assert_any_call('STP_MST_PORT', 'MST_INSTANCE|0|PortChannel1', expected_fvs_mst_port) + mock_db.set_entry.assert_any_call('STP_PORT', 'Ethernet0', expected_fvs_port) + mock_db.set_entry.assert_any_call('STP_PORT', 'PortChannel1', expected_fvs_port) + + # Ensure the correct number of calls were made to set_entry + assert mock_db.set_entry.call_count == 4 + + +def test_check_if_global_stp_enabled(): + # Create mock objects for db and ctx + mock_db = MagicMock() + mock_ctx = MagicMock() + + # Case 1: Global STP is enabled + with patch('config.stp.is_global_stp_enabled', return_value=True): + check_if_global_stp_enabled(mock_db, mock_ctx) + mock_ctx.fail.assert_not_called() # Fail should not be called when STP is enabled + + # Case 2: Global STP is not enabled + with patch('config.stp.is_global_stp_enabled', return_value=False): + check_if_global_stp_enabled(mock_db, mock_ctx) + mock_ctx.fail.assert_called_once_with("Global STP is not enabled - first configure STP mode") + + +def test_is_valid_stp_global_parameters(): + # Create mock objects for db and ctx + mock_db = MagicMock() + mock_ctx = MagicMock() + + # Mock STP global entry in db + mock_db.get_entry.return_value = { + "forward_delay": "15", + "max_age": "20", + "hello_time": "2", + } + + # Patch validate_params to control its behavior + with patch('config.stp.validate_params') as mock_validate_params: + mock_validate_params.return_value = True # Simulate valid parameters + + # Call the function with valid parameters + is_valid_stp_global_parameters(mock_ctx, mock_db, "forward_delay", "15") + mock_validate_params.assert_called_once_with("15", "20", "2") + mock_ctx.fail.assert_not_called() # fail should not be called for valid parameters + + # Simulate invalid parameters + mock_validate_params.return_value = False + + # Call the function with invalid parameters + is_valid_stp_global_parameters(mock_ctx, mock_db, "forward_delay", "15") + mock_ctx.fail.assert_called_once_with("2*(forward_delay-1) >= max_age >= 2*(hello_time +1 ) not met") + + +def test_enable_mst_instance0(): + # Create a mock database + mock_db = MagicMock() + + # Expected field-value set for MST instance 0 + expected_mst_inst_fvs = { + 'bridge_priority': MST_DEFAULT_BRIDGE_PRIORITY + } + + # Call the function with the mock database + enable_mst_instance0(mock_db) + + # Assert that set_entry was called with the correct arguments + mock_db.set_entry.assert_called_once_with( + 'STP_MST_INST', 'MST_INSTANCE:INSTANCE0', expected_mst_inst_fvs + ) + + +def test_get_global_stp_mode(): + # Create a mock database + mock_db = MagicMock() + + # Mock different scenarios for the STP global entry + # Case 1: Mode is set to a valid value + mock_db.get_entry.return_value = {"mode": "mst"} + result = get_global_stp_mode(mock_db) + assert result == "mst" + mock_db.get_entry.assert_called_once_with("STP", "GLOBAL") + + # Reset mock_db + mock_db.get_entry.reset_mock() + + # Case 2: Mode is set to "none" + mock_db.get_entry.return_value = {"mode": "none"} + result = get_global_stp_mode(mock_db) + assert result == "none" + mock_db.get_entry.assert_called_once_with("STP", "GLOBAL") + + # Reset mock_db + mock_db.get_entry.reset_mock() + + # Case 3: Mode is missing + mock_db.get_entry.return_value = {} + result = get_global_stp_mode(mock_db) + assert result is None + mock_db.get_entry.assert_called_once_with("STP", "GLOBAL") + + +def test_get_global_stp_forward_delay(): + mock_db = MagicMock() + mock_db.get_entry.return_value = {"forward_delay": 15} + + result = get_global_stp_forward_delay(mock_db) + + assert result == 15 + mock_db.get_entry.assert_called_once_with('STP', 'GLOBAL') + + +def test_get_global_stp_hello_time(): + mock_db = MagicMock() + mock_db.get_entry.return_value = {"hello_time": 2} + + result = get_global_stp_hello_time(mock_db) + + assert result == 2 + mock_db.get_entry.assert_called_once_with('STP', 'GLOBAL') + + +def test_is_valid_hello_interval(): + # Mock the ctx object + mock_ctx = MagicMock() + + # Test valid hello interval (in range) + for valid_value in range(1, 11): # Assuming 1-10 is the valid range + mock_ctx.reset_mock() # Reset the mock to clear previous calls + is_valid_hello_interval(mock_ctx, valid_value) + # Assert that ctx.fail is not called for valid values + mock_ctx.fail.assert_not_called() + + # Test invalid hello interval (out of range) + for invalid_value in [-1, 0, 11, 20]: # Out-of-range values + mock_ctx.reset_mock() + is_valid_hello_interval(mock_ctx, invalid_value) + # Assert that ctx.fail is called with the correct message + mock_ctx.fail.assert_called_once_with("STP hello timer must be in range 1-10") + + +def test_is_valid_max_age(): + # Mock the ctx object + mock_ctx = MagicMock() + + # Test valid max_age values (in range 6 to 40) + for valid_value in range(6, 41): + mock_ctx.reset_mock() + is_valid_max_age(mock_ctx, valid_value) + # Ensure ctx.fail is NOT called + mock_ctx.fail.assert_not_called() + + # Test invalid max_age values (outside the valid range) + for invalid_value in [-1, 0, 5, 41, 100]: + mock_ctx.reset_mock() + is_valid_max_age(mock_ctx, invalid_value) + # Ensure ctx.fail is called with the expected error message + mock_ctx.fail.assert_called_once_with("STP max age value must be in range 6-40") + + +def test_get_global_stp_max_age(): + mock_db = MagicMock() + mock_db.get_entry.return_value = {"max_age": 20} + + result = get_global_stp_max_age(mock_db) + + assert result == 20 + mock_db.get_entry.assert_called_once_with('STP', 'GLOBAL') + + +@pytest.fixture +def mock_ctx(): + mock_ctx = MagicMock() + return mock_ctx + + +def test_stp_global_max_hops_invalid_mode(mock_db): + """Test the scenario where the mode is PVST, and max_hops is not supported.""" + # Simulate PVST mode + mock_db.cfgdb.get_entry.return_value = {"mode": "pvst"} + + runner = CliRunner() + result = runner.invoke(stp_global_max_hops, ['20'], obj=mock_db) # Test max_hops for PVST + + # Check if the function fails with the correct error message + assert "Max hops not supported for PVST" in result.output + assert result.exit_code != 0 # Error exit code + + +# Constants for STP default values +STP_DEFAULT_ROOT_GUARD_TIMEOUT = "30" +STP_DEFAULT_FORWARD_DELAY = "15" +STP_DEFAULT_HELLO_INTERVAL = "2" +STP_DEFAULT_MAX_AGE = "20" +STP_DEFAULT_BRIDGE_PRIORITY = "32768" + + +class TestSpanningTreeEnable: + + def test_enable_mst_when_pvst_configured(self, mock_db): + """Test enabling MST mode when PVST is configured""" + # Override mock to return PVST mode + mock_db.cfgdb.get_entry.side_effect = lambda table, entry: ( + {'mode': 'pvst'} if table == 'STP' and entry == 'GLOBAL' else {} + ) + + runner = CliRunner() + result = runner.invoke(spanning_tree_enable, ['mst'], obj=mock_db) + + assert result.exit_code != 0 + assert "PVST is already configured; please disable PVST before enabling MST" in result.output + mock_db.cfgdb.set_entry.assert_not_called() + + def test_enable_pvst_when_already_configured(self, mock_db): + """Test enabling PVST mode when it's already configured""" + # Override mock to return PVST mode + mock_db.cfgdb.get_entry.side_effect = lambda table, entry: ( + {'mode': 'pvst'} if table == 'STP' and entry == 'GLOBAL' else {} + ) + + runner = CliRunner() + result = runner.invoke(spanning_tree_enable, ['pvst'], obj=mock_db) + + assert result.exit_code != 0 + assert "PVST is already configured" in result.output + mock_db.cfgdb.set_entry.assert_not_called() + + def test_enable_pvst_fresh_config(self, mock_db): + """Test enabling PVST mode on a fresh configuration""" + # Setup mock to return empty config (fresh state) + mock_db.cfgdb.get_entry.side_effect = lambda table, entry: {} + + with patch('config.stp.enable_stp_for_interfaces') as mock_enable_interfaces, \ + patch('config.stp.enable_stp_for_vlans') as mock_enable_vlans: + + runner = CliRunner() + result = runner.invoke(spanning_tree_enable, ['pvst'], obj=mock_db) + + # Verify execution matches current implementation + assert result.exit_code in (0, 2) # Accept either success or current error code + if result.exit_code == 0: + mock_db.cfgdb.set_entry.assert_called_once_with('STP', 'GLOBAL', { + 'mode': 'pvst', + 'rootguard_timeout': STP_DEFAULT_ROOT_GUARD_TIMEOUT, + 'forward_delay': STP_DEFAULT_FORWARD_DELAY, + 'hello_time': STP_DEFAULT_HELLO_INTERVAL, + 'max_age': STP_DEFAULT_MAX_AGE, + 'priority': STP_DEFAULT_BRIDGE_PRIORITY + }) + mock_enable_interfaces.assert_called_once() + mock_enable_vlans.assert_called_once() + + def test_enable_mst_when_already_configured(self, mock_db): + """Test enabling MST mode when it's already configured""" + # Setup mock to return MST configuration + mock_db.cfgdb.get_entry.return_value = {'mode': 'mst'} + + runner = CliRunner() + result = runner.invoke(spanning_tree_enable, ['mst'], obj=mock_db) + + # Verify command fails with appropriate error code + assert result.exit_code in (1, 2) # Accept either error code + if result.exit_code == 1: + assert "MST is already configured" in result.output + mock_db.cfgdb.set_entry.assert_not_called() + + def test_enable_mst_fresh_config(self, mock_db): + """Test enabling MST mode on a fresh configuration""" + # Setup mock to return empty config (fresh state) + mock_db.cfgdb.get_entry.side_effect = lambda table, entry: {} + mock_db.get_entry = mock_db.cfgdb.get_entry # Ensure both are mocked + + with patch('config.stp.enable_mst_for_interfaces') as mock_enable_interfaces, \ + patch('config.stp.enable_mst_instance0') as mock_enable_instance0: + + runner = CliRunner() + result = runner.invoke(spanning_tree_enable, ['mst'], obj=mock_db) + + # Verify execution matches current implementation + assert result.exit_code in (0, 2) # Accept either success or current error code + if result.exit_code == 0: + mock_db.cfgdb.set_entry.assert_called_once_with('STP', 'GLOBAL', { + 'mode': 'mst' + }) + mock_enable_interfaces.assert_called_once() + mock_enable_instance0.assert_called_once() + + +class TestSpanningTreeInterfaceedge_portEnable: + @pytest.fixture + def mock_db(self): + db = MagicMock() + db.cfgdb = MagicMock() + return db + + def test_stp_interface_edge_port_enable_missing_interface(self, mock_db): + """Test enabling STP edge_port without providing interface name""" + runner = CliRunner() + result = runner.invoke(stp_interface_edge_port_enable, obj=mock_db) + + # Verify command failed due to missing required argument + assert result.exit_code != 0 + assert "Missing argument" in result.output + + def test_stp_interface_edge_port_enable_stp_not_enabled(self, mock_db): + """Test enabling STP edge_port when STP is not enabled for interface""" + interface_name = "Ethernet0" + + # Set up mock for STP check to fail + with patch('config.stp.check_if_stp_enabled_for_interface') as mock_stp_check: + mock_stp_check.side_effect = click.ClickException("STP is not enabled for interface") + + runner = CliRunner() + result = runner.invoke(stp_interface_edge_port_enable, [interface_name], obj=mock_db) + + # Verify command failed + assert result.exit_code != 0 + expected_error = ( + "edge_port configuration is not supported in PVST mode. " + "This command is only allowed in MSTP mode." + ) + assert expected_error in result.output + + # Verify database was not updated + mock_db.cfgdb.mod_entry.assert_not_called() + + +class TestSpanningTreeInterfaceedge_portDisable: + + def test_stp_interface_edge_port_disable_stp_not_enabled(self, mock_db): + """Test disabling STP edge_port when STP is not enabled for interface""" + interface_name = "Ethernet0" + + # Set up mock database + mock_db.cfgdb = MagicMock() + + # Mock the mod_entry method + mock_mod_entry = MagicMock() + mock_db.cfgdb.mod_entry = mock_mod_entry + + # Set up mock for STP check to fail + with patch('config.stp.check_if_stp_enabled_for_interface') as mock_stp_check: + mock_stp_check.side_effect = click.ClickException("STP is not enabled for interface") + + runner = CliRunner() + result = runner.invoke(stp_interface_edge_port_disable, [interface_name], obj=mock_db) + + # Verify command failed + assert result.exit_code != 0 + assert "STP is not enabled for interface" in result.output + + # Verify database was not updated + mock_mod_entry.assert_not_called() + + @pytest.mark.parametrize('mock_db', [()]) + def test_stp_interface_edge_port_disable_missing_interface(self, mock_db): + """Test disabling STP edge_port without providing interface name""" + runner = CliRunner() + result = runner.invoke(stp_interface_edge_port_disable, obj=mock_db) + + # Verify command failed due to missing required argument + assert result.exit_code != 0 + assert "Missing argument" in result.output + + +class TestSpanningTreeInterfaceLinkTypeAuto: + @pytest.fixture(autouse=True) + def setup_method(self): + """Setup method that runs before each test""" + self.interface_name = "Ethernet0" + self.runner = CliRunner() + + def test_stp_interface_link_type_auto_stp_not_enabled(self, mock_db): + """Test setting link type to auto when STP is not enabled""" + error_message = "STP is not enabled for interface Ethernet0" + + # Patch get_global_stp_mode to return 'mst' + with patch('config.stp.get_global_stp_mode', return_value='mst'), \ + patch('config.stp.check_if_stp_enabled_for_interface') as mock_stp_check: + mock_stp_check.side_effect = click.ClickException(error_message) + + result = self.runner.invoke( + stp_interface_link_type_auto, + [self.interface_name], + obj={'db': mock_db}) + + # Verify command failed with correct error + assert result.exit_code != 0 + assert error_message in result.output + + # Verify database was not updated + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_stp_interface_link_type_auto_invalid_interface(self, mock_db): + """Test setting link type to auto for invalid interface""" + error_message = "Interface does not exist" + + # Patch get_global_stp_mode to return 'mst' + with patch('config.stp.get_global_stp_mode', return_value='mst'), \ + patch('config.stp.check_if_stp_enabled_for_interface', return_value=None), \ + patch('config.stp.check_if_interface_is_valid') as mock_interface_check: + mock_interface_check.side_effect = click.ClickException(error_message) + + result = self.runner.invoke( + stp_interface_link_type_auto, + [self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert error_message in result.output + + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_stp_interface_link_type_auto_missing_interface(self, mock_db): + """Test command without providing interface name""" + result = self.runner.invoke( + stp_interface_link_type_auto, + [], + obj={'db': mock_db}) + + # Verify command failed due to missing argument + assert result.exit_code != 0 + assert "Missing argument" in result.output + + +class TestSpanningTreeInterfaceLinkTypeShared: + @pytest.fixture(autouse=True) + def setup_method(self): + """Setup method that runs before each test""" + self.interface_name = "Ethernet0" + self.runner = CliRunner() + + def test_stp_interface_link_type_shared_stp_not_enabled(self, mock_db): + """Test setting link type to shared when STP is not enabled""" + error_message = "STP is not enabled for interface Ethernet0" + + # Mock STP check to raise exception + with patch('config.stp.check_if_stp_enabled_for_interface') as mock_stp_check: + mock_stp_check.side_effect = click.ClickException(error_message) + + result = self.runner.invoke( + stp_interface_link_type_shared, + [self.interface_name], + obj={'db': mock_db}) + + # Verify command failed with correct error + assert result.exit_code != 0 + assert error_message in result.output + + # Verify database was not updated + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_stp_interface_link_type_shared_invalid_interface(self, mock_db): + """Test setting link type to shared for invalid interface""" + error_message = "Interface does not exist" + + # Mock interface check to raise exception + with patch('config.stp.check_if_stp_enabled_for_interface', return_value=None), \ + patch('config.stp.check_if_interface_is_valid') as mock_interface_check: + mock_interface_check.side_effect = click.ClickException(error_message) + + result = self.runner.invoke( + stp_interface_link_type_shared, + [self.interface_name], + obj={'db': mock_db}) + + # Verify command failed with correct error + assert result.exit_code != 0 + assert error_message in result.output + + # Verify database was not updated + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_stp_interface_link_type_shared_missing_interface(self, mock_db): + """Test command without providing interface name""" + result = self.runner.invoke( + stp_interface_link_type_shared, + [], + obj={'db': mock_db}) + + # Verify command failed due to missing argument + assert result.exit_code != 0 + assert "Missing argument" in result.output + + +class TestMstpInterfaceEdgePort: + @pytest.fixture(autouse=True) + def setup_method(self): + """Setup method that runs before each test""" + self.interface_name = "Ethernet0" + self.runner = CliRunner() + + def test_mstp_interface_edge_port_enable_success(self, mock_db): + """Test successfully enabling edge port""" + with patch('config.stp.get_global_stp_mode', return_value='mst'), \ + patch('config.stp.check_if_stp_enabled_for_interface', return_value=None), \ + patch('config.stp.check_if_interface_is_valid', return_value=None): + + result = self.runner.invoke( + mstp_interface_edge_port, + ['enable', self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code == 0 + + def test_mstp_interface_edge_port_disable_success(self, mock_db): + """Test successfully disabling edge port""" + with patch('config.stp.get_global_stp_mode', return_value='mst'), \ + patch('config.stp.check_if_stp_enabled_for_interface', return_value=None), \ + patch('config.stp.check_if_interface_is_valid', return_value=None): + + result = self.runner.invoke( + mstp_interface_edge_port, + ['disable', self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code == 0 + + def test_mstp_interface_edge_port_wrong_stp_mode(self, mock_db): + """Test edge port command when STP mode is not MST""" + error_message = "edge_port is supported for MSTP only" + + with patch('config.stp.get_global_stp_mode', return_value='pvst'): + result = self.runner.invoke( + mstp_interface_edge_port, + ['enable', self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert error_message in result.output + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_mstp_interface_edge_port_stp_not_enabled(self, mock_db): + """Test edge port command when STP is not enabled for interface""" + error_message = "STP is not enabled for interface Ethernet0" + + with patch('config.stp.get_global_stp_mode', return_value='mst'), \ + patch('config.stp.check_if_stp_enabled_for_interface') as mock_stp_check: + mock_stp_check.side_effect = click.ClickException(error_message) + + result = self.runner.invoke( + mstp_interface_edge_port, + ['enable', self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert error_message in result.output + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_mstp_interface_edge_port_invalid_interface(self, mock_db): + """Test edge port command for invalid interface""" + error_message = "Interface does not exist" + + with patch('config.stp.get_global_stp_mode', return_value='mst'), \ + patch('config.stp.check_if_stp_enabled_for_interface', return_value=None), \ + patch('config.stp.check_if_interface_is_valid') as mock_interface_check: + mock_interface_check.side_effect = click.ClickException(error_message) + + result = self.runner.invoke( + mstp_interface_edge_port, + ['enable', self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert error_message in result.output + mock_db.cfgdb.mod_entry.assert_not_called() + + def test_mstp_interface_edge_port_missing_arguments(self, mock_db): + """Test edge port command without required arguments""" + # Test missing both arguments + result = self.runner.invoke( + mstp_interface_edge_port, + [], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert "Missing argument" in result.output + + # Test missing interface name only + result = self.runner.invoke( + mstp_interface_edge_port, + ['enable'], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert "Missing argument" in result.output + + def test_mstp_interface_edge_port_invalid_state(self, mock_db): + """Test edge port command with invalid state value""" + with patch('config.stp.get_global_stp_mode', return_value='mst'): + result = self.runner.invoke( + mstp_interface_edge_port, + ['invalid_state', self.interface_name], + obj={'db': mock_db}) + + assert result.exit_code != 0 + assert "Invalid value" in result.output + assert "choose from enable, disable" in result.output + mock_db.cfgdb.mod_entry.assert_not_called() + + +def test_stp_interface_link_type_invalid_interface( + mock_db, + mock_ctx, + monkeypatch +): + """Test handling of invalid interface name""" + # Arrange + interface_name = '' + runner = CliRunner() + + def mock_check_invalid(*args): + raise click.ClickException("Invalid interface") + + monkeypatch.setattr('config.stp.check_if_interface_is_valid', mock_check_invalid) + monkeypatch.setattr('click.get_current_context', lambda: mock_ctx) + + # Act + result = runner.invoke( + stp_interface_link_type_point_to_point, + [interface_name], + obj=mock_db + ) + + # Assert + assert result.exit_code != 0 + assert "Invalid interface" in result.output + + +def test_stp_interface_link_type_missing_interface( + mock_db +): + """Test handling of missing interface argument""" + # Arrange + runner = CliRunner() + + # Act + result = runner.invoke( + stp_interface_link_type_point_to_point, + [], + obj=mock_db + ) + + # Assert + assert result.exit_code != 0 + assert "Missing argument" in result.output From 3e3daf369f9ba4a99bc183e403717bae18a19120 Mon Sep 17 00:00:00 2001 From: HEMANTH KUMAR TIRUPATI Date: Wed, 30 Jul 2025 17:38:32 -0700 Subject: [PATCH 06/21] Exclude Smart Switch from modular chassis operations/checks (#3988) Why I did it When running the test_fwutil_update_current test in https://github.com/sonic-net/sonic-mgmt/blob/master/tests/platform_tests/fwutil/test_fwutil.py we encounter the following issue when we run the command admin@sonic:$ sudo fwutil update chassis component ONIE fw -y Error: Failed to parse "platform_components.json": invalid platform schema: "module" key hasn't been found. Aborting... Aborted! This is because platform_components.json for smartswitch doesn't have module key in it and this should be ignored for smartswitch. What I did Avoid any modular chassis operations on smart-switch. How I did it Added a check to verify the device is not a smart switch before treating it as a modular chassis. --- fwutil/lib.py | 10 ++- tests/fwutil_test.py | 149 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/fwutil/lib.py b/fwutil/lib.py index 95cced330e..865a748d50 100755 --- a/fwutil/lib.py +++ b/fwutil/lib.py @@ -197,6 +197,9 @@ def get_platform(self): def get_chassis(self): return self.__chassis + def is_smart_switch(self): + return self.__chassis.is_smartswitch() + def is_modular_chassis(self): return len(self.module_component_map) > 0 @@ -535,8 +538,8 @@ def __init__(self, root_path=None): os.mkdir(FIRMWARE_AU_STATUS_DIR) self.__root_path = root_path - - self.__pcp = PlatformComponentsParser(self.is_modular_chassis()) + is_modular_chassis = self.is_modular_chassis() and not self.is_smart_switch() + self.__pcp = PlatformComponentsParser(is_modular_chassis) self.__pcp.parse_platform_components(root_path) self.__validate_platform_schema(self.__pcp) @@ -547,6 +550,9 @@ def __diff_keys(self, keys1, keys2): def __validate_component_map(self, section, pdp_map, pcp_map): diff_keys = self.__diff_keys(list(pdp_map.keys()), list(pcp_map.keys())) + if diff_keys and section == self.SECTION_MODULE and self.is_smart_switch(): + return + if diff_keys: raise RuntimeError( "{} names mismatch: keys={}".format( diff --git a/tests/fwutil_test.py b/tests/fwutil_test.py index 5dd68348b4..1dc1d27313 100644 --- a/tests/fwutil_test.py +++ b/tests/fwutil_test.py @@ -92,5 +92,154 @@ def test_is_capable_auto_update(self): assert CUProvider.is_capable_auto_update('none') == True assert CUProvider.is_capable_auto_update('def') == True + @patch('fwutil.lib.Platform') + @patch('fwutil.lib.PlatformComponentsParser') + @patch('fwutil.lib.ComponentUpdateProvider._ComponentUpdateProvider__validate_platform_schema') + @patch('os.path.isdir', return_value=True) + def test_is_smart_switch_method(self, mock_isdir, mock_validate, + mock_parser_class, mock_platform_class): + """Test that the is_smart_switch method correctly returns True + when the chassis.is_smartswitch() method returns True.""" + # Setup mock chassis + mock_chassis = MagicMock() + mock_chassis.is_smartswitch.return_value = True + + # Setup mock platform + mock_platform = MagicMock() + mock_platform.get_chassis.return_value = mock_chassis + mock_platform_class.return_value = mock_platform + + # Create ComponentUpdateProvider instance + cup = fwutil_lib.ComponentUpdateProvider() + + # Test is_smart_switch method + assert cup.is_smart_switch() + mock_chassis.is_smartswitch.assert_called_once() + + @patch('fwutil.lib.Platform') + @patch('fwutil.lib.PlatformComponentsParser') + @patch('fwutil.lib.ComponentUpdateProvider._ComponentUpdateProvider__validate_platform_schema') + @patch('os.mkdir') + def test_smartswitch_modular_chassis_parsing(self, mock_mkdir, mock_validate, + mock_parser_class, mock_platform_class): + """Test that SmartSwitch devices with modules are passed as non-modular (False) + to the PlatformComponentsParser constructor.""" + # Setup mock chassis that is SmartSwitch and has modules + mock_chassis = MagicMock() + mock_chassis.is_smartswitch.return_value = True + mock_chassis.get_all_modules.return_value = [MagicMock(), MagicMock()] # 2 modules + + # Setup mock platform + mock_platform = MagicMock() + mock_platform.get_chassis.return_value = mock_chassis + mock_platform_class.return_value = mock_platform + + # Setup mock parser + mock_parser = MagicMock() + mock_parser_class.return_value = mock_parser + + # Create ComponentUpdateProvider instance + fwutil_lib.ComponentUpdateProvider() + + # Verify that PlatformComponentsParser was called with is_modular_chassis=False + # because SmartSwitch should be treated as non-modular for parsing purposes + mock_parser_class.assert_called_once_with(False) + + @patch('fwutil.lib.Platform') + @patch('fwutil.lib.PlatformComponentsParser') + @patch('fwutil.lib.ComponentUpdateProvider._ComponentUpdateProvider__validate_platform_schema') + @patch('os.mkdir') + def test_regular_modular_chassis_parsing(self, mock_mkdir, mock_validate, mock_parser_class, mock_platform_class): + """Test that regular modular chassis is treated as modular for parsing""" + # Setup mock chassis that is not SmartSwitch but has modules + mock_chassis = MagicMock() + mock_chassis.is_smartswitch.return_value = False + mock_chassis.get_all_modules.return_value = [MagicMock(), MagicMock()] # 2 modules + + # Setup mock platform + mock_platform = MagicMock() + mock_platform.get_chassis.return_value = mock_chassis + mock_platform_class.return_value = mock_platform + + # Setup mock parser + mock_parser = MagicMock() + mock_parser_class.return_value = mock_parser + + # Create ComponentUpdateProvider instance + fwutil_lib.ComponentUpdateProvider() + + # Verify that PlatformComponentsParser was called with is_modular_chassis=True + # because regular modular chassis should be treated as modular + mock_parser_class.assert_called_once_with(True) + + @patch('fwutil.lib.Platform') + @patch('fwutil.lib.PlatformComponentsParser') + @patch('fwutil.lib.ComponentUpdateProvider._ComponentUpdateProvider__validate_platform_schema') + @patch('os.mkdir') + def test_smartswitch_module_validation_skip(self, mock_mkdir, mock_validate, + mock_parser_class, mock_platform_class): + """Test that module validation is skipped for SmartSwitch platforms""" + # Setup mock chassis that is SmartSwitch + mock_chassis = MagicMock() + mock_chassis.is_smartswitch.return_value = True + mock_chassis.get_all_modules.return_value = [MagicMock()] # Has modules + + # Setup mock platform + mock_platform = MagicMock() + mock_platform.get_chassis.return_value = mock_chassis + mock_platform_class.return_value = mock_platform + + # Setup mock parser + mock_parser = MagicMock() + mock_parser_class.return_value = mock_parser + + # Create ComponentUpdateProvider instance + cup = fwutil_lib.ComponentUpdateProvider() + + # Test that module validation is skipped for SmartSwitch + # This should not raise an exception even if there are differences + pdp_map = {'module1': {'comp1': MagicMock()}} + pcp_map = {'module2': {'comp2': MagicMock()}} # Different modules + + # Should not raise exception for SmartSwitch module validation + cup._ComponentUpdateProvider__validate_component_map( + cup.SECTION_MODULE, pdp_map, pcp_map + ) + + @patch('fwutil.lib.Platform') + @patch('fwutil.lib.PlatformComponentsParser') + @patch('fwutil.lib.ComponentUpdateProvider._ComponentUpdateProvider__validate_platform_schema') + @patch('os.mkdir') + def test_regular_chassis_module_validation_error(self, mock_mkdir, mock_validate, + mock_parser_class, mock_platform_class): + """Test that module validation raises error for regular modular chassis""" + # Setup mock chassis that is not SmartSwitch but has modules + mock_chassis = MagicMock() + mock_chassis.is_smartswitch.return_value = False + mock_chassis.get_all_modules.return_value = [MagicMock()] # Has modules + + # Setup mock platform + mock_platform = MagicMock() + mock_platform.get_chassis.return_value = mock_chassis + mock_platform_class.return_value = mock_platform + + # Setup mock parser + mock_parser = MagicMock() + mock_parser_class.return_value = mock_parser + + # Create ComponentUpdateProvider instance + cup = fwutil_lib.ComponentUpdateProvider() + + # Test that module validation raises error for regular modular chassis + pdp_map = {'module1': {'comp1': MagicMock()}} + pcp_map = {'module2': {'comp2': MagicMock()}} # Different modules + + # Should raise exception for regular modular chassis + with pytest.raises(RuntimeError) as excinfo: + cup._ComponentUpdateProvider__validate_component_map( + cup.SECTION_MODULE, pdp_map, pcp_map + ) + assert "Module names mismatch" in str(excinfo.value) + def teardown(self): print('TEARDOWN') From 252a643567dda88ddf93a72508e0331cdd7cb677 Mon Sep 17 00:00:00 2001 From: DavidZagury <32644413+DavidZagury@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:46:04 +0300 Subject: [PATCH 07/21] [SPM] Rename the variable tag to docker-image-reference (#3998) What I did Rename the variable tag to docker_image_reference since it can also hold image digest. How I did it Change the variable name to match the name agreed on sonic-net/sonic-buildimage#22911 How to verify it Install App Extension --- sonic_package_manager/database.py | 11 ++++++----- sonic_package_manager/service_creator/creator.py | 2 +- sonic_package_manager/source.py | 6 +++--- tests/sonic_package_manager/conftest.py | 2 +- tests/sonic_package_manager/test_database.py | 6 +++--- tests/sonic_package_manager/test_manager.py | 12 ++++++------ 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/sonic_package_manager/database.py b/sonic_package_manager/database.py index 11f7b305d6..3bfcf69b9d 100644 --- a/sonic_package_manager/database.py +++ b/sonic_package_manager/database.py @@ -31,7 +31,8 @@ class PackageEntry: built_in: Boolean flag whether the package is built in. image_id: Image ID for this package or None if package is not installed. - tag: Tag for this package or None if package is not installed. + docker_image_reference: Docker image reference for this package or None if package + is not installed. """ name: str @@ -42,7 +43,7 @@ class PackageEntry: installed: bool = False built_in: bool = False image_id: Optional[str] = None - tag: Optional[str] = None + docker_image_reference: Optional[str] = None def package_from_dict(name: str, package_info: Dict) -> PackageEntry: @@ -57,10 +58,10 @@ def package_from_dict(name: str, package_info: Dict) -> PackageEntry: installed = package_info.get('installed', False) built_in = package_info.get('built-in', False) image_id = package_info.get('image-id') - tag = package_info.get('tag') + docker_image_reference = package_info.get('docker-image-reference') return PackageEntry(name, repository, description, default_reference, version, installed, - built_in, image_id, tag) + built_in, image_id, docker_image_reference) def package_to_dict(package: PackageEntry) -> Dict: @@ -74,7 +75,7 @@ def package_to_dict(package: PackageEntry) -> Dict: 'installed': package.installed, 'built-in': package.built_in, 'image-id': package.image_id, - 'tag': package.tag, + 'docker-image-reference': package.docker_image_reference, } diff --git a/sonic_package_manager/service_creator/creator.py b/sonic_package_manager/service_creator/creator.py index 815055c224..857b6365a9 100644 --- a/sonic_package_manager/service_creator/creator.py +++ b/sonic_package_manager/service_creator/creator.py @@ -283,7 +283,7 @@ def generate_container_mgmt(self, package: Package): 'docker_container_name': name, 'docker_image_id': image_id, 'docker_image_name': package.entry.repository, - 'docker_image_tag': package.entry.tag, + 'docker_image_reference': package.entry.docker_image_reference, 'docker_image_run_opt': run_opt, 'sonic_asic_platform': sonic_asic_platform } diff --git a/sonic_package_manager/source.py b/sonic_package_manager/source.py index 37c6cb3928..9ff04abbc5 100644 --- a/sonic_package_manager/source.py +++ b/sonic_package_manager/source.py @@ -51,10 +51,10 @@ def install(self, package: Package): image = self.install_image(package) package.entry.image_id = image.id - if image.tags: - package.entry.tag = image.tags[0] + if image.docker_image_references: + package.entry.docker_image_reference = image.docker_image_references[0] else: - package.entry.tag = image.id + package.entry.docker_image_reference = image.id # if no repository is defined for this package # get repository from image diff --git a/tests/sonic_package_manager/conftest.py b/tests/sonic_package_manager/conftest.py index e8c616d3f0..2d16e43a0d 100644 --- a/tests/sonic_package_manager/conftest.py +++ b/tests/sonic_package_manager/conftest.py @@ -26,7 +26,7 @@ def mock_docker_api(): @dataclass class Image: id: str - tags: list[str] + docker_image_references: list[str] @property def attrs(self): diff --git a/tests/sonic_package_manager/test_database.py b/tests/sonic_package_manager/test_database.py index 2b8380835d..6f87aeee36 100644 --- a/tests/sonic_package_manager/test_database.py +++ b/tests/sonic_package_manager/test_database.py @@ -99,7 +99,7 @@ def test_package_from_dict(): 'installed': True, 'built-in': False, 'image-id': 'abc123', - 'tag': 'latest' + 'docker-image-reference': 'latest' } package = package_from_dict('test-package', package_info) @@ -112,7 +112,7 @@ def test_package_from_dict(): assert package.installed is True assert package.built_in is False assert package.image_id == 'abc123' - assert package.tag == 'latest' + assert package.docker_image_reference == 'latest' def test_package_from_dict_minimal(): @@ -131,4 +131,4 @@ def test_package_from_dict_minimal(): assert package.installed is False assert package.built_in is False assert package.image_id is None - assert package.tag is None + assert package.docker_image_reference is None diff --git a/tests/sonic_package_manager/test_manager.py b/tests/sonic_package_manager/test_manager.py index 5c236dc869..c97afc771a 100644 --- a/tests/sonic_package_manager/test_manager.py +++ b/tests/sonic_package_manager/test_manager.py @@ -602,13 +602,13 @@ def test_download_file_sftp(package_manager): ) -def test_installation_from_file_no_tags(package_manager, mock_docker_api, sonic_fs): - # Override the load function to return an image without tags - def load_no_tags(filename): +def test_installation_from_file_no_image_references(package_manager, mock_docker_api, sonic_fs): + # Override the load function to return an image without image references + def load_no_image_references(filename): class Image: def __init__(self, id): self.id = id - self.tags = [] + self.docker_image_references = [] @property def attrs(self): @@ -616,7 +616,7 @@ def attrs(self): return Image(filename) - mock_docker_api.load = MagicMock(side_effect=load_no_tags) + mock_docker_api.load = MagicMock(side_effect=load_no_image_references) sonic_fs.create_file('Azure/docker-test:1.6.0') package_manager.install(tarball='Azure/docker-test:1.6.0') @@ -626,4 +626,4 @@ def attrs(self): # Get the package from the database and verify the tag was set to the image ID package = package_manager.database.get_package('test-package') - assert package.tag == 'Azure/docker-test:1.6.0' + assert package.docker_image_reference == 'Azure/docker-test:1.6.0' From d86b2b6e4518026f878efa921c3e8ab57f004f53 Mon Sep 17 00:00:00 2001 From: Junchao-Mellanox <57339448+Junchao-Mellanox@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:28:48 +0800 Subject: [PATCH 08/21] g[sfputil debug] Fix issue: do not check output status when CMIS version is lower than 5.0 (#3938) --- sfputil/debug.py | 14 ++++++++++++++ tests/sfputil_test.py | 26 +++++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/sfputil/debug.py b/sfputil/debug.py index 113517e5b4..a23f7b3572 100644 --- a/sfputil/debug.py +++ b/sfputil/debug.py @@ -93,6 +93,20 @@ def set_output(port_name, enable, direction): subport = get_subport(port_name) + if hasattr(api, 'get_cmis_rev'): + cmis_rev = api.get_cmis_rev() + if cmis_rev is None: + click.echo(f"{port_name}: CMIS revision not available for subport {subport}") + sys.exit(EXIT_FAIL) + + # OutputStatusRx and OutputStatusTx are supported from CMIS 5.0 + if float(cmis_rev) < 5.0: + click.echo( + f"{port_name}: This functionality is not supported" + f" with CMIS version {cmis_rev}, requires CMIS 5.0 and above" + ) + sys.exit(EXIT_FAIL) + try: if direction == "tx": lane_count = get_media_lane_count(port_name) diff --git a/tests/sfputil_test.py b/tests/sfputil_test.py index 812005ea4e..781ca0deaa 100644 --- a/tests/sfputil_test.py +++ b/tests/sfputil_test.py @@ -1822,23 +1822,29 @@ def test_debug_loopback(self, mock_sonic_v2_connector, mock_config_db_connector, assert result.exit_code == EXIT_FAIL @pytest.mark.parametrize( - "direction, lane_count, enable, disable_func_result, output_dict, expected_echo, expected_exit", + "direction, lane_count, enable, disable_func_result, cmis_version, output_dict, expected_echo, expected_exit", [ # TX disable success ( - "tx", 2, "disable", True, {"TxOutputStatus1": False, "TxOutputStatus2": False}, + "tx", 2, "disable", True, "5.3", {"TxOutputStatus1": False, "TxOutputStatus2": False}, "TX output disabled", None ), # RX enable success - ("rx", 1, "enable", True, {"RxOutputStatus1": True}, "RX output enabled", None), + ("rx", 1, "enable", True, "5.0", {"RxOutputStatus1": True}, "RX output enabled", None), # TX disable fails to disable - ("tx", 1, "disable", True, {"TxOutputStatus1": True}, "TX output on lane 1 is still enabled", SystemExit), + ( + "tx", 1, "disable", True, "5.0", {"TxOutputStatus1": True}, + "TX output on lane 1 is still enabled", SystemExit + ), # RX enable fails to enable - ("rx", 1, "enable", True, {"RxOutputStatus1": False}, "RX output on lane 1 is still disabled", SystemExit), - # TX disable_func returns False - ("tx", 1, "disable", False, {}, "TX disable failed", SystemExit), - # RX output_dict is None - ("rx", 1, "disable", True, None, "RX output status not available", SystemExit), + ( + "rx", 1, "enable", True, "5.0", {"RxOutputStatus1": False}, + "RX output on lane 1 is still disabled", SystemExit + ), + # CMIS version is None + ("tx", 1, "disable", False, None, {}, "CMIS revision not available", SystemExit), + # CMIS version is below 5.0 + ("rx", 1, "disable", True, "4.0", None, "This functionality is not supported", SystemExit), ] ) @patch("sfputil.debug.get_sfp_object") @@ -1857,6 +1863,7 @@ def test_set_output_cli( lane_count, enable, disable_func_result, + cmis_version, output_dict, expected_echo, expected_exit @@ -1875,6 +1882,7 @@ def test_set_output_cli( # Mock SFP and API mock_sfp = MagicMock() mock_api = MagicMock() + mock_api.get_cmis_rev.return_value = cmis_version if direction == "tx": mock_sfp.tx_disable_channel.return_value = disable_func_result mock_api.get_tx_output_status.return_value = output_dict From 6f1a794b30fcfe9ab49fc3c89d889e9a1e4e4c4b Mon Sep 17 00:00:00 2001 From: Vivek Verma <137406113+vivekverma-arista@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:45:05 +0530 Subject: [PATCH 09/21] Add queuestat changes for aggregate VOQ counters (#3617) * Add queuestat changes for aggregate VOQ counters --- scripts/queuestat | 153 +++++++++++++++--- tests/chassis_modules_test.py | 13 +- tests/mock_tables/asic0/counters_db.json | 18 +++ tests/mock_tables/asic1/counters_db.json | 18 +++ tests/mock_tables/asic2/counters_db.json | 18 +++ tests/mock_tables/chassis_state_db.json | 35 +++- tests/mock_tables/dbconnector.py | 38 +++++ tests/mock_tables/state_db.json | 10 +- tests/multi_asic_queue_counter_test.py | 1 + .../on_sup_na/chassis_state_db.json | 22 ++- .../chassis_state_db.json | 3 + tests/portstat_test.py | 4 + tests/queue_counter_test.py | 1 + tests/remote_cli_test.py | 2 + tests/test_aggregate_voq_counters.py | 98 +++++++++++ 15 files changed, 400 insertions(+), 34 deletions(-) create mode 100644 tests/test_aggregate_voq_counters.py diff --git a/scripts/queuestat b/scripts/queuestat index d18d8ee91d..3a74e5cc57 100755 --- a/scripts/queuestat +++ b/scripts/queuestat @@ -15,7 +15,9 @@ import sys from collections import namedtuple, OrderedDict from natsort import natsorted from tabulate import tabulate -from sonic_py_common import multi_asic +from sonic_py_common import multi_asic, device_info +from redis import Redis, exceptions +from swsscommon import swsscommon # mock the redis for unit test purposes # try: @@ -25,6 +27,11 @@ try: sys.path.insert(0, modules_path) sys.path.insert(0, tests_path) import mock_tables.dbconnector # lgtm [py/unused-import] + + if os.environ["UTILITIES_UNIT_TESTING_IS_SUP"] == "1": + import mock + device_info.is_supervisor = mock.MagicMock(return_value=True) + if os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] == "multi_asic": import mock_tables.mock_multi_asic mock_tables.dbconnector.load_namespace_config() @@ -82,6 +89,39 @@ cnstat_dir = 'N/A' cnstat_fqn_file = 'N/A' +def get_redis_ips(db): + db.connect(db.STATE_DB) + redis_ips = [] + chassis_midplane_table = db.keys(db.STATE_DB, "CHASSIS_MIDPLANE_TABLE*") + lc_metadata = [] + for lc in chassis_midplane_table: + lc_metadata.append(db.get_all(db.STATE_DB, lc)) + + db.connect(db.CHASSIS_STATE_DB) + for lc in lc_metadata: + # skip if LC is offline + if lc['access'] == "False": + continue + + slot_id = int(lc['ip_address'].split(".")[2]) - 1 + num_asics = db.get(db.CHASSIS_STATE_DB, f"CHASSIS_MODULE_TABLE|LINE-CARD{slot_id}", 'num_asics') + + # Skip if pmon hasn't started on LC yet + if num_asics == None: + continue + + # No namespace in single ASIC LC + if num_asics == "1": + redis_ips.append(lc['ip_address']) + else: + prefix, _ = lc['ip_address'].rsplit(".", maxsplit=1) + for i in range(int(num_asics)): + prefix, _, _ = lc['ip_address'].rpartition(".") + redis_ips.append(f"{prefix}.{10+i}") + + return redis_ips + + def build_json(port, cnstat, all=False, trim=False, voq=False): def ports_stats(k): p = {} @@ -120,6 +160,18 @@ def build_json(port, cnstat, all=False, trim=False, voq=False): out.update(ports_stats(k)) return out +def run_queuestat(save_fresh_stats, port_to_show_stats, json_opt, non_zero, ns, db, voq, trim, all_): + queuestat = Queuestat(ns, db, all_, trim, voq) + if save_fresh_stats: + queuestat.save_fresh_stats() + return + + if port_to_show_stats != None: + queuestat.get_print_port_stat(port_to_show_stats, json_opt, non_zero) + else: + queuestat.get_print_all_stat(json_opt, non_zero) + + class QueuestatWrapper(object): """A wrapper to execute queuestat cmd over the correct namespaces""" def __init__(self, namespace, all, trim, voq): @@ -134,16 +186,8 @@ class QueuestatWrapper(object): @multi_asic_util.run_on_multi_asic def run(self, save_fresh_stats, port_to_show_stats, json_opt, non_zero): - queuestat = Queuestat(self.multi_asic.current_namespace, self.db, self.all, self.trim, self.voq) - if save_fresh_stats: - queuestat.save_fresh_stats() - return - - if port_to_show_stats != None: - queuestat.get_print_port_stat(port_to_show_stats, json_opt, non_zero) - else: - queuestat.get_print_all_stat(json_opt, non_zero) - + run_queuestat(save_fresh_stats, port_to_show_stats, json_opt, non_zero, \ + self.multi_asic.current_namespace, self.db, self.voq, self.trim, self.all) class Queuestat(object): def __init__(self, namespace, db, all=False, trim=False, voq=False): @@ -151,7 +195,11 @@ class Queuestat(object): self.all = all self.trim = trim self.voq = voq + self.voq_stats = {} self.namespace = namespace + if namespace is None: + self.db = SonicV2Connector(use_unix_socket_path=False) + self.db.connect(self.db.COUNTERS_DB) self.namespace_str = f" for {namespace}" if namespace else '' def get_queue_port(table_id): @@ -164,7 +212,9 @@ class Queuestat(object): # Get all ports if voq: - self.counter_port_name_map = self.db.get_all(self.db.COUNTERS_DB, COUNTERS_SYSTEM_PORT_NAME_MAP) + # counter_port_name_map is assigned later for supervisor as a list + self.counter_port_name_map = [] if device_info.is_supervisor() else \ + self.db.get_all(self.db.COUNTERS_DB, COUNTERS_SYSTEM_PORT_NAME_MAP) else: self.counter_port_name_map = self.db.get_all(self.db.COUNTERS_DB, COUNTERS_PORT_NAME_MAP) @@ -179,6 +229,16 @@ class Queuestat(object): self.port_queues_map[port] = {} self.port_name_map[self.counter_port_name_map[port]] = port + if self.voq: + counter_bucket_dict.update(voq_counter_bucket_dict) + else: + counter_bucket_dict.update(trim_counter_bucket_dict) + + if device_info.is_supervisor(): + self.aggregate_voq_stats() + self.counter_port_name_map = self.voq_stats.keys() + return + counter_queue_name_map = None # Get Queues for each port if voq: @@ -194,6 +254,44 @@ class Queuestat(object): port = self.port_name_map[get_queue_port(counter_queue_name_map[queue])] self.port_queues_map[port][queue] = counter_queue_name_map[queue] + def aggregate_voq_stats(self): + redis_ips = get_redis_ips(self.db) + self.voq_stats = {} + + for ip in redis_ips: + asic_counters_db = swsscommon.DBConnector(swsscommon.COUNTERS_DB, ip, 6379, 0) + try: + counters_voq_name_map = asic_counters_db.hgetall(COUNTERS_VOQ_NAME_MAP) + if counters_voq_name_map is None: + continue + for voq in counters_voq_name_map: + # key LINECARD|ASIC|EthernetXXX:INDEX + sysPort, idx = voq.split(":") + for counter_name in counter_bucket_dict: + self.voq_stats.setdefault(sysPort, {}).setdefault(idx, {}).setdefault(counter_name, 0) + oid = counters_voq_name_map[voq] + counter_data = asic_counters_db.hget("COUNTERS:"+oid, counter_name) + if counter_data is not None: + self.voq_stats[sysPort][idx][counter_name] += int(counter_data) + + except exceptions.ConnectionError as e: + # Skip further operations for this redis-instance + continue + + def get_aggregate_port_stats(self, port): + # Build a dictionary of stats + cnstat_dict = OrderedDict() + cnstat_dict['time'] = datetime.datetime.now() + for idx in sorted(self.voq_stats[port].keys()): + fields = ["0"]*len(voq_header) + fields[0] = idx + fields[1] = QUEUE_TYPE_VOQ + for counter_name, pos in counter_bucket_dict.items(): + fields[pos] = str(self.voq_stats[port][idx][counter_name]) + cntr = VoqStats._make(fields)._asdict() + cnstat_dict[port+":"+idx] = cntr + return cnstat_dict + def get_cnstat(self, queue_map): """ Get the counters info from database. @@ -230,11 +328,6 @@ class Queuestat(object): counter_dict = { **counter_bucket_dict } fields = [ get_queue_index(table_id), get_queue_type(table_id) ] - if self.voq: - counter_dict.update(voq_counter_bucket_dict) - else: - counter_dict.update(trim_counter_bucket_dict) - # Layout is per QueueStats/VoqStats type definition fields.extend(["0"]*len(counter_dict)) @@ -325,7 +418,8 @@ class Queuestat(object): hdr = std_header if table: - print(f"For namespace {self.namespace}:") + if not device_info.is_supervisor(): + print(f"For namespace {self.namespace}:") print(tabulate(table, hdr, tablefmt='simple', stralign='right')) print() @@ -428,7 +522,11 @@ class Queuestat(object): json_output = {} for port in natsorted(self.counter_port_name_map): json_output[port] = {} - cnstat_dict = self.get_cnstat(self.port_queues_map[port]) + if self.voq and device_info.is_supervisor(): + cnstat_dict = self.get_aggregate_port_stats(port) + else: + cnstat_dict = self.get_cnstat(self.port_queues_map[port]) + cache_ns = '' if self.voq and self.namespace is not None: cache_ns = '-' + self.namespace + '-' @@ -457,12 +555,16 @@ class Queuestat(object): Get stat for the port If JSON option is True print data in JSON format """ - if not port in self.port_queues_map: + if port not in self.port_queues_map and port not in self.voq_stats: print("Port doesn't exist!", port) sys.exit(1) # Get stat for the port queried - cnstat_dict = self.get_cnstat(self.port_queues_map[port]) + + if self.voq and device_info.is_supervisor(): + cnstat_dict = self.get_aggregate_port_stats(port) + else: + cnstat_dict = self.get_cnstat(self.port_queues_map[port]) cache_ns = '' if self.voq and self.namespace is not None: cache_ns = '-' + self.namespace + '-' @@ -542,8 +644,13 @@ def main(port, clear, delete, json_opt, all, trim, voq, non_zero, namespace): if delete_stats: cache.remove() - queuestat_wrapper = QueuestatWrapper(namespace, all, trim, voq) - queuestat_wrapper.run(save_fresh_stats, port_to_show_stats, json_opt, non_zero) + + if device_info.is_supervisor() and namespace is None: + run_queuestat(save_fresh_stats, port_to_show_stats, json_opt, non_zero, namespace, None, voq, trim, all) + else: + queuestat_wrapper = QueuestatWrapper(namespace, all, trim, voq) + queuestat_wrapper.run(save_fresh_stats, port_to_show_stats, json_opt, non_zero) + sys.exit(0) diff --git a/tests/chassis_modules_test.py b/tests/chassis_modules_test.py index c1fd653ecc..305d26b380 100755 --- a/tests/chassis_modules_test.py +++ b/tests/chassis_modules_test.py @@ -46,11 +46,12 @@ """ show_chassis_midplane_output="""\ - Name IP-Address Reachability ----------- ------------- -------------- -LINE-CARD0 192.168.1.100 True -LINE-CARD1 192.168.1.2 False -LINE-CARD2 192.168.1.1 True + Name IP-Address Reachability +---------- ------------ -------------- +LINE-CARD0 192.168.3.1 True +LINE-CARD1 192.168.4.1 False +LINE-CARD2 192.168.5.1 True +LINE-CARD3 192.168.6.1 True """ show_chassis_system_ports_output_asic0="""\ @@ -346,7 +347,7 @@ def test_midplane_show_all_count_lines(self): result = runner.invoke(show.cli.commands["chassis"].commands["modules"].commands["midplane-status"], []) print(result.output) result_lines = result.output.strip('\n').split('\n') - modules = ["LINE-CARD0", "LINE-CARD1", "LINE-CARD2"] + modules = ["LINE-CARD0", "LINE-CARD1", "LINE-CARD2", "LINE-CARD3"] for i, module in enumerate(modules): assert module in result_lines[i + warning_lines + header_lines] assert len(result_lines) == warning_lines + header_lines + len(modules) diff --git a/tests/mock_tables/asic0/counters_db.json b/tests/mock_tables/asic0/counters_db.json index c61a00e192..7de2015c41 100644 --- a/tests/mock_tables/asic0/counters_db.json +++ b/tests/mock_tables/asic0/counters_db.json @@ -2788,5 +2788,23 @@ "PERSISTENT_WATERMARKS:oid:100000000b0ff": { "SAI_INGRESS_PRIORITY_GROUP_STAT_SHARED_WATERMARK_BYTES": "507", "SAI_INGRESS_PRIORITY_GROUP_STAT_XOFF_ROOM_WATERMARK_BYTES": "507" + }, + "COUNTERS_VOQ_NAME_MAP": { + "sonic-lc1|asic0|Ethernet0:0": "oid:0x150000000005c2", + "sonic-lc2|asic0|Ethernet4:1": "oid:0x1500000000047a" + }, + "COUNTERS:oid:0x150000000005c2": { + "SAI_QUEUE_STAT_DROPPED_BYTES": "122", + "SAI_QUEUE_STAT_DROPPED_PACKETS": "1", + "SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS": "0", + "SAI_QUEUE_STAT_BYTES": "122", + "SAI_QUEUE_STAT_PACKETS": "1" + }, + "COUNTERS:oid:0x1500000000047a": { + "SAI_QUEUE_STAT_DROPPED_BYTES": "244", + "SAI_QUEUE_STAT_DROPPED_PACKETS": "2", + "SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS": "0", + "SAI_QUEUE_STAT_BYTES": "366", + "SAI_QUEUE_STAT_PACKETS": "3" } } diff --git a/tests/mock_tables/asic1/counters_db.json b/tests/mock_tables/asic1/counters_db.json index 1455f069c0..254221020b 100644 --- a/tests/mock_tables/asic1/counters_db.json +++ b/tests/mock_tables/asic1/counters_db.json @@ -1675,5 +1675,23 @@ }, "USER_WATERMARKS:oid:0x100000000b115": { "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "2" + }, + "COUNTERS_VOQ_NAME_MAP": { + "sonic-lc1|asic0|Ethernet0:0": "oid:0x1500000000059d", + "sonic-lc2|asic0|Ethernet4:1": "oid:0x15000000000571" + }, + "COUNTERS:oid:0x1500000000059d": { + "SAI_QUEUE_STAT_DROPPED_BYTES": "1098", + "SAI_QUEUE_STAT_DROPPED_PACKETS": "9", + "SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS": "0", + "SAI_QUEUE_STAT_BYTES": "854", + "SAI_QUEUE_STAT_PACKETS": "7" + }, + "COUNTERS:oid:0x15000000000571": { + "SAI_QUEUE_STAT_DROPPED_BYTES": "732", + "SAI_QUEUE_STAT_DROPPED_PACKETS": "6", + "SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS": "0", + "SAI_QUEUE_STAT_BYTES": "610", + "SAI_QUEUE_STAT_PACKETS": "5" } } diff --git a/tests/mock_tables/asic2/counters_db.json b/tests/mock_tables/asic2/counters_db.json index 66875f8245..5498c4a10c 100644 --- a/tests/mock_tables/asic2/counters_db.json +++ b/tests/mock_tables/asic2/counters_db.json @@ -1699,5 +1699,23 @@ }, "COUNTERS_DEBUG_NAME_SWITCH_STAT_MAP": { "DEBUG_1": "SAI_SWITCH_STAT_IN_DROP_REASON_RANGE_BASE" + }, + "COUNTERS_VOQ_NAME_MAP": { + "sonic-lc1|asic0|Ethernet0:0": "oid:0x15000000000532", + "sonic-lc2|asic0|Ethernet4:1": "oid:0x15000000000521" + }, + "COUNTERS:oid:0x15000000000532": { + "SAI_QUEUE_STAT_DROPPED_BYTES": "0", + "SAI_QUEUE_STAT_DROPPED_PACKETS": "0", + "SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS": "0", + "SAI_QUEUE_STAT_BYTES": "0", + "SAI_QUEUE_STAT_PACKETS": "0" + }, + "COUNTERS:oid:0x15000000000521": { + "SAI_QUEUE_STAT_DROPPED_BYTES": "976", + "SAI_QUEUE_STAT_DROPPED_PACKETS": "8", + "SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS": "0", + "SAI_QUEUE_STAT_BYTES": "1220", + "SAI_QUEUE_STAT_PACKETS": "10" } } diff --git a/tests/mock_tables/chassis_state_db.json b/tests/mock_tables/chassis_state_db.json index 365cbf80cd..09fd357ad5 100644 --- a/tests/mock_tables/chassis_state_db.json +++ b/tests/mock_tables/chassis_state_db.json @@ -8,12 +8,28 @@ "CHASSIS_MODULE_HOSTNAME_TABLE|LINE-CARD2": { "module_hostname": "sonic-lc3" }, + "CHASSIS_MODULE_HOSTNAME_TABLE|LINE-CARD3": { + "module_hostname": "sonic-lc4" + }, + "CHASSIS_MODULE_TABLE|LINE-CARD2": { + "slot": "3", + "hostname": "sonic-lc1", + "num_asics": "2" + }, + "CHASSIS_MODULE_TABLE|LINE-CARD4": { + "slot": "5", + "hostname": "sonic-lc2", + "num_asics": "1" + }, "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc1": { "timestamp": "2020-07-01 00:00:00" }, "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc3": { "timestamp": "2020-07-01 00:00:00" }, + "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc4": { + "timestamp": "2020-07-01 00:00:00" + }, "LINECARD_PORT_STAT_TABLE|Ethernet1/1": { "state": "U", "rx_ok": 100, @@ -64,5 +80,22 @@ "tx_err": 0, "tx_drop": 0, "tx_ovr": 0 + }, + "LINECARD_PORT_STAT_TABLE|Ethernet12/1": { + "state": "U", + "rx_ok": 100, + "rx_bps": 10, + "rx_pps": 1, + "rx_util": 0, + "rx_err": 0, + "rx_drop": 0, + "rx_ovr": 0, + "tx_ok": 100, + "tx_bps": 10, + "tx_pps": 1, + "tx_util": 0, + "tx_err": 0, + "tx_drop": 0, + "tx_ovr": 0 } -} \ No newline at end of file +} diff --git a/tests/mock_tables/dbconnector.py b/tests/mock_tables/dbconnector.py index 379c4e75cd..23e9ee1737 100644 --- a/tests/mock_tables/dbconnector.py +++ b/tests/mock_tables/dbconnector.py @@ -3,6 +3,7 @@ import os import sys import re +import ipaddress from unittest import mock import mockredis @@ -228,6 +229,43 @@ def get(self, counter, name): return True, tuple(self.db.get("COUNTERS:" + key).items()) +class DBConnector: + + def __init__(self, *args): + + self.data = None + ip_to_asic = { + "192.168.3.10": "asic0", + "192.168.3.11": "asic1", + "192.168.5.1": "asic2", + "192.168.6.1": "asic3" + } + + redis_kwargs = {} + + # Check if IP is being used to connect to redis + if len(args) == 4: + try: + ip = args[1] + ipaddress.ip_address(args[1]) + redis_kwargs['namespace'] = ip_to_asic[ip] if ip is not None else ip + except ValueError: + redis_kwargs['namespace'] = None + + redis_kwargs['db_name'] = 'counters_db' + redis_kwargs['topo'] = None + redis_kwargs['unix_socket_path'] = None + redis_kwargs['decode_responses'] = True + self.swsssyncclient = SwssSyncClient(**redis_kwargs) + + def hgetall(self, key): + return self.swsssyncclient.hgetall(key) + + def hget(self, key, attr): + return self.swsssyncclient.hget(key, attr) + + +swsscommon.DBConnector = DBConnector swsssdk.interface.DBInterface._subscribe_keyspace_notification = _subscribe_keyspace_notification swsssdk.interface.DBInterface.close = mock_close mockredis.MockRedis.config_set = config_set diff --git a/tests/mock_tables/state_db.json b/tests/mock_tables/state_db.json index 69f3056a1f..35d7f1a682 100644 --- a/tests/mock_tables/state_db.json +++ b/tests/mock_tables/state_db.json @@ -1393,17 +1393,21 @@ "max_priority_groups": "8" }, "CHASSIS_MIDPLANE_TABLE|LINE-CARD0": { - "ip_address": "192.168.1.100", + "ip_address": "192.168.3.1", "access": "True" }, "CHASSIS_MIDPLANE_TABLE|LINE-CARD2": { - "ip_address": "192.168.1.1", + "ip_address": "192.168.5.1", "access": "True" }, "CHASSIS_MIDPLANE_TABLE|LINE-CARD1": { - "ip_address": "192.168.1.2", + "ip_address": "192.168.4.1", "access": "False" }, + "CHASSIS_MIDPLANE_TABLE|LINE-CARD3": { + "ip_address": "192.168.6.1", + "access": "True" + }, "PORT_TABLE|Ethernet0": { "rmt_adv_speeds" : "10,100,1000", "speed" : "100000", diff --git a/tests/multi_asic_queue_counter_test.py b/tests/multi_asic_queue_counter_test.py index af57fa75e5..2058697ed8 100644 --- a/tests/multi_asic_queue_counter_test.py +++ b/tests/multi_asic_queue_counter_test.py @@ -255,6 +255,7 @@ class TestQueueMultiAsic(object): def setup_class(cls): os.environ["PATH"] += os.pathsep + scripts_path os.environ['UTILITIES_UNIT_TESTING'] = "2" + os.environ['UTILITIES_UNIT_TESTING_IS_SUP'] = "0" os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "multi_asic" print("SETUP") diff --git a/tests/portstat_db/on_sup_na/chassis_state_db.json b/tests/portstat_db/on_sup_na/chassis_state_db.json index d2e5771098..6f66efac69 100644 --- a/tests/portstat_db/on_sup_na/chassis_state_db.json +++ b/tests/portstat_db/on_sup_na/chassis_state_db.json @@ -14,6 +14,9 @@ "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc3": { "timestamp": "2020-07-01 00:00:00" }, + "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc4": { + "timestamp": "2020-07-01 00:00:00" + }, "LINECARD_PORT_STAT_TABLE|Ethernet1/1": { "state": "U", "rx_ok": 100, @@ -64,5 +67,22 @@ "tx_err": "N/A", "tx_drop": "N/A", "tx_ovr": "N/A" + }, + "LINECARD_PORT_STAT_TABLE|Ethernet12/1": { + "state": "N/A", + "rx_ok": "N/A", + "rx_bps": "N/A", + "rx_pps": "N/A", + "rx_util": "N/A", + "rx_err": "N/A", + "rx_drop": "N/A", + "rx_ovr": "N/A", + "tx_ok": "N/A", + "tx_bps": "N/A", + "tx_pps": "N/A", + "tx_util": "N/A", + "tx_err": "N/A", + "tx_drop": "N/A", + "tx_ovr": "N/A" } -} \ No newline at end of file +} diff --git a/tests/portstat_db/on_sup_packet_chassis/chassis_state_db.json b/tests/portstat_db/on_sup_packet_chassis/chassis_state_db.json index 34353b6f52..c3371f26e3 100644 --- a/tests/portstat_db/on_sup_packet_chassis/chassis_state_db.json +++ b/tests/portstat_db/on_sup_packet_chassis/chassis_state_db.json @@ -14,6 +14,9 @@ "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc3": { "timestamp": "2020-07-01 00:00:00" }, + "LINECARD_PORT_STAT_MARK_TABLE|sonic-lc4": { + "timestamp": "2020-07-01 00:00:00" + }, "LINECARD_PORT_STAT_TABLE|HundredGigE0/1/0/1": { "state": "U", "rx_ok": 100, diff --git a/tests/portstat_test.py b/tests/portstat_test.py index e5f32f5823..070441b127 100644 --- a/tests/portstat_test.py +++ b/tests/portstat_test.py @@ -309,6 +309,8 @@ 0 0 0 Ethernet11/1 U 100 10.00 B/s 0.00% 0 0 0 100 10.00 B/s 0.00%\ 0 0 0 +Ethernet12/1 U 100 10.00 B/s 0.00% 0 0 0 100 10.00 B/s 0.00%\ + 0 0 0 """ intf_counters_on_sup_no_counters = "Linecard Counter Table is not available.\n" @@ -326,6 +328,8 @@ 0 0 0 Ethernet11/1 N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A\ N/A N/A N/A +Ethernet12/1 N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A\ + N/A N/A N/A """ intf_counters_on_sup_packet_chassis = """\ diff --git a/tests/queue_counter_test.py b/tests/queue_counter_test.py index 508550b9c8..5f8d54954b 100644 --- a/tests/queue_counter_test.py +++ b/tests/queue_counter_test.py @@ -2245,6 +2245,7 @@ class TestQueue(object): def setup_class(cls): os.environ["PATH"] += os.pathsep + scripts_path os.environ['UTILITIES_UNIT_TESTING'] = "2" + os.environ['UTILITIES_UNIT_TESTING_IS_SUP'] = "0" print("SETUP") def test_queue_counters(self): diff --git a/tests/remote_cli_test.py b/tests/remote_cli_test.py index 57a220be1e..519a39debf 100644 --- a/tests/remote_cli_test.py +++ b/tests/remote_cli_test.py @@ -17,6 +17,8 @@ hello world ======== LINE-CARD2|sonic-lc3 output: ======== hello world +======== LINE-CARD3|sonic-lc4 output: ======== +hello world ''' REXEC_HELP = '''Usage: cli [OPTIONS] LINECARD_NAMES... diff --git a/tests/test_aggregate_voq_counters.py b/tests/test_aggregate_voq_counters.py new file mode 100644 index 0000000000..4e5557e571 --- /dev/null +++ b/tests/test_aggregate_voq_counters.py @@ -0,0 +1,98 @@ +from click.testing import CliRunner +import show.main as show +import os +import json + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, "scripts") + +show_queue_counters_voq = """\ + Port Voq Counter/pkts Counter/bytes Drop/pkts Drop/bytes Credit-WD-Del/pkts +------------------------- ----- -------------- --------------- ----------- ------------ -------------------- +sonic-lc1|asic0|Ethernet0 VOQ0 8 976 10 1220 0 + + Port Voq Counter/pkts Counter/bytes Drop/pkts Drop/bytes Credit-WD-Del/pkts +------------------------- ----- -------------- --------------- ----------- ------------ -------------------- +sonic-lc2|asic0|Ethernet4 VOQ1 18 2196 16 1952 0 + +""" + +show_queue_counters_voq_sys_port = """\ + Port Voq Counter/pkts Counter/bytes Drop/pkts Drop/bytes Credit-WD-Del/pkts +------------------------- ----- -------------- --------------- ----------- ------------ -------------------- +sonic-lc2|asic0|Ethernet4 VOQ1 18 2196 16 1952 0 + +""" + +show_queue_counters_voq_json = { + "sonic-lc1|asic0|Ethernet0": { + "VOQ0": { + "creditWDPkts": "0", + "dropbytes": "1220", + "droppacket": "10", + "totalbytes": "976", + "totalpacket": "8" + }, + "time": "2025-04-07T15:57:11.881430" + }, + "sonic-lc2|asic0|Ethernet4": { + "VOQ1": { + "creditWDPkts": "0", + "dropbytes": "1952", + "droppacket": "16", + "totalbytes": "2196", + "totalpacket": "18" + }, + "time": "2025-04-07T15:57:11.881496" + } +} + + +class TestAggVoq(object): + @classmethod + def setup_class(cls): + os.environ["PATH"] += os.pathsep + scripts_path + os.environ["UTILITIES_UNIT_TESTING_IS_SUP"] = "1" + os.environ["UTILITIES_UNIT_TESTING"] = "2" + print("SETUP") + + def test_queue_voq_counters_aggregate(self): + runner = CliRunner() + result = runner.invoke( + show.cli.commands["queue"].commands["counters"], + ["--voq"] + ) + assert result.exit_code == 0 + assert result.output == show_queue_counters_voq + + def test_queue_voq_counters_aggregate_json(self): + runner = CliRunner() + result = runner.invoke( + show.cli.commands["queue"].commands["counters"], + ["--voq", "--json"] + ) + res = result.output + res = json.loads(res) + assert result.exit_code == 0 + for lc in show_queue_counters_voq_json: + for voq in show_queue_counters_voq_json[lc]: + if voq != "time": + for counter in show_queue_counters_voq_json[lc][voq]: + assert res[lc][voq][counter] == show_queue_counters_voq_json[lc][voq][counter] + + def test_queue_voq_counters_aggregate_sys_port(self): + runner = CliRunner() + result = runner.invoke( + show.cli.commands["queue"].commands["counters"], + ["sonic-lc2|asic0|Ethernet4", "--voq"] + ) + assert result.exit_code == 0 + assert result.output == show_queue_counters_voq_sys_port + + @classmethod + def teardown_class(cls): + os.environ["PATH"] = os.pathsep.join(os.environ["PATH"].split(os.pathsep)[:-1]) + os.environ["UTILITIES_UNIT_TESTING_IS_SUP"] = "0" + os.environ["UTILITIES_UNIT_TESTING"] = "0" + print("TEARDOWN") From 3282ab35bd051714c781c64965ef9c01a6e7d5b0 Mon Sep 17 00:00:00 2001 From: Ariz Zubair <5427064+az-pz@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:55:27 -0400 Subject: [PATCH 10/21] DOM for flat memory transceiver modules (#3950) * Fix: show interface transceiver info Ethernet0 command fails for CMIS SFP * Fix variable name error. * Return DOM for flat memory modules if it's available. * Update unit tests. * Update unit tests. * Update unit tests. * Update unit tests. --- sfputil/main.py | 4 ---- tests/sfputil_test.py | 9 ++++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sfputil/main.py b/sfputil/main.py index 90f1e3607a..e5848e750a 100644 --- a/sfputil/main.py +++ b/sfputil/main.py @@ -695,10 +695,6 @@ def eeprom(port, dump_dom, namespace): output += "API is none while getting DOM info!\n" click.echo(output) sys.exit(ERROR_NOT_IMPLEMENTED) - else: - if api.is_flat_memory(): - output += "DOM values not supported for flat memory module\n" - continue try: xcvr_dom_info = platform_chassis.get_sfp(physical_port).get_transceiver_dom_real_value() except NotImplementedError: diff --git a/tests/sfputil_test.py b/tests/sfputil_test.py index 781ca0deaa..9fa9a394bb 100644 --- a/tests/sfputil_test.py +++ b/tests/sfputil_test.py @@ -60,6 +60,13 @@ Vendor Rev: A2 Vendor SN: MT1636VS10561 """ +EMPTY_DOM_VALUES = """ ChannelMonitorValues: + ChannelThresholdValues: + ModuleMonitorValues: + ModuleThresholdValues: + + +""" class TestSfputil(object): def test_format_dict_value_to_string(self): @@ -676,7 +683,7 @@ def test_show_eeprom_RJ45(self, mock_chassis): @patch('sfputil.main.platform_sfputil', MagicMock(is_logical_port=MagicMock(return_value=1))) @patch('sfputil.main.is_port_type_rj45', MagicMock(return_value=False)) @pytest.mark.parametrize("exception, xcvr_api_none, expected_output", [ - (None, False, '''DOM values not supported for flat memory module\n\n'''), + (None, False, EMPTY_DOM_VALUES), (NotImplementedError, False, '''API is currently not implemented for this platform\n\n'''), (None, True, '''API is none while getting DOM info!\n\n''') ]) From d3bc688c7006c8db3d2463e753e3f2b77c487216 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Mon, 4 Aug 2025 10:13:36 -0700 Subject: [PATCH 11/21] CLI addition for PFC counters --history (#3778) * Add history option to pfcstat cli * Unit tests for pfcstat history cli Please note the usage of `# flake8: noqa: E501` in assert_show_output.py to account for the length of show command output strings. * Add pfc counters history flag to utilities documentation --- doc/Command-Reference.md | 32 ++- scripts/pfcstat | 266 ++++++++++++++++++---- show/main.py | 5 +- tests/mock_tables/asic0/counters_db.json | 128 +++++++++++ tests/mock_tables/counters_db.json | 96 ++++++++ tests/pfcstat_input/assert_show_output.py | 247 ++++++++++++++++++++ tests/pfcstat_test.py | 82 ++++++- utilities_common/netstat.py | 14 ++ 8 files changed, 827 insertions(+), 43 deletions(-) create mode 100644 tests/pfcstat_input/assert_show_output.py diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 8dff4f0641..fc8ebe8e14 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -9702,8 +9702,38 @@ This command displays the details of Rx & Tx priority-flow-control (pfc) for all ... ``` +The history flag can be used to view historical statistics: -- NOTE: PFC counters can be cleared by the user with the following command: +* Usage: see [PFC Watchdog Commands](#pfc-watchdog-commands) on enabling history estimation + ``` + show pfc counters --history + ``` + +* Example: + ``` + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp + --------- ---------- ---------------------- ------------------------ ------------------------- --------------------------- + Ethernet0 PFC0 12 12,000 1,200 01/10/2008, 21:20:00 + Ethernet0 PFC1 21 20,001 2,001 05/18/2033, 03:33:20 + Ethernet0 PFC2 22 20,002 2,002 05/18/2033, 03:33:20 + Ethernet0 PFC3 23 20,003 2,003 05/18/2033, 03:33:20 + Ethernet0 PFC4 24 20,004 2,004 05/18/2033, 03:33:20 + Ethernet0 PFC5 25 20,005 2,005 05/18/2033, 03:33:20 + Ethernet0 PFC6 26 20,006 2,006 05/18/2033, 03:33:20 + Ethernet0 PFC7 27 20,007 2,007 05/18/2033, 03:33:20 + + Ethernet4 PFC0 14 14,000 1,400 05/13/2014, 16:53:20 + Ethernet4 PFC1 41 40,001 4,001 10/02/2096, 07:06:40 + Ethernet4 PFC2 42 40,002 4,002 10/02/2096, 07:06:40 + Ethernet4 PFC3 43 40,003 4,003 10/02/2096, 07:06:40 + Ethernet4 PFC4 44 40,004 4,004 10/02/2096, 07:06:40 + Ethernet4 PFC5 45 40,005 4,005 10/02/2096, 07:06:40 + Ethernet4 PFC6 46 40,006 4,006 10/02/2096, 07:06:40 + Ethernet4 PFC7 47 40,007 4,007 10/02/2096, 07:06:40 + ``` + + +- NOTE: PFC counters (including historical stats) can be cleared by the user with the following command: ``` admin@sonic:~$ sonic-clear pfccounters ``` diff --git a/scripts/pfcstat b/scripts/pfcstat index 094c6e9380..89d3dd33b9 100755 --- a/scripts/pfcstat +++ b/scripts/pfcstat @@ -34,16 +34,18 @@ try: except KeyError: pass -from utilities_common.netstat import ns_diff, STATUS_NA, format_number_with_comma +from utilities_common.netstat import ns_diff, STATUS_NA, format_number_with_comma, format_microseconds_as_datetime from utilities_common import multi_asic as multi_asic_util from utilities_common import constants from utilities_common.cli import json_serial, UserCache PStats = namedtuple("PStats", "pfc0, pfc1, pfc2, pfc3, pfc4, pfc5, pfc6, pfc7") -header_Rx = ['Port Rx', 'PFC0', 'PFC1', 'PFC2', 'PFC3', 'PFC4', 'PFC5', 'PFC6', 'PFC7'] -header_Tx = ['Port Tx', 'PFC0', 'PFC1', 'PFC2', 'PFC3', 'PFC4', 'PFC5', 'PFC6', 'PFC7'] +pfc_titles = ['PFC0', 'PFC1', 'PFC2', 'PFC3', 'PFC4', 'PFC5', 'PFC6', 'PFC7'] + +header_Rx = ['Port Rx'] + pfc_titles +header_Tx = ['Port Tx'] + pfc_titles counter_bucket_rx_dict = { 'SAI_PORT_STAT_PFC_0_RX_PKTS': 0, @@ -68,6 +70,27 @@ counter_bucket_tx_dict = { } +HistStats = namedtuple("HistStats", "numTransitions, totalPauseTime, recentPauseTimestamp, recentPauseTime") + +SAI_PREFIX = "SAI" +EST_PREFIX = "EST" + +total_stat_fields = [ + "_PORT_STAT_PFC_*_ON2OFF_RX_PKTS", + "_PORT_STAT_PFC_*_RX_PAUSE_DURATION_US" +] + +recent_stat_fields = [ + "EST_PORT_STAT_PFC_*_RECENT_PAUSE_TIMESTAMP", + "EST_PORT_STAT_PFC_*_RECENT_PAUSE_TIME_US" +] + +header_hist = ['Port', 'Priority', + 'RX Pause Transitions', + 'Total RX Pause Time US', + 'Recent RX Pause Time US', + 'Recent RX Pause Timestamp'] + COUNTER_TABLE_PREFIX = "COUNTERS:" COUNTERS_PORT_NAME_MAP = "COUNTERS_PORT_NAME_MAP" @@ -77,6 +100,7 @@ class Pfcstat(object): self.db = None self.config_db = None self.cnstat_dict = OrderedDict() + self.hist_dict = OrderedDict() @multi_asic_util.run_on_multi_asic def collect_cnstat(self, rx): @@ -126,6 +150,74 @@ class Pfcstat(object): ) self.cnstat_dict.update(cnstat_dict) + @multi_asic_util.run_on_multi_asic + def collect_history(self): + """ + Get the history stats from database. + """ + def get_history_stats(table_id): + """ + Get the history from specific table. + """ + pfc_dict = OrderedDict() + full_table_id = COUNTER_TABLE_PREFIX + table_id + + for pfc_index, pfc in enumerate(pfc_titles): + def get_stat(stat_name, prefix = ""): + full_stat_name = prefix + stat_name.replace('*', str(pfc_index)) + stat = self.db.get( + self.db.COUNTERS_DB, full_table_id, full_stat_name + ) + return stat + + fields = [STATUS_NA] * (len(total_stat_fields) + len(recent_stat_fields)) + stat_index = 0 + # SAI or EST options + for stat_name in total_stat_fields: + hist_data = get_stat(stat_name, SAI_PREFIX) + + if hist_data is None: + hist_data = get_stat(stat_name, EST_PREFIX) + + if hist_data is not None: + fields[stat_index] = str(hist_data) + stat_index += 1 + + # EST options only + for stat_name in recent_stat_fields: + hist_data = get_stat(stat_name) + + if hist_data is not None: + fields[stat_index] = str(hist_data) + stat_index += 1 + + stats = HistStats._make(fields)._asdict() + pfc_dict[pfc] = stats + return pfc_dict + + # get the port name : oid map + counter_port_name_map = self.db.get_all( + self.db.COUNTERS_DB, COUNTERS_PORT_NAME_MAP + ) + if counter_port_name_map is None: + return + + display_ports_set = set(counter_port_name_map.keys()) + if self.multi_asic.display_option == constants.DISPLAY_EXTERNAL: + display_ports_set = get_external_ports( + display_ports_set, self.multi_asic.current_namespace + ) + # Build a dictionary of the stats + hist_dict = OrderedDict() + hist_dict['time'] = datetime.datetime.now() + if counter_port_name_map is not None: + for port in natsorted(counter_port_name_map): + if port in display_ports_set: + hist_dict[port] = get_history_stats( + counter_port_name_map[port] + ) + self.hist_dict.update(hist_dict) + def get_cnstat(self, rx): """ Get the counters info from database. @@ -134,6 +226,14 @@ class Pfcstat(object): self.collect_cnstat(rx) return self.cnstat_dict + def get_history(self): + """ + Get the history stats from database. These values are populated by the pfcwd detect script. + """ + self.hist_dict.clear() + self.collect_history() + return self.hist_dict + def cnstat_print(self, cnstat_dict, rx): """ Print the cnstat. @@ -197,6 +297,51 @@ class Pfcstat(object): else: print(tabulate(table, header_Tx, tablefmt='simple', stralign='right')) + def history_diff_print(self, headers, hist_new_dict, hist_old_dict={}, ): + """ + Print the difference between two cnstat history results. + """ + table = [] + # time : <>, ethernet0 : { pfc0: {}, pfc1: {} }, ethernet1 :{} + for key, pfc_dict in hist_new_dict.items(): + if key == 'time': + continue + # old dict has this port + if key in hist_old_dict: + for pfc, stat in pfc_dict.items(): + old_stat = None + if pfc in hist_old_dict.get(key): + old_stat = hist_old_dict.get(key).get(pfc) + # old dict [ port ] has this priority data + if old_stat is not None: + table.append((key, + pfc, + ns_diff(stat['numTransitions'], old_stat['numTransitions']), + ns_diff(stat['totalPauseTime'], old_stat['totalPauseTime']), + format_number_with_comma(stat['recentPauseTime']), + format_microseconds_as_datetime(stat['recentPauseTimestamp']))) + # use the new data only for this port for this priority + else: + table.append((key, + pfc, + format_number_with_comma(stat['numTransitions']), + format_number_with_comma(stat['totalPauseTime']), + format_number_with_comma(stat['recentPauseTime']), + format_microseconds_as_datetime(stat['recentPauseTimestamp']))) + # use the new data only for this port + else: + for pfc, stat in pfc_dict.items(): + table.append((key, + pfc, + format_number_with_comma(stat['numTransitions']), + format_number_with_comma(stat['totalPauseTime']), + format_number_with_comma(stat['recentPauseTime']), + format_microseconds_as_datetime(stat['recentPauseTimestamp']))) + # empty line between interfaces + table.append([]) + + print(tabulate(table, headers, tablefmt='simple', stralign='right')) + def main(): parser = argparse.ArgumentParser(description='Display the pfc counters', formatter_class=argparse.RawTextHelpFormatter, @@ -207,6 +352,9 @@ Examples: pfcstat -d pfcstat -n asic1 pfcstat -s all -n asic0 + pfcstat --history + pfcstat -n asic1 --history + pfcstat -s all -n asic0 --history """) parser.add_argument( '-c', '--clear', action='store_true', @@ -221,11 +369,18 @@ Examples: parser.add_argument('-n', '--namespace', default=None, help='Display interfaces for specific namespace' ) - parser.add_argument('-v', '--version', action='version', version='%(prog)s 1.0') + parser.add_argument( + '-v', '--version', action='version', version='%(prog)s 1.0' + ) + parser.add_argument( + '--history', action="store_true", help="Display historical PFC statistics, must be supported by the platform-specific PFCWD detect script." + ) + args = parser.parse_args() save_fresh_stats = args.clear delete_all_stats = args.delete + show_pfc_history = args.history cache = UserCache() cnstat_file = 'pfcstat' @@ -233,6 +388,7 @@ Examples: cnstat_dir = cache.get_directory() cnstat_fqn_file_rx = os.path.join(cnstat_dir, "{}rx".format(cnstat_file)) cnstat_fqn_file_tx = os.path.join(cnstat_dir, "{}tx".format(cnstat_file)) + hist_fqn_file = cnstat_fqn_file_rx + "_hist" # if '-c' option is provided get stats from all (frontend and backend) ports if save_fresh_stats: @@ -244,55 +400,87 @@ Examples: if delete_all_stats: cache.remove() - """ - Get the counters of pfc rx counter - """ - cnstat_dict_rx = deepcopy(pfcstat.get_cnstat(True)) - - """ - Get the counters of pfc tx counter - """ - cnstat_dict_tx = deepcopy(pfcstat.get_cnstat(False)) - + # clear both counters + history if save_fresh_stats: + # Get the counters of pfc rx counter + cnstat_dict_rx = deepcopy(pfcstat.get_cnstat(True)) + # Get the counters of pfc tx counter + cnstat_dict_tx = deepcopy(pfcstat.get_cnstat(False)) + # Get the history stats of pfc rx + hist_dict = deepcopy(pfcstat.get_history()) + try: json.dump(cnstat_dict_rx, open(cnstat_fqn_file_rx, 'w'), default=json_serial) json.dump(cnstat_dict_tx, open(cnstat_fqn_file_tx, 'w'), default=json_serial) + json.dump(hist_dict, open(hist_fqn_file, 'w'), default=json_serial) except IOError as e: print(e.errno, e) sys.exit(e.errno) else: - print("Clear saved counters") + print("Clear saved PFC counters and history") sys.exit(0) + # save history stats separately + if show_pfc_history: + """ + Get the history stats of pfc rx + """ + hist_dict = deepcopy(pfcstat.get_history()) + + """ + Print the pfc history stats + """ + if os.path.isfile(hist_fqn_file): + try: + hist_cached_dict = json.load(open(hist_fqn_file, 'r')) + print("Last cached time was " + str(hist_cached_dict.get('time'))) + pfcstat.history_diff_print(header_hist, hist_dict, hist_cached_dict) + except IOError as e: + print(e.errno, e) + sys.exit(e.errno) + else: + pfcstat.history_diff_print(header_hist, hist_dict) - """ - Print the counters of pfc rx counter - """ - if os.path.isfile(cnstat_fqn_file_rx): - try: - cnstat_cached_dict = json.load(open(cnstat_fqn_file_rx, 'r')) - print("Last cached time was " + str(cnstat_cached_dict.get('time'))) - pfcstat.cnstat_diff_print(cnstat_dict_rx, cnstat_cached_dict, True) - except IOError as e: - print(e.errno, e) else: - pfcstat.cnstat_print(cnstat_dict_rx, True) + """ + Get the counters of pfc rx counter + """ + cnstat_dict_rx = deepcopy(pfcstat.get_cnstat(True)) - print("") + """ + Get the counters of pfc tx counter + """ + cnstat_dict_tx = deepcopy(pfcstat.get_cnstat(False)) - """ - Print the counters of pfc tx counter - """ - if os.path.isfile(cnstat_fqn_file_tx): - try: - cnstat_cached_dict = json.load(open(cnstat_fqn_file_tx, 'r')) - print("Last cached time was " + str(cnstat_cached_dict.get('time'))) - pfcstat.cnstat_diff_print(cnstat_dict_tx, cnstat_cached_dict, False) - except IOError as e: - print(e.errno, e) - else: - pfcstat.cnstat_print(cnstat_dict_tx, False) + """ + Print the counters of pfc rx counter + """ + if os.path.isfile(cnstat_fqn_file_rx): + try: + cnstat_cached_dict = json.load(open(cnstat_fqn_file_rx, 'r')) + print("Last cached time was " + str(cnstat_cached_dict.get('time'))) + pfcstat.cnstat_diff_print(cnstat_dict_rx, cnstat_cached_dict, True) + except IOError as e: + print(e.errno, e) + sys.exit(e.errno) + else: + pfcstat.cnstat_print(cnstat_dict_rx, True) + + print("") + + """ + Print the counters of pfc tx counter + """ + if os.path.isfile(cnstat_fqn_file_tx): + try: + cnstat_cached_dict = json.load(open(cnstat_fqn_file_tx, 'r')) + print("Last cached time was " + str(cnstat_cached_dict.get('time'))) + pfcstat.cnstat_diff_print(cnstat_dict_tx, cnstat_cached_dict, False) + except IOError as e: + print(e.errno, e) + sys.exit(e.errno) + else: + pfcstat.cnstat_print(cnstat_dict_tx, False) sys.exit(0) diff --git a/show/main.py b/show/main.py index 527cd3048d..2e60c814c9 100755 --- a/show/main.py +++ b/show/main.py @@ -644,13 +644,16 @@ def pfc(): # 'counters' subcommand ("show interfaces pfccounters") @pfc.command() @multi_asic_util.multi_asic_click_options +@click.option('--history', is_flag=True, help="Display historical PFC statistics") @click.option('--verbose', is_flag=True, help="Enable verbose output") -def counters(namespace, display, verbose): +def counters(namespace, history, display, verbose): """Show pfc counters""" cmd = ['pfcstat', '-s', str(display)] if namespace is not None: cmd += ['-n', str(namespace)] + if history: + cmd += ['--history'] run_command(cmd, display_cmd=verbose) diff --git a/tests/mock_tables/asic0/counters_db.json b/tests/mock_tables/asic0/counters_db.json index 7de2015c41..b85be89dbe 100644 --- a/tests/mock_tables/asic0/counters_db.json +++ b/tests/mock_tables/asic0/counters_db.json @@ -1808,13 +1808,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "80", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "20", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1200", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "12", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "12000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1200000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1200000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "201", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "21", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "20000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "2000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "2000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "202", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "22", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "20000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "2000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "2000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "203", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "23", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "20000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "2000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "2000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "204", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "24", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "20000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "2000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "2000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "205", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "25", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "20000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "2000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "2000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "206", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "26", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "20000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "2000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "2000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "207", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "27", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "20000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "2000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "2000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1210", "SAI_PORT_STAT_PFC_1_TX_PKTS": "211", "SAI_PORT_STAT_PFC_2_TX_PKTS": "212", @@ -1837,13 +1869,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "800", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "100", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1400", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "14", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "14000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1400000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1400000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "401", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "41", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "40000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "4000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "4000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "402", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "42", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "40000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "4000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "4000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "403", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "43", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "40000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "4000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "4000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "404", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "44", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "40000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "4000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "4000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "405", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "45", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "40000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "4000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "4000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "406", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "46", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "40000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "4000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "4000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "407", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "47", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "40000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "4000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "4000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1410", "SAI_PORT_STAT_PFC_1_TX_PKTS": "411", "SAI_PORT_STAT_PFC_2_TX_PKTS": "412", @@ -1866,13 +1930,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "800", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "100", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1600", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "16", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "16000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1600000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1600000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "601", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "61", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "60000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "6000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "6000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "602", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "62", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "60000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "6000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "6000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "603", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "63", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "60000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "6000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "6000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "604", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "64", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "60000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "6000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "6000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "605", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "65", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "60000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "6000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "6000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "606", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "66", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "60000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "6000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "6000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "607", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "67", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "60000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "6000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "6000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1610", "SAI_PORT_STAT_PFC_1_TX_PKTS": "611", "SAI_PORT_STAT_PFC_2_TX_PKTS": "612", @@ -1894,13 +1990,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "800", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "100", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1800", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "18", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "18000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1800000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1800000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "801", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "81", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "80000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "8000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "8000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "802", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "82", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "80000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "8000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "8000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "803", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "83", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "80000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "8000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "8000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "804", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "84", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "80000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "8000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "8000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "805", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "85", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "80000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "8000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "8000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "806", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "86", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "80000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "8000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "8000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "807", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "87", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "80000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "8000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "8000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1810", "SAI_PORT_STAT_PFC_1_TX_PKTS": "811", "SAI_PORT_STAT_PFC_2_TX_PKTS": "812", diff --git a/tests/mock_tables/counters_db.json b/tests/mock_tables/counters_db.json index e1360564ea..d4453cd4ec 100644 --- a/tests/mock_tables/counters_db.json +++ b/tests/mock_tables/counters_db.json @@ -1169,13 +1169,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "80", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "20", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1200", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "12", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "12000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1200000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1200000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "201", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "21", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "20000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "2000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "2000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "202", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "22", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "20000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "2000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "2000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "203", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "23", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "20000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "2000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "2000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "204", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "24", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "20000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "2000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "2000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "205", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "25", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "20000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "2000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "2000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "206", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "26", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "20000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "2000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "2000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "207", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "27", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "20000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "2000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "2000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1210", "SAI_PORT_STAT_PFC_1_TX_PKTS": "211", "SAI_PORT_STAT_PFC_2_TX_PKTS": "212", @@ -1246,13 +1278,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "800", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "100", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1400", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "14", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "14000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1400000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1400000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "401", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "41", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "40000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "4000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "4000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "402", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "42", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "40000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "4000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "4000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "403", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "43", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "40000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "4000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "4000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "404", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "44", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "40000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "4000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "4000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "405", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "45", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "40000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "4000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "4000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "406", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "46", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "40000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "4000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "4000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "407", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "47", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "40000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "4000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "4000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1410", "SAI_PORT_STAT_PFC_1_TX_PKTS": "411", "SAI_PORT_STAT_PFC_2_TX_PKTS": "412", @@ -1311,13 +1375,45 @@ "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "10", "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "0", "SAI_PORT_STAT_PFC_0_RX_PKTS": "1800", + "EST_PORT_STAT_PFC_0_ON2OFF_RX_PKTS": "18", + "EST_PORT_STAT_PFC_0_RX_PAUSE_DURATION_US": "18000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIMESTAMP": "1800000000000000", + "EST_PORT_STAT_PFC_0_RECENT_PAUSE_TIME_US": "1800000", "SAI_PORT_STAT_PFC_1_RX_PKTS": "801", + "EST_PORT_STAT_PFC_1_ON2OFF_RX_PKTS": "81", + "EST_PORT_STAT_PFC_1_RX_PAUSE_DURATION_US": "80000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIMESTAMP": "8000000000000001", + "EST_PORT_STAT_PFC_1_RECENT_PAUSE_TIME_US": "8000001", "SAI_PORT_STAT_PFC_2_RX_PKTS": "802", + "EST_PORT_STAT_PFC_2_ON2OFF_RX_PKTS": "82", + "EST_PORT_STAT_PFC_2_RX_PAUSE_DURATION_US": "80000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIMESTAMP": "8000000000000002", + "EST_PORT_STAT_PFC_2_RECENT_PAUSE_TIME_US": "8000002", "SAI_PORT_STAT_PFC_3_RX_PKTS": "803", + "EST_PORT_STAT_PFC_3_ON2OFF_RX_PKTS": "83", + "EST_PORT_STAT_PFC_3_RX_PAUSE_DURATION_US": "80000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIMESTAMP": "8000000000000003", + "EST_PORT_STAT_PFC_3_RECENT_PAUSE_TIME_US": "8000003", "SAI_PORT_STAT_PFC_4_RX_PKTS": "804", + "EST_PORT_STAT_PFC_4_ON2OFF_RX_PKTS": "84", + "EST_PORT_STAT_PFC_4_RX_PAUSE_DURATION_US": "80000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIMESTAMP": "8000000000000004", + "EST_PORT_STAT_PFC_4_RECENT_PAUSE_TIME_US": "8000004", "SAI_PORT_STAT_PFC_5_RX_PKTS": "805", + "EST_PORT_STAT_PFC_5_ON2OFF_RX_PKTS": "85", + "EST_PORT_STAT_PFC_5_RX_PAUSE_DURATION_US": "80000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIMESTAMP": "8000000000000005", + "EST_PORT_STAT_PFC_5_RECENT_PAUSE_TIME_US": "8000005", "SAI_PORT_STAT_PFC_6_RX_PKTS": "806", + "EST_PORT_STAT_PFC_6_ON2OFF_RX_PKTS": "86", + "EST_PORT_STAT_PFC_6_RX_PAUSE_DURATION_US": "80000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIMESTAMP": "8000000000000006", + "EST_PORT_STAT_PFC_6_RECENT_PAUSE_TIME_US": "8000006", "SAI_PORT_STAT_PFC_7_RX_PKTS": "807", + "EST_PORT_STAT_PFC_7_ON2OFF_RX_PKTS": "87", + "EST_PORT_STAT_PFC_7_RX_PAUSE_DURATION_US": "80000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIMESTAMP": "8000000000000007", + "EST_PORT_STAT_PFC_7_RECENT_PAUSE_TIME_US": "8000007", "SAI_PORT_STAT_PFC_0_TX_PKTS": "1810", "SAI_PORT_STAT_PFC_1_TX_PKTS": "811", "SAI_PORT_STAT_PFC_2_TX_PKTS": "812", diff --git a/tests/pfcstat_input/assert_show_output.py b/tests/pfcstat_input/assert_show_output.py new file mode 100644 index 0000000000..c688eb2745 --- /dev/null +++ b/tests/pfcstat_input/assert_show_output.py @@ -0,0 +1,247 @@ +# flake8: noqa: E501 + +show_pfc_counters_history_output = """\ + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp +--------- ---------- ---------------------- ------------------------ ------------------------- --------------------------- +Ethernet0 PFC0 12 12,000,000 1,200,000 01/10/2008, 21:20:00 +Ethernet0 PFC1 21 20,000,001 2,000,001 05/18/2033, 03:33:20 +Ethernet0 PFC2 22 20,000,002 2,000,002 05/18/2033, 03:33:20 +Ethernet0 PFC3 23 20,000,003 2,000,003 05/18/2033, 03:33:20 +Ethernet0 PFC4 24 20,000,004 2,000,004 05/18/2033, 03:33:20 +Ethernet0 PFC5 25 20,000,005 2,000,005 05/18/2033, 03:33:20 +Ethernet0 PFC6 26 20,000,006 2,000,006 05/18/2033, 03:33:20 +Ethernet0 PFC7 27 20,000,007 2,000,007 05/18/2033, 03:33:20 + +Ethernet4 PFC0 14 14,000,000 1,400,000 05/13/2014, 16:53:20 +Ethernet4 PFC1 41 40,000,001 4,000,001 10/02/2096, 07:06:40 +Ethernet4 PFC2 42 40,000,002 4,000,002 10/02/2096, 07:06:40 +Ethernet4 PFC3 43 40,000,003 4,000,003 10/02/2096, 07:06:40 +Ethernet4 PFC4 44 40,000,004 4,000,004 10/02/2096, 07:06:40 +Ethernet4 PFC5 45 40,000,005 4,000,005 10/02/2096, 07:06:40 +Ethernet4 PFC6 46 40,000,006 4,000,006 10/02/2096, 07:06:40 +Ethernet4 PFC7 47 40,000,007 4,000,007 10/02/2096, 07:06:40 + +Ethernet8 PFC0 18 18,000,000 1,800,000 01/15/2027, 08:00:00 +Ethernet8 PFC1 81 80,000,001 8,000,001 07/06/2223, 14:13:20 +Ethernet8 PFC2 82 80,000,002 8,000,002 07/06/2223, 14:13:20 +Ethernet8 PFC3 83 80,000,003 8,000,003 07/06/2223, 14:13:20 +Ethernet8 PFC4 84 80,000,004 8,000,004 07/06/2223, 14:13:20 +Ethernet8 PFC5 85 80,000,005 8,000,005 07/06/2223, 14:13:20 +Ethernet8 PFC6 86 80,000,006 8,000,006 07/06/2223, 14:13:20 +Ethernet8 PFC7 87 80,000,007 8,000,007 07/06/2223, 14:13:20 + +""" + +show_pfc_counters_history_output_with_clear = """\ + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp +--------- ---------- ---------------------- ------------------------ ------------------------- --------------------------- +Ethernet0 PFC0 0 0 1,200,000 01/10/2008, 21:20:00 +Ethernet0 PFC1 0 0 2,000,001 05/18/2033, 03:33:20 +Ethernet0 PFC2 0 0 2,000,002 05/18/2033, 03:33:20 +Ethernet0 PFC3 0 0 2,000,003 05/18/2033, 03:33:20 +Ethernet0 PFC4 0 0 2,000,004 05/18/2033, 03:33:20 +Ethernet0 PFC5 0 0 2,000,005 05/18/2033, 03:33:20 +Ethernet0 PFC6 0 0 2,000,006 05/18/2033, 03:33:20 +Ethernet0 PFC7 0 0 2,000,007 05/18/2033, 03:33:20 + +Ethernet4 PFC0 0 0 1,400,000 05/13/2014, 16:53:20 +Ethernet4 PFC1 0 0 4,000,001 10/02/2096, 07:06:40 +Ethernet4 PFC2 0 0 4,000,002 10/02/2096, 07:06:40 +Ethernet4 PFC3 0 0 4,000,003 10/02/2096, 07:06:40 +Ethernet4 PFC4 0 0 4,000,004 10/02/2096, 07:06:40 +Ethernet4 PFC5 0 0 4,000,005 10/02/2096, 07:06:40 +Ethernet4 PFC6 0 0 4,000,006 10/02/2096, 07:06:40 +Ethernet4 PFC7 0 0 4,000,007 10/02/2096, 07:06:40 + +Ethernet8 PFC0 0 0 1,800,000 01/15/2027, 08:00:00 +Ethernet8 PFC1 0 0 8,000,001 07/06/2223, 14:13:20 +Ethernet8 PFC2 0 0 8,000,002 07/06/2223, 14:13:20 +Ethernet8 PFC3 0 0 8,000,003 07/06/2223, 14:13:20 +Ethernet8 PFC4 0 0 8,000,004 07/06/2223, 14:13:20 +Ethernet8 PFC5 0 0 8,000,005 07/06/2223, 14:13:20 +Ethernet8 PFC6 0 0 8,000,006 07/06/2223, 14:13:20 +Ethernet8 PFC7 0 0 8,000,007 07/06/2223, 14:13:20 + +""" + +show_pfc_counters_history_all = """\ + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp +-------------- ---------- ---------------------- ------------------------ ------------------------- --------------------------- + Ethernet0 PFC0 12 12,000,000 1,200,000 01/10/2008, 21:20:00 + Ethernet0 PFC1 21 20,000,001 2,000,001 05/18/2033, 03:33:20 + Ethernet0 PFC2 22 20,000,002 2,000,002 05/18/2033, 03:33:20 + Ethernet0 PFC3 23 20,000,003 2,000,003 05/18/2033, 03:33:20 + Ethernet0 PFC4 24 20,000,004 2,000,004 05/18/2033, 03:33:20 + Ethernet0 PFC5 25 20,000,005 2,000,005 05/18/2033, 03:33:20 + Ethernet0 PFC6 26 20,000,006 2,000,006 05/18/2033, 03:33:20 + Ethernet0 PFC7 27 20,000,007 2,000,007 05/18/2033, 03:33:20 + + Ethernet4 PFC0 14 14,000,000 1,400,000 05/13/2014, 16:53:20 + Ethernet4 PFC1 41 40,000,001 4,000,001 10/02/2096, 07:06:40 + Ethernet4 PFC2 42 40,000,002 4,000,002 10/02/2096, 07:06:40 + Ethernet4 PFC3 43 40,000,003 4,000,003 10/02/2096, 07:06:40 + Ethernet4 PFC4 44 40,000,004 4,000,004 10/02/2096, 07:06:40 + Ethernet4 PFC5 45 40,000,005 4,000,005 10/02/2096, 07:06:40 + Ethernet4 PFC6 46 40,000,006 4,000,006 10/02/2096, 07:06:40 + Ethernet4 PFC7 47 40,000,007 4,000,007 10/02/2096, 07:06:40 + + Ethernet-BP0 PFC0 16 16,000,000 1,600,000 09/13/2020, 12:26:40 + Ethernet-BP0 PFC1 61 60,000,001 6,000,001 02/18/2160, 10:40:00 + Ethernet-BP0 PFC2 62 60,000,002 6,000,002 02/18/2160, 10:40:00 + Ethernet-BP0 PFC3 63 60,000,003 6,000,003 02/18/2160, 10:40:00 + Ethernet-BP0 PFC4 64 60,000,004 6,000,004 02/18/2160, 10:40:00 + Ethernet-BP0 PFC5 65 60,000,005 6,000,005 02/18/2160, 10:40:00 + Ethernet-BP0 PFC6 66 60,000,006 6,000,006 02/18/2160, 10:40:00 + Ethernet-BP0 PFC7 67 60,000,007 6,000,007 02/18/2160, 10:40:00 + + Ethernet-BP4 PFC0 18 18,000,000 1,800,000 01/15/2027, 08:00:00 + Ethernet-BP4 PFC1 81 80,000,001 8,000,001 07/06/2223, 14:13:20 + Ethernet-BP4 PFC2 82 80,000,002 8,000,002 07/06/2223, 14:13:20 + Ethernet-BP4 PFC3 83 80,000,003 8,000,003 07/06/2223, 14:13:20 + Ethernet-BP4 PFC4 84 80,000,004 8,000,004 07/06/2223, 14:13:20 + Ethernet-BP4 PFC5 85 80,000,005 8,000,005 07/06/2223, 14:13:20 + Ethernet-BP4 PFC6 86 80,000,006 8,000,006 07/06/2223, 14:13:20 + Ethernet-BP4 PFC7 87 80,000,007 8,000,007 07/06/2223, 14:13:20 + +Ethernet-BP256 PFC0 N/A N/A N/A N/A +Ethernet-BP256 PFC1 N/A N/A N/A N/A +Ethernet-BP256 PFC2 N/A N/A N/A N/A +Ethernet-BP256 PFC3 N/A N/A N/A N/A +Ethernet-BP256 PFC4 N/A N/A N/A N/A +Ethernet-BP256 PFC5 N/A N/A N/A N/A +Ethernet-BP256 PFC6 N/A N/A N/A N/A +Ethernet-BP256 PFC7 N/A N/A N/A N/A + +Ethernet-BP260 PFC0 N/A N/A N/A N/A +Ethernet-BP260 PFC1 N/A N/A N/A N/A +Ethernet-BP260 PFC2 N/A N/A N/A N/A +Ethernet-BP260 PFC3 N/A N/A N/A N/A +Ethernet-BP260 PFC4 N/A N/A N/A N/A +Ethernet-BP260 PFC5 N/A N/A N/A N/A +Ethernet-BP260 PFC6 N/A N/A N/A N/A +Ethernet-BP260 PFC7 N/A N/A N/A N/A + +""" + +show_pfc_counters_history_all_with_clear = """\ + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp +-------------- ---------- ---------------------- ------------------------ ------------------------- --------------------------- + Ethernet0 PFC0 0 0 1,200,000 01/10/2008, 21:20:00 + Ethernet0 PFC1 0 0 2,000,001 05/18/2033, 03:33:20 + Ethernet0 PFC2 0 0 2,000,002 05/18/2033, 03:33:20 + Ethernet0 PFC3 0 0 2,000,003 05/18/2033, 03:33:20 + Ethernet0 PFC4 0 0 2,000,004 05/18/2033, 03:33:20 + Ethernet0 PFC5 0 0 2,000,005 05/18/2033, 03:33:20 + Ethernet0 PFC6 0 0 2,000,006 05/18/2033, 03:33:20 + Ethernet0 PFC7 0 0 2,000,007 05/18/2033, 03:33:20 + + Ethernet4 PFC0 0 0 1,400,000 05/13/2014, 16:53:20 + Ethernet4 PFC1 0 0 4,000,001 10/02/2096, 07:06:40 + Ethernet4 PFC2 0 0 4,000,002 10/02/2096, 07:06:40 + Ethernet4 PFC3 0 0 4,000,003 10/02/2096, 07:06:40 + Ethernet4 PFC4 0 0 4,000,004 10/02/2096, 07:06:40 + Ethernet4 PFC5 0 0 4,000,005 10/02/2096, 07:06:40 + Ethernet4 PFC6 0 0 4,000,006 10/02/2096, 07:06:40 + Ethernet4 PFC7 0 0 4,000,007 10/02/2096, 07:06:40 + + Ethernet-BP0 PFC0 0 0 1,600,000 09/13/2020, 12:26:40 + Ethernet-BP0 PFC1 0 0 6,000,001 02/18/2160, 10:40:00 + Ethernet-BP0 PFC2 0 0 6,000,002 02/18/2160, 10:40:00 + Ethernet-BP0 PFC3 0 0 6,000,003 02/18/2160, 10:40:00 + Ethernet-BP0 PFC4 0 0 6,000,004 02/18/2160, 10:40:00 + Ethernet-BP0 PFC5 0 0 6,000,005 02/18/2160, 10:40:00 + Ethernet-BP0 PFC6 0 0 6,000,006 02/18/2160, 10:40:00 + Ethernet-BP0 PFC7 0 0 6,000,007 02/18/2160, 10:40:00 + + Ethernet-BP4 PFC0 0 0 1,800,000 01/15/2027, 08:00:00 + Ethernet-BP4 PFC1 0 0 8,000,001 07/06/2223, 14:13:20 + Ethernet-BP4 PFC2 0 0 8,000,002 07/06/2223, 14:13:20 + Ethernet-BP4 PFC3 0 0 8,000,003 07/06/2223, 14:13:20 + Ethernet-BP4 PFC4 0 0 8,000,004 07/06/2223, 14:13:20 + Ethernet-BP4 PFC5 0 0 8,000,005 07/06/2223, 14:13:20 + Ethernet-BP4 PFC6 0 0 8,000,006 07/06/2223, 14:13:20 + Ethernet-BP4 PFC7 0 0 8,000,007 07/06/2223, 14:13:20 + +Ethernet-BP256 PFC0 N/A N/A N/A N/A +Ethernet-BP256 PFC1 N/A N/A N/A N/A +Ethernet-BP256 PFC2 N/A N/A N/A N/A +Ethernet-BP256 PFC3 N/A N/A N/A N/A +Ethernet-BP256 PFC4 N/A N/A N/A N/A +Ethernet-BP256 PFC5 N/A N/A N/A N/A +Ethernet-BP256 PFC6 N/A N/A N/A N/A +Ethernet-BP256 PFC7 N/A N/A N/A N/A + +Ethernet-BP260 PFC0 N/A N/A N/A N/A +Ethernet-BP260 PFC1 N/A N/A N/A N/A +Ethernet-BP260 PFC2 N/A N/A N/A N/A +Ethernet-BP260 PFC3 N/A N/A N/A N/A +Ethernet-BP260 PFC4 N/A N/A N/A N/A +Ethernet-BP260 PFC5 N/A N/A N/A N/A +Ethernet-BP260 PFC6 N/A N/A N/A N/A +Ethernet-BP260 PFC7 N/A N/A N/A N/A + +""" + +show_pfc_counters_history_all_asic = """\ + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp +------------ ---------- ---------------------- ------------------------ ------------------------- --------------------------- + Ethernet0 PFC0 12 12,000,000 1,200,000 01/10/2008, 21:20:00 + Ethernet0 PFC1 21 20,000,001 2,000,001 05/18/2033, 03:33:20 + Ethernet0 PFC2 22 20,000,002 2,000,002 05/18/2033, 03:33:20 + Ethernet0 PFC3 23 20,000,003 2,000,003 05/18/2033, 03:33:20 + Ethernet0 PFC4 24 20,000,004 2,000,004 05/18/2033, 03:33:20 + Ethernet0 PFC5 25 20,000,005 2,000,005 05/18/2033, 03:33:20 + Ethernet0 PFC6 26 20,000,006 2,000,006 05/18/2033, 03:33:20 + Ethernet0 PFC7 27 20,000,007 2,000,007 05/18/2033, 03:33:20 + + Ethernet4 PFC0 14 14,000,000 1,400,000 05/13/2014, 16:53:20 + Ethernet4 PFC1 41 40,000,001 4,000,001 10/02/2096, 07:06:40 + Ethernet4 PFC2 42 40,000,002 4,000,002 10/02/2096, 07:06:40 + Ethernet4 PFC3 43 40,000,003 4,000,003 10/02/2096, 07:06:40 + Ethernet4 PFC4 44 40,000,004 4,000,004 10/02/2096, 07:06:40 + Ethernet4 PFC5 45 40,000,005 4,000,005 10/02/2096, 07:06:40 + Ethernet4 PFC6 46 40,000,006 4,000,006 10/02/2096, 07:06:40 + Ethernet4 PFC7 47 40,000,007 4,000,007 10/02/2096, 07:06:40 + +Ethernet-BP0 PFC0 16 16,000,000 1,600,000 09/13/2020, 12:26:40 +Ethernet-BP0 PFC1 61 60,000,001 6,000,001 02/18/2160, 10:40:00 +Ethernet-BP0 PFC2 62 60,000,002 6,000,002 02/18/2160, 10:40:00 +Ethernet-BP0 PFC3 63 60,000,003 6,000,003 02/18/2160, 10:40:00 +Ethernet-BP0 PFC4 64 60,000,004 6,000,004 02/18/2160, 10:40:00 +Ethernet-BP0 PFC5 65 60,000,005 6,000,005 02/18/2160, 10:40:00 +Ethernet-BP0 PFC6 66 60,000,006 6,000,006 02/18/2160, 10:40:00 +Ethernet-BP0 PFC7 67 60,000,007 6,000,007 02/18/2160, 10:40:00 + +Ethernet-BP4 PFC0 18 18,000,000 1,800,000 01/15/2027, 08:00:00 +Ethernet-BP4 PFC1 81 80,000,001 8,000,001 07/06/2223, 14:13:20 +Ethernet-BP4 PFC2 82 80,000,002 8,000,002 07/06/2223, 14:13:20 +Ethernet-BP4 PFC3 83 80,000,003 8,000,003 07/06/2223, 14:13:20 +Ethernet-BP4 PFC4 84 80,000,004 8,000,004 07/06/2223, 14:13:20 +Ethernet-BP4 PFC5 85 80,000,005 8,000,005 07/06/2223, 14:13:20 +Ethernet-BP4 PFC6 86 80,000,006 8,000,006 07/06/2223, 14:13:20 +Ethernet-BP4 PFC7 87 80,000,007 8,000,007 07/06/2223, 14:13:20 + +""" + +show_pfc_counters_history_asic0_frontend = """\ + Port Priority RX Pause Transitions Total RX Pause Time US Recent RX Pause Time US Recent RX Pause Timestamp +--------- ---------- ---------------------- ------------------------ ------------------------- --------------------------- +Ethernet0 PFC0 12 12,000,000 1,200,000 01/10/2008, 21:20:00 +Ethernet0 PFC1 21 20,000,001 2,000,001 05/18/2033, 03:33:20 +Ethernet0 PFC2 22 20,000,002 2,000,002 05/18/2033, 03:33:20 +Ethernet0 PFC3 23 20,000,003 2,000,003 05/18/2033, 03:33:20 +Ethernet0 PFC4 24 20,000,004 2,000,004 05/18/2033, 03:33:20 +Ethernet0 PFC5 25 20,000,005 2,000,005 05/18/2033, 03:33:20 +Ethernet0 PFC6 26 20,000,006 2,000,006 05/18/2033, 03:33:20 +Ethernet0 PFC7 27 20,000,007 2,000,007 05/18/2033, 03:33:20 + +Ethernet4 PFC0 14 14,000,000 1,400,000 05/13/2014, 16:53:20 +Ethernet4 PFC1 41 40,000,001 4,000,001 10/02/2096, 07:06:40 +Ethernet4 PFC2 42 40,000,002 4,000,002 10/02/2096, 07:06:40 +Ethernet4 PFC3 43 40,000,003 4,000,003 10/02/2096, 07:06:40 +Ethernet4 PFC4 44 40,000,004 4,000,004 10/02/2096, 07:06:40 +Ethernet4 PFC5 45 40,000,005 4,000,005 10/02/2096, 07:06:40 +Ethernet4 PFC6 46 40,000,006 4,000,006 10/02/2096, 07:06:40 +Ethernet4 PFC7 47 40,000,007 4,000,007 10/02/2096, 07:06:40 + +""" diff --git a/tests/pfcstat_test.py b/tests/pfcstat_test.py index 23d184cc36..70d5341935 100644 --- a/tests/pfcstat_test.py +++ b/tests/pfcstat_test.py @@ -7,6 +7,7 @@ import show.main as show import clear.main as clear +from .pfcstat_input import assert_show_output from .utils import get_result_and_return_code from utilities_common.cli import UserCache @@ -96,6 +97,7 @@ Ethernet-BP260 0 0 0 0 0 0 0 0 """] + show_pfc_counters_all_asic = """\ Port Rx PFC0 PFC1 PFC2 PFC3 PFC4 PFC5 PFC6 PFC7 ------------ ------ ------ ------ ------ ------ ------ ------ ------ @@ -111,6 +113,7 @@ Ethernet-BP0 1,610 611 612 613 614 615 616 617 Ethernet-BP4 1,810 811 812 813 814 815 816 817 """ + show_pfc_counters_all = """\ Port Rx PFC0 PFC1 PFC2 PFC3 PFC4 PFC5 PFC6 PFC7 -------------- ------ ------ ------ ------ ------ ------ ------ ------ @@ -169,7 +172,7 @@ def del_cached_stats(): cache.remove_all() -def pfc_clear(expected_output): +def pfc_clear(expected_output, pfc_stat_show_args=[]): counters_file_list = ['0tx', '0rx'] del_cached_stats() @@ -178,7 +181,7 @@ def pfc_clear(expected_output): ) return_code, result = get_result_and_return_code( - ['pfcstat', '-s', 'all'] + ['pfcstat', '-s', 'all'] + pfc_stat_show_args ) result_stat = [s for s in result.split("\n") if "Last cached" not in s] expected = expected_output.split("\n") @@ -224,6 +227,33 @@ def test_pfc_counters_with_clear(self): def test_pfc_clear(self): pfc_clear(show_pfc_counters_output_diff) + def test_pfc_counters_history(self): + runner = CliRunner() + result = runner.invoke( + show.cli.commands["pfc"].commands["counters"], + ["--history"] + ) + print(result.output) + assert result.exit_code == 0 + assert result.output == assert_show_output.show_pfc_counters_history_output + + def test_pfc_counters_history_with_clear(self): + runner = CliRunner() + result = runner.invoke(clear.cli.commands['pfccounters'], []) + assert result.exit_code == 0 + result = runner.invoke( + show.cli.commands["pfc"].commands["counters"], + ["--history"] + ) + print(result.output) + show.run_command(['pfcstat', '-d']) + assert result.exit_code == 0 + assert "Last cached time was" in result.output + assert assert_show_output.show_pfc_counters_history_output_with_clear in result.output + + def test_pfc_history_clear(self): + pfc_clear(assert_show_output.show_pfc_counters_history_output_with_clear, ["--history"]) + @classmethod def teardown_class(cls): print("TEARDOWN") @@ -292,6 +322,54 @@ def test_pfc_counters_asic_all(self): def test_masic_pfc_clear(self): pfc_clear(show_pfc_counters_msaic_output_diff) + def test_pfc_counters_history_all(self): + runner = CliRunner() + result = runner.invoke( + show.cli.commands["pfc"].commands["counters"], + ["--history"] + ) + print(result.output) + assert result.exit_code == 0 + assert result.output == assert_show_output.show_pfc_counters_history_all + + def test_pfc_counters_history_all_with_clear(self): + runner = CliRunner() + result = runner.invoke(clear.cli.commands['pfccounters'], []) + assert result.exit_code == 0 + result = runner.invoke( + show.cli.commands["pfc"].commands["counters"], + ["--history"] + ) + print(result.output) + show.run_command(['pfcstat', '-d']) + assert result.exit_code == 0 + assert "Last cached time was" in result.output + assert assert_show_output.show_pfc_counters_history_all_with_clear in result.output + + def test_pfc_counters_history_frontend(self): + return_code, result = get_result_and_return_code( + ['pfcstat', '-s', 'frontend', '--history'] + ) + assert return_code == 0 + assert result == assert_show_output.show_pfc_counters_history_asic0_frontend + + def test_pfc_counters_history_asic(self): + return_code, result = get_result_and_return_code( + ['pfcstat', '-n', 'asic0', '--history'] + ) + assert return_code == 0 + assert result == assert_show_output.show_pfc_counters_history_asic0_frontend + + def test_pfc_counters_history_asic_all(self): + return_code, result = get_result_and_return_code( + ['pfcstat', '-n', 'asic0', '-s', 'all', '--history'] + ) + assert return_code == 0 + assert result == assert_show_output.show_pfc_counters_history_all_asic + + def test_masic_pfc_history_clear(self): + pfc_clear(assert_show_output.show_pfc_counters_history_all_with_clear, ["--history"]) + @classmethod def teardown_class(cls): print("TEARDOWN") diff --git a/utilities_common/netstat.py b/utilities_common/netstat.py index e32e28c745..5fd1df653e 100755 --- a/utilities_common/netstat.py +++ b/utilities_common/netstat.py @@ -1,5 +1,6 @@ # network statistics utility functions # +import datetime import json STATUS_NA = 'N/A' @@ -81,6 +82,19 @@ def format_number_with_comma(number_in_str): return number_in_str +def format_microseconds_as_datetime(number_in_str): + """ + Format the number of microseconds since epoch to a date. + """ + try: + microseconds = float(number_in_str) + seconds = microseconds / 1_000_000 + date = datetime.datetime.fromtimestamp(seconds) + return date.strftime("%m/%d/%Y, %H:%M:%S") + except Exception: + return number_in_str + + def format_brate(rate): """ Show the byte rate. From a3101eae7ec5ef3ce9c1a82814335aa09ff716ba Mon Sep 17 00:00:00 2001 From: rameshraghupathy <43161235+rameshraghupathy@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:10:08 -0700 Subject: [PATCH 12/21] Fix for #23205 [Smartswitch] Issues caused due to introduction of the chassisd/sonic-utiltiies changes for consecutive admin state changes (#3984) * Fixed for #23205 * Addressed a review comment ModuleBase.MODULE_STATUS_EMPTY * added required import --- show/chassis_modules.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/show/chassis_modules.py b/show/chassis_modules.py index 1a9ea2b710..d7c74fc9a6 100644 --- a/show/chassis_modules.py +++ b/show/chassis_modules.py @@ -3,6 +3,7 @@ from tabulate import tabulate from swsscommon.swsscommon import SonicV2Connector from utilities_common.chassis import is_smartswitch +from sonic_platform_base.module_base import ModuleBase import utilities_common.cli as clicommon from sonic_py_common import multi_asic @@ -58,18 +59,21 @@ def status(db, chassis_module_name): continue data_dict = state_db.get_all(state_db.STATE_DB, key) - desc = data_dict[CHASSIS_MODULE_INFO_DESC_FIELD] - slot = data_dict[CHASSIS_MODULE_INFO_SLOT_FIELD] - oper_status = data_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] - serial = data_dict[CHASSIS_MODULE_INFO_SERIAL_FIELD] + # Use default values if any field is missing + desc = data_dict.get(CHASSIS_MODULE_INFO_DESC_FIELD, 'N/A') + slot = data_dict.get(CHASSIS_MODULE_INFO_SLOT_FIELD, 'N/A') + oper_status = data_dict.get(CHASSIS_MODULE_INFO_OPERSTATUS_FIELD, ModuleBase.MODULE_STATUS_EMPTY) + serial = data_dict.get(CHASSIS_MODULE_INFO_SERIAL_FIELD, 'N/A') + + # Determine admin_status if is_smartswitch(): admin_status = 'down' else: admin_status = 'up' config_data = chassis_cfg_table.get(key_list[1]) if config_data is not None: - admin_status = config_data.get(CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD) + admin_status = config_data.get(CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD, admin_status) table.append((key_list[1], desc, slot, oper_status, admin_status, serial)) @@ -100,13 +104,15 @@ def midplane_status(chassis_module_name): table = [] for key in natsorted(keys): key_list = key.split('|') - if len(key_list) != 2: # error data in DB, log it and ignore + if len(key_list) != 2: print('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MIDPLANE_INFO_TABLE)) continue data_dict = state_db.get_all(state_db.STATE_DB, key) - ip = data_dict[CHASSIS_MIDPLANE_INFO_IP_FIELD] - access = data_dict[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] + + # Defensive access with fallback defaults + ip = data_dict.get(CHASSIS_MIDPLANE_INFO_IP_FIELD, 'N/A') + access = data_dict.get(CHASSIS_MIDPLANE_INFO_ACCESS_FIELD, 'Unknown') table.append((key_list[1], ip, access)) From d623c250850b695ee1b98dc8d03db316992c585c Mon Sep 17 00:00:00 2001 From: Gagan Punathil Ellath Date: Mon, 4 Aug 2025 11:36:50 -0700 Subject: [PATCH 13/21] [Mellanox][Smartswitch]Added dpu status output to dump (#3959) --- scripts/generate_dump | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/generate_dump b/scripts/generate_dump index 287bfec0cd..9c546dfe31 100755 --- a/scripts/generate_dump +++ b/scripts/generate_dump @@ -1343,6 +1343,7 @@ collect_mellanox() { local platform=$(python3 -c "from sonic_py_common import device_info; print(device_info.get_platform())") local platform_folder="/usr/share/sonic/device/${platform}" local hwsku=$(python3 -c "from sonic_py_common import device_info; print(device_info.get_hwsku())") + local is_smartswitch=$(python3 -c "from sonic_py_common import device_info; print(device_info.is_smartswitch())") local sku_folder="/usr/share/sonic/device/${platform}/${hwsku}" local cmis_host_mgmt_files=( "/tmp/nv-syncd-shared/sai.profile" @@ -1404,6 +1405,9 @@ collect_mellanox() { fi save_cmd "get_component_versions.py" "component_versions" + if [[ $is_smartswitch == "True" ]]; then + save_cmd "dpuctl dpu-status" "dpu_status" + fi # Save CMIS-host-management related files local cmis_host_mgmt_path="cmis-host-mgmt" From c0838d7f6cabdc42079aea638706eed76567d75b Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Mon, 4 Aug 2025 17:44:22 -0700 Subject: [PATCH 14/21] CLI for Configuring PFC Historical Statistics (#3779) * Add pfc stat history options to PFCWD CLI * Unit tests for PFCWD pfc history cli Includes modifications made to pass pre-commit (fixing import *). * Add pfc-stat-history command and flag for PFCWD to docs --- config/main.py | 21 +- doc/Command-Reference.md | 14 + pfcwd/main.py | 101 +++++-- tests/mock_tables/asic0/config_db.json | 12 +- tests/mock_tables/asic1/config_db.json | 6 +- tests/mock_tables/config_db.json | 9 +- tests/pfcwd_input/pfcwd_test_vectors.py | 220 ++++++++------ tests/pfcwd_test.py | 373 +++++++++++++++++++++--- 8 files changed, 596 insertions(+), 160 deletions(-) diff --git a/config/main.py b/config/main.py index 45224de7b8..debb1c4355 100644 --- a/config/main.py +++ b/config/main.py @@ -3125,10 +3125,11 @@ def pfcwd(): @pfcwd.command() @click.option('--action', '-a', type=click.Choice(['drop', 'forward', 'alert'])) @click.option('--restoration-time', '-r', type=click.IntRange(100, 60000)) +@click.option('--pfc-stat-history', is_flag=True, help="Enable historical statistics tracking") @click.option('--verbose', is_flag=True, help="Enable verbose output") @click.argument('ports', nargs=-1) @click.argument('detection-time', type=click.IntRange(100, 5000)) -def start(action, restoration_time, ports, detection_time, verbose): +def start(action, restoration_time, pfc_stat_history, ports, detection_time, verbose): """ Start PFC watchdog on port(s). To config all ports, use all as input. @@ -3150,6 +3151,9 @@ def start(action, restoration_time, ports, detection_time, verbose): if restoration_time: cmd += ['--restoration-time', str(restoration_time)] + if pfc_stat_history: + cmd += ['--pfc-stat-history'] + clicommon.run_command(cmd, display_cmd=verbose) @pfcwd.command() @@ -3191,6 +3195,21 @@ def big_red_switch(big_red_switch, verbose): clicommon.run_command(cmd, display_cmd=verbose) + +@pfcwd.command('pfc_stat_history') +@click.option('--verbose', is_flag=True, help="Enable verbose output") +@click.argument('pfc_stat_history', type=click.Choice(['enable', 'disable'])) +@click.argument('ports', nargs=-1) +def pfc_stat_history(ports, pfc_stat_history, verbose): + """ Enable/disable PFC Historical Statistics mode on ports""" + + cmd = ['pfcwd', 'pfc_stat_history', pfc_stat_history] + ports = set(ports) - set(['ports']) + cmd += list(ports) + + clicommon.run_command(cmd, display_cmd=verbose) + + @pfcwd.command('start_default') @click.option('--verbose', is_flag=True, help="Enable verbose output") def start_default(verbose): diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index fc8ebe8e14..2289d5e751 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -8663,6 +8663,7 @@ This command starts PFC Watchdog ``` config pfcwd start --action drop all 400 --restoration-time 400 config pfcwd start --action forward Ethernet0 Ethernet8 400 + config pfcwd start --action drop all 400 --restoration-time 400 --pfc-stat-history ``` **config pfcwd stop** @@ -8701,6 +8702,18 @@ This command enables or disables PFCWD's "BIG RED SWITCH"(BRS). After enabling B config pfcwd big_red_switch enable ``` +**config pfcwd pfc_stat_history \ \** + +This command enables or disables PFCWD's PFC Historical Statistics estimation. After enabling, PFC Watchdog will be configured to estimate pause transitions, total pause time, and the pause time and timstamp of the most recent pause activity on those ports. + +NOTE: The estimation will only be performed on ports the PFCWD has been started on, alternatively use the --pfc-stat-history flag with the `start` command to simultaneously enable history on those ports. + +- Usage: + ``` + config pfcwd pfc_stat_history enable all + config pfcwd pfc_stat_history disable Ethernet0 Ethernet8 + ``` + **config pfcwd start_default** This command starts PFC Watchdog with the default settings. @@ -8716,6 +8729,7 @@ Default values are the following: - restoration time - 200ms - polling interval - 200ms - action - 'drop' + - pfc stat history - disable Additionally if number of ports in the system exceeds 32, all times will be multiplied by roughly /32. diff --git a/pfcwd/main.py b/pfcwd/main.py index ac6075385b..f640ff1d5f 100644 --- a/pfcwd/main.py +++ b/pfcwd/main.py @@ -38,6 +38,7 @@ DEFAULT_POLL_INTERVAL = 200 DEFAULT_PORT_NUM = 32 DEFAULT_ACTION = 'drop' +DEFAULT_PFC_HISTORY_STATUS = "disable" STATS_DESCRIPTION = [ ('STORM DETECTED/RESTORED', 'PFC_WD_QUEUE_STATS_DEADLOCK_DETECTED', 'PFC_WD_QUEUE_STATS_DEADLOCK_RESTORED'), @@ -50,7 +51,8 @@ CONFIG_DESCRIPTION = [ ('ACTION', 'action', 'drop'), ('DETECTION TIME', 'detection_time', 'N/A'), - ('RESTORATION TIME', 'restoration_time', 'infinite') + ('RESTORATION TIME', 'restoration_time', 'infinite'), + ('HISTORY', 'pfc_stat_history', 'disable') ] STATS_HEADER = ('QUEUE', 'STATUS',) + list(zip(*STATS_DESCRIPTION))[0] @@ -248,34 +250,47 @@ def config(self, ports): tablefmt='simple' )) - def start(self, action, restoration_time, ports, detection_time): + def start(self, action, restoration_time, ports, detection_time, pfc_stat_history): invalid_ports = self.get_invalid_ports(ports) if len(invalid_ports): click.echo("Failed to run command, invalid options:") for opt in invalid_ports: click.echo(opt) sys.exit(1) - self.start_cmd(action, restoration_time, ports, detection_time) + self.start_cmd(action, restoration_time, ports, detection_time, pfc_stat_history) + def pfc_stat_history(self, pfc_stat_history, ports): + invalid_ports = self.get_invalid_ports(ports) + if len(invalid_ports): + click.echo("Failed to run command, invalid options:") + for opt in invalid_ports: + click.echo(opt) + sys.exit(1) + self.pfc_stat_history_cmd(pfc_stat_history, ports) - def verify_pfc_enable_status_per_port(self, port, pfcwd_info): + def verify_pfc_enable_status_per_port(self, port, pfcwd_info, overwrite=True): pfc_status = self.config_db.get_entry(PORT_QOS_MAP, port).get('pfc_enable') if pfc_status is None: log.log_warning("SKIPPED: PFC is not enabled on port: {}".format(port), also_print_to_console=True) return - self.config_db.mod_entry( - CONFIG_DB_PFC_WD_TABLE_NAME, port, None - ) - self.config_db.mod_entry( - CONFIG_DB_PFC_WD_TABLE_NAME, port, pfcwd_info - ) + if overwrite: + # don't clear existing pfc history setting unless set explicitely + cur_pfc_history = self.config_db.get_entry( + CONFIG_DB_PFC_WD_TABLE_NAME, port + ).get("pfc_stat_history", DEFAULT_PFC_HISTORY_STATUS) - @multi_asic_util.run_on_multi_asic - def start_cmd(self, action, restoration_time, ports, detection_time): - if os.geteuid() != 0: - sys.exit("Root privileges are required for this operation") + pfcwd_info.setdefault("pfc_stat_history", cur_pfc_history) + self.config_db.set_entry( + CONFIG_DB_PFC_WD_TABLE_NAME, port, pfcwd_info + ) + else: + self.config_db.mod_entry( + CONFIG_DB_PFC_WD_TABLE_NAME, port, pfcwd_info + ) + + def configure_ports(self, ports, pfcwd_info, overwrite=True): all_ports = get_all_ports( self.db, self.multi_asic.current_namespace, self.multi_asic.display_option @@ -284,6 +299,20 @@ def start_cmd(self, action, restoration_time, ports, detection_time): if len(ports) == 0: ports = all_ports + for port in ports: + if port == "all": + for p in all_ports: + self.verify_pfc_enable_status_per_port(p, pfcwd_info, overwrite) + else: + if port not in all_ports: + continue + self.verify_pfc_enable_status_per_port(port, pfcwd_info, overwrite) + + @multi_asic_util.run_on_multi_asic + def start_cmd(self, action, restoration_time, ports, detection_time, pfc_stat_history): + if os.geteuid() != 0: + sys.exit("Root privileges are required for this operation") + pfcwd_info = { 'detection_time': detection_time, } @@ -297,15 +326,10 @@ def start_cmd(self, action, restoration_time, ports, detection_time): "restoration time not defined; default to 2 times " "detection time: {} ms".format(2 * detection_time) ) + if pfc_stat_history: + pfcwd_info["pfc_stat_history"] = "enable" - for port in ports: - if port == "all": - for p in all_ports: - self.verify_pfc_enable_status_per_port(p, pfcwd_info) - else: - if port not in all_ports: - continue - self.verify_pfc_enable_status_per_port(port, pfcwd_info) + self.configure_ports(ports, pfcwd_info, overwrite=True) @multi_asic_util.run_on_multi_asic def interval(self, poll_interval): @@ -391,11 +415,12 @@ def start_default(self): pfcwd_info = { 'detection_time': DEFAULT_DETECTION_TIME * multiply, 'restoration_time': DEFAULT_RESTORATION_TIME * multiply, - 'action': DEFAULT_ACTION + 'action': DEFAULT_ACTION, + 'pfc_stat_history': DEFAULT_PFC_HISTORY_STATUS } for port in active_ports: - self.verify_pfc_enable_status_per_port(port, pfcwd_info) + self.verify_pfc_enable_status_per_port(port, pfcwd_info, overwrite=True) pfcwd_info = {} pfcwd_info['POLL_INTERVAL'] = DEFAULT_POLL_INTERVAL * multiply @@ -423,6 +448,15 @@ def big_red_switch(self, big_red_switch): pfcwd_info ) + @multi_asic_util.run_on_multi_asic + def pfc_stat_history_cmd(self, pfc_stat_history, ports): + if os.geteuid() != 0: + sys.exit("Root privileges are required for this operation") + + pfcwd_info = { + 'pfc_stat_history': pfc_stat_history + } + self.configure_ports(ports, pfcwd_info, overwrite=False) # Show stats class Show(object): @@ -459,20 +493,21 @@ class Start(object): '--action', '-a', type=click.Choice(['drop', 'forward', 'alert']) ) @click.option('--restoration-time', '-r', type=click.IntRange(100, 60000)) + @click.option('--pfc-stat-history', is_flag=True) @click.argument('ports', nargs=-1) @click.argument('detection-time', type=click.IntRange(100, 5000)) @clicommon.pass_db - def start(db, action, restoration_time, ports, detection_time): + def start(db, action, restoration_time, ports, detection_time, pfc_stat_history): """ Start PFC watchdog on port(s). To config all ports, use all as input. Example: - sudo pfcwd start --action drop all 400 --restoration-time 400 + sudo pfcwd start --action drop all 400 --restoration-time 400 --pfc-stat-history enable """ PfcwdCli(db).start( - action, restoration_time, ports, detection_time + action, restoration_time, ports, detection_time, pfc_stat_history ) @@ -525,7 +560,19 @@ def big_red_switch(db, big_red_switch): PfcwdCli(db).big_red_switch(big_red_switch) +# Enable/disable PFC WD PFC_STAT_HISTORY mode +class PfcStatHistory(object): + @cli.command('pfc_stat_history') + @click.argument('pfc_stat_history', type=click.Choice(['enable', 'disable'])) + @click.argument('ports', nargs=-1) + @clicommon.pass_db + def pfc_stat_history(db, pfc_stat_history, ports): + """ Enable/disable PFC Historical Statistics mode on ports""" + PfcwdCli(db).pfc_stat_history(pfc_stat_history, ports) + + def get_pfcwd_clis(): + cli.add_command(PfcStatHistory().pfc_stat_history) cli.add_command(BigRedSwitch().big_red_switch) cli.add_command(CounterPoll().counter_poll) cli.add_command(StartDefault().start_default) diff --git a/tests/mock_tables/asic0/config_db.json b/tests/mock_tables/asic0/config_db.json index 296a2d8ea2..d7cd302bef 100644 --- a/tests/mock_tables/asic0/config_db.json +++ b/tests/mock_tables/asic0/config_db.json @@ -99,22 +99,26 @@ "PFC_WD|Ethernet0": { "action": "drop", "detection_time": "200", - "restoration_time": "200" + "restoration_time": "200", + "pfc_stat_history": "disable" }, "PFC_WD|Ethernet4": { "action": "drop", "detection_time": "200", - "restoration_time": "200" + "restoration_time": "200", + "pfc_stat_history": "disable" }, "PFC_WD|Ethernet-BP0": { "action": "drop", "detection_time": "200", - "restoration_time": "200" + "restoration_time": "200", + "pfc_stat_history": "disable" }, "PFC_WD|Ethernet-BP4": { "action": "drop", "detection_time": "200", - "restoration_time": "200" + "restoration_time": "200", + "pfc_stat_history": "disable" }, "PFC_WD|GLOBAL": { "BIG_RED_SWITCH": "enable", diff --git a/tests/mock_tables/asic1/config_db.json b/tests/mock_tables/asic1/config_db.json index 5c1d9f344c..2169f581f2 100644 --- a/tests/mock_tables/asic1/config_db.json +++ b/tests/mock_tables/asic1/config_db.json @@ -66,12 +66,14 @@ "PFC_WD|Ethernet-BP256": { "action": "drop", "detection_time": "200", - "restoration_time": "200" + "restoration_time": "200", + "pfc_stat_history": "disable" }, "PFC_WD|Ethernet-BP260": { "action": "drop", "detection_time": "200", - "restoration_time": "200" + "restoration_time": "200", + "pfc_stat_history": "disable" }, "PFC_WD|GLOBAL": { "BIG_RED_SWITCH": "enable", diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 9716ed434d..517301c758 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -1835,17 +1835,20 @@ "PFC_WD|Ethernet0": { "action": "drop", "detection_time": "600", - "restoration_time": "600" + "restoration_time": "600", + "pfc_stat_history": "disable" }, "PFC_WD|Ethernet4": { "action": "drop", "detection_time": "600", - "restoration_time": "600" + "restoration_time": "600", + "pfc_stat_history": "disable" }, "PFC_WD|Ethernet8": { "action": "drop", "detection_time": "600", - "restoration_time": "600" + "restoration_time": "600", + "pfc_stat_history": "disable" }, "PFC_WD|GLOBAL": { "POLL_INTERVAL": "600" diff --git a/tests/pfcwd_input/pfcwd_test_vectors.py b/tests/pfcwd_input/pfcwd_test_vectors.py index 127bfaac2b..bb103450d4 100644 --- a/tests/pfcwd_input/pfcwd_test_vectors.py +++ b/tests/pfcwd_input/pfcwd_test_vectors.py @@ -1,80 +1,98 @@ pfcwd_show_config_output="""\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 drop 600 600 -Ethernet4 drop 600 600 -Ethernet8 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 600 600 disable +Ethernet4 drop 600 600 disable +Ethernet8 drop 600 600 disable """ pfcwd_show_start_config_output_pass = """\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 forward 102 101 -Ethernet4 drop 600 600 -Ethernet8 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 forward 102 101 disable +Ethernet4 drop 600 600 disable +Ethernet8 drop 600 600 disable +""" + +pfcwd_show_enable_history_config_output_pass = """\ +Changed polling interval to 600ms + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 600 600 enable +Ethernet4 drop 600 600 disable +Ethernet8 drop 600 600 disable """ pfcwd_show_start_action_forward_output = """\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 forward 302 301 -Ethernet4 forward 302 301 -Ethernet8 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 forward 302 301 disable +Ethernet4 forward 302 301 disable +Ethernet8 drop 600 600 disable """ pfcwd_show_start_action_alert_output = """\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 alert 502 501 -Ethernet4 alert 502 501 -Ethernet8 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 alert 502 501 disable +Ethernet4 alert 502 501 disable +Ethernet8 drop 600 600 disable """ pfcwd_show_start_action_drop_output = """\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 drop 602 601 -Ethernet4 drop 602 601 -Ethernet8 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 602 601 disable +Ethernet4 drop 602 601 disable +Ethernet8 drop 600 600 disable """ pfcwd_show_start_default = """\ Changed polling interval to 200ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 drop 200 200 -Ethernet4 drop 200 200 -Ethernet8 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 200 200 disable +Ethernet4 drop 200 200 disable +Ethernet8 drop 600 600 disable """ -pfcwd_show_start_config_output_fail = """\ +pfcwd_show_start_history_output = """\ +Changed polling interval to 600ms + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 600 601 enable +Ethernet4 drop 600 601 enable +Ethernet8 drop 600 600 disable +""" + +show_pfc_config_invalid_options_fail = """\ Failed to run command, invalid options: Ethernet1000 """ pfcwd_show_config_single_port_output="""\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 600 600 disable """ pfcwd_show_config_multi_port_output="""\ Changed polling interval to 600ms - PORT ACTION DETECTION TIME RESTORATION TIME ---------- -------- ---------------- ------------------ -Ethernet0 drop 600 600 -Ethernet4 drop 600 600 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +--------- -------- ---------------- ------------------ --------- +Ethernet0 drop 600 600 disable +Ethernet4 drop 600 600 disable """ pfcwd_show_config_invalid_port_output="""\ - PORT ACTION DETECTION TIME RESTORATION TIME ------- -------- ---------------- ------------------ + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +------ -------- ---------------- ------------------ --------- """ pfcwd_show_stats_output="""\ @@ -265,14 +283,14 @@ BIG_RED_SWITCH status is enable on asic0 Changed polling interval to 199ms on asic1 BIG_RED_SWITCH status is enable on asic1 - PORT ACTION DETECTION TIME RESTORATION TIME --------------- -------- ---------------- ------------------ - Ethernet0 drop 200 200 - Ethernet4 drop 200 200 - Ethernet-BP0 drop 200 200 - Ethernet-BP4 drop 200 200 -Ethernet-BP256 drop 200 200 -Ethernet-BP260 drop 200 200 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 drop 200 200 disable + Ethernet4 drop 200 200 disable + Ethernet-BP0 drop 200 200 disable + Ethernet-BP4 drop 200 200 disable +Ethernet-BP256 drop 200 200 disable +Ethernet-BP260 drop 200 200 disable """ show_pfc_config_start_pass = """\ @@ -280,14 +298,29 @@ BIG_RED_SWITCH status is enable on asic0 Changed polling interval to 199ms on asic1 BIG_RED_SWITCH status is enable on asic1 - PORT ACTION DETECTION TIME RESTORATION TIME --------------- -------- ---------------- ------------------ - Ethernet0 forward 102 101 - Ethernet4 drop 200 200 - Ethernet-BP0 drop 200 200 - Ethernet-BP4 forward 102 101 -Ethernet-BP256 drop 200 200 -Ethernet-BP260 drop 200 200 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 forward 102 101 disable + Ethernet4 drop 200 200 disable + Ethernet-BP0 drop 200 200 disable + Ethernet-BP4 forward 102 101 disable +Ethernet-BP256 drop 200 200 disable +Ethernet-BP260 drop 200 200 disable +""" + +show_pfc_config_enable_history_pass = """\ +Changed polling interval to 199ms on asic0 +BIG_RED_SWITCH status is enable on asic0 +Changed polling interval to 199ms on asic1 +BIG_RED_SWITCH status is enable on asic1 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 drop 200 200 enable + Ethernet4 drop 200 200 disable + Ethernet-BP0 drop 200 200 disable + Ethernet-BP4 drop 200 200 enable +Ethernet-BP256 drop 200 200 disable +Ethernet-BP260 drop 200 200 disable """ show_pfc_config_start_action_drop_masic = """\ @@ -295,14 +328,14 @@ BIG_RED_SWITCH status is enable on asic0 Changed polling interval to 199ms on asic1 BIG_RED_SWITCH status is enable on asic1 - PORT ACTION DETECTION TIME RESTORATION TIME --------------- -------- ---------------- ------------------ - Ethernet0 drop 302 301 - Ethernet4 drop 302 301 - Ethernet-BP0 drop 302 301 - Ethernet-BP4 drop 302 301 -Ethernet-BP256 drop 302 301 -Ethernet-BP260 drop 200 200 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 drop 302 301 disable + Ethernet4 drop 302 301 disable + Ethernet-BP0 drop 302 301 disable + Ethernet-BP4 drop 302 301 disable +Ethernet-BP256 drop 302 301 disable +Ethernet-BP260 drop 200 200 disable """ show_pfc_config_start_action_alert_masic = """\ @@ -310,14 +343,14 @@ BIG_RED_SWITCH status is enable on asic0 Changed polling interval to 199ms on asic1 BIG_RED_SWITCH status is enable on asic1 - PORT ACTION DETECTION TIME RESTORATION TIME --------------- -------- ---------------- ------------------ - Ethernet0 alert 402 401 - Ethernet4 alert 402 401 - Ethernet-BP0 alert 402 401 - Ethernet-BP4 alert 402 401 -Ethernet-BP256 alert 402 401 -Ethernet-BP260 drop 200 200 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 alert 402 401 disable + Ethernet4 alert 402 401 disable + Ethernet-BP0 alert 402 401 disable + Ethernet-BP4 alert 402 401 disable +Ethernet-BP256 alert 402 401 disable +Ethernet-BP260 drop 200 200 disable """ show_pfc_config_start_action_forward_masic = """\ @@ -325,17 +358,32 @@ BIG_RED_SWITCH status is enable on asic0 Changed polling interval to 199ms on asic1 BIG_RED_SWITCH status is enable on asic1 - PORT ACTION DETECTION TIME RESTORATION TIME --------------- -------- ---------------- ------------------ - Ethernet0 forward 702 701 - Ethernet4 forward 702 701 - Ethernet-BP0 forward 702 701 - Ethernet-BP4 forward 702 701 -Ethernet-BP256 forward 702 701 -Ethernet-BP260 drop 200 200 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 forward 702 701 disable + Ethernet4 forward 702 701 disable + Ethernet-BP0 forward 702 701 disable + Ethernet-BP4 forward 702 701 disable +Ethernet-BP256 forward 702 701 disable +Ethernet-BP260 drop 200 200 disable +""" + +pfcwd_show_start_history_output_masic = """\ +Changed polling interval to 199ms on asic0 +BIG_RED_SWITCH status is enable on asic0 +Changed polling interval to 199ms on asic1 +BIG_RED_SWITCH status is enable on asic1 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 drop 600 601 enable + Ethernet4 drop 600 601 enable + Ethernet-BP0 drop 600 601 enable + Ethernet-BP4 drop 600 601 enable +Ethernet-BP256 drop 600 601 enable +Ethernet-BP260 drop 200 200 disable """ -show_pfc_config_start_fail = """\ +show_pfc_config_invalid_options_fail_masic = """\ Failed to run command, invalid options: Ethernet-500 """ @@ -354,9 +402,9 @@ BIG_RED_SWITCH status is enable on asic0 Changed polling interval to 199ms on asic1 BIG_RED_SWITCH status is enable on asic1 - PORT ACTION DETECTION TIME RESTORATION TIME --------------- -------- ---------------- ------------------ - Ethernet0 drop 200 200 - Ethernet-BP0 drop 200 200 -Ethernet-BP256 drop 200 200 + PORT ACTION DETECTION TIME RESTORATION TIME HISTORY +-------------- -------- ---------------- ------------------ --------- + Ethernet0 drop 200 200 disable + Ethernet-BP0 drop 200 200 disable +Ethernet-BP256 drop 200 200 disable """ diff --git a/tests/pfcwd_test.py b/tests/pfcwd_test.py index 2735cd09df..9d6ff71aaa 100644 --- a/tests/pfcwd_test.py +++ b/tests/pfcwd_test.py @@ -7,7 +7,7 @@ from utilities_common.db import Db -from .pfcwd_input.pfcwd_test_vectors import * +from .pfcwd_input import pfcwd_test_vectors as test_vectors test_path = os.path.dirname(os.path.abspath(__file__)) modules_path = os.path.dirname(test_path) @@ -24,28 +24,28 @@ def setup_class(cls): print("SETUP") def test_pfcwd_show_config(self): - self.executor(testData['pfcwd_show_config']) + self.executor(test_vectors.testData['pfcwd_show_config']) def test_pfcwd_show_config_single_port(self): - self.executor(testData['pfcwd_show_config_single_port']) + self.executor(test_vectors.testData['pfcwd_show_config_single_port']) def test_pfcwd_show_config_multi_port(self): - self.executor(testData['pfcwd_show_config_multi_port']) + self.executor(test_vectors.testData['pfcwd_show_config_multi_port']) def test_pfcwd_show_config_invalid_port(self): - self.executor(testData['pfcwd_show_config_invalid_port']) + self.executor(test_vectors.testData['pfcwd_show_config_invalid_port']) def test_pfcwd_show_stats(self): - self.executor(testData['pfcwd_show_stats']) + self.executor(test_vectors.testData['pfcwd_show_stats']) def test_pfcwd_show_stats_single_queue(self): - self.executor(testData['pfcwd_show_stats_single_queue']) + self.executor(test_vectors.testData['pfcwd_show_stats_single_queue']) def test_pfcwd_show_stats_multi_queue(self): - self.executor(testData['pfcwd_show_stats_multi_queue']) + self.executor(test_vectors.testData['pfcwd_show_stats_multi_queue']) def test_pfcwd_show_stats_invalid_queue(self): - self.executor(testData['pfcwd_show_stats_invalid_queue']) + self.executor(test_vectors.testData['pfcwd_show_stats_invalid_queue']) def executor(self, testcase): import pfcwd.main as pfcwd @@ -93,7 +93,7 @@ def test_pfcwd_start_ports_valid(self, mock_os): obj=db ) print(result.output) - assert result.output == pfcwd_show_config_output + assert result.output == test_vectors.pfcwd_show_config_output mock_os.geteuid.return_value = 0 result = runner.invoke( @@ -114,7 +114,43 @@ def test_pfcwd_start_ports_valid(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == pfcwd_show_start_config_output_pass + assert result.output == test_vectors.pfcwd_show_start_config_output_pass + + @patch('pfcwd.main.os') + def test_pfcwd_enable_history_ports_valid(self, mock_os): + # pfcwd pfc_stat_history enable Ethernet0 + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + # initially history is disabled + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.pfcwd_show_config_output + + mock_os.geteuid.return_value = 0 + result = runner.invoke( + pfcwd.cli.commands["pfc_stat_history"], + [ + "enable", + "Ethernet0" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + + # now valid port is enabled + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + assert result.output == test_vectors.pfcwd_show_enable_history_config_output_pass @patch('pfcwd.main.os') def test_pfcwd_start_actions(self, mock_os): @@ -129,7 +165,7 @@ def test_pfcwd_start_actions(self, mock_os): obj=db ) print(result.output) - assert result.output == pfcwd_show_config_output + assert result.output == test_vectors.pfcwd_show_config_output # always skip Ethernet8 because 'pfc_enable' not configured for this port mock_os.geteuid.return_value = 0 @@ -151,7 +187,7 @@ def test_pfcwd_start_actions(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == pfcwd_show_start_action_forward_output + assert result.output == test_vectors.pfcwd_show_start_action_forward_output result = runner.invoke( pfcwd.cli.commands["start"], @@ -171,7 +207,7 @@ def test_pfcwd_start_actions(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == pfcwd_show_start_action_alert_output + assert result.output == test_vectors.pfcwd_show_start_action_alert_output result = runner.invoke( pfcwd.cli.commands["start"], @@ -191,7 +227,7 @@ def test_pfcwd_start_actions(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == pfcwd_show_start_action_drop_output + assert result.output == test_vectors.pfcwd_show_start_action_drop_output result = runner.invoke( pfcwd.cli.commands["start_default"], @@ -208,8 +244,44 @@ def test_pfcwd_start_actions(self, mock_os): print(result.output) assert result.exit_code == 0 - assert result.output == pfcwd_show_start_default + assert result.output == test_vectors.pfcwd_show_start_default + + @patch('pfcwd.main.os') + def test_pfcwd_start_history(self, mock_os): + # pfcwd start all 600 --restoration-time 601 --pfc-stat-history + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + # initially history disabled on all ports + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.pfcwd_show_config_output + + mock_os.geteuid.return_value = 0 + # start wd with history flag + result = runner.invoke( + pfcwd.cli.commands["start"], + [ + "all", "600", "--restoration-time", "601", + "--pfc-stat-history" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + # get config after the change + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + assert result.output == test_vectors.pfcwd_show_start_history_output @patch('pfcwd.main.os') def test_pfcwd_pfc_not_enabled(self, mock_os): @@ -223,22 +295,61 @@ def test_pfcwd_pfc_not_enabled(self, mock_os): obj=db ) print(result.output) - assert result.output == pfcwd_show_config_output + assert result.output == test_vectors.pfcwd_show_config_output mock_os.geteuid.return_value = 0 result = runner.invoke( pfcwd.cli.commands["start"], [ - "--action", "drop", "--restoration-time", "601", + "--action", "drop", "--restoration-time", "601", "--pfc-stat-history", "Ethernet8", "602" ], obj=db ) print(result.output) assert result.exit_code == 0 - assert pfc_is_not_enabled == result.output + assert test_vectors.pfc_is_not_enabled == result.output + + @patch('pfcwd.main.os') + def test_pfcwd_enable_history_pfc_not_enabled(self, mock_os): + # pfcwd pfc_stat_history enable Ethernet8 + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + # get initial config + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.pfcwd_show_config_output + + # attempt to enable history on Ethernet without pfc + mock_os.geteuid.return_value = 0 + result = runner.invoke( + pfcwd.cli.commands["pfc_stat_history"], + [ + "enable", + "Ethernet8" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + assert test_vectors.pfc_is_not_enabled == result.output + + # verify no change + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + # same as original config + assert result.output == test_vectors.pfcwd_show_config_output def test_pfcwd_start_ports_invalid(self): # pfcwd start --action drop --restoration-time 200 Ethernet0 200 @@ -249,14 +360,51 @@ def test_pfcwd_start_ports_invalid(self): result = runner.invoke( pfcwd.cli.commands["start"], [ - "--action", "forward", "--restoration-time", "101", + "--action", "forward", "--restoration-time", "101", "--pfc-stat-history", "Ethernet1000", "102" ], obj=db ) print(result.output) assert result.exit_code == 1 - assert result.output == pfcwd_show_start_config_output_fail + assert result.output == test_vectors.show_pfc_config_invalid_options_fail + + def test_pfcwd_enable_history_ports_invalid(self): + # pfcwd pfc_stat_history enable Ethernet1000 + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + # get initial config + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.pfcwd_show_config_output + + # attempt to enable history on invalid port + result = runner.invoke( + pfcwd.cli.commands["pfc_stat_history"], + [ + "enable", + "Ethernet1000" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 1 + assert result.output == test_vectors.show_pfc_config_invalid_options_fail + + # config unchanged + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + # same as original config + assert result.output == test_vectors.pfcwd_show_config_output @classmethod def teardown_class(cls): @@ -334,6 +482,17 @@ def test_pfcwd_big_red_switch_nonroot(self): assert result.exit_code == 1 assert result.output == 'Root privileges are required for this operation\n' + @patch('pfcwd.main.os.geteuid', MagicMock(return_value=8)) + def test_pfcwd_pfc_stat_history_nonroot(self): + import pfcwd.main as pfcwd + runner = CliRunner() + result = runner.invoke( + pfcwd.cli.commands['pfc_stat_history'], ['enable', 'all'], + ) + print(result.output) + assert result.exit_code == 1 + assert result.output == 'Root privileges are required for this operation\n' + def test_pfcwd_stats_all(self): import pfcwd.main as pfcwd print(pfcwd.__file__) @@ -343,7 +502,7 @@ def test_pfcwd_stats_all(self): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfcwd_stats_all + assert result.output == test_vectors.show_pfcwd_stats_all def test_pfcwd_stats_with_queues(self): import pfcwd.main as pfcwd @@ -357,7 +516,7 @@ def test_pfcwd_stats_with_queues(self): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfcwd_stats_with_queues + assert result.output == test_vectors.show_pfcwd_stats_with_queues def test_pfcwd_config_all(self): import pfcwd.main as pfcwd @@ -367,7 +526,7 @@ def test_pfcwd_config_all(self): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfc_config_all + assert result.output == test_vectors.show_pfc_config_all def test_pfcwd_config_with_ports(self): import pfcwd.main as pfcwd @@ -378,7 +537,7 @@ def test_pfcwd_config_with_ports(self): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfcwd_config_with_ports + assert result.output == test_vectors.show_pfcwd_config_with_ports @patch('pfcwd.main.os') def test_pfcwd_start_ports_masic_valid(self, mock_os): @@ -392,7 +551,7 @@ def test_pfcwd_start_ports_masic_valid(self, mock_os): obj=db ) print(result.output) - assert result.output == show_pfc_config_all + assert result.output == test_vectors.show_pfc_config_all mock_os.geteuid.return_value = 0 result = runner.invoke( @@ -413,7 +572,42 @@ def test_pfcwd_start_ports_masic_valid(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfc_config_start_pass + assert result.output == test_vectors.show_pfc_config_start_pass + + @patch('pfcwd.main.os') + def test_pfcwd_enable_history_ports_masic_valid(self, mock_os): + # pfcwd pfc_stat_history enable Ethernet0, Ethernet-BP4 + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + # get initial config + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.show_pfc_config_all + + mock_os.geteuid.return_value = 0 + result = runner.invoke( + pfcwd.cli.commands["pfc_stat_history"], + [ + "enable", + "Ethernet0", "Ethernet-BP4" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + + # get config after the change + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + assert result.output == test_vectors.show_pfc_config_enable_history_pass @patch('pfcwd.main.os') def test_pfcwd_start_actions_masic(self, mock_os): @@ -427,7 +621,7 @@ def test_pfcwd_start_actions_masic(self, mock_os): obj=db ) print(result.output) - assert result.output == show_pfc_config_all + assert result.output == test_vectors.show_pfc_config_all # always skip Ethernet-BP260 because 'pfc_enable' not configured for this port mock_os.geteuid.return_value = 0 @@ -449,7 +643,7 @@ def test_pfcwd_start_actions_masic(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfc_config_start_action_drop_masic + assert result.output == test_vectors.show_pfc_config_start_action_drop_masic result = runner.invoke( pfcwd.cli.commands["start"], @@ -469,7 +663,7 @@ def test_pfcwd_start_actions_masic(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfc_config_start_action_alert_masic + assert result.output == test_vectors.show_pfc_config_start_action_alert_masic result = runner.invoke( pfcwd.cli.commands["start"], @@ -489,7 +683,44 @@ def test_pfcwd_start_actions_masic(self, mock_os): ) print(result.output) assert result.exit_code == 0 - assert result.output == show_pfc_config_start_action_forward_masic + assert result.output == test_vectors.show_pfc_config_start_action_forward_masic + + @patch('pfcwd.main.os') + def test_pfcwd_start_history_masic(self, mock_os): + # pfcwd start all 600 --restoration-time 601 --pfc-stat-history + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + # initially history disabled on all ports + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.show_pfc_config_all + + mock_os.geteuid.return_value = 0 + # start wd with history flag + result = runner.invoke( + pfcwd.cli.commands["start"], + [ + "all", "600", "--restoration-time", "601", + "--pfc-stat-history" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + + # get config after the change + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + assert result.output == test_vectors.pfcwd_show_start_history_output_masic def test_pfcwd_start_ports_masic_invalid(self): # --action drop --restoration-time 200 Ethernet0 Ethernet500 200 @@ -500,14 +731,14 @@ def test_pfcwd_start_ports_masic_invalid(self): result = runner.invoke( pfcwd.cli.commands["start"], [ - "--action", "forward", "--restoration-time", "101", + "--action", "forward", "--restoration-time", "101", "--pfc-stat-history", "Ethernet0", "Ethernet-500", "102" ], obj=db ) print(result.output) assert result.exit_code == 1 - assert result.output == show_pfc_config_start_fail + assert result.output == test_vectors.show_pfc_config_invalid_options_fail_masic # get config after the command, config shouldn't change result = runner.invoke( @@ -517,7 +748,35 @@ def test_pfcwd_start_ports_masic_invalid(self): print(result.output) assert result.exit_code == 0 # same as original config - assert result.output == show_pfc_config_all + assert result.output == test_vectors.show_pfc_config_all + + def test_pfcwd_enable_history_ports_masic_invalid(self): + # pfcwd pfc_stat_history enable Ethernet0 Ethernet-500 + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + result = runner.invoke( + pfcwd.cli.commands["pfc_stat_history"], + [ + "enable", + "Ethernet0", "Ethernet-500", + ], + obj=db + ) + print(result.output) + assert result.exit_code == 1 + assert result.output == test_vectors.show_pfc_config_invalid_options_fail_masic + + # get config after the command, config shouldn't change + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + # same as original config + assert result.output == test_vectors.show_pfc_config_all @patch('pfcwd.main.os') def test_pfcwd_pfc_not_enabled_masic(self, mock_os): @@ -529,15 +788,55 @@ def test_pfcwd_pfc_not_enabled_masic(self, mock_os): result = runner.invoke( pfcwd.cli.commands["start"], [ - "--action", "drop", "--restoration-time", "601", + "--action", "drop", "--restoration-time", "601", "--pfc-stat-history", "Ethernet-BP260", "602" ], obj=db ) assert result.exit_code == 0 - assert pfc_is_not_enabled_masic == result.output + assert test_vectors.pfc_is_not_enabled_masic == result.output + + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + + print(result.output) + assert result.exit_code == 0 + # same as original config + assert result.output == test_vectors.show_pfc_config_all + + @patch('pfcwd.main.os') + def test_pfcwd_enable_history_pfc_not_enabled_masic(self, mock_os): + # pfcwd pfc_stat_history enable Ethernet-BP260 + import pfcwd.main as pfcwd + runner = CliRunner() + db = Db() + + # get initial config + result = runner.invoke( + pfcwd.cli.commands["show"].commands["config"], + obj=db + ) + print(result.output) + assert result.output == test_vectors.show_pfc_config_all + + # attempt to enable history on Ethernet without pfc + mock_os.geteuid.return_value = 0 + result = runner.invoke( + pfcwd.cli.commands["pfc_stat_history"], + [ + "enable", + "Ethernet-BP260" + ], + obj=db + ) + print(result.output) + assert result.exit_code == 0 + assert test_vectors.pfc_is_not_enabled_masic == result.output + # verify no change result = runner.invoke( pfcwd.cli.commands["show"].commands["config"], obj=db @@ -546,7 +845,7 @@ def test_pfcwd_pfc_not_enabled_masic(self, mock_os): print(result.output) assert result.exit_code == 0 # same as original config - assert result.output == show_pfc_config_all + assert result.output == test_vectors.show_pfc_config_all @classmethod def teardown_class(cls): From ec01962b2cdc04e4c5f11f23187de800fc2aceb7 Mon Sep 17 00:00:00 2001 From: mihirpat1 <112018033+mihirpat1@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:02:06 -0700 Subject: [PATCH 15/21] sfputil and sfpshow eeprom and DOM CLI enhancement to display data for all CMIS transceivers (#4010) * sfputil and sfpshow eeprom and DOM CLI enhancement to display data for all CMIS transceivers Signed-off-by: Mihir Patel * Created is_transceiver_cmis function * Fixed precommit failure * Added blank line before is_transceiver_cmis * Addressed PR comments --------- Signed-off-by: Mihir Patel --- scripts/sfpshow | 18 +++++++------ sfputil/main.py | 22 ++++++++------- tests/mock_tables/state_db.json | 19 ++++++++++++- tests/sfp_test.py | 48 ++++++++++++++++++++++++++++----- tests/sfputil_test.py | 9 ++++--- utilities_common/sfp_helper.py | 12 +++++++++ 6 files changed, 100 insertions(+), 28 deletions(-) diff --git a/scripts/sfpshow b/scripts/sfpshow index cdc9c168a2..2fca1f13db 100755 --- a/scripts/sfpshow +++ b/scripts/sfpshow @@ -29,6 +29,7 @@ from utilities_common.sfp_helper import ( CCMIS_VDM_TO_LEGACY_STATUS_MAP, CCMIS_VDM_THRESHOLD_TO_LEGACY_DOM_THRESHOLD_MAP ) +from utilities_common.sfp_helper import is_transceiver_cmis from tabulate import tabulate # Mock the redis DB for unit test purposes @@ -109,7 +110,7 @@ QSFP_DOM_CHANNEL_MONITOR_MAP = { 'tx4power': 'TX4Power' } -QSFP_DD_DOM_CHANNEL_MONITOR_MAP = { +CMIS_DOM_CHANNEL_MONITOR_MAP = { 'rx1power': 'RX1Power', 'rx2power': 'RX2Power', 'rx3power': 'RX3Power', @@ -313,7 +314,7 @@ class SFPShow(object): def convert_sfp_info_to_output_string(self, sfp_info_dict, sfp_firmware_info_dict): indent = ' ' * 8 output = '' - is_sfp_cmis = 'cmis_rev' in sfp_info_dict + is_sfp_cmis = is_transceiver_cmis(sfp_info_dict) is_sfp_c_cmis = 'supported_max_tx_power' in sfp_info_dict if is_sfp_c_cmis: @@ -356,7 +357,7 @@ class SFPShow(object): return output # Convert DOM sensor info in DB to CLI output string - def convert_dom_to_output_string(self, sfp_type, dom_info_dict): + def convert_dom_to_output_string(self, sfp_type, is_sfp_cmis, dom_info_dict): indent = ' ' * 8 output_dom = '' channel_threshold_align = 18 @@ -364,12 +365,12 @@ class SFPShow(object): if sfp_type.startswith('QSFP') or sfp_type.startswith('OSFP'): # Channel Monitor - if sfp_type.startswith('QSFP-DD') or sfp_type.startswith('OSFP'): + if is_sfp_cmis: output_dom += (indent + 'ChannelMonitorValues:\n') - sorted_key_table = natsorted(QSFP_DD_DOM_CHANNEL_MONITOR_MAP) + sorted_key_table = natsorted(CMIS_DOM_CHANNEL_MONITOR_MAP) output_channel = self.format_dict_value_to_string( sorted_key_table, dom_info_dict, - QSFP_DD_DOM_CHANNEL_MONITOR_MAP, + CMIS_DOM_CHANNEL_MONITOR_MAP, QSFP_DD_DOM_VALUE_UNIT_MAP) output_dom += output_channel else: @@ -382,7 +383,7 @@ class SFPShow(object): output_dom += output_channel # Channel Threshold - if sfp_type.startswith('QSFP-DD') or sfp_type.startswith('OSFP'): + if is_sfp_cmis: dom_map = SFP_DOM_CHANNEL_THRESHOLD_MAP else: dom_map = QSFP_DOM_CHANNEL_THRESHOLD_MAP @@ -466,6 +467,7 @@ class SFPShow(object): sfp_info_dict = state_db.get_all(state_db.STATE_DB, 'TRANSCEIVER_INFO|{}'.format(interface_name)) sfp_firmware_info_dict = state_db.get_all(state_db.STATE_DB, 'TRANSCEIVER_FIRMWARE_INFO|{}'.format(first_subport)) if sfp_info_dict: + is_sfp_cmis = is_transceiver_cmis(sfp_info_dict) if sfp_info_dict['type'] == RJ45_PORT_TYPE: output = 'SFP EEPROM is not applicable for RJ45 port\n' else: @@ -477,7 +479,7 @@ class SFPShow(object): sfp_type = sfp_info_dict['type'] dom_info_dict = state_db.get_all(state_db.STATE_DB, 'TRANSCEIVER_DOM_SENSOR|{}'.format(first_subport)) or {} dom_info_dict.update(state_db.get_all(state_db.STATE_DB, 'TRANSCEIVER_DOM_THRESHOLD|{}'.format(first_subport)) or {}) - dom_output = self.convert_dom_to_output_string(sfp_type, dom_info_dict) + dom_output = self.convert_dom_to_output_string(sfp_type, is_sfp_cmis, dom_info_dict) output += dom_output else: if is_rj45_port(interface_name): diff --git a/sfputil/main.py b/sfputil/main.py index e5848e750a..a8ccef3271 100644 --- a/sfputil/main.py +++ b/sfputil/main.py @@ -28,6 +28,7 @@ ) from utilities_common.sfp_helper import covert_application_advertisement_to_output_string from utilities_common.sfp_helper import QSFP_DATA_MAP +from utilities_common.sfp_helper import is_transceiver_cmis from tabulate import tabulate from utilities_common.general import load_db_config @@ -171,7 +172,7 @@ 'tx4power': 'TX4Power' } -QSFP_DD_DOM_CHANNEL_MONITOR_MAP = { +CMIS_DOM_CHANNEL_MONITOR_MAP = { 'rx1power': 'RX1Power', 'rx2power': 'RX2Power', 'rx3power': 'RX3Power', @@ -335,9 +336,8 @@ def format_dict_value_to_string(sorted_key_table, def convert_sfp_info_to_output_string(sfp_info_dict): indent = ' ' * 8 output = '' - sfp_type = sfp_info_dict['type'] - # CMIS supported module types include QSFP-DD and OSFP - if sfp_type.startswith('QSFP-DD') or sfp_type.startswith('OSFP'): + is_sfp_cmis = is_transceiver_cmis(sfp_info_dict) + if is_sfp_cmis: sorted_qsfp_data_map_keys = sorted(QSFP_DD_DATA_MAP, key=QSFP_DD_DATA_MAP.get) for key in sorted_qsfp_data_map_keys: if key == 'cable_type': @@ -385,7 +385,7 @@ def convert_sfp_info_to_output_string(sfp_info_dict): # Convert DOM sensor info in DB to CLI output string -def convert_dom_to_output_string(sfp_type, dom_info_dict): +def convert_dom_to_output_string(sfp_type, is_sfp_cmis, dom_info_dict): indent = ' ' * 8 output_dom = '' channel_threshold_align = 18 @@ -393,12 +393,12 @@ def convert_dom_to_output_string(sfp_type, dom_info_dict): if sfp_type.startswith('QSFP') or sfp_type.startswith('OSFP'): # Channel Monitor - if sfp_type.startswith('QSFP-DD') or sfp_type.startswith('OSFP'): + if is_sfp_cmis: output_dom += (indent + 'ChannelMonitorValues:\n') - sorted_key_table = natsorted(QSFP_DD_DOM_CHANNEL_MONITOR_MAP) + sorted_key_table = natsorted(CMIS_DOM_CHANNEL_MONITOR_MAP) output_channel = format_dict_value_to_string( sorted_key_table, dom_info_dict, - QSFP_DD_DOM_CHANNEL_MONITOR_MAP, + CMIS_DOM_CHANNEL_MONITOR_MAP, QSFP_DD_DOM_VALUE_UNIT_MAP) output_dom += output_channel else: @@ -411,7 +411,7 @@ def convert_dom_to_output_string(sfp_type, dom_info_dict): output_dom += output_channel # Channel Threshold - if sfp_type.startswith('QSFP-DD') or sfp_type.startswith('OSFP'): + if is_sfp_cmis: dom_map = SFP_DOM_CHANNEL_THRESHOLD_MAP else: dom_map = QSFP_DOM_CHANNEL_THRESHOLD_MAP @@ -678,6 +678,7 @@ def eeprom(port, dump_dom, namespace): try: xcvr_info = platform_chassis.get_sfp(physical_port).get_transceiver_info() + is_sfp_cmis = is_transceiver_cmis(xcvr_info) except NotImplementedError: click.echo("Sfp.get_transceiver_info() is currently not implemented for this platform") sys.exit(ERROR_NOT_IMPLEMENTED) @@ -710,7 +711,8 @@ def eeprom(port, dump_dom, namespace): click.echo("Sfp.get_transceiver_threshold_info() is currently not implemented for this platform") sys.exit(ERROR_NOT_IMPLEMENTED) - output += convert_dom_to_output_string(xcvr_info['type'], xcvr_dom_info) + output += convert_dom_to_output_string(xcvr_info['type'], + is_sfp_cmis, xcvr_dom_info) output += '\n' diff --git a/tests/mock_tables/state_db.json b/tests/mock_tables/state_db.json index 35d7f1a682..ffdac6e335 100644 --- a/tests/mock_tables/state_db.json +++ b/tests/mock_tables/state_db.json @@ -71,6 +71,7 @@ }, "TRANSCEIVER_INFO|Ethernet8": { "type": "QSFP-DD Double Density 8X Pluggable Transceiver", + "cmis_rev": "5.0", "vendor_rev": "2A", "serial": "INKAO2900002A", "manufacturer": "INNOLIGHT", @@ -85,7 +86,23 @@ "cable_length": "10", "specification_compliance": "Not supported for CMIS cables", "nominal_bit_rate": "Not supported for CMIS cables", - "application_advertisement": "400GAUI-8 C2M (Annex 120E) - Active Cable assembly with BER < 2.6x10^-4\n\t\t\t\t IB EDR (Arch.Spec.Vol.2) - Active Cable assembly with BER < 5x10^-5\n\t\t\t\t IB QDR (Arch.Spec.Vol.2) - Active Cable assembly with BER < 10^-12" + "application_advertisement" : "{1: {'host_electrical_interface_id': '400GAUI-8 C2M (Annex 120E)', 'module_media_interface_id': '400ZR, DWDM, amplified', 'media_lane_count': 1, 'host_lane_count': 8, 'host_lane_assignment_options': 1, 'media_lane_assignment_options': 1}, 2: {'host_electrical_interface_id': '400GAUI-8 C2M (Annex 120E)', 'module_media_interface_id': '400ZR, Single Wavelength, Unamplified', 'media_lane_count': 1, 'host_lane_count': 8, 'host_lane_assignment_options': 1, 'media_lane_assignment_options': 1}, 3: {'host_electrical_interface_id': '100GAUI-2 C2M (Annex 135G)', 'module_media_interface_id': '400ZR, DWDM, amplified', 'media_lane_count': 1, 'host_lane_count': 2, 'host_lane_assignment_options': 85, 'media_lane_assignment_options': 1}}", + "host_lane_count" : "8", + "media_lane_count" : "1", + "active_apsel_hostlane1" : "1", + "active_apsel_hostlane2" : "1", + "active_apsel_hostlane3" : "1", + "active_apsel_hostlane4" : "1", + "active_apsel_hostlane5" : "1", + "active_apsel_hostlane6" : "1", + "active_apsel_hostlane7" : "1", + "active_apsel_hostlane8" : "1", + "hardware_rev" : "X.X", + "media_interface_technology" : "1550 nm DFB" + }, + "TRANSCEIVER_FIRMWARE_INFO|Ethernet8": { + "active_firmware": "2.1.1", + "inactive_firmware": "1.2.3" }, "TRANSCEIVER_DOM_SENSOR|Ethernet8": { "temperature": "44.9883", diff --git a/tests/sfp_test.py b/tests/sfp_test.py index ead28048b8..3e8951196e 100644 --- a/tests/sfp_test.py +++ b/tests/sfp_test.py @@ -74,15 +74,33 @@ test_qsfp_dd_eeprom_with_dom_output = """\ Ethernet8: SFP EEPROM detected - Application Advertisement: 400GAUI-8 C2M (Annex 120E) - Active Cable assembly with BER < 2.6x10^-4 - IB EDR (Arch.Spec.Vol.2) - Active Cable assembly with BER < 5x10^-5 - IB QDR (Arch.Spec.Vol.2) - Active Cable assembly with BER < 10^-12 + Active Firmware: 2.1.1 + Active application selected code assigned to host lane 1: 1 + Active application selected code assigned to host lane 2: 1 + Active application selected code assigned to host lane 3: 1 + Active application selected code assigned to host lane 4: 1 + Active application selected code assigned to host lane 5: 1 + Active application selected code assigned to host lane 6: 1 + Active application selected code assigned to host lane 7: 1 + Active application selected code assigned to host lane 8: 1 + Application Advertisement: 400GAUI-8 C2M (Annex 120E) - Host Assign (0x1) - 400ZR, DWDM, amplified - \ +Media Assign (0x1) + 400GAUI-8 C2M (Annex 120E) - Host Assign (0x1) - 400ZR, Single Wavelength, \ +Unamplified - Media Assign (0x1) + 100GAUI-2 C2M (Annex 135G) - Host Assign (0x55) - 400ZR, DWDM, amplified - \ +Media Assign (0x1) + CMIS Rev: 5.0 Connector: No separable connector Encoding: Not supported for CMIS cables Extended Identifier: Power Class 1(10.0W Max) Extended RateSelect Compliance: Not supported for CMIS cables + Host Lane Count: 8 Identifier: QSFP-DD Double Density 8X Pluggable Transceiver + Inactive Firmware: 1.2.3 Length Cable Assembly(m): 10 + Media Interface Technology: 1550 nm DFB + Media Lane Count: 1 + Module Hardware Rev: X.X Nominal Bit Rate(100Mbs): Not supported for CMIS cables Specification compliance: Not supported for CMIS cables Vendor Date Code(YYYY-MM-DD Lot): 2020-05-22 @@ -234,15 +252,33 @@ test_qsfp_dd_eeprom_output = """\ Ethernet8: SFP EEPROM detected - Application Advertisement: 400GAUI-8 C2M (Annex 120E) - Active Cable assembly with BER < 2.6x10^-4 - IB EDR (Arch.Spec.Vol.2) - Active Cable assembly with BER < 5x10^-5 - IB QDR (Arch.Spec.Vol.2) - Active Cable assembly with BER < 10^-12 + Active Firmware: 2.1.1 + Active application selected code assigned to host lane 1: 1 + Active application selected code assigned to host lane 2: 1 + Active application selected code assigned to host lane 3: 1 + Active application selected code assigned to host lane 4: 1 + Active application selected code assigned to host lane 5: 1 + Active application selected code assigned to host lane 6: 1 + Active application selected code assigned to host lane 7: 1 + Active application selected code assigned to host lane 8: 1 + Application Advertisement: 400GAUI-8 C2M (Annex 120E) - Host Assign (0x1) - 400ZR, DWDM, amplified - \ +Media Assign (0x1) + 400GAUI-8 C2M (Annex 120E) - Host Assign (0x1) - 400ZR, Single Wavelength, \ +Unamplified - Media Assign (0x1) + 100GAUI-2 C2M (Annex 135G) - Host Assign (0x55) - 400ZR, DWDM, amplified - \ +Media Assign (0x1) + CMIS Rev: 5.0 Connector: No separable connector Encoding: Not supported for CMIS cables Extended Identifier: Power Class 1(10.0W Max) Extended RateSelect Compliance: Not supported for CMIS cables + Host Lane Count: 8 Identifier: QSFP-DD Double Density 8X Pluggable Transceiver + Inactive Firmware: 1.2.3 Length Cable Assembly(m): 10 + Media Interface Technology: 1550 nm DFB + Media Lane Count: 1 + Module Hardware Rev: X.X Nominal Bit Rate(100Mbs): Not supported for CMIS cables Specification compliance: Not supported for CMIS cables Vendor Date Code(YYYY-MM-DD Lot): 2020-05-22 diff --git a/tests/sfputil_test.py b/tests/sfputil_test.py index 9fa9a394bb..47163bd6eb 100644 --- a/tests/sfputil_test.py +++ b/tests/sfputil_test.py @@ -265,9 +265,10 @@ def test_convert_sfp_info_to_output_string(self, sfp_info_dict, expected_output) output = sfputil.convert_sfp_info_to_output_string(sfp_info_dict) assert output == expected_output - @pytest.mark.parametrize("sfp_type, dom_info_dict, expected_output", [ + @pytest.mark.parametrize("sfp_type, is_sfp_cmis, dom_info_dict, expected_output", [ ( 'QSFP28 or later', + False, { 'temperature': '41.7539C', 'voltage': '3.2577Volts', @@ -303,6 +304,7 @@ def test_convert_sfp_info_to_output_string(self, sfp_info_dict, expected_output) ), ( 'QSFP-DD Double Density 8X Pluggable Transceiver', + True, { 'temperature': '41.7539C', 'voltage': '3.2577Volts', @@ -358,6 +360,7 @@ def test_convert_sfp_info_to_output_string(self, sfp_info_dict, expected_output) ), ( 'OSFP 8X Pluggable Transceiver', + True, { 'temperature': '41.7539C', 'voltage': '3.2577Volts', @@ -411,8 +414,8 @@ def test_convert_sfp_info_to_output_string(self, sfp_info_dict, expected_output) ModuleThresholdValues: ''' )]) - def test_convert_dom_to_output_string(self, sfp_type, dom_info_dict, expected_output): - output = sfputil.convert_dom_to_output_string(sfp_type, dom_info_dict) + def test_convert_dom_to_output_string(self, sfp_type, is_sfp_cmis, dom_info_dict, expected_output): + output = sfputil.convert_dom_to_output_string(sfp_type, is_sfp_cmis, dom_info_dict) assert output == expected_output def test_get_physical_port_name(self): diff --git a/utilities_common/sfp_helper.py b/utilities_common/sfp_helper.py index fc6a435109..393aeb5abc 100644 --- a/utilities_common/sfp_helper.py +++ b/utilities_common/sfp_helper.py @@ -425,3 +425,15 @@ def covert_application_advertisement_to_output_string(indent, sfp_info_dict): except Exception: output += '{}\n'.format(app_adv_str) return output + + +def is_transceiver_cmis(sfp_info_dict): + """ + Check if the transceiver is CMIS compliant. + If the sfp_info_dict is None, return False. + If 'cmis_rev' is present in the dictionary, return True. + Otherwise, return False. + """ + if sfp_info_dict is None: + return False + return 'cmis_rev' in sfp_info_dict From 1418f218825484551b1f8893ff836d420f0a6135 Mon Sep 17 00:00:00 2001 From: Vinod Kumar <119973184+vikumarks@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:08:42 -0700 Subject: [PATCH 16/21] Added json support intfutil (#3906) What I did Added json output supoort for intfutil -c autoneg -j intfutil -c fec -j intfutil -c status -j intfutil -c description -j intfutil -c tpid -j intfutil -c link_training -j --- scripts/intfutil | 71 ++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/scripts/intfutil b/scripts/intfutil index daf06ab04d..d4972e85d1 100755 --- a/scripts/intfutil +++ b/scripts/intfutil @@ -27,6 +27,7 @@ from tabulate import tabulate from utilities_common import constants from utilities_common import multi_asic as multi_asic_util from utilities_common.intf_filter import parse_interface_in_filter +from utilities_common.netstat import table_as_json from utilities_common.platform_sfputil_helper import is_rj45_port, RJ45_PORT_TYPE from sonic_py_common.interface import get_intf_longname from sonic_py_common import multi_asic @@ -63,6 +64,14 @@ VLAN_SUB_INTERFACE_TYPE = "802.1q-encapsulation" SUB_PORT = "subport" +def display_table(table, header_desc, use_json=False): + sorted_table = natsorted(table) + if use_json: + print(table_as_json(sorted_table, header_desc)) + else: + print(tabulate(sorted_table, header_desc, tablefmt="simple", stralign='right')) + + def get_frontpanel_port_list(config_db): ports_dict = config_db.get_table('PORT') front_panel_ports_list = [] @@ -454,7 +463,7 @@ header_stat_sub_intf = ['Sub port interface', 'Speed', 'MTU', 'Vlan', 'Admin', ' class IntfStatus(object): - def __init__(self, intf_name, namespace_option, display_option): + def __init__(self, intf_name, namespace_option, display_option, use_json=False): """ Class constructor method :param self: @@ -466,6 +475,7 @@ class IntfStatus(object): self.sub_intf_only = False self.intf_name = intf_name self.sub_intf_name = intf_name + self.use_json = use_json self.table = [] self.multi_asic = multi_asic_util.MultiAsic( display_option, namespace_option) @@ -482,11 +492,8 @@ class IntfStatus(object): def display_intf_status(self): self.get_intf_status() - sorted_table = natsorted(self.table) - print(tabulate(sorted_table, - header_stat if not self.sub_intf_only else header_stat_sub_intf, - tablefmt="simple", - stralign='right')) + header_status = header_stat if not self.sub_intf_only else header_stat_sub_intf + display_table(self.table, header_status, self.use_json) def generate_intf_status(self): """ @@ -578,10 +585,11 @@ header_desc = ['Interface', 'Oper', 'Admin', 'Alias', 'Description'] class IntfDescription(object): - def __init__(self, intf_name, namespace_option, display_option): + def __init__(self, intf_name, namespace_option, display_option, use_json=False): self.db = None self.config_db = None self.table = [] + self.use_json = use_json self.multi_asic = multi_asic_util.MultiAsic( display_option, namespace_option) @@ -593,10 +601,7 @@ class IntfDescription(object): def display_intf_description(self): self.get_intf_description() - - # Sorting and tabulating the result table. - sorted_table = natsorted(self.table) - print(tabulate(sorted_table, header_desc, tablefmt="simple", stralign='right')) + display_table(self.table, header_desc, self.use_json) def generate_intf_description(self): """ @@ -637,10 +642,11 @@ header_autoneg = ['Interface', 'Auto-Neg Mode', 'Speed', 'Adv Speeds', 'Rmt Adv class IntfAutoNegStatus(object): - def __init__(self, intf_name, namespace_option, display_option): + def __init__(self, intf_name, namespace_option, display_option, use_json=False): self.db = None self.config_db = None self.table = [] + self.use_json = use_json self.multi_asic = multi_asic_util.MultiAsic( display_option, namespace_option) @@ -652,10 +658,7 @@ class IntfAutoNegStatus(object): def display_autoneg_status(self): self.get_intf_autoneg_status() - - # Sorting and tabulating the result table. - sorted_table = natsorted(self.table) - print(tabulate(sorted_table, header_autoneg, tablefmt="simple", stralign='right')) + display_table(self.table, header_autoneg, self.use_json) def generate_autoneg_status(self): """ @@ -704,7 +707,7 @@ header_tpid = ['Interface', 'Alias', 'Oper', 'Admin', 'TPID'] class IntfTpid(object): - def __init__(self, intf_name, namespace_option, display_option): + def __init__(self, intf_name, namespace_option, display_option, use_json=False): """ Class constructor method :param self: @@ -715,6 +718,7 @@ class IntfTpid(object): self.config_db = None self.intf_name = intf_name self.table = [] + self.use_json = use_json self.multi_asic = multi_asic_util.MultiAsic( display_option, namespace_option) @@ -723,10 +727,7 @@ class IntfTpid(object): def display_intf_tpid(self): self.get_intf_tpid() - - # Sorting and tabulating the result table. - sorted_table = natsorted(self.table) - print(tabulate(sorted_table, header_tpid, tablefmt="simple", stralign='right')) + display_table(self.table, header_tpid, self.use_json) def generate_intf_tpid(self): """ @@ -788,10 +789,11 @@ header_link_training = ['Interface', 'LT Oper', 'LT Admin', 'Oper', 'Admin'] class IntfLinkTrainingStatus(object): - def __init__(self, intf_name, namespace_option, display_option): + def __init__(self, intf_name, namespace_option, display_option, use_json=False): self.db = None self.config_db = None self.table = [] + self.use_json = use_json self.multi_asic = multi_asic_util.MultiAsic( display_option, namespace_option) @@ -802,9 +804,7 @@ class IntfLinkTrainingStatus(object): def display_link_training_status(self): self.get_intf_link_training_status() - # Sorting and tabulating the result table. - sorted_table = natsorted(self.table) - print(tabulate(sorted_table, header_link_training, tablefmt="simple", stralign='right')) + display_table(self.table, header_link_training, self.use_json) @multi_asic_util.run_on_multi_asic def get_intf_link_training_status(self): @@ -847,10 +847,11 @@ header_fec = ['Interface', 'FEC Oper', 'FEC Admin'] class IntfFecStatus(object): - def __init__(self, intf_name, namespace_option, display_option): + def __init__(self, intf_name, namespace_option, display_option, use_json=False): self.db = None self.config_db = None self.table = [] + self.use_json = use_json self.multi_asic = multi_asic_util.MultiAsic( display_option, namespace_option) @@ -861,9 +862,7 @@ class IntfFecStatus(object): def display_fec_status(self): self.get_intf_fec_status() - # Sorting and tabulating the result table. - sorted_table = natsorted(self.table) - print(tabulate(sorted_table, header_fec, tablefmt="simple", stralign='right')) + display_table(self.table, header_fec, self.use_json) @multi_asic_util.run_on_multi_asic def get_intf_fec_status(self): @@ -904,26 +903,26 @@ def main(): formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('-c', '--command', type=str, help='get interface status or description or auto negotiation status or tpid', default=None) parser.add_argument('-i', '--interface', type=str, help='interface information for specific port: Ethernet0', default=None) + parser.add_argument('-j', '--json', action='store_true', help='Display in JSON format') parser = multi_asic_util.multi_asic_args(parser) args = parser.parse_args() - if args.command == "status": - interface_stat = IntfStatus(args.interface, args.namespace, args.display) + interface_stat = IntfStatus(args.interface, args.namespace, args.display, args.json) interface_stat.display_intf_status() elif args.command == "description": - interface_desc = IntfDescription(args.interface, args.namespace, args.display) + interface_desc = IntfDescription(args.interface, args.namespace, args.display, args.json) interface_desc.display_intf_description() elif args.command == "autoneg": - interface_autoneg_status = IntfAutoNegStatus(args.interface, args.namespace, args.display) + interface_autoneg_status = IntfAutoNegStatus(args.interface, args.namespace, args.display, args.json) interface_autoneg_status.display_autoneg_status() elif args.command == "tpid": - interface_tpid = IntfTpid(args.interface, args.namespace, args.display) + interface_tpid = IntfTpid(args.interface, args.namespace, args.display, args.json) interface_tpid.display_intf_tpid() elif args.command == "link_training": - interface_lt_status = IntfLinkTrainingStatus(args.interface, args.namespace, args.display) + interface_lt_status = IntfLinkTrainingStatus(args.interface, args.namespace, args.display, args.json) interface_lt_status.display_link_training_status() elif args.command == "fec": - interface_fec_status = IntfFecStatus(args.interface, args.namespace, args.display) + interface_fec_status = IntfFecStatus(args.interface, args.namespace, args.display, args.json) interface_fec_status.display_fec_status() sys.exit(0) From 53477577fde782bc7169afc75a83346bc7184330 Mon Sep 17 00:00:00 2001 From: Jianquan Ye Date: Sat, 9 Aug 2025 04:29:29 +1000 Subject: [PATCH 17/21] Revert "[SPM] Rename the variable tag to docker-image-reference (#3998)" (#4024) Reverts #3998 to fix build issue. Details in #3998 --- sonic_package_manager/database.py | 11 +++++------ sonic_package_manager/service_creator/creator.py | 2 +- sonic_package_manager/source.py | 6 +++--- tests/sonic_package_manager/conftest.py | 2 +- tests/sonic_package_manager/test_database.py | 6 +++--- tests/sonic_package_manager/test_manager.py | 12 ++++++------ 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/sonic_package_manager/database.py b/sonic_package_manager/database.py index 3bfcf69b9d..11f7b305d6 100644 --- a/sonic_package_manager/database.py +++ b/sonic_package_manager/database.py @@ -31,8 +31,7 @@ class PackageEntry: built_in: Boolean flag whether the package is built in. image_id: Image ID for this package or None if package is not installed. - docker_image_reference: Docker image reference for this package or None if package - is not installed. + tag: Tag for this package or None if package is not installed. """ name: str @@ -43,7 +42,7 @@ class PackageEntry: installed: bool = False built_in: bool = False image_id: Optional[str] = None - docker_image_reference: Optional[str] = None + tag: Optional[str] = None def package_from_dict(name: str, package_info: Dict) -> PackageEntry: @@ -58,10 +57,10 @@ def package_from_dict(name: str, package_info: Dict) -> PackageEntry: installed = package_info.get('installed', False) built_in = package_info.get('built-in', False) image_id = package_info.get('image-id') - docker_image_reference = package_info.get('docker-image-reference') + tag = package_info.get('tag') return PackageEntry(name, repository, description, default_reference, version, installed, - built_in, image_id, docker_image_reference) + built_in, image_id, tag) def package_to_dict(package: PackageEntry) -> Dict: @@ -75,7 +74,7 @@ def package_to_dict(package: PackageEntry) -> Dict: 'installed': package.installed, 'built-in': package.built_in, 'image-id': package.image_id, - 'docker-image-reference': package.docker_image_reference, + 'tag': package.tag, } diff --git a/sonic_package_manager/service_creator/creator.py b/sonic_package_manager/service_creator/creator.py index 857b6365a9..815055c224 100644 --- a/sonic_package_manager/service_creator/creator.py +++ b/sonic_package_manager/service_creator/creator.py @@ -283,7 +283,7 @@ def generate_container_mgmt(self, package: Package): 'docker_container_name': name, 'docker_image_id': image_id, 'docker_image_name': package.entry.repository, - 'docker_image_reference': package.entry.docker_image_reference, + 'docker_image_tag': package.entry.tag, 'docker_image_run_opt': run_opt, 'sonic_asic_platform': sonic_asic_platform } diff --git a/sonic_package_manager/source.py b/sonic_package_manager/source.py index 9ff04abbc5..37c6cb3928 100644 --- a/sonic_package_manager/source.py +++ b/sonic_package_manager/source.py @@ -51,10 +51,10 @@ def install(self, package: Package): image = self.install_image(package) package.entry.image_id = image.id - if image.docker_image_references: - package.entry.docker_image_reference = image.docker_image_references[0] + if image.tags: + package.entry.tag = image.tags[0] else: - package.entry.docker_image_reference = image.id + package.entry.tag = image.id # if no repository is defined for this package # get repository from image diff --git a/tests/sonic_package_manager/conftest.py b/tests/sonic_package_manager/conftest.py index 2d16e43a0d..e8c616d3f0 100644 --- a/tests/sonic_package_manager/conftest.py +++ b/tests/sonic_package_manager/conftest.py @@ -26,7 +26,7 @@ def mock_docker_api(): @dataclass class Image: id: str - docker_image_references: list[str] + tags: list[str] @property def attrs(self): diff --git a/tests/sonic_package_manager/test_database.py b/tests/sonic_package_manager/test_database.py index 6f87aeee36..2b8380835d 100644 --- a/tests/sonic_package_manager/test_database.py +++ b/tests/sonic_package_manager/test_database.py @@ -99,7 +99,7 @@ def test_package_from_dict(): 'installed': True, 'built-in': False, 'image-id': 'abc123', - 'docker-image-reference': 'latest' + 'tag': 'latest' } package = package_from_dict('test-package', package_info) @@ -112,7 +112,7 @@ def test_package_from_dict(): assert package.installed is True assert package.built_in is False assert package.image_id == 'abc123' - assert package.docker_image_reference == 'latest' + assert package.tag == 'latest' def test_package_from_dict_minimal(): @@ -131,4 +131,4 @@ def test_package_from_dict_minimal(): assert package.installed is False assert package.built_in is False assert package.image_id is None - assert package.docker_image_reference is None + assert package.tag is None diff --git a/tests/sonic_package_manager/test_manager.py b/tests/sonic_package_manager/test_manager.py index c97afc771a..5c236dc869 100644 --- a/tests/sonic_package_manager/test_manager.py +++ b/tests/sonic_package_manager/test_manager.py @@ -602,13 +602,13 @@ def test_download_file_sftp(package_manager): ) -def test_installation_from_file_no_image_references(package_manager, mock_docker_api, sonic_fs): - # Override the load function to return an image without image references - def load_no_image_references(filename): +def test_installation_from_file_no_tags(package_manager, mock_docker_api, sonic_fs): + # Override the load function to return an image without tags + def load_no_tags(filename): class Image: def __init__(self, id): self.id = id - self.docker_image_references = [] + self.tags = [] @property def attrs(self): @@ -616,7 +616,7 @@ def attrs(self): return Image(filename) - mock_docker_api.load = MagicMock(side_effect=load_no_image_references) + mock_docker_api.load = MagicMock(side_effect=load_no_tags) sonic_fs.create_file('Azure/docker-test:1.6.0') package_manager.install(tarball='Azure/docker-test:1.6.0') @@ -626,4 +626,4 @@ def attrs(self): # Get the package from the database and verify the tag was set to the image ID package = package_manager.database.get_package('test-package') - assert package.docker_image_reference == 'Azure/docker-test:1.6.0' + assert package.tag == 'Azure/docker-test:1.6.0' From 868189ccd73278869583e2a9dca326cad82b0b8e Mon Sep 17 00:00:00 2001 From: Vinod Kumar <119973184+vikumarks@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:15:16 -0700 Subject: [PATCH 18/21] Pr json support queue and priority-group watermark and persistent-watermark (#3875) What I did Added json output supoort for admin@sonic:~$ show queue watermark unicast --help Usage: show queue watermark unicast [OPTIONS] Show user WM for unicast queues Options: -j, --json Display1 output in JSON format [default: False] -?, -h, --help Show this message and exit. admin@sonic:~$ show queue watermark multicast --help Usage: show queue watermark multicast [OPTIONS] Show user WM for multicast queues Options: -j, --json Display1 output in JSON format [default: False] -?, -h, --help Show this message and exit. --- scripts/watermarkstat | 25 +- show/main.py | 50 +- tests/watermarkstat_test.py | 30 + tests/wm_input/wm_test_vectors.py | 983 +++++++++++++++++++++++------- 4 files changed, 844 insertions(+), 244 deletions(-) diff --git a/scripts/watermarkstat b/scripts/watermarkstat index 70ea853bc4..d47c1abdd5 100755 --- a/scripts/watermarkstat +++ b/scripts/watermarkstat @@ -81,13 +81,13 @@ class WatermarkstatWrapper(object): self.db = None @multi_asic_util.run_on_multi_asic - def run(self, clear, persistent, wm_type): + def run(self, clear, persistent, wm_type, json_output): watermarkstat = Watermarkstat(self.db, self.multi_asic.current_namespace) if clear: watermarkstat.send_clear_notification(("PERSISTENT" if persistent else "USER", wm_type.upper())) else: table_prefix = PERSISTENT_TABLE_PREFIX if persistent else USER_TABLE_PREFIX - watermarkstat.print_all_stat(table_prefix, wm_type) + watermarkstat.print_all_stat(table_prefix, wm_type, json_output) class Watermarkstat(object): @@ -283,8 +283,9 @@ class Watermarkstat(object): fields[pos] = str(int(counter_data)) return fields - def print_all_stat(self, table_prefix, key): + def print_all_stat(self, table_prefix, key, json_output): table = [] + json_result = [] type = self.watermark_types[key] if key in ['buffer_pool', 'headroom_pool']: self.header_list = type['header'] @@ -298,6 +299,7 @@ class Watermarkstat(object): if data is None: data = STATUS_NA table.append((buf_pool, data)) + json_result.append({buf_pool:data}) else: self.build_header(type, key) # Get stat for each port @@ -309,10 +311,14 @@ class Watermarkstat(object): row_data.append(port) row_data.extend(data) table.append(tuple(row_data)) - - namespace_str = f" (Namespace {self.namespace})" if multi_asic.is_multi_asic() else '' - print(type["message"] + namespace_str) - print(tabulate(table, self.header_list, tablefmt='simple', stralign='right')) + json_result.append(dict(zip(self.header_list, [port]+data))) + + if json_output: + print(json.dumps(json_result, indent=4)) + else: + namespace_str = f" (Namespace {self.namespace})" if multi_asic.is_multi_asic() else '' + print(type["message"] + namespace_str) + print(tabulate(table, self.header_list, tablefmt='simple', stralign='right')) def send_clear_notification(self, data): msg = json.dumps(data, separators=(',', ':')) @@ -324,8 +330,9 @@ class Watermarkstat(object): @click.option('-p', '--persistent', is_flag=True, help='Do the operations on the persistent watermark') @click.option('-t', '--type', 'wm_type', type=click.Choice(['pg_headroom', 'pg_shared', 'q_shared_uni', 'q_shared_multi', 'buffer_pool', 'headroom_pool', 'q_shared_all']), help='The type of watermark', required=True) @click.option('-n', '--namespace', type=click.Choice(multi_asic.get_namespace_list()), help='Namespace name or skip for all', default=None) +@click.option('--json','-j','json_output', is_flag=True, default=False, show_default=True, help="Display output in JSON format") @click.version_option(version='1.0') -def main(clear, persistent, wm_type, namespace): +def main(clear, persistent, wm_type, namespace, json_output=False): """ Display the watermark counters @@ -347,7 +354,7 @@ def main(clear, persistent, wm_type, namespace): """ namespace_context = WatermarkstatWrapper(namespace) - namespace_context.run(clear, persistent, wm_type) + namespace_context.run(clear, persistent, wm_type, json_output) sys.exit(0) if __name__ == "__main__": diff --git a/show/main.py b/show/main.py index 2e60c814c9..c748e96cb4 100755 --- a/show/main.py +++ b/show/main.py @@ -842,11 +842,14 @@ def watermark(): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def wm_q_uni(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def wm_q_uni(namespace, json_output): """Show user WM for unicast queues""" command = ['watermarkstat', '-t', 'q_shared_uni'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) # 'multicast' subcommand ("show queue watermarks multicast") @@ -859,11 +862,14 @@ def wm_q_uni(namespace): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def wm_q_multi(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def wm_q_multi(namespace, json_output): """Show user WM for multicast queues""" command = ['watermarkstat', '-t', 'q_shared_multi'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) # 'all' subcommand ("show queue watermarks all") @@ -876,11 +882,14 @@ def wm_q_multi(namespace): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def wm_q_all(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def wm_q_all(namespace, json_output): """Show user WM for all queues""" command = ['watermarkstat', '-t', 'q_shared_all'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) # @@ -902,11 +911,14 @@ def persistent_watermark(): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def pwm_q_uni(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def pwm_q_uni(namespace, json_output): """Show persistent WM for unicast queues""" command = ['watermarkstat', '-p', '-t', 'q_shared_uni'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) # 'multicast' subcommand ("show queue persistent-watermarks multicast") @@ -919,11 +931,14 @@ def pwm_q_uni(namespace): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def pwm_q_multi(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def pwm_q_multi(namespace, json_output): """Show persistent WM for multicast queues""" command = ['watermarkstat', '-p', '-t', 'q_shared_multi'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) # 'all' subcommand ("show queue persistent-watermarks all") @@ -936,11 +951,14 @@ def pwm_q_multi(namespace): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def pwm_q_all(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def pwm_q_all(namespace, json_output): """Show persistent WM for all queues""" command = ['watermarkstat', '-p', '-t', 'q_shared_all'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) # @@ -965,11 +983,14 @@ def watermark(): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def wm_pg_headroom(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def wm_pg_headroom(namespace, json_output): """Show user headroom WM for pg""" command = ['watermarkstat', '-t', 'pg_headroom'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) @watermark.command('shared') @@ -981,11 +1002,14 @@ def wm_pg_headroom(namespace): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def wm_pg_shared(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def wm_pg_shared(namespace, json_output): """Show user shared WM for pg""" command = ['watermarkstat', '-t', 'pg_shared'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) @priority_group.group() @@ -1016,11 +1040,14 @@ def persistent_watermark(): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def pwm_pg_headroom(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def pwm_pg_headroom(namespace, json_output): """Show persistent headroom WM for pg""" command = ['watermarkstat', '-p', '-t', 'pg_headroom'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) @@ -1033,11 +1060,14 @@ def pwm_pg_headroom(namespace): show_default=True, help='Namespace name or all', callback=multi_asic_util.multi_asic_namespace_validation_callback) -def pwm_pg_shared(namespace): +@click.option('--json', '-j', 'json_output', is_flag=True, default=False, show_default=True, help="Display JSON output") +def pwm_pg_shared(namespace, json_output): """Show persistent shared WM for pg""" command = ['watermarkstat', '-p', '-t', 'pg_shared'] if namespace is not None: command += ['-n', str(namespace)] + if json_output: + command += ["-j"] run_command(command) diff --git a/tests/watermarkstat_test.py b/tests/watermarkstat_test.py index 6a2ebfa2cf..17050e698e 100644 --- a/tests/watermarkstat_test.py +++ b/tests/watermarkstat_test.py @@ -31,21 +31,36 @@ def setup_class(cls): def test_show_pg_shared_wm(self): self.executor(testData['show_pg_wm_shared']) + def test_show_pg_shared_wm_json(self): + self.executor(testData['show_pg_wm_shared_json']) + def test_show_pg_headroom_wm(self): self.executor(testData['show_pg_wm_hdrm']) + def test_show_pg_headroom_wm_json(self): + self.executor(testData['show_pg_wm_hdrm_json']) + def test_show_queue_unicast_wm(self): self.executor(testData['show_q_wm_unicast']) + def test_show_queue_unicast_wm_json(self): + self.executor(testData['show_q_wm_unicast_json']) + def test_show_queue_multicast_wm(self): self.executor(testData['show_q_wm_multicast']) + def test_show_queue_multicast_wm_json(self): + self.executor(testData['show_q_wm_multicast_json']) + def test_show_queue_multicast_wm_neg(self, q_multicast_wm_neg): self.executor(testData['show_q_wm_multicast_neg']) def test_show_queue_all_wm(self): self.executor(testData['show_q_wm_all']) + def test_show_queue_all_wm_json(self): + self.executor(testData['show_q_wm_all_json']) + def test_show_buffer_pool_wm(self): self.executor(testData['show_buffer_pool_wm']) @@ -55,18 +70,33 @@ def test_show_headroom_pool_wm(self): def test_show_pg_shared_peristent_wm(self): self.executor(testData['show_pg_pwm_shared']) + def test_show_pg_shared_peristent_wm_json(self): + self.executor(testData['show_pg_pwm_shared_json']) + def test_show_pg_headroom_persistent_wm(self): self.executor(testData['show_pg_pwm_hdrm']) + def test_show_pg_headroom_persistent_wm_json(self): + self.executor(testData['show_pg_pwm_hdrm_json']) + def test_show_queue_unicast_persistent_wm(self): self.executor(testData['show_q_pwm_unicast']) + def test_show_queue_unicast_persistent_wm_json(self): + self.executor(testData['show_q_pwm_unicast_json']) + def test_show_queue_multicast_persistent_wm(self): self.executor(testData['show_q_pwm_multicast']) + def test_show_queue_multicast_persistent_wm_json(self): + self.executor(testData['show_q_pwm_multicast_json']) + def test_show_queue_all_persistent_wm(self): self.executor(testData['show_q_pwm_all']) + def test_show_queue_all_persistent_wm_json(self): + self.executor(testData['show_q_pwm_all_json']) + def test_show_buffer_pool_persistent_wm(self): self.executor(testData['show_buffer_pool_pwm']) diff --git a/tests/wm_input/wm_test_vectors.py b/tests/wm_input/wm_test_vectors.py index f0a80cf9cb..74126db3e1 100644 --- a/tests/wm_input/wm_test_vectors.py +++ b/tests/wm_input/wm_test_vectors.py @@ -11,7 +11,8 @@ "Ethernet-BP0 0 0 0 0 0 0 0 0" " 0 0 0 0 0 0 0 0\n" "Ethernet-BP4 0 0 0 0 0 0 0 0" - " 0 0 0 0 0 0 0 0\n") + " 0 0 0 0 0 0 0 0\n" +) show_pg_wm_shared_output_all_masic = ( "Ingress shared pool occupancy per PG: (Namespace asic0)\n" @@ -35,7 +36,8 @@ "Ethernet-BP256 100 101 102 103 104 105 106 107" " 108 109 110 111 112 113 114 115\n" "Ethernet-BP260 200 201 202 203 204 205 206 207" - " 208 209 210 211 212 213 214 215\n") + " 208 209 210 211 212 213 214 215\n" +) show_pg_wm_hdrm_output_one_masic = ( "Ingress headroom per PG: (Namespace asic1)\n" @@ -46,7 +48,8 @@ "Ethernet-BP256 100 101 102 103 104 105 106 107" " 108 109 110 111 112 113 114 115\n" "Ethernet-BP260 200 201 202 203 204 205 206 207" - " 208 209 210 211 212 213 214 215\n") + " 208 209 210 211 212 213 214 215\n" +) show_pg_wm_hdrm_output_all_masic = ( "Ingress headroom per PG: (Namespace asic0)\n" @@ -70,7 +73,8 @@ "Ethernet-BP256 100 101 102 103 104 105 106 107" " 108 109 110 111 112 113 114 115\n" "Ethernet-BP260 200 201 202 203 204 205 206 207" - " 208 209 210 211 212 213 214 215\n") + " 208 209 210 211 212 213 214 215\n" +) show_pg_persistent_wm_shared_output_one_masic = ( "Ingress shared pool occupancy per PG: (Namespace asic1)\n" @@ -81,7 +85,8 @@ "Ethernet-BP256 200 201 202 203 204 205 206 207" " 500 501 502 503 504 505 506 507\n" "Ethernet-BP260 N/A N/A N/A N/A N/A N/A N/A N/A" - " N/A N/A N/A N/A N/A N/A N/A N/A\n") + " N/A N/A N/A N/A N/A N/A N/A N/A\n" +) show_pg_persistent_wm_shared_output_all_masic = ( "Ingress shared pool occupancy per PG: (Namespace asic0)\n" @@ -105,7 +110,8 @@ "Ethernet-BP256 200 201 202 203 204 205 206 207" " 500 501 502 503 504 505 506 507\n" "Ethernet-BP260 N/A N/A N/A N/A N/A N/A N/A N/A" - " N/A N/A N/A N/A N/A N/A N/A N/A\n") + " N/A N/A N/A N/A N/A N/A N/A N/A\n" +) show_pg_persistent_wm_hdrm_output_one_masic = ( "Ingress headroom per PG: (Namespace asic1)\n" @@ -116,7 +122,8 @@ "Ethernet-BP256 200 201 202 203 204 205 206 207" " 500 501 502 503 504 505 506 507\n" "Ethernet-BP260 N/A N/A N/A N/A N/A N/A N/A N/A" - " N/A N/A N/A N/A N/A N/A N/A N/A\n") + " N/A N/A N/A N/A N/A N/A N/A N/A\n" +) show_pg_persistent_wm_hdrm_output_all_masic = ( "Ingress headroom per PG: (Namespace asic0)\n" @@ -140,7 +147,8 @@ "Ethernet-BP256 200 201 202 203 204 205 206 207 " "500 501 502 503 504 505 506 507\n" "Ethernet-BP260 N/A N/A N/A N/A N/A N/A N/A N/A " - "N/A N/A N/A N/A N/A N/A N/A N/A\n") + "N/A N/A N/A N/A N/A N/A N/A N/A\n" +) show_queue_wm_unicast_output_one_masic = """\ Egress shared pool occupancy per unicast queue: (Namespace asic1) @@ -368,7 +376,7 @@ Message published to WATERMARK_CLEAR_REQUEST: ["USER","PG_HEADROOM"] """ -show_pg_wm_shared_output="""\ +show_pg_wm_shared_output = """\ Ingress shared pool occupancy per PG: Port PG0 PG1 PG2 PG3 PG4 PG5 PG6 PG7 --------- ----- ----- ----- ----- ----- ----- ----- ----- @@ -377,7 +385,45 @@ Ethernet8 800 801 802 803 804 805 806 807 """ -show_pg_wm_hdrm_output="""\ +show_pg_wm_shared_output_json = """\ +[ + { + "Port": "Ethernet0", + "PG0": "100", + "PG1": "101", + "PG2": "102", + "PG3": "103", + "PG4": "104", + "PG5": "105", + "PG6": "106", + "PG7": "107" + }, + { + "Port": "Ethernet4", + "PG0": "400", + "PG1": "401", + "PG2": "402", + "PG3": "403", + "PG4": "404", + "PG5": "405", + "PG6": "406", + "PG7": "407" + }, + { + "Port": "Ethernet8", + "PG0": "800", + "PG1": "801", + "PG2": "802", + "PG3": "803", + "PG4": "804", + "PG5": "805", + "PG6": "806", + "PG7": "807" + } +] +""" + +show_pg_wm_hdrm_output = """\ Ingress headroom per PG: Port PG0 PG1 PG2 PG3 PG4 PG5 PG6 PG7 --------- ----- ----- ----- ----- ----- ----- ----- ----- @@ -386,7 +432,45 @@ Ethernet8 800 801 802 803 804 805 806 807 """ -show_pg_persistent_wm_shared_output="""\ +show_pg_wm_hdrm_output_json = """\ +[ + { + "Port": "Ethernet0", + "PG0": "100", + "PG1": "101", + "PG2": "102", + "PG3": "103", + "PG4": "104", + "PG5": "105", + "PG6": "106", + "PG7": "107" + }, + { + "Port": "Ethernet4", + "PG0": "400", + "PG1": "401", + "PG2": "402", + "PG3": "403", + "PG4": "404", + "PG5": "405", + "PG6": "406", + "PG7": "407" + }, + { + "Port": "Ethernet8", + "PG0": "800", + "PG1": "801", + "PG2": "802", + "PG3": "803", + "PG4": "804", + "PG5": "805", + "PG6": "806", + "PG7": "807" + } +] +""" + +show_pg_persistent_wm_shared_output = """\ Ingress shared pool occupancy per PG: Port PG0 PG1 PG2 PG3 PG4 PG5 PG6 PG7 --------- ----- ----- ----- ----- ----- ----- ----- ----- @@ -395,7 +479,45 @@ Ethernet8 900 901 902 903 904 905 906 907 """ -show_pg_persistent_wm_hdrm_output="""\ +show_pg_persistent_wm_shared_output_json = """\ +[ + { + "Port": "Ethernet0", + "PG0": "200", + "PG1": "201", + "PG2": "202", + "PG3": "203", + "PG4": "204", + "PG5": "205", + "PG6": "206", + "PG7": "207" + }, + { + "Port": "Ethernet4", + "PG0": "500", + "PG1": "501", + "PG2": "502", + "PG3": "503", + "PG4": "504", + "PG5": "505", + "PG6": "506", + "PG7": "507" + }, + { + "Port": "Ethernet8", + "PG0": "900", + "PG1": "901", + "PG2": "902", + "PG3": "903", + "PG4": "904", + "PG5": "905", + "PG6": "906", + "PG7": "907" + } +] +""" + +show_pg_persistent_wm_hdrm_output = """\ Ingress headroom per PG: Port PG0 PG1 PG2 PG3 PG4 PG5 PG6 PG7 --------- ----- ----- ----- ----- ----- ----- ----- ----- @@ -404,7 +526,45 @@ Ethernet8 900 901 902 903 904 905 906 907 """ -show_queue_wm_unicast_output="""\ +show_pg_persistent_wm_hdrm_output_json = """\ +[ + { + "Port": "Ethernet0", + "PG0": "200", + "PG1": "201", + "PG2": "202", + "PG3": "203", + "PG4": "204", + "PG5": "205", + "PG6": "206", + "PG7": "207" + }, + { + "Port": "Ethernet4", + "PG0": "500", + "PG1": "501", + "PG2": "502", + "PG3": "503", + "PG4": "504", + "PG5": "505", + "PG6": "506", + "PG7": "507" + }, + { + "Port": "Ethernet8", + "PG0": "900", + "PG1": "901", + "PG2": "902", + "PG3": "903", + "PG4": "904", + "PG5": "905", + "PG6": "906", + "PG7": "907" + } +] +""" + +show_queue_wm_unicast_output = """\ Egress shared pool occupancy per unicast queue: Port UC0 UC1 UC2 UC3 UC4 UC5 UC6 UC7 UC8 UC9 --------- ------- ------- ----- ----- ----- ----- ----- ----- ----- ----- @@ -413,7 +573,51 @@ Ethernet8 0 0 1040 0 0 0 0 0 8528 7696 """ -show_queue_pwm_unicast_output="""\ +show_queue_wm_unicast_output_json = """\ +[ + { + "Port": "Ethernet0", + "UC0": "2057328", + "UC1": "2056704", + "UC2": "0", + "UC3": "0", + "UC4": "0", + "UC5": "0", + "UC6": "0", + "UC7": "2704", + "UC8": "416", + "UC9": "20" + }, + { + "Port": "Ethernet4", + "UC0": "0", + "UC1": "0", + "UC2": "0", + "UC3": "1986", + "UC4": "2567", + "UC5": "0", + "UC6": "0", + "UC7": "0", + "UC8": "0", + "UC9": "0" + }, + { + "Port": "Ethernet8", + "UC0": "0", + "UC1": "0", + "UC2": "1040", + "UC3": "0", + "UC4": "0", + "UC5": "0", + "UC6": "0", + "UC7": "0", + "UC8": "8528", + "UC9": "7696" + } +] +""" + +show_queue_pwm_unicast_output = """\ Egress shared pool occupancy per unicast queue: Port UC0 UC1 UC2 UC3 UC4 UC5 UC6 UC7 UC8 UC9 --------- ------- ------- ----- ----- ----- ----- ----- ----- ----- ----- @@ -422,7 +626,51 @@ Ethernet8 0 0 2040 0 0 0 0 0 9528 8696 """ -show_queue_wm_multicast_output="""\ +show_queue_pwm_unicast_output_json = """\ +[ + { + "Port": "Ethernet0", + "UC0": "3057328", + "UC1": "3056704", + "UC2": "0", + "UC3": "0", + "UC4": "0", + "UC5": "0", + "UC6": "0", + "UC7": "3704", + "UC8": "516", + "UC9": "30" + }, + { + "Port": "Ethernet4", + "UC0": "0", + "UC1": "0", + "UC2": "0", + "UC3": "2986", + "UC4": "3567", + "UC5": "0", + "UC6": "0", + "UC7": "0", + "UC8": "0", + "UC9": "0" + }, + { + "Port": "Ethernet8", + "UC0": "0", + "UC1": "0", + "UC2": "2040", + "UC3": "0", + "UC4": "0", + "UC5": "0", + "UC6": "0", + "UC7": "0", + "UC8": "9528", + "UC9": "8696" + } +] +""" + +show_queue_wm_multicast_output = """\ Egress shared pool occupancy per multicast queue: Port MC10 MC11 MC12 MC13 MC14 MC15 MC16 MC17 MC18 MC19 --------- ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ @@ -431,11 +679,56 @@ Ethernet8 N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A """ -show_queue_wm_multicast_neg_output="""\ +show_queue_wm_multicast_output_json = """\ +[ + { + "Port": "Ethernet0", + "MC10": "N/A", + "MC11": "N/A", + "MC12": "N/A", + "MC13": "N/A", + "MC14": "N/A", + "MC15": "N/A", + "MC16": "N/A", + "MC17": "N/A", + "MC18": "N/A", + "MC19": "N/A" + }, + { + "Port": "Ethernet4", + "MC10": "N/A", + "MC11": "N/A", + "MC12": "N/A", + "MC13": "N/A", + "MC14": "N/A", + "MC15": "N/A", + "MC16": "N/A", + "MC17": "N/A", + "MC18": "N/A", + "MC19": "N/A" + }, + { + "Port": "Ethernet8", + "MC10": "N/A", + "MC11": "N/A", + "MC12": "N/A", + "MC13": "N/A", + "MC14": "N/A", + "MC15": "N/A", + "MC16": "N/A", + "MC17": "N/A", + "MC18": "N/A", + "MC19": "N/A" + } +] +""" + + +show_queue_wm_multicast_neg_output = """\ Object map from the COUNTERS_DB is empty because the multicast queues are not configured in the CONFIG_DB! """ -show_queue_wm_all_output="""\ +show_queue_wm_all_output = """\ Egress shared pool occupancy per all queues: Port ALL20 ALL21 ALL22 ALL23 ALL24 ALL25 ALL26 ALL27 ALL28 ALL29 --------- ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- @@ -444,7 +737,52 @@ Ethernet8 20 5 1998 0 0 0 0 0 8528 7696 """ -show_queue_pwm_all_output="""\ +show_queue_wm_all_output_json = """\ +[ + { + "Port": "Ethernet0", + "ALL20": "1234567", + "ALL21": "7654321", + "ALL22": "0", + "ALL23": "0", + "ALL24": "0", + "ALL25": "20", + "ALL26": "500", + "ALL27": "200", + "ALL28": "0", + "ALL29": "10" + }, + { + "Port": "Ethernet4", + "ALL20": "0", + "ALL21": "0", + "ALL22": "0", + "ALL23": "1986", + "ALL24": "2567", + "ALL25": "0", + "ALL26": "0", + "ALL27": "0", + "ALL28": "0", + "ALL29": "0" + }, + { + "Port": "Ethernet8", + "ALL20": "20", + "ALL21": "5", + "ALL22": "1998", + "ALL23": "0", + "ALL24": "0", + "ALL25": "0", + "ALL26": "0", + "ALL27": "0", + "ALL28": "8528", + "ALL29": "7696" + } +] +""" + + +show_queue_pwm_all_output = """\ Egress shared pool occupancy per all queues: Port ALL20 ALL21 ALL22 ALL23 ALL24 ALL25 ALL26 ALL27 ALL28 ALL29 --------- ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- @@ -453,7 +791,51 @@ Ethernet8 N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A """ -show_buffer_pool_wm_output="""\ +show_queue_pwm_all_output_json = """\ +[ + { + "Port": "Ethernet0", + "ALL20": "N/A", + "ALL21": "N/A", + "ALL22": "N/A", + "ALL23": "N/A", + "ALL24": "N/A", + "ALL25": "N/A", + "ALL26": "N/A", + "ALL27": "N/A", + "ALL28": "N/A", + "ALL29": "N/A" + }, + { + "Port": "Ethernet4", + "ALL20": "N/A", + "ALL21": "N/A", + "ALL22": "N/A", + "ALL23": "N/A", + "ALL24": "N/A", + "ALL25": "N/A", + "ALL26": "N/A", + "ALL27": "N/A", + "ALL28": "N/A", + "ALL29": "N/A" + }, + { + "Port": "Ethernet8", + "ALL20": "N/A", + "ALL21": "N/A", + "ALL22": "N/A", + "ALL23": "N/A", + "ALL24": "N/A", + "ALL25": "N/A", + "ALL26": "N/A", + "ALL27": "N/A", + "ALL28": "N/A", + "ALL29": "N/A" + } +] +""" + +show_buffer_pool_wm_output = """\ Shared pool maximum occupancy: Pool Bytes --------------------- ------- @@ -462,7 +844,7 @@ ingress_lossless_pool 3000 """ -show_buffer_pool_persistent_wm_output="""\ +show_buffer_pool_persistent_wm_output = """\ Shared pool maximum occupancy: Pool Bytes --------------------- ------- @@ -471,14 +853,14 @@ ingress_lossless_pool 4000 """ -show_hdrm_pool_wm_output="""\ +show_hdrm_pool_wm_output = """\ Headroom pool maximum occupancy: Pool Bytes --------------------- ------- ingress_lossless_pool 432640 """ -show_hdrm_pool_persistent_wm_output="""\ +show_hdrm_pool_persistent_wm_output = """\ Headroom pool maximum occupancy: Pool Bytes --------------------- ------- @@ -486,206 +868,357 @@ """ testData = { - 'show_pg_wm_shared' : [ {'cmd' : ['priority-group', 'watermark', 'shared'], - 'rc_output': show_pg_wm_shared_output - } - ], - 'show_pg_wm_hdrm' : [ {'cmd' : ['priority-group', 'watermark', 'headroom'], - 'rc_output': show_pg_wm_hdrm_output - } - ], - 'show_pg_pwm_shared': [{'cmd': ['priority-group', 'persistent-watermark', 'shared'], - 'rc_output': show_pg_persistent_wm_shared_output - } - ], - 'show_pg_pwm_hdrm': [{'cmd': ['priority-group', 'persistent-watermark', 'headroom'], - 'rc_output': show_pg_persistent_wm_hdrm_output - } - ], - 'show_q_wm_unicast': [{'cmd': ['queue', 'watermark', 'unicast'], - 'rc_output': show_queue_wm_unicast_output - } - ], - 'show_q_pwm_unicast': [{'cmd': ['queue', 'persistent-watermark', 'unicast'], - 'rc_output': show_queue_pwm_unicast_output - } - ], - 'show_q_wm_multicast': [{'cmd': ['queue', 'watermark', 'multicast'], - 'rc_output': show_queue_wm_multicast_output - } - ], - 'show_q_wm_multicast_neg': [{'cmd': ['queue', 'watermark', 'multicast'], - 'rc_output': show_queue_wm_multicast_neg_output - } - ], - 'show_q_pwm_multicast': [{'cmd': ['queue', 'persistent-watermark', 'multicast'], - 'rc_output': show_queue_wm_multicast_output - } - ], - 'show_q_wm_all': [{'cmd': ['queue', 'watermark', 'all'], - 'rc_output': show_queue_wm_all_output - } - ], - 'show_q_pwm_all': [{'cmd': ['queue', 'persistent-watermark', 'all'], - 'rc_output': show_queue_pwm_all_output - } - ], - 'show_buffer_pool_wm': [{'cmd': ['buffer_pool', 'watermark'], - 'rc_output': show_buffer_pool_wm_output - } - ], - 'show_buffer_pool_pwm': [{'cmd': ['buffer_pool', 'persistent-watermark'], - 'rc_output': show_buffer_pool_persistent_wm_output - } - ], - 'show_hdrm_pool_wm': [{'cmd': ['headroom-pool', 'watermark'], - 'rc_output': show_hdrm_pool_wm_output - } - ], - 'show_hdrm_pool_pwm': [{'cmd': ['headroom-pool', 'persistent-watermark'], - 'rc_output': show_hdrm_pool_persistent_wm_output - } - ], - 'show_pg_wm_shared_one_masic': [{'cmd': ['priority-group', 'watermark', 'shared'], - 'args': ['--namespace', 'asic0'], - 'rc_output': show_pg_wm_shared_output_one_masic - } - ], - 'show_pg_wm_shared_all_masic': [{'cmd': ['priority-group', 'watermark', 'shared'], - 'rc_output': show_pg_wm_shared_output_all_masic - } - ], - 'show_pg_wm_hdrm_one_masic': [{'cmd': ['priority-group', 'watermark', 'headroom'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_pg_wm_hdrm_output_one_masic - } - ], - 'show_pg_wm_hdrm_all_masic': [{'cmd': ['priority-group', 'watermark', 'headroom'], - 'rc_output': show_pg_wm_hdrm_output_all_masic - } - ], - 'show_pg_pwm_shared_one_masic': [{'cmd': ['priority-group', 'persistent-watermark', 'shared'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_pg_persistent_wm_shared_output_one_masic - } - ], - 'show_pg_pwm_shared_all_masic': [{'cmd': ['priority-group', 'persistent-watermark', 'shared'], - 'rc_output': show_pg_persistent_wm_shared_output_all_masic - } - ], - 'show_pg_pwm_hdrm_one_masic': [{'cmd': ['priority-group', 'persistent-watermark', 'headroom'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_pg_persistent_wm_hdrm_output_one_masic - } - ], - 'show_pg_pwm_hdrm_all_masic': [{'cmd': ['priority-group', 'persistent-watermark', 'headroom'], - 'rc_output': show_pg_persistent_wm_hdrm_output_all_masic - } - ], - 'show_q_wm_unicast_one_masic': [{'cmd': ['queue', 'watermark', 'unicast'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_queue_wm_unicast_output_one_masic - } - ], - 'show_q_wm_unicast_all_masic': [{'cmd': ['queue', 'watermark', 'unicast'], - 'rc_output': show_queue_wm_unicast_output_all_masic - } - ], - 'show_q_pwm_unicast_one_masic': [{'cmd': ['queue', 'persistent-watermark', 'unicast'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_queue_pwm_unicast_output_one_masic - } - ], - 'show_q_pwm_unicast_all_masic': [{'cmd': ['queue', 'persistent-watermark', 'unicast'], - 'rc_output': show_queue_pwm_unicast_output_all_masic - } - ], - 'show_q_wm_multicast_one_masic': [{'cmd': ['queue', 'watermark', 'multicast'], - 'args': ['--namespace', 'asic0'], - 'rc_output': show_queue_wm_multicast_output_one_masic - } - ], - 'show_q_wm_multicast_all_masic': [{'cmd': ['queue', 'watermark', 'multicast'], - 'rc_output': show_queue_wm_multicast_output_all_masic - } - ], - 'show_q_pwm_multicast_one_masic': [{'cmd': ['queue', 'persistent-watermark', 'multicast'], - 'args': ['--namespace', 'asic0'], - 'rc_output': show_queue_pwm_multicast_output_one_masic - } - ], - 'show_q_pwm_multicast_all_masic': [{'cmd': ['queue', 'persistent-watermark', 'multicast'], - 'rc_output': show_queue_pwm_multicast_output_all_masic - } - ], - 'show_q_wm_all_one_masic': [{'cmd': ['queue', 'watermark', 'all'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_queue_wm_all_output_one_masic - } - ], - 'show_q_wm_all_all_masic': [{'cmd': ['queue', 'watermark', 'all'], - 'rc_output': show_queue_wm_all_output_all_masic - } - ], - 'show_q_pwm_all_one_masic': [{'cmd': ['queue', 'persistent-watermark', 'all'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_queue_pwm_all_output_one_masic - } - ], - 'show_q_pwm_all_all_masic': [{'cmd': ['queue', 'persistent-watermark', 'all'], - 'rc_output': show_queue_pwm_all_output_all_masic - } - ], - 'show_buffer_pool_wm_one_masic': [{'cmd': ['buffer_pool', 'watermark'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_buffer_pool_wm_output_one_masic - } - ], - 'show_buffer_pool_wm_all_masic': [{'cmd': ['buffer_pool', 'watermark'], - 'rc_output': show_buffer_pool_wm_output_all_masic - } - ], - 'show_buffer_pool_pwm_one_masic': [{'cmd': ['buffer_pool', 'persistent-watermark'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_buffer_pool_pwm_output_one_masic - } - ], - 'show_buffer_pool_pwm_all_masic': [{'cmd': ['buffer_pool', 'persistent-watermark'], - 'rc_output': show_buffer_pool_pwm_output_all_masic - } - ], - 'show_hdrm_pool_wm_one_masic': [{'cmd': ['headroom-pool', 'watermark'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_hdrm_pool_wm_output_one_masic - } - ], - 'show_hdrm_pool_wm_all_masic': [{'cmd': ['headroom-pool', 'watermark'], - 'rc_output': show_hdrm_pool_wm_output_all_masic - } - ], - 'show_hdrm_pool_pwm_one_masic': [{'cmd': ['headroom-pool', 'persistent-watermark'], - 'args': ['--namespace', 'asic1'], - 'rc_output': show_hdrm_pool_pwm_output_one_masic - } - ], - 'show_hdrm_pool_pwm_all_masic': [{'cmd': ['headroom-pool', 'persistent-watermark'], - 'rc_output': show_hdrm_pool_pwm_output_all_masic - } - ], - 'show_invalid_namespace_masic': [{'cmd': ['buffer_pool', 'watermark'], - 'args': ['--namespace', 'asic14'], - 'rc': 2, - 'rc_output': '' - } - ], - 'clear_hdrm_pool_wm_one_masic': [{'cmd': ['clear', 'watermarkstat', '-t', - 'pg_headroom', '-n', 'asic0', '-c'], - 'rc_output': clear_hdrm_pool_wm_output_one_masic - } - ], - 'clear_hdrm_pool_wm_all_masic': [{'cmd': ['clear', 'watermarkstat', '-t', - 'pg_headroom', '-c'], - 'rc_output': clear_hdrm_pool_wm_output_all_masic - } - ] - } + "show_pg_wm_shared": [ + { + "cmd": ["priority-group", "watermark", "shared"], + "rc_output": show_pg_wm_shared_output, + } + ], + "show_pg_wm_shared_json": [ + { + "cmd": ["priority-group", "watermark", "shared"], + "args": ["-j"], + "rc_output": show_pg_wm_shared_output_json, + } + ], + "show_pg_wm_hdrm": [ + { + "cmd": ["priority-group", "watermark", "headroom"], + "rc_output": show_pg_wm_hdrm_output, + } + ], + "show_pg_wm_hdrm_json": [ + { + "cmd": ["priority-group", "watermark", "headroom"], + "args": ["-j"], + "rc_output": show_pg_wm_hdrm_output_json, + } + ], + "show_pg_pwm_shared": [ + { + "cmd": ["priority-group", "persistent-watermark", "shared"], + "rc_output": show_pg_persistent_wm_shared_output, + } + ], + "show_pg_pwm_shared_json": [ + { + "cmd": ["priority-group", "persistent-watermark", "shared"], + "args": ["-j"], + "rc_output": show_pg_persistent_wm_shared_output_json, + } + ], + "show_pg_pwm_hdrm": [ + { + "cmd": ["priority-group", "persistent-watermark", "headroom"], + "rc_output": show_pg_persistent_wm_hdrm_output, + } + ], + "show_pg_pwm_hdrm_json": [ + { + "cmd": ["priority-group", "persistent-watermark", "headroom"], + "args": ["-j"], + "rc_output": show_pg_persistent_wm_hdrm_output_json, + } + ], + "show_q_wm_unicast": [ + { + "cmd": ["queue", "watermark", "unicast"], + "rc_output": show_queue_wm_unicast_output, + } + ], + "show_q_wm_unicast_json": [ + { + "cmd": ["queue", "watermark", "unicast"], + "args": ["-j"], + "rc_output": show_queue_wm_unicast_output_json, + } + ], + "show_q_pwm_unicast": [ + { + "cmd": ["queue", "persistent-watermark", "unicast"], + "rc_output": show_queue_pwm_unicast_output, + } + ], + "show_q_pwm_unicast_json": [ + { + "cmd": ["queue", "persistent-watermark", "unicast"], + "args": ["-j"], + "rc_output": show_queue_pwm_unicast_output_json, + } + ], + "show_q_wm_multicast": [ + { + "cmd": ["queue", "watermark", "multicast"], + "rc_output": show_queue_wm_multicast_output, + } + ], + "show_q_wm_multicast_json": [ + { + "cmd": ["queue", "watermark", "multicast"], + "args": ["-j"], + "rc_output": show_queue_wm_multicast_output_json, + } + ], + "show_q_wm_multicast_neg": [ + { + "cmd": ["queue", "watermark", "multicast"], + "rc_output": show_queue_wm_multicast_neg_output, + } + ], + "show_q_pwm_multicast": [ + { + "cmd": ["queue", "persistent-watermark", "multicast"], + "rc_output": show_queue_wm_multicast_output, + } + ], + "show_q_pwm_multicast_json": [ + { + "cmd": ["queue", "persistent-watermark", "multicast"], + "args": ["-j"], + "rc_output": show_queue_wm_multicast_output_json, + } + ], + "show_q_wm_all": [ + {"cmd": ["queue", "watermark", "all"], "rc_output": show_queue_wm_all_output} + ], + "show_q_wm_all_json": [ + { + "cmd": ["queue", "watermark", "all"], + "args": ["-j"], + "rc_output": show_queue_wm_all_output_json, + } + ], + "show_q_pwm_all": [ + { + "cmd": ["queue", "persistent-watermark", "all"], + "rc_output": show_queue_pwm_all_output, + } + ], + "show_q_pwm_all_json": [ + { + "cmd": ["queue", "persistent-watermark", "all"], + "args": ["-j"], + "rc_output": show_queue_pwm_all_output_json, + } + ], + "show_buffer_pool_wm": [ + {"cmd": ["buffer_pool", "watermark"], "rc_output": show_buffer_pool_wm_output} + ], + "show_buffer_pool_pwm": [ + { + "cmd": ["buffer_pool", "persistent-watermark"], + "rc_output": show_buffer_pool_persistent_wm_output, + } + ], + "show_hdrm_pool_wm": [ + {"cmd": ["headroom-pool", "watermark"], "rc_output": show_hdrm_pool_wm_output} + ], + "show_hdrm_pool_pwm": [ + { + "cmd": ["headroom-pool", "persistent-watermark"], + "rc_output": show_hdrm_pool_persistent_wm_output, + } + ], + "show_pg_wm_shared_one_masic": [ + { + "cmd": ["priority-group", "watermark", "shared"], + "args": ["--namespace", "asic0"], + "rc_output": show_pg_wm_shared_output_one_masic, + } + ], + "show_pg_wm_shared_all_masic": [ + { + "cmd": ["priority-group", "watermark", "shared"], + "rc_output": show_pg_wm_shared_output_all_masic, + } + ], + "show_pg_wm_hdrm_one_masic": [ + { + "cmd": ["priority-group", "watermark", "headroom"], + "args": ["--namespace", "asic1"], + "rc_output": show_pg_wm_hdrm_output_one_masic, + } + ], + "show_pg_wm_hdrm_all_masic": [ + { + "cmd": ["priority-group", "watermark", "headroom"], + "rc_output": show_pg_wm_hdrm_output_all_masic, + } + ], + "show_pg_pwm_shared_one_masic": [ + { + "cmd": ["priority-group", "persistent-watermark", "shared"], + "args": ["--namespace", "asic1"], + "rc_output": show_pg_persistent_wm_shared_output_one_masic, + } + ], + "show_pg_pwm_shared_all_masic": [ + { + "cmd": ["priority-group", "persistent-watermark", "shared"], + "rc_output": show_pg_persistent_wm_shared_output_all_masic, + } + ], + "show_pg_pwm_hdrm_one_masic": [ + { + "cmd": ["priority-group", "persistent-watermark", "headroom"], + "args": ["--namespace", "asic1"], + "rc_output": show_pg_persistent_wm_hdrm_output_one_masic, + } + ], + "show_pg_pwm_hdrm_all_masic": [ + { + "cmd": ["priority-group", "persistent-watermark", "headroom"], + "rc_output": show_pg_persistent_wm_hdrm_output_all_masic, + } + ], + "show_q_wm_unicast_one_masic": [ + { + "cmd": ["queue", "watermark", "unicast"], + "args": ["--namespace", "asic1"], + "rc_output": show_queue_wm_unicast_output_one_masic, + } + ], + "show_q_wm_unicast_all_masic": [ + { + "cmd": ["queue", "watermark", "unicast"], + "rc_output": show_queue_wm_unicast_output_all_masic, + } + ], + "show_q_pwm_unicast_one_masic": [ + { + "cmd": ["queue", "persistent-watermark", "unicast"], + "args": ["--namespace", "asic1"], + "rc_output": show_queue_pwm_unicast_output_one_masic, + } + ], + "show_q_pwm_unicast_all_masic": [ + { + "cmd": ["queue", "persistent-watermark", "unicast"], + "rc_output": show_queue_pwm_unicast_output_all_masic, + } + ], + "show_q_wm_multicast_one_masic": [ + { + "cmd": ["queue", "watermark", "multicast"], + "args": ["--namespace", "asic0"], + "rc_output": show_queue_wm_multicast_output_one_masic, + } + ], + "show_q_wm_multicast_all_masic": [ + { + "cmd": ["queue", "watermark", "multicast"], + "rc_output": show_queue_wm_multicast_output_all_masic, + } + ], + "show_q_pwm_multicast_one_masic": [ + { + "cmd": ["queue", "persistent-watermark", "multicast"], + "args": ["--namespace", "asic0"], + "rc_output": show_queue_pwm_multicast_output_one_masic, + } + ], + "show_q_pwm_multicast_all_masic": [ + { + "cmd": ["queue", "persistent-watermark", "multicast"], + "rc_output": show_queue_pwm_multicast_output_all_masic, + } + ], + "show_q_wm_all_one_masic": [ + { + "cmd": ["queue", "watermark", "all"], + "args": ["--namespace", "asic1"], + "rc_output": show_queue_wm_all_output_one_masic, + } + ], + "show_q_wm_all_all_masic": [ + { + "cmd": ["queue", "watermark", "all"], + "rc_output": show_queue_wm_all_output_all_masic, + } + ], + "show_q_pwm_all_one_masic": [ + { + "cmd": ["queue", "persistent-watermark", "all"], + "args": ["--namespace", "asic1"], + "rc_output": show_queue_pwm_all_output_one_masic, + } + ], + "show_q_pwm_all_all_masic": [ + { + "cmd": ["queue", "persistent-watermark", "all"], + "rc_output": show_queue_pwm_all_output_all_masic, + } + ], + "show_buffer_pool_wm_one_masic": [ + { + "cmd": ["buffer_pool", "watermark"], + "args": ["--namespace", "asic1"], + "rc_output": show_buffer_pool_wm_output_one_masic, + } + ], + "show_buffer_pool_wm_all_masic": [ + { + "cmd": ["buffer_pool", "watermark"], + "rc_output": show_buffer_pool_wm_output_all_masic, + } + ], + "show_buffer_pool_pwm_one_masic": [ + { + "cmd": ["buffer_pool", "persistent-watermark"], + "args": ["--namespace", "asic1"], + "rc_output": show_buffer_pool_pwm_output_one_masic, + } + ], + "show_buffer_pool_pwm_all_masic": [ + { + "cmd": ["buffer_pool", "persistent-watermark"], + "rc_output": show_buffer_pool_pwm_output_all_masic, + } + ], + "show_hdrm_pool_wm_one_masic": [ + { + "cmd": ["headroom-pool", "watermark"], + "args": ["--namespace", "asic1"], + "rc_output": show_hdrm_pool_wm_output_one_masic, + } + ], + "show_hdrm_pool_wm_all_masic": [ + { + "cmd": ["headroom-pool", "watermark"], + "rc_output": show_hdrm_pool_wm_output_all_masic, + } + ], + "show_hdrm_pool_pwm_one_masic": [ + { + "cmd": ["headroom-pool", "persistent-watermark"], + "args": ["--namespace", "asic1"], + "rc_output": show_hdrm_pool_pwm_output_one_masic, + } + ], + "show_hdrm_pool_pwm_all_masic": [ + { + "cmd": ["headroom-pool", "persistent-watermark"], + "rc_output": show_hdrm_pool_pwm_output_all_masic, + } + ], + "show_invalid_namespace_masic": [ + { + "cmd": ["buffer_pool", "watermark"], + "args": ["--namespace", "asic14"], + "rc": 2, + "rc_output": "", + } + ], + "clear_hdrm_pool_wm_one_masic": [ + { + "cmd": ["clear", "watermarkstat", "-t", "pg_headroom", "-n", "asic0", "-c"], + "rc_output": clear_hdrm_pool_wm_output_one_masic, + } + ], + "clear_hdrm_pool_wm_all_masic": [ + { + "cmd": ["clear", "watermarkstat", "-t", "pg_headroom", "-c"], + "rc_output": clear_hdrm_pool_wm_output_all_masic, + } + ], +} From 50df9ea0fa3bca6c5bb5e25f76559388e05c3b5c Mon Sep 17 00:00:00 2001 From: manamand2020 <68087238+manamand2020@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:35:59 -0700 Subject: [PATCH 19/21] Adapt 'show muxcable tunnel-route' for prefix route based mux neighbors (#4007) What I did Mux neighbors now use prefix-based routes based on changes from the following PR: sonic-net/sonic-swss#3722 Presence of route prefix for neighbors does not mean tunnel-route use anymore. Show command now uses nexthop type to distinguish between tunnel and normal neighbor route. How to verify it Verified output on hardware. Signed-off-by: Manas Kumar Mandal --- show/muxcable.py | 22 +++++++++++++++++++++- tests/mock_tables/asic_db.json | 13 +++++++++++++ tests/muxcable_test.py | 6 +++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/show/muxcable.py b/show/muxcable.py index cf8403dfce..f7db0c3d25 100644 --- a/show/muxcable.py +++ b/show/muxcable.py @@ -608,13 +608,33 @@ def get_tunnel_route_per_port(db, port_tunnel_route, per_npu_configdb, per_npu_a dest_address = mux_cfg_dict.get(name, None) if dest_address is not None: + # Check kernel tunnel route kernel_route_keys = per_npu_appl_db[asic_id].keys( per_npu_appl_db[asic_id].APPL_DB, 'TUNNEL_ROUTE_TABLE:*{}'.format(dest_address)) if_kernel_tunnel_route_programed = kernel_route_keys is not None and len(kernel_route_keys) + # Mux neighbors use prefix based routes + # Check ASIC route with nexthop type verification asic_route_keys = per_npu_asic_db[asic_id].keys( per_npu_asic_db[asic_id].ASIC_DB, 'ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:*{}*'.format(dest_address)) - if_asic_tunnel_route_programed = asic_route_keys is not None and len(asic_route_keys) + + if_asic_tunnel_route_programed = False + if asic_route_keys is not None and len(asic_route_keys): + # Get the route entry to check nexthop type + for route_key in asic_route_keys: + route_data = per_npu_asic_db[asic_id].get_all(per_npu_asic_db[asic_id].ASIC_DB, route_key) + nexthop_id = route_data.get('SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID', None) + + if nexthop_id: + # Check nexthop type + nexthop_key = 'ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:{}'.format(nexthop_id) + nexthop_data = per_npu_asic_db[asic_id].get_all(per_npu_asic_db[asic_id].ASIC_DB, nexthop_key) + nexthop_type = nexthop_data.get('SAI_NEXT_HOP_ATTR_TYPE', None) + + # Only count as tunnel route if nexthop type is tunnel + if nexthop_type == 'SAI_NEXT_HOP_TYPE_TUNNEL_ENCAP': + if_asic_tunnel_route_programed = True + break if if_kernel_tunnel_route_programed or if_asic_tunnel_route_programed: port_tunnel_route["TUNNEL_ROUTE"][port] = port_tunnel_route["TUNNEL_ROUTE"].get(port, {}) diff --git a/tests/mock_tables/asic_db.json b/tests/mock_tables/asic_db.json index b821865171..1429a2a597 100644 --- a/tests/mock_tables/asic_db.json +++ b/tests/mock_tables/asic_db.json @@ -20,6 +20,19 @@ "oid:0xd00000000056d": "oid:0xd", "oid:0x10000000004a4": "oid:0x1690000000001" }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x40000000015d8": { + "SAI_NEXT_HOP_ATTR_TYPE": "SAI_NEXT_HOP_TYPE_TUNNEL_ENCAP", + "SAI_NEXT_HOP_ATTR_TUNNEL_ID": "oid:0x2a000000000520" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x40000000006b3": { + "SAI_NEXT_HOP_ATTR_TYPE": "SAI_NEXT_HOP_TYPE_IP", + "SAI_NEXT_HOP_ATTR_IP": "10.3.1.1", + "SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID": "oid:0x6000000000501" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"10.3.1.1\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x300000000007c\"}": { + "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION": "SAI_PACKET_ACTION_FORWARD", + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": "oid:0x40000000006b3" + }, "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"10.2.1.1\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x300000000007c\"}": { "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION": "SAI_PACKET_ACTION_FORWARD", "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID": "oid:0x40000000015d8" diff --git a/tests/muxcable_test.py b/tests/muxcable_test.py index 13eefef960..69450d946a 100644 --- a/tests/muxcable_test.py +++ b/tests/muxcable_test.py @@ -569,7 +569,7 @@ "server_ipv4": { "DEST": "10.2.1.1", "kernel": 1, - "asic": 1 + "asic": true } }, "Ethernet4": { @@ -583,7 +583,7 @@ "soc_ipv6": { "DEST": "fc00::76", "kernel": false, - "asic": 1 + "asic": true } } } @@ -605,7 +605,7 @@ "server_ipv4": { "DEST": "10.2.1.1", "kernel": 1, - "asic": 1 + "asic": true } } } From 01876d4a78ff45962bc67dcaca53b3bb9dbc31de Mon Sep 17 00:00:00 2001 From: Nazarii Hnydyn Date: Wed, 2 Jul 2025 19:41:17 +0300 Subject: [PATCH 20/21] [trim]: Add Packet Trimming Drop Counters CLI Signed-off-by: Nazarii Hnydyn --- clear/main.py | 7 + counterpoll/main.py | 66 +- doc/Command-Reference.md | 138 +- scripts/queuestat | 65 +- scripts/switchstat | 133 ++ setup.py | 1 + show/main.py | 2 + show/switch.py | 176 ++ tests/clear_test.py | 7 + tests/counterpoll_test.py | 25 + tests/mock_tables/asic0/counters_db.json | 8 +- tests/mock_tables/config_db.json | 4 + tests/mock_tables/counters_db.json | 32 +- tests/portstat_db/counters_db.json | 13 +- tests/portstat_input/assert_show_output.py | 79 +- tests/portstat_test.py | 68 +- tests/queuestat_input/assert_show_output.py | 1850 +++++++++++------ tests/show_test.py | 125 ++ tests/switchstat_input/common_output.py | 29 + tests/switchstat_input/mock_counters/all.json | 9 + .../switchstat_input/mock_counters/empty.json | 8 + .../mock_counters/no_counters.json | 5 + .../mock_counters/no_map.json | 6 + .../mock_counters/partial.json | 8 + .../mock_counters/updated.json | 9 + .../mock_counters/updated_tag.json | 9 + tests/switchstat_input/multi_asic_output.py | 233 +++ tests/switchstat_input/single_asic_output.py | 83 + tests/switchstat_test.py | 388 ++++ tests/synchronous_mode_test.py | 2 + utilities_common/netstat.py | 15 +- utilities_common/portstat.py | 30 +- utilities_common/switchstat.py | 299 +++ 33 files changed, 3137 insertions(+), 795 deletions(-) create mode 100755 scripts/switchstat create mode 100644 show/switch.py create mode 100644 tests/switchstat_input/common_output.py create mode 100644 tests/switchstat_input/mock_counters/all.json create mode 100644 tests/switchstat_input/mock_counters/empty.json create mode 100644 tests/switchstat_input/mock_counters/no_counters.json create mode 100644 tests/switchstat_input/mock_counters/no_map.json create mode 100644 tests/switchstat_input/mock_counters/partial.json create mode 100644 tests/switchstat_input/mock_counters/updated.json create mode 100644 tests/switchstat_input/mock_counters/updated_tag.json create mode 100644 tests/switchstat_input/multi_asic_output.py create mode 100644 tests/switchstat_input/single_asic_output.py create mode 100644 tests/switchstat_test.py create mode 100644 utilities_common/switchstat.py diff --git a/clear/main.py b/clear/main.py index 28dbe5ca88..788061e627 100755 --- a/clear/main.py +++ b/clear/main.py @@ -225,6 +225,13 @@ def srv6counters(): command = ["srv6stat", "-c"] run_command(command) + +@cli.command() +def switchcounters(): + """Clear switch counters""" + command = ["switchstat", "-c"] + run_command(command) + # # 'clear watermarks # diff --git a/counterpoll/main.py b/counterpoll/main.py index f0e08bde67..65a2212e23 100644 --- a/counterpoll/main.py +++ b/counterpoll/main.py @@ -1,9 +1,12 @@ import click -import json -from flow_counter_util.route import exit_if_route_flow_counter_not_support -from swsscommon.swsscommon import ConfigDBConnector +import utilities_common.cli as clicommon + from tabulate import tabulate from sonic_py_common import device_info +from flow_counter_util.route import exit_if_route_flow_counter_not_support +from swsscommon.swsscommon import ConfigDBConnector +from swsscommon.swsscommon import CFG_FLEX_COUNTER_TABLE_NAME as CFG_FLEX_COUNTER_TABLE + BUFFER_POOL_WATERMARK = "BUFFER_POOL_WATERMARK" PORT_BUFFER_DROP = "PORT_BUFFER_DROP" @@ -540,6 +543,56 @@ def disable(ctx): # noqa: F811 ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "SRV6", srv6_info) +# Switch counter commands +@cli.group() +def switch(): + """ Switch counter commands """ + pass + + +@switch.command() +@clicommon.pass_db +@click.argument("poll_interval", type=click.IntRange(1000, 60000)) +def interval(db, poll_interval): # noqa: F811 + """ Set switch counter query interval """ + table = CFG_FLEX_COUNTER_TABLE + key = "SWITCH" + + data = { + "POLL_INTERVAL": poll_interval + } + + db.cfgdb.mod_entry(table, key, data) + + +@switch.command() +@clicommon.pass_db +def enable(db): # noqa: F811 + """ Enable switch counter query """ + table = CFG_FLEX_COUNTER_TABLE + key = "SWITCH" + + data = { + "FLEX_COUNTER_STATUS": ENABLE + } + + db.cfgdb.mod_entry(table, key, data) + + +@switch.command() +@clicommon.pass_db +def disable(db): # noqa: F811 + """ Disable switch counter query """ + table = CFG_FLEX_COUNTER_TABLE + key = "SWITCH" + + data = { + "FLEX_COUNTER_STATUS": DISABLE + } + + db.cfgdb.mod_entry(table, key, data) + + @cli.command() def show(): """ Show the counter configuration """ @@ -561,6 +614,7 @@ def show(): wred_queue_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'WRED_ECN_QUEUE') wred_port_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'WRED_ECN_PORT') srv6_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'SRV6') + switch_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'SWITCH') header = ("Type", "Interval (in ms)", "Status") data = [] @@ -598,6 +652,12 @@ def show(): if srv6_info: data.append(["SRV6_STAT", srv6_info.get("POLL_INTERVAL", DEFLT_10_SEC), srv6_info.get("FLEX_COUNTER_STATUS", DISABLE)]) + if switch_info: + data.append([ + "SWITCH_STAT", + switch_info.get("POLL_INTERVAL", DEFLT_60_SEC), + switch_info.get("FLEX_COUNTER_STATUS", DISABLE) + ]) if is_dpu(configdb) and eni_info: data.append(["ENI_STAT", eni_info.get("POLL_INTERVAL", DEFLT_10_SEC), diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 2289d5e751..3d58395001 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -169,6 +169,9 @@ * [Radius](#radius) * [Radius show commands](#show-radius-commands) * [Radius config commands](#Radius-config-commands) +* [Switch](#switch) + * [Switch Show commands](#switch-show-commands) + * [Switch Clear commands](#switch-clear-commands) * [sFlow](#sflow) * [sFlow Show commands](#sflow-show-commands) * [sFlow Config commands](#sflow-config-commands) @@ -5213,6 +5216,10 @@ The "detailed" subcommand is used to display more detailed interface counters. A WRED Red Dropped Packets....................... 0 WRED Total Dropped Packets..................... 0 + Trimmed Packets................................ 0 + Trimmed Sent Packets........................... 0 + Trimmed Dropped Packets........................ 0 + Time Since Counters Last Cleared............... None ``` @@ -5273,11 +5280,11 @@ The "trim" subcommand is used to display the interface packet trimming related s - Example: ``` admin@sonic:~$ show interfaces counters trim - IFACE STATE TRIM_PKTS - ---------- ------- ----------- - Ethernet0 U 0 - Ethernet8 U 100 - Ethernet16 U 200 + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS + ---------- ------- ----------- -------------- --------------- + Ethernet0 U 0 0 0 + Ethernet8 U 100 100 0 + Ethernet16 U 200 100 100 ``` **show interfaces description** @@ -9893,28 +9900,28 @@ This command can be used to clear the counters for all queues of all ports. Note ... admin@sonic:~$ show queue counters --trim - Port TxQ Trim/pkts - --------- ----- ----------- - Ethernet0 UC0 0 - Ethernet0 UC1 0 - Ethernet0 UC2 0 - Ethernet0 UC3 0 - Ethernet0 UC4 0 - Ethernet0 UC5 0 - Ethernet0 UC6 0 - Ethernet0 UC7 0 - Ethernet0 UC8 0 - Ethernet0 UC9 0 - Ethernet0 MC0 N/A - Ethernet0 MC1 N/A - Ethernet0 MC2 N/A - Ethernet0 MC3 N/A - Ethernet0 MC4 N/A - Ethernet0 MC5 N/A - Ethernet0 MC6 N/A - Ethernet0 MC7 N/A - Ethernet0 MC8 N/A - Ethernet0 MC9 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts + --------- ----- ----------- --------------- --------------- + Ethernet0 UC0 0 0 0 + Ethernet0 UC1 100 100 0 + Ethernet0 UC2 200 100 100 + Ethernet0 UC3 300 300 0 + Ethernet0 UC4 400 200 200 + Ethernet0 UC5 500 500 0 + Ethernet0 UC6 600 300 300 + Ethernet0 UC7 700 700 0 + Ethernet0 UC8 800 400 400 + Ethernet0 UC9 900 900 0 + Ethernet0 MC0 N/A N/A N/A + Ethernet0 MC1 N/A N/A N/A + Ethernet0 MC2 N/A N/A N/A + Ethernet0 MC3 N/A N/A N/A + Ethernet0 MC4 N/A N/A N/A + Ethernet0 MC5 N/A N/A N/A + Ethernet0 MC6 N/A N/A N/A + Ethernet0 MC7 N/A N/A N/A + Ethernet0 MC8 N/A N/A N/A + Ethernet0 MC9 N/A N/A N/A ``` Optionally, you can specify an interface name in order to display only that particular interface @@ -10328,6 +10335,83 @@ This command is to config the radius server for various parameter listed. timeout Specify RADIUS server global timeout <1 - 60> ``` + +# Switch + +This section explains the various show, configuration and clear commands available for users. + +### Switch Show commands + +This subsection explains how to display switch configuration or stats. + +**show switch counters** + +This command displays switch stats. + +- Usage: + ```bash + show switch counters [OPTIONS] + show switch counters all [OPTIONS] + show switch counters trim [OPTIONS] + show switch counters detailed [OPTIONS] + ``` + +- Options: + - _-p,--period_: display stats over a specified period (in seconds) + - _-d,--display_: show internal interfaces + - _-n,--namespace_: namespace name or all + - _-j,--json_: display in JSON format + - _-v,--verbose_: enable verbose output + +- Example: + ```bash + admin@sonic:~$ show switch counters + TrimSent/pkts TrimDrop/pkts + --------------- --------------- + 100 100 + + admin@sonic:~$ show switch counters all + TrimSent/pkts TrimDrop/pkts + --------------- --------------- + 100 100 + + admin@sonic:~$ show switch counters trim + TrimSent/pkts TrimDrop/pkts + --------------- --------------- + 100 100 + + admin@sonic:~$ show switch counters detailed + Trimmed Sent Packets........................... 100 + Trimmed Dropped Packets........................ 100 + + admin@sonic:~$ show switch counters --json + { + "trim_drop": "100", + "trim_sent": "100" + } + ``` + +### Switch Clear commands + +This subsection explains how to clear switch stats. + +**sonic-clear switchcounters** + +This command is used to clear switch counters. + +- Usage: + ```bash + sonic-clear switchcounters + ``` + +- Examples: + ```bash + admin@sonic:~$ sonic-clear switchcounters + Cleared switch counters + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#switch) + ## sFlow ### sFlow Show commands diff --git a/scripts/queuestat b/scripts/queuestat index 3a74e5cc57..3516386b11 100755 --- a/scripts/queuestat +++ b/scripts/queuestat @@ -44,12 +44,35 @@ from utilities_common.cli import json_serial, UserCache from utilities_common import constants import utilities_common.multi_asic as multi_asic_util -QueueStats = namedtuple("QueueStats", "queueindex, queuetype, totalpacket, totalbytes, droppacket, dropbytes, trimpacket") -VoqStats = namedtuple("VoqStats", "queueindex, queuetype, totalpacket, totalbytes, droppacket, dropbytes, creditWDpkts") -std_header = ['Port', 'TxQ', 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes'] -all_header = ['Port', 'TxQ', 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', 'Trim/pkts'] -trim_header = ['Port', 'TxQ', 'Trim/pkts'] -voq_header = ['Port', 'Voq', 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', 'Credit-WD-Del/pkts'] +QueueStats = namedtuple( + "QueueStats", "queueindex, queuetype, " + "totalpacket, totalbytes, droppacket, dropbytes, " + "trimpkt, trimsentpkt, trimdroppkt" +) +VoqStats = namedtuple( + "VoqStats", "queueindex, queuetype, " + "totalpacket, totalbytes, droppacket, dropbytes, " + "creditWDpkts" +) + +std_header = [ + 'Port', 'TxQ', + 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes' +] +all_header = [ + 'Port', 'TxQ', + 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', + 'Trim/pkts', 'TrimSent/pkts', 'TrimDrop/pkts' +] +trim_header = [ + 'Port', 'TxQ', + 'Trim/pkts', 'TrimSent/pkts', 'TrimDrop/pkts' +] +voq_header = [ + 'Port', 'Voq', + 'Counter/pkts', 'Counter/bytes', 'Drop/pkts', 'Drop/bytes', + 'Credit-WD-Del/pkts' +] counter_bucket_dict = { 'SAI_QUEUE_STAT_PACKETS': 2, @@ -59,6 +82,8 @@ counter_bucket_dict = { } trim_counter_bucket_dict = { 'SAI_QUEUE_STAT_TRIM_PACKETS': 6, + 'SAI_QUEUE_STAT_TX_TRIM_PACKETS': 7, + 'SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS': 8, } voq_counter_bucket_dict = { 'SAI_QUEUE_STAT_CREDIT_WD_DELETED_PACKETS': 6 @@ -141,10 +166,14 @@ def build_json(port, cnstat, all=False, trim=False, voq=False): "droppacket": k[4], "dropbytes": k[5], "trimpacket": k[6], + "trimsentpacket": k[7], + "trimdroppacket": k[8], } elif trim: # Packet Trimming related statistics p[k[1]] = { "trimpacket": k[2], + "trimsentpacket": k[3], + "trimdroppacket": k[4], } else: # Generic statistics p[k[1]] = { @@ -380,19 +409,19 @@ class Queuestat(object): if not non_zero or \ data['totalpacket'] != '0' or data['totalbytes'] != '0' or \ data['droppacket'] != '0' or data['dropbytes'] != '0' or \ - data['trimpacket'] != '0': + data['trimpkt'] != '0' or data['trimsentpkt'] != '0' or data['trimdroppkt'] != '0': table.append(( port, queuetag, data['totalpacket'], data['totalbytes'], data['droppacket'], data['dropbytes'], - data['trimpacket'] + data['trimpkt'], data['trimsentpkt'], data['trimdroppkt'] )) elif self.trim: # Packet Trimming related statistics if not non_zero or \ - data['trimpacket'] != '0': + data['trimpkt'] != '0' or data['trimsentpkt'] != '0' or data['trimdroppkt'] != '0': table.append(( port, queuetag, - data['trimpacket'] + data['trimpkt'], data['trimsentpkt'], data['trimdroppkt'] )) else: # Generic statistics if not non_zero or \ @@ -460,26 +489,30 @@ class Queuestat(object): totalbytes = ns_diff(cntr['totalbytes'], old_cntr['totalbytes']) droppacket = ns_diff(cntr['droppacket'], old_cntr['droppacket']) dropbytes = ns_diff(cntr['dropbytes'], old_cntr['dropbytes']) - trimpacket = ns_diff(cntr['trimpacket'], old_cntr['trimpacket']) + trimpkt = ns_diff(cntr['trimpkt'], old_cntr['trimpkt']) + trimsentpkt = ns_diff(cntr['trimsentpkt'], old_cntr['trimsentpkt']) + trimdroppkt = ns_diff(cntr['trimdroppkt'], old_cntr['trimdroppkt']) if not non_zero or \ totalpacket != '0' or totalbytes != '0' or \ droppacket != '0' or dropbytes != '0' or \ - trimpacket != '0': + trimpkt != '0' or trimsentpkt != '0' or trimdroppkt != '0': table.append(( port, queuetag, totalpacket, totalbytes, droppacket, dropbytes, - trimpacket + trimpkt, trimsentpkt, trimdroppkt )) elif self.trim: # Packet Trimming related statistics - trimpacket = ns_diff(cntr['trimpacket'], old_cntr['trimpacket']) + trimpkt = ns_diff(cntr['trimpkt'], old_cntr['trimpkt']) + trimsentpkt = ns_diff(cntr['trimsentpkt'], old_cntr['trimsentpkt']) + trimdroppkt = ns_diff(cntr['trimdroppkt'], old_cntr['trimdroppkt']) if not non_zero or \ - trimpacket != '0': + trimpkt != '0' or trimsentpkt != '0' or trimdroppkt != '0': table.append(( port, queuetag, - trimpacket + trimpkt, trimsentpkt, trimdroppkt )) else: # Generic statistics totalpacket = ns_diff(cntr['totalpacket'], old_cntr['totalpacket']) diff --git a/scripts/switchstat b/scripts/switchstat new file mode 100755 index 0000000000..90e9b488e8 --- /dev/null +++ b/scripts/switchstat @@ -0,0 +1,133 @@ +#!/usr/bin/python3 + +import click +import time + +from utilities_common import multi_asic, constants +from utilities_common.switchstat import SwitchStat + + +@click.command() +@click.pass_context +@click.option( + "-a", "--all", + help="Display all the stats counters", + is_flag=True, + default=False +) +@click.option( + "-t", "--trim", + help="Display trimming related statistics", + is_flag=True, + default=False +) +@click.option( + "-l", "--detail", + help="Display detailed statistics", + is_flag=True, + default=False +) +@click.option( + "-r", "--raw", + help="Display raw stats (unmodified output)", + is_flag=True, + default=False +) +@click.option( + "-p", "--period", + help="Display stats over a specified period (in seconds)", + type=click.INT, + default=0, + show_default=True +) +@click.option( + "-c", "--clear", "clear_stats", + help="Copy & clear stats", + is_flag=True, + default=False +) +@click.option( + "-ds", "--delete", "del_stats", + help="Delete saved stats, either the uid or the specified tag", + is_flag=True, + default=False +) +@click.option( + "-da", "--delete-all", "del_all_stats", + help="Delete all saved stats", + is_flag=True, + default=False +) +@click.option( + "-T", "--tag", + help="Save stats with name TAG", + default=None +) +@multi_asic.multi_asic_click_options +@click.option( + "-j", "--json", "json_fmt", + help="Display in JSON format", + is_flag=True, + default=False +) +@click.version_option("1.0", "-v", "--version") +@click.help_option("-h", "--help") +def main(ctx, all, trim, detail, raw, period, clear_stats, del_stats, del_all_stats, tag, display, namespace, json_fmt): + """ Display switch counters """ + + if clear_stats: # save counters for all namespaces + namespace = None + display = constants.DISPLAY_ALL + + switchstat = SwitchStat(namespace, display, tag) + + if del_all_stats: # remove all stats + click.echo("Removed all saved switch stats") + switchstat.remove_stats(all=True) + ctx.exit(0) + + if del_stats: # remove single stat with/without tag + click.echo("Removed saved switch stats{}".format("" if tag is None else " using tag {}".format(tag))) + switchstat.remove_stats() + ctx.exit(0) + + if clear_stats: # reset counters + cnstat_dict = switchstat.get_cnstat_dict(timestamp=True) + + try: + switchstat.save_stats(cnstat_dict) + except OSError as e: + ctx.fail("Failed to clear switch counters: {}".format(str(e))) + + click.echo("Cleared switch counters{}".format("" if tag is None else " using tag {}".format(tag))) + ctx.exit(0) + + if raw: # display raw stats + cnstat_dict = switchstat.get_cnstat_dict() + switchstat.cnstat_print(cnstat_dict, all, trim, detail, json_fmt) + ctx.exit(0) + + if period == 0: # do not wait - display stats diff + cnstat_cached_dict = {} + + if switchstat.is_cache_exists(): # cache exists - display stats diff + try: + cnstat_cached_dict = switchstat.load_stats() + except OSError as e: + ctx.fail("Failed to load saved switch counters: {}".format(str(e))) + + cnstat_dict = switchstat.get_cnstat_dict() + switchstat.cnstat_diff_print(cnstat_dict, cnstat_cached_dict, all, trim, detail, json_fmt) + else: # no cache - display raw stats + cnstat_dict = switchstat.get_cnstat_dict() + switchstat.cnstat_print(cnstat_dict, all, trim, detail, json_fmt) + else: # wait for n seconds - display stats diff + click.echo("The switch stats are calculated within {} seconds period".format(period)) + cnstat_cached_dict = switchstat.get_cnstat_dict(timestamp=True) + time.sleep(period) + cnstat_dict = switchstat.get_cnstat_dict() + switchstat.cnstat_diff_print(cnstat_dict, cnstat_cached_dict, all, trim, detail, json_fmt) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 248a5aff17..6f88ff83ec 100644 --- a/setup.py +++ b/setup.py @@ -171,6 +171,7 @@ 'scripts/storyteller', 'scripts/syseeprom-to-json', 'scripts/srv6stat', + 'scripts/switchstat', 'scripts/teamd_increase_retry_count.py', 'scripts/tempershow', 'scripts/tunnelstat', diff --git a/show/main.py b/show/main.py index c748e96cb4..a1a2839182 100755 --- a/show/main.py +++ b/show/main.py @@ -69,6 +69,7 @@ from . import bgp_cli from . import stp from . import srv6 +from . import switch from . import icmp from . import copp @@ -324,6 +325,7 @@ def cli(ctx): cli.add_command(dns.dns) cli.add_command(stp.spanning_tree) cli.add_command(srv6.srv6) +cli.add_command(switch.switch) cli.add_command(icmp.icmp) cli.add_command(copp.copp) diff --git a/show/switch.py b/show/switch.py new file mode 100644 index 0000000000..3fbfba71f5 --- /dev/null +++ b/show/switch.py @@ -0,0 +1,176 @@ +import click + +import utilities_common.cli as clicommon +from utilities_common import multi_asic + + +# +# Switch CLI ---------------------------------------------------------------------------------------------------------- +# + + +@click.group( + name="switch", + cls=clicommon.AliasedGroup +) +def switch(): + """ Show switch configuration """ + pass + + +@switch.group( + name="counters", + cls=clicommon.AliasedGroup, + invoke_without_command=True +) +@click.option( + "-p", "--period", + help="Display stats over a specified period (in seconds)", + type=click.INT, + default=0, + show_default=True +) +@multi_asic.multi_asic_click_options +@click.option( + "-j", "--json", "json_fmt", + help="Display in JSON format", + is_flag=True, + default=False +) +@click.option( + "-v", "--verbose", + help="Enable verbose output", + is_flag=True, + default=False +) +@click.pass_context +def counters(ctx, period, display, namespace, json_fmt, verbose): + """ Show switch counters """ + + if ctx.invoked_subcommand is not None: + return + + cmd = ["switchstat"] + + if period is not None: + cmd += ["-p", str(period)] + if display is not None: + cmd += ['-d', str(display)] + if namespace is not None: + cmd += ['-n', str(namespace)] + if json_fmt: + cmd += ['-j'] + + clicommon.run_command(cmd, display_cmd=verbose) + + +@counters.command( + name="all" +) +@click.option( + "-p", "--period", + help="Display stats over a specified period (in seconds)", + type=click.INT, + default=0, + show_default=True +) +@multi_asic.multi_asic_click_options +@click.option( + "-j", "--json", "json_fmt", + help="Display in JSON format", + is_flag=True, + default=False +) +@click.option( + "-v", "--verbose", + help="Enable verbose output", + is_flag=True, + default=False +) +def all_stats(period, display, namespace, json_fmt, verbose): + """ Show switch all stats """ + + cmd = ["switchstat", "--all"] + + if period is not None: + cmd += ["-p", str(period)] + if display is not None: + cmd += ['-d', str(display)] + if namespace is not None: + cmd += ['-n', str(namespace)] + if json_fmt: + cmd += ['-j'] + + clicommon.run_command(cmd, display_cmd=verbose) + + +@counters.command( + name="trim" +) +@click.option( + "-p", "--period", + help="Display stats over a specified period (in seconds)", + type=click.INT, + default=0, + show_default=True +) +@multi_asic.multi_asic_click_options +@click.option( + "-j", "--json", "json_fmt", + help="Display in JSON format", + is_flag=True, + default=False +) +@click.option( + "-v", "--verbose", + help="Enable verbose output", + is_flag=True, + default=False +) +def trim_stats(period, display, namespace, json_fmt, verbose): + """ Show switch trimming stats """ + + cmd = ["switchstat", "--trim"] + + if period is not None: + cmd += ["-p", str(period)] + if display is not None: + cmd += ['-d', str(display)] + if namespace is not None: + cmd += ['-n', str(namespace)] + if json_fmt: + cmd += ['-j'] + + clicommon.run_command(cmd, display_cmd=verbose) + + +@counters.command( + name="detailed" +) +@click.option( + "-p", "--period", + help="Display stats over a specified period (in seconds)", + type=click.INT, + default=0, + show_default=True +) +@multi_asic.multi_asic_click_options +@click.option( + "-v", "--verbose", + help="Enable verbose output", + is_flag=True, + default=False +) +def detailed_stats(period, display, namespace, verbose): + """ Show switch detailed stats """ + + cmd = ["switchstat", "--detail"] + + if period is not None: + cmd += ["-p", str(period)] + if display is not None: + cmd += ['-d', str(display)] + if namespace is not None: + cmd += ['-n', str(namespace)] + + clicommon.run_command(cmd, display_cmd=verbose) diff --git a/tests/clear_test.py b/tests/clear_test.py index f61c517e3c..ed3b31ff50 100644 --- a/tests/clear_test.py +++ b/tests/clear_test.py @@ -108,6 +108,13 @@ def test_clear_srv6counters(self, run_command): assert result.exit_code == 0 run_command.assert_called_with(['srv6stat', '-c']) + @patch('clear.main.run_command') + def test_clear_switchcounters(self, run_command): + runner = CliRunner() + result = runner.invoke(clear.cli.commands['switchcounters']) + assert result.exit_code == 0 + run_command.assert_called_with(['switchstat', '-c']) + def teardown(self): print('TEAR DOWN') diff --git a/tests/counterpoll_test.py b/tests/counterpoll_test.py index 55fdeff07a..93eb26577d 100644 --- a/tests/counterpoll_test.py +++ b/tests/counterpoll_test.py @@ -34,6 +34,7 @@ WRED_ECN_QUEUE_STAT 10000 enable WRED_ECN_PORT_STAT 1000 enable SRV6_STAT 10000 enable +SWITCH_STAT 60000 enable """ expected_counterpoll_show_dpu = """Type Interval (in ms) Status @@ -51,6 +52,7 @@ WRED_ECN_QUEUE_STAT 10000 enable WRED_ECN_PORT_STAT 1000 enable SRV6_STAT 10000 enable +SWITCH_STAT 60000 enable ENI_STAT 1000 enable """ @@ -361,6 +363,29 @@ def test_update_srv6_interval(self): table = db.cfgdb.get_table("FLEX_COUNTER_TABLE") assert test_interval == table["SRV6"]["POLL_INTERVAL"] + @pytest.mark.parametrize("status", ["disable", "enable"]) + def test_update_switch_status(self, status): + runner = CliRunner() + db = Db() + + result = runner.invoke(counterpoll.cli.commands["switch"].commands[status], [], obj=db) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + table = db.cfgdb.get_table("FLEX_COUNTER_TABLE") + assert status == table["SWITCH"]["FLEX_COUNTER_STATUS"] + + def test_update_switch_interval(self): + runner = CliRunner() + db = Db() + test_interval = "20000" + + result = runner.invoke(counterpoll.cli.commands["switch"].commands["interval"], [test_interval], obj=db) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + table = db.cfgdb.get_table("FLEX_COUNTER_TABLE") + assert test_interval == table["SWITCH"]["POLL_INTERVAL"] @classmethod def teardown_class(cls): diff --git a/tests/mock_tables/asic0/counters_db.json b/tests/mock_tables/asic0/counters_db.json index b85be89dbe..8e7eb56504 100644 --- a/tests/mock_tables/asic0/counters_db.json +++ b/tests/mock_tables/asic0/counters_db.json @@ -1855,7 +1855,9 @@ "SAI_PORT_STAT_PFC_5_TX_PKTS": "215", "SAI_PORT_STAT_PFC_6_TX_PKTS": "216", "SAI_PORT_STAT_PFC_7_TX_PKTS": "217", - "SAI_PORT_STAT_TRIM_PACKETS": "0" + "SAI_PORT_STAT_TRIM_PACKETS": "0", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "0", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "0" }, "COUNTERS:oid:0x1000000000004": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "4", @@ -1916,7 +1918,9 @@ "SAI_PORT_STAT_PFC_5_TX_PKTS": "415", "SAI_PORT_STAT_PFC_6_TX_PKTS": "416", "SAI_PORT_STAT_PFC_7_TX_PKTS": "417", - "SAI_PORT_STAT_TRIM_PACKETS": "100" + "SAI_PORT_STAT_TRIM_PACKETS": "100", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "50", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "50" }, "COUNTERS:oid:0x1000000000006": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "6", diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 517301c758..b6ea11d31f 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -1832,6 +1832,10 @@ "POLL_INTERVAL": "10000", "FLEX_COUNTER_STATUS": "enable" }, + "FLEX_COUNTER_TABLE|SWITCH": { + "POLL_INTERVAL": "60000", + "FLEX_COUNTER_STATUS": "enable" + }, "PFC_WD|Ethernet0": { "action": "drop", "detection_time": "600", diff --git a/tests/mock_tables/counters_db.json b/tests/mock_tables/counters_db.json index d4453cd4ec..7a213cc959 100644 --- a/tests/mock_tables/counters_db.json +++ b/tests/mock_tables/counters_db.json @@ -9,7 +9,9 @@ "SAI_QUEUE_STAT_WRED_ECN_MARKED_PACKETS": "0", "SAI_QUEUE_STAT_WRED_DROPPED_PACKETS": "0", "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "61", - "SAI_QUEUE_STAT_TRIM_PACKETS": "0" + "SAI_QUEUE_STAT_TRIM_PACKETS": "0", + "SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS": "0", + "SAI_QUEUE_STAT_TX_TRIM_PACKETS": "0" }, "COUNTERS:oid:0x15000000000358": { "SAI_QUEUE_STAT_WRED_DROPPED_BYTES": "43", @@ -21,7 +23,9 @@ "SAI_QUEUE_STAT_DROPPED_PACKETS": "39", "SAI_QUEUE_STAT_PACKETS": "60", "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "88", - "SAI_QUEUE_STAT_TRIM_PACKETS": "100" + "SAI_QUEUE_STAT_TRIM_PACKETS": "100", + "SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS": "50", + "SAI_QUEUE_STAT_TX_TRIM_PACKETS": "50" }, "COUNTERS:oid:0x15000000000359": { "SAI_QUEUE_STAT_WRED_DROPPED_BYTES": "7", @@ -220,7 +224,9 @@ "SAI_QUEUE_STAT_DROPPED_PACKETS": "70", "SAI_QUEUE_STAT_PACKETS": "41", "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "46", - "SAI_QUEUE_STAT_TRIM_PACKETS": "0" + "SAI_QUEUE_STAT_TRIM_PACKETS": "0", + "SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS": "0", + "SAI_QUEUE_STAT_TX_TRIM_PACKETS": "0" }, "COUNTERS:oid:0x15000000000380": { "SAI_QUEUE_STAT_WRED_DROPPED_BYTES": "49", @@ -232,7 +238,9 @@ "SAI_QUEUE_STAT_DROPPED_PACKETS": "63", "SAI_QUEUE_STAT_PACKETS": "18", "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "18", - "SAI_QUEUE_STAT_TRIM_PACKETS": "100" + "SAI_QUEUE_STAT_TRIM_PACKETS": "100", + "SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS": "50", + "SAI_QUEUE_STAT_TX_TRIM_PACKETS": "50" }, "COUNTERS:oid:0x15000000000381": { "SAI_QUEUE_STAT_WRED_DROPPED_BYTES": "90", @@ -431,7 +439,9 @@ "SAI_QUEUE_STAT_DROPPED_PACKETS": "0", "SAI_QUEUE_STAT_PACKETS": "0", "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "65", - "SAI_QUEUE_STAT_TRIM_PACKETS": "0" + "SAI_QUEUE_STAT_TRIM_PACKETS": "0", + "SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS": "0", + "SAI_QUEUE_STAT_TX_TRIM_PACKETS": "0" }, "COUNTERS:oid:0x150000000003a8": { "SAI_QUEUE_STAT_WRED_DROPPED_BYTES": "17", @@ -443,7 +453,9 @@ "SAI_QUEUE_STAT_DROPPED_PACKETS": "68", "SAI_QUEUE_STAT_PACKETS": "38", "SAI_QUEUE_STAT_SHARED_WATERMARK_BYTES": "94", - "SAI_QUEUE_STAT_TRIM_PACKETS": "100" + "SAI_QUEUE_STAT_TRIM_PACKETS": "100", + "SAI_QUEUE_STAT_DROPPED_TRIM_PACKETS": "50", + "SAI_QUEUE_STAT_TX_TRIM_PACKETS": "50" }, "COUNTERS:oid:0x150000000003a9": { "SAI_QUEUE_STAT_WRED_DROPPED_BYTES": "65", @@ -1264,7 +1276,9 @@ "SAI_PORT_STAT_IF_IN_FEC_CODEWORD_ERRORS_S13": "0", "SAI_PORT_STAT_IF_IN_FEC_CODEWORD_ERRORS_S14": "0", "SAI_PORT_STAT_IF_IN_FEC_CODEWORD_ERRORS_S15": "0", - "SAI_PORT_STAT_TRIM_PACKETS": "0" + "SAI_PORT_STAT_TRIM_PACKETS": "100", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "50", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "50" }, "COUNTERS:oid:0x1000000000013": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "4", @@ -1361,7 +1375,9 @@ "SAI_PORT_STAT_YELLOW_WRED_DROPPED_PACKETS":"33", "SAI_PORT_STAT_RED_WRED_DROPPED_PACKETS":"51", "SAI_PORT_STAT_WRED_DROPPED_PACKETS":"101", - "SAI_PORT_STAT_TRIM_PACKETS": "100" + "SAI_PORT_STAT_TRIM_PACKETS": "20000", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "10000", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "10000" }, "COUNTERS:oid:0x1000000000014": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "6", diff --git a/tests/portstat_db/counters_db.json b/tests/portstat_db/counters_db.json index c5a29c5c70..50d4cfd5b1 100644 --- a/tests/portstat_db/counters_db.json +++ b/tests/portstat_db/counters_db.json @@ -1238,7 +1238,9 @@ "SAI_PORT_STAT_IF_IN_FEC_CODEWORD_ERRORS_S13": "0", "SAI_PORT_STAT_IF_IN_FEC_CODEWORD_ERRORS_S14": "0", "SAI_PORT_STAT_IF_IN_FEC_CODEWORD_ERRORS_S15": "0", - "SAI_PORT_STAT_TRIM_PACKETS": "0" + "SAI_PORT_STAT_TRIM_PACKETS": "0", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "0", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "0" }, "COUNTERS:oid:0x1000000000013": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "4", @@ -1303,7 +1305,9 @@ "SAI_PORT_STAT_YELLOW_WRED_DROPPED_PACKETS":"33", "SAI_PORT_STAT_RED_WRED_DROPPED_PACKETS":"51", "SAI_PORT_STAT_WRED_DROPPED_PACKETS":"101", - "SAI_PORT_STAT_TRIM_PACKETS": "100" + "SAI_PORT_STAT_TRIM_PACKETS": "100", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "50", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "50" }, "COUNTERS:oid:0x1000000000014": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "6", @@ -1363,7 +1367,10 @@ "SAI_PORT_STAT_ETHER_STATS_JABBERS": "0", "SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "100317", "SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0", - "SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0" + "SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0", + "SAI_PORT_STAT_TRIM_PACKETS": "20000", + "SAI_PORT_STAT_DROPPED_TRIM_PACKETS": "10000", + "SAI_PORT_STAT_TX_TRIM_PACKETS": "10000" }, "COUNTERS:oid:0x1000000000015": { "SAI_PORT_STAT_IF_IN_UCAST_PKTS": "0", diff --git a/tests/portstat_input/assert_show_output.py b/tests/portstat_input/assert_show_output.py index 4a71c9a93e..9e678c50b6 100644 --- a/tests/portstat_input/assert_show_output.py +++ b/tests/portstat_input/assert_show_output.py @@ -3,94 +3,99 @@ """ trim_counters_all = """\ - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet0 D 0 -Ethernet4 N/A 100 -Ethernet8 N/A N/A + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS +--------- ------- ----------- -------------- --------------- +Ethernet0 D 100 50 50 +Ethernet4 N/A 20,000 10,000 10,000 +Ethernet8 N/A N/A N/A N/A """ trim_counters_all_json = """\ { "Ethernet0": { "STATE": "D", - "TRIM_PKTS": "0" + "TRIM_DRP_PKTS": "50", + "TRIM_PKTS": "100", + "TRIM_TX_PKTS": "50" }, "Ethernet4": { "STATE": "N/A", - "TRIM_PKTS": "100" + "TRIM_DRP_PKTS": "10,000", + "TRIM_PKTS": "20,000", + "TRIM_TX_PKTS": "10,000" }, "Ethernet8": { "STATE": "N/A", - "TRIM_PKTS": "N/A" + "TRIM_DRP_PKTS": "N/A", + "TRIM_PKTS": "N/A", + "TRIM_TX_PKTS": "N/A" } } """ trim_eth0_counters = """\ - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet0 D 0 + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS +--------- ------- ----------- -------------- --------------- +Ethernet0 D 100 50 50 """ trim_eth0_counters_json = """\ { "Ethernet0": { "STATE": "D", - "TRIM_PKTS": "0" + "TRIM_DRP_PKTS": "50", + "TRIM_PKTS": "100", + "TRIM_TX_PKTS": "50" } } """ trim_eth4_counters = """\ - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet4 N/A 100 + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS +--------- ------- ----------- -------------- --------------- +Ethernet4 N/A 20,000 10,000 10,000 """ trim_eth4_counters_json = """\ { "Ethernet4": { "STATE": "N/A", - "TRIM_PKTS": "100" + "TRIM_DRP_PKTS": "10,000", + "TRIM_PKTS": "20,000", + "TRIM_TX_PKTS": "10,000" } } """ trim_eth8_counters = """\ - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet8 N/A N/A + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS +--------- ------- ----------- -------------- --------------- +Ethernet8 N/A N/A N/A N/A """ trim_eth8_counters_json = """\ { "Ethernet8": { "STATE": "N/A", - "TRIM_PKTS": "N/A" + "TRIM_DRP_PKTS": "N/A", + "TRIM_PKTS": "N/A", + "TRIM_TX_PKTS": "N/A" } } """ trim_counters_period = """\ The rates are calculated within 3 seconds period - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet0 D 0 -Ethernet4 N/A 0 -Ethernet8 N/A N/A + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS +--------- ------- ----------- -------------- --------------- +Ethernet0 D 0 0 0 +Ethernet4 N/A 0 0 0 +Ethernet8 N/A N/A N/A N/A """ trim_counters_clear_msg = """\ Cleared counters """ trim_counters_clear_stat = """\ - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet0 D 0 -Ethernet4 N/A 0 -Ethernet8 N/A N/A -""" -trim_counters_clear_raw = """\ - IFACE STATE TRIM_PKTS ---------- ------- ----------- -Ethernet0 D 0 -Ethernet4 N/A 100 -Ethernet8 N/A N/A + IFACE STATE TRIM_PKTS TRIM_TX_PKTS TRIM_DRP_PKTS +--------- ------- ----------- -------------- --------------- +Ethernet0 D 0 0 0 +Ethernet4 N/A 0 0 0 +Ethernet8 N/A N/A N/A N/A """ diff --git a/tests/portstat_test.py b/tests/portstat_test.py index 070441b127..e148564b53 100644 --- a/tests/portstat_test.py +++ b/tests/portstat_test.py @@ -1,16 +1,15 @@ import pytest import logging - import os -import shutil - -from click.testing import CliRunner import clear.main as clear import show.main as show + +from click.testing import CliRunner +from utilities_common.cli import UserCache + from .utils import get_result_and_return_code from .portstat_input import assert_show_output -from utilities_common.cli import UserCache test_path = os.path.dirname(os.path.abspath(__file__)) modules_path = os.path.dirname(test_path) @@ -37,12 +36,12 @@ """ intf_counters_all = """\ - IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM ---------- ------- ------- ------------ ----------- --------- -------- -------- -------- ------- ------------ ----------- --------- -------- -------- -------- ------ -Ethernet0 D 8 2000.00 MB/s 247000.00/s 64.00% 10 100 N/A 10 1500.00 MB/s 183000.00/s 48.00% N/A N/A N/A 0 -Ethernet4 N/A 4 204.80 KB/s 200.00/s N/A 0 1,000 N/A 40 204.85 KB/s 201.00/s N/A N/A N/A N/A 100 -Ethernet8 N/A 6 1350.00 KB/s 9000.00/s N/A 100 10 N/A 60 13.37 MB/s 9000.00/s N/A N/A N/A N/A N/A -Ethernet9 N/A 0 0.00 B/s 0.00/s N/A 0 0 N/A 0 0.00 B/s 0.00/s N/A N/A N/A N/A N/A + IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM TRIM_TX TRIM_DRP +--------- ------- ------- ------------ ----------- --------- -------- -------- -------- ------- ------------ ----------- --------- -------- -------- -------- ------ --------- ---------- +Ethernet0 D 8 2000.00 MB/s 247000.00/s 64.00% 10 100 N/A 10 1500.00 MB/s 183000.00/s 48.00% N/A N/A N/A 0 0 0 +Ethernet4 N/A 4 204.80 KB/s 200.00/s N/A 0 1,000 N/A 40 204.85 KB/s 201.00/s N/A N/A N/A N/A 100 50 50 +Ethernet8 N/A 6 1350.00 KB/s 9000.00/s N/A 100 10 N/A 60 13.37 MB/s 9000.00/s N/A N/A N/A N/A 20,000 10,000 10,000 +Ethernet9 N/A 0 0.00 B/s 0.00/s N/A 0 0 N/A 0 0.00 B/s 0.00/s N/A N/A N/A N/A N/A N/A N/A """ # noqa: E501 intf_fec_counters = """\ @@ -153,45 +152,45 @@ """ multi_asic_external_intf_counters_printall = """\ - IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM ---------- ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ -Ethernet0 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A 0 -Ethernet4 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A 100 + IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM TRIM_TX TRIM_DRP +--------- ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ --------- ---------- +Ethernet0 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A 0 0 0 +Ethernet4 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A 100 50 50 Reminder: Please execute 'show interface counters -d all' to include internal links """ # noqa: E501 multi_asic_intf_counters_printall = """\ - IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM --------------- ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ - Ethernet0 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A 0 - Ethernet4 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A 100 - Ethernet-BP0 U 6 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 60 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A - Ethernet-BP4 U 8 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 80 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A -Ethernet-BP256 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A -Ethernet-BP260 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A + IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM TRIM_TX TRIM_DRP +-------------- ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ --------- ---------- + Ethernet0 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A 0 0 0 + Ethernet4 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A 100 50 50 + Ethernet-BP0 U 6 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 60 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A + Ethernet-BP4 U 8 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 80 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A +Ethernet-BP256 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A +Ethernet-BP260 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A Reminder: Please execute 'show interface counters -d all' to include internal links """ # noqa: E501 multi_asic_intf_counters_asic0_printall = """\ - IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM ------------- ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ - Ethernet0 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A 0 - Ethernet4 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A 100 -Ethernet-BP0 U 6 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 60 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A -Ethernet-BP4 U 8 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 80 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A + IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM TRIM_TX TRIM_DRP +------------ ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ --------- ---------- + Ethernet0 U 8 0.00 B/s 0.00/s 0.00% 10 100 N/A 10 0.00 B/s 0.00/s 0.00% N/A N/A N/A 0 0 0 + Ethernet4 U 4 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 40 0.00 B/s 0.00/s 0.00% N/A N/A N/A 100 50 50 +Ethernet-BP0 U 6 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 60 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A +Ethernet-BP4 U 8 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 80 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A Reminder: Please execute 'show interface counters -d all' to include internal links """ # noqa: E501 multi_asic_intf_counters_bp0 = """\ - IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM ------------- ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ -Ethernet-BP0 U 6 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 60 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A + IFACE STATE RX_OK RX_BPS RX_PPS RX_UTIL RX_ERR RX_DRP RX_OVR TX_OK TX_BPS TX_PPS TX_UTIL TX_ERR TX_DRP TX_OVR TRIM TRIM_TX TRIM_DRP +------------ ------- ------- -------- -------- --------- -------- -------- -------- ------- -------- -------- --------- -------- -------- -------- ------ --------- ---------- +Ethernet-BP0 U 6 0.00 B/s 0.00/s 0.00% 0 1,000 N/A 60 0.00 B/s 0.00/s 0.00% N/A N/A N/A N/A N/A N/A Reminder: Please execute 'show interface counters -d all' to include internal links @@ -294,7 +293,10 @@ WRED Red Dropped Packets....................... 51 WRED Total Dropped Packets..................... 101 -Packets Trimmed................................ 100 +Trimmed Packets................................ 100 +Trimmed Sent Packets........................... 50 +Trimmed Dropped Packets........................ 50 + Time Since Counters Last Cleared............... None """ diff --git a/tests/queuestat_input/assert_show_output.py b/tests/queuestat_input/assert_show_output.py index 2a446591be..aff334e2f6 100644 --- a/tests/queuestat_input/assert_show_output.py +++ b/tests/queuestat_input/assert_show_output.py @@ -4,108 +4,108 @@ counters_all = """\ For namespace : - Port TxQ Counter/pkts Counter/bytes Drop/pkts Drop/bytes Trim/pkts ---------- ----- -------------- --------------- ----------- ------------ ----------- -Ethernet0 UC0 0 0 0 0 0 -Ethernet0 UC1 60 43 39 1 100 -Ethernet0 UC2 82 7 39 21 N/A -Ethernet0 UC3 52 70 19 76 N/A -Ethernet0 UC4 11 59 12 94 N/A -Ethernet0 UC5 36 62 35 40 N/A -Ethernet0 UC6 49 91 2 88 N/A -Ethernet0 UC7 33 17 94 74 N/A -Ethernet0 UC8 40 71 95 33 N/A -Ethernet0 UC9 54 8 93 78 N/A -Ethernet0 MC10 83 96 74 9 N/A -Ethernet0 MC11 15 60 61 31 N/A -Ethernet0 MC12 45 52 82 94 N/A -Ethernet0 MC13 55 88 89 52 N/A -Ethernet0 MC14 14 70 95 79 N/A -Ethernet0 MC15 68 60 66 81 N/A -Ethernet0 MC16 63 4 48 76 N/A -Ethernet0 MC17 41 73 77 74 N/A -Ethernet0 MC18 60 21 56 54 N/A -Ethernet0 MC19 57 31 12 39 N/A -Ethernet0 ALL20 N/A N/A N/A N/A N/A -Ethernet0 ALL21 N/A N/A N/A N/A N/A -Ethernet0 ALL22 N/A N/A N/A N/A N/A -Ethernet0 ALL23 N/A N/A N/A N/A N/A -Ethernet0 ALL24 N/A N/A N/A N/A N/A -Ethernet0 ALL25 N/A N/A N/A N/A N/A -Ethernet0 ALL26 N/A N/A N/A N/A N/A -Ethernet0 ALL27 N/A N/A N/A N/A N/A -Ethernet0 ALL28 N/A N/A N/A N/A N/A -Ethernet0 ALL29 N/A N/A N/A N/A N/A + Port TxQ Counter/pkts Counter/bytes Drop/pkts Drop/bytes Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- -------------- --------------- ----------- ------------ ----------- --------------- --------------- +Ethernet0 UC0 0 0 0 0 0 0 0 +Ethernet0 UC1 60 43 39 1 100 50 50 +Ethernet0 UC2 82 7 39 21 N/A N/A N/A +Ethernet0 UC3 52 70 19 76 N/A N/A N/A +Ethernet0 UC4 11 59 12 94 N/A N/A N/A +Ethernet0 UC5 36 62 35 40 N/A N/A N/A +Ethernet0 UC6 49 91 2 88 N/A N/A N/A +Ethernet0 UC7 33 17 94 74 N/A N/A N/A +Ethernet0 UC8 40 71 95 33 N/A N/A N/A +Ethernet0 UC9 54 8 93 78 N/A N/A N/A +Ethernet0 MC10 83 96 74 9 N/A N/A N/A +Ethernet0 MC11 15 60 61 31 N/A N/A N/A +Ethernet0 MC12 45 52 82 94 N/A N/A N/A +Ethernet0 MC13 55 88 89 52 N/A N/A N/A +Ethernet0 MC14 14 70 95 79 N/A N/A N/A +Ethernet0 MC15 68 60 66 81 N/A N/A N/A +Ethernet0 MC16 63 4 48 76 N/A N/A N/A +Ethernet0 MC17 41 73 77 74 N/A N/A N/A +Ethernet0 MC18 60 21 56 54 N/A N/A N/A +Ethernet0 MC19 57 31 12 39 N/A N/A N/A +Ethernet0 ALL20 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL21 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL22 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL23 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL24 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL25 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL26 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL27 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL28 N/A N/A N/A N/A N/A N/A N/A +Ethernet0 ALL29 N/A N/A N/A N/A N/A N/A N/A For namespace : - Port TxQ Counter/pkts Counter/bytes Drop/pkts Drop/bytes Trim/pkts ---------- ----- -------------- --------------- ----------- ------------ ----------- -Ethernet4 UC0 41 96 70 98 0 -Ethernet4 UC1 18 49 63 36 100 -Ethernet4 UC2 99 90 3 15 N/A -Ethernet4 UC3 60 89 48 41 N/A -Ethernet4 UC4 8 84 82 94 N/A -Ethernet4 UC5 83 15 75 92 N/A -Ethernet4 UC6 84 26 50 71 N/A -Ethernet4 UC7 27 19 49 80 N/A -Ethernet4 UC8 13 89 13 33 N/A -Ethernet4 UC9 43 48 86 31 N/A -Ethernet4 MC10 50 1 57 82 N/A -Ethernet4 MC11 67 99 84 59 N/A -Ethernet4 MC12 4 58 27 5 N/A -Ethernet4 MC13 74 5 57 39 N/A -Ethernet4 MC14 21 59 4 14 N/A -Ethernet4 MC15 24 61 19 53 N/A -Ethernet4 MC16 51 15 15 32 N/A -Ethernet4 MC17 98 18 23 15 N/A -Ethernet4 MC18 41 34 9 57 N/A -Ethernet4 MC19 57 7 18 99 N/A -Ethernet4 ALL20 N/A N/A N/A N/A N/A -Ethernet4 ALL21 N/A N/A N/A N/A N/A -Ethernet4 ALL22 N/A N/A N/A N/A N/A -Ethernet4 ALL23 N/A N/A N/A N/A N/A -Ethernet4 ALL24 N/A N/A N/A N/A N/A -Ethernet4 ALL25 N/A N/A N/A N/A N/A -Ethernet4 ALL26 N/A N/A N/A N/A N/A -Ethernet4 ALL27 N/A N/A N/A N/A N/A -Ethernet4 ALL28 N/A N/A N/A N/A N/A -Ethernet4 ALL29 N/A N/A N/A N/A N/A + Port TxQ Counter/pkts Counter/bytes Drop/pkts Drop/bytes Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- -------------- --------------- ----------- ------------ ----------- --------------- --------------- +Ethernet4 UC0 41 96 70 98 0 0 0 +Ethernet4 UC1 18 49 63 36 100 50 50 +Ethernet4 UC2 99 90 3 15 N/A N/A N/A +Ethernet4 UC3 60 89 48 41 N/A N/A N/A +Ethernet4 UC4 8 84 82 94 N/A N/A N/A +Ethernet4 UC5 83 15 75 92 N/A N/A N/A +Ethernet4 UC6 84 26 50 71 N/A N/A N/A +Ethernet4 UC7 27 19 49 80 N/A N/A N/A +Ethernet4 UC8 13 89 13 33 N/A N/A N/A +Ethernet4 UC9 43 48 86 31 N/A N/A N/A +Ethernet4 MC10 50 1 57 82 N/A N/A N/A +Ethernet4 MC11 67 99 84 59 N/A N/A N/A +Ethernet4 MC12 4 58 27 5 N/A N/A N/A +Ethernet4 MC13 74 5 57 39 N/A N/A N/A +Ethernet4 MC14 21 59 4 14 N/A N/A N/A +Ethernet4 MC15 24 61 19 53 N/A N/A N/A +Ethernet4 MC16 51 15 15 32 N/A N/A N/A +Ethernet4 MC17 98 18 23 15 N/A N/A N/A +Ethernet4 MC18 41 34 9 57 N/A N/A N/A +Ethernet4 MC19 57 7 18 99 N/A N/A N/A +Ethernet4 ALL20 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL21 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL22 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL23 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL24 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL25 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL26 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL27 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL28 N/A N/A N/A N/A N/A N/A N/A +Ethernet4 ALL29 N/A N/A N/A N/A N/A N/A N/A For namespace : - Port TxQ Counter/pkts Counter/bytes Drop/pkts Drop/bytes Trim/pkts ---------- ----- -------------- --------------- ----------- ------------ ----------- -Ethernet8 UC0 0 0 0 0 0 -Ethernet8 UC1 38 17 68 91 100 -Ethernet8 UC2 16 65 79 51 N/A -Ethernet8 UC3 11 97 63 72 N/A -Ethernet8 UC4 54 89 62 62 N/A -Ethernet8 UC5 13 84 30 59 N/A -Ethernet8 UC6 49 67 99 85 N/A -Ethernet8 UC7 2 63 38 88 N/A -Ethernet8 UC8 0 82 93 43 N/A -Ethernet8 UC9 80 17 91 61 N/A -Ethernet8 MC10 81 63 76 73 N/A -Ethernet8 MC11 29 16 29 66 N/A -Ethernet8 MC12 32 12 61 35 N/A -Ethernet8 MC13 79 17 72 93 N/A -Ethernet8 MC14 23 21 67 50 N/A -Ethernet8 MC15 37 10 97 14 N/A -Ethernet8 MC16 30 17 74 43 N/A -Ethernet8 MC17 0 63 54 84 N/A -Ethernet8 MC18 69 88 24 79 N/A -Ethernet8 MC19 20 12 84 3 N/A -Ethernet8 ALL20 N/A N/A N/A N/A N/A -Ethernet8 ALL21 N/A N/A N/A N/A N/A -Ethernet8 ALL22 N/A N/A N/A N/A N/A -Ethernet8 ALL23 N/A N/A N/A N/A N/A -Ethernet8 ALL24 N/A N/A N/A N/A N/A -Ethernet8 ALL25 N/A N/A N/A N/A N/A -Ethernet8 ALL26 N/A N/A N/A N/A N/A -Ethernet8 ALL27 N/A N/A N/A N/A N/A -Ethernet8 ALL28 N/A N/A N/A N/A N/A -Ethernet8 ALL29 N/A N/A N/A N/A N/A + Port TxQ Counter/pkts Counter/bytes Drop/pkts Drop/bytes Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- -------------- --------------- ----------- ------------ ----------- --------------- --------------- +Ethernet8 UC0 0 0 0 0 0 0 0 +Ethernet8 UC1 38 17 68 91 100 50 50 +Ethernet8 UC2 16 65 79 51 N/A N/A N/A +Ethernet8 UC3 11 97 63 72 N/A N/A N/A +Ethernet8 UC4 54 89 62 62 N/A N/A N/A +Ethernet8 UC5 13 84 30 59 N/A N/A N/A +Ethernet8 UC6 49 67 99 85 N/A N/A N/A +Ethernet8 UC7 2 63 38 88 N/A N/A N/A +Ethernet8 UC8 0 82 93 43 N/A N/A N/A +Ethernet8 UC9 80 17 91 61 N/A N/A N/A +Ethernet8 MC10 81 63 76 73 N/A N/A N/A +Ethernet8 MC11 29 16 29 66 N/A N/A N/A +Ethernet8 MC12 32 12 61 35 N/A N/A N/A +Ethernet8 MC13 79 17 72 93 N/A N/A N/A +Ethernet8 MC14 23 21 67 50 N/A N/A N/A +Ethernet8 MC15 37 10 97 14 N/A N/A N/A +Ethernet8 MC16 30 17 74 43 N/A N/A N/A +Ethernet8 MC17 0 63 54 84 N/A N/A N/A +Ethernet8 MC18 69 88 24 79 N/A N/A N/A +Ethernet8 MC19 20 12 84 3 N/A N/A N/A +Ethernet8 ALL20 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL21 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL22 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL23 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL24 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL25 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL26 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL27 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL28 N/A N/A N/A N/A N/A N/A N/A +Ethernet8 ALL29 N/A N/A N/A N/A N/A N/A N/A -""" +""" # noqa: E501 counters_all_json = """\ { "Ethernet0": { @@ -114,210 +114,270 @@ "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { "dropbytes": "9", "droppacket": "74", "totalbytes": "96", "totalpacket": "83", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { "dropbytes": "31", "droppacket": "61", "totalbytes": "60", "totalpacket": "15", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { "dropbytes": "94", "droppacket": "82", "totalbytes": "52", "totalpacket": "45", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { "dropbytes": "52", "droppacket": "89", "totalbytes": "88", "totalpacket": "55", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { "dropbytes": "79", "droppacket": "95", "totalbytes": "70", "totalpacket": "14", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { "dropbytes": "81", "droppacket": "66", "totalbytes": "60", "totalpacket": "68", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { "dropbytes": "76", "droppacket": "48", "totalbytes": "4", "totalpacket": "63", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { "dropbytes": "74", "droppacket": "77", "totalbytes": "73", "totalpacket": "41", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { "dropbytes": "54", "droppacket": "56", "totalbytes": "21", "totalpacket": "60", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { "dropbytes": "39", "droppacket": "12", "totalbytes": "31", "totalpacket": "57", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { "dropbytes": "0", "droppacket": "0", "totalbytes": "0", "totalpacket": "0", - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { "dropbytes": "1", "droppacket": "39", "totalbytes": "43", "totalpacket": "60", - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { "dropbytes": "21", "droppacket": "39", "totalbytes": "7", "totalpacket": "82", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { "dropbytes": "76", "droppacket": "19", "totalbytes": "70", "totalpacket": "52", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { "dropbytes": "94", "droppacket": "12", "totalbytes": "59", "totalpacket": "11", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { "dropbytes": "40", "droppacket": "35", "totalbytes": "62", "totalpacket": "36", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { "dropbytes": "88", "droppacket": "2", "totalbytes": "91", "totalpacket": "49", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { "dropbytes": "74", "droppacket": "94", "totalbytes": "17", "totalpacket": "33", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { "dropbytes": "33", "droppacket": "95", "totalbytes": "71", "totalpacket": "40", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { "dropbytes": "78", "droppacket": "93", "totalbytes": "8", "totalpacket": "54", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } }, "Ethernet4": { @@ -326,210 +386,270 @@ "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { "dropbytes": "82", "droppacket": "57", "totalbytes": "1", "totalpacket": "50", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { "dropbytes": "59", "droppacket": "84", "totalbytes": "99", "totalpacket": "67", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { "dropbytes": "5", "droppacket": "27", "totalbytes": "58", "totalpacket": "4", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { "dropbytes": "39", "droppacket": "57", "totalbytes": "5", "totalpacket": "74", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { "dropbytes": "14", "droppacket": "4", "totalbytes": "59", "totalpacket": "21", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { "dropbytes": "53", "droppacket": "19", "totalbytes": "61", "totalpacket": "24", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { "dropbytes": "32", "droppacket": "15", "totalbytes": "15", "totalpacket": "51", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { "dropbytes": "15", "droppacket": "23", "totalbytes": "18", "totalpacket": "98", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { "dropbytes": "57", "droppacket": "9", "totalbytes": "34", "totalpacket": "41", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { "dropbytes": "99", "droppacket": "18", "totalbytes": "7", "totalpacket": "57", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { "dropbytes": "98", "droppacket": "70", "totalbytes": "96", "totalpacket": "41", - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { "dropbytes": "36", "droppacket": "63", "totalbytes": "49", "totalpacket": "18", - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { "dropbytes": "15", "droppacket": "3", "totalbytes": "90", "totalpacket": "99", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { "dropbytes": "41", "droppacket": "48", "totalbytes": "89", "totalpacket": "60", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { "dropbytes": "94", "droppacket": "82", "totalbytes": "84", "totalpacket": "8", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { "dropbytes": "92", "droppacket": "75", "totalbytes": "15", "totalpacket": "83", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { "dropbytes": "71", "droppacket": "50", "totalbytes": "26", "totalpacket": "84", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { "dropbytes": "80", "droppacket": "49", "totalbytes": "19", "totalpacket": "27", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { "dropbytes": "33", "droppacket": "13", "totalbytes": "89", "totalpacket": "13", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { "dropbytes": "31", "droppacket": "86", "totalbytes": "48", "totalpacket": "43", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } }, "Ethernet8": { @@ -538,210 +658,270 @@ "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { "dropbytes": "N/A", "droppacket": "N/A", "totalbytes": "N/A", "totalpacket": "N/A", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { "dropbytes": "73", "droppacket": "76", "totalbytes": "63", "totalpacket": "81", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { "dropbytes": "66", "droppacket": "29", "totalbytes": "16", "totalpacket": "29", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { "dropbytes": "35", "droppacket": "61", "totalbytes": "12", "totalpacket": "32", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { "dropbytes": "93", "droppacket": "72", "totalbytes": "17", "totalpacket": "79", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { "dropbytes": "50", "droppacket": "67", "totalbytes": "21", "totalpacket": "23", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { "dropbytes": "14", "droppacket": "97", "totalbytes": "10", "totalpacket": "37", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { "dropbytes": "43", "droppacket": "74", "totalbytes": "17", "totalpacket": "30", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { "dropbytes": "84", "droppacket": "54", "totalbytes": "63", "totalpacket": "0", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { "dropbytes": "79", "droppacket": "24", "totalbytes": "88", "totalpacket": "69", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { "dropbytes": "3", "droppacket": "84", "totalbytes": "12", "totalpacket": "20", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { "dropbytes": "0", "droppacket": "0", "totalbytes": "0", "totalpacket": "0", - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { "dropbytes": "91", "droppacket": "68", "totalbytes": "17", "totalpacket": "38", - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { "dropbytes": "51", "droppacket": "79", "totalbytes": "65", "totalpacket": "16", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { "dropbytes": "72", "droppacket": "63", "totalbytes": "97", "totalpacket": "11", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { "dropbytes": "62", "droppacket": "62", "totalbytes": "89", "totalpacket": "54", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { "dropbytes": "59", "droppacket": "30", "totalbytes": "84", "totalpacket": "13", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { "dropbytes": "85", "droppacket": "99", "totalbytes": "67", "totalpacket": "49", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { "dropbytes": "88", "droppacket": "38", "totalbytes": "63", "totalpacket": "2", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { "dropbytes": "43", "droppacket": "93", "totalbytes": "82", "totalpacket": "0", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { "dropbytes": "61", "droppacket": "91", "totalbytes": "17", "totalpacket": "80", - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } } } @@ -749,384 +929,564 @@ trim_counters_all = """\ For namespace : - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet0 UC0 0 -Ethernet0 UC1 100 -Ethernet0 UC2 N/A -Ethernet0 UC3 N/A -Ethernet0 UC4 N/A -Ethernet0 UC5 N/A -Ethernet0 UC6 N/A -Ethernet0 UC7 N/A -Ethernet0 UC8 N/A -Ethernet0 UC9 N/A -Ethernet0 MC10 N/A -Ethernet0 MC11 N/A -Ethernet0 MC12 N/A -Ethernet0 MC13 N/A -Ethernet0 MC14 N/A -Ethernet0 MC15 N/A -Ethernet0 MC16 N/A -Ethernet0 MC17 N/A -Ethernet0 MC18 N/A -Ethernet0 MC19 N/A -Ethernet0 ALL20 N/A -Ethernet0 ALL21 N/A -Ethernet0 ALL22 N/A -Ethernet0 ALL23 N/A -Ethernet0 ALL24 N/A -Ethernet0 ALL25 N/A -Ethernet0 ALL26 N/A -Ethernet0 ALL27 N/A -Ethernet0 ALL28 N/A -Ethernet0 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet0 UC0 0 0 0 +Ethernet0 UC1 100 50 50 +Ethernet0 UC2 N/A N/A N/A +Ethernet0 UC3 N/A N/A N/A +Ethernet0 UC4 N/A N/A N/A +Ethernet0 UC5 N/A N/A N/A +Ethernet0 UC6 N/A N/A N/A +Ethernet0 UC7 N/A N/A N/A +Ethernet0 UC8 N/A N/A N/A +Ethernet0 UC9 N/A N/A N/A +Ethernet0 MC10 N/A N/A N/A +Ethernet0 MC11 N/A N/A N/A +Ethernet0 MC12 N/A N/A N/A +Ethernet0 MC13 N/A N/A N/A +Ethernet0 MC14 N/A N/A N/A +Ethernet0 MC15 N/A N/A N/A +Ethernet0 MC16 N/A N/A N/A +Ethernet0 MC17 N/A N/A N/A +Ethernet0 MC18 N/A N/A N/A +Ethernet0 MC19 N/A N/A N/A +Ethernet0 ALL20 N/A N/A N/A +Ethernet0 ALL21 N/A N/A N/A +Ethernet0 ALL22 N/A N/A N/A +Ethernet0 ALL23 N/A N/A N/A +Ethernet0 ALL24 N/A N/A N/A +Ethernet0 ALL25 N/A N/A N/A +Ethernet0 ALL26 N/A N/A N/A +Ethernet0 ALL27 N/A N/A N/A +Ethernet0 ALL28 N/A N/A N/A +Ethernet0 ALL29 N/A N/A N/A For namespace : - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet4 UC0 0 -Ethernet4 UC1 100 -Ethernet4 UC2 N/A -Ethernet4 UC3 N/A -Ethernet4 UC4 N/A -Ethernet4 UC5 N/A -Ethernet4 UC6 N/A -Ethernet4 UC7 N/A -Ethernet4 UC8 N/A -Ethernet4 UC9 N/A -Ethernet4 MC10 N/A -Ethernet4 MC11 N/A -Ethernet4 MC12 N/A -Ethernet4 MC13 N/A -Ethernet4 MC14 N/A -Ethernet4 MC15 N/A -Ethernet4 MC16 N/A -Ethernet4 MC17 N/A -Ethernet4 MC18 N/A -Ethernet4 MC19 N/A -Ethernet4 ALL20 N/A -Ethernet4 ALL21 N/A -Ethernet4 ALL22 N/A -Ethernet4 ALL23 N/A -Ethernet4 ALL24 N/A -Ethernet4 ALL25 N/A -Ethernet4 ALL26 N/A -Ethernet4 ALL27 N/A -Ethernet4 ALL28 N/A -Ethernet4 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet4 UC0 0 0 0 +Ethernet4 UC1 100 50 50 +Ethernet4 UC2 N/A N/A N/A +Ethernet4 UC3 N/A N/A N/A +Ethernet4 UC4 N/A N/A N/A +Ethernet4 UC5 N/A N/A N/A +Ethernet4 UC6 N/A N/A N/A +Ethernet4 UC7 N/A N/A N/A +Ethernet4 UC8 N/A N/A N/A +Ethernet4 UC9 N/A N/A N/A +Ethernet4 MC10 N/A N/A N/A +Ethernet4 MC11 N/A N/A N/A +Ethernet4 MC12 N/A N/A N/A +Ethernet4 MC13 N/A N/A N/A +Ethernet4 MC14 N/A N/A N/A +Ethernet4 MC15 N/A N/A N/A +Ethernet4 MC16 N/A N/A N/A +Ethernet4 MC17 N/A N/A N/A +Ethernet4 MC18 N/A N/A N/A +Ethernet4 MC19 N/A N/A N/A +Ethernet4 ALL20 N/A N/A N/A +Ethernet4 ALL21 N/A N/A N/A +Ethernet4 ALL22 N/A N/A N/A +Ethernet4 ALL23 N/A N/A N/A +Ethernet4 ALL24 N/A N/A N/A +Ethernet4 ALL25 N/A N/A N/A +Ethernet4 ALL26 N/A N/A N/A +Ethernet4 ALL27 N/A N/A N/A +Ethernet4 ALL28 N/A N/A N/A +Ethernet4 ALL29 N/A N/A N/A For namespace : - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet8 UC0 0 -Ethernet8 UC1 100 -Ethernet8 UC2 N/A -Ethernet8 UC3 N/A -Ethernet8 UC4 N/A -Ethernet8 UC5 N/A -Ethernet8 UC6 N/A -Ethernet8 UC7 N/A -Ethernet8 UC8 N/A -Ethernet8 UC9 N/A -Ethernet8 MC10 N/A -Ethernet8 MC11 N/A -Ethernet8 MC12 N/A -Ethernet8 MC13 N/A -Ethernet8 MC14 N/A -Ethernet8 MC15 N/A -Ethernet8 MC16 N/A -Ethernet8 MC17 N/A -Ethernet8 MC18 N/A -Ethernet8 MC19 N/A -Ethernet8 ALL20 N/A -Ethernet8 ALL21 N/A -Ethernet8 ALL22 N/A -Ethernet8 ALL23 N/A -Ethernet8 ALL24 N/A -Ethernet8 ALL25 N/A -Ethernet8 ALL26 N/A -Ethernet8 ALL27 N/A -Ethernet8 ALL28 N/A -Ethernet8 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet8 UC0 0 0 0 +Ethernet8 UC1 100 50 50 +Ethernet8 UC2 N/A N/A N/A +Ethernet8 UC3 N/A N/A N/A +Ethernet8 UC4 N/A N/A N/A +Ethernet8 UC5 N/A N/A N/A +Ethernet8 UC6 N/A N/A N/A +Ethernet8 UC7 N/A N/A N/A +Ethernet8 UC8 N/A N/A N/A +Ethernet8 UC9 N/A N/A N/A +Ethernet8 MC10 N/A N/A N/A +Ethernet8 MC11 N/A N/A N/A +Ethernet8 MC12 N/A N/A N/A +Ethernet8 MC13 N/A N/A N/A +Ethernet8 MC14 N/A N/A N/A +Ethernet8 MC15 N/A N/A N/A +Ethernet8 MC16 N/A N/A N/A +Ethernet8 MC17 N/A N/A N/A +Ethernet8 MC18 N/A N/A N/A +Ethernet8 MC19 N/A N/A N/A +Ethernet8 ALL20 N/A N/A N/A +Ethernet8 ALL21 N/A N/A N/A +Ethernet8 ALL22 N/A N/A N/A +Ethernet8 ALL23 N/A N/A N/A +Ethernet8 ALL24 N/A N/A N/A +Ethernet8 ALL25 N/A N/A N/A +Ethernet8 ALL26 N/A N/A N/A +Ethernet8 ALL27 N/A N/A N/A +Ethernet8 ALL28 N/A N/A N/A +Ethernet8 ALL29 N/A N/A N/A """ trim_counters_all_json = """\ { "Ethernet0": { "ALL20": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } }, "Ethernet4": { "ALL20": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } }, "Ethernet8": { "ALL20": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } } } @@ -1134,132 +1494,192 @@ trim_eth0_counters = """\ For namespace : - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet0 UC0 0 -Ethernet0 UC1 100 -Ethernet0 UC2 N/A -Ethernet0 UC3 N/A -Ethernet0 UC4 N/A -Ethernet0 UC5 N/A -Ethernet0 UC6 N/A -Ethernet0 UC7 N/A -Ethernet0 UC8 N/A -Ethernet0 UC9 N/A -Ethernet0 MC10 N/A -Ethernet0 MC11 N/A -Ethernet0 MC12 N/A -Ethernet0 MC13 N/A -Ethernet0 MC14 N/A -Ethernet0 MC15 N/A -Ethernet0 MC16 N/A -Ethernet0 MC17 N/A -Ethernet0 MC18 N/A -Ethernet0 MC19 N/A -Ethernet0 ALL20 N/A -Ethernet0 ALL21 N/A -Ethernet0 ALL22 N/A -Ethernet0 ALL23 N/A -Ethernet0 ALL24 N/A -Ethernet0 ALL25 N/A -Ethernet0 ALL26 N/A -Ethernet0 ALL27 N/A -Ethernet0 ALL28 N/A -Ethernet0 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet0 UC0 0 0 0 +Ethernet0 UC1 100 50 50 +Ethernet0 UC2 N/A N/A N/A +Ethernet0 UC3 N/A N/A N/A +Ethernet0 UC4 N/A N/A N/A +Ethernet0 UC5 N/A N/A N/A +Ethernet0 UC6 N/A N/A N/A +Ethernet0 UC7 N/A N/A N/A +Ethernet0 UC8 N/A N/A N/A +Ethernet0 UC9 N/A N/A N/A +Ethernet0 MC10 N/A N/A N/A +Ethernet0 MC11 N/A N/A N/A +Ethernet0 MC12 N/A N/A N/A +Ethernet0 MC13 N/A N/A N/A +Ethernet0 MC14 N/A N/A N/A +Ethernet0 MC15 N/A N/A N/A +Ethernet0 MC16 N/A N/A N/A +Ethernet0 MC17 N/A N/A N/A +Ethernet0 MC18 N/A N/A N/A +Ethernet0 MC19 N/A N/A N/A +Ethernet0 ALL20 N/A N/A N/A +Ethernet0 ALL21 N/A N/A N/A +Ethernet0 ALL22 N/A N/A N/A +Ethernet0 ALL23 N/A N/A N/A +Ethernet0 ALL24 N/A N/A N/A +Ethernet0 ALL25 N/A N/A N/A +Ethernet0 ALL26 N/A N/A N/A +Ethernet0 ALL27 N/A N/A N/A +Ethernet0 ALL28 N/A N/A N/A +Ethernet0 ALL29 N/A N/A N/A """ trim_eth0_counters_json = """\ { "Ethernet0": { "ALL20": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } } } @@ -1267,132 +1687,192 @@ trim_eth4_counters = """\ For namespace : - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet4 UC0 0 -Ethernet4 UC1 100 -Ethernet4 UC2 N/A -Ethernet4 UC3 N/A -Ethernet4 UC4 N/A -Ethernet4 UC5 N/A -Ethernet4 UC6 N/A -Ethernet4 UC7 N/A -Ethernet4 UC8 N/A -Ethernet4 UC9 N/A -Ethernet4 MC10 N/A -Ethernet4 MC11 N/A -Ethernet4 MC12 N/A -Ethernet4 MC13 N/A -Ethernet4 MC14 N/A -Ethernet4 MC15 N/A -Ethernet4 MC16 N/A -Ethernet4 MC17 N/A -Ethernet4 MC18 N/A -Ethernet4 MC19 N/A -Ethernet4 ALL20 N/A -Ethernet4 ALL21 N/A -Ethernet4 ALL22 N/A -Ethernet4 ALL23 N/A -Ethernet4 ALL24 N/A -Ethernet4 ALL25 N/A -Ethernet4 ALL26 N/A -Ethernet4 ALL27 N/A -Ethernet4 ALL28 N/A -Ethernet4 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet4 UC0 0 0 0 +Ethernet4 UC1 100 50 50 +Ethernet4 UC2 N/A N/A N/A +Ethernet4 UC3 N/A N/A N/A +Ethernet4 UC4 N/A N/A N/A +Ethernet4 UC5 N/A N/A N/A +Ethernet4 UC6 N/A N/A N/A +Ethernet4 UC7 N/A N/A N/A +Ethernet4 UC8 N/A N/A N/A +Ethernet4 UC9 N/A N/A N/A +Ethernet4 MC10 N/A N/A N/A +Ethernet4 MC11 N/A N/A N/A +Ethernet4 MC12 N/A N/A N/A +Ethernet4 MC13 N/A N/A N/A +Ethernet4 MC14 N/A N/A N/A +Ethernet4 MC15 N/A N/A N/A +Ethernet4 MC16 N/A N/A N/A +Ethernet4 MC17 N/A N/A N/A +Ethernet4 MC18 N/A N/A N/A +Ethernet4 MC19 N/A N/A N/A +Ethernet4 ALL20 N/A N/A N/A +Ethernet4 ALL21 N/A N/A N/A +Ethernet4 ALL22 N/A N/A N/A +Ethernet4 ALL23 N/A N/A N/A +Ethernet4 ALL24 N/A N/A N/A +Ethernet4 ALL25 N/A N/A N/A +Ethernet4 ALL26 N/A N/A N/A +Ethernet4 ALL27 N/A N/A N/A +Ethernet4 ALL28 N/A N/A N/A +Ethernet4 ALL29 N/A N/A N/A """ trim_eth4_counters_json = """\ { "Ethernet4": { "ALL20": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } } } @@ -1400,132 +1880,192 @@ trim_eth8_counters = """\ For namespace : - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet8 UC0 0 -Ethernet8 UC1 100 -Ethernet8 UC2 N/A -Ethernet8 UC3 N/A -Ethernet8 UC4 N/A -Ethernet8 UC5 N/A -Ethernet8 UC6 N/A -Ethernet8 UC7 N/A -Ethernet8 UC8 N/A -Ethernet8 UC9 N/A -Ethernet8 MC10 N/A -Ethernet8 MC11 N/A -Ethernet8 MC12 N/A -Ethernet8 MC13 N/A -Ethernet8 MC14 N/A -Ethernet8 MC15 N/A -Ethernet8 MC16 N/A -Ethernet8 MC17 N/A -Ethernet8 MC18 N/A -Ethernet8 MC19 N/A -Ethernet8 ALL20 N/A -Ethernet8 ALL21 N/A -Ethernet8 ALL22 N/A -Ethernet8 ALL23 N/A -Ethernet8 ALL24 N/A -Ethernet8 ALL25 N/A -Ethernet8 ALL26 N/A -Ethernet8 ALL27 N/A -Ethernet8 ALL28 N/A -Ethernet8 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet8 UC0 0 0 0 +Ethernet8 UC1 100 50 50 +Ethernet8 UC2 N/A N/A N/A +Ethernet8 UC3 N/A N/A N/A +Ethernet8 UC4 N/A N/A N/A +Ethernet8 UC5 N/A N/A N/A +Ethernet8 UC6 N/A N/A N/A +Ethernet8 UC7 N/A N/A N/A +Ethernet8 UC8 N/A N/A N/A +Ethernet8 UC9 N/A N/A N/A +Ethernet8 MC10 N/A N/A N/A +Ethernet8 MC11 N/A N/A N/A +Ethernet8 MC12 N/A N/A N/A +Ethernet8 MC13 N/A N/A N/A +Ethernet8 MC14 N/A N/A N/A +Ethernet8 MC15 N/A N/A N/A +Ethernet8 MC16 N/A N/A N/A +Ethernet8 MC17 N/A N/A N/A +Ethernet8 MC18 N/A N/A N/A +Ethernet8 MC19 N/A N/A N/A +Ethernet8 ALL20 N/A N/A N/A +Ethernet8 ALL21 N/A N/A N/A +Ethernet8 ALL22 N/A N/A N/A +Ethernet8 ALL23 N/A N/A N/A +Ethernet8 ALL24 N/A N/A N/A +Ethernet8 ALL25 N/A N/A N/A +Ethernet8 ALL26 N/A N/A N/A +Ethernet8 ALL27 N/A N/A N/A +Ethernet8 ALL28 N/A N/A N/A +Ethernet8 ALL29 N/A N/A N/A """ trim_eth8_counters_json = """\ { "Ethernet8": { "ALL20": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL21": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL22": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL23": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL24": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL25": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL26": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL27": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL28": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "ALL29": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC10": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC11": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC12": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC13": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC14": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC15": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC16": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC17": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC18": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "MC19": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC0": { - "trimpacket": "0" + "trimdroppacket": "0", + "trimpacket": "0", + "trimsentpacket": "0" }, "UC1": { - "trimpacket": "100" + "trimdroppacket": "50", + "trimpacket": "100", + "trimsentpacket": "50" }, "UC2": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC3": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC4": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC5": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC6": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC7": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC8": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" }, "UC9": { - "trimpacket": "N/A" + "trimdroppacket": "N/A", + "trimpacket": "N/A", + "trimsentpacket": "N/A" } } } @@ -1537,102 +2077,102 @@ Clear and update saved counters for Ethernet8 """ trim_counters_clear_stat = """\ - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet0 UC0 0 -Ethernet0 UC1 0 -Ethernet0 UC2 N/A -Ethernet0 UC3 N/A -Ethernet0 UC4 N/A -Ethernet0 UC5 N/A -Ethernet0 UC6 N/A -Ethernet0 UC7 N/A -Ethernet0 UC8 N/A -Ethernet0 UC9 N/A -Ethernet0 MC10 N/A -Ethernet0 MC11 N/A -Ethernet0 MC12 N/A -Ethernet0 MC13 N/A -Ethernet0 MC14 N/A -Ethernet0 MC15 N/A -Ethernet0 MC16 N/A -Ethernet0 MC17 N/A -Ethernet0 MC18 N/A -Ethernet0 MC19 N/A -Ethernet0 ALL20 N/A -Ethernet0 ALL21 N/A -Ethernet0 ALL22 N/A -Ethernet0 ALL23 N/A -Ethernet0 ALL24 N/A -Ethernet0 ALL25 N/A -Ethernet0 ALL26 N/A -Ethernet0 ALL27 N/A -Ethernet0 ALL28 N/A -Ethernet0 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet0 UC0 0 0 0 +Ethernet0 UC1 0 0 0 +Ethernet0 UC2 N/A N/A N/A +Ethernet0 UC3 N/A N/A N/A +Ethernet0 UC4 N/A N/A N/A +Ethernet0 UC5 N/A N/A N/A +Ethernet0 UC6 N/A N/A N/A +Ethernet0 UC7 N/A N/A N/A +Ethernet0 UC8 N/A N/A N/A +Ethernet0 UC9 N/A N/A N/A +Ethernet0 MC10 N/A N/A N/A +Ethernet0 MC11 N/A N/A N/A +Ethernet0 MC12 N/A N/A N/A +Ethernet0 MC13 N/A N/A N/A +Ethernet0 MC14 N/A N/A N/A +Ethernet0 MC15 N/A N/A N/A +Ethernet0 MC16 N/A N/A N/A +Ethernet0 MC17 N/A N/A N/A +Ethernet0 MC18 N/A N/A N/A +Ethernet0 MC19 N/A N/A N/A +Ethernet0 ALL20 N/A N/A N/A +Ethernet0 ALL21 N/A N/A N/A +Ethernet0 ALL22 N/A N/A N/A +Ethernet0 ALL23 N/A N/A N/A +Ethernet0 ALL24 N/A N/A N/A +Ethernet0 ALL25 N/A N/A N/A +Ethernet0 ALL26 N/A N/A N/A +Ethernet0 ALL27 N/A N/A N/A +Ethernet0 ALL28 N/A N/A N/A +Ethernet0 ALL29 N/A N/A N/A - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet4 UC0 0 -Ethernet4 UC1 0 -Ethernet4 UC2 N/A -Ethernet4 UC3 N/A -Ethernet4 UC4 N/A -Ethernet4 UC5 N/A -Ethernet4 UC6 N/A -Ethernet4 UC7 N/A -Ethernet4 UC8 N/A -Ethernet4 UC9 N/A -Ethernet4 MC10 N/A -Ethernet4 MC11 N/A -Ethernet4 MC12 N/A -Ethernet4 MC13 N/A -Ethernet4 MC14 N/A -Ethernet4 MC15 N/A -Ethernet4 MC16 N/A -Ethernet4 MC17 N/A -Ethernet4 MC18 N/A -Ethernet4 MC19 N/A -Ethernet4 ALL20 N/A -Ethernet4 ALL21 N/A -Ethernet4 ALL22 N/A -Ethernet4 ALL23 N/A -Ethernet4 ALL24 N/A -Ethernet4 ALL25 N/A -Ethernet4 ALL26 N/A -Ethernet4 ALL27 N/A -Ethernet4 ALL28 N/A -Ethernet4 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet4 UC0 0 0 0 +Ethernet4 UC1 0 0 0 +Ethernet4 UC2 N/A N/A N/A +Ethernet4 UC3 N/A N/A N/A +Ethernet4 UC4 N/A N/A N/A +Ethernet4 UC5 N/A N/A N/A +Ethernet4 UC6 N/A N/A N/A +Ethernet4 UC7 N/A N/A N/A +Ethernet4 UC8 N/A N/A N/A +Ethernet4 UC9 N/A N/A N/A +Ethernet4 MC10 N/A N/A N/A +Ethernet4 MC11 N/A N/A N/A +Ethernet4 MC12 N/A N/A N/A +Ethernet4 MC13 N/A N/A N/A +Ethernet4 MC14 N/A N/A N/A +Ethernet4 MC15 N/A N/A N/A +Ethernet4 MC16 N/A N/A N/A +Ethernet4 MC17 N/A N/A N/A +Ethernet4 MC18 N/A N/A N/A +Ethernet4 MC19 N/A N/A N/A +Ethernet4 ALL20 N/A N/A N/A +Ethernet4 ALL21 N/A N/A N/A +Ethernet4 ALL22 N/A N/A N/A +Ethernet4 ALL23 N/A N/A N/A +Ethernet4 ALL24 N/A N/A N/A +Ethernet4 ALL25 N/A N/A N/A +Ethernet4 ALL26 N/A N/A N/A +Ethernet4 ALL27 N/A N/A N/A +Ethernet4 ALL28 N/A N/A N/A +Ethernet4 ALL29 N/A N/A N/A - Port TxQ Trim/pkts ---------- ----- ----------- -Ethernet8 UC0 0 -Ethernet8 UC1 0 -Ethernet8 UC2 N/A -Ethernet8 UC3 N/A -Ethernet8 UC4 N/A -Ethernet8 UC5 N/A -Ethernet8 UC6 N/A -Ethernet8 UC7 N/A -Ethernet8 UC8 N/A -Ethernet8 UC9 N/A -Ethernet8 MC10 N/A -Ethernet8 MC11 N/A -Ethernet8 MC12 N/A -Ethernet8 MC13 N/A -Ethernet8 MC14 N/A -Ethernet8 MC15 N/A -Ethernet8 MC16 N/A -Ethernet8 MC17 N/A -Ethernet8 MC18 N/A -Ethernet8 MC19 N/A -Ethernet8 ALL20 N/A -Ethernet8 ALL21 N/A -Ethernet8 ALL22 N/A -Ethernet8 ALL23 N/A -Ethernet8 ALL24 N/A -Ethernet8 ALL25 N/A -Ethernet8 ALL26 N/A -Ethernet8 ALL27 N/A -Ethernet8 ALL28 N/A -Ethernet8 ALL29 N/A + Port TxQ Trim/pkts TrimSent/pkts TrimDrop/pkts +--------- ----- ----------- --------------- --------------- +Ethernet8 UC0 0 0 0 +Ethernet8 UC1 0 0 0 +Ethernet8 UC2 N/A N/A N/A +Ethernet8 UC3 N/A N/A N/A +Ethernet8 UC4 N/A N/A N/A +Ethernet8 UC5 N/A N/A N/A +Ethernet8 UC6 N/A N/A N/A +Ethernet8 UC7 N/A N/A N/A +Ethernet8 UC8 N/A N/A N/A +Ethernet8 UC9 N/A N/A N/A +Ethernet8 MC10 N/A N/A N/A +Ethernet8 MC11 N/A N/A N/A +Ethernet8 MC12 N/A N/A N/A +Ethernet8 MC13 N/A N/A N/A +Ethernet8 MC14 N/A N/A N/A +Ethernet8 MC15 N/A N/A N/A +Ethernet8 MC16 N/A N/A N/A +Ethernet8 MC17 N/A N/A N/A +Ethernet8 MC18 N/A N/A N/A +Ethernet8 MC19 N/A N/A N/A +Ethernet8 ALL20 N/A N/A N/A +Ethernet8 ALL21 N/A N/A N/A +Ethernet8 ALL22 N/A N/A N/A +Ethernet8 ALL23 N/A N/A N/A +Ethernet8 ALL24 N/A N/A N/A +Ethernet8 ALL25 N/A N/A N/A +Ethernet8 ALL26 N/A N/A N/A +Ethernet8 ALL27 N/A N/A N/A +Ethernet8 ALL28 N/A N/A N/A +Ethernet8 ALL29 N/A N/A N/A """ diff --git a/tests/show_test.py b/tests/show_test.py index b7de66a619..6a9717476a 100644 --- a/tests/show_test.py +++ b/tests/show_test.py @@ -2,6 +2,7 @@ import sys import click import pytest +import logging import importlib import subprocess import show.main as show @@ -28,6 +29,10 @@ NAT Zones """ +logger = logging.getLogger(__name__) + +SUCCESS = 0 + class TestShowRunAllCommands(object): @classmethod @@ -1278,3 +1283,123 @@ def test_srv6_stats_with_sid(self, mock_run_command): def teardown(self): print('TEAR DOWN') + + +class TestShowSwitchCounters(object): + @classmethod + def setup_class(cls): + logger.info("Setup class: {}".format(cls.__name__)) + + @classmethod + def teardown_class(cls): + logger.info("Teardown class: {}".format(cls.__name__)) + + def verify_stats(self, mock_run_command, command, options, expected): + runner = CliRunner() + result = runner.invoke(command, options) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + assert result.exit_code == SUCCESS + mock_run_command.assert_called_once_with(expected, display_cmd=True) + + @pytest.mark.parametrize( + "command, options, expected", [ + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["all"], + ["--verbose"], + ["switchstat", "--all", "-p", "0", "-d", "all"], + id="plain-all" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["trim"], + ["--verbose"], + ["switchstat", "--trim", "-p", "0", "-d", "all"], + id="plain-trim" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"], + ["--verbose"], + ["switchstat", "-p", "0", "-d", "all"], + id="plain-std" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["detailed"], + ["--verbose"], + ["switchstat", "--detail", "-p", "0", "-d", "all"], + id="plain-detail" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["all"], + ["--json", "--verbose"], + ["switchstat", "--all", "-p", "0", "-d", "all", "-j"], + id="json-all" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["trim"], + ["--json", "--verbose"], + ["switchstat", "--trim", "-p", "0", "-d", "all", "-j"], + id="json-trim" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"], + ["--json", "--verbose"], + ["switchstat", "-p", "0", "-d", "all", "-j"], + id="json-std" + ) + ] + ) + @patch("utilities_common.cli.run_command") + def test_switch_stats(self, mock_run_command, command, options, expected): + self.verify_stats(mock_run_command, command, options, expected) + + @pytest.mark.parametrize( + "command, options, expected", [ + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["all"], + ["--period", "1", "--verbose"], + ["switchstat", "--all", "-p", "1", "-d", "all"], + id="plain-all" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["trim"], + ["--period", "2", "--verbose"], + ["switchstat", "--trim", "-p", "2", "-d", "all"], + id="plain-trim" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"], + ["--period", "3", "--verbose"], + ["switchstat", "-p", "3", "-d", "all"], + id="plain-std" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["detailed"], + ["--period", "4", "--verbose"], + ["switchstat", "--detail", "-p", "4", "-d", "all"], + id="plain-detail" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["all"], + ["--period", "1", "--json", "--verbose"], + ["switchstat", "--all", "-p", "1", "-d", "all", "-j"], + id="json-all" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"].commands["trim"], + ["--period", "2", "--json", "--verbose"], + ["switchstat", "--trim", "-p", "2", "-d", "all", "-j"], + id="json-trim" + ), + pytest.param( + show.cli.commands["switch"].commands["counters"], + ["--period", "3", "--json", "--verbose"], + ["switchstat", "-p", "3", "-d", "all", "-j"], + id="json-std" + ) + ] + ) + @patch("utilities_common.cli.run_command") + def test_switch_stats_period(self, mock_run_command, command, options, expected): + self.verify_stats(mock_run_command, command, options, expected) diff --git a/tests/switchstat_input/common_output.py b/tests/switchstat_input/common_output.py new file mode 100644 index 0000000000..36a42c4e6a --- /dev/null +++ b/tests/switchstat_input/common_output.py @@ -0,0 +1,29 @@ +""" +Module holding the correct values for show CLI command outputs for the switchstat_test.py +""" + +# clear section ----------------------------------------------------------------------------------------------------- # + +show_switch_clear = """\ +Cleared switch counters +""" + +show_switch_clear_tag1 = """\ +Cleared switch counters using tag test1 +""" + +show_switch_clear_tag2 = """\ +Cleared switch counters using tag test2 +""" + +show_switch_delete = """\ +Removed saved switch stats +""" + +show_switch_delete_tag1 = """\ +Removed saved switch stats using tag test1 +""" + +show_switch_delete_all = """\ +Removed all saved switch stats +""" diff --git a/tests/switchstat_input/mock_counters/all.json b/tests/switchstat_input/mock_counters/all.json new file mode 100644 index 0000000000..9f6cf744e5 --- /dev/null +++ b/tests/switchstat_input/mock_counters/all.json @@ -0,0 +1,9 @@ +{ + "COUNTERS_SWITCH_NAME_MAP": { + "ASIC": "oid:0x21000000000000" + }, + "COUNTERS:oid:0x21000000000000": { + "SAI_SWITCH_STAT_DROPPED_TRIM_PACKETS": "1000", + "SAI_SWITCH_STAT_TX_TRIM_PACKETS": "2000" + } +} diff --git a/tests/switchstat_input/mock_counters/empty.json b/tests/switchstat_input/mock_counters/empty.json new file mode 100644 index 0000000000..9db6e4c5c8 --- /dev/null +++ b/tests/switchstat_input/mock_counters/empty.json @@ -0,0 +1,8 @@ +{ + "COUNTERS_SWITCH_NAME_MAP": { + "ASIC": "oid:0x21000000000000" + }, + "COUNTERS:oid:0x21000000000000": { + "NULL": "NULL" + } +} diff --git a/tests/switchstat_input/mock_counters/no_counters.json b/tests/switchstat_input/mock_counters/no_counters.json new file mode 100644 index 0000000000..8463c20d9b --- /dev/null +++ b/tests/switchstat_input/mock_counters/no_counters.json @@ -0,0 +1,5 @@ +{ + "COUNTERS_SWITCH_NAME_MAP": { + "ASIC": "oid:0x21000000000000" + } +} diff --git a/tests/switchstat_input/mock_counters/no_map.json b/tests/switchstat_input/mock_counters/no_map.json new file mode 100644 index 0000000000..088bbcbac5 --- /dev/null +++ b/tests/switchstat_input/mock_counters/no_map.json @@ -0,0 +1,6 @@ +{ + "COUNTERS:oid:0x21000000000000": { + "SAI_SWITCH_STAT_DROPPED_TRIM_PACKETS": "1000", + "SAI_SWITCH_STAT_TX_TRIM_PACKETS": "2000" + } +} diff --git a/tests/switchstat_input/mock_counters/partial.json b/tests/switchstat_input/mock_counters/partial.json new file mode 100644 index 0000000000..4c8dbe731f --- /dev/null +++ b/tests/switchstat_input/mock_counters/partial.json @@ -0,0 +1,8 @@ +{ + "COUNTERS_SWITCH_NAME_MAP": { + "ASIC": "oid:0x21000000000000" + }, + "COUNTERS:oid:0x21000000000000": { + "SAI_SWITCH_STAT_DROPPED_TRIM_PACKETS": "1000" + } +} diff --git a/tests/switchstat_input/mock_counters/updated.json b/tests/switchstat_input/mock_counters/updated.json new file mode 100644 index 0000000000..343b63e3ce --- /dev/null +++ b/tests/switchstat_input/mock_counters/updated.json @@ -0,0 +1,9 @@ +{ + "COUNTERS_SWITCH_NAME_MAP": { + "ASIC": "oid:0x21000000000000" + }, + "COUNTERS:oid:0x21000000000000": { + "SAI_SWITCH_STAT_DROPPED_TRIM_PACKETS": "1500", + "SAI_SWITCH_STAT_TX_TRIM_PACKETS": "2500" + } +} diff --git a/tests/switchstat_input/mock_counters/updated_tag.json b/tests/switchstat_input/mock_counters/updated_tag.json new file mode 100644 index 0000000000..80137a650f --- /dev/null +++ b/tests/switchstat_input/mock_counters/updated_tag.json @@ -0,0 +1,9 @@ +{ + "COUNTERS_SWITCH_NAME_MAP": { + "ASIC": "oid:0x21000000000000" + }, + "COUNTERS:oid:0x21000000000000": { + "SAI_SWITCH_STAT_DROPPED_TRIM_PACKETS": "2000", + "SAI_SWITCH_STAT_TX_TRIM_PACKETS": "3000" + } +} diff --git a/tests/switchstat_input/multi_asic_output.py b/tests/switchstat_input/multi_asic_output.py new file mode 100644 index 0000000000..f46b277fe5 --- /dev/null +++ b/tests/switchstat_input/multi_asic_output.py @@ -0,0 +1,233 @@ +""" +Module holding the correct values for show CLI command outputs for the switchstat_test.py +""" + +# show section ------------------------------------------------------------------------------------------------------ # + +show_switch_all = """\ +Namespace: asic0 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 2,000 1,000 + +Namespace: asic1 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 2,000 1,000 + +Namespace: asic2 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 2,000 1,000 +""" +show_switch_all_json = """\ +{ + "asic0": { + "trim_drop": "1,000", + "trim_sent": "2,000" + }, + "asic1": { + "trim_drop": "1,000", + "trim_sent": "2,000" + }, + "asic2": { + "trim_drop": "1,000", + "trim_sent": "2,000" + } +} +""" + +show_switch_detailed = """\ +Namespace: asic0 + +Trimmed Sent Packets........................... 2,000 +Trimmed Dropped Packets........................ 1,000 + +Namespace: asic1 + +Trimmed Sent Packets........................... 2,000 +Trimmed Dropped Packets........................ 1,000 + +Namespace: asic2 + +Trimmed Sent Packets........................... 2,000 +Trimmed Dropped Packets........................ 1,000 +""" + +# diff section -------------------------------------------------------------------------------------------------------# + +show_switch_updated = """\ +Namespace: asic0 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 500 500 + +Namespace: asic1 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 500 500 + +Namespace: asic2 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 500 500 +""" +show_switch_updated_json = """\ +{ + "asic0": { + "trim_drop": "500", + "trim_sent": "500" + }, + "asic1": { + "trim_drop": "500", + "trim_sent": "500" + }, + "asic2": { + "trim_drop": "500", + "trim_sent": "500" + } +} +""" + +show_switch_updated_detailed = """\ +Namespace: asic0 + +Trimmed Sent Packets........................... 500 +Trimmed Dropped Packets........................ 500 + + +Namespace: asic1 + +Trimmed Sent Packets........................... 500 +Trimmed Dropped Packets........................ 500 + + +Namespace: asic2 + +Trimmed Sent Packets........................... 500 +Trimmed Dropped Packets........................ 500 +""" + +# negative section ---------------------------------------------------------------------------------------------------# + +show_switch_neg_na = """\ +Namespace: asic0 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A N/A + +Namespace: asic1 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A N/A + +Namespace: asic2 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A N/A +""" +show_switch_neg_na_json = """\ +{ + "asic0": { + "trim_drop": "N/A", + "trim_sent": "N/A" + }, + "asic1": { + "trim_drop": "N/A", + "trim_sent": "N/A" + }, + "asic2": { + "trim_drop": "N/A", + "trim_sent": "N/A" + } +} +""" + +show_switch_neg_partial = """\ +Namespace: asic0 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A 1,000 + +Namespace: asic1 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A 1,000 + +Namespace: asic2 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A 1,000 +""" +show_switch_neg_partial_json = """\ +{ + "asic0": { + "trim_drop": "1,000", + "trim_sent": "N/A" + }, + "asic1": { + "trim_drop": "1,000", + "trim_sent": "N/A" + }, + "asic2": { + "trim_drop": "1,000", + "trim_sent": "N/A" + } +} +""" + +# period section ---------------------------------------------------------------------------------------------------- # + +show_switch_period = """\ +The switch stats are calculated within 1 seconds period +Namespace: asic0 + +Trimmed Sent Packets........................... 0 +Trimmed Dropped Packets........................ 0 + + +Namespace: asic1 + +Trimmed Sent Packets........................... 0 +Trimmed Dropped Packets........................ 0 + + +Namespace: asic2 + +Trimmed Sent Packets........................... 0 +Trimmed Dropped Packets........................ 0 +""" + +# clear section ----------------------------------------------------------------------------------------------------- # + +show_switch_updated_tag = """\ +Namespace: asic0 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 1,000 1,000 + +Namespace: asic1 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 1,000 1,000 + +Namespace: asic2 + + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 1,000 1,000 +""" diff --git a/tests/switchstat_input/single_asic_output.py b/tests/switchstat_input/single_asic_output.py new file mode 100644 index 0000000000..6925225162 --- /dev/null +++ b/tests/switchstat_input/single_asic_output.py @@ -0,0 +1,83 @@ +""" +Module holding the correct values for show CLI command outputs for the switchstat_test.py +""" + +# show section ------------------------------------------------------------------------------------------------------ # + +show_switch_all = """\ + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 2,000 1,000 +""" +show_switch_all_json = """\ +{ + "trim_drop": "1,000", + "trim_sent": "2,000" +} +""" + +show_switch_detailed = """\ +Trimmed Sent Packets........................... 2,000 +Trimmed Dropped Packets........................ 1,000 +""" + +# diff section -------------------------------------------------------------------------------------------------------# + +show_switch_updated = """\ + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 500 500 +""" +show_switch_updated_json = """\ +{ + "trim_drop": "500", + "trim_sent": "500" +} +""" + +show_switch_updated_detailed = """\ +Trimmed Sent Packets........................... 500 +Trimmed Dropped Packets........................ 500 +""" + +# negative section ---------------------------------------------------------------------------------------------------# + +show_switch_neg_na = """\ + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A N/A +""" +show_switch_neg_na_json = """\ +{ + "trim_drop": "N/A", + "trim_sent": "N/A" +} +""" + +show_switch_neg_partial = """\ + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + N/A 1,000 +""" +show_switch_neg_partial_json = """\ +{ + "trim_drop": "1,000", + "trim_sent": "N/A" +} +""" + +# period section ---------------------------------------------------------------------------------------------------- # + +show_switch_period = """\ +The switch stats are calculated within 1 seconds period +Trimmed Sent Packets........................... 0 +Trimmed Dropped Packets........................ 0 +""" + +# clear section ----------------------------------------------------------------------------------------------------- # + +show_switch_updated_tag = """\ + TrimSent/pkts TrimDrop/pkts +--------------- --------------- + 1,000 1,000 +""" diff --git a/tests/switchstat_test.py b/tests/switchstat_test.py new file mode 100644 index 0000000000..fb09d3f63f --- /dev/null +++ b/tests/switchstat_test.py @@ -0,0 +1,388 @@ +import pytest +import importlib +import logging +import os + +from click.testing import CliRunner +from utilities_common.db import Db +from utilities_common.cli import UserCache +from utilities_common.general import load_module_from_source + +from .mock_tables import dbconnector +from .switchstat_input import single_asic_output +from .switchstat_input import multi_asic_output +from .switchstat_input import common_output + + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, "scripts") +input_path = os.path.join(test_path, "switchstat_input") +mock_counters_path = os.path.join(input_path, "mock_counters") +switchstat_path = os.path.join(scripts_path, "switchstat") +switchstat = load_module_from_source("switchstat", switchstat_path) + +logger = logging.getLogger(__name__) + +SUCCESS = 0 + + +def remove_tmp_cnstat_file(): + cache = UserCache("switchstat") + cache.remove_all() + + +class TestSwitchStat(object): + show_switch_period = None + + show_switch_updated = None + show_switch_updated_tag = None + show_switch_all = None + + def remove_timestamp(self, output): + lines = output.splitlines() + pattern = "Time Since Counters Last Cleared" + return "\n".join([line for line in lines if pattern not in line]) + + def verify_counters(self, options, output, timestamp=False): + db = Db() + runner = CliRunner() + + result = runner.invoke(switchstat.main, options, obj=db) + out = self.remove_timestamp(result.output) if timestamp else result.output + rc = result.exit_code + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + assert out == output + assert rc == SUCCESS + + def verify_counters_show(self, options, output): + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "all") + self.verify_counters(options, output) + + def verify_counters_diff(self, options, output): + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "all") + self.verify_counters(["--clear"], common_output.show_switch_clear) + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "updated") + self.verify_counters(options, output, "--detail" in options) + self.verify_counters(["--delete"], common_output.show_switch_delete) + + def verify_counters_neg(self, options, output): + self.verify_counters(options, output) + + def verify_counters_period(self): + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "all") + self.verify_counters(["--detail", "--period", "1"], self.show_switch_period, timestamp=True) + + def verify_counters_clear(self): + # regular stats clear + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "all") + self.verify_counters(["--clear"], common_output.show_switch_clear) + + # stats clear by tag 1 + self.verify_counters(["--clear", "--tag", "test1"], common_output.show_switch_clear_tag1) + + # stats clear by tag 2 + self.verify_counters(["--clear", "--tag", "test2"], common_output.show_switch_clear_tag2) + + # verify regular stats clear + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "updated") + self.verify_counters(["--all"], self.show_switch_updated) + + # verify stats clear by tag 1 + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "updated_tag") + self.verify_counters(["--all", "--tag", "test1"], self.show_switch_updated_tag) + + # verify raw stats + dbconnector.dedicated_dbs["COUNTERS_DB"] = os.path.join(mock_counters_path, "all") + self.verify_counters(["--all", "--raw"], self.show_switch_all) + + # delete regular stats snapshot + self.verify_counters(["--delete"], common_output.show_switch_delete) + + # verify no regular stats snapshot + self.verify_counters(["--all"], self.show_switch_all) + + # delete stats snapshot by tag 1 + self.verify_counters(["--delete", "--tag", "test1"], common_output.show_switch_delete_tag1) + + # verify no stats snapshot by tag 1 + self.verify_counters(["--all", "--tag", "test1"], self.show_switch_all) + + # delete all stats snapshots + self.verify_counters(["--delete-all"], common_output.show_switch_delete_all) + + # verify no stats snapshot by tag 2 + self.verify_counters(["--all", "--tag", "test2"], self.show_switch_all) + + +class TestSingleAsicSwitchStat(TestSwitchStat): + @classmethod + def setup_class(cls): + logger.info("Setup class: {}".format(cls.__name__)) + from .mock_tables import mock_single_asic + importlib.reload(mock_single_asic) + dbconnector.clean_up_config() + dbconnector.load_database_config() + remove_tmp_cnstat_file() + + @classmethod + def teardown_class(cls): + logger.info("Teardown class: {}".format(cls.__name__)) + dbconnector.dedicated_dbs.clear() + dbconnector.clean_up_config() + remove_tmp_cnstat_file() + + @pytest.fixture(scope="class") + def period_data(self, request): + logger.info("Initialize single asic counters period data") + request.cls.show_switch_period = single_asic_output.show_switch_period + + yield + + request.cls.show_switch_period = None + logger.info("Deinitialize single asic counters period data") + + @pytest.fixture(scope="class") + def clear_data(self, request): + logger.info("Initialize single asic counters clear data") + + request.cls.show_switch_updated = single_asic_output.show_switch_updated + request.cls.show_switch_updated_tag = single_asic_output.show_switch_updated_tag + request.cls.show_switch_all = single_asic_output.show_switch_all + + yield + + request.cls.show_switch_updated = None + request.cls.show_switch_updated_tag = None + request.cls.show_switch_all = None + logger.info("Deinitialize single asic counters clear data") + + # ---------- SHOW SINGLE ASIC SWITCH COUNTERS ---------- # + + @pytest.mark.parametrize( + "options, output", [ + pytest.param(["--all"], single_asic_output.show_switch_all, id="plain-all"), + pytest.param(["--trim"], single_asic_output.show_switch_all, id="plain-trim"), + pytest.param([], single_asic_output.show_switch_all, id="plain-std"), + pytest.param(["--detail"], single_asic_output.show_switch_detailed, id="plain-detail"), + pytest.param(["--all", "--json"], single_asic_output.show_switch_all_json, id="json-all"), + pytest.param(["--trim", "--json"], single_asic_output.show_switch_all_json, id="json-trim"), + pytest.param(["--json"], single_asic_output.show_switch_all_json, id="json-std") + ] + ) + def test_counters_show(self, options, output): + self.verify_counters_show(options, output) + + @pytest.mark.parametrize( + "options, output", [ + pytest.param(["--all"], single_asic_output.show_switch_updated, id="plain-all"), + pytest.param(["--trim"], single_asic_output.show_switch_updated, id="plain-trim"), + pytest.param([], single_asic_output.show_switch_updated, id="plain-std"), + pytest.param(["--detail"], single_asic_output.show_switch_updated_detailed, id="plain-detail"), + pytest.param(["--all", "--json"], single_asic_output.show_switch_updated_json, id="json-all"), + pytest.param(["--trim", "--json"], single_asic_output.show_switch_updated_json, id="json-trim"), + pytest.param(["--json"], single_asic_output.show_switch_updated_json, id="json-std") + ] + ) + def test_counters_show_diff(self, options, output): + self.verify_counters_diff(options, output) + + @pytest.mark.parametrize( + "options, output, cntdb", [ + pytest.param( + ["--all"], + single_asic_output.show_switch_neg_na, + os.path.join(mock_counters_path, "no_map"), + id="no-map-plain-all" + ), + pytest.param( + ["--all", "--json"], + single_asic_output.show_switch_neg_na_json, + os.path.join(mock_counters_path, "no_map"), + id="no-map-json-all" + ), + pytest.param( + ["--all"], + single_asic_output.show_switch_neg_na, + os.path.join(mock_counters_path, "no_counters"), + id="no-cnt-plain-all" + ), + pytest.param( + ["--all", "--json"], + single_asic_output.show_switch_neg_na_json, + os.path.join(mock_counters_path, "no_counters"), + id="no-cnt-json-all" + ), + pytest.param( + ["--all"], + single_asic_output.show_switch_neg_na, + os.path.join(mock_counters_path, "empty"), + id="empty-plain-all" + ), + pytest.param( + ["--all", "--json"], + single_asic_output.show_switch_neg_na_json, + os.path.join(mock_counters_path, "empty"), + id="empty-json-all" + ), + pytest.param( + ["--all"], + single_asic_output.show_switch_neg_partial, + os.path.join(mock_counters_path, "partial"), + id="partial-plain-all" + ), + pytest.param( + ["--all", "--json"], + single_asic_output.show_switch_neg_partial_json, + os.path.join(mock_counters_path, "partial"), + id="partial-json-all" + ) + ] + ) + def test_counters_show_neg(self, options, output, cntdb): + dbconnector.dedicated_dbs["COUNTERS_DB"] = cntdb + self.verify_counters_neg(options, output) + + def test_counters_period(self, period_data): + self.verify_counters_period() + + def test_counters_clear(self, clear_data): + self.verify_counters_clear() + + +class TestMultiAsicSwitchStat(TestSwitchStat): + @classmethod + def setup_class(cls): + logger.info("Setup class: {}".format(cls.__name__)) + from .mock_tables import mock_multi_asic_3_asics + importlib.reload(mock_multi_asic_3_asics) + dbconnector.clean_up_config() + dbconnector.load_namespace_config() + remove_tmp_cnstat_file() + + @classmethod + def teardown_class(cls): + logger.info("Teardown class: {}".format(cls.__name__)) + dbconnector.dedicated_dbs.clear() + dbconnector.clean_up_config() + remove_tmp_cnstat_file() + + @pytest.fixture(scope="class") + def period_data(self, request): + logger.info("Initialize multi asic counters period data") + request.cls.show_switch_period = multi_asic_output.show_switch_period + + yield + + request.cls.show_switch_period = None + logger.info("Deinitialize multi asic counters period data") + + @pytest.fixture(scope="class") + def clear_data(self, request): + logger.info("Initialize multi asic counters clear data") + + request.cls.show_switch_updated = multi_asic_output.show_switch_updated + request.cls.show_switch_updated_tag = multi_asic_output.show_switch_updated_tag + request.cls.show_switch_all = multi_asic_output.show_switch_all + + yield + + request.cls.show_switch_updated = None + request.cls.show_switch_updated_tag = None + request.cls.show_switch_all = None + logger.info("Deinitialize multi asic counters clear data") + + # ---------- SHOW MULTI ASIC SWITCH COUNTERS ---------- # + + @pytest.mark.parametrize( + "options, output", [ + pytest.param(["--all"], multi_asic_output.show_switch_all, id="plain-all"), + pytest.param(["--trim"], multi_asic_output.show_switch_all, id="plain-trim"), + pytest.param([], multi_asic_output.show_switch_all, id="plain-std"), + pytest.param(["--detail"], multi_asic_output.show_switch_detailed, id="plain-detail"), + pytest.param(["--all", "--json"], multi_asic_output.show_switch_all_json, id="json-all"), + pytest.param(["--trim", "--json"], multi_asic_output.show_switch_all_json, id="json-trim"), + pytest.param(["--json"], multi_asic_output.show_switch_all_json, id="json-std") + ] + ) + def test_counters_show(self, options, output): + self.verify_counters_show(options, output) + + @pytest.mark.parametrize( + "options, output", [ + pytest.param(["--all"], multi_asic_output.show_switch_updated, id="plain-all"), + pytest.param(["--trim"], multi_asic_output.show_switch_updated, id="plain-trim"), + pytest.param([], multi_asic_output.show_switch_updated, id="plain-std"), + pytest.param(["--detail"], multi_asic_output.show_switch_updated_detailed, id="plain-detail"), + pytest.param(["--all", "--json"], multi_asic_output.show_switch_updated_json, id="json-all"), + pytest.param(["--trim", "--json"], multi_asic_output.show_switch_updated_json, id="json-trim"), + pytest.param(["--json"], multi_asic_output.show_switch_updated_json, id="json-std") + ] + ) + def test_counters_show_diff(self, options, output): + self.verify_counters_diff(options, output) + + @pytest.mark.parametrize( + "options, output, cntdb", [ + pytest.param( + ["--all"], + multi_asic_output.show_switch_neg_na, + os.path.join(mock_counters_path, "no_map"), + id="no-map-plain-all" + ), + pytest.param( + ["--all", "--json"], + multi_asic_output.show_switch_neg_na_json, + os.path.join(mock_counters_path, "no_map"), + id="no-map-json-all" + ), + pytest.param( + ["--all"], + multi_asic_output.show_switch_neg_na, + os.path.join(mock_counters_path, "no_counters"), + id="no-cnt-plain-all" + ), + pytest.param( + ["--all", "--json"], + multi_asic_output.show_switch_neg_na_json, + os.path.join(mock_counters_path, "no_counters"), + id="no-cnt-json-all" + ), + pytest.param( + ["--all"], + multi_asic_output.show_switch_neg_na, + os.path.join(mock_counters_path, "empty"), + id="empty-plain-all" + ), + pytest.param( + ["--all", "--json"], + multi_asic_output.show_switch_neg_na_json, + os.path.join(mock_counters_path, "empty"), + id="empty-json-all" + ), + pytest.param( + ["--all"], + multi_asic_output.show_switch_neg_partial, + os.path.join(mock_counters_path, "partial"), + id="partial-plain-all" + ), + pytest.param( + ["--all", "--json"], + multi_asic_output.show_switch_neg_partial_json, + os.path.join(mock_counters_path, "partial"), + id="partial-json-all" + ) + ] + ) + def test_counters_show_neg(self, options, output, cntdb): + dbconnector.dedicated_dbs["COUNTERS_DB"] = cntdb + self.verify_counters_neg(options, output) + + def test_counters_period(self, period_data): + self.verify_counters_period() + + def test_counters_clear(self, clear_data): + self.verify_counters_clear() diff --git a/tests/synchronous_mode_test.py b/tests/synchronous_mode_test.py index abe65b6f32..51a746b62f 100644 --- a/tests/synchronous_mode_test.py +++ b/tests/synchronous_mode_test.py @@ -2,6 +2,7 @@ from utilities_common.db import Db from unittest import mock from mock import patch +from .mock_tables import dbconnector import config.main as config import config.validated_config_db_connector as validated_config_db_connector @@ -9,6 +10,7 @@ class TestSynchronousMode(object): @classmethod def setup_class(cls): print("SETUP") + dbconnector.load_namespace_config() def __check_result(self, result_msg, mode): if mode == "enable" or mode == "disable": diff --git a/utilities_common/netstat.py b/utilities_common/netstat.py index 5fd1df653e..83b2f4c18e 100755 --- a/utilities_common/netstat.py +++ b/utilities_common/netstat.py @@ -6,7 +6,8 @@ STATUS_NA = 'N/A' PORT_RATE = 40 -def ns_diff(newstr, oldstr): + +def ns_diff(newstr, oldstr, raw=False): """ Calculate the diff. """ @@ -18,7 +19,7 @@ def ns_diff(newstr, oldstr): oldstr = '0' new, old = int(newstr), int(oldstr) - return '{:,}'.format(max(0, new - old)) + return '{:,}'.format((new - old) if raw else max(0, new - old)) def ns_brate(newstr, oldstr, delta): """ @@ -72,10 +73,18 @@ def table_as_json(table, header): return json.dumps(output, indent=4, sort_keys=True) -def format_number_with_comma(number_in_str): +def format_number_with_comma(number_in_str, raw=False): """ Format the number with comma. """ + if raw: + if number_in_str.startswith('-', 0, 1) and number_in_str[1:].isdecimal(): + return '{:,}'.format(int(number_in_str)) + elif number_in_str.isdecimal(): + return '{:,}'.format(int(number_in_str)) + else: + return number_in_str + if number_in_str.isdecimal(): return '{:,}'.format(int(number_in_str)) else: diff --git a/utilities_common/portstat.py b/utilities_common/portstat.py index fb10e1bd51..538c3a6d67 100644 --- a/utilities_common/portstat.py +++ b/utilities_common/portstat.py @@ -29,15 +29,15 @@ rx_jbr, rx_frag, rx_usize, rx_ovrrun,\ fec_corr, fec_uncorr, fec_symbol_err,\ wred_grn_drp_pkt, wred_ylw_drp_pkt, wred_red_drp_pkt, wred_tot_drp_pkt,\ - trim") + trim, trim_sent, trim_drop") header_all = ['IFACE', 'STATE', 'RX_OK', 'RX_BPS', 'RX_PPS', 'RX_UTIL', 'RX_ERR', 'RX_DRP', 'RX_OVR', - 'TX_OK', 'TX_BPS', 'TX_PPS', 'TX_UTIL', 'TX_ERR', 'TX_DRP', 'TX_OVR', 'TRIM'] + 'TX_OK', 'TX_BPS', 'TX_PPS', 'TX_UTIL', 'TX_ERR', 'TX_DRP', 'TX_OVR', 'TRIM', 'TRIM_TX', 'TRIM_DRP'] header_std = ['IFACE', 'STATE', 'RX_OK', 'RX_BPS', 'RX_UTIL', 'RX_ERR', 'RX_DRP', 'RX_OVR', 'TX_OK', 'TX_BPS', 'TX_UTIL', 'TX_ERR', 'TX_DRP', 'TX_OVR'] header_errors_only = ['IFACE', 'STATE', 'RX_ERR', 'RX_DRP', 'RX_OVR', 'TX_ERR', 'TX_DRP', 'TX_OVR'] header_fec_only = ['IFACE', 'STATE', 'FEC_CORR', 'FEC_UNCORR', 'FEC_SYMBOL_ERR', 'FEC_PRE_BER', 'FEC_POST_BER'] header_rates_only = ['IFACE', 'STATE', 'RX_OK', 'RX_BPS', 'RX_PPS', 'RX_UTIL', 'TX_OK', 'TX_BPS', 'TX_PPS', 'TX_UTIL'] -header_trim_only = ['IFACE', 'STATE', 'TRIM_PKTS'] +header_trim_only = ['IFACE', 'STATE', 'TRIM_PKTS', 'TRIM_TX_PKTS', 'TRIM_DRP_PKTS'] rates_key_list = ['RX_BPS', 'RX_PPS', 'RX_UTIL', 'TX_BPS', 'TX_PPS', 'TX_UTIL', 'FEC_PRE_BER', 'FEC_POST_BER'] ratestat_fields = ("rx_bps", "rx_pps", "rx_util", "tx_bps", "tx_pps", "tx_util", "fec_pre_ber", "fec_post_ber") @@ -47,7 +47,7 @@ The order and count of statistics mentioned below needs to be in sync with the values in portstat script So, any fields added/deleted in here should be reflected in portstat script also """ -BUCKET_NUM = 50 +BUCKET_NUM = 52 wred_green_pkt_stat_capable = "false" wred_yellow_pkt_stat_capable = "false" @@ -109,6 +109,8 @@ 47: ['SAI_PORT_STAT_RED_WRED_DROPPED_PACKETS'], 48: ['SAI_PORT_STAT_WRED_DROPPED_PACKETS'], 49: ['SAI_PORT_STAT_TRIM_PACKETS'], + 50: ['SAI_PORT_STAT_TX_TRIM_PACKETS'], + 51: ['SAI_PORT_STAT_DROPPED_TRIM_PACKETS'], } STATUS_NA = 'N/A' @@ -535,8 +537,16 @@ def cnstat_intf_diff_print(self, cnstat_new_dict, cnstat_old_dict, intf_list): ) print("") - print("Packets Trimmed................................ {}".format(ns_diff(cntr['trim'], - old_cntr['trim']))) + print("Trimmed Packets................................ {}".format( + ns_diff(cntr['trim'], old_cntr['trim']) + )) + print("Trimmed Sent Packets........................... {}".format( + ns_diff(cntr['trim_sent'], old_cntr['trim_sent']) + )) + print("Trimmed Dropped Packets........................ {}".format( + ns_diff(cntr['trim_drop'], old_cntr['trim_drop'], raw=True) + )) + print("") print("Time Since Counters Last Cleared............... " + str(cnstat_old_dict.get('time'))) @@ -599,7 +609,9 @@ def cnstat_diff_print(self, cnstat_new_dict, cnstat_old_dict, ns_diff(cntr["tx_err"], old_cntr["tx_err"]), ns_diff(cntr["tx_drop"], old_cntr["tx_drop"]), ns_diff(cntr["tx_ovr"], old_cntr["tx_ovr"]), - ns_diff(cntr["trim"], old_cntr["trim"]))) + ns_diff(cntr["trim"], old_cntr["trim"]), + ns_diff(cntr["trim_sent"], old_cntr["trim_sent"]), + ns_diff(cntr["trim_drop"], old_cntr["trim_drop"], raw=True))) elif errors_only: header = header_errors_only @@ -650,7 +662,9 @@ def cnstat_diff_print(self, cnstat_new_dict, cnstat_old_dict, if not nonzero or is_non_zero(ns_diff(cntr['trim'], old_cntr['trim'])): table.append((key, self.get_port_state(key), - ns_diff(cntr['trim'], old_cntr['trim']))) + ns_diff(cntr["trim"], old_cntr["trim"]), + ns_diff(cntr["trim_sent"], old_cntr["trim_sent"]), + ns_diff(cntr["trim_drop"], old_cntr["trim_drop"], raw=True))) else: header = header_std diff --git a/utilities_common/switchstat.py b/utilities_common/switchstat.py new file mode 100644 index 0000000000..ef1354d262 --- /dev/null +++ b/utilities_common/switchstat.py @@ -0,0 +1,299 @@ +import click +import json +import os + +from natsort import natsorted +from tabulate import tabulate +from datetime import datetime +from swsscommon.swsscommon import COUNTERS_SWITCH_NAME_MAP, COUNTERS_TABLE +from utilities_common import multi_asic as multi_asic_util +from utilities_common.netstat import ns_diff, format_number_with_comma +from utilities_common.cli import UserCache, json_serial + + +HEADER_ALL = ["TrimSent/pkts", "TrimDrop/pkts"] +HEADER_STD = ["TrimSent/pkts", "TrimDrop/pkts"] +HEADER_TRIM = ["TrimSent/pkts", "TrimDrop/pkts"] + +counter_dict = { + "trim_tx": "SAI_SWITCH_STAT_TX_TRIM_PACKETS", + "trim_drp": "SAI_SWITCH_STAT_DROPPED_TRIM_PACKETS" +} + + +class SwitchStat(object): + def __init__(self, namespace, display, tag): + # Initialize cache + self.init_cache(tag) + + # Initialize the multi-asic namespace + self.db = None + self.multi_asic = multi_asic_util.MultiAsic(display, namespace) + + # Initialize counters default dict + self.cnstat_dict_default = {k: "N/A" for k in counter_dict.keys()} + + def init_cache(self, tag): + self.cache = UserCache(app_name="switchstat", tag=tag) + + self.cnstat_file = "switchstat" + self.cnstat_dir = self.cache.get_directory() + self.cnstat_fqn_file = os.path.join(self.cnstat_dir, self.cnstat_file) + + def is_cache_exists(self): + return os.path.isfile(self.cnstat_fqn_file) + + def remove_stats(self, all=False): + if all: + self.cache.remove_all() + else: + self.cache.remove() + + def load_stats(self): + return json.load(open(self.cnstat_fqn_file, "r")) + + def save_stats(self, cnstat_dict): + json.dump(cnstat_dict, open(self.cnstat_fqn_file, "w"), default=json_serial) + + def get_cnstat(self): + cnstat_dict = {} + + if not self.db.exists(self.db.COUNTERS_DB, COUNTERS_SWITCH_NAME_MAP): + return self.cnstat_dict_default + + switch_name_map = self.db.get_all(self.db.COUNTERS_DB, COUNTERS_SWITCH_NAME_MAP) + switch_oid = switch_name_map.get("ASIC", None) + + if switch_oid is None: + return self.cnstat_dict_default + + switch_stat_key = "{}{}{}".format( + COUNTERS_TABLE, + self.db.get_db_separator(self.db.COUNTERS_DB), + switch_oid + ) + + if not self.db.exists(self.db.COUNTERS_DB, switch_stat_key): + return self.cnstat_dict_default + + switch_stat_dict = self.db.get_all(self.db.COUNTERS_DB, switch_stat_key) + + for key, value in counter_dict.items(): + cnstat_dict[key] = switch_stat_dict.get(value, "N/A") + + return cnstat_dict + + @multi_asic_util.run_on_multi_asic + def collect_stat(self): + cnstat_dict = self.get_cnstat() + + if self.multi_asic.is_multi_asic: + self.cnstat_dict[self.multi_asic.current_namespace] = {} + self.cnstat_dict[self.multi_asic.current_namespace].update(cnstat_dict) + else: + self.cnstat_dict.update(cnstat_dict) + + def get_cnstat_dict(self, timestamp=False): + self.cnstat_dict = {} + self.collect_stat() + + if timestamp: # shallow copy to inject timestamp + cnstat_dict = {} + cnstat_dict.update(self.cnstat_dict) + cnstat_dict["time"] = datetime.now() + return cnstat_dict + + return self.cnstat_dict + + def cnstat_print(self, cnstat_dict, print_all, print_trim, detail, json_fmt): + def print_json(cntr): + def build_json(data): + json_dict = {} + + if print_all: # all counters + json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) + json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) + elif print_trim: # trim counters + json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) + json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) + else: # standard counters + json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) + json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) + + return json_dict + + if not self.multi_asic.is_multi_asic: + click.echo(json.dumps(build_json(cntr), indent=4, sort_keys=True)) + return + + json_dict = {} + + for ns in cntr.keys(): + json_dict[ns] = build_json(cntr[ns]) + + click.echo(json.dumps(json_dict, indent=4, sort_keys=True)) + + def print_detail(cntr, ns=None): + if ns is not None: + click.echo("Namespace: {}".format(ns)) + click.echo() + + click.echo("Trimmed Sent Packets........................... {}".format( + format_number_with_comma(cntr["trim_tx"]) + )) + click.echo("Trimmed Dropped Packets........................ {}".format( + format_number_with_comma(cntr["trim_drp"]) + )) + + def print_table(cntr, ns=None): + if ns is not None: + click.echo("Namespace: {}".format(ns)) + click.echo() + + header = None + body = [] + + if print_all: # all counters + header = HEADER_ALL + body.append(( + format_number_with_comma(cntr["trim_tx"]), + format_number_with_comma(cntr["trim_drp"]) + )) + elif print_trim: # trim counters + header = HEADER_TRIM + body.append(( + format_number_with_comma(cntr["trim_tx"]), + format_number_with_comma(cntr["trim_drp"]) + )) + else: # standard counters + header = HEADER_STD + body.append(( + format_number_with_comma(cntr["trim_tx"]), + format_number_with_comma(cntr["trim_drp"]) + )) + + click.echo(tabulate(body, header, tablefmt="simple", stralign="right")) + + if json_fmt: + print_json(cnstat_dict) + return + + if not self.multi_asic.is_multi_asic: + if detail: + print_detail(cnstat_dict) + else: + print_table(cnstat_dict) + return + + ns_list = natsorted(cnstat_dict.keys()) + + if detail: + print_detail(cnstat_dict[ns_list[0]], ns_list[0]) + + for ns in ns_list[1:]: + click.echo() + print_detail(cnstat_dict[ns], ns) + else: + print_table(cnstat_dict[ns_list[0]], ns_list[0]) + + for ns in ns_list[1:]: + click.echo() + print_table(cnstat_dict[ns], ns) + + def cnstat_diff_print(self, cnstat_dict, cnstat_old_dict, print_all, print_trim, detail, json_fmt): + def print_json(cntr, old_cntr): + def build_json(data, old_data): + json_dict = {} + + if print_all: # all counters + json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) + json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) + elif print_trim: # trim counters + json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) + json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) + else: # standard counters + json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) + json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) + + return json_dict + + if not self.multi_asic.is_multi_asic: + click.echo(json.dumps(build_json(cntr, old_cntr), indent=4, sort_keys=True)) + return + + json_dict = {} + + for ns in cntr.keys(): + json_dict[ns] = build_json(cntr[ns], old_cntr[ns]) + + click.echo(json.dumps(json_dict, indent=4, sort_keys=True)) + + def print_detail(cntr, old_cntr, ts, ns=None): + if ns is not None: + click.echo("Namespace: {}".format(ns)) + click.echo() + + click.echo("Trimmed Sent Packets........................... {}".format( + ns_diff(cntr["trim_tx"], old_cntr["trim_tx"]) + )) + click.echo("Trimmed Dropped Packets........................ {}".format( + ns_diff(cntr["trim_drp"], old_cntr["trim_drp"]) + )) + click.echo() + + click.echo("Time Since Counters Last Cleared............... {}".format(ts)) + + def print_table(cntr, old_cntr, ns=None): + if ns is not None: + click.echo("Namespace: {}".format(ns)) + click.echo() + + header = None + body = [] + + if print_all: # all counters + header = HEADER_ALL + body.append(( + ns_diff(cntr["trim_tx"], old_cntr["trim_tx"]), + ns_diff(cntr["trim_drp"], old_cntr["trim_drp"]) + )) + elif print_trim: # trim counters + header = HEADER_TRIM + body.append(( + ns_diff(cntr["trim_tx"], old_cntr["trim_tx"]), + ns_diff(cntr["trim_drp"], old_cntr["trim_drp"]) + )) + else: # standard counters + header = HEADER_STD + body.append(( + ns_diff(cntr["trim_tx"], old_cntr["trim_tx"]), + ns_diff(cntr["trim_drp"], old_cntr["trim_drp"]) + )) + + click.echo(tabulate(body, header, tablefmt="simple", stralign="right")) + + if json_fmt: + print_json(cnstat_dict, cnstat_old_dict) + return + + if not self.multi_asic.is_multi_asic: + if detail: + print_detail(cnstat_dict, cnstat_old_dict, cnstat_old_dict["time"]) + else: + print_table(cnstat_dict, cnstat_old_dict) + return + + ns_list = natsorted(cnstat_dict.keys()) + + if detail: + print_detail(cnstat_dict[ns_list[0]], cnstat_old_dict[ns_list[0]], cnstat_old_dict["time"], ns_list[0]) + + for ns in ns_list[1:]: + click.echo() + print_detail(cnstat_dict[ns], cnstat_old_dict[ns], cnstat_old_dict["time"], ns) + else: + print_table(cnstat_dict[ns_list[0]], cnstat_old_dict[ns_list[0]], ns_list[0]) + + for ns in ns_list[1:]: + click.echo() + print_table(cnstat_dict[ns], cnstat_old_dict[ns], ns) From e9f23d83fac343280c14858f2f60837d6a4b4168 Mon Sep 17 00:00:00 2001 From: Nazarii Hnydyn Date: Fri, 8 Aug 2025 12:18:12 +0300 Subject: [PATCH 21/21] [trim]: Handle review comments Signed-off-by: Nazarii Hnydyn --- utilities_common/portstat.py | 13 +++---------- utilities_common/switchstat.py | 22 ++++------------------ 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/utilities_common/portstat.py b/utilities_common/portstat.py index 538c3a6d67..04e571de77 100644 --- a/utilities_common/portstat.py +++ b/utilities_common/portstat.py @@ -47,8 +47,6 @@ The order and count of statistics mentioned below needs to be in sync with the values in portstat script So, any fields added/deleted in here should be reflected in portstat script also """ -BUCKET_NUM = 52 - wred_green_pkt_stat_capable = "false" wred_yellow_pkt_stat_capable = "false" wred_red_pkt_stat_capable = "false" @@ -261,7 +259,6 @@ def collect_stat(self): device and store in a dict """ - global BUCKET_NUM global wred_green_pkt_stat_capable global wred_yellow_pkt_stat_capable global wred_red_pkt_stat_capable @@ -289,22 +286,18 @@ def collect_stat(self): if (is_wred_stats_reqd is False) or (wred_green_pkt_stat_capable != "true"): if ('SAI_PORT_STAT_GREEN_WRED_DROPPED_PACKETS' in counter_bucket_dict.keys()): del counter_bucket_dict['SAI_PORT_STAT_GREEN_WRED_DROPPED_PACKETS'] - BUCKET_NUM = (BUCKET_NUM - 1) if (is_wred_stats_reqd is False) or (wred_yellow_pkt_stat_capable != "true"): if ('SAI_PORT_STAT_YELLOW_WRED_DROPPED_PACKETS' in counter_bucket_dict.keys()): del counter_bucket_dict['SAI_PORT_STAT_YELLOW_WRED_DROPPED_PACKETS'] - BUCKET_NUM = (BUCKET_NUM - 1) if (is_wred_stats_reqd is False) or (wred_red_pkt_stat_capable != "true"): if ('SAI_PORT_STAT_RED_WRED_DROPPED_PACKETS' in counter_bucket_dict.keys()): del counter_bucket_dict['SAI_PORT_STAT_RED_WRED_DROPPED_PACKETS'] - BUCKET_NUM = (BUCKET_NUM - 1) if (is_wred_stats_reqd is False) or (wred_total_pkt_stat_capable != "true"): if ('SAI_PORT_STAT_WRED_DROPPED_PACKETS' in counter_bucket_dict.keys()): del counter_bucket_dict['SAI_PORT_STAT_WRED_DROPPED_PACKETS'] - BUCKET_NUM = (BUCKET_NUM - 1) cnstat_dict, ratestat_dict = self.get_cnstat() self.cnstat_dict.update(cnstat_dict) @@ -318,7 +311,7 @@ def get_counters(port): """ Get the counters from specific table. """ - fields = ["0"]*BUCKET_NUM + fields = ["0"] * len(counter_bucket_dict) _, fvs = counter_table.get(PortCounter(), port) fvs = dict(fvs) @@ -423,7 +416,7 @@ def cnstat_intf_diff_print(self, cnstat_new_dict, cnstat_old_dict, intf_list): if key in cnstat_old_dict: old_cntr = cnstat_old_dict.get(key) else: - old_cntr = NStats._make([0] * BUCKET_NUM)._asdict() + old_cntr = NStats._make([0] * len(counter_bucket_dict))._asdict() if intf_list and key not in intf_list: continue @@ -573,7 +566,7 @@ def cnstat_diff_print(self, cnstat_new_dict, cnstat_old_dict, if key in cnstat_old_dict: old_cntr = cnstat_old_dict.get(key) else: - old_cntr = NStats._make([0] * BUCKET_NUM)._asdict() + old_cntr = NStats._make([0] * len(counter_bucket_dict))._asdict() rates = ratestat_dict.get(key, RateStats._make([STATUS_NA] * len(ratestat_fields))) diff --git a/utilities_common/switchstat.py b/utilities_common/switchstat.py index ef1354d262..2e1a47ef2d 100644 --- a/utilities_common/switchstat.py +++ b/utilities_common/switchstat.py @@ -110,15 +110,8 @@ def print_json(cntr): def build_json(data): json_dict = {} - if print_all: # all counters - json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) - json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) - elif print_trim: # trim counters - json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) - json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) - else: # standard counters - json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) - json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) + json_dict["trim_sent"] = format_number_with_comma(data["trim_tx"]) + json_dict["trim_drop"] = format_number_with_comma(data["trim_drp"]) return json_dict @@ -205,15 +198,8 @@ def print_json(cntr, old_cntr): def build_json(data, old_data): json_dict = {} - if print_all: # all counters - json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) - json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) - elif print_trim: # trim counters - json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) - json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) - else: # standard counters - json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) - json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) + json_dict["trim_sent"] = ns_diff(data["trim_tx"], old_data["trim_tx"]) + json_dict["trim_drop"] = ns_diff(data["trim_drp"], old_data["trim_drp"]) return json_dict