diff --git a/clear/main.py b/clear/main.py index c3642c6c67..4632e04abe 100755 --- a/clear/main.py +++ b/clear/main.py @@ -218,6 +218,13 @@ def tunnelcounters(): command = ["tunnelstat", "-c"] run_command(command) + +@cli.command() +def policercounters(): + """Clear Policer counters""" + command = ["policerstat", "-c"] + run_command(command) + # # 'clear watermarks # diff --git a/counterpoll/main.py b/counterpoll/main.py index 83646b4069..41be07141f 100644 --- a/counterpoll/main.py +++ b/counterpoll/main.py @@ -10,6 +10,7 @@ PG_DROP = "PG_DROP" ACL = "ACL" ENI = "ENI" +POLICER = "POLICER" DISABLE = "disable" ENABLE = "enable" DEFLT_60_SEC= "default (60000)" @@ -256,6 +257,45 @@ def disable(): configdb.mod_entry("FLEX_COUNTER_TABLE", "PG_WATERMARK", fc_info) configdb.mod_entry("FLEX_COUNTER_TABLE", BUFFER_POOL_WATERMARK, fc_info) + +# Policer counter commands +@cli.group() +@click.pass_context +def policer(ctx): + """ Policer counter commands """ + ctx.obj = ConfigDBConnector() + ctx.obj.connect() + + +@policer.command(name='interval') +@click.pass_context +@click.argument('poll_interval', type=click.IntRange(1000, 60000)) +def policer_interval(ctx, poll_interval): + """ Set policer counter query interval """ + policer_info = {} + if poll_interval is not None: + policer_info['POLL_INTERVAL'] = poll_interval + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", POLICER, policer_info) + + +@policer.command(name='enable') +@click.pass_context +def policer_enable(ctx): + """ Enable policer counter query """ + policer_info = {} + policer_info['FLEX_COUNTER_STATUS'] = ENABLE + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", POLICER, policer_info) + + +@policer.command(name='disable') +@click.pass_context +def policer_disable(ctx): + """ Disable policer counter query """ + policer_info = {} + policer_info['FLEX_COUNTER_STATUS'] = DISABLE + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", POLICER, policer_info) + + # ACL counter commands @cli.group() @click.pass_context @@ -525,6 +565,7 @@ def show(): eni_info = configdb.get_entry('FLEX_COUNTER_TABLE', ENI) wred_queue_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'WRED_ECN_QUEUE') wred_port_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'WRED_ECN_PORT') + policer_info = configdb.get_entry('FLEX_COUNTER_TABLE', POLICER) header = ("Type", "Interval (in ms)", "Status") data = [] @@ -559,6 +600,9 @@ def show(): if wred_port_info: data.append(["WRED_ECN_PORT_STAT", wred_port_info.get("POLL_INTERVAL", DEFLT_1_SEC), wred_port_info.get("FLEX_COUNTER_STATUS", DISABLE)]) + if policer_info: + data.append([POLICER, policer_info.get("POLL_INTERVAL", DEFLT_10_SEC), + policer_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/scripts/policerstat b/scripts/policerstat new file mode 100755 index 0000000000..367a3b9d80 --- /dev/null +++ b/scripts/policerstat @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 + +##################################################################### +# +# policerstat is a tool for summarizing policer counter statistics. +# +##################################################################### + +import json +import argparse +import datetime +import sys +import os +import time +from collections import namedtuple, OrderedDict +from natsort import natsorted +from tabulate import tabulate + +try: + if os.environ["UTILITIES_UNIT_TESTING"] == "2": + modules_path = os.path.join(os.path.dirname(__file__), "..") + tests_path = os.path.join(modules_path, "tests") + sys.path.insert(0, modules_path) + sys.path.insert(0, tests_path) + import mock_tables.dbconnector + if os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] == "multi_asic": + import mock_tables.mock_multi_asic + mock_tables.dbconnector.load_namespace_config() +except KeyError: + pass + +from utilities_common.netstat import ns_diff, STATUS_NA, format_number_with_comma, table_as_json +from utilities_common import multi_asic as multi_asic_util +from utilities_common import constants +from utilities_common.cli import json_serial, UserCache +from swsscommon.swsscommon import ConfigDBConnector, SonicV2Connector + +pstat_fields = ( + "totalpacket", + "totalbytes", + "gtotalpacket", + "gtotalbytes", + "ytotalpacket", + "ytotalbytes", + "rtotalpacket", + "rtotalbytes" +) + +PStats = namedtuple("PStats", pstat_fields) + +header = [ + 'Policer', + 'Total Packets', + 'Total Bytes', + 'Green Packets', + 'Green Bytes', + 'Yellow Packets', + 'Yellow Bytes', + 'Red Packets', + 'Red Bytes' +] + +def build_json(cnstat): + def policers_stats(policer_name, counters): + return { + policer_name: { + "totalpacket": counters.get("totalpacket", "N/A"), + "totalbytes": counters.get("totalbytes", "N/A"), + "gtotalpacket": counters.get("gtotalpacket", "N/A"), + "gtotalbytes": counters.get("gtotalbytes", "N/A"), + "ytotalpacket": counters.get("ytotalpacket", "N/A"), + "ytotalbytes": counters.get("ytotalbytes", "N/A"), + "rtotalpacket": counters.get("rtotalpacket", "N/A"), + "rtotalbytes": counters.get("rtotalbytes", "N/A") + } + } + + out = {} + for policer_name, counters in cnstat.items(): + if policer_name == 'time': + continue + out.update(policers_stats(policer_name, counters)) + return out + +counter_names = ( + 'SAI_POLICER_STAT_PACKETS', + 'SAI_POLICER_STAT_ATTR_BYTES', + 'SAI_POLICER_STAT_GREEN_PACKETS', + 'SAI_POLICER_STAT_GREEN_BYTES', + 'SAI_POLICER_STAT_YELLOW_PACKETS', + 'SAI_POLICER_STAT_YELLOW_BYTES', + 'SAI_POLICER_STAT_RED_PACKETS', + 'SAI_POLICER_STAT_RED_BYTES' +) + +COUNTER_TABLE_PREFIX = "COUNTERS:" +COUNTERS_POLICER_NAME_MAP = "COUNTERS_POLICER_NAME_MAP" + +class Policerstat: + def __init__(self): + self.db = SonicV2Connector(host='127.0.0.1') + self.db.connect(self.db.COUNTERS_DB) + + def get_cnstat(self, policer=None): + """ + Get the counters info from database. + """ + def get_counters(policer, table_id): + """ + Get the counters from specific table. + """ + fields = [STATUS_NA] * len(pstat_fields) + for pos, counter_name in enumerate(counter_names): + full_table_id = f"{COUNTER_TABLE_PREFIX}{table_id}" + counter_data = self.db.get(self.db.COUNTERS_DB, full_table_id, counter_name) + if counter_data: + fields[pos] = str(counter_data) + cntr = PStats._make(fields)._asdict() + cntr['policername'] = policer + return cntr + + + cnstat_dict = OrderedDict(time=datetime.datetime.now()) + + counter_policer_name_map = self.db.get_all(self.db.COUNTERS_DB, COUNTERS_POLICER_NAME_MAP) + if counter_policer_name_map is None: + print(f"No {COUNTERS_POLICER_NAME_MAP} in the DB!") + sys.exit(1) + + if policer and policer not in counter_policer_name_map: + print(f"Policer {policer} missing from {COUNTERS_POLICER_NAME_MAP}! Make sure it exists.") + sys.exit(2) + + if policer: + cnstat_dict[policer] = get_counters(policer, counter_policer_name_map[policer]) + return cnstat_dict + + for policer in natsorted(counter_policer_name_map): + cnstat_dict[policer] = get_counters(policer, counter_policer_name_map[policer]) + return cnstat_dict + + def cnstat_print(self, policer, cnstat_dict, use_json): + """ + Print the cnstat. + """ + table = [] + for key, data in cnstat_dict.items(): + if key == 'time': + continue + table.append(( + data.get('policername', 'N/A'), + data.get('totalpacket', 'N/A'), + data.get('totalbytes', 'N/A'), + data.get('gtotalpacket', 'N/A'), + data.get('gtotalbytes', 'N/A'), + data.get('ytotalpacket', 'N/A'), + data.get('ytotalbytes', 'N/A'), + data.get('rtotalpacket', 'N/A'), + data.get('rtotalbytes', 'N/A') + )) + if table: + if use_json: + print(json.dumps(build_json(cnstat_dict), indent=4)) + else: + print(tabulate(table, header, tablefmt='simple', stralign='right')) + print() + else: + print("No data available for the given policer.") + + def cnstat_diff_print(self, policer, cnstat_new_dict, cnstat_old_dict, use_json): + """ + Print the difference between two cnstat results. + """ + table = [] + + for key, cntr in cnstat_new_dict.items(): + if key == 'time': + continue + old_cntr = cnstat_old_dict.get(key) + if old_cntr: + table.append((key, + ns_diff(cntr['totalpacket'], old_cntr['totalpacket']), + ns_diff(cntr['totalbytes'], old_cntr['totalbytes']), + ns_diff(cntr['gtotalpacket'], old_cntr['gtotalpacket']), + ns_diff(cntr['gtotalbytes'], old_cntr['gtotalbytes']), + ns_diff(cntr['ytotalpacket'], old_cntr['ytotalpacket']), + ns_diff(cntr['ytotalbytes'], old_cntr['ytotalbytes']), + ns_diff(cntr['rtotalpacket'], old_cntr['rtotalpacket']), + ns_diff(cntr['rtotalbytes'], old_cntr['rtotalbytes']))) + else: + table.append((key, + format_number_with_comma(cntr['totalpacket']), + format_number_with_comma(cntr['totalbytes']), + format_number_with_comma(cntr['gtotalpacket']), + format_number_with_comma(cntr['gtotalbytes']), + format_number_with_comma(cntr['ytotalpacket']), + format_number_with_comma(cntr['ytotalbytes']), + format_number_with_comma(cntr['rtotalpacket']), + format_number_with_comma(cntr['rtotalbytes']))) + if table: + if use_json: + print(table_as_json(table, header)) + else: + print(tabulate(table, header, tablefmt='simple', stralign='right')) + print() + + +def main(): + parser = argparse.ArgumentParser( + description='Display the policer counters', + formatter_class=argparse.RawTextHelpFormatter, + epilog=""" + Examples: + policerstat -c + policerstat -j + policerstat -p span_policer + """ + ) + + parser.add_argument('-c', '--clear', action='store_true', help='Copy & clear stats') + parser.add_argument('-j', '--json', action='store_true', help='Display in JSON format') + parser.add_argument('-p', '--policer', type=str, help='Show stats for a single policer') + args = parser.parse_args() + + cache = UserCache() + + cnstat_dir = cache.get_directory() + cnstat_fqn_file = os.path.join(cnstat_dir, 'policerstat') + + policerstat = Policerstat() + cnstat_dict = policerstat.get_cnstat(policer=args.policer) + + if args.clear: + json.dump(cnstat_dict, open(cnstat_fqn_file, 'w'), default=json_serial) + print("Cleared counters") + sys.exit(0) + else: + if os.path.isfile(cnstat_fqn_file): + try: + cnstat_cached_dict = json.load(open(cnstat_fqn_file, 'r')) + policerstat.cnstat_diff_print(args.policer, cnstat_dict, cnstat_cached_dict, args.json) + except IOError as e: + print(e.errno, e) + else: + policerstat.cnstat_print(args.policer, cnstat_dict, args.json) + sys.exit(0) + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 597dfadbfb..2f7d3dc2df 100644 --- a/setup.py +++ b/setup.py @@ -143,6 +143,7 @@ 'scripts/generate_shutdown_order.py', 'scripts/intfutil', 'scripts/intfstat', + 'scripts/policerstat', 'scripts/ipintutil', 'scripts/lag_keepalive.py', 'scripts/lldpshow', diff --git a/show/main.py b/show/main.py index f71567d008..3083f4409d 100755 --- a/show/main.py +++ b/show/main.py @@ -2290,15 +2290,22 @@ def mirror_session(session_name, verbose): # @cli.command() @click.argument('policer_name', required=False) +@click.option('--counter', is_flag=True, help="Show counters output") @click.option('--verbose', is_flag=True, help="Enable verbose output") -def policer(policer_name, verbose): - """Show existing policers""" - cmd = ['acl-loader', 'show', 'policer'] +def policer(policer_name, counter, verbose): + if counter: + cmd = ['policerstat'] - if policer_name is not None: - cmd += [str(policer_name)] + if policer_name is not None: + cmd += ["-p", str(policer_name)] + run_command(cmd, display_cmd=verbose) + else: + cmd = ['acl-loader', 'show', 'policer'] - run_command(cmd, display_cmd=verbose) + if policer_name is not None: + cmd += [str(policer_name)] + + run_command(cmd, display_cmd=verbose) # diff --git a/tests/counterpoll_input/config_db.json b/tests/counterpoll_input/config_db.json index 6e43a66a0a..34ce6e8a1c 100644 --- a/tests/counterpoll_input/config_db.json +++ b/tests/counterpoll_input/config_db.json @@ -796,6 +796,9 @@ }, "WRED_ECN_PORT": { "FLEX_COUNTER_STATUS": "enable" + }, + "POLICER": { + "FLEX_COUNTER_STATUS": "enable" } }, "PORT": { @@ -2674,5 +2677,13 @@ "pool": "ingress_lossless_pool", "size": "56368" } + }, + "POLICER|policer0": { + "meter_type":"packets", + "mode":"sr_tcm", + "color": "aware", + "cir":"700", + "cbs":"700", + "red_packet_action":"drop" } } diff --git a/tests/counterpoll_test.py b/tests/counterpoll_test.py index 1150c19640..3db009f57b 100644 --- a/tests/counterpoll_test.py +++ b/tests/counterpoll_test.py @@ -33,6 +33,7 @@ FLOW_CNT_ROUTE_STAT 10000 enable WRED_ECN_QUEUE_STAT 10000 enable WRED_ECN_PORT_STAT 1000 enable +POLICER 10000 enable """ expected_counterpoll_show_dpu = """Type Interval (in ms) Status @@ -49,6 +50,7 @@ FLOW_CNT_ROUTE_STAT 10000 enable WRED_ECN_QUEUE_STAT 10000 enable WRED_ECN_PORT_STAT 1000 enable +POLICER 10000 enable ENI_STAT 1000 enable """ @@ -347,6 +349,45 @@ def test_update_wred_queue_counter_interval(self): print(table) assert test_interval == table["WRED_ECN_QUEUE"]["POLL_INTERVAL"] + @pytest.mark.parametrize("status", ["disable", "enable"]) + def test_update_policer_counter_status(self, status): + runner = CliRunner() + db = Db() + + result = runner.invoke(counterpoll.cli.commands["policer"].commands[status], [], obj=db.cfgdb) + assert result.exit_code == 0 + + table = db.cfgdb.get_table('FLEX_COUNTER_TABLE') + assert status == table["POLICER"]["FLEX_COUNTER_STATUS"] + + def test_update_policer_counter_interval(self): + runner = CliRunner() + db = Db() + test_interval = "20000" + + result = runner.invoke(counterpoll.cli.commands["policer"].commands["interval"], [test_interval], + obj=db.cfgdb) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + table = db.cfgdb.get_table('FLEX_COUNTER_TABLE') + assert test_interval == table["POLICER"]["POLL_INTERVAL"] + + test_interval = "500" + result = runner.invoke(counterpoll.cli.commands["policer"].commands["interval"], [test_interval], + obj=db.cfgdb) + expected = "Invalid value for \"POLL_INTERVAL\": 500 is not in the valid range of 1000 to 60000." + assert result.exit_code == 2 + assert expected in result.output + + test_interval = "70000" + result = runner.invoke(counterpoll.cli.commands["policer"].commands["interval"], [test_interval], + obj=db.cfgdb) + + expected = "Invalid value for \"POLL_INTERVAL\": 70000 is not in the valid range of 1000 to 60000." + assert result.exit_code == 2 + assert expected in result.output + @classmethod def teardown_class(cls): print("TEARDOWN") diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 2f12201b10..288bbff856 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -1797,6 +1797,10 @@ "POLL_INTERVAL": "1000", "FLEX_COUNTER_STATUS": "enable" }, + "FLEX_COUNTER_TABLE|POLICER": { + "POLL_INTERVAL": "10000", + "FLEX_COUNTER_STATUS": "enable" + }, "PFC_WD|Ethernet0": { "action": "drop", "detection_time": "600", @@ -2755,6 +2759,14 @@ "src_port": "Ethernet40,Ethernet48", "direction": "RX" }, + "POLICER|policer0": { + "meter_type":"packets", + "mode":"sr_tcm", + "color": "aware", + "cir":"700", + "cbs":"700", + "red_packet_action":"drop" + }, "FABRIC_MONITOR|FABRIC_MONITOR_DATA": { "monCapacityThreshWarn": "100", "monErrThreshCrcCells": "1", diff --git a/tests/mock_tables/counters_db.json b/tests/mock_tables/counters_db.json index 1dfdaaac74..874dda0d94 100644 --- a/tests/mock_tables/counters_db.json +++ b/tests/mock_tables/counters_db.json @@ -2853,5 +2853,18 @@ "COUNTERS:oid:0x1600000000035f":{ "SAI_COUNTER_STAT_PACKETS": 1000, "SAI_COUNTER_STAT_BYTES": 25000 + }, + "COUNTERS_POLICER_NAME_MAP": { + "policer0": "oid:0x1200000000040a" + }, + "COUNTERS:oid:0x1200000000040a": { + "SAI_POLICER_STAT_ATTR_BYTES": "1000", + "SAI_POLICER_STAT_GREEN_BYTES": "2000", + "SAI_POLICER_STAT_GREEN_PACKETS": "3000", + "SAI_POLICER_STAT_PACKETS": "4000", + "SAI_POLICER_STAT_RED_BYTES": "5000", + "SAI_POLICER_STAT_RED_PACKETS": "6000", + "SAI_POLICER_STAT_YELLOW_BYTES": "7000", + "SAI_POLICER_STAT_YELLOW_PACKETS": "8000" } } diff --git a/tests/policerstat_test.py b/tests/policerstat_test.py new file mode 100755 index 0000000000..331f483cd2 --- /dev/null +++ b/tests/policerstat_test.py @@ -0,0 +1,98 @@ +import os + +from click.testing import CliRunner +import show.main as show +import clear.main as clear +from .utils import get_result_and_return_code +from utilities_common.cli import UserCache + +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_policer_counters_output = """\ + Policer Total Packets Total Bytes Green Packets Green Bytes\ + Yellow Packets Yellow Bytes Red Packets Red Bytes +--------- --------------- ------------- --------------- -------------\ + ---------------- -------------- ------------- ----------- + policer0 4000 1000 3000 2000\ + 8000 7000 6000 5000 + +""" + +show_policer_counters_output_diff = """\ + Policer Total Packets Total Bytes Green Packets Green Bytes\ + Yellow Packets Yellow Bytes Red Packets Red Bytes +--------- --------------- ------------- --------------- -------------\ + ---------------- -------------- ------------- ----------- + policer0 0 0 0 0\ + 0 0 0 0 + +""" + + +def del_cached_stats(): + cache = UserCache("policerstat") + cache.remove_all() + + +def policer_clear(expected_output): + del_cached_stats() + + return_code, result = get_result_and_return_code( + ['policerstat', '-c'] + ) + + assert return_code == 0 + + return_code, result = get_result_and_return_code( + ['policerstat'] + ) + + result_stat = [s for s in result.split("\n") if "Last cached" not in s] + expected = expected_output.split("\n") + assert result_stat == expected + del_cached_stats() + + +class TestPolicerstat(object): + @classmethod + def setup_class(cls): + print("SETUP") + os.environ["PATH"] += os.pathsep + scripts_path + os.environ["UTILITIES_UNIT_TESTING"] = "2" + del_cached_stats() + + def test_policer_counters(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands['policer'], ['policer0', '--counter']) + assert result.exit_code == 0 + assert result.output == show_policer_counters_output + + def test_policerstat(self): + return_code, result = get_result_and_return_code(['policerstat']) + assert return_code == 0 + assert result == show_policer_counters_output + + def test_policer_counters_with_clear(self): + runner = CliRunner() + result = runner.invoke(clear.cli.commands['policercounters'], []) + assert result.exit_code == 0 + assert result.output == "Cleared counters\n\n" + result = runner.invoke( + show.cli.commands["policer"], + ["--counter"] + ) + assert result.output == show_policer_counters_output_diff + + def test_policer_clear(self): + policer_clear(show_policer_counters_output_diff) + + @classmethod + def teardown_class(cls): + print("TEARDOWN") + os.environ["PATH"] = os.pathsep.join( + os.environ["PATH"].split(os.pathsep)[:-1] + ) + os.environ["UTILITIES_UNIT_TESTING"] = "0" + del_cached_stats()