diff --git a/counterpoll/main.py b/counterpoll/main.py index c3be705f0..443024cf3 100755 --- a/counterpoll/main.py +++ b/counterpoll/main.py @@ -1,11 +1,13 @@ import click import os -from sonic_py_common import device_info, multi_asic +from sonic_py_common import device_info, multi_asic, logger from tabulate import tabulate from flow_counter_util.route import exit_if_route_flow_counter_not_support from swsscommon.swsscommon import ConfigDBConnector, SonicDBConfig from swsscommon.swsscommon import CFG_FLEX_COUNTER_TABLE_NAME as CFG_FLEX_COUNTER_TABLE +log = logger.Logger("counterpoll") + BUFFER_POOL_WATERMARK = "BUFFER_POOL_WATERMARK" PORT_BUFFER_DROP = "PORT_BUFFER_DROP" PORT_PHY_ATTR = "PORT_PHY_ATTR" @@ -870,8 +872,18 @@ def register_dynamic_commands(cmds): """ Dynamically register commands based on condition callback. """ - db = ConfigDBConnector() - db.connect() + try: + db = ConfigDBConnector() + db.connect() + except RuntimeError as e: + # Runs at import time. swsscommon raises RuntimeError when CONFIG_DB is + # unreachable (e.g. build-time completion generation in a redis-less + # container). Skip DPU-only dynamic commands rather than fail the import, + # else the completion file is never generated. + log.log_warning( + "CONFIG_DB not available at import, skipping DPU dynamic commands " + "(eni/ha_set): {}".format(e)) + return for cmd, cb in cmds: if cb(db): cli.add_command(cmd) diff --git a/tests/counterpoll_test.py b/tests/counterpoll_test.py index 3bb636789..7d145b4f3 100644 --- a/tests/counterpoll_test.py +++ b/tests/counterpoll_test.py @@ -543,6 +543,28 @@ def test_tunnel_interval(self): table = db.cfgdb.get_table("FLEX_COUNTER_TABLE") assert test_interval == table["TUNNEL"]["POLL_INTERVAL"] + def test_register_dynamic_commands_survives_db_connect_failure(self): + # Importing counterpoll.main must not require a live CONFIG_DB. The + # build-time bash-completion generator imports every CLI module in a + # redis-less container; if the connect failure (RuntimeError) + # propagates from register_dynamic_commands (which runs at import), the + # generator drops the counterpoll completion file and "counterpoll " + # stops working. + mock_conn = mock.Mock() + mock_conn.connect.side_effect = RuntimeError("Unable to connect to redis") + with mock.patch.object(counterpoll, "ConfigDBConnector", return_value=mock_conn): + # Must not raise even though connect() fails. + counterpoll.register_dynamic_commands(counterpoll.dynamic_commands) + + def test_register_dynamic_commands_propagates_unexpected_error(self): + # Only swsscommon connection failures (RuntimeError) are tolerated; + # unrelated errors must still surface rather than be silently swallowed. + mock_conn = mock.Mock() + mock_conn.connect.side_effect = ValueError("unexpected") + with mock.patch.object(counterpoll, "ConfigDBConnector", return_value=mock_conn): + with pytest.raises(ValueError): + counterpoll.register_dynamic_commands(counterpoll.dynamic_commands) + @classmethod def teardown_class(cls): print("TEARDOWN")