diff --git a/config/main.py b/config/main.py index fbacc12712..5691f0664c 100644 --- a/config/main.py +++ b/config/main.py @@ -4909,6 +4909,96 @@ def interface(ctx, namespace): config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=str(namespace)) config_db.connect() ctx.obj = {'config_db': config_db, 'namespace': str(namespace)} + + +@config.group(cls=clicommon.AliasedGroup, name='switch-fast-linkup', context_settings=CONTEXT_SETTINGS) +@click.pass_context +def switch_fast_linkup_group(ctx): + """Configure fast link-up global configuration parameters""" + pass + + +# 'global' subcommand +@switch_fast_linkup_group.command(name='global') +@click.option('--polling-time', type=int, required=False, help='Polling time (sec)') +@click.option('--guard-time', type=int, required=False, help='Guard time (sec)') +@click.option('--ber', '--ber-threshold', type=int, required=False, help='BER threshold exponent (e.g., 12 for 1e-12)') +@clicommon.pass_db +def switch_fast_linkup_global_cmd(db, polling_time, guard_time, ber): + """Configure global fast link-up feature parameters""" + if polling_time is None and guard_time is None and ber is None: + raise click.UsageError('Failed to configure fast link-up global: no options are provided') + # Read capability and ranges from STATE_DB for validation + state_db = db.db.STATE_DB + cap_tbl = db.db.get_all(state_db, 'SWITCH_CAPABILITY|switch') or {} + if cap_tbl.get('FAST_LINKUP_CAPABLE', 'false') != 'true': + raise click.ClickException('Fast link-up is not supported on this platform') + + poll_range_str = cap_tbl.get('FAST_LINKUP_POLLING_TIMER_RANGE') + guard_range_str = cap_tbl.get('FAST_LINKUP_GUARD_TIMER_RANGE') + if not poll_range_str or not guard_range_str: + raise click.ClickException('Fast link-up capability ranges are not defined on this platform') + + poll_range = poll_range_str.split(',') + guard_range = guard_range_str.split(',') + + data = {} + if polling_time is not None: + if not (int(poll_range[0]) <= int(polling_time) <= int(poll_range[1])): + raise click.ClickException('polling_time {} out of supported range [{}, {}]'.format( + polling_time, poll_range[0], poll_range[1])) + data['polling_time'] = str(polling_time) + if guard_time is not None: + if not (int(guard_range[0]) <= int(guard_time) <= int(guard_range[1])): + raise click.ClickException('guard_time {} out of supported range [{}, {}]'.format( + guard_time, guard_range[0], guard_range[1])) + data['guard_time'] = str(guard_time) + if ber is not None: + if int(ber) < 1 or int(ber) > 255: + raise click.ClickException('ber_threshold {} out of supported range [1, 255]'.format(ber)) + data['ber_threshold'] = str(ber) + try: + db.cfgdb.mod_entry('SWITCH_FAST_LINKUP', 'GLOBAL', data) + + log.log_notice('Configured fast link-up global: {}'.format(data)) + except Exception as e: + log.log_error('Failed to configure fast link-up global: {}'.format(str(e))) + raise SystemExit(1) + + +# 'fast-linkup' subcommand +@interface.command('fast-linkup') +@click.argument('interface_name', metavar='', required=True) +@click.argument('mode', metavar='', required=True, type=click.Choice(['enabled', 'disabled'])) +@click.option('-v', '--verbose', is_flag=True, help='Enable verbose output') +@click.pass_context +def fast_linkup(ctx, interface_name, mode, verbose): + """Enable/disable fast link-up on an interface""" + config_db = ctx.obj['config_db'] + namespace = ctx.obj.get('namespace', DEFAULT_NAMESPACE) + + if clicommon.get_interface_naming_mode() == 'alias': + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + raise click.ClickException("'interface_name' is None!") + if not interface_name_is_valid(config_db, interface_name): + raise click.ClickException('Interface name is invalid. Please enter a valid interface name') + + # Read capability from STATE_DB for validation + db = Db() + state_db = db.db_clients.get(namespace, db.db) + cap_tbl = state_db.get_all(state_db.STATE_DB, 'SWITCH_CAPABILITY|switch') or {} + if cap_tbl.get('FAST_LINKUP_CAPABLE', 'false') != 'true': + raise click.ClickException('Fast link-up is not supported on this platform') + + log.log_info("'interface fast-linkup {} {}' executing...".format(interface_name, mode)) + if namespace is DEFAULT_NAMESPACE: + command = ['portconfig', '-p', str(interface_name), '-fl', str(mode)] + else: + command = ['portconfig', '-p', str(interface_name), '-fl', str(mode), '-n', str(namespace)] + if verbose: + command += ['-vv'] + clicommon.run_command(command, display_cmd=verbose) # # 'startup' subcommand # diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index acb4963b7c..742a60af8d 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -5323,6 +5323,92 @@ This command is used to manage switch hash algorithm global configuration. admin@sonic:~$ config switch-hash global lag-hash-algorithm 'CRC' ``` +## Fast Link-Up + +This section documents the commands to configure and display the Fast Link-Up feature. + +### Fast Link-Up Show Commands + +**show switch-fast-linkup global** + +Display switch Fast Link-Up global configuration. + +- Usage: + ```bash + show switch-fast-linkup global [--json] + ``` + +- Examples: + ```bash + admin@sonic:~$ show switch-fast-linkup global + Field Value + ------------- ----- + polling_time 60 + guard_time 10 + ber_threshold 12 + ``` + +**show interfaces fast-linkup status** + +Display per-interface Fast Link-Up mode. + +- Usage: + ```bash + show interfaces fast-linkup status + ``` + +- Example: + ```bash + admin@sonic:~$ show interfaces fast-linkup status + Interface fast_linkup + ----------- ----------- + Ethernet0 true + Ethernet4 false + ``` + +### Fast Link-Up Config Commands + +**config switch-fast-linkup global** + +Configure the switch Fast Link-Up global parameters. + +- Usage: + ```bash + config switch-fast-linkup global [--polling-time ] [--guard-time ] [--ber ] + ``` + +- Parameters: + - _polling-time_: time in seconds to attempt fast link-up (uint16). + - _guard-time_: time in seconds link must stay up with low BER to keep fast link-up (uint8). + - _ber_: BER threshold exponent (uint8). Example: 12 means 1e-12. + +- Validation: + - Reads `SWITCH_CAPABILITY|switch` from STATE_DB. Fails if `FAST_LINKUP_CAPABLE != true`. + - If ranges are present, rejects out-of-range `polling_time`/`guard_time`. + +- Examples: + ```bash + admin@sonic:~$ config switch-fast-linkup global --polling-time 60 --guard-time 10 --ber 12 + ``` + +**config interface fast-linkup** + +Enable/disable Fast Link-Up per interface. + +- Usage: + ```bash + config interface fast-linkup + ``` + +- Behavior: + - Writes `PORT|:fast_linkup` as `true` (enabled) or `false` (disabled). + +- Examples: + ```bash + admin@sonic:~$ config interface fast-linkup Ethernet0 enabled + admin@sonic:~$ config interface fast-linkup Ethernet4 disabled + ``` + ## Interfaces ### Interface Show Commands diff --git a/scripts/portconfig b/scripts/portconfig index acdebcc236..c04c516a15 100755 --- a/scripts/portconfig +++ b/scripts/portconfig @@ -6,6 +6,7 @@ portconfig is the utility to show and change ECN configuration usage: portconfig [-h] [-v] [-s] [-f] [-m] [-tp] [-p PROFILE] [-gmin GREEN_MIN] [-gmax GREEN_MAX] [-ymin YELLOW_MIN] [-ymax YELLOW_MAX] [-rmin RED_MIN] [-rmax RED_MAX] [-vv] [-n namespace] + [-fl FAST_LINKUP] optional arguments: -h --help show this help message and exit @@ -22,6 +23,7 @@ optional arguments: -t --interface-type port interface type -T --adv-interface-types port advertised interface types -lt --link-training port link training mode + -fl --fast-linkup port fast link-up mode (enabled/disabled) -P --tx-power 400G ZR modulet target Tx output power (dBm) -F --laser-freq 400G ZR module 75GHz grid frequency (GHz) """ @@ -56,6 +58,7 @@ PORT_ADV_SPEEDS_CONFIG_FIELD_NAME = "adv_speeds" PORT_INTERFACE_TYPE_CONFIG_FIELD_NAME = "interface_type" PORT_ADV_INTERFACE_TYPES_CONFIG_FIELD_NAME = "adv_interface_types" PORT_LINK_TRAINING_CONFIG_FIELD_NAME = "link_training" +PORT_FAST_LINKUP_CONFIG_FIELD_NAME = "fast_linkup" PORT_XCVR_LASER_FREQ_FIELD_NAME = "laser_freq" PORT_XCVR_TX_POWER_FIELD_NAME = "tx_power" PORT_CHANNEL_TABLE_NAME = "PORTCHANNEL" @@ -325,6 +328,23 @@ class portconfig(object): else: raise Exception("System not ready to accept TPID config. Please try again later.") + def set_fast_linkup(self, port, mode): + if self.is_lag: + raise Exception("Invalid port %s" % (port)) + if self.verbose: + print("Setting fast-linkup %s on port %s" % (mode, port)) + + normalized = str(mode).strip().lower() + if normalized == 'enabled': + value = 'true' + elif normalized == 'disabled': + value = 'false' + else: + print('Invalid fast-linkup mode specified: {}'.format(mode)) + print('Valid modes: enabled, disabled') + exit(1) + self.db.mod_entry(PORT_TABLE_NAME, port, {PORT_FAST_LINKUP_CONFIG_FIELD_NAME: value}) + def main(): parser = argparse.ArgumentParser(description='Set SONiC port parameters', @@ -349,6 +369,8 @@ def main(): help = 'port advertised interface types', default=None) parser.add_argument('-lt', '--link-training', type = str, required = False, help = 'port link training mode', default=None) + parser.add_argument('-fl', '--fast-linkup', type=str, required=False, + help='port fast link-up mode (enabled|disabled)', default=None) parser.add_argument('-P', '--tx-power', type=float, required=False, help='Tx output power(dBm)', default=None) parser.add_argument('-F', '--laser-freq', type=int, required=False, @@ -363,7 +385,7 @@ def main(): port.list_params(args.port) elif args.speed or args.fec or args.mtu or args.link_training or args.autoneg or args.adv_speeds or \ args.interface_type or args.adv_interface_types or args.tpid or \ - args.tx_power or args.laser_freq: + args.tx_power or args.laser_freq or args.fast_linkup: if args.speed: port.set_speed(args.port, args.speed) if args.fec: @@ -374,6 +396,8 @@ def main(): port.set_link_training(args.port, args.link_training) if args.autoneg: port.set_autoneg(args.port, args.autoneg) + if args.fast_linkup: + port.set_fast_linkup(args.port, args.fast_linkup) if args.adv_speeds: port.set_adv_speeds(args.port, args.adv_speeds) if args.interface_type: diff --git a/show/interfaces/__init__.py b/show/interfaces/__init__.py index 61498b5c12..62a7e3ec25 100644 --- a/show/interfaces/__init__.py +++ b/show/interfaces/__init__.py @@ -1214,3 +1214,27 @@ def tablelize(keys): header = ['Interface', 'DHCP Mitigation Rate'] click.echo(tabulate(tablelize(keys), header, tablefmt="simple", stralign='left')) + + +# +# fast-linkup group (show interfaces fast-linkup ...) +# + + +@interfaces.group(name='fast-linkup', cls=clicommon.AliasedGroup) +def fast_linkup(): + """Show interface fast-linkup information""" + pass + + +@fast_linkup.command(name='status') +@clicommon.pass_db +def fast_linkup_status(db): + """show interfaces fast-linkup status""" + config_db = db.cfgdb + ports = config_db.get_table('PORT') or {} + rows = [] + for ifname, entry in natsorted(ports.items()): + fast_linkup = entry.get('fast_linkup', 'false') + rows.append([ifname, fast_linkup]) + click.echo(tabulate(rows, headers=['Interface', 'fast_linkup'], tablefmt='outline')) diff --git a/show/main.py b/show/main.py index 893a2199fa..953b879501 100755 --- a/show/main.py +++ b/show/main.py @@ -2875,6 +2875,28 @@ def banner(db): click.echo(tabulate(messages, headers=hdrs, tablefmt='simple', missingval='')) +# +# 'switch-fast-linkup' command group ("show switch-fast-linkup ...") +# +@cli.group(cls=clicommon.AliasedGroup, name='switch-fast-linkup', context_settings=CONTEXT_SETTINGS) +@click.pass_context +def switch_fast_linkup_group(ctx): + """Show fast link-up feature configuration (global)""" + pass + + +@switch_fast_linkup_group.command(name='global') +@click.option('--json', 'json_output', is_flag=True, default=False, help='JSON output') +@clicommon.pass_db +def show_fast_linkup_global(db, json_output): + data = db.cfgdb.get_entry('SWITCH_FAST_LINKUP', 'GLOBAL') or {} + if json_output: + click.echo(json.dumps(data, indent=2)) + return + rows = [[k, v] for k, v in data.items()] + click.echo(tabulate(rows, headers=['Field', 'Value'], tablefmt='grid')) + + # Load plugins and register them helper = util_base.UtilHelper() helper.load_and_register_plugins(plugins, cli) diff --git a/tests/fast_linkup_input/mock_config/global/config_db.json b/tests/fast_linkup_input/mock_config/global/config_db.json new file mode 100644 index 0000000000..6a5c27db08 --- /dev/null +++ b/tests/fast_linkup_input/mock_config/global/config_db.json @@ -0,0 +1,9 @@ +{ + "SWITCH_FAST_LINKUP|GLOBAL": { + "polling_time": "60", + "guard_time": "10", + "ber_threshold": "12" + } +} + + diff --git a/tests/fast_linkup_input/mock_config/ports/config_db.json b/tests/fast_linkup_input/mock_config/ports/config_db.json new file mode 100644 index 0000000000..5d32cfa1a6 --- /dev/null +++ b/tests/fast_linkup_input/mock_config/ports/config_db.json @@ -0,0 +1,12 @@ +{ + "PORT|Ethernet0": { + "admin_status": "up", + "fast_linkup": "true" + }, + "PORT|Ethernet4": { + "admin_status": "up", + "fast_linkup": "false" + } +} + + diff --git a/tests/fast_linkup_input/mock_state/missing_ranges/state_db.json b/tests/fast_linkup_input/mock_state/missing_ranges/state_db.json new file mode 100644 index 0000000000..2577c688e0 --- /dev/null +++ b/tests/fast_linkup_input/mock_state/missing_ranges/state_db.json @@ -0,0 +1,5 @@ +{ + "SWITCH_CAPABILITY|switch": { + "FAST_LINKUP_CAPABLE": "true" + } +} diff --git a/tests/fast_linkup_input/mock_state/not_supported/state_db.json b/tests/fast_linkup_input/mock_state/not_supported/state_db.json new file mode 100644 index 0000000000..f7c54302c5 --- /dev/null +++ b/tests/fast_linkup_input/mock_state/not_supported/state_db.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "FAST_LINKUP_CAPABLE": "false" + } +} + + diff --git a/tests/fast_linkup_input/mock_state/supported/state_db.json b/tests/fast_linkup_input/mock_state/supported/state_db.json new file mode 100644 index 0000000000..6e0224b3ca --- /dev/null +++ b/tests/fast_linkup_input/mock_state/supported/state_db.json @@ -0,0 +1,9 @@ +{ + "SWITCH_CAPABILITY|switch": { + "FAST_LINKUP_CAPABLE": "true", + "FAST_LINKUP_POLLING_TIMER_RANGE": "5,120", + "FAST_LINKUP_GUARD_TIMER_RANGE": "1,20" + } +} + + diff --git a/tests/fast_linkup_test.py b/tests/fast_linkup_test.py new file mode 100644 index 0000000000..99d4b199f8 --- /dev/null +++ b/tests/fast_linkup_test.py @@ -0,0 +1,391 @@ +import os +import logging + + +import show.main as show +import config.main as config + +from click.testing import CliRunner +from utilities_common.db import Db +from .mock_tables import dbconnector + + +logger = logging.getLogger(__name__) + + +SUCCESS = 0 +ERROR2 = 2 + + +test_path = os.path.dirname(os.path.abspath(__file__)) +input_path = os.path.join(test_path, "fast_linkup_input") +mock_state_path = os.path.join(input_path, "mock_state") +mock_config_path = os.path.join(input_path, "mock_config") + + +class TestFastLinkupCLI: + @classmethod + def setup_class(cls): + logger.info("Setup class: %s", cls.__name__) + os.environ['UTILITIES_UNIT_TESTING'] = "1" + + @classmethod + def teardown_class(cls): + logger.info("Teardown class: %s", cls.__name__) + os.environ['UTILITIES_UNIT_TESTING'] = "0" + dbconnector.dedicated_dbs.clear() + + def test_config_global_not_supported(self): + # STATE_DB indicates not supported + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "not_supported", "state_db") + db = Db() + runner = CliRunner() + result = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--polling-time", "60"], obj=db + ) + # Current CLI raises ClickException -> exit code 1 + assert result.exit_code != SUCCESS + assert "not supported" in result.output.lower() + + def test_config_global_range_validation(self): + # STATE_DB indicates supported with ranges polling:[5,120], guard:[1,20] + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + db = Db() + runner = CliRunner() + + # Below min polling -> error + res1 = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--polling-time", "4"], obj=db + ) + assert res1.exit_code != SUCCESS + + # Above max guard -> error + res2 = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--guard-time", "21"], obj=db + ) + assert res2.exit_code != SUCCESS + + # In-range values -> success + res3 = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--polling-time", "60", "--guard-time", "10", "--ber", "12"], obj=db + ) + assert res3.exit_code == SUCCESS + + # show command tests: + # 1. Validate default global values match show output (feature supported) + # 2. Validate configured global values via config CLI match show output + def test_show_global_configured_values(self, monkeypatch): + # Provide CONFIG_DB with a pre-set global entry and verify JSON output matches exactly + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "global", "config_db") + db = Db() + runner = CliRunner() + # Ensure show command uses our injected Db + monkeypatch.setattr(show, 'Db', lambda: db) + result = runner.invoke( + show.cli.commands["switch-fast-linkup"].commands["global"], + ["--json"], obj=db + ) + assert result.exit_code == SUCCESS + import json + data = json.loads(result.output) + assert data == {"polling_time": "60", "guard_time": "10", "ber_threshold": "12"} + + def test_show_interfaces_mode(self, monkeypatch): + # Provide CONFIG_DB with PORT table fast_linkup fields + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + result = runner.invoke( + show.cli.commands["interfaces"].commands["fast-linkup"].commands["status"], + [], obj=db + ) + assert result.exit_code == SUCCESS + self.assert_interface_fast_linkup_mode(result.output, "Ethernet0", "true") + + def test_enable_fast_linkup_supported(self, monkeypatch): + # Use supported STATE_DB (FAST_LINKUP_CAPABLE == 'true') + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + + # Patch command runner to simulate 'portconfig -fl' writing to CONFIG_DB + import utilities_common.cli as clicommon + + def fake_run_command( + cmd, + display_cmd=False, + ignore_error=False, + return_cmd=False, + interactive_mode=False, + shell=False, + ): + # Expect: ['portconfig', '-p', , '-fl', ] + assert cmd[0] == 'portconfig' + iface = cmd[cmd.index('-p') + 1] + mode = cmd[cmd.index('-fl') + 1] + value = 'true' if mode == 'enabled' else 'false' + db.cfgdb.mod_entry('PORT', iface, {'fast_linkup': value}) + return + + monkeypatch.setattr(clicommon, 'run_command', fake_run_command) + # Enable fast-linkup on Ethernet0 via config CLI + result = runner.invoke( + config.config.commands["interface"].commands["fast-linkup"], + ["Ethernet0", "enabled"], + obj={'config_db': db.cfgdb, 'namespace': config.DEFAULT_NAMESPACE} + ) + assert result.exit_code == SUCCESS + + # Show reflects change + import show.interfaces as show_interfaces + # Ensure 'show interfaces ...' reads from the same CONFIG_DB instance modified above + monkeypatch.setattr(show_interfaces, 'ConfigDBConnector', lambda: db.cfgdb) + show_result = runner.invoke( + show.cli.commands["interfaces"].commands["fast-linkup"].commands["status"], + [], obj=db + ) + self.assert_interface_fast_linkup_mode(show_result.output, "Ethernet0", "true") + + def test_disable_fast_linkup_supported(self, monkeypatch): + # Use supported STATE_DB (FAST_LINKUP_CAPABLE == 'true') + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + + import utilities_common.cli as clicommon + + def fake_run_command( + cmd, + display_cmd=False, + ignore_error=False, + return_cmd=False, + interactive_mode=False, + shell=False, + ): + iface = cmd[cmd.index('-p') + 1] + mode = cmd[cmd.index('-fl') + 1] + value = 'true' if mode == 'enabled' else 'false' + db.cfgdb.mod_entry('PORT', iface, {'fast_linkup': value}) + return + + monkeypatch.setattr(clicommon, 'run_command', fake_run_command) + + # Disable fast-linkup on Ethernet0 via config CLI + result = runner.invoke( + config.config.commands["interface"].commands["fast-linkup"], + ["Ethernet0", "disabled"], + obj={'config_db': db.cfgdb, 'namespace': config.DEFAULT_NAMESPACE} + ) + assert result.exit_code == SUCCESS + + # Show reflects change + import show.interfaces as show_interfaces + # Ensure 'show interfaces ...' reads from the same CONFIG_DB instance modified above + monkeypatch.setattr(show_interfaces, 'ConfigDBConnector', lambda: db.cfgdb) + show_result = runner.invoke( + show.cli.commands["interfaces"].commands["fast-linkup"].commands["status"], + [], obj=db + ) + self.assert_interface_fast_linkup_mode(show_result.output, "Ethernet0", "false") + + def test_enable_fast_linkup_not_supported(self, monkeypatch): + # Use not_supported STATE_DB (FAST_LINKUP_CAPABLE == 'false') + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "not_supported", "state_db") + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + + import utilities_common.cli as clicommon + + def fake_run_command( + cmd, + display_cmd=False, + ignore_error=False, + return_cmd=False, + interactive_mode=False, + shell=False, + ): + # In not-supported scenario, config command should fail before invoking portconfig, + # but if invoked, simulate failure. + raise SystemExit(1) + + monkeypatch.setattr(clicommon, 'run_command', fake_run_command) + + result = runner.invoke( + config.config.commands["interface"].commands["fast-linkup"], + ["Ethernet0", "enabled"], + obj={'config_db': db.cfgdb, 'namespace': config.DEFAULT_NAMESPACE} + ) + assert result.exit_code != SUCCESS + + # Helper: Assert that the specified interface has the expected fast-linkup mode in the CLI output. + def assert_interface_fast_linkup_mode(self, output, intf_name, expected_mode): + for line in output.splitlines(): + if intf_name in line and expected_mode.lower() in line.lower(): + return + raise AssertionError(f"{intf_name} fast-linkup mode is not set to {expected_mode}") + + def test_config_global_no_options(self): + # No options -> UsageError + db = Db() + runner = CliRunner() + result = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + [], + obj=db + ) + assert result.exit_code != SUCCESS + assert "no options are provided" in result.output.lower() + + def test_config_global_set_entry_failure(self, monkeypatch): + # If set_entry raises, command should exit with code 1 + class FakeState: + STATE_DB = "STATE_DB" + + def get_all(self, db, key): + return { + "FAST_LINKUP_CAPABLE": "true", + "FAST_LINKUP_POLLING_TIMER_RANGE": "5,120", + "FAST_LINKUP_GUARD_TIMER_RANGE": "1,20" + } + + class FakeCfg: + def get_entry(self, *args, **kwargs): + return {} + + def set_entry(self, *args, **kwargs): + raise Exception() + + class FakeDb: + def __init__(self): + self.db = FakeState() + self.cfgdb = FakeCfg() + fake_db = FakeDb() + + runner = CliRunner() + res = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--polling-time", "60"], + obj=fake_db + ) + assert res.exit_code != SUCCESS + + def test_config_global_ber_out_of_range(self): + # Use supported STATE_DB; out-of-range BER should error + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + db = Db() + runner = CliRunner() + res = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--ber", "0"], + obj=db + ) + assert res.exit_code != SUCCESS + assert "ber_threshold" in res.output + + def test_config_interface_alias_none(self, monkeypatch): + # Alias mode with alias not found -> ctx.fail("'interface_name' is None!") + import utilities_common.cli as clicommon + monkeypatch.setattr(clicommon, "get_interface_naming_mode", lambda: "alias") + monkeypatch.setattr(config, "interface_alias_to_name", lambda cfgdb, name: None) + # STATE_DB capability supported so we get to alias resolution + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + result = runner.invoke( + config.config.commands["interface"].commands["fast-linkup"], + ["EthAlias0", "enabled"], + obj={'config_db': db.cfgdb, 'namespace': config.DEFAULT_NAMESPACE} + ) + assert result.exit_code != SUCCESS + assert "interface_name" in result.output + + def test_config_interface_invalid_name(self, monkeypatch): + # Invalid interface name -> ctx.fail() + import utilities_common.cli as clicommon + monkeypatch.setattr(clicommon, "get_interface_naming_mode", lambda: "default") + monkeypatch.setattr(config, "interface_name_is_valid", lambda cfgdb, name: False) + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + result = runner.invoke( + config.config.commands["interface"].commands["fast-linkup"], + ["Ethernet999", "enabled"], + obj={'config_db': db.cfgdb, 'namespace': config.DEFAULT_NAMESPACE} + ) + assert result.exit_code != SUCCESS + assert "invalid" in result.output.lower() + + def test_config_interface_namespace_portconfig(self, monkeypatch): + # Ensure namespace is passed to portconfig in multi-ASIC mode + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "supported", "state_db") + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "ports", "config_db") + db = Db() + runner = CliRunner() + + import utilities_common.cli as clicommon + + def fake_run_command( + cmd, + display_cmd=False, + ignore_error=False, + return_cmd=False, + interactive_mode=False, + shell=False, + ): + assert '-n' in cmd + ns_index = cmd.index('-n') + 1 + assert cmd[ns_index] == 'asic0' + return + + monkeypatch.setattr(clicommon, 'run_command', fake_run_command) + result = runner.invoke( + config.config.commands["interface"].commands["fast-linkup"], + ["Ethernet0", "enabled"], + obj={'config_db': db.cfgdb, 'namespace': 'asic0'} + ) + assert result.exit_code == SUCCESS + + def test_show_switch_fast_linkup_group_help(self): + # Enter group to cover the 'pass' in group callback + runner = CliRunner() + res = runner.invoke(show.cli, ["switch-fast-linkup", "--help"]) + assert res.exit_code == SUCCESS + assert "Show fast link-up feature configuration" in res.output + + def test_show_global_table_output(self, monkeypatch): + # Non-JSON output should render table rows + # Align with test_show_global_configured_values: set dedicated_dbs and monkeypatch show.Db + dbconnector.dedicated_dbs["CONFIG_DB"] = os.path.join(mock_config_path, "global", "config_db") + db = Db() + runner = CliRunner() + monkeypatch.setattr(show, 'Db', lambda: db) + res = runner.invoke(show.cli.commands["switch-fast-linkup"].commands["global"], [], obj=db) + assert res.exit_code == SUCCESS + assert "polling_time" in res.output and "60" in res.output + + def test_show_interfaces_fast_linkup_group_help(self): + # Cover 'pass' in 'show interfaces fast-linkup' group + runner = CliRunner() + res = runner.invoke(show.cli, ["interfaces", "fast-linkup", "--help"]) + assert res.exit_code == SUCCESS + + def test_config_global_missing_ranges(self): + # STATE_DB indicates supported but missing range fields + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "missing_ranges", "state_db") + db = Db() + runner = CliRunner() + result = runner.invoke( + config.config.commands["switch-fast-linkup"].commands["global"], + ["--polling-time", "60"], obj=db + ) + assert result.exit_code != SUCCESS + assert "capability ranges are not defined" in result.output.lower()