From 61aa07046c29c7da35bccdc641066dbd266f71b8 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Sat, 14 Sep 2019 17:23:53 -0700 Subject: [PATCH 01/12] Add CLI support for configurable drop counters - Adds dropconfig script to configure drop counters - Adds dropstat script to show and clear drop counters - Adds handlers for drop counters to show, config, and sonic-clear Signed-off-by: Danny Allen --- clear/main.py | 6 + config/main.py | 54 ++++++ scripts/dropconfig | 306 ++++++++++++++++++++++++++++++++++ scripts/dropstat | 397 +++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 + show/main.py | 47 ++++++ 6 files changed, 812 insertions(+) create mode 100644 scripts/dropconfig create mode 100755 scripts/dropstat diff --git a/clear/main.py b/clear/main.py index 6c274ae99b..f6e1d8c477 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 a6f0e8df2d..98c903eeee 100755 --- a/config/main.py +++ b/config/main.py @@ -1591,6 +1591,60 @@ def incremental(file_name): command = "acl-loader update incremental {}".format(file_name) run_command(command) +# +# 'drops' group ('config drops ...') +# +@config.group() +def drops(): + """Drop counter related configuration tasks""" + pass + +# +# 'init' subcommand ('config drops init') +# +@drops.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 init(counter_name, alias, group, counter_type, desc, reasons, verbose): + """Initialize 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) + +@drops.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) + +@drops.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(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) + +@drops.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(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/scripts/dropconfig b/scripts/dropconfig new file mode 100644 index 0000000000..18172f5e81 --- /dev/null +++ b/scripts/dropconfig @@ -0,0 +1,306 @@ +#!/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 alias, filter by name, etc.) +# - Add the ability to change user-only attributes like alias, group, description + +import swsssdk +import argparse +import os + +from tabulate import tabulate + +# 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'] + +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() + + 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: + print('\n{}'.format(counter)) + for reason in supported_reasons: + 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 Exception('Counter name not provided') + + if not counter_type: + raise Exception('Counter type not provided') + + if not reasons: + raise Exception('No drop reasons provided') + + if self.counter_name_in_use(counter_name): + raise Exception('Counter name \'{}\' already in use'.format(counter_name)) + + # TODO: Ideally we would like to be able to check the device capabilities + # to see if the counter type and reasons are supported on this device. Once + # these features have broader platform support we can incorporate these checks. + + 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: + 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 Exception('No counter name provided') + + if not self.counter_name_in_use(counter_name): + raise Exception('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 Exception('No counter name provided') + + if not reasons: + raise Exception('No drop reasons provided') + + if not self.counter_name_in_use(counter_name): + raise Exception('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, {}) + + def remove_reasons(self, counter_name, reasons): + """ + Remove a drop reason from a given counter's configuration + """ + + if not counter_name: + raise Exception('No counter name provided') + + if not reasons: + raise Exception('No drop reasons provided') + + if not self.counter_name_in_use(counter_name): + raise Exception('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 deserialize_reason_list(list_str): + if list_str is None: + return None + + if '|' in list_str or ':' in list_str: + raise Exception('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 Exception as err: + print('Encountered error trying to install counter: {}'.format(err)) + exit(1) + elif command == 'uninstall': + try: + dconfig.delete_counter(name) + except Exception as err: + print('Encountered error trying to uninstall counter: {}'.format(err)) + exit(1) + elif command == 'add': + try: + dconfig.add_reasons(name, reasons) + except Exception as err: + print('Encountered error trying to add reasons: {}'.format(err)) + exit(1) + elif command == 'remove': + try: + dconfig.remove_reasons(name, reasons) + except Exception as err: + print('Encountered error trying to remove reasons: {}'.format(err)) + 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 0000000000..1b0b533243 --- /dev/null +++ b/scripts/dropstat @@ -0,0 +1,397 @@ +#!/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 + +# 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' +COUNTERS_SWITCH_NAME_MAP = 'COUNTERS_SWITCH_NAME_MAP' +COUNTER_TABLE_PREFIX = 'COUNTERS:' + +# 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_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.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_table(self.gather_counters([], DEBUG_COUNTER_SWITCH_STAT_MAP), COUNTERS_SWITCH_NAME_MAP), + 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 + + table = [] + for key, value in self.get_counts_table(counters, COUNTERS_SWITCH_NAME_MAP).iteritems(): + row = [key] + for counter in counters: + row.append(value.get(counter, 0) - switch_drop_ckpt.get(key, {}).get(counter, 0)) + table.append(row) + + if table: + print(tabulate(table, 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_headers_map.iterkeys(): + headers.append(std_headers_map[counter]) + else: + headers.append(self.get_alias(counter_names[counter])) + + return headers + + 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. + """ + + def get_counts(counters, oid): + """ + Get the drop counts for an individual counter. + """ + + counts = {} + + for counter in counters: + table_id = COUNTER_TABLE_PREFIX + oid + 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 + + 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] = get_counts(counters, counter_object_name_map[obj]) + return current_stat_dict + + 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 44c8dabe1c..b2abcc1208 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 824de63c99..9519729dbb 100755 --- a/show/main.py +++ b/show/main.py @@ -2064,6 +2064,53 @@ def table(table_name, verbose): run_command(cmd, display_cmd=verbose) +# +# 'drops' group ### +# + +@cli.group(cls=AliasedGroup, default_if_no_args=False) +def drops(): + """Show drop counter related information""" + pass + +# 'config' subcommand ("show drops config") +@drops.command() +@click.option('-g', '--group', required=False) +@click.option('--verbose', is_flag=True, help="Enable verbose output") +def config(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 drops capabilities") +@drops.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 drops counts") +@drops.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") From 3b8d26d556101c79c87bf684daf8abb264f64b07 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Tue, 29 Oct 2019 23:51:52 -0700 Subject: [PATCH 02/12] Update command naming --- scripts/dropconfig | 32 ++++++++++++++++---------------- scripts/dropstat | 6 +++--- show/main.py | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/scripts/dropconfig b/scripts/dropconfig index 18172f5e81..49d2380e53 100644 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -35,7 +35,7 @@ class DropConfig(object): self.state_db = swsssdk.SonicV2Connector(host='127.0.0.1') self.state_db.connect(self.state_db.STATE_DB) - # -c show_config + # -c show_config def print_counter_config(self, group): """ Prints out the configuration for all counters that are currently set up @@ -43,7 +43,7 @@ class DropConfig(object): table = [] for counter in self.get_config(group): - table.append((counter.get('name', ''), + table.append((counter.get('name', ''), counter.get('alias', ''), counter.get('group', ''), counter.get('type', ''), @@ -69,7 +69,7 @@ class DropConfig(object): print('\n{}'.format(counter)) for reason in supported_reasons: print('\t{}'.format(reason)) - + def create_counter(self, counter_name, alias, group, counter_type, description, reasons): """ Creates a new counter configuration @@ -77,20 +77,20 @@ class DropConfig(object): if not counter_name: raise Exception('Counter name not provided') - + if not counter_type: raise Exception('Counter type not provided') if not reasons: raise Exception('No drop reasons provided') - + if self.counter_name_in_use(counter_name): raise Exception('Counter name \'{}\' already in use'.format(counter_name)) # TODO: Ideally we would like to be able to check the device capabilities # to see if the counter type and reasons are supported on this device. Once # these features have broader platform support we can incorporate these checks. - + for reason in reasons: self.config_db.set_entry(self.config_db.serialize_key((DROP_REASON_TABLE_PREFIX, counter_name)), reason, {}) @@ -163,27 +163,27 @@ class DropConfig(object): Gets the configuration for a specific counter. """ - counter_metadata = { + 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:]: @@ -206,7 +206,7 @@ class DropConfig(object): """ capability_query = self.state_db.keys(self.state_db.STATE_DB, '{}|*'.format(DEBUG_COUNTER_CAPABILITY_TABLE)) - + if not capability_query: return None @@ -215,7 +215,7 @@ class DropConfig(object): # 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) != {} @@ -229,7 +229,7 @@ def deserialize_reason_list(list_str): list_str = list_str.replace(' ', '') list_str = list_str.strip('[') list_str = list_str.strip(']') - + if len(list_str) == 0: return [] else: @@ -254,8 +254,8 @@ Examples: 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 diff --git a/scripts/dropstat b/scripts/dropstat index 1b0b533243..880630955b 100755 --- a/scripts/dropstat +++ b/scripts/dropstat @@ -52,7 +52,7 @@ std_port_tx_counters = ['SAI_PORT_STAT_IF_OUT_ERRORS', 'SAI_PORT_STAT_IF_OUT_DIS # Standard Port-Level Headers std_port_description_header = ['IFACE', 'STATE'] std_headers_map = { - 'SAI_PORT_STAT_IF_IN_ERRORS': 'RX_ERR', + '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' @@ -234,7 +234,7 @@ class DropStat(object): 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 @@ -368,7 +368,7 @@ Examples: # 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 diff --git a/show/main.py b/show/main.py index 9519729dbb..c767c5fd2e 100755 --- a/show/main.py +++ b/show/main.py @@ -2073,11 +2073,11 @@ def drops(): """Show drop counter related information""" pass -# 'config' subcommand ("show drops config") +# 'configuration' subcommand ("show drops configuration") @drops.command() @click.option('-g', '--group', required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") -def config(group, verbose): +def configuration(group, verbose): """Show current drop counter configuration""" cmd = "dropconfig -c show_config" From 3fcb0a8b728a1195a549e45777f33dae9e1a07b0 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Wed, 30 Oct 2019 00:08:39 -0700 Subject: [PATCH 03/12] Add safety check for unsupported drop counters/reasons --- scripts/dropconfig | 48 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/scripts/dropconfig b/scripts/dropconfig index 49d2380e53..792cd776e2 100644 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -58,6 +58,9 @@ class DropConfig(object): 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'))) @@ -87,9 +90,17 @@ class DropConfig(object): if self.counter_name_in_use(counter_name): raise Exception('Counter name \'{}\' already in use'.format(counter_name)) - # TODO: Ideally we would like to be able to check the device capabilities - # to see if the counter type and reasons are supported on this device. Once - # these features have broader platform support we can incorporate these checks. + available_counters = self.get_available_counters(counter_type) + if available_counters is None: + raise Exception('Counter type not supported on this device') + elif int(available_counters) == 0: + raise Exception('All counters of this type are currently in use') + + supported_reasons = self.get_supported_reasons(counter_type) + if supported_reasons is None: + raise Exception('No drop reasons found for this device') + elif not all(r in supported_reasons for r in reasons): + raise Exception('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, {}) @@ -133,6 +144,12 @@ class DropConfig(object): if not self.counter_name_in_use(counter_name): raise Exception('Counter \'{}\' not found'.format(counter_name)) + supported_reasons = self.get_supported_reasons(self.get_counter_type(counter_name)) + if supported_reasons is None: + raise Exception('No drop reasons found for this device') + elif not all(r in supported_reasons for r in reasons): + raise Exception('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, {}) @@ -219,6 +236,31 @@ class DropConfig(object): 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 + + return deserialize_reason_list(cap_query.get('reasons', '')) + def deserialize_reason_list(list_str): if list_str is None: return None From 0cdda0d4a6714e23bd8c991686a893cf4621bde6 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Wed, 30 Oct 2019 00:46:04 -0700 Subject: [PATCH 04/12] Make drop reasons more easily readable --- scripts/dropconfig | 16 +++++++++++++++- scripts/dropstat | 40 ++++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 21 deletions(-) mode change 100644 => 100755 scripts/dropconfig diff --git a/scripts/dropconfig b/scripts/dropconfig old mode 100644 new mode 100755 index 792cd776e2..ee325635e4 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -27,6 +27,10 @@ DEBUG_COUNTER_CAPABILITY_TABLE = 'DEBUG_COUNTER_CAPABILITIES' 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 DropConfig(object): def __init__(self): self.config_db = swsssdk.ConfigDBConnector() @@ -71,6 +75,10 @@ class DropConfig(object): if supported_reasons: 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): @@ -259,7 +267,13 @@ class DropConfig(object): if not cap_query: return None - return deserialize_reason_list(cap_query.get('reasons', '')) + 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: diff --git a/scripts/dropstat b/scripts/dropstat index 880630955b..fb15346232 100755 --- a/scripts/dropstat +++ b/scripts/dropstat @@ -51,7 +51,7 @@ std_port_tx_counters = ['SAI_PORT_STAT_IF_OUT_ERRORS', 'SAI_PORT_STAT_IF_OUT_DIS # Standard Port-Level Headers std_port_description_header = ['IFACE', 'STATE'] -std_headers_map = { +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', @@ -96,9 +96,9 @@ class DropStat(object): """ 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), + 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_table(self.gather_counters([], DEBUG_COUNTER_SWITCH_STAT_MAP), COUNTERS_SWITCH_NAME_MAP), + pickle.dump(self.get_counts_table(self.gather_counters([], DEBUG_COUNTER_SWITCH_STAT_MAP), COUNTERS_SWITCH_NAME_MAP), open(self.switch_drop_stats_file, 'w+')) except IOError as e: print(e) @@ -111,7 +111,7 @@ class DropStat(object): """ 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')) @@ -123,12 +123,12 @@ class DropStat(object): return table = [] - for key, value in self.get_counts_table(counters, COUNTERS_PORT_NAME_MAP).iteritems(): + 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')) @@ -138,7 +138,7 @@ class DropStat(object): """ 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')) @@ -150,7 +150,7 @@ class DropStat(object): return table = [] - for key, value in self.get_counts_table(counters, COUNTERS_SWITCH_NAME_MAP).iteritems(): + for key, value in self.get_counts_table(counters, COUNTERS_SWITCH_NAME_MAP).iteritems(): row = [key] for counter in counters: row.append(value.get(counter, 0) - switch_drop_ckpt.get(key, {}).get(counter, 0)) @@ -167,7 +167,7 @@ class DropStat(object): configured_counters = self.get_configured_counters(object_stat_map) counters = std_counters + configured_counters - return [ ctr for ctr in 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) ] @@ -180,8 +180,8 @@ class DropStat(object): counter_names = self.get_reverse_stat_lookup(object_stat_map) for counter in counters: - if counter in std_headers_map.iterkeys(): - headers.append(std_headers_map[counter]) + if counter in std_port_headers_map.iterkeys(): + headers.append(std_port_headers_map[counter]) else: headers.append(self.get_alias(counter_names[counter])) @@ -200,7 +200,7 @@ class DropStat(object): """ counts = {} - + for counter in counters: table_id = COUNTER_TABLE_PREFIX + oid counter_data = self.db.get(self.db.COUNTERS_DB, table_id, counter) @@ -209,13 +209,13 @@ class DropStat(object): else: counts[counter] = int(counter_data) return 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] = get_counts(counters, counter_object_name_map[obj]) return current_stat_dict @@ -257,7 +257,7 @@ class DropStat(object): """ counters = self.get_stat_lookup(object_stat_map) - + configured_counters = [] if not counters: return configured_counters @@ -287,7 +287,7 @@ class DropStat(object): if not alias_query: return counter_name - + return alias_query.get('alias', counter_name) def in_group(self, counter_stat, object_stat_map, group): @@ -300,7 +300,7 @@ class DropStat(object): if not group: return True - + if counter_stat in std_port_rx_counters or counter_stat in std_port_tx_counters: return False @@ -308,7 +308,7 @@ class DropStat(object): if not group_query: return False - + return group == group_query.get('group', None) def is_type(self, counter_stat, object_stat_map, counter_type): @@ -332,7 +332,7 @@ class DropStat(object): if not type_query: return False - + return counter_type == type_query.get('type', None) def get_port_state(self, port_name): From 35971540bc79a96884e759ce477d7919d268194a Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Tue, 5 Nov 2019 09:41:23 -0800 Subject: [PATCH 05/12] Respond to community feedback --- clear/main.py | 2 +- config/main.py | 47 +++-- scripts/dropconfig | 164 ++++++++++++------ scripts/dropstat | 99 +++++++---- show/main.py | 21 ++- sonic-utilities-tests/drops_group_test.py | 135 ++++++++++++++ .../mock_tables/asic_db.json | 6 + .../mock_tables/config_db.json | 19 +- .../mock_tables/counters_db.json | 35 +++- .../mock_tables/dbconnector.py | 2 + .../mock_tables/state_db.json | 9 +- 11 files changed, 426 insertions(+), 113 deletions(-) create mode 100644 sonic-utilities-tests/drops_group_test.py create mode 100644 sonic-utilities-tests/mock_tables/asic_db.json diff --git a/clear/main.py b/clear/main.py index f6e1d8c477..60f733997d 100755 --- a/clear/main.py +++ b/clear/main.py @@ -195,7 +195,7 @@ def pfccounters(): run_command(command) @cli.command() -def dropcounters(): +def dropcounter(): """Clear drop counters""" command = "dropstat -c clear" run_command(command) diff --git a/config/main.py b/config/main.py index 98c903eeee..8b9723b6f6 100755 --- a/config/main.py +++ b/config/main.py @@ -1591,18 +1591,21 @@ def incremental(file_name): command = "acl-loader update incremental {}".format(file_name) run_command(command) + # -# 'drops' group ('config drops ...') +# 'dropcounter' group ('config dropcounter ...') # + @config.group() -def drops(): +def dropcounter(): """Drop counter related configuration tasks""" pass + # -# 'init' subcommand ('config drops init') +# 'initialize_counter' subcommand ('config dropcounter initialize_counter') # -@drops.command() +@dropcounter.command() @click.argument("counter_name", type=str, required=True) @click.argument("counter_type", type=str, required=True) @click.argument("reasons", type=str, required=True) @@ -1610,41 +1613,57 @@ def drops(): @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 init(counter_name, alias, group, counter_type, desc, reasons, verbose): +def initialize_counter(counter_name, alias, group, counter_type, desc, reasons, verbose): """Initialize 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) + if alias: + command += " -a '{}'".format(alias) + if group: + command += " -g '{}'".format(group) + if desc: + command += " -d '{}'".format(desc) run_command(command, display_cmd=verbose) -@drops.command() + +# +# 'delete_counter' subcommand ('config dropcounter delete_counter') +# +@dropcounter.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): +def delete_counter(counter_name, verbose): """Delete an existing drop counter""" command = "dropconfig -c uninstall -n {}".format(counter_name) run_command(command, display_cmd=verbose) -@drops.command() + +# +# 'add_reason' subcommand ('config dropcounter add_reason') +# +@dropcounter.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(counter_name, reasons, verbose): +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) -@drops.command() + +# +# 'remove_reason' subcommand ('config dropcounter remove_reason') +# +@dropcounter.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(counter_name, reasons, verbose): +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/scripts/dropconfig b/scripts/dropconfig index ee325635e4..9d4f31cd9b 100755 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -7,15 +7,27 @@ ##################################################################### # FUTURE IMPROVEMENTS -# - Add more filters to the show commands (e.g. filter by alias, filter by name, etc.) -# - Add the ability to change user-only attributes like alias, group, description +# - 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' @@ -24,13 +36,24 @@ DROP_REASON_TABLE_PREFIX = 'DEBUG_COUNTER_DROP_REASON' DEBUG_COUNTER_CAPABILITY_TABLE = 'DEBUG_COUNTER_CAPABILITIES' # Drop Counter Configuration Headers -drop_counter_config_header = ['Counter', 'Alias', 'Group', 'Type', 'Reasons', 'Description'] +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, arg): + self.args = arg + + class DropConfig(object): def __init__(self): self.config_db = swsssdk.ConfigDBConnector() @@ -42,7 +65,8 @@ class DropConfig(object): # -c show_config def print_counter_config(self, group): """ - Prints out the configuration for all counters that are currently set up + Prints out the configuration for all counters that are currently + set up """ table = [] @@ -53,7 +77,11 @@ class DropConfig(object): counter.get('type', ''), counter.get('reason', ''), counter.get('description', ''))) - print(tabulate(table, drop_counter_config_header, tablefmt='simple', stralign='left')) + + print(tabulate(table, + drop_counter_config_header, + tablefmt='simple', + stralign='left')) def print_device_capabilities(self): """ @@ -68,11 +96,14 @@ class DropConfig(object): 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')) + 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: + 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): @@ -81,48 +112,58 @@ class DropConfig(object): reason = reason[len(out_drop_reason_prefix):] print('\t{}'.format(reason)) - def create_counter(self, counter_name, alias, group, counter_type, description, reasons): + def create_counter(self, counter_name, alias, group, counter_type, + description, reasons): """ Creates a new counter configuration """ if not counter_name: - raise Exception('Counter name not provided') + raise InvalidArgumentError('Counter name not provided') if not counter_type: - raise Exception('Counter type not provided') + raise InvalidArgumentError('Counter type not provided') if not reasons: - raise Exception('No drop reasons provided') + raise InvalidArgumentError('No drop reasons provided') if self.counter_name_in_use(counter_name): - raise Exception('Counter name \'{}\' already in use'.format(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 Exception('Counter type not supported on this device') + raise InvalidArgumentError('Counter type not supported on this \ + device') elif int(available_counters) == 0: - raise Exception('All counters of this type are currently in use') + 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 Exception('No drop reasons found for this device') + raise InvalidArgumentError('No drop reasons found for this device') elif not all(r in supported_reasons for r in reasons): - raise Exception('One or more provided drop reason not supported on this device') + 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, {}) + self.config_db.set_entry(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name)), + reason, + {}) - drop_counter_entry = {'type' : counter_type} + drop_counter_entry = {'type': counter_type} if alias: drop_counter_entry['alias'] = alias if group: drop_counter_entry['group'] = group - if description: + if description or description == '': drop_counter_entry['desc'] = description - self.config_db.set_entry(DEBUG_COUNTER_CONFIG_TABLE, counter_name, drop_counter_entry) + self.config_db.set_entry(DEBUG_COUNTER_CONFIG_TABLE, + counter_name, + drop_counter_entry) def delete_counter(self, counter_name): """ @@ -130,13 +171,17 @@ class DropConfig(object): """ if not counter_name: - raise Exception('No counter name provided') + raise InvalidArgumentError('No counter name provided') if not self.counter_name_in_use(counter_name): - raise Exception('Counter \'{}\' not found'.format(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))) + 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): """ @@ -144,22 +189,27 @@ class DropConfig(object): """ if not counter_name: - raise Exception('No counter name provided') + raise InvalidArgumentError('No counter name provided') if not reasons: - raise Exception('No drop reasons provided') + raise InvalidArgumentError('No drop reasons provided') if not self.counter_name_in_use(counter_name): - raise Exception('Counter \'{}\' not found'.format(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 Exception('No drop reasons found for this device') + raise InvalidArgumentError('No drop reasons found for this device') elif not all(r in supported_reasons for r in reasons): - raise Exception('One or more provided drop reason not supported on this device') + 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, {}) + self.config_db.set_entry(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name)), + reason, + {}) def remove_reasons(self, counter_name, reasons): """ @@ -167,16 +217,20 @@ class DropConfig(object): """ if not counter_name: - raise Exception('No counter name provided') + raise InvalidArgumentError('No counter name provided') if not reasons: - raise Exception('No drop reasons provided') + raise InvalidArgumentError('No drop reasons provided') if not self.counter_name_in_use(counter_name): - raise Exception('Counter \'{}\' not found'.format(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) + self.config_db.set_entry(self.config_db.serialize_key( + (DROP_REASON_TABLE_PREFIX, counter_name)), + reason, + None) def get_config(self, group): """ @@ -275,12 +329,13 @@ class DropConfig(object): 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 Exception('Malformed drop reason provided') + raise InvalidArgumentError('Malformed drop reason provided') list_str = list_str.replace(' ', '') list_str = list_str.strip('[') @@ -291,11 +346,12 @@ def deserialize_reason_list(list_str): else: return list_str.split(',') + def main(): - parser = argparse.ArgumentParser(description='Manage drop counters', - version='1.0.0', - formatter_class=argparse.RawTextHelpFormatter, - epilog=""" + parser = argparse.ArgumentParser(description='Manage drop counters', + version='1.0.0', + formatter_class=argparse.RawTextHelpFormatter, + epilog=""" Examples: dropconfig """) @@ -311,16 +367,15 @@ Examples: 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 + name = args.name + alias = args.alias + group = args.group counter_type = args.type - description = args.desc + description = args.desc drop_reasons = args.reasons reasons = deserialize_reason_list(drop_reasons) @@ -329,26 +384,33 @@ Examples: if command == 'install': try: - dconfig.create_counter(name, alias, group, counter_type, description, reasons) - except Exception as err: - print('Encountered error trying to install counter: {}'.format(err)) + dconfig.create_counter(name, + alias, + group, + counter_type, + description, + reasons) + except InvalidArgumentError as err: + print('Encountered error trying to install counter: {}' + .format(err)) exit(1) elif command == 'uninstall': try: dconfig.delete_counter(name) - except Exception as err: - print('Encountered error trying to uninstall counter: {}'.format(err)) + except InvalidArgumentError as err: + print('Encountered error trying to uninstall counter: {}' + .format(err)) exit(1) elif command == 'add': try: dconfig.add_reasons(name, reasons) - except Exception as err: + except InvalidArgumentError as err: print('Encountered error trying to add reasons: {}'.format(err)) exit(1) elif command == 'remove': try: dconfig.remove_reasons(name, reasons) - except Exception as err: + except InvalidArgumentError as err: print('Encountered error trying to remove reasons: {}'.format(err)) exit(1) elif command == 'show_config': diff --git a/scripts/dropstat b/scripts/dropstat index fb15346232..7baedd6fd5 100755 --- a/scripts/dropstat +++ b/scripts/dropstat @@ -22,13 +22,27 @@ 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' -COUNTERS_SWITCH_NAME_MAP = 'COUNTERS_SWITCH_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" @@ -46,7 +60,7 @@ PORT_STATE_NA = 'N/A' 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_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 @@ -64,14 +78,15 @@ std_switch_description_header = ['DEVICE'] # Bookkeeping Files dropstat_dir = '/tmp/dropstat/' -class DropStat(object): +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())) @@ -97,9 +112,9 @@ class DropStat(object): 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_table(self.gather_counters([], DEBUG_COUNTER_SWITCH_STAT_MAP), COUNTERS_SWITCH_NAME_MAP), - open(self.switch_drop_stats_file, 'w+')) + 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) @@ -149,17 +164,20 @@ class DropStat(object): if not counters: return - table = [] - for key, value in self.get_counts_table(counters, COUNTERS_SWITCH_NAME_MAP).iteritems(): - row = [key] - for counter in counters: - row.append(value.get(counter, 0) - switch_drop_ckpt.get(key, {}).get(counter, 0)) - table.append(row) + switch_id = self.get_switch_id() + switch_stats = self.get_counts(counters, switch_id) - if table: - print(tabulate(table, headers, tablefmt='simple', stralign='right')) + 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): + 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. @@ -167,9 +185,9 @@ class DropStat(object): 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) ] + 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): """ @@ -187,22 +205,15 @@ class DropStat(object): return headers - 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. - """ - - def get_counts(counters, oid): + 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: - table_id = COUNTER_TABLE_PREFIX + oid counter_data = self.db.get(self.db.COUNTERS_DB, table_id, counter) if counter_data is None: counts[counter] = 0 @@ -210,6 +221,13 @@ class DropStat(object): 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() @@ -217,9 +235,17 @@ class DropStat(object): return current_stat_dict for obj in natsorted(counter_object_name_map): - current_stat_dict[obj] = get_counts(counters, counter_object_name_map[obj]) + 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 @@ -244,7 +270,7 @@ class DropStat(object): 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() } + 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 @@ -262,7 +288,7 @@ class DropStat(object): if not counters: return configured_counters - return [ ctr for ctr in counters.itervalues() ] + return [ctr for ctr in counters.itervalues()] def get_counter_name(self, object_stat_map, counter_stat): """ @@ -343,7 +369,7 @@ class DropStat(object): 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 + 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: @@ -353,11 +379,12 @@ class DropStat(object): else: return PORT_STATE_NA + def main(): - parser = argparse.ArgumentParser(description='Display drop counters', - version='1.0.0', - formatter_class=argparse.RawTextHelpFormatter, - epilog=""" + parser = argparse.ArgumentParser(description='Display drop counters', + version='1.0.0', + formatter_class=argparse.RawTextHelpFormatter, + epilog=""" Examples: dropstat """) @@ -373,7 +400,7 @@ Examples: command = args.command - group = args.group + group = args.group counter_type = args.type # Create the directory to hold clear results diff --git a/show/main.py b/show/main.py index c767c5fd2e..2a2ca36d6c 100755 --- a/show/main.py +++ b/show/main.py @@ -2064,17 +2064,19 @@ def table(table_name, verbose): run_command(cmd, display_cmd=verbose) + # -# 'drops' group ### +# 'dropcounter' group ### # @cli.group(cls=AliasedGroup, default_if_no_args=False) -def drops(): +def dropcounter(): """Show drop counter related information""" pass -# 'configuration' subcommand ("show drops configuration") -@drops.command() + +# 'configuration' subcommand ("show dropcounter configuration") +@dropcounter.command() @click.option('-g', '--group', required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") def configuration(group, verbose): @@ -2086,8 +2088,9 @@ def configuration(group, verbose): run_command(cmd, display_cmd=verbose) -# 'capabilities' subcommand ("show drops capabilities") -@drops.command() + +# 'capabilities' subcommand ("show dropcounter capabilities") +@dropcounter.command() @click.option('--verbose', is_flag=True, help="Enable verbose output") def capabilities(verbose): """Show device drop counter capabilities""" @@ -2095,8 +2098,9 @@ def capabilities(verbose): run_command(cmd, display_cmd=verbose) -# 'counts' subcommand ("show drops counts") -@drops.command() + +# 'counts' subcommand ("show dropcounter counts") +@dropcounter.command() @click.option('-g', '--group', required=False) @click.option('-t', '--counter_type', required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") @@ -2112,6 +2116,7 @@ def counts(group, counter_type, verbose): 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 0000000000..b8ee6d8b9b --- /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["dropcounter"].commands["capabilities"], []) + print(result.output) + assert result.output == expected_counter_capabilities + + def test_show_configuration(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounter"].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["dropcounter"].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["dropcounter"].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["dropcounter"].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["dropcounter"].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["dropcounter"].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 0000000000..1a769b82b5 --- /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 97ae4b2edb..42ba9b5c61 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 b86c9d9d8d..c475841193 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 4d79034d7a..5a67337e6d 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 67b0567234..f3fdf3ec24 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" } } - From eb432e18fe429d67c155fbc145e1cbb7dba06f01 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Tue, 5 Nov 2019 10:03:40 -0800 Subject: [PATCH 06/12] Update test to match command --- sonic-utilities-tests/drops_group_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonic-utilities-tests/drops_group_test.py b/sonic-utilities-tests/drops_group_test.py index b8ee6d8b9b..edc5af4fea 100644 --- a/sonic-utilities-tests/drops_group_test.py +++ b/sonic-utilities-tests/drops_group_test.py @@ -123,7 +123,7 @@ def test_show_counts_with_type(self): def test_show_counts_with_clear(self): runner = CliRunner() - runner.invoke(clear.cli.commands["dropcounters"]) + runner.invoke(clear.cli.commands["dropcounter"]) result = runner.invoke(show.cli.commands["dropcounter"].commands["counts"], []) print(result.output) assert result.output == expected_counts_with_clear From 32c4f6e53e3eb26ef317e0c86d161d4b2352ef8a Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Tue, 5 Nov 2019 10:25:23 -0800 Subject: [PATCH 07/12] Add reference for show commands --- doc/Command-Reference.md | 110 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index b8e98af88f..a4d1bfaccb 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,112 @@ 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 dropcounter 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 dropcounter capabilities + ``` + +- Examples: + ``` + admin@sonic:~$ show dropcounter capabilities + Counter Type Total + -------------------- ------- + PORT_INGRESS_DROPS 3 + PORT_EGRESS_DROPS 0 + SWITCH_EGRESS_DROPS 2 + SWITCH_INGRESS_DROPS 0 + + 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 dropcounter configuration** + +This command is used to show the current running configuration of the drop counters on this device. + +- Usage: + ``` + show dropcounter configuration [-g ] + ``` + +- Examples: + ``` + admin@sonic:~$ show dropcounter 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 dropcounter 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 dropcounter counts** + +This command is used to show the current statistics for the configured drop counters. Standard drop counters are displayed as well for convenience. + +- Usage: + ``` + show dropcounter counts [-g ] [-t ] + ``` + +- Example: + ``` + admin@sonic:~$ show dropcounter 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 dropcounter 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 dropcounter counts -t SWITCH_EGRESS_DROPS + DEVICE TX_LEGIT + ------ -------- + sonic 1000 + ``` + +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. From a87d83d1777c28c11635e9ac8b246eeaa63f5b60 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Tue, 5 Nov 2019 11:42:16 -0800 Subject: [PATCH 08/12] Finish command reference --- config/main.py | 4 +- doc/Command-Reference.md | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/config/main.py b/config/main.py index 8b9723b6f6..b1ea087cb2 100755 --- a/config/main.py +++ b/config/main.py @@ -1639,7 +1639,7 @@ def delete_counter(counter_name, verbose): # -# 'add_reason' subcommand ('config dropcounter add_reason') +# 'add_reasons' subcommand ('config dropcounter add_reasons') # @dropcounter.command() @click.argument("counter_name", type=str, required=True) @@ -1652,7 +1652,7 @@ def add_reasons(counter_name, reasons, verbose): # -# 'remove_reason' subcommand ('config dropcounter remove_reason') +# 'remove_reasons' subcommand ('config dropcounter remove_reasons') # @dropcounter.command() @click.argument("counter_name", type=str, required=True) diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index a4d1bfaccb..cac3bc104b 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -1815,6 +1815,8 @@ This command is used to show the current running configuration of the drop count 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 dropcounter counts [-g ] [-t ] @@ -1846,6 +1848,85 @@ This command is used to show the current statistics for the configured drop coun sonic 1000 ``` +### Drop Counters config commands + +**config dropcounter initialize_counter** + +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 dropcounter initialize_counter [-d ] [-g ] [-a ] + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounter initialize_counter DEBUG_2 PORT_INGRESS_DROPS [EXCEEDS_L2_MTU,DECAP_ERROR] -d "More port ingress drops" -g BAD -a BAD_DROPS + ``` + +**config dropcounter 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 dropcounter add_reasons + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounter add_reasons DEBUG_2 [SIP_CLASS_E] + ``` + +**config dropcounter remove_reasons** + +This command is used to remove drop reasons from an already initialized counter. + +- Usage: + ``` + admin@sonic:~$ sudo config dropcounter remove_reasons + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounter remove_reasons DEBUG_2 [SIP_CLASS_E] + ``` + +**config dropcounter delete_counter** + +This command is used to delete a drop counter. + +- Usage: + ``` + admin@sonic:~$ sudo config dropcounter delete_counter + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dropcounter delete_counter DEBUG_2 + ``` + +### Drop Counters clear commands + +**sonic-clear dropcounter** + +This comnmand is used to clear drop counters. This is done on a per-user basis. + +- Usage: + ``` + admin@sonic:~$ sonic-clear dropcounter + ``` + +- Example: + ``` + admin@sonic:~$ sonic-clear dropcounter + Cleared drop counters + ``` + Go Back To [Beginning of the document](#) or [Beginning of this section](#drop-counters) From f87ea25990d0bcfeccb05cb29208f2834fb4f777 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Thu, 14 Nov 2019 16:17:09 -0800 Subject: [PATCH 09/12] Fix error messages --- scripts/dropconfig | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/scripts/dropconfig b/scripts/dropconfig index 9d4f31cd9b..1948cd7bca 100755 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -50,8 +50,8 @@ out_drop_reason_prefix = 'SAI_OUT_DROP_REASON_' class InvalidArgumentError(RuntimeError): - def __init__(self, arg): - self.args = arg + def __init__(self, msg): + self.message = msg class DropConfig(object): @@ -128,23 +128,19 @@ class DropConfig(object): raise InvalidArgumentError('No drop reasons provided') if self.counter_name_in_use(counter_name): - raise InvalidArgumentError('Counter name \'{}\' already in use' - .format(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') + raise InvalidArgumentError('Counter type not supported on this device') elif int(available_counters) == 0: - raise InvalidArgumentError('All counters of this type are \ - currently in use') + 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') + 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( @@ -174,8 +170,7 @@ class DropConfig(object): raise InvalidArgumentError('No counter name provided') if not self.counter_name_in_use(counter_name): - raise InvalidArgumentError('Counter \'{}\' not found' - .format(counter_name)) + raise InvalidArgumentError('Counter \'{}\' not found'.format(counter_name)) self.config_db.set_entry(DEBUG_COUNTER_CONFIG_TABLE, counter_name, @@ -195,15 +190,13 @@ class DropConfig(object): raise InvalidArgumentError('No drop reasons provided') if not self.counter_name_in_use(counter_name): - raise InvalidArgumentError('Counter \'{}\' not found' - .format(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') + 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( @@ -223,8 +216,7 @@ class DropConfig(object): raise InvalidArgumentError('No drop reasons provided') if not self.counter_name_in_use(counter_name): - raise InvalidArgumentError('Counter \'{}\' not found' - .format(counter_name)) + raise InvalidArgumentError('Counter \'{}\' not found'.format(counter_name)) for reason in reasons: self.config_db.set_entry(self.config_db.serialize_key( @@ -391,27 +383,25 @@ Examples: description, reasons) except InvalidArgumentError as err: - print('Encountered error trying to install counter: {}' - .format(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)) + 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)) + 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)) + print('Encountered error trying to remove reasons: {}'.format(err.message)) exit(1) elif command == 'show_config': dconfig.print_counter_config(group) From 93ff214008f6fe4c8ff6af35bf73cbd6e5f30891 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Thu, 14 Nov 2019 17:02:17 -0800 Subject: [PATCH 10/12] Update command name --- clear/main.py | 2 +- config/main.py | 20 ++++---- doc/Command-Reference.md | 56 +++++++++++------------ show/main.py | 16 +++---- sonic-utilities-tests/drops_group_test.py | 16 +++---- 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/clear/main.py b/clear/main.py index 60f733997d..f6e1d8c477 100755 --- a/clear/main.py +++ b/clear/main.py @@ -195,7 +195,7 @@ def pfccounters(): run_command(command) @cli.command() -def dropcounter(): +def dropcounters(): """Clear drop counters""" command = "dropstat -c clear" run_command(command) diff --git a/config/main.py b/config/main.py index b1ea087cb2..ca9df8285d 100755 --- a/config/main.py +++ b/config/main.py @@ -1593,19 +1593,19 @@ def incremental(file_name): # -# 'dropcounter' group ('config dropcounter ...') +# 'dropcounters' group ('config dropcounters ...') # @config.group() -def dropcounter(): +def dropcounters(): """Drop counter related configuration tasks""" pass # -# 'initialize_counter' subcommand ('config dropcounter initialize_counter') +# 'initialize_counter' subcommand ('config dropcounters initialize_counter') # -@dropcounter.command() +@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) @@ -1627,9 +1627,9 @@ def initialize_counter(counter_name, alias, group, counter_type, desc, reasons, # -# 'delete_counter' subcommand ('config dropcounter delete_counter') +# 'delete_counter' subcommand ('config dropcounters delete_counter') # -@dropcounter.command() +@dropcounters.command() @click.argument("counter_name", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") def delete_counter(counter_name, verbose): @@ -1639,9 +1639,9 @@ def delete_counter(counter_name, verbose): # -# 'add_reasons' subcommand ('config dropcounter add_reasons') +# 'add_reasons' subcommand ('config dropcounters add_reasons') # -@dropcounter.command() +@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") @@ -1652,9 +1652,9 @@ def add_reasons(counter_name, reasons, verbose): # -# 'remove_reasons' subcommand ('config dropcounter remove_reasons') +# 'remove_reasons' subcommand ('config dropcounters remove_reasons') # -@dropcounter.command() +@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") diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index cac3bc104b..fce1a8b1d6 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -1749,24 +1749,22 @@ This section explains all the Configurable Drop Counters show commands and confi ### Drop Counters show commands -**show dropcounter capabilities** +**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 dropcounter capabilities + show dropcounters capabilities ``` - Examples: ``` - admin@sonic:~$ show dropcounter capabilities + admin@sonic:~$ show dropcounters capabilities Counter Type Total -------------------- ------- PORT_INGRESS_DROPS 3 - PORT_EGRESS_DROPS 0 SWITCH_EGRESS_DROPS 2 - SWITCH_INGRESS_DROPS 0 PORT_INGRESS_DROPS: L2_ANY @@ -1786,32 +1784,32 @@ This command is used to show the drop counter capabilities that are available on A_CUSTOM_REASON ``` -**show dropcounter configuration** +**show dropcounters configuration** This command is used to show the current running configuration of the drop counters on this device. - Usage: ``` - show dropcounter configuration [-g ] + show dropcounters configuration [-g ] ``` - Examples: ``` - admin@sonic:~$ show dropcounter configuration + 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 dropcounter configuration -g LEGIT + 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 dropcounter counts** +**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. @@ -1819,12 +1817,12 @@ Because clear (see below) is handled on a per-user basis different users may see - Usage: ``` - show dropcounter counts [-g ] [-t ] + show dropcounters counts [-g ] [-t ] ``` - Example: ``` - admin@sonic:~$ show dropcounter counts + admin@sonic:~$ show dropcounters counts IFACE STATE RX_ERR RX_DROPS TX_ERR TX_DROPS RX_LEGIT --------- ------- -------- ---------- -------- ---------- --------- Ethernet0 U 10 100 0 0 20 @@ -1835,14 +1833,14 @@ Because clear (see below) is handled on a per-user basis different users may see ------ -------- sonic 1000 - admin@sonic:~$ show dropcounter counts -g LEGIT + 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 dropcounter counts -t SWITCH_EGRESS_DROPS + admin@sonic:~$ show dropcounters counts -t SWITCH_EGRESS_DROPS DEVICE TX_LEGIT ------ -------- sonic 1000 @@ -1850,7 +1848,7 @@ Because clear (see below) is handled on a per-user basis different users may see ### Drop Counters config commands -**config dropcounter initialize_counter** +**config dropcounters initialize_counter** This command is used to initialize a new drop counter. The user must specify a name, type, and initial list of drop reasons. @@ -1858,15 +1856,15 @@ This command will fail if the given name is already in use, if the type of count - Usage: ``` - admin@sonic:~$ sudo config dropcounter initialize_counter [-d ] [-g ] [-a ] + admin@sonic:~$ sudo config dropcounters initialize_counter [-d ] [-g ] [-a ] ``` - Example: ``` - admin@sonic:~$ sudo config dropcounter initialize_counter DEBUG_2 PORT_INGRESS_DROPS [EXCEEDS_L2_MTU,DECAP_ERROR] -d "More port ingress drops" -g BAD -a BAD_DROPS + admin@sonic:~$ sudo config dropcounters initialize_counter DEBUG_2 PORT_INGRESS_DROPS [EXCEEDS_L2_MTU,DECAP_ERROR] -d "More port ingress drops" -g BAD -a BAD_DROPS ``` -**config dropcounter add_reasons** +**config dropcounters add_reasons** This command is used to add drop reasons to an already initialized counter. @@ -1874,56 +1872,56 @@ This command will fail if any of the specified drop reasons are not supported. - Usage: ``` - admin@sonic:~$ sudo config dropcounter add_reasons + admin@sonic:~$ sudo config dropcounters add_reasons ``` - Example: ``` - admin@sonic:~$ sudo config dropcounter add_reasons DEBUG_2 [SIP_CLASS_E] + admin@sonic:~$ sudo config dropcounters add_reasons DEBUG_2 [SIP_CLASS_E] ``` -**config dropcounter remove_reasons** +**config dropcounters remove_reasons** This command is used to remove drop reasons from an already initialized counter. - Usage: ``` - admin@sonic:~$ sudo config dropcounter remove_reasons + admin@sonic:~$ sudo config dropcounters remove_reasons ``` - Example: ``` - admin@sonic:~$ sudo config dropcounter remove_reasons DEBUG_2 [SIP_CLASS_E] + admin@sonic:~$ sudo config dropcounters remove_reasons DEBUG_2 [SIP_CLASS_E] ``` -**config dropcounter delete_counter** +**config dropcounters delete_counter** This command is used to delete a drop counter. - Usage: ``` - admin@sonic:~$ sudo config dropcounter delete_counter + admin@sonic:~$ sudo config dropcounters delete_counter ``` - Example: ``` - admin@sonic:~$ sudo config dropcounter delete_counter DEBUG_2 + admin@sonic:~$ sudo config dropcounters delete_counter DEBUG_2 ``` ### Drop Counters clear commands -**sonic-clear dropcounter** +**sonic-clear dropcounters** This comnmand is used to clear drop counters. This is done on a per-user basis. - Usage: ``` - admin@sonic:~$ sonic-clear dropcounter + admin@sonic:~$ sonic-clear dropcounters ``` - Example: ``` - admin@sonic:~$ sonic-clear dropcounter + admin@sonic:~$ sonic-clear dropcounters Cleared drop counters ``` diff --git a/show/main.py b/show/main.py index 2a2ca36d6c..80a0681d40 100755 --- a/show/main.py +++ b/show/main.py @@ -2066,17 +2066,17 @@ def table(table_name, verbose): # -# 'dropcounter' group ### +# 'dropcounters' group ### # @cli.group(cls=AliasedGroup, default_if_no_args=False) -def dropcounter(): +def dropcounters(): """Show drop counter related information""" pass -# 'configuration' subcommand ("show dropcounter configuration") -@dropcounter.command() +# '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): @@ -2089,8 +2089,8 @@ def configuration(group, verbose): run_command(cmd, display_cmd=verbose) -# 'capabilities' subcommand ("show dropcounter capabilities") -@dropcounter.command() +# '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""" @@ -2099,8 +2099,8 @@ def capabilities(verbose): run_command(cmd, display_cmd=verbose) -# 'counts' subcommand ("show dropcounter counts") -@dropcounter.command() +# '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") diff --git a/sonic-utilities-tests/drops_group_test.py b/sonic-utilities-tests/drops_group_test.py index edc5af4fea..6a9e71099c 100644 --- a/sonic-utilities-tests/drops_group_test.py +++ b/sonic-utilities-tests/drops_group_test.py @@ -87,44 +87,44 @@ def setup_class(cls): def test_show_capabilities(self): runner = CliRunner() - result = runner.invoke(show.cli.commands["dropcounter"].commands["capabilities"], []) + 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["dropcounter"].commands["configuration"], []) + 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["dropcounter"].commands["configuration"], ["-g", "PACKET_DROPS"]) + 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["dropcounter"].commands["counts"], []) + 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["dropcounter"].commands["counts"], ["-g", "PACKET_DROPS"]) + 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["dropcounter"].commands["counts"], ["-t", "PORT_INGRESS_DROPS"]) + 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["dropcounter"]) - result = runner.invoke(show.cli.commands["dropcounter"].commands["counts"], []) + 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 From 315bff9bd1f71137116e4f4fce68f1c8f7797570 Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Thu, 14 Nov 2019 17:06:19 -0800 Subject: [PATCH 11/12] Make subcommands less verbose --- config/main.py | 10 +++++----- doc/Command-Reference.md | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/main.py b/config/main.py index ca9df8285d..bb0216e796 100755 --- a/config/main.py +++ b/config/main.py @@ -1603,7 +1603,7 @@ def dropcounters(): # -# 'initialize_counter' subcommand ('config dropcounters initialize_counter') +# 'install' subcommand ('config dropcounters install') # @dropcounters.command() @click.argument("counter_name", type=str, required=True) @@ -1613,8 +1613,8 @@ def dropcounters(): @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 initialize_counter(counter_name, alias, group, counter_type, desc, reasons, verbose): - """Initialize a new drop counter""" +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) @@ -1627,12 +1627,12 @@ def initialize_counter(counter_name, alias, group, counter_type, desc, reasons, # -# 'delete_counter' subcommand ('config dropcounters delete_counter') +# '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(counter_name, verbose): +def delete(counter_name, verbose): """Delete an existing drop counter""" command = "dropconfig -c uninstall -n {}".format(counter_name) run_command(command, display_cmd=verbose) diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index fce1a8b1d6..ccba9c6112 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -1848,7 +1848,7 @@ Because clear (see below) is handled on a per-user basis different users may see ### Drop Counters config commands -**config dropcounters initialize_counter** +**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. @@ -1856,12 +1856,12 @@ This command will fail if the given name is already in use, if the type of count - Usage: ``` - admin@sonic:~$ sudo config dropcounters initialize_counter [-d ] [-g ] [-a ] + admin@sonic:~$ sudo config dropcounters install [-d ] [-g ] [-a ] ``` - Example: ``` - admin@sonic:~$ sudo config dropcounters initialize_counter DEBUG_2 PORT_INGRESS_DROPS [EXCEEDS_L2_MTU,DECAP_ERROR] -d "More port ingress drops" -g BAD -a BAD_DROPS + 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** @@ -1894,18 +1894,18 @@ This command is used to remove drop reasons from an already initialized counter. admin@sonic:~$ sudo config dropcounters remove_reasons DEBUG_2 [SIP_CLASS_E] ``` -**config dropcounters delete_counter** +**config dropcounters delete** This command is used to delete a drop counter. - Usage: ``` - admin@sonic:~$ sudo config dropcounters delete_counter + admin@sonic:~$ sudo config dropcounters delete ``` - Example: ``` - admin@sonic:~$ sudo config dropcounters delete_counter DEBUG_2 + admin@sonic:~$ sudo config dropcounters delete DEBUG_2 ``` ### Drop Counters clear commands From 299f02effb6c3be21949d434742f1d981074799f Mon Sep 17 00:00:00 2001 From: Danny Allen Date: Mon, 18 Nov 2019 15:45:56 -0800 Subject: [PATCH 12/12] Add safety check for number of counters in use --- scripts/dropconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dropconfig b/scripts/dropconfig index 1948cd7bca..2ff8fab236 100755 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -133,7 +133,7 @@ class DropConfig(object): 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) == 0: + 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)