diff --git a/config/main.py b/config/main.py index 50f36de9b4..b963e424c5 100644 --- a/config/main.py +++ b/config/main.py @@ -3812,92 +3812,156 @@ def warm_restart(ctx, redis_unix_socket_path): # Note: redis_unix_socket_path is a path string, and the ground truth is now from database_config.json. # We only use it as a bool indicator on either unix_socket_path or tcp port use_unix_socket_path = bool(redis_unix_socket_path) - config_db = ConfigDBConnector(use_unix_socket_path=use_unix_socket_path) - config_db.connect(wait_for_init=False) - - # warm restart enable/disable config is put in stateDB, not persistent across cold reboot, not saved to config_DB.json file - state_db = SonicV2Connector(use_unix_socket_path=use_unix_socket_path) - state_db.connect(state_db.STATE_DB, False) TABLE_NAME_SEPARATOR = '|' prefix = 'WARM_RESTART_ENABLE_TABLE' + TABLE_NAME_SEPARATOR - ctx.obj = {'db': config_db, 'state_db': state_db, 'prefix': prefix} + ctx.obj = {'prefix': prefix} + + asic_namespaces = multi_asic.get_namespace_list() + all_namespaces = asic_namespaces + if multi_asic.is_multi_asic(): + all_namespaces = [multi_asic_util.constants.DEFAULT_NAMESPACE] + asic_namespaces + ctx.obj["all_namespaces"] = all_namespaces + ctx.obj["asic_namespaces"] = asic_namespaces + ctx.obj["state_db"] = {} + ctx.obj["config_db"] = {} + for namespace in all_namespaces: + config_db = ConfigDBConnector(namespace=namespace, use_unix_socket_path=use_unix_socket_path) + config_db.connect(wait_for_init=False) + state_db = SonicV2Connector(namespace=namespace, use_unix_socket_path=use_unix_socket_path) + state_db.connect(state_db.STATE_DB, False) + ctx.obj["state_db"][namespace] = state_db + ctx.obj["config_db"][namespace] = config_db + @warm_restart.command('enable') +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.argument('module', metavar='', default='system', required=False) @click.pass_context -def warm_restart_enable(ctx, module): - state_db = ctx.obj['state_db'] - config_db = ctx.obj['db'] +def warm_restart_enable(ctx, namespace, module): + if namespace is not None: + if namespace not in ctx.obj["all_namespaces"]: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + namespaces = [namespace] if namespace else ctx.obj["all_namespaces"] + + config_db = ctx.obj["config_db"][multi_asic_util.constants.DEFAULT_NAMESPACE] feature_table = config_db.get_table('FEATURE') if module != 'system' and module not in feature_table: sys.exit('Feature {} is unknown'.format(module)) prefix = ctx.obj['prefix'] _hash = '{}{}'.format(prefix, module) - state_db.set(state_db.STATE_DB, _hash, 'enable', 'true') - state_db.close(state_db.STATE_DB) + + for namespace in namespaces: + state_db = ctx.obj["state_db"][namespace] + state_db.set(state_db.STATE_DB, _hash, 'enable', 'true') + state_db.close(state_db.STATE_DB) + @warm_restart.command('disable') +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.argument('module', metavar='', default='system', required=False) @click.pass_context -def warm_restart_disable(ctx, module): - state_db = ctx.obj['state_db'] - config_db = ctx.obj['db'] +def warm_restart_disable(ctx, namespace, module): + if namespace is not None: + if namespace not in ctx.obj["all_namespaces"]: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + namespaces = [namespace] if namespace else ctx.obj["all_namespaces"] + + config_db = ctx.obj["config_db"][multi_asic_util.constants.DEFAULT_NAMESPACE] feature_table = config_db.get_table('FEATURE') if module != 'system' and module not in feature_table: sys.exit('Feature {} is unknown'.format(module)) prefix = ctx.obj['prefix'] _hash = '{}{}'.format(prefix, module) - state_db.set(state_db.STATE_DB, _hash, 'enable', 'false') - state_db.close(state_db.STATE_DB) + + for namespace in namespaces: + state_db = ctx.obj["state_db"][namespace] + state_db.set(state_db.STATE_DB, _hash, 'enable', 'false') + state_db.close(state_db.STATE_DB) + @warm_restart.command('neighsyncd_timer') +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.argument('seconds', metavar='', required=True, type=int) @click.pass_context -def warm_restart_neighsyncd_timer(ctx, seconds): - db = ValidatedConfigDBConnector(ctx.obj['db']) +def warm_restart_neighsyncd_timer(ctx, namespace, seconds): + if namespace is not None: + if namespace not in ctx.obj["asic_namespaces"]: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + namespaces = [namespace] if namespace else ctx.obj["asic_namespaces"] + if ADHOC_VALIDATION: if seconds not in range(1, 9999): ctx.fail("neighsyncd warm restart timer must be in range 1-9999") - try: - db.mod_entry('WARM_RESTART', 'swss', {'neighsyncd_timer': seconds}) - except ValueError as e: - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + + for namespace in namespaces: + db = ValidatedConfigDBConnector(ctx.obj["config_db"][namespace]) + try: + db.mod_entry('WARM_RESTART', 'swss', {'neighsyncd_timer': seconds}) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + @warm_restart.command('bgp_timer') +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.argument('seconds', metavar='', required=True, type=int) @click.pass_context -def warm_restart_bgp_timer(ctx, seconds): - db = ValidatedConfigDBConnector(ctx.obj['db']) +def warm_restart_bgp_timer(ctx, namespace, seconds): + if namespace is not None: + if namespace not in ctx.obj["asic_namespaces"]: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + namespaces = [namespace] if namespace else ctx.obj["asic_namespaces"] + if ADHOC_VALIDATION: if seconds not in range(1, 3600): ctx.fail("bgp warm restart timer must be in range 1-3600") - try: - db.mod_entry('WARM_RESTART', 'bgp', {'bgp_timer': seconds}) - except ValueError as e: - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + + for namespace in namespaces: + db = ValidatedConfigDBConnector(ctx.obj["config_db"][namespace]) + try: + db.mod_entry('WARM_RESTART', 'bgp', {'bgp_timer': seconds}) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + @warm_restart.command('teamsyncd_timer') +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.argument('seconds', metavar='', required=True, type=int) @click.pass_context -def warm_restart_teamsyncd_timer(ctx, seconds): - db = ValidatedConfigDBConnector(ctx.obj['db']) +def warm_restart_teamsyncd_timer(ctx, namespace, seconds): + if namespace is not None: + if namespace not in ctx.obj["asic_namespaces"]: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + namespaces = [namespace] if namespace else ctx.obj["asic_namespaces"] + if ADHOC_VALIDATION: if seconds not in range(1, 3600): ctx.fail("teamsyncd warm restart timer must be in range 1-3600") - try: - db.mod_entry('WARM_RESTART', 'teamd', {'teamsyncd_timer': seconds}) - except ValueError as e: - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + + for namespace in namespaces: + db = ValidatedConfigDBConnector(ctx.obj["config_db"][namespace]) + try: + db.mod_entry('WARM_RESTART', 'teamd', {'teamsyncd_timer': seconds}) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + @warm_restart.command('bgp_eoiu') +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.argument('enable', metavar='', default='true', required=False, type=click.Choice(["true", "false"])) @click.pass_context -def warm_restart_bgp_eoiu(ctx, enable): - db = ValidatedConfigDBConnector(ctx.obj['db']) - try: - db.mod_entry('WARM_RESTART', 'bgp', {'bgp_eoiu': enable}) - except ValueError as e: - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) +def warm_restart_bgp_eoiu(ctx, namespace, enable): + if namespace is not None: + if namespace not in ctx.obj["asic_namespaces"]: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + namespaces = [namespace] if namespace else ctx.obj["asic_namespaces"] + + for namespace in namespaces: + db = ValidatedConfigDBConnector(ctx.obj["config_db"][namespace]) + try: + db.mod_entry('WARM_RESTART', 'bgp', {'bgp_eoiu': enable}) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + def vrf_add_management_vrf(config_db): """Enable management vrf in config DB""" diff --git a/show/warm_restart.py b/show/warm_restart.py index 2d0ea4b69c..d4df4a2bce 100644 --- a/show/warm_restart.py +++ b/show/warm_restart.py @@ -1,5 +1,7 @@ import click import utilities_common.cli as clicommon +import utilities_common.multi_asic as multi_asic_util +from sonic_py_common import multi_asic from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector from tabulate import tabulate @@ -11,14 +13,38 @@ def warm_restart(): @warm_restart.command() +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.option('-s', '--redis-unix-socket-path', help='unix socket path for redis connection') -def state(redis_unix_socket_path): +def state(namespace, redis_unix_socket_path): """Show warm restart state""" - kwargs = {} + if redis_unix_socket_path: - kwargs['unix_socket_path'] = redis_unix_socket_path + click.secho( + "Warning: '-s|--redis-unix-socket-path' has no effect and is left for compatibility", + fg="red", err=True) + + if namespace and namespace not in multi_asic.get_namespace_list(): + raise click.UsageError("Invalid namespace: {}".format(namespace)) + + asic_namespaces = multi_asic.get_namespace_list() + all_namespaces = asic_namespaces + if multi_asic.is_multi_asic(): + all_namespaces = [multi_asic_util.constants.DEFAULT_NAMESPACE] + asic_namespaces + if namespace is not None and namespace not in asic_namespaces: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + + namespaces = [namespace] if namespace else all_namespaces + + for namespace in namespaces: + if len(namespaces) > 1: + namespace_str = namespace or "global" + click.echo(f"\nFor namespace {namespace_str}:\n") + + show_warm_restart_state_for_namespace(namespace) - db = SonicV2Connector(host='127.0.0.1') + +def show_warm_restart_state_for_namespace(namespace): + db = SonicV2Connector(namespace=namespace) db.connect(db.STATE_DB, False) # Make one attempt only TABLE_NAME_SEPARATOR = '|' @@ -53,19 +79,42 @@ def remove_prefix(text, prefix): @warm_restart.command() +@click.option('--namespace', '-n', 'namespace', default=None, help='Namespace name') @click.option('-s', '--redis-unix-socket-path', help='unix socket path for redis connection') -def config(redis_unix_socket_path): +def config(namespace, redis_unix_socket_path): """Show warm restart config""" kwargs = {} if redis_unix_socket_path: kwargs['unix_socket_path'] = redis_unix_socket_path - config_db = ConfigDBConnector(**kwargs) + + if namespace is not None and redis_unix_socket_path: + raise click.UsageError("Cannot specify both namespace and redis unix socket path") + + asic_namespaces = multi_asic.get_namespace_list() + all_namespaces = asic_namespaces + if multi_asic.is_multi_asic(): + all_namespaces = [multi_asic_util.constants.DEFAULT_NAMESPACE] + asic_namespaces + if namespace is not None and namespace not in asic_namespaces: + raise click.UsageError("Invalid namespace: {}".format(namespace)) + + namespaces = [namespace] if namespace else all_namespaces + + for namespace in namespaces: + if len(namespaces) > 1: + namespace_str = namespace or "global" + click.echo(f"\nFor namespace {namespace_str}:\n") + + show_warm_restart_config_for_namespace(namespace, **kwargs) + + +def show_warm_restart_config_for_namespace(namespace, **kwargs): + config_db = ConfigDBConnector(namespace=namespace, **kwargs) config_db.connect(wait_for_init=False) data = config_db.get_table('WARM_RESTART') # Python dictionary keys() Method keys = list(data.keys()) - state_db = SonicV2Connector(host='127.0.0.1') + state_db = SonicV2Connector(namespace=namespace) state_db.connect(state_db.STATE_DB, False) # Make one attempt only TABLE_NAME_SEPARATOR = '|' prefix = 'WARM_RESTART_ENABLE_TABLE' + TABLE_NAME_SEPARATOR diff --git a/tests/config_test.py b/tests/config_test.py index f987b5701f..632bdbb110 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -3015,7 +3015,7 @@ def test_warm_restart_neighsyncd_timer_yang_validation(self): config.ADHOC_VALIDATION = False runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["neighsyncd_timer"], ["2000"], obj=obj) print(result.exit_code) @@ -3027,7 +3027,7 @@ def test_warm_restart_neighsyncd_timer(self): config.ADHOC_VALIDATION = True runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["neighsyncd_timer"], ["0"], obj=obj) print(result.exit_code) @@ -3041,7 +3041,7 @@ def test_warm_restart_bgp_timer_yang_validation(self): config.ADHOC_VALIDATION = False runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["bgp_timer"], ["2000"], obj=obj) print(result.exit_code) @@ -3053,7 +3053,7 @@ def test_warm_restart_bgp_timer(self): config.ADHOC_VALIDATION = True runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["bgp_timer"], ["0"], obj=obj) print(result.exit_code) @@ -3067,7 +3067,7 @@ def test_warm_restart_teamsyncd_timer_yang_validation(self): config.ADHOC_VALIDATION = False runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["teamsyncd_timer"], ["2000"], obj=obj) print(result.exit_code) @@ -3079,7 +3079,7 @@ def test_warm_restart_teamsyncd_timer(self): config.ADHOC_VALIDATION = True runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["teamsyncd_timer"], ["0"], obj=obj) print(result.exit_code) @@ -3093,7 +3093,7 @@ def test_warm_restart_bgp_eoiu_yang_validation(self): config.ADHOC_VALIDATION = False runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'config_db': {'': db.cfgdb}, 'asic_namespaces': ['']} result = runner.invoke(config.config.commands["warm_restart"].commands["bgp_eoiu"], ["true"], obj=obj) print(result.exit_code) diff --git a/tests/test_warm_restart.py b/tests/test_warm_restart.py new file mode 100644 index 0000000000..3e5276f7d4 --- /dev/null +++ b/tests/test_warm_restart.py @@ -0,0 +1,501 @@ +import pytest +import fnmatch +import textwrap + +from unittest.mock import patch, MagicMock +from config.main import config as config_cli +from show.main import cli as show_cli +from click.testing import CliRunner + + +@pytest.fixture +def configdbconnector_mock(): + class DB(MagicMock): + CONFIG_DB = "CONFIG_DB" + DBs = {} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + namespace = kwargs.get("namespace", "") + self.dbs = self.DBs.setdefault(namespace, {"CONFIG_DB": {}}) + + def mod_entry(self, table, key, value): + value = {k: str(v) for k, v in value.items()} + self.dbs[self.CONFIG_DB].setdefault(table, {})[key] = value + + def get_table(self, table): + return self.dbs[self.CONFIG_DB].get(table, {}) + + with patch("config.main.ConfigDBConnector", new=DB), \ + patch("show.warm_restart.ConfigDBConnector", new=DB): + yield DB + + +@pytest.fixture +def sonicv2connector_mock(): + class DB(MagicMock): + STATE_DB = "STATE_DB" + DBs = {} + + def __init__(self, *args, **kwargs): + namespace = kwargs.get("namespace", "") + super().__init__(*args, **kwargs) + self.dbs = self.DBs.setdefault(namespace, {"STATE_DB": {}}) + + def keys(self, db, hash): + return [key for key in self.dbs[db].keys() if fnmatch.fnmatch(key, hash)] + + def set(self, db, key, field, value): + self.dbs[db].setdefault(key, {})[field] = value + + def get(self, db, key, field): + return self.dbs[db].get(key, {}).get(field, None) + + def get_all(self, db, key): + return self.dbs[db][key] + + with patch("config.main.SonicV2Connector", new=DB), \ + patch("show.warm_restart.SonicV2Connector", new=DB): + yield DB + + +@pytest.fixture +def multi_asic(): + global_namespace = "" + asic_namespaces = ["asic0", "asic1", "asic2", "asic3"] + namespaces = [global_namespace] + asic_namespaces + with patch("config.main.multi_asic.is_multi_asic", return_value=True), \ + patch("config.main.multi_asic.get_namespace_list", return_value=asic_namespaces), \ + patch("show.warm_restart.multi_asic.is_multi_asic", return_value=True), \ + patch("show.warm_restart.multi_asic.get_namespace_list", return_value=asic_namespaces): + yield { + "namespaces": namespaces, + "asic_namespaces": asic_namespaces, + } + + +def test_config_warm_restart_enable(sonicv2connector_mock, configdbconnector_mock): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["enable"], + ) + state_db = sonicv2connector_mock() + assert result.exit_code == 0 + assert state_db.get(state_db.STATE_DB, + "WARM_RESTART_ENABLE_TABLE|system", "enable") == "true" + + +def test_config_warm_restart_disable(sonicv2connector_mock, configdbconnector_mock): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["disable"], + ) + state_db = sonicv2connector_mock() + assert result.exit_code == 0 + assert state_db.get( + state_db.STATE_DB, "WARM_RESTART_ENABLE_TABLE|system", "enable") == "false" + + +def test_config_warm_restart_enable_multi_asic(sonicv2connector_mock, configdbconnector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["enable"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["namespaces"]: + state_db = sonicv2connector_mock(namespace=namespace) + assert state_db.get( + state_db.STATE_DB, "WARM_RESTART_ENABLE_TABLE|system", "enable") == "true" + + +def test_config_warm_restart_disable_multi_asic(sonicv2connector_mock, configdbconnector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["disable"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["namespaces"]: + state_db = sonicv2connector_mock(namespace=namespace) + assert state_db.get( + state_db.STATE_DB, "WARM_RESTART_ENABLE_TABLE|system", "enable") == "false" + + +def test_config_warm_restart_disable_multi_asic_one_asic(sonicv2connector_mock, configdbconnector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["enable"], + ) + assert result.exit_code == 0 + result = runner.invoke( + config_cli.commands["warm_restart"], ["disable", "-n", "asic1"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + expected_value = "false" if namespace == "asic1" else "true" + state_db = sonicv2connector_mock(namespace=namespace) + assert state_db.get( + state_db.STATE_DB, "WARM_RESTART_ENABLE_TABLE|system", "enable") == expected_value + + +def test_config_warm_restart_neighsyncd_timer(configdbconnector_mock, sonicv2connector_mock): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["neighsyncd_timer", "180"], + ) + cfg_db = configdbconnector_mock() + assert result.exit_code == 0 + assert cfg_db.get_table("WARM_RESTART")[ + "swss"]["neighsyncd_timer"] == "180" + + +def test_config_warm_restart_neighsyncd_timer_multi_asic( + configdbconnector_mock, sonicv2connector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["neighsyncd_timer", "180"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + cfg_db = configdbconnector_mock(namespace=namespace) + assert cfg_db.get_table("WARM_RESTART")[ + "swss"]["neighsyncd_timer"] == "180" + + +def test_config_warm_restart_neighsyncd_timer_multi_asic_one_asic( + configdbconnector_mock, sonicv2connector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["neighsyncd_timer", "180"], + ) + assert result.exit_code == 0 + result = runner.invoke( + config_cli.commands["warm_restart"], [ + "neighsyncd_timer", "200", "-n", "asic1"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + expected_value = "200" if namespace == "asic1" else "180" + cfg_db = configdbconnector_mock(namespace=namespace) + assert cfg_db.get_table("WARM_RESTART")[ + "swss"]["neighsyncd_timer"] == expected_value + + +def test_config_warm_restart_bgp_timer(configdbconnector_mock, sonicv2connector_mock): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["bgp_timer", "180"], + ) + cfg_db = configdbconnector_mock() + assert result.exit_code == 0 + assert cfg_db.get_table("WARM_RESTART")["bgp"]["bgp_timer"] == "180" + + +def test_config_warm_restart_bgp_timer_multi_asic(configdbconnector_mock, sonicv2connector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["bgp_timer", "180"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + cfg_db = configdbconnector_mock(namespace=namespace) + assert cfg_db.get_table("WARM_RESTART")["bgp"]["bgp_timer"] == "180" + + +def test_config_warm_restart_bgp_timer_multi_asic_one_asic(configdbconnector_mock, sonicv2connector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["bgp_timer", "180"], + ) + assert result.exit_code == 0 + result = runner.invoke( + config_cli.commands["warm_restart"], [ + "bgp_timer", "200", "-n", "asic1"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + expected_value = "200" if namespace == "asic1" else "180" + cfg_db = configdbconnector_mock(namespace=namespace) + assert cfg_db.get_table("WARM_RESTART")[ + "bgp"]["bgp_timer"] == expected_value + + +def test_config_warm_restart_enable_bgp_eoiu(configdbconnector_mock, sonicv2connector_mock): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["bgp_eoiu", "true"], + ) + cfg_db = configdbconnector_mock() + assert result.exit_code == 0 + assert cfg_db.get_table("WARM_RESTART")["bgp"]["bgp_eoiu"] == "true" + + +def test_config_warm_restart_disable_bgp_eoiu_multi_asic(configdbconnector_mock, sonicv2connector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["bgp_eoiu", "true"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + cfg_db = configdbconnector_mock(namespace=namespace) + assert cfg_db.get_table("WARM_RESTART")["bgp"]["bgp_eoiu"] == "true" + + +def test_config_warm_restart_disable_bgp_eoiu_multi_asic_one_asic( + configdbconnector_mock, sonicv2connector_mock, multi_asic): + runner = CliRunner() + result = runner.invoke( + config_cli.commands["warm_restart"], ["bgp_eoiu", "true"], + ) + assert result.exit_code == 0 + result = runner.invoke( + config_cli.commands["warm_restart"], [ + "bgp_eoiu", "false", "-n", "asic1"], + ) + assert result.exit_code == 0 + for namespace in multi_asic["asic_namespaces"]: + expected_value = "false" if namespace == "asic1" else "true" + cfg_db = configdbconnector_mock(namespace=namespace) + assert cfg_db.get_table("WARM_RESTART")[ + "bgp"]["bgp_eoiu"] == expected_value + + +@pytest.fixture +def setup_state_db(sonicv2connector_mock): + state_db = sonicv2connector_mock() + state_db.set(state_db.STATE_DB, "WARM_RESTART_ENABLE_TABLE|system", "enable", "true") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|orchagent", "state", "restored") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|syncd", "state", "reconciled") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|orchagent", "restore_count", "1") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|syncd", "restore_count", "1") + + +@pytest.fixture +def setup_state_db_multi_asic(sonicv2connector_mock, multi_asic): + for namespace in multi_asic["asic_namespaces"]: + state_db = sonicv2connector_mock(namespace=namespace) + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|orchagent", "state", "restored") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|syncd", "state", "reconciled") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|orchagent", "restore_count", "1") + state_db.set(state_db.STATE_DB, "WARM_RESTART_TABLE|syncd", "restore_count", "1") + + +@pytest.fixture +def setup_config_db(configdbconnector_mock): + cfg_db = configdbconnector_mock() + cfg_db.mod_entry("WARM_RESTART", "teamd", {"teamsyncd_timer": "120"}) + + +@pytest.fixture +def setup_config_db_multi_asic(configdbconnector_mock, multi_asic): + for namespace in multi_asic["asic_namespaces"]: + cfg_db = configdbconnector_mock(namespace=namespace) + cfg_db.mod_entry("WARM_RESTART", "teamd", {"teamsyncd_timer": "180"}) + + +def test_show_warm_restart_state(setup_state_db): + runner = CliRunner() + result = runner.invoke( + show_cli.commands["warm_restart"], ["state"] + ) + assert result.exit_code == 0 + assert result.output == textwrap.dedent("""\ + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + """) + + +def test_show_warm_restart_state_multi_asic(setup_state_db_multi_asic): + runner = CliRunner() + result = runner.invoke( + show_cli.commands["warm_restart"], ["state"] + ) + assert result.exit_code == 0 + assert result.output == textwrap.dedent("""\ + + For namespace global: + + name restore_count state + ------ --------------- ------- + + For namespace asic0: + + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + + For namespace asic1: + + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + + For namespace asic2: + + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + + For namespace asic3: + + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + """) + + +def test_show_warm_restart_state_multi_asic_show_namespace(setup_state_db_multi_asic): + runner = CliRunner() + arguments = ["-n", "asic1"] + result = runner.invoke( + show_cli.commands["warm_restart"], ["state"] + arguments + ) + assert result.exit_code == 0 + assert result.output == textwrap.dedent("""\ + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + """) + + +def test_show_warm_restart_state_invalid_namespace(setup_state_db_multi_asic): + runner = CliRunner() + arguments = ["-n", "asicX"] + result = runner.invoke( + show_cli.commands["warm_restart"], ["state"] + arguments, + ) + expected_output = textwrap.dedent("""\ + Usage: warm_restart state [OPTIONS] + Try 'warm_restart state --help' for help. + + Error: Invalid namespace: asicX + """) + assert result.exit_code == 2 + assert result.output == expected_output + + +def test_show_warm_restart_state_unix_sock_usage(setup_state_db): + runner = CliRunner() + arguments = ["-s", "/var/run/redis/redis.sock"] + result = runner.invoke( + show_cli.commands["warm_restart"], ["state"] + arguments, + ) + expected_output = textwrap.dedent("""\ + Warning: '-s|--redis-unix-socket-path' has no effect and is left for compatibility + name restore_count state + --------- --------------- ---------- + orchagent 1 restored + syncd 1 reconciled + """) + assert result.exit_code == 0 + assert result.output == expected_output + + +def test_show_warm_restart_config(setup_state_db, setup_config_db): + runner = CliRunner() + result = runner.invoke( + show_cli.commands["warm_restart"], ["config"], + ) + expected_output = textwrap.dedent("""\ + name enable timer_name timer_duration eoiu_enable + ------ -------- --------------- ---------------- ------------- + teamd false teamsyncd_timer 120 NULL + system true NULL NULL NULL + """) + assert result.exit_code == 0 + assert result.output == expected_output + + +def test_show_warm_restart_config_multi_asic(setup_state_db_multi_asic, + setup_config_db_multi_asic): + runner = CliRunner() + result = runner.invoke( + show_cli.commands["warm_restart"], ["config"], + ) + expected_output = textwrap.dedent("""\ + + For namespace global: + + name enable timer_name timer_duration eoiu_enable + ------ -------- ------------ ---------------- ------------- + + For namespace asic0: + + name enable timer_name timer_duration eoiu_enable + ------ -------- --------------- ---------------- ------------- + teamd false teamsyncd_timer 180 NULL + + For namespace asic1: + + name enable timer_name timer_duration eoiu_enable + ------ -------- --------------- ---------------- ------------- + teamd false teamsyncd_timer 180 NULL + + For namespace asic2: + + name enable timer_name timer_duration eoiu_enable + ------ -------- --------------- ---------------- ------------- + teamd false teamsyncd_timer 180 NULL + + For namespace asic3: + + name enable timer_name timer_duration eoiu_enable + ------ -------- --------------- ---------------- ------------- + teamd false teamsyncd_timer 180 NULL + """) + assert result.exit_code == 0 + assert result.output == expected_output + + +def test_show_warm_restart_config_multi_asic_show_namespace(setup_state_db_multi_asic, + setup_config_db_multi_asic): + runner = CliRunner() + arguments = ["-n", "asic1"] + result = runner.invoke( + show_cli.commands["warm_restart"], ["config"] + arguments, + ) + expected_output = textwrap.dedent("""\ + name enable timer_name timer_duration eoiu_enable + ------ -------- --------------- ---------------- ------------- + teamd false teamsyncd_timer 180 NULL + """) + assert result.exit_code == 0 + assert result.output == expected_output + + +def test_show_warm_restart_config_invalid_namespace(setup_state_db_multi_asic): + runner = CliRunner() + arguments = ["-n", "asicX"] + result = runner.invoke( + show_cli.commands["warm_restart"], ["config"] + arguments, + ) + expected_output = textwrap.dedent("""\ + Usage: warm_restart config [OPTIONS] + Try 'warm_restart config --help' for help. + + Error: Invalid namespace: asicX + """) + assert result.exit_code == 2 + assert result.output == expected_output + + +def test_show_warm_restart_config_unix_sock_usage_and_namespace(setup_state_db_multi_asic): + runner = CliRunner() + arguments = ["-s", "/var/run/redis/redis.sock", "-n", "asic1"] + result = runner.invoke( + show_cli.commands["warm_restart"], ["config"] + arguments, + ) + expected_output = textwrap.dedent("""\ + Usage: warm_restart config [OPTIONS] + Try 'warm_restart config --help' for help. + + Error: Cannot specify both namespace and redis unix socket path + """) + assert result.exit_code == 2 + assert result.output == expected_output