diff --git a/config/main.py b/config/main.py index 6ab10f634ff..4449b9f7872 100644 --- a/config/main.py +++ b/config/main.py @@ -5422,6 +5422,148 @@ def interface_type(ctx, interface_name, interface_type_value, verbose): command += ["-vv"] clicommon.run_command(command, display_cmd=verbose) + +# +# 'damping' subgroup ('config interface damping ...') +# + +@interface.group(cls=clicommon.AbbreviationGroup) +@click.pass_context +def damping(ctx): + """Set interface damping configurations""" + pass + + +# +# 'algo' subcommand ('config interface damping algo ...') +# + +@damping.command() +@click.pass_context +@click.argument('interface_name', metavar='', required=True) +@click.argument('algo_type', metavar='', required=True, type=click.Choice(["aied", "disabled"])) +def algo(ctx, interface_name, algo_type): + """Set link event damping algorithm""" + # Get the config_db connector + config_db = ctx.obj['config_db'] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + port_dict = config_db.get_table('PORT') + if interface_name not in port_dict: + ctx.fail("Invalid port {}".format(interface_name)) + + log.log_info("Executing: interface link_event_damping_algorithm {} {}".format(interface_name, algo_type)) + + config_db.mod_entry("PORT", interface_name, {"link_event_damping_algorithm": algo_type}) + + +# +# 'aied-param' subcommand ('config interface damping aied-param ...') +# + +@damping.command() +@click.pass_context +@click.argument('interface_name', metavar='', required=True) +@click.option( + '--max-suppress-time', + required=False, + type=int, + help="Set max suppress time in ms" +) +@click.option( + '--decay-half-life', + required=False, + type=int, + help="Set decay half life in ms" +) +@click.option( + '--suppress-threshold', + required=False, + type=int, + help="Set suppress threshold" +) +@click.option( + '--reuse-threshold', + required=False, + type=int, + help="Set reuse threshold" +) +@click.option( + '--flap-penalty', + required=False, + type=int, + help="Set flap penalty" +) +def aied_param( + ctx, + interface_name, + max_suppress_time, + decay_half_life, + suppress_threshold, + reuse_threshold, + flap_penalty +): + """Set AIED link event damping configuration""" + # Get the config_db connector + config_db = ctx.obj['config_db'] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + port_dict = config_db.get_table('PORT') + if interface_name not in port_dict: + ctx.fail("Invalid port {}".format(interface_name)) + + config_set = {} + + if max_suppress_time is not None: + if max_suppress_time < 0: + ctx.fail("Invalid max_suppress_time value {}. It should be >= 0".format(max_suppress_time)) + config_set['max_suppress_time'] = max_suppress_time + if decay_half_life is not None: + if decay_half_life < 0: + ctx.fail("Invalid decay_half_life value {}. It should be >= 0".format(decay_half_life)) + config_set['decay_half_life'] = decay_half_life + if suppress_threshold is not None: + if suppress_threshold < 0: + ctx.fail("Invalid suppress_threshold value {}. It should be >= 0".format(suppress_threshold)) + config_set['suppress_threshold'] = suppress_threshold + if reuse_threshold is not None: + if reuse_threshold < 0: + ctx.fail("Invalid reuse_threshold value {}. It should be >= 0".format(reuse_threshold)) + config_set['reuse_threshold'] = reuse_threshold + if flap_penalty is not None: + if flap_penalty < 0: + ctx.fail("Invalid flap_penalty value {}. It should be >= 0".format(flap_penalty)) + config_set['flap_penalty'] = flap_penalty + + if reuse_threshold is not None and suppress_threshold is not None: + if reuse_threshold >= suppress_threshold: + ctx.fail( + "Invalid configuration: reuse_threshold ({}) must be less than suppress_threshold ({})" + .format(reuse_threshold, suppress_threshold) + ) + + if decay_half_life is not None and max_suppress_time is not None: + if decay_half_life > max_suppress_time: + ctx.fail( + "Invalid configuration: decay_half_life ({}) must be <= max_suppress_time ({})" + .format(decay_half_life, max_suppress_time) + ) + + if len(config_set) == 0: + ctx.fail("Expected at least one valid AIED config parameter") + + log.log_info("Executing: interface aied_config {}".format(interface_name)) + config_db.mod_entry("PORT", interface_name, config_set) + + # # 'advertised-interface-types' subcommand # diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 022963be128..24cfd015288 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -6931,6 +6931,7 @@ This sub-section explains the following list of configuration on the interfaces. 11) mpls - To add or remove MPLS operation for the interface 12) loopback-action - to set action for packet that ingress and gets routed on the same IP interface 13) link-training - to set interface link-training mode +14) damping - to set link event damping configuration on an interface From 201904 release onwards, the “config interface” command syntax is changed and the format is as follows: @@ -7540,6 +7541,80 @@ This command is used for setting link-training mode of a interface. admin@sonic:~$ sudo config interface link-training Ethernet0 off ``` +**config interface damping <...> (Versions >= 202311)** + +This sub-section contains the config commands that are supported for configuring link event damping on an interface. + - Link event damping algorithm. + - Link event damping configuration. + +***config interface damping algo (Versions >= 202311)*** + + This command is used to configure link event damping algorithm on an interface. + +- Usage + ``` + config interface damping algo --help + Usage: config interface damping algo [OPTIONS] + + Set link event damping algorithm + + Options: + -h, -?, --help Show this message and exit. + ``` + Currently link event damping supports `aied` (Additive Increase Exponential Decrease) algorithm, so expected algo value is either to set `aied` or disable the algorithm using `disabled` value. + +- Example + + To set `aied` algorithm: + ``` + config interface damping algo Ethernet20 aied + ``` + + To disable the link event damping algorithm: + ``` + config interface damping algo Ethernet20 disabled + ``` + +***config interface damping aied-param (Versions >= 202311*** + +This command is used to configure link event damping AIED parameters on an interface. + +- Usage + ``` + config interface damping aied-param --help + Usage: config interface damping aied-param [OPTIONS] + + Set AIED link event damping configuration + + Options: + --max-suppress-time INTEGER Set max suppress time in ms + --decay-half-life INTEGER Set decay half life in ms + --suppress-threshold INTEGER Set suppress threshold + --reuse-threshold INTEGER Set reuse threshold + --flap-penalty INTEGER Set flap penalty + -h, -?, --help Show this message and exit. + + ``` + + One or more AIED link event damping config params can be configured at a time. + +- Examples + + Set all the config parameters: + ``` + config interface damping aied-param Ethernet20 --suppress-threshold 1200 --decay-half-life 15000 --max-suppress-time 30000 --flap-penalty 1000 --reuse-threshold 1000 + ``` + + Set only flap penalty: + ``` + config interface damping aied-param Ethernet20 --flap-penalty 500 + ``` + + Set suppress threshold and reuse threshold: + ``` + config interface damping aied-param Ethernet20 --suppress-threshold 1500 --reuse-threshold 1100 + ``` + Go Back To [Beginning of the document](#) or [Beginning of this section](#interfaces) ## Interface Naming Mode diff --git a/tests/config_int_damping_test.py b/tests/config_int_damping_test.py new file mode 100644 index 00000000000..9b126c01fa2 --- /dev/null +++ b/tests/config_int_damping_test.py @@ -0,0 +1,104 @@ +import config.main as config +import operator +import os +import pytest +import sys + +from click.testing import CliRunner +from utilities_common.db import Db + +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, modules_path) + + +@pytest.fixture(scope='module') +def ctx(scope='module'): + db = Db() + obj = {'config_db': db.cfgdb, 'namespace': ''} + yield obj + + +class TestDampingConfig(object): + @classmethod + def setup_class(cls): + print("SETUP") + os.environ["UTILITIES_UNIT_TESTING"] = "1" + + def test_damping_algorithm(self, ctx): + self.basic_check("algo", ["Ethernet0", "aied"], ctx) + self.basic_check("algo", ["Ethernet0", "disabled"], ctx) + + def test_invalid_damping_algorithm(self, ctx): + self.basic_check("algo", ["Ethernet0", "invalid"], ctx, operator.ne) + result = self.basic_check("algo", ["Invalid", "aied"], ctx, op=operator.ne) + assert "Error: Invalid port" in result.output + + def test_invalid_aied_config(self, ctx): + result = self.basic_check("aied-param", ["Invalid"], ctx, op=operator.ne) + assert "Error: Invalid port" in result.output + result = self.basic_check("aied-param", ["Ethernet0"], ctx, op=operator.ne) + assert "Error: Expected at least one valid AIED config parameter" in result.output + result = self.basic_check( + "aied-param", + [ + "Ethernet0", + "--suppress-threshold", "10", + "--max-suppress-time", "10", + "--decay-half-life", "-1" + ], + ctx, + op=operator.ne) + assert "Error: Invalid decay_half_life value -1. It should be >= 0" in result.output + + def test_max_suppress_time_config(self, ctx): + result = self.basic_check("aied-param", ["Ethernet0", "--max-suppress-time", "-1"], ctx, op=operator.ne) + assert "Error: Invalid max_suppress_time value" in result.output + self.basic_check("aied-param", ["Ethernet0", "--max-suppress-time", "50"], ctx) + + def test_decay_half_life_config(self, ctx): + result = self.basic_check("aied-param", ["Ethernet0", "--decay-half-life", "-1"], ctx, op=operator.ne) + assert "Error: Invalid decay_half_life value" in result.output + self.basic_check("aied-param", ["Ethernet0", "--decay-half-life", "50"], ctx) + + def test_suppress_threshold_config(self, ctx): + result = self.basic_check("aied-param", ["Ethernet0", "--suppress-threshold", "-1"], ctx, op=operator.ne) + assert "Error: Invalid suppress_threshold value" in result.output + self.basic_check("aied-param", ["Ethernet0", "--suppress-threshold", "50"], ctx) + + def test_reuse_threshold_config(self, ctx): + result = self.basic_check("aied-param", ["Ethernet0", "--reuse-threshold", "-1"], ctx, op=operator.ne) + assert "Error: Invalid reuse_threshold value" in result.output + self.basic_check("aied-param", ["Ethernet0", "--reuse-threshold", "50"], ctx) + + def test_flap_penalty_config(self, ctx): + result = self.basic_check("aied-param", ["Ethernet0", "--flap-penalty", "-1"], ctx, op=operator.ne) + assert "Error: Invalid flap_penalty value" in result.output + self.basic_check("aied-param", ["Ethernet0", "--flap-penalty", "50"], ctx) + + def test_all_config(self, ctx): + self.basic_check( + "aied-param", + [ + "Ethernet0", + "--decay-half-life", "15000", + "--suppress-threshold", "1600", + "--max-suppress-time", "30000", + "--flap-penalty", "1000", + "--reuse-threshold", "1200" + ], + ctx) + + def basic_check(self, command_name, para_list, ctx, op=operator.eq, expect_result=0): + runner = CliRunner() + result = runner.invoke( + config.config.commands["interface"] + .commands["damping"] + .commands[command_name], + para_list, + obj=ctx + ) + print(result.exit_code, result.output) + assert op(result.exit_code, expect_result) + return result diff --git a/tests/link_event_damping_test.py b/tests/link_event_damping_test.py new file mode 100644 index 00000000000..ee6122680a7 --- /dev/null +++ b/tests/link_event_damping_test.py @@ -0,0 +1,192 @@ +import os +from click.testing import CliRunner + +import config.main as config +from utilities_common.db import Db + + +class TestConfigInterfaceDamping(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + + @classmethod + def teardown_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + + def test_set_algo_aied(self): + """Test setting damping algorithm to AIED""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["algo"], + ["Ethernet0", "aied"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_set_algo_disabled(self): + """Test disabling damping""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["algo"], + ["Ethernet0", "disabled"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_set_algo_invalid_interface(self): + """Test invalid interface for algo""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["algo"], + ["EthernetINVALID", "aied"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0 + + def test_set_aied_params_valid(self): + """Test setting valid AIED parameters""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + [ + "Ethernet0", + "--suppress-threshold", "1400", + "--decay-half-life", "20000", + "--max-suppress-time", "40000", + "--flap-penalty", "1000", + "--reuse-threshold", "1100" + ], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_set_aied_params_single_param(self): + """Test setting only one parameter""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + ["Ethernet0", "--decay-half-life", "20000"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_aied_params_no_args(self): + """Test failure if no params provided""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + ["Ethernet0"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert "Expected at least one valid AIED config parameter" in result.output + + def test_aied_negative_value(self): + """Test invalid negative values""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + ["Ethernet0", "--flap-penalty", "-1"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert "Invalid flap_penalty" in result.output + + def test_aied_reuse_ge_suppress(self): + """Test that reuse_threshold >= suppress_threshold is rejected""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + [ + "Ethernet0", + "--reuse-threshold", "3000", + "--suppress-threshold", "2000" + ], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert "reuse_threshold" in result.output.lower() or "Reuse threshold" in result.output + + def test_aied_halflife_gt_maxsuppress(self): + """Test that decay_half_life > max_suppress_time is rejected""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + [ + "Ethernet0", + "--decay-half-life", "30000", + "--max-suppress-time", "20000" + ], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert "decay_half_life" in result.output.lower() or "Half-life" in result.output + + def test_aied_invalid_halflife(self): + """Test invalid (negative) decay_half_life""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + [ + "Ethernet0", + "--decay-half-life", "-1" + ], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert "Invalid decay_half_life" in result.output or "Invalid value" in result.output + + def test_aied_invalid_interface(self): + """Test invalid interface for aied-param""" + runner = CliRunner() + db = Db() + obj = {'config_db': db.cfgdb} + + result = runner.invoke( + config.config.commands["interface"].commands["damping"].commands["aied-param"], + ["EthernetINVALID", "--decay-half-life", "1000"], + obj=obj + ) + print(result.exit_code, result.output) + assert result.exit_code != 0