From 2dabdc6781859da4aaff84501ccb77a9f16bd6d0 Mon Sep 17 00:00:00 2001 From: Sonic Build Admin Date: Wed, 1 Jul 2026 05:23:54 +0000 Subject: [PATCH] [config reload] Skip swss readiness check when swss is not present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What I did `config reload` (without `-f`) fails on platforms that do not ship the swss container (e.g. BMC) with `swss container is not ready. Retry later or use -f to avoid system checks`. The readiness gate waits on a `swss.service` that does not exist on such images, so reload can never proceed without `-f`. This makes the gate skip the wait when swss is not present. #### How I did it In `_per_namespace_swss_ready()`, check the service's systemd `LoadState` first. If it is `not-found` (service not built into the image) or `masked`, return ready — there is no swss to wait for. Platforms that ship swss read `LoadState=loaded`, so the existing active/120s-settle checks are unchanged. Added unit tests for the not-found/masked paths and regression guards for the loaded/inactive and active/settle behavior. #### How to verify it - `pytest tests/config_test.py::TestSwssReady` - On a platform without swss (`swss.service` is `not-found`): `config reload -y` proceeds instead of aborting. - On a switch that ships swss: behavior is unchanged (still waits until swss has been active for >120s). #### Previous command output (if the output of a command-line utility has changed) ``` root@sonic:~# config reload -y Acquired lock on /etc/sonic/reload.lock swss container is not ready. Retry later or use -f to avoid system checks Released lock on /etc/sonic/reload.lock ``` #### New command output (if the output of a command-line utility has changed) ``` root@sonic:~# config reload -y Acquired lock on /etc/sonic/reload.lock Running command: ... # reload proceeds normally Released lock on /etc/sonic/reload.lock ``` Signed-off-by: Sonic Build Admin --- config/main.py | 5 ++++ tests/config_test.py | 62 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/config/main.py b/config/main.py index 45dce1b13..7cdea6fbc 100644 --- a/config/main.py +++ b/config/main.py @@ -1206,6 +1206,11 @@ def _restart_services(): reset_mgmt_interface_if_usb_not_running() def _per_namespace_swss_ready(service_name): + out, _ = clicommon.run_command( + ['systemctl', 'show', str(service_name), '--property', 'LoadState', '--value'], return_cmd=True) + if out.strip() in ("not-found", "masked"): + # swss not present on this platform (e.g. BMC): nothing to wait for. + return True out, _ = clicommon.run_command(['systemctl', 'show', str(service_name), '--property', 'ActiveState', '--value'], return_cmd=True) if out.strip() != "active": return False diff --git a/tests/config_test.py b/tests/config_test.py index c0e29c70a..5ffdb5edb 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -5290,3 +5290,65 @@ def test_banner_motd(self): @classmethod def teardown_class(cls): print('TEARDOWN') + + +class TestSwssReady(object): + """Tests for the 'config reload' swss readiness gate on platforms without + swss (e.g. BMC), plus regression guards for normal switches.""" + + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + import config.main + importlib.reload(config.main) + + @classmethod + def teardown_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + + def test_swss_ready_when_service_not_found(self): + # BMC: swss not built in -> not-found -> ready, only LoadState queried. + with mock.patch('config.main.clicommon.run_command', + mock.MagicMock(return_value=("not-found", 0))) as mock_run: + assert config._per_namespace_swss_ready("swss.service") is True + assert mock_run.call_count == 1 + + def test_swss_ready_when_service_masked(self): + # Masked service -> ready, same as not-found. + with mock.patch('config.main.clicommon.run_command', + mock.MagicMock(return_value=("masked", 0))) as mock_run: + assert config._per_namespace_swss_ready("swss.service") is True + assert mock_run.call_count == 1 + + def test_swss_not_ready_when_loaded_but_inactive(self): + # Switch still booting: loaded but inactive -> not ready. + side_effect = [("loaded", 0), ("inactive", 0)] + with mock.patch('config.main.clicommon.run_command', + mock.MagicMock(side_effect=side_effect)): + assert config._per_namespace_swss_ready("swss.service") is False + + def test_swss_not_ready_when_active_but_not_settled(self): + # Active for < 120s -> not ready. + side_effect = [("loaded", 0), ("active", 0), ("1000000000", 0)] # up = 1000s + with mock.patch('config.main.clicommon.run_command', + mock.MagicMock(side_effect=side_effect)), \ + mock.patch('config.main.time.monotonic', + mock.MagicMock(return_value=1050.0)): # 50s < 120s + assert config._per_namespace_swss_ready("swss.service") is False + + def test_swss_ready_when_active_and_settled(self): + # Active for > 120s -> ready. + side_effect = [("loaded", 0), ("active", 0), ("1000000000", 0)] # up = 1000s + with mock.patch('config.main.clicommon.run_command', + mock.MagicMock(side_effect=side_effect)), \ + mock.patch('config.main.time.monotonic', + mock.MagicMock(return_value=2000.0)): # 1000s > 120s + assert config._per_namespace_swss_ready("swss.service") is True + + def test_swss_ready_single_asic_not_found(self): + # _swss_ready() end-to-end, single-ASIC, swss not-found. + with mock.patch('config.main.multi_asic.get_num_asics', + mock.MagicMock(return_value=1)), \ + mock.patch('config.main.clicommon.run_command', + mock.MagicMock(return_value=("not-found", 0))): + assert config._swss_ready() is True