diff --git a/clear/main.py b/clear/main.py index 6c274ae99bc9..f6e1d8c4774b 100755 --- a/clear/main.py +++ b/clear/main.py @@ -194,6 +194,12 @@ def pfccounters(): command = "pfcstat -c" run_command(command) +@cli.command() +def dropcounters(): + """Clear drop counters""" + command = "dropstat -c clear" + run_command(command) + # # 'clear watermarks # diff --git a/config/main.py b/config/main.py index 3f3fa63ebe90..6fcc0f46f839 100755 --- a/config/main.py +++ b/config/main.py @@ -1614,6 +1614,79 @@ def incremental(file_name): command = "acl-loader update incremental {}".format(file_name) run_command(command) + +# +# 'dropcounters' group ('config dropcounters ...') +# + +@config.group() +def dropcounters(): + """Drop counter related configuration tasks""" + pass + + +# +# 'install' subcommand ('config dropcounters install') +# +@dropcounters.command() +@click.argument("counter_name", type=str, required=True) +@click.argument("counter_type", type=str, required=True) +@click.argument("reasons", type=str, required=True) +@click.option("-a", "--alias", type=str, help="Alias for this counter") +@click.option("-g", "--group", type=str, help="Group for this counter") +@click.option("-d", "--desc", type=str, help="Description for this counter") +@click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") +def install(counter_name, alias, group, counter_type, desc, reasons, verbose): + """Install a new drop counter""" + command = "dropconfig -c install -n '{}' -t '{}' -r '{}'".format(counter_name, counter_type, reasons) + if alias: + command += " -a '{}'".format(alias) + if group: + command += " -g '{}'".format(group) + if desc: + command += " -d '{}'".format(desc) + + run_command(command, display_cmd=verbose) + + +# +# 'delete' subcommand ('config dropcounters delete') +# +@dropcounters.command() +@click.argument("counter_name", type=str, required=True) +@click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") +def delete(counter_name, verbose): + """Delete an existing drop counter""" + command = "dropconfig -c uninstall -n {}".format(counter_name) + run_command(command, display_cmd=verbose) + + +# +# 'add_reasons' subcommand ('config dropcounters add_reasons') +# +@dropcounters.command() +@click.argument("counter_name", type=str, required=True) +@click.argument("reasons", type=str, required=True) +@click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") +def add_reasons(counter_name, reasons, verbose): + """Add reasons to an existing drop counter""" + command = "dropconfig -c add -n {} -r {}".format(counter_name, reasons) + run_command(command, display_cmd=verbose) + + +# +# 'remove_reasons' subcommand ('config dropcounters remove_reasons') +# +@dropcounters.command() +@click.argument("counter_name", type=str, required=True) +@click.argument("reasons", type=str, required=True) +@click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") +def remove_reasons(counter_name, reasons, verbose): + """Remove reasons from an existing drop counter""" + command = "dropconfig -c remove -n {} -r {}".format(counter_name, reasons) + run_command(command, display_cmd=verbose) + + # # 'ecn' command ('config ecn ...') # diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index b8e98af88f09..ccba9c611270 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -33,6 +33,10 @@ * [BGP config commands](#bgp-config-commands) * [DHCP Relay](#dhcp-relay) * [DHCP Relay config commands](#dhcp-relay-config-commands) +* [Drop Counters](#drop-counters) + * [Drop Counter show commands](#drop-counters-show-commands) + * [Drop Counter config commands](#drop-counters-config-commands) + * [Drop Counter clear commands](#drop-counters-clear-commands) * [ECN](#ecn) * [ECN show commands](#ecn-show-commands) * [ECN config commands](#ecn-config-commands) @@ -1739,6 +1743,191 @@ This command is used to delete a configured DHCP Relay Destination IP address fr Go Back To [Beginning of the document](#) or [Beginning of this section](#dhcp-relay) +# Drop Counters + +This section explains all the Configurable Drop Counters show commands and configuration options that are supported in SONiC. + +### Drop Counters show commands + +**show dropcounters capabilities** + +This command is used to show the drop counter capabilities that are available on this device. It displays the total number of drop counters that can be configured on this device as well as the drop reasons that can be configured for the counters. + +- Usage: + ``` + show dropcounters capabilities + ``` + +- Examples: + ``` + admin@sonic:~$ show dropcounters capabilities + Counter Type Total + -------------------- ------- + PORT_INGRESS_DROPS 3 + SWITCH_EGRESS_DROPS 2 + + PORT_INGRESS_DROPS: + L2_ANY + SMAC_MULTICAST + SMAC_EQUALS_DMAC + INGRESS_VLAN_FILTER + EXCEEDS_L2_MTU + SIP_CLASS_E + SIP_LINK_LOCAL + DIP_LINK_LOCAL + UNRESOLVED_NEXT_HOP + DECAP_ERROR + + SWITCH_EGRESS_DROPS: + L2_ANY + L3_ANY + A_CUSTOM_REASON + ``` + +**show dropcounters configuration** + +This command is used to show the current running configuration of the drop counters on this device. + +- Usage: + ``` + show dropcounters configuration [-g ] + ``` + +- Examples: + ``` + admin@sonic:~$ show dropcounters configuration + Counter Alias Group Type Reasons Description + -------- -------- ----- ------------------ ------------------- -------------- + DEBUG_0 RX_LEGIT LEGIT PORT_INGRESS_DROPS SMAC_EQUALS_DMAC Legitimate port-level RX pipeline drops + INGRESS_VLAN_FILTER + DEBUG_1 TX_LEGIT None SWITCH_EGRESS_DROPS EGRESS_VLAN_FILTER Legitimate switch-level TX pipeline drops + + admin@sonic:~$ show dropcounters configuration -g LEGIT + Counter Alias Group Type Reasons Description + -------- -------- ----- ------------------ ------------------- -------------- + DEBUG_0 RX_LEGIT LEGIT PORT_INGRESS_DROPS SMAC_EQUALS_DMAC Legitimate port-level RX pipeline drops + INGRESS_VLAN_FILTER + ``` + +**show dropcounters counts** + +This command is used to show the current statistics for the configured drop counters. Standard drop counters are displayed as well for convenience. + +Because clear (see below) is handled on a per-user basis different users may see different drop counts. + +- Usage: + ``` + show dropcounters counts [-g ] [-t ] + ``` + +- Example: + ``` + admin@sonic:~$ show dropcounters counts + IFACE STATE RX_ERR RX_DROPS TX_ERR TX_DROPS RX_LEGIT + --------- ------- -------- ---------- -------- ---------- --------- + Ethernet0 U 10 100 0 0 20 + Ethernet4 U 0 1000 0 0 100 + Ethernet8 U 100 10 0 0 0 + + DEVICE TX_LEGIT + ------ -------- + sonic 1000 + + admin@sonic:~$ show dropcounters counts -g LEGIT + IFACE STATE RX_ERR RX_DROPS TX_ERR TX_DROPS RX_LEGIT + --------- ------- -------- ---------- -------- ---------- --------- + Ethernet0 U 10 100 0 0 20 + Ethernet4 U 0 1000 0 0 100 + Ethernet8 U 100 10 0 0 0 + + admin@sonic:~$ show dropcounters counts -t SWITCH_EGRESS_DROPS + DEVICE TX_LEGIT + ------ -------- + sonic 1000 + ``` + +### Drop Counters config commands + +**config dropcounters install** + +This command is used to initialize a new drop counter. The user must specify a name, type, and initial list of drop reasons. + +This command will fail if the given name is already in use, if the type of counter is not supported, or if any of the specified drop reasons are not supported. It will also fail if all avaialble counters are already in use on the device. + +- Usage: + ``` + admin@sonic:~$ sudo config dropcounters install [-d ] [-g ] [-a ] + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounters install DEBUG_2 PORT_INGRESS_DROPS [EXCEEDS_L2_MTU,DECAP_ERROR] -d "More port ingress drops" -g BAD -a BAD_DROPS + ``` + +**config dropcounters add_reasons** + +This command is used to add drop reasons to an already initialized counter. + +This command will fail if any of the specified drop reasons are not supported. + +- Usage: + ``` + admin@sonic:~$ sudo config dropcounters add_reasons + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounters add_reasons DEBUG_2 [SIP_CLASS_E] + ``` + +**config dropcounters remove_reasons** + +This command is used to remove drop reasons from an already initialized counter. + +- Usage: + ``` + admin@sonic:~$ sudo config dropcounters remove_reasons + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounters remove_reasons DEBUG_2 [SIP_CLASS_E] + ``` + +**config dropcounters delete** + +This command is used to delete a drop counter. + +- Usage: + ``` + admin@sonic:~$ sudo config dropcounters delete + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounters delete DEBUG_2 + ``` + +### Drop Counters clear commands + +**sonic-clear dropcounters** + +This comnmand is used to clear drop counters. This is done on a per-user basis. + +- Usage: + ``` + admin@sonic:~$ sonic-clear dropcounters + ``` + +- Example: + ``` + admin@sonic:~$ sonic-clear dropcounters + Cleared drop counters + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#drop-counters) + + ## ECN This section explains all the Explicit Congestion Notification (ECN) show commands and ECN configuation options that are supported in SONiC. diff --git a/scripts/dropconfig b/scripts/dropconfig new file mode 100755 index 000000000000..2ff8fab23642 --- /dev/null +++ b/scripts/dropconfig @@ -0,0 +1,414 @@ +#!/usr/bin/env python + +##################################################################### +# +# dropconfig is a tool for configuring drop counters. +# +##################################################################### + +# FUTURE IMPROVEMENTS +# - Add more filters to the show commands (e.g. filter by name, alias, etc.) +# - Add the ability to change readonly attributes like group, description, etc. + +import swsssdk +import argparse +import os +import sys + +from tabulate import tabulate + +# mock the redis for unit test purposes # +try: + if os.environ["UTILITIES_UNIT_TESTING"] == "1": + modules_path = os.path.join(os.path.dirname(__file__), "..") + test_path = os.path.join(modules_path, "sonic-utilities-tests") + sys.path.insert(0, modules_path) + sys.path.insert(0, test_path) + import mock_tables.dbconnector +except KeyError: + pass + +# CONFIG_DB Tables +DEBUG_COUNTER_CONFIG_TABLE = 'DEBUG_COUNTER' +DROP_REASON_TABLE_PREFIX = 'DEBUG_COUNTER_DROP_REASON' + +# STATE_DB Tables +DEBUG_COUNTER_CAPABILITY_TABLE = 'DEBUG_COUNTER_CAPABILITIES' + +# Drop Counter Configuration Headers +drop_counter_config_header = ['Counter', + 'Alias', + 'Group', + 'Type', + 'Reasons', + 'Description'] +drop_counter_capability_header = ['Counter Type', 'Total'] + +# Drop Reason Prefixes +in_drop_reason_prefix = 'SAI_IN_DROP_REASON_' +out_drop_reason_prefix = 'SAI_OUT_DROP_REASON_' + + +class InvalidArgumentError(RuntimeError): + def __init__(self, msg): + self.message = msg + + +class DropConfig(object): + def __init__(self): + self.config_db = swsssdk.ConfigDBConnector() + self.config_db.connect() + + self.state_db = swsssdk.SonicV2Connector(host='127.0.0.1') + self.state_db.connect(self.state_db.STATE_DB) + + # -c show_config + def print_counter_config(self, group): + """ + Prints out the configuration for all counters that are currently + set up + """ + + table = [] + for counter in self.get_config(group): + table.append((counter.get('name', ''), + counter.get('alias', ''), + counter.get('group', ''), + counter.get('type', ''), + counter.get('reason', ''), + counter.get('description', ''))) + + print(tabulate(table, + drop_counter_config_header, + tablefmt='simple', + stralign='left')) + + def print_device_capabilities(self): + """ + Prints out the capabilities that this device has + """ + + device_caps = self.get_device_capabilities() + + if not device_caps: + print('Current device does not support drop counters') + + table = [] + for counter, capabilities in device_caps.iteritems(): + table.append((counter, capabilities.get('count', 'N/A'))) + print(tabulate(table, + drop_counter_capability_header, + tablefmt='simple', + stralign='left')) + + for counter, capabilities in device_caps.iteritems(): + supported_reasons = deserialize_reason_list(capabilities.get('reasons', '')) + if supported_reasons and int(capabilities.get('count', 0)) > 0: + print('\n{}'.format(counter)) + for reason in supported_reasons: + if reason.startswith(in_drop_reason_prefix): + reason = reason[len(in_drop_reason_prefix):] + elif reason.startswith(out_drop_reason_prefix): + reason = reason[len(out_drop_reason_prefix):] + print('\t{}'.format(reason)) + + def create_counter(self, counter_name, alias, group, counter_type, + description, reasons): + """ + Creates a new counter configuration + """ + + if not counter_name: + raise InvalidArgumentError('Counter name not provided') + + if not counter_type: + raise InvalidArgumentError('Counter type not provided') + + if not reasons: + raise InvalidArgumentError('No drop reasons provided') + + if self.counter_name_in_use(counter_name): + raise InvalidArgumentError('Counter name \'{}\' already in use'.format(counter_name)) + + available_counters = self.get_available_counters(counter_type) + if available_counters is None: + raise InvalidArgumentError('Counter type not supported on this device') + elif int(available_counters) <= len(self.config_db.get_keys(DEBUG_COUNTER_CONFIG_TABLE)): + raise InvalidArgumentError('All counters of this type are currently in use') + + supported_reasons = self.get_supported_reasons(counter_type) + if supported_reasons is None: + raise InvalidArgumentError('No drop reasons found for this device') + elif not all(r in supported_reasons for r in reasons): + raise InvalidArgumentError('One or more provided drop reason not supported on this device') + + for reason in reasons: + self.config_db.set_entry(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name)), + reason, + {}) + + drop_counter_entry = {'type': counter_type} + + if alias: + drop_counter_entry['alias'] = alias + if group: + drop_counter_entry['group'] = group + if description or description == '': + drop_counter_entry['desc'] = description + + self.config_db.set_entry(DEBUG_COUNTER_CONFIG_TABLE, + counter_name, + drop_counter_entry) + + def delete_counter(self, counter_name): + """ + Deletes a given counter configuration + """ + + if not counter_name: + raise InvalidArgumentError('No counter name provided') + + if not self.counter_name_in_use(counter_name): + raise InvalidArgumentError('Counter \'{}\' not found'.format(counter_name)) + + self.config_db.set_entry(DEBUG_COUNTER_CONFIG_TABLE, + counter_name, + None) + self.config_db.delete_table(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name))) + + def add_reasons(self, counter_name, reasons): + """ + Add a drop reason to a given counter's configuration + """ + + if not counter_name: + raise InvalidArgumentError('No counter name provided') + + if not reasons: + raise InvalidArgumentError('No drop reasons provided') + + if not self.counter_name_in_use(counter_name): + raise InvalidArgumentError('Counter \'{}\' not found'.format(counter_name)) + + supported_reasons = self.get_supported_reasons(self.get_counter_type(counter_name)) + if supported_reasons is None: + raise InvalidArgumentError('No drop reasons found for this device') + elif not all(r in supported_reasons for r in reasons): + raise InvalidArgumentError('One or more provided drop reason not supported on this device') + + for reason in reasons: + self.config_db.set_entry(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name)), + reason, + {}) + + def remove_reasons(self, counter_name, reasons): + """ + Remove a drop reason from a given counter's configuration + """ + + if not counter_name: + raise InvalidArgumentError('No counter name provided') + + if not reasons: + raise InvalidArgumentError('No drop reasons provided') + + if not self.counter_name_in_use(counter_name): + raise InvalidArgumentError('Counter \'{}\' not found'.format(counter_name)) + + for reason in reasons: + self.config_db.set_entry(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name)), + reason, + None) + + def get_config(self, group): + """ + Get the current counter configuration from CONFIG_DB + """ + + def get_counter_config(counter_name, counter_attributes): + """ + Gets the configuration for a specific counter. + """ + + counter_metadata = { + 'name': counter_name, + 'alias': counter_attributes.get('alias', counter_name), + 'group': counter_attributes.get('group', 'N/A'), + 'type': counter_attributes.get('type', 'N/A'), + 'description': counter_attributes.get('desc', 'N/A') + } + + # Get the drop reasons for this counter + drop_reason_keys = sorted(self.config_db.get_keys(self.config_db.serialize_key((DROP_REASON_TABLE_PREFIX, counter_name))), key=lambda x: x[1]) + + # Fill in the first drop reason + num_reasons = len(drop_reason_keys) + if num_reasons == 0: + counter_metadata['reason'] = 'None' + else: + counter_metadata['reason'] = drop_reason_keys[0][1] + + if num_reasons <= 1: + return [counter_metadata] + + # Add additional rows for remaining drop reasons + counter_config = [counter_metadata] + for drop_reason in drop_reason_keys[1:]: + counter_config.append({'reason': drop_reason[1]}) + + return counter_config + + config_table = self.config_db.get_table(DEBUG_COUNTER_CONFIG_TABLE) + config = [] + for counter_name, counter_attributes in sorted(config_table.iteritems()): + if group and counter_attributes.get('group', '') != group: + continue + + config += get_counter_config(counter_name, counter_attributes) + return config + + def get_device_capabilities(self): + """ + Get the device capabilities from STATE_DB + """ + + capability_query = self.state_db.keys(self.state_db.STATE_DB, '{}|*'.format(DEBUG_COUNTER_CAPABILITY_TABLE)) + + if not capability_query: + return None + + counter_caps = {} + for counter_type in capability_query: + # Because keys returns the whole key, we trim off the DEBUG_COUNTER_CAPABILITY prefix here + counter_caps[counter_type[len(DEBUG_COUNTER_CAPABILITY_TABLE) + 1:]] = self.state_db.get_all(self.state_db.STATE_DB, counter_type) + return counter_caps + + def counter_name_in_use(self, counter_name): + return self.config_db.get_entry(DEBUG_COUNTER_CONFIG_TABLE, counter_name) != {} + + def get_counter_type(self, counter_name): + return self.config_db.get_entry(DEBUG_COUNTER_CONFIG_TABLE, counter_name).get('type', None) + + def get_available_counters(self, counter_type): + if counter_type is None: + return None + + cap_query = self.state_db.get_all(self.state_db.STATE_DB, '{}|{}'.format(DEBUG_COUNTER_CAPABILITY_TABLE, counter_type)) + + if not cap_query: + return None + + return cap_query.get('count', 0) + + def get_supported_reasons(self, counter_type): + if counter_type is None: + return None + + cap_query = self.state_db.get_all(self.state_db.STATE_DB, '{}|{}'.format(DEBUG_COUNTER_CAPABILITY_TABLE, counter_type)) + + if not cap_query: + return None + + reasons = [] + for reason in deserialize_reason_list(cap_query.get('reasons', '')): + if reason.startswith(in_drop_reason_prefix): + reasons.append(reason[len(in_drop_reason_prefix):]) + elif reason.startswith(out_drop_reason_prefix): + reasons.append(reason[len(out_drop_reason_prefix):]) + return reasons + + +def deserialize_reason_list(list_str): + if list_str is None: + return None + + if '|' in list_str or ':' in list_str: + raise InvalidArgumentError('Malformed drop reason provided') + + list_str = list_str.replace(' ', '') + list_str = list_str.strip('[') + list_str = list_str.strip(']') + + if len(list_str) == 0: + return [] + else: + return list_str.split(',') + + +def main(): + parser = argparse.ArgumentParser(description='Manage drop counters', + version='1.0.0', + formatter_class=argparse.RawTextHelpFormatter, + epilog=""" +Examples: + dropconfig +""") + + # Actions + parser.add_argument('-c', '--command', type=str, help='Desired action to perform') + + # Variables + parser.add_argument('-n', '--name', type=str, help='The name of the target drop counter', default=None) + parser.add_argument('-a', '--alias', type=str, help='The alias of the target drop counter', default=None) + parser.add_argument('-g', '--group', type=str, help='The group of the target drop counter', default=None) + parser.add_argument('-t', '--type', type=str, help='The type of the target drop counter', default=None) + parser.add_argument('-d', '--desc', type=str, help='The description for the target drop counter', default=None) + parser.add_argument('-r', '--reasons', type=str, help='The list of drop reasons for the target drop counter', default=None) + + args = parser.parse_args() + + command = args.command + + name = args.name + alias = args.alias + group = args.group + counter_type = args.type + description = args.desc + drop_reasons = args.reasons + + reasons = deserialize_reason_list(drop_reasons) + + dconfig = DropConfig() + + if command == 'install': + try: + dconfig.create_counter(name, + alias, + group, + counter_type, + description, + reasons) + except InvalidArgumentError as err: + print('Encountered error trying to install counter: {}'.format(err.message)) + exit(1) + elif command == 'uninstall': + try: + dconfig.delete_counter(name) + except InvalidArgumentError as err: + print('Encountered error trying to uninstall counter: {}'.format(err.message)) + exit(1) + elif command == 'add': + try: + dconfig.add_reasons(name, reasons) + except InvalidArgumentError as err: + print('Encountered error trying to add reasons: {}'.format(err.message)) + exit(1) + elif command == 'remove': + try: + dconfig.remove_reasons(name, reasons) + except InvalidArgumentError as err: + print('Encountered error trying to remove reasons: {}'.format(err.message)) + exit(1) + elif command == 'show_config': + dconfig.print_counter_config(group) + elif command == 'show_capabilities': + dconfig.print_device_capabilities() + else: + print("Command not recognized") + +if __name__ == '__main__': + main() diff --git a/scripts/dropstat b/scripts/dropstat new file mode 100755 index 000000000000..7baedd6fd5e1 --- /dev/null +++ b/scripts/dropstat @@ -0,0 +1,424 @@ +#!/usr/bin/env python + +##################################################################### +# +# dropstat is a tool for displaying drop counters. +# +##################################################################### + +# FUTURE IMPROVEMENTS +# - Add the ability to filter by group and type +# - Refactor calls to COUNTERS_DB to reduce redundancy +# - Cache DB queries to reduce # of expensive queries + +import argparse +import swsssdk +import os +import sys +import cPickle as pickle +import socket + +from tabulate import tabulate +from collections import OrderedDict +from natsort import natsorted + +# mock the redis for unit test purposes # +try: + if os.environ["UTILITIES_UNIT_TESTING"] == "1": + modules_path = os.path.join(os.path.dirname(__file__), "..") + test_path = os.path.join(modules_path, "sonic-utilities-tests") + sys.path.insert(0, modules_path) + sys.path.insert(0, test_path) + import mock_tables.dbconnector + socket.gethostname = lambda : 'sonic_drops_test' +except KeyError: + pass + +# COUNTERS_DB Tables +DEBUG_COUNTER_PORT_STAT_MAP = 'COUNTERS_DEBUG_NAME_PORT_STAT_MAP' +DEBUG_COUNTER_SWITCH_STAT_MAP = 'COUNTERS_DEBUG_NAME_SWITCH_STAT_MAP' +COUNTERS_PORT_NAME_MAP = 'COUNTERS_PORT_NAME_MAP' +COUNTER_TABLE_PREFIX = 'COUNTERS:' + +# ASIC_DB Tables +ASIC_SWITCH_INFO_PREFIX = 'ASIC_STATE:SAI_OBJECT_TYPE_SWITCH:' + +# APPL_DB Tables +PORT_STATUS_TABLE_PREFIX = "PORT_TABLE:" +PORT_OPER_STATUS_FIELD = "oper_status" +PORT_ADMIN_STATUS_FIELD = "admin_status" +PORT_STATUS_VALUE_UP = 'UP' +PORT_STATUS_VALUE_DOWN = 'DOWN' +PORT_SPEED_FIELD = "speed" + +PORT_STATE_UP = 'U' +PORT_STATE_DOWN = 'D' +PORT_STATE_DISABLED = 'X' +PORT_STATE_NA = 'N/A' + +# CONFIG_DB Tables +DEBUG_COUNTER_CONFIG_TABLE = 'DEBUG_COUNTER' + +# Standard Port-Level Counters +std_port_rx_counters = ['SAI_PORT_STAT_IF_IN_ERRORS', 'SAI_PORT_STAT_IF_IN_DISCARDS'] +std_port_tx_counters = ['SAI_PORT_STAT_IF_OUT_ERRORS', 'SAI_PORT_STAT_IF_OUT_DISCARDS'] + +# Standard Port-Level Headers +std_port_description_header = ['IFACE', 'STATE'] +std_port_headers_map = { + 'SAI_PORT_STAT_IF_IN_ERRORS': 'RX_ERR', + 'SAI_PORT_STAT_IF_IN_DISCARDS': 'RX_DROPS', + 'SAI_PORT_STAT_IF_OUT_ERRORS': 'TX_ERR', + 'SAI_PORT_STAT_IF_OUT_DISCARDS': 'TX_DROPS' +} + +# Standard Switch-Level Headers +std_switch_description_header = ['DEVICE'] + +# Bookkeeping Files +dropstat_dir = '/tmp/dropstat/' + + +class DropStat(object): + def __init__(self): + self.config_db = swsssdk.ConfigDBConnector() + self.config_db.connect() + + self.db = swsssdk.SonicV2Connector(host='127.0.0.1') + self.db.connect(self.db.COUNTERS_DB) + self.db.connect(self.db.ASIC_DB) + self.db.connect(self.db.APPL_DB) + + self.port_drop_stats_file = os.path.join(dropstat_dir, 'port-stats-{}'.format(os.getuid())) + self.switch_drop_stats_file = os.path.join(dropstat_dir + 'switch-stats-{}'.format(os.getuid())) + + self.stat_lookup = {} + self.reverse_stat_lookup = {} + + def show_drop_counts(self, group, counter_type): + """ + Prints out the current drop counts at the port-level and + switch-level. + """ + + self.show_port_drop_counts(group, counter_type) + print('') + self.show_switch_drop_counts(group, counter_type) + + def clear_drop_counts(self): + """ + Clears the current drop counts. + """ + + try: + pickle.dump(self.get_counts_table(self.gather_counters(std_port_rx_counters + std_port_tx_counters, DEBUG_COUNTER_PORT_STAT_MAP), COUNTERS_PORT_NAME_MAP), + open(self.port_drop_stats_file, 'w+')) + pickle.dump(self.get_counts(self.gather_counters([], DEBUG_COUNTER_SWITCH_STAT_MAP), self.get_switch_id()), + open(self.switch_drop_stats_file, 'w+')) + except IOError as e: + print(e) + sys.exit(e.errno) + print("Cleared drop counters") + + def show_port_drop_counts(self, group, counter_type): + """ + Prints out the drop counts at the port level, if such counts exist. + """ + + port_drop_ckpt = {} + + # Grab the latest clear checkpoint, if it exists + if os.path.isfile(self.port_drop_stats_file): + port_drop_ckpt = pickle.load(open(self.port_drop_stats_file, 'r')) + + counters = self.gather_counters(std_port_rx_counters + std_port_tx_counters, DEBUG_COUNTER_PORT_STAT_MAP, group, counter_type) + headers = std_port_description_header + self.gather_headers(counters, DEBUG_COUNTER_PORT_STAT_MAP) + + if not counters: + return + + table = [] + for key, value in self.get_counts_table(counters, COUNTERS_PORT_NAME_MAP).iteritems(): + row = [key, self.get_port_state(key)] + for counter in counters: + row.append(value.get(counter, 0) - port_drop_ckpt.get(key, {}).get(counter, 0)) + table.append(row) + + if table: + print(tabulate(table, headers, tablefmt='simple', stralign='right')) + + def show_switch_drop_counts(self, group, counter_type): + """ + Prints out the drop counts at the switch level, if such counts exist. + """ + + switch_drop_ckpt = {} + + # Grab the latest clear checkpoint, if it exists + if os.path.isfile(self.switch_drop_stats_file): + switch_drop_ckpt = pickle.load(open(self.switch_drop_stats_file, 'r')) + + counters = self.gather_counters([], DEBUG_COUNTER_SWITCH_STAT_MAP, group, counter_type) + headers = std_switch_description_header + self.gather_headers(counters, DEBUG_COUNTER_SWITCH_STAT_MAP) + + if not counters: + return + + switch_id = self.get_switch_id() + switch_stats = self.get_counts(counters, switch_id) + + if not switch_stats: + return + + row = [socket.gethostname()] + for counter in counters: + row.append(switch_stats.get(counter, 0) - switch_drop_ckpt.get(counter, 0)) + + if row: + print(tabulate([row], headers, tablefmt='simple', stralign='right')) + + def gather_counters(self, std_counters, object_stat_map, group=None, counter_type=None): + """ + Gather the list of counters to be counted, filtering out those that are not in + the group or not the right counter type. + """ + + configured_counters = self.get_configured_counters(object_stat_map) + counters = std_counters + configured_counters + return [ctr for ctr in counters + if self.in_group(ctr, object_stat_map, group) and + self.is_type(ctr, object_stat_map, counter_type)] + + def gather_headers(self, counters, object_stat_map): + """ + Gather the list of headers that are needed to display the given counters. + """ + + headers = [] + counter_names = self.get_reverse_stat_lookup(object_stat_map) + + for counter in counters: + if counter in std_port_headers_map.iterkeys(): + headers.append(std_port_headers_map[counter]) + else: + headers.append(self.get_alias(counter_names[counter])) + + return headers + + def get_counts(self, counters, oid): + """ + Get the drop counts for an individual counter. + """ + + counts = {} + + table_id = COUNTER_TABLE_PREFIX + oid + for counter in counters: + counter_data = self.db.get(self.db.COUNTERS_DB, table_id, counter) + if counter_data is None: + counts[counter] = 0 + else: + counts[counter] = int(counter_data) + return counts + + def get_counts_table(self, counters, object_table): + """ + Returns a dictionary containing a mapping from an object (like a port) + to its drop counts. Drop counts are contained in a dictionary that maps + counter name to its counts. + """ + + counter_object_name_map = self.db.get_all(self.db.COUNTERS_DB, object_table) + current_stat_dict = OrderedDict() + + if counter_object_name_map is None: + return current_stat_dict + + for obj in natsorted(counter_object_name_map): + current_stat_dict[obj] = self.get_counts(counters, counter_object_name_map[obj]) + return current_stat_dict + + def get_switch_id(self): + """ + Returns the ID of the current switch + """ + + switch_id = self.db.keys(self.db.ASIC_DB, ASIC_SWITCH_INFO_PREFIX + '*')[0] + return switch_id[len(ASIC_SWITCH_INFO_PREFIX):] + + def get_stat_lookup(self, object_stat_map): + """ + Retrieves the mapping from counter name -> object stat for + the given object type. + """ + + if not self.stat_lookup.get(object_stat_map, None): + stats_map = self.db.get_all(self.db.COUNTERS_DB, object_stat_map) + if stats_map: + self.stat_lookup[object_stat_map] = stats_map + else: + self.stat_lookup[object_stat_map] = None + + return self.stat_lookup[object_stat_map] + + def get_reverse_stat_lookup(self, object_stat_map): + """ + Retrieves the mapping from object stat -> counter name for + the given object type. + """ + + if not self.reverse_stat_lookup.get(object_stat_map, None): + stats_map = self.get_stat_lookup(object_stat_map) + if stats_map: + self.reverse_stat_lookup[object_stat_map] = {v: k for k, v in stats_map.iteritems()} + else: + self.reverse_stat_lookup[object_stat_map] = None + + return self.reverse_stat_lookup[object_stat_map] + + def get_configured_counters(self, object_stat_map): + """ + Returns the list of counters that have been configured to + track packet drops. + """ + + counters = self.get_stat_lookup(object_stat_map) + + configured_counters = [] + if not counters: + return configured_counters + + return [ctr for ctr in counters.itervalues()] + + def get_counter_name(self, object_stat_map, counter_stat): + """ + Gets the name of the counter associated with the given + counter stat. + """ + + lookup_table = self.get_reverse_stat_lookup(object_stat_map) + + if not lookup_table: + return None + + return lookup_table.get(counter_stat, None) + + def get_alias(self, counter_name): + """ + Gets the alias for the given counter name. If the counter + has no alias then the counter name is returned. + """ + + alias_query = self.config_db.get_entry(DEBUG_COUNTER_CONFIG_TABLE, counter_name) + + if not alias_query: + return counter_name + + return alias_query.get('alias', counter_name) + + def in_group(self, counter_stat, object_stat_map, group): + """ + Checks whether the given counter_stat is part of the + given group. + + If no group is provided this method will return True. + """ + + if not group: + return True + + if counter_stat in std_port_rx_counters or counter_stat in std_port_tx_counters: + return False + + group_query = self.config_db.get_entry(DEBUG_COUNTER_CONFIG_TABLE, self.get_counter_name(object_stat_map, counter_stat)) + + if not group_query: + return False + + return group == group_query.get('group', None) + + def is_type(self, counter_stat, object_stat_map, counter_type): + """ + Checks whether the type of the given counter_stat is the same as + counter_type. + + If no counter_type is provided this method will return True. + """ + + if not counter_type: + return True + + if counter_stat in std_port_rx_counters and counter_type == 'PORT_INGRESS_DROPS': + return True + + if counter_stat in std_port_tx_counters and counter_type == 'PORT_EGRESS_DROPS': + return True + + type_query = self.config_db.get_entry(DEBUG_COUNTER_CONFIG_TABLE, self.get_counter_name(object_stat_map, counter_stat)) + + if not type_query: + return False + + return counter_type == type_query.get('type', None) + + def get_port_state(self, port_name): + """ + Get the state of the given port. + """ + full_table_id = PORT_STATUS_TABLE_PREFIX + port_name + admin_state = self.db.get(self.db.APPL_DB, full_table_id, PORT_ADMIN_STATUS_FIELD) + oper_state = self.db.get(self.db.APPL_DB, full_table_id, PORT_OPER_STATUS_FIELD) + if admin_state is None or oper_state is None: + return PORT_STATE_NA + elif admin_state.upper() == PORT_STATUS_VALUE_DOWN: + return PORT_STATE_DISABLED + elif admin_state.upper() == PORT_STATUS_VALUE_UP and oper_state.upper() == PORT_STATUS_VALUE_UP: + return PORT_STATE_UP + elif admin_state.upper() == PORT_STATUS_VALUE_UP and oper_state.upper() == PORT_STATUS_VALUE_DOWN: + return PORT_STATE_DOWN + else: + return PORT_STATE_NA + + +def main(): + parser = argparse.ArgumentParser(description='Display drop counters', + version='1.0.0', + formatter_class=argparse.RawTextHelpFormatter, + epilog=""" +Examples: + dropstat +""") + + # Actions + parser.add_argument('-c', '--command', type=str, help='Desired action to perform') + + # Variables + parser.add_argument('-g', '--group', type=str, help='The group of the target drop counter', default=None) + parser.add_argument('-t', '--type', type=str, help='The type of the target drop counter', default=None) + + args = parser.parse_args() + + command = args.command + + group = args.group + counter_type = args.type + + # Create the directory to hold clear results + if not os.path.exists(dropstat_dir): + try: + os.makedirs(dropstat_dir) + except IOError as e: + print(e) + sys.exit(e.errno) + + dcstat = DropStat() + + if command == 'clear': + dcstat.clear_drop_counts() + elif command == 'show': + dcstat.show_drop_counts(group, counter_type) + else: + print("Command not recognized") + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 44c8dabe1c7e..b2abcc120810 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,8 @@ 'scripts/db_migrator.py', 'scripts/decode-syseeprom', 'scripts/dropcheck', + 'scripts/dropconfig', + 'scripts/dropstat', 'scripts/ecnconfig', 'scripts/fast-reboot', 'scripts/fast-reboot-dump.py', diff --git a/show/main.py b/show/main.py index adee9d138dd3..7edbfa758e76 100755 --- a/show/main.py +++ b/show/main.py @@ -2151,6 +2151,58 @@ def table(table_name, verbose): run_command(cmd, display_cmd=verbose) +# +# 'dropcounters' group ### +# + +@cli.group(cls=AliasedGroup, default_if_no_args=False) +def dropcounters(): + """Show drop counter related information""" + pass + + +# 'configuration' subcommand ("show dropcounters configuration") +@dropcounters.command() +@click.option('-g', '--group', required=False) +@click.option('--verbose', is_flag=True, help="Enable verbose output") +def configuration(group, verbose): + """Show current drop counter configuration""" + cmd = "dropconfig -c show_config" + + if group: + cmd += " -g '{}'".format(group) + + run_command(cmd, display_cmd=verbose) + + +# 'capabilities' subcommand ("show dropcounters capabilities") +@dropcounters.command() +@click.option('--verbose', is_flag=True, help="Enable verbose output") +def capabilities(verbose): + """Show device drop counter capabilities""" + cmd = "dropconfig -c show_capabilities" + + run_command(cmd, display_cmd=verbose) + + +# 'counts' subcommand ("show dropcounters counts") +@dropcounters.command() +@click.option('-g', '--group', required=False) +@click.option('-t', '--counter_type', required=False) +@click.option('--verbose', is_flag=True, help="Enable verbose output") +def counts(group, counter_type, verbose): + """Show drop counts""" + cmd = "dropstat -c show" + + if group: + cmd += " -g '{}'".format(group) + + if counter_type: + cmd += " -t '{}'".format(counter_type) + + run_command(cmd, display_cmd=verbose) + + # # 'ecn' command ("show ecn") # diff --git a/sonic-utilities-tests/drops_group_test.py b/sonic-utilities-tests/drops_group_test.py new file mode 100644 index 000000000000..6a9e71099c31 --- /dev/null +++ b/sonic-utilities-tests/drops_group_test.py @@ -0,0 +1,135 @@ +import sys +import os +import pytest +import click +import swsssdk +from click.testing import CliRunner + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, "scripts") +sys.path.insert(0, test_path) +sys.path.insert(0, modules_path) + +import mock_tables.dbconnector +import show.main as show +import clear.main as clear + +expected_counter_capabilities = """Counter Type Total +------------------- ------- +PORT_INGRESS_DROPS 4 +SWITCH_EGRESS_DROPS 2 + +PORT_INGRESS_DROPS + IP_HEADER_ERROR + NO_L3_HEADER + +SWITCH_EGRESS_DROPS + ACL_ANY + L2_ANY + L3_ANY +""" + +expected_counter_configuration = """Counter Alias Group Type Reasons Description +--------- ------------ ------------ ------------------- --------- -------------------------------------------------- +DEBUG_0 DEBUG_0 N/A PORT_INGRESS_DROPS None N/A +DEBUG_1 SWITCH_DROPS PACKET_DROPS SWITCH_EGRESS_DROPS None Outgoing packet drops, tracked at the switch level +DEBUG_2 DEBUG_2 N/A PORT_INGRESS_DROPS None +""" + +expected_counter_configuration_with_group = """Counter Alias Group Type Reasons Description +--------- ------------ ------------ ------------------- --------- -------------------------------------------------- +DEBUG_1 SWITCH_DROPS PACKET_DROPS SWITCH_EGRESS_DROPS None Outgoing packet drops, tracked at the switch level +""" + +expected_counts = """ IFACE STATE RX_ERR RX_DROPS TX_ERR TX_DROPS DEBUG_2 DEBUG_0 +--------- ------- -------- ---------- -------- ---------- --------- --------- +Ethernet0 D 10 100 0 0 20 80 +Ethernet4 N/A 0 1000 0 0 100 800 +Ethernet8 N/A 100 10 0 0 0 10 + + DEVICE SWITCH_DROPS +---------------- -------------- +sonic_drops_test 1000 +""" + +expected_counts_with_group = """ + DEVICE SWITCH_DROPS +---------------- -------------- +sonic_drops_test 1000 +""" + +expected_counts_with_type = """ IFACE STATE RX_ERR RX_DROPS DEBUG_2 DEBUG_0 +--------- ------- -------- ---------- --------- --------- +Ethernet0 D 10 100 20 80 +Ethernet4 N/A 0 1000 100 800 +Ethernet8 N/A 100 10 0 10 + +""" + +expected_counts_with_clear = """ IFACE STATE RX_ERR RX_DROPS TX_ERR TX_DROPS DEBUG_2 DEBUG_0 +--------- ------- -------- ---------- -------- ---------- --------- --------- +Ethernet0 D 0 0 0 0 0 0 +Ethernet4 N/A 0 0 0 0 0 0 +Ethernet8 N/A 0 0 0 0 0 0 + + DEVICE SWITCH_DROPS +---------------- -------------- +sonic_drops_test 0 +""" + +class TestDropCounters(object): + @classmethod + def setup_class(cls): + print("SETUP") + os.environ["PATH"] += os.pathsep + scripts_path + os.environ["UTILITIES_UNIT_TESTING"] = "1" + + def test_show_capabilities(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["capabilities"], []) + print(result.output) + assert result.output == expected_counter_capabilities + + def test_show_configuration(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["configuration"], []) + print(result.output) + assert result.output == expected_counter_configuration + + def test_show_configuration_with_group(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["configuration"], ["-g", "PACKET_DROPS"]) + print(result.output) + assert result.output == expected_counter_configuration_with_group + + def test_show_counts(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["counts"], []) + print(result.output) + assert result.output == expected_counts + + def test_show_counts_with_group(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["counts"], ["-g", "PACKET_DROPS"]) + print(result.output) + assert result.output == expected_counts_with_group + + def test_show_counts_with_type(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["counts"], ["-t", "PORT_INGRESS_DROPS"]) + print(result.output) + assert result.output == expected_counts_with_type + + def test_show_counts_with_clear(self): + runner = CliRunner() + runner.invoke(clear.cli.commands["dropcounters"]) + result = runner.invoke(show.cli.commands["dropcounters"].commands["counts"], []) + print(result.output) + assert result.output == expected_counts_with_clear + + @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" diff --git a/sonic-utilities-tests/mock_tables/asic_db.json b/sonic-utilities-tests/mock_tables/asic_db.json new file mode 100644 index 000000000000..1a769b82b567 --- /dev/null +++ b/sonic-utilities-tests/mock_tables/asic_db.json @@ -0,0 +1,6 @@ +{ + "ASIC_STATE:SAI_OBJECT_TYPE_SWITCH:oid:0x21000000000000": { + "SAI_SWITCH_ATTR_INIT_SWITCH": "true", + "SAI_SWITCH_ATTR_SRC_MAC_ADDRESS": "DE:AD:BE:EF:CA:FE" + } +} diff --git a/sonic-utilities-tests/mock_tables/config_db.json b/sonic-utilities-tests/mock_tables/config_db.json index 97ae4b2edbe1..42ba9b5c6135 100644 --- a/sonic-utilities-tests/mock_tables/config_db.json +++ b/sonic-utilities-tests/mock_tables/config_db.json @@ -119,5 +119,22 @@ "policy_desc": "SSH_ONLY", "services@": "SSH", "type": "CTRLPLANE" - } + }, + "DEBUG_COUNTER|DEBUG_0": { + "type": "PORT_INGRESS_DROPS" + }, + "DEBUG_COUNTER|DEBUG_1": { + "type": "SWITCH_EGRESS_DROPS", + "alias": "SWITCH_DROPS", + "group": "PACKET_DROPS", + "desc": "Outgoing packet drops, tracked at the switch level" + }, + "DEBUG_COUNTER|DEBUG_2": { + "type": "PORT_INGRESS_DROPS", + "desc": "" + }, + "DEBUG_COUNTER_DROP_REASON|DEBUG_0|IP_HEADER_ERROR": {}, + "DEBUG_COUNTER_DROP_REASON|DEBUG_1|ACL_ANY": {}, + "DEBUG_COUNTER_DROP_REASON|DEBUG_2|IP_HEADER_ERROR": {}, + "DEBUG_COUNTER_DROP_REASON|DEBUG_2|NO_L3_HEADER": {} } diff --git a/sonic-utilities-tests/mock_tables/counters_db.json b/sonic-utilities-tests/mock_tables/counters_db.json index b86c9d9d8dcf..c47584119320 100644 --- a/sonic-utilities-tests/mock_tables/counters_db.json +++ b/sonic-utilities-tests/mock_tables/counters_db.json @@ -114,5 +114,38 @@ "COUNTERS:DATAACL:RULE_9": { "Bytes": "900", "Packets": "901" + }, + "COUNTERS:oid:0x1000000000002": { + "SAI_PORT_STAT_IF_IN_ERRORS": "10", + "SAI_PORT_STAT_IF_IN_DISCARDS": "100", + "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "80", + "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "20" + }, + "COUNTERS:oid:0x1000000000004": { + "SAI_PORT_STAT_IF_IN_ERRORS": "0", + "SAI_PORT_STAT_IF_IN_DISCARDS": "1000", + "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "800", + "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "100" + }, + "COUNTERS:oid:0x1000000000006": { + "SAI_PORT_STAT_IF_IN_ERRORS": "100", + "SAI_PORT_STAT_IF_IN_DISCARDS": "10", + "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE": "10", + "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS": "0" + }, + "COUNTERS:oid:0x21000000000000": { + "SAI_SWITCH_STAT_IN_DROP_REASON_RANGE_BASE": "1000" + }, + "COUNTERS_PORT_NAME_MAP": { + "Ethernet0": "oid:0x1000000000002", + "Ethernet4": "oid:0x1000000000004", + "Ethernet8": "oid:0x1000000000006" + }, + "COUNTERS_DEBUG_NAME_PORT_STAT_MAP": { + "DEBUG_0": "SAI_PORT_STAT_IN_DROP_REASON_RANGE_BASE", + "DEBUG_2": "SAI_PORT_STAT_OUT_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS" + }, + "COUNTERS_DEBUG_NAME_SWITCH_STAT_MAP": { + "DEBUG_1": "SAI_SWITCH_STAT_IN_DROP_REASON_RANGE_BASE" } -} \ No newline at end of file +} diff --git a/sonic-utilities-tests/mock_tables/dbconnector.py b/sonic-utilities-tests/mock_tables/dbconnector.py index 4d79034d7a46..5a67337e6dfc 100644 --- a/sonic-utilities-tests/mock_tables/dbconnector.py +++ b/sonic-utilities-tests/mock_tables/dbconnector.py @@ -42,6 +42,8 @@ def __init__(self, *args, **kwargs): db = kwargs.pop('db') if db == 0: fname = 'appl_db.json' + elif db == 1: + fname = 'asic_db.json' elif db == 2: fname = 'counters_db.json' elif db == 4: diff --git a/sonic-utilities-tests/mock_tables/state_db.json b/sonic-utilities-tests/mock_tables/state_db.json index 67b056723466..f3fdf3ec248d 100644 --- a/sonic-utilities-tests/mock_tables/state_db.json +++ b/sonic-utilities-tests/mock_tables/state_db.json @@ -65,6 +65,13 @@ "ACL_ACTIONS|INGRESS": "PACKET_ACTION,REDIRECT_ACTION,MIRROR_INGRESS_ACTION", "ACL_ACTIONS|EGRESS": "PACKET_ACTION,MIRROR_EGRESS_ACTION", "ACL_ACTION|PACKET_ACTION": "FORWARD" + }, + "DEBUG_COUNTER_CAPABILITIES|PORT_INGRESS_DROPS": { + "reasons": "[SAI_IN_DROP_REASON_IP_HEADER_ERROR,SAI_IN_DROP_REASON_NO_L3_HEADER]", + "count": "4" + }, + "DEBUG_COUNTER_CAPABILITIES|SWITCH_EGRESS_DROPS": { + "reasons": "[SAI_IN_DROP_REASON_ACL_ANY,SAI_IN_DROP_REASON_L2_ANY,SAI_IN_DROP_REASON_L3_ANY]", + "count": "2" } } -