diff --git a/config/main.py b/config/main.py index bfa6dccadc..3ddb747e3e 100644 --- a/config/main.py +++ b/config/main.py @@ -5102,6 +5102,105 @@ def loopback_action(ctx, interface_name, action): table_name = get_interface_table_name(interface_name) config_db.mod_entry(table_name, interface_name, {"loopback_action": action}) +# +# 'dhcp-mitigation-rate' subgroup ('config interface dhcp-mitigation-rate ...') +# + + +@interface.group(cls=clicommon.AbbreviationGroup, name='dhcp-mitigation-rate') +@click.pass_context +def dhcp_mitigation_rate(ctx): + """Set interface DHCP rate limit attribute""" + pass + +# +# 'add' subcommand +# + + +@dhcp_mitigation_rate.command(name='add') +@click.argument('interface_name', metavar='', required=True) +@click.argument('packet_rate', metavar='', required=True, type=int) +@click.pass_context +@clicommon.pass_db +def add_dhcp_mitigation_rate(db, ctx, interface_name, packet_rate): + """Add a new DHCP mitigation rate on an interface""" + # Get the config_db connector + config_db = ValidatedConfigDBConnector(db.cfgdb) + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + + if clicommon.is_valid_port(config_db, interface_name): + pass + elif clicommon.is_valid_portchannel(config_db, interface_name): + ctx.fail("{} is a PortChannel!".format(interface_name)) + else: + ctx.fail("{} does not exist".format(interface_name)) + + if packet_rate <= 0: + ctx.fail("DHCP rate limit is not valid. \nIt must be greater than 0.") + + port_data = config_db.get_entry('PORT', interface_name) + + if 'dhcp_rate_limit' in port_data: + rate = port_data["dhcp_rate_limit"] + else: + rate = '0' + + if rate != '0': + ctx.fail("{} has DHCP rate limit configured. \nRemove it to add new DHCP rate limit.".format(interface_name)) + + try: + config_db.mod_entry('PORT', interface_name, {"dhcp_rate_limit": "{}".format(str(packet_rate))}) + except ValueError as e: + ctx.fail("{} invalid or does not exist. Error: {}".format(interface_name, e)) + +# +# 'del' subcommand +# + + +@dhcp_mitigation_rate.command(name='del') +@click.argument('interface_name', metavar='', required=True) +@click.argument('packet_rate', metavar='', required=True, type=int) +@click.pass_context +@clicommon.pass_db +def del_dhcp_mitigation_rate(db, ctx, interface_name, packet_rate): + """Delete an existing DHCP mitigation rate on an interface""" + # Get the config_db connector + config_db = ValidatedConfigDBConnector(db.cfgdb) + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + + if clicommon.is_valid_port(config_db, interface_name): + pass + elif clicommon.is_valid_portchannel(config_db, interface_name): + ctx.fail("{} is a PortChannel!".format(interface_name)) + else: + ctx.fail("{} does not exist".format(interface_name)) + + if packet_rate <= 0: + ctx.fail("DHCP rate limit is not valid. \nIt must be greater than 0.") + + port_data = config_db.get_entry('PORT', interface_name) + + if 'dhcp_rate_limit' in port_data: + rate = port_data["dhcp_rate_limit"] + else: + rate = '0' + + if rate != str(packet_rate): + ctx.fail("{} DHCP rate limit does not exist on {}.".format(packet_rate, interface_name)) + + port_data["dhcp_rate_limit"] = "0" + + try: + config_db.mod_entry('PORT', interface_name, {"dhcp_rate_limit": "0"}) + except ValueError as e: + ctx.fail("{} invalid or does not exist. Error: {}".format(interface_name, e)) + # # buffer commands and utilities # diff --git a/scripts/db_migrator.py b/scripts/db_migrator.py index 9be3ce325b..e49df9ebff 100755 --- a/scripts/db_migrator.py +++ b/scripts/db_migrator.py @@ -509,6 +509,18 @@ def migrate_config_db_port_table_for_auto_neg(self): elif value['autoneg'] == '0': self.configDB.set(self.configDB.CONFIG_DB, '{}|{}'.format(table_name, key), 'autoneg', 'off') + def migrate_config_db_port_table_for_dhcp_rate_limit(self): + port_table_name = 'PORT' + port_table = self.configDB.get_table(port_table_name) + + for p_key, p_value in port_table.items(): + if 'dhcp_rate_limit' in p_value: + self.configDB.set(self.configDB.CONFIG_DB, '{}|{}'.format(port_table_name, p_key), + 'dhcp_rate_limit', p_value['dhcp_rate_limit']) + else: + self.configDB.set(self.configDB.CONFIG_DB, '{}|{}'.format(port_table_name, p_key), + 'dhcp_rate_limit', '300') + def migrate_qos_db_fieldval_reference_remove(self, table_list, db, db_num, db_delimeter): for pair in table_list: table_name, fields_list = pair @@ -1029,6 +1041,7 @@ def version_3_0_0(self): """ log.log_info('Handling version_3_0_0') self.migrate_config_db_port_table_for_auto_neg() + self.migrate_config_db_port_table_for_dhcp_rate_limit() self.set_version('version_3_0_1') return 'version_3_0_1' diff --git a/show/interfaces/__init__.py b/show/interfaces/__init__.py index f8889e6c32..42d0282d53 100644 --- a/show/interfaces/__init__.py +++ b/show/interfaces/__init__.py @@ -22,6 +22,7 @@ # Read given JSON file def readJsonFile(fileName): + try: with open(fileName) as f: result = json.load(f) @@ -36,6 +37,7 @@ def readJsonFile(fileName): raise click.Abort() return result + def try_convert_interfacename_from_alias(ctx, interfacename): """try to convert interface name from alias""" @@ -52,11 +54,14 @@ def try_convert_interfacename_from_alias(ctx, interfacename): # # 'interfaces' group ("show interfaces ...") # + + @click.group(cls=clicommon.AliasedGroup) def interfaces(): """Show details of the network interfaces""" pass + # 'alias' subcommand ("show interfaces alias") @interfaces.command() @click.argument('interfacename', required=False) @@ -96,6 +101,7 @@ def alias(interfacename, namespace, display): click.echo(tabulate(body, header)) + @interfaces.command() @click.argument('interfacename', required=False) @multi_asic_util.multi_asic_click_options @@ -107,7 +113,7 @@ def description(interfacename, namespace, display, verbose): cmd = ['intfutil', '-c', 'description'] - #ignore the display option when interface name is passed + # ignore the display option when interface name is passed if interfacename is not None: interfacename = try_convert_interfacename_from_alias(ctx, interfacename) @@ -121,6 +127,8 @@ def description(interfacename, namespace, display, verbose): clicommon.run_command(cmd, display_cmd=verbose) # 'naming_mode' subcommand ("show interfaces naming_mode") + + @interfaces.command('naming_mode') @click.option('--verbose', is_flag=True, help="Enable verbose output") def naming_mode(verbose): @@ -128,6 +136,7 @@ def naming_mode(verbose): click.echo(clicommon.get_interface_naming_mode()) + @interfaces.command() @click.argument('interfacename', required=False) @multi_asic_util.multi_asic_click_options @@ -143,6 +152,7 @@ def status(interfacename, namespace, display, verbose): interfacename = try_convert_interfacename_from_alias(ctx, interfacename) cmd += ['-i', str(interfacename)] + else: cmd += ['-d', str(display)] @@ -151,6 +161,7 @@ def status(interfacename, namespace, display, verbose): clicommon.run_command(cmd, display_cmd=verbose) + @interfaces.command() @click.argument('interfacename', required=False) @multi_asic_util.multi_asic_click_options @@ -174,6 +185,7 @@ def tpid(interfacename, namespace, display, verbose): clicommon.run_command(cmd, display_cmd=verbose) + # # 'breakout' group ### # @@ -234,9 +246,11 @@ def breakout(ctx): platform_dict[port_name]["child port speeds"] = ",".join(speeds) # Sorted keys by name in natural sort Order for human readability + parsed = OrderedDict((k, platform_dict[k]) for k in natsorted(list(platform_dict.keys()))) click.echo(json.dumps(parsed, indent=4)) + # 'breakout current-mode' subcommand ("show interfaces breakout current-mode") @breakout.command('current-mode') @click.argument('interface', metavar='', required=False, type=str) @@ -270,6 +284,7 @@ def currrent_mode(ctx, interface): body.append([name, str(cur_brkout_tbl[name]['brkout_mode'])]) click.echo(tabulate(body, header, tablefmt="grid")) + # # 'neighbor' group ### # @@ -278,6 +293,7 @@ def neighbor(): """Show neighbor related information""" pass + # 'expected' subcommand ("show interface neighbor expected") @neighbor.command() @click.argument('interfacename', required=False) @@ -300,8 +316,8 @@ def expected(db, interfacename): if clicommon.get_interface_naming_mode() == "alias": port = clicommon.InterfaceAliasConverter().name_to_alias(port) neighbor_dict[port] = neighbor_dict.pop(temp_port) - - header = ['LocalPort', 'Neighbor', 'NeighborPort', 'NeighborLoopback', 'NeighborMgmt', 'NeighborType'] + header = ['LocalPort', 'Neighbor', 'NeighborPort', + 'NeighborLoopback', 'NeighborMgmt', 'NeighborType'] body = [] if interfacename: try: @@ -309,9 +325,12 @@ def expected(db, interfacename): body.append([interfacename, device, neighbor_dict[interfacename]['port'], - neighbor_metadata_dict[device]['lo_addr'] if 'lo_addr' in neighbor_metadata_dict[device] else 'None', - neighbor_metadata_dict[device]['mgmt_addr'] if 'mgmt_addr' in neighbor_metadata_dict[device] else 'None', - neighbor_metadata_dict[device]['type'] if 'type' in neighbor_metadata_dict[device] else 'None']) + neighbor_metadata_dict[device]['lo_addr'] if 'lo_addr' + in neighbor_metadata_dict[device] else 'None', + neighbor_metadata_dict[device]['mgmt_addr'] if 'mgmt_addr' + in neighbor_metadata_dict[device] else 'None', + neighbor_metadata_dict[device]['type'] if 'type' + in neighbor_metadata_dict[device] else 'None']) except KeyError: click.echo("No neighbor information available for interface {}".format(interfacename)) return @@ -322,14 +341,18 @@ def expected(db, interfacename): body.append([port, device, neighbor_dict[port]['port'], - neighbor_metadata_dict[device]['lo_addr'] if 'lo_addr' in neighbor_metadata_dict[device] else 'None', - neighbor_metadata_dict[device]['mgmt_addr'] if 'mgmt_addr' in neighbor_metadata_dict[device] else 'None', - neighbor_metadata_dict[device]['type'] if 'type' in neighbor_metadata_dict[device] else 'None']) + neighbor_metadata_dict[device]['lo_addr'] if 'lo_addr' + in neighbor_metadata_dict[device] else 'None', + neighbor_metadata_dict[device]['mgmt_addr'] if 'mgmt_addr' + in neighbor_metadata_dict[device] else 'None', + neighbor_metadata_dict[device]['type'] if 'type' + in neighbor_metadata_dict[device] else 'None']) except KeyError: pass click.echo(tabulate(body, header)) + @interfaces.command() @click.argument('interfacename', required=False) @click.option('--namespace', '-n', 'namespace', default=None, @@ -338,10 +361,10 @@ def expected(db, interfacename): @click.option('--display', '-d', 'display', default=None, show_default=False, type=str, help='all|frontend') @click.pass_context + def mpls(ctx, interfacename, namespace, display): """Show Interface MPLS status""" - - #Edge case: Force show frontend interfaces on single asic + # Edge case: Force show frontend interfaces on single asic if not (multi_asic.is_multi_asic()): if (display == 'frontend' or display == 'all' or display is None): display = None @@ -387,7 +410,6 @@ def mpls(ctx, interfacename, namespace, display): if ifname.startswith("PortChannel") and multi_asic.is_port_channel_internal(ifname, ns): continue - mpls_intf = appl_db.get_all(appl_db.APPL_DB, key) if 'mpls' not in mpls_intf or mpls_intf['mpls'] == 'disable': @@ -412,8 +434,10 @@ def mpls(ctx, interfacename, namespace, display): click.echo(tabulate(body, header)) + interfaces.add_command(portchannel.portchannel) + # # transceiver group (show interfaces trasceiver ...) # @@ -422,6 +446,7 @@ def transceiver(): """Show SFP Transceiver information""" pass + @transceiver.command() @click.argument('interfacename', required=False) @click.option('-d', '--dom', 'dump_dom', is_flag=True, help="Also display Digital Optical Monitoring (DOM) data") @@ -448,6 +473,7 @@ def eeprom(interfacename, dump_dom, namespace, verbose): clicommon.run_command(cmd, display_cmd=verbose) + @transceiver.command() @click.argument('interfacename', required=False) @click.option('--namespace', '-n', 'namespace', default=None, show_default=True, @@ -471,7 +497,8 @@ def pm(interfacename, namespace, verbose): clicommon.run_command(cmd, display_cmd=verbose) -@transceiver.command('status') # 'status' is the actual sub-command name under 'transceiver' command + +@transceiver.command('status') # 'status' is the actual sub-command name under 'transceiver' command @click.argument('interfacename', required=False) @click.option('--namespace', '-n', 'namespace', default=None, show_default=True, type=click.Choice(multi_asic_util.multi_asic_ns_choices()), help='Namespace name or all') @@ -494,6 +521,7 @@ def transceiver_status(interfacename, namespace, verbose): clicommon.run_command(cmd, display_cmd=verbose) + @transceiver.command() @click.argument('interfacename', required=False) @click.option('--namespace', '-n', 'namespace', default=None, show_default=True, @@ -516,6 +544,7 @@ def info(interfacename, namespace, verbose): clicommon.run_command(cmd, display_cmd=verbose) + @transceiver.command() @click.argument('interfacename', required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") @@ -533,6 +562,7 @@ def lpmode(interfacename, verbose): clicommon.run_command(cmd, display_cmd=verbose) + @transceiver.command() @click.argument('interfacename', required=False) @click.option('--namespace', '-n', 'namespace', default=None, show_default=True, @@ -614,6 +644,7 @@ def counters(ctx, verbose, period, interface, printall, namespace, display): clicommon.run_command(cmd, display_cmd=verbose) + # 'errors' subcommand ("show interfaces counters errors") @counters.command() @click.option('-p', '--period') @@ -631,6 +662,7 @@ def errors(verbose, period, namespace, display): clicommon.run_command(cmd, display_cmd=verbose) + # 'fec-stats' subcommand ("show interfaces counters errors") @counters.command('fec-stats') @click.option('-p', '--period') @@ -731,6 +763,7 @@ def rates(verbose, period, namespace, display): cmd += ['-n', str(namespace)] clicommon.run_command(cmd, display_cmd=verbose) + # 'counters' subcommand ("show interfaces counters rif") @counters.command() @click.argument('interface', metavar='', required=False, type=str) @@ -749,6 +782,7 @@ def rif(interface, period, verbose): clicommon.run_command(cmd, display_cmd=verbose) + # 'counters' subcommand ("show interfaces counters detailed") @counters.command() @click.argument('interface', metavar='', required=True, type=str) @@ -802,6 +836,7 @@ def autoneg_status(interfacename, namespace, display, verbose): clicommon.run_command(cmd, display_cmd=verbose) + # # link-training group (show interfaces link-training ...) # @@ -812,6 +847,7 @@ def link_training(): """Show interface link-training information""" pass + # 'link-training status' subcommand ("show interfaces link-training status") @link_training.command(name='status') @click.argument('interfacename', required=False) @@ -836,9 +872,12 @@ def link_training_status(interfacename, namespace, display, verbose): cmd += ['-n', str(namespace)] clicommon.run_command(cmd, display_cmd=verbose) + # # fec group (show interfaces fec ...) # + + @interfaces.group(name='fec', cls=clicommon.AliasedGroup) def fec(): """Show interface fec information""" @@ -933,10 +972,53 @@ def tablelize(keys): table = [] for key in natsorted(keys): - r = [clicommon.get_interface_name_for_display(db, key), clicommon.get_interface_switchport_mode(db, key)] + r = [clicommon.get_interface_name_for_display(db, key), + clicommon.get_interface_switchport_mode(db, key)] table.append(r) return table header = ['Interface', 'Mode'] click.echo(tabulate(tablelize(keys), header, tablefmt="simple", stralign='left')) + +# +# dhcp-mitigation-rate group (show interfaces dhcp-mitigation-rate ...) +# + + +@interfaces.command(name='dhcp-mitigation-rate') +@click.argument('interfacename', required=False) +@clicommon.pass_db +def dhcp_mitigation_rate(db, interfacename): + """Show interface dhcp-mitigation-rate information""" + + ctx = click.get_current_context() + + keys = [] + + if interfacename is None: + port_data = list(db.cfgdb.get_table('PORT').keys()) + keys = port_data + + else: + if clicommon.is_valid_port(db.cfgdb, interfacename): + pass + elif clicommon.is_valid_portchannel(db.cfgdb, interfacename): + ctx.fail("{} is a PortChannel!".format(interfacename)) + else: + ctx.fail("{} does not exist".format(interfacename)) + + keys.append(interfacename) + + def tablelize(keys): + table = [] + for key in natsorted(keys): + r = [ + clicommon.get_interface_name_for_display(db, key), + clicommon.get_interface_dhcp_mitigation_rate(db.cfgdb, key) + ] + table.append(r) + return table + + header = ['Interface', 'DHCP Mitigation Rate'] + click.echo(tabulate(tablelize(keys), header, tablefmt="simple", stralign='left')) diff --git a/tests/db_migrator_input/config_db/port-an-expected.json b/tests/db_migrator_input/config_db/port-an-expected.json index 1ef2cf4916..18ac05f304 100644 --- a/tests/db_migrator_input/config_db/port-an-expected.json +++ b/tests/db_migrator_input/config_db/port-an-expected.json @@ -9,7 +9,8 @@ "speed": "10000", "fec": "none", "autoneg": "on", - "adv_speeds": "10000" + "adv_speeds": "10000", + "dhcp_rate_limit": "100" }, "PORT|Ethernet2": { "index": "0", @@ -21,7 +22,8 @@ "pfc_asym": "off", "speed": "25000", "fec": "none", - "autoneg": "off" + "autoneg": "off", + "dhcp_rate_limit": "300" }, "PORT|Ethernet4": { "index": "1", @@ -32,7 +34,8 @@ "alias": "etp2a", "pfc_asym": "off", "speed": "50000", - "fec": "none" + "fec": "none", + "dhcp_rate_limit": "300" }, "VERSIONS|DATABASE": { "VERSION": "version_3_0_1" diff --git a/tests/db_migrator_input/config_db/port-an-input.json b/tests/db_migrator_input/config_db/port-an-input.json index 6cda388135..2504308af8 100644 --- a/tests/db_migrator_input/config_db/port-an-input.json +++ b/tests/db_migrator_input/config_db/port-an-input.json @@ -8,7 +8,8 @@ "pfc_asym": "off", "speed": "10000", "fec": "none", - "autoneg": "1" + "autoneg": "1", + "dhcp_rate_limit": "100" }, "PORT|Ethernet2": { "index": "0", diff --git a/tests/dhcp_rate_test.py b/tests/dhcp_rate_test.py new file mode 100644 index 0000000000..23b81213df --- /dev/null +++ b/tests/dhcp_rate_test.py @@ -0,0 +1,350 @@ +import os +from click.testing import CliRunner +from utilities_common.db import Db +from unittest import mock +from mock import patch + +import config.main as config +import show.main as show + +show_interfaces_dhcp_rate_limit_output = """\ +Interface DHCP Mitigation Rate +----------- ---------------------- +Ethernet0 300 +Ethernet4 300 +Ethernet8 300 +Ethernet12 300 +Ethernet16 300 +Ethernet20 300 +Ethernet24 300 +Ethernet28 300 +Ethernet32 300 +Ethernet36 300 +Ethernet40 300 +Ethernet44 300 +Ethernet48 300 +Ethernet52 300 +Ethernet56 300 +Ethernet60 300 +Ethernet64 300 +Ethernet68 300 +Ethernet72 +Ethernet76 300 +Ethernet80 300 +Ethernet84 300 +Ethernet88 300 +Ethernet92 300 +Ethernet96 300 +Ethernet100 300 +Ethernet104 300 +Ethernet108 300 +Ethernet112 300 +Ethernet116 300 +Ethernet120 300 +Ethernet124 300 +""" + +show_dhcp_rate_limit_in_alias_mode_output = """\ +Interface DHCP Mitigation Rate +----------- ---------------------- +etp1 300 +etp2 300 +etp3 300 +etp4 300 +etp5 300 +etp6 300 +etp7 300 +etp8 300 +etp9 300 +etp10 300 +etp11 300 +etp12 300 +etp13 300 +etp14 300 +etp15 300 +etp16 300 +etp17 300 +etp18 300 +etp19 +etp20 300 +etp21 300 +etp22 300 +etp23 300 +etp24 300 +etp25 300 +etp26 300 +etp27 300 +etp28 300 +etp29 300 +etp30 300 +etp31 300 +etp32 300 +""" + +show_dhcp_rate_limit_single_interface_output = """\ +Interface DHCP Mitigation Rate +----------- ---------------------- +Ethernet0 300 +""" + + +class TestDHCPRate(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + print("SETUP") + + def test_config_dhcp_rate_add_on_portchannel(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["PortChannel0001", "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: PortChannel0001 is a PortChannel!" in result.output + + def test_config_dhcp_rate_del_on_portchannel(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["PortChannel0001", "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: PortChannel0001 is a PortChannel!" in result.output + + def test_config_dhcp_rate_add_on_invalid_port(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + intf = "test_fail_case" + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + [intf, "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: {} does not exist".format(intf) in result.output + + def test_config_dhcp_rate_del_on_invalid_port(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + intf = "test_fail_case" + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + [intf, "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: {} does not exist".format(intf) in result.output + + def test_config_dhcp_rate_add_invalid_rate(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["Ethernet0", "0"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: DHCP rate limit is not valid. \nIt must be greater than 0." in result.output + + def test_config_dhcp_rate_del_invalid_rate(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["Ethernet0", "0"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: DHCP rate limit is not valid. \nIt must be greater than 0." in result.output + + def test_config_dhcp_rate_add_rate_with_exist_rate(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["Ethernet0", "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: Ethernet0 has DHCP rate limit configured. \nRemove it to add new DHCP rate limit." \ + in result.output + + def test_config_dhcp_rate_del_rate_with_nonexist_rate(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["Ethernet0", "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: 20 DHCP rate limit does not exist on Ethernet0." in result.output + + def test_config_dhcp_rate_add_del_with_no_rate(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["Ethernet72", "80"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: 80 DHCP rate limit does not exist on Ethernet72." in result.output + + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["Ethernet72", "80"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + + def test_config_dhcp_rate_add_del(self): + db = Db() + runner = CliRunner() + # Remove default DHCP rate limit from Ethernet24 + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["Ethernet24", "300"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + # Remove default DHCP rate limit from Ethernet32 + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["Ethernet32", "300"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + # Add DHCP rate limit 45 on Ethernet32 + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["Ethernet32", "45"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + + def test_config_dhcp_rate_add_del_in_alias_mode(self): + db = Db() + runner = CliRunner() + # Enable alias mode by setting environment variable + os.environ['SONIC_CLI_IFACE_MODE'] = "alias" + interface_alias = "etp1" + # Remove default 300 rate limit from the interface using alias + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + [interface_alias, "300"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + # Remove the 300 rate limit again, expecting an error + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + [interface_alias, "300"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "300 DHCP rate limit does not exist on Ethernet0" in result.output + # Add new rate limit of 80 to the interface using alias + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + [interface_alias, "80"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + # Disable alias mode + os.environ['SONIC_CLI_IFACE_MODE'] = "default" + + def test_config_dhcp_rate_add_on_invalid_interface(self): + db = Db() + runner = CliRunner() + obj = {'config_db': db.cfgdb} + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["etp33", "20"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: etp33 does not exist" in result.output + + @patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True)) + @patch( + "config.validated_config_db_connector.ValidatedConfigDBConnector.validated_mod_entry", + mock.Mock(side_effect=ValueError) + ) + def test_config_dhcp_rate_add_del_with_value_error(self): + db = Db() + runner = CliRunner() + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["del"], + ["Ethernet84", "300"], obj=db) + print(result.exit_code) + print(result.output) + assert result.output != 0 + assert "Ethernet84 invalid or does not exist" in result.output + + result = runner.invoke(config.config.commands["interface"].commands["dhcp-mitigation-rate"].commands["add"], + ["Ethernet72", "65"], obj=db) + print(result.exit_code) + print(result.output) + assert result.output != 0 + assert "Ethernet72 invalid or does not exist" in result.output + + def test_show_dhcp_rate_limit(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["interfaces"].commands["dhcp-mitigation-rate"]) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == show_interfaces_dhcp_rate_limit_output + + def test_show_dhcp_rate_limit_in_alias_mode(self): + runner = CliRunner() + os.environ['SONIC_CLI_IFACE_MODE'] = "alias" + # Run show interfaces dhcp-mitigation-rate command + result = runner.invoke(show.cli.commands["interfaces"].commands["dhcp-mitigation-rate"]) + # Go back to default mode + os.environ['SONIC_CLI_IFACE_MODE'] = "default" + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == show_dhcp_rate_limit_in_alias_mode_output + + def test_show_dhcp_rate_limit_single_interface(self): + runner = CliRunner() + # Interface to test + interface_name = "Ethernet0" + # Run show interfaces dhcp-mitigation-rate command with valid interface + result = runner.invoke( + show.cli.commands["interfaces"].commands["dhcp-mitigation-rate"], [interface_name]) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == show_dhcp_rate_limit_single_interface_output + + def test_show_dhcp_rate_limit_single_interface_portchannel(self): + runner = CliRunner() + # Portchannel interface to test + portchannel_name = "PortChannel0001" + # Run show interfaces dhcp-mitigation-rate command with valid portchannel + result = runner.invoke( + show.cli.commands["interfaces"].commands["dhcp-mitigation-rate"], [portchannel_name]) + print(result.exit_code) + print(result.output) + # Assert error message + assert result.exit_code != 0 + assert f"{portchannel_name} is a PortChannel!" in result.output + + def test_show_dhcp_rate_limit_single_interface_with_nonexist_interface(self): + runner = CliRunner() + # Invalid interface name to test + invalid_interface_name = "etp35" + # etp35 is a non-existing interface + # Run show interfaces dhcp-mitigation-rate command with invalid interface + result = runner.invoke( + show.cli.commands["interfaces"].commands["dhcp-mitigation-rate"], [invalid_interface_name]) + print(result.exit_code) + print(result.output) + # Assert error message + assert result.exit_code != 0 + assert f"{invalid_interface_name} does not exist" in result.output + + @classmethod + def teardown_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + print("TEARDOWN") diff --git a/tests/interfaces_test.py b/tests/interfaces_test.py old mode 100644 new mode 100755 index 6acdb505ac..d2979d5e6d --- a/tests/interfaces_test.py +++ b/tests/interfaces_test.py @@ -264,6 +264,7 @@ """ + class TestInterfaces(object): @classmethod def setup_class(cls): @@ -339,7 +340,8 @@ def test_show_interfaces_neighbor_expected(self): def test_show_interfaces_neighbor_expected_t1(self, setup_t1_topo): runner = CliRunner() - result = runner.invoke(show.cli.commands["interfaces"].commands["neighbor"].commands["expected"], []) + result = runner.invoke( + show.cli.commands["interfaces"].commands["neighbor"].commands["expected"], []) print(result.exit_code) print(result.output) # traceback.print_tb(result.exc_info[2]) @@ -358,7 +360,8 @@ def test_show_interfaces_neighbor_expected_Ethernet112(self): def test_show_interfaces_neighbor_expected_t1_Ethernet0(self, setup_t1_topo): runner = CliRunner() - result = runner.invoke(show.cli.commands["interfaces"].commands["neighbor"].commands["expected"], ["Ethernet0"]) + result = runner.invoke( + show.cli.commands["interfaces"].commands["neighbor"].commands["expected"], ["Ethernet0"]) print(result.exit_code) print(result.output) # traceback.print_tb(result.exc_info[2]) @@ -368,7 +371,8 @@ def test_show_interfaces_neighbor_expected_t1_Ethernet0(self, setup_t1_topo): def test_show_interfaces_neighbor_expected_etp29(self): runner = CliRunner() os.environ['SONIC_CLI_IFACE_MODE'] = "alias" - result = runner.invoke(show.cli.commands["interfaces"].commands["neighbor"].commands["expected"], ["etp29"]) + result = runner.invoke( + show.cli.commands["interfaces"].commands["neighbor"].commands["expected"], ["etp29"]) os.environ['SONIC_CLI_IFACE_MODE'] = "default" print(result.exit_code) print(result.output) @@ -443,17 +447,14 @@ def test_parse_interface_in_filter(self): intf_list = parse_interface_in_filter(intf_filter) assert len(intf_list) == 1 assert intf_list[0] == "Ethernet0" - intf_filter = "Ethernet1-3" intf_list = parse_interface_in_filter(intf_filter) assert len(intf_list) == 3 assert intf_list == ["Ethernet1", "Ethernet2", "Ethernet3"] - intf_filter = "Ethernet-BP10" intf_list = parse_interface_in_filter(intf_filter) assert len(intf_list) == 1 assert intf_list[0] == "Ethernet-BP10" - intf_filter = "Ethernet-BP10-12" intf_list = parse_interface_in_filter(intf_filter) assert len(intf_list) == 3 @@ -462,17 +463,14 @@ def test_parse_interface_in_filter(self): def test_show_interfaces_switchport_status(self): runner = CliRunner() db = Db() - result = runner.invoke( config.config.commands["switchport"].commands["mode"], ["routed", "PortChannel0001"], obj=db) print(result.exit_code) print(result.output) assert result.exit_code == 0 - result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["status"]) print(result.exit_code) print(result.output) - assert result.exit_code == 0 assert result.output == show_interfaces_switchport_status_output @@ -481,7 +479,6 @@ def test_show_interfaces_switchport_config(self): result = runner.invoke(show.cli.commands["interfaces"].commands["switchport"].commands["config"]) print(result.exit_code) print(result.output) - assert result.exit_code == 0 assert result.output == show_interfaces_switchport_config_output @@ -492,7 +489,6 @@ def test_show_interfaces_switchport_config_in_alias_mode(self): os.environ['SONIC_CLI_IFACE_MODE'] = "default" print(result.exit_code) print(result.output) - assert result.exit_code == 0 assert result.output == show_interfaces_switchport_config_in_alias_mode_output diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 187efed553..bd505d4b4b 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -32,7 +32,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet4": { "admin_status": "up", @@ -44,7 +45,8 @@ "tpid": "0x8100", "mode": "trunk", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet8": { "admin_status": "up", @@ -56,7 +58,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet12": { "admin_status": "up", @@ -68,7 +71,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet16": { "admin_status": "up", @@ -80,7 +84,8 @@ "tpid": "0x8100", "mode": "trunk", "pfc_asym": "off", - "speed": "100" + "speed": "100", + "dhcp_rate_limit": "300" }, "PORT|Ethernet20": { "admin_status": "up", @@ -91,7 +96,8 @@ "mtu": "9100", "tpid": "0x9200", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet24": { "admin_status": "up", @@ -104,7 +110,8 @@ "mode": "trunk", "pfc_asym": "off", "speed": "1000", - "role": "Dpc" + "role": "Dpc", + "dhcp_rate_limit": "300" }, "PORT|Ethernet28": { "admin_status": "up", @@ -116,7 +123,8 @@ "tpid": "0x8100", "mode": "trunk", "pfc_asym": "off", - "speed": "1000" + "speed": "1000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet32": { "admin_status": "up", @@ -128,7 +136,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet36": { "admin_status": "up", @@ -140,7 +149,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "10" + "speed": "10", + "dhcp_rate_limit": "300" }, "PORT|Ethernet40": { "admin_status": "up", @@ -152,7 +162,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet44": { "admin_status": "up", @@ -164,7 +175,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet48": { "admin_status": "up", @@ -176,7 +188,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet52": { "admin_status": "up", @@ -188,7 +201,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet56": { "admin_status": "up", @@ -200,7 +214,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet60": { "admin_status": "up", @@ -212,7 +227,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet64": { "admin_status": "up", @@ -224,7 +240,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet68": { "admin_status": "up", @@ -236,7 +253,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet72": { "admin_status": "up", @@ -260,7 +278,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet80": { "admin_status": "up", @@ -272,7 +291,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet84": { "admin_status": "up", @@ -284,7 +304,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet88": { "admin_status": "up", @@ -296,7 +317,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet92": { "admin_status": "up", @@ -308,7 +330,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet96": { "admin_status": "up", @@ -320,7 +343,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet100": { "alias": "etp26", @@ -331,7 +355,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet104": { "alias": "etp27", @@ -342,7 +367,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet108": { "alias": "etp28", @@ -353,7 +379,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet112": { "admin_status": "up", @@ -365,7 +392,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet116": { "admin_status": "up", @@ -377,7 +405,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet120": { "admin_status": "up", @@ -389,7 +418,8 @@ "tpid": "0x8100", "mode": "routed", "pfc_asym": "off", - "speed": "40000" + "speed": "40000", + "dhcp_rate_limit": "300" }, "PORT|Ethernet124": { "admin_status": "up", @@ -402,6 +432,7 @@ "mode": "routed", "pfc_asym": "off", "speed": "40000", + "dhcp_rate_limit": "300", "fec" : "auto" }, "VLAN_SUB_INTERFACE|Ethernet0.10": { diff --git a/utilities_common/cli.py b/utilities_common/cli.py index c8a314b704..6084f4155c 100644 --- a/utilities_common/cli.py +++ b/utilities_common/cli.py @@ -803,6 +803,19 @@ def is_interface_in_config_db(config_db, interface_name): return True +def get_interface_dhcp_mitigation_rate(config_db, interface): + port_data = config_db.get_entry('PORT', interface) + + if "dhcp_rate_limit" in port_data: + rate = port_data["dhcp_rate_limit"] + else: + rate = "0" + + if rate == "0": + return "" + return rate + + class MutuallyExclusiveOption(click.Option): """ This option type is extended with `mutually_exclusive` parameter which make