diff --git a/config/main.py b/config/main.py index e7b93fca7c..1635b9553d 100644 --- a/config/main.py +++ b/config/main.py @@ -7369,7 +7369,15 @@ 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 install(counter_name, alias, group, counter_type, desc, reasons, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def install(counter_name, alias, group, counter_type, desc, reasons, verbose, namespace): """Install a new drop counter""" command = ['dropconfig', '-c', 'install', '-n', str(counter_name), '-t', str(counter_type), '-r', str(reasons)] if alias: @@ -7378,6 +7386,8 @@ def install(counter_name, alias, group, counter_type, desc, reasons, verbose): command += ['-g', str(group)] if desc: command += ['-d', str(desc)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -7388,9 +7398,19 @@ def install(counter_name, alias, group, counter_type, desc, reasons, verbose): @dropcounters.command() @click.argument("counter_name", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def delete(counter_name, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def delete(counter_name, verbose, namespace): """Delete an existing drop counter""" command = ['dropconfig', '-c', 'uninstall', '-n', str(counter_name)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -7401,9 +7421,19 @@ def delete(counter_name, verbose): @click.argument("counter_name", type=str, required=True) @click.argument("reasons", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def add_reasons(counter_name, reasons, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def add_reasons(counter_name, reasons, verbose, namespace): """Add reasons to an existing drop counter""" command = ['dropconfig', '-c', 'add', '-n', str(counter_name), '-r', str(reasons)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -7414,9 +7444,19 @@ def add_reasons(counter_name, reasons, verbose): @click.argument("counter_name", type=str, required=True) @click.argument("reasons", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def remove_reasons(counter_name, reasons, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def remove_reasons(counter_name, reasons, verbose, namespace): """Remove reasons from an existing drop counter""" command = ['dropconfig', '-c', 'remove', '-n', str(counter_name), '-r', str(reasons)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) diff --git a/scripts/dropconfig b/scripts/dropconfig index 1fc812a474..79f4b2f906 100755 --- a/scripts/dropconfig +++ b/scripts/dropconfig @@ -13,17 +13,24 @@ import argparse import os import sys +from utilities_common import constants +from sonic_py_common import multi_asic +from utilities_common import multi_asic as multi_asic_util from tabulate import tabulate # mock the redis for unit test purposes # try: - if os.environ["UTILITIES_UNIT_TESTING"] == "1": + if os.getenv("UTILITIES_UNIT_TESTING") == "1": modules_path = os.path.join(os.path.dirname(__file__), "..") test_path = os.path.join(modules_path, "tests") sys.path.insert(0, modules_path) sys.path.insert(0, test_path) import mock_tables.dbconnector + if os.getenv("UTILITIES_UNIT_TESTING_TOPOLOGY") == "multi_asic": + import tests.mock_tables.mock_multi_asic + mock_tables.dbconnector.load_namespace_config() + except KeyError: pass @@ -51,14 +58,11 @@ class InvalidArgumentError(RuntimeError): def __init__(self, msg): self.message = msg - class DropConfig(object): - def __init__(self): - self.config_db = ConfigDBConnector() - self.config_db.connect() - - self.state_db = SonicV2Connector(use_unix_socket_path=False) - self.state_db.connect(self.state_db.STATE_DB) + def __init__(self, namespace, db, config_db): + self.db = db + self.config_db = config_db + self.namespace = namespace # -c show_config def print_counter_config(self, group): @@ -76,6 +80,9 @@ class DropConfig(object): counter.get('reason', ''), counter.get('description', ''))) + if multi_asic.is_multi_asic(): + print("For namespace:", self.namespace) + print(tabulate(table, drop_counter_config_header, tablefmt='simple', @@ -95,6 +102,10 @@ class DropConfig(object): table = [] for counter, capabilities in device_caps.items(): table.append((counter, capabilities.get('count', 'N/A'))) + + if multi_asic.is_multi_asic(): + print("For namespace:", self.namespace) + print(tabulate(table, drop_counter_capability_header, tablefmt='simple', @@ -266,7 +277,7 @@ class DropConfig(object): Get the device capabilities from STATE_DB """ - capability_query = self.state_db.keys(self.state_db.STATE_DB, '{}|*'.format(DEBUG_COUNTER_CAPABILITY_TABLE)) + capability_query = self.db.keys(self.db.STATE_DB, '{}|*'.format(DEBUG_COUNTER_CAPABILITY_TABLE)) if not capability_query: return None @@ -274,7 +285,7 @@ class DropConfig(object): 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) + counter_caps[counter_type[len(DEBUG_COUNTER_CAPABILITY_TABLE) + 1:]] = self.db.get_all(self.db.STATE_DB, counter_type) return counter_caps def counter_name_in_use(self, counter_name): @@ -287,7 +298,7 @@ class DropConfig(object): 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)) + cap_query = self.db.get_all(self.db.STATE_DB, '{}|{}'.format(DEBUG_COUNTER_CAPABILITY_TABLE, counter_type)) if not cap_query: return None @@ -298,7 +309,7 @@ class DropConfig(object): 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)) + cap_query = self.db.get_all(self.db.STATE_DB, '{}|{}'.format(DEBUG_COUNTER_CAPABILITY_TABLE, counter_type)) if not cap_query: return None @@ -310,6 +321,66 @@ class DropConfig(object): # get_keys will normalize the table name to uppercase. return [key for key in self.config_db.get_keys(DROP_REASON_CONFIG_TABLE) if key[0] == counter_name] +class DropConfigWrapper(object): + """A wrapper to execute dropconfig cmd over the correct namespaces""" + def __init__(self, namespace): + self.namespace = namespace + if namespace is not None and namespace not in multi_asic.get_namespace_list(): + print('Encountered error, namespace not recognized: {}. Valid namespaces {}'.format(namespace, get_namespace_list())) + sys.exit(1) + + # Initialize the multi-asic namespace + self.multi_asic = multi_asic_util.MultiAsic(constants.DISPLAY_ALL, namespace_option=namespace) + self.db = None + self.config_db = None + + @multi_asic_util.run_on_multi_asic + def run(self, + command, + name, + alias, + group, + counter_type, + description, + reasons): + + dconfig = DropConfig(self.multi_asic.current_namespace, self.db, self.config_db) + + if command == 'install': + try: + dconfig.create_counter(name, + alias, + group, + counter_type, + description, + reasons) + except InvalidArgumentError as err: + print('Encountered error trying to install counter: {}'.format(err.message)) + sys.exit(1) + elif command == 'uninstall': + try: + dconfig.delete_counter(name) + except InvalidArgumentError as err: + print('Encountered error trying to uninstall counter: {}'.format(err.message)) + sys.exit(1) + elif command == 'add': + try: + dconfig.add_reasons(name, reasons) + except InvalidArgumentError as err: + print('Encountered error trying to add reasons: {}'.format(err.message)) + sys.exit(1) + elif command == 'remove': + try: + dconfig.remove_reasons(name, reasons) + except InvalidArgumentError as err: + print('Encountered error trying to remove reasons: {}'.format(err.message)) + sys.exit(1) + elif command == 'show_config': + dconfig.print_counter_config(group) + elif command == 'show_capabilities': + dconfig.print_device_capabilities() + else: + print("Command not recognized") def deserialize_reason_list(list_str): if list_str is None: @@ -334,6 +405,7 @@ def main(): epilog=""" Examples: dropconfig + dropconfig -ns asic0 """) # Version @@ -349,6 +421,7 @@ 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) + parser.add_argument('-ns', '--namespace', type=str, help='Perform operation on a specific namespace or skip for all', default=None) args = parser.parse_args() @@ -360,46 +433,19 @@ Examples: counter_type = args.type description = args.desc drop_reasons = args.reasons + namespace = args.namespace reasons = deserialize_reason_list(drop_reasons) - dconfig = DropConfig() - - if command == 'install': - try: - dconfig.create_counter(name, - alias, - group, - counter_type, - description, - reasons) - except InvalidArgumentError as err: - print('Encountered error trying to install counter: {}'.format(err.message)) - sys.exit(1) - elif command == 'uninstall': - try: - dconfig.delete_counter(name) - except InvalidArgumentError as err: - print('Encountered error trying to uninstall counter: {}'.format(err.message)) - sys.exit(1) - elif command == 'add': - try: - dconfig.add_reasons(name, reasons) - except InvalidArgumentError as err: - print('Encountered error trying to add reasons: {}'.format(err.message)) - sys.exit(1) - elif command == 'remove': - try: - dconfig.remove_reasons(name, reasons) - except InvalidArgumentError as err: - print('Encountered error trying to remove reasons: {}'.format(err.message)) - sys.exit(1) - elif command == 'show_config': - dconfig.print_counter_config(group) - elif command == 'show_capabilities': - dconfig.print_device_capabilities() - else: - print("Command not recognized") + dropconfig_wrapper = DropConfigWrapper(namespace) + dropconfig_wrapper.run(command, + name, + alias, + group, + counter_type, + description, + reasons) + if __name__ == '__main__': main() diff --git a/show/dropcounters.py b/show/dropcounters.py index 9bb988fc5b..28e1fb897d 100644 --- a/show/dropcounters.py +++ b/show/dropcounters.py @@ -17,23 +17,45 @@ def dropcounters(): @dropcounters.command() @click.option('-g', '--group', required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") -def configuration(group, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def configuration(group, verbose, namespace): """Show current drop counter configuration""" cmd = ['dropconfig', '-c', 'show_config'] if group: cmd += ['-g', str(group)] + if namespace: + cmd += ['-ns', str(namespace)] + clicommon.run_command(cmd, display_cmd=verbose) # 'capabilities' subcommand ("show dropcounters capabilities") @dropcounters.command() @click.option('--verbose', is_flag=True, help="Enable verbose output") -def capabilities(verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def capabilities(verbose, namespace): """Show device drop counter capabilities""" cmd = ['dropconfig', '-c', 'show_capabilities'] + if namespace: + cmd += ['-ns', str(namespace)] + clicommon.run_command(cmd, display_cmd=verbose) diff --git a/tests/config_test.py b/tests/config_test.py index 627c0ce833..863ae485b4 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -3182,6 +3182,81 @@ def test_remove_reasons(self, mock_run_command): def teardown(self): print("TEARDOWN") +class TestConfigDropcountersMasic(object): + def setup(self): + print("SETUP") + os.environ['UTILITIES_UNIT_TESTING'] = "1" + os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "multi_asic" + import config.main + importlib.reload(config.main) + # change to multi asic config + from .mock_tables import dbconnector + from .mock_tables import mock_multi_asic + importlib.reload(mock_multi_asic) + dbconnector.load_namespace_config() + + @patch('utilities_common.cli.run_command') + def test_install_multi_asic(self, mock_run_command): + counter_name = 'DEBUG_2' + counter_type = 'PORT_INGRESS_DROPS' + reasons = '[EXCEEDS_L2_MTU,DECAP_ERROR]' + alias = 'BAD_DROPS' + group = 'BAD' + desc = 'more port ingress drops' + namespace = 'asic0' + + runner = CliRunner() + result = runner.invoke(config.config.commands['dropcounters'].commands['install'], [counter_name, counter_type, reasons, '-d', desc, '-g', group, '-a', alias, '-n', namespace]) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + mock_run_command.assert_called_once_with(['dropconfig', '-c', 'install', '-n', str(counter_name), '-t', str(counter_type), '-r', str(reasons), '-a', str(alias), '-g', str(group), '-d', str(desc), '-ns', str(namespace)], display_cmd=False) + + @patch('utilities_common.cli.run_command') + def test_delete_multi_asic(self, mock_run_command): + counter_name = 'DEBUG_2' + namespace = 'asic0' + runner = CliRunner() + result = runner.invoke(config.config.commands['dropcounters'].commands['delete'], [counter_name, '-v', '-n', namespace]) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + mock_run_command.assert_called_once_with(['dropconfig', '-c', 'uninstall', '-n', str(counter_name), '-ns', namespace], display_cmd=True) + + @patch('utilities_common.cli.run_command') + def test_add_reasons_multi_asic(self, mock_run_command): + counter_name = 'DEBUG_2' + reasons = '[EXCEEDS_L2_MTU,DECAP_ERROR]' + namespace = 'asic0' + runner = CliRunner() + result = runner.invoke(config.config.commands['dropcounters'].commands['add-reasons'], [counter_name, reasons, '-v', '-n', namespace]) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + mock_run_command.assert_called_once_with(['dropconfig', '-c', 'add', '-n', str(counter_name), '-r', str(reasons), '-ns', namespace], display_cmd=True) + + @patch('utilities_common.cli.run_command') + def test_remove_reasons_multi_asic(self, mock_run_command): + counter_name = 'DEBUG_2' + reasons = '[EXCEEDS_L2_MTU,DECAP_ERROR]' + namespace = 'asic0' + runner = CliRunner() + result = runner.invoke(config.config.commands['dropcounters'].commands['remove-reasons'], [counter_name, reasons, '-v', '-n', namespace]) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + mock_run_command.assert_called_once_with(['dropconfig', '-c', 'remove', '-n', str(counter_name), '-r', str(reasons), '-ns', namespace], display_cmd=True) + + @classmethod + def teardown_class(cls): + print("TEARDOWN") + os.environ['UTILITIES_UNIT_TESTING'] = "0" + os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "" + # change back to single asic config + from .mock_tables import dbconnector + from .mock_tables import mock_single_asic + importlib.reload(mock_single_asic) + dbconnector.load_namespace_config() class TestConfigWatermarkTelemetry(object): def setup(self): diff --git a/tests/drops_group_test.py b/tests/drops_group_test.py index 93f99e3f1b..b590b184a9 100644 --- a/tests/drops_group_test.py +++ b/tests/drops_group_test.py @@ -1,5 +1,6 @@ import os import sys +import importlib import shutil from click.testing import CliRunner @@ -168,3 +169,50 @@ 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" + + +class TestDropCountersMasic(object): + @classmethod + def setup_class(cls): + print("SETUP") + remove_tmp_dropstat_file() + os.environ["PATH"] += os.pathsep + scripts_path + os.environ['UTILITIES_UNIT_TESTING'] = "1" + os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "multi_asic" + import show.dropcounters + importlib.reload(show.dropcounters) + # change to multi asic config + from .mock_tables import dbconnector + from .mock_tables import mock_multi_asic + importlib.reload(mock_multi_asic) + dbconnector.load_namespace_config() + + def test_show_capabilities(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["capabilities"], ['-n', 'asic0']) + print(result.output) + assert result.output == "For namespace: asic0\n" + expected_counter_capabilities + + def test_show_configuration(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["configuration"], ['-n', 'asic0']) + print(result.output) + assert result.output == "For namespace: asic0\n" + expected_counter_configuration + + def test_show_configuration_with_group(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["dropcounters"].commands["configuration"], ["-g", "PACKET_DROPS", '-n', 'asic0']) + print(result.output) + assert result.output == "For namespace: asic0\n" + expected_counter_configuration_with_group + + @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" + os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "" + # change back to single asic config + from .mock_tables import dbconnector + from .mock_tables import mock_single_asic + importlib.reload(mock_single_asic) + dbconnector.load_namespace_config() diff --git a/tests/mock_tables/asic0/config_db.json b/tests/mock_tables/asic0/config_db.json index 593170630f..53c231f187 100644 --- a/tests/mock_tables/asic0/config_db.json +++ b/tests/mock_tables/asic0/config_db.json @@ -345,5 +345,26 @@ "static_th": "12121212", "pool": "ingress_lossless_pool_hbm", "size": "0" - } + }, + "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|lowercase_counter": { + "type": "SWITCH_EGRESS_DROPS" + }, + "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": {}, + "DEBUG_COUNTER_DROP_REASON|lowercase_counter|L2_ANY": {} }