diff --git a/software/software/software_controller.py b/software/software/software_controller.py index f5ce8c976..a43a10268 100644 --- a/software/software/software_controller.py +++ b/software/software/software_controller.py @@ -106,6 +106,7 @@ from software.sysinv_utils import are_all_hosts_unlocked_and_online from software.sysinv_utils import get_system_info from software.sysinv_utils import get_oot_drivers from software.sysinv_utils import trigger_evaluate_apps_reapply +from software.sysinv_utils import trigger_vim_host_audit from software.db.api import get_instance @@ -1080,6 +1081,40 @@ class PatchController(PatchService): else: self._update_state_to_peer() + def _notify_vim_on_state_change(self, target_state): + """Notify VIM of state change. + + This method will notify VIM when one of the following state changes is made: + - start-done + - start-failed + - activate-done + - activate-failed + - activate-rollback-done + - activate-rollback-failed + + If new async states are added they should be added here. + + Args: + target_state: The new deployment state to notify VIM about + """ + + if self.pre_bootstrap: + return + + if target_state not in [ + DEPLOY_STATES.START_DONE, + DEPLOY_STATES.START_FAILED, + DEPLOY_STATES.ACTIVATE_DONE, + DEPLOY_STATES.ACTIVATE_FAILED, + DEPLOY_STATES.ACTIVATE_ROLLBACK_DONE, + DEPLOY_STATES.ACTIVATE_ROLLBACK_FAILED, + ]: + return + + # Get local hostname + LOG.info("Notifying VIM of state change: %s", target_state) + trigger_vim_host_audit(socket.gethostname()) + def register_deploy_state_change_listeners(self): # data sync listener DeployState.register_event_listener(self._state_changed_sync) @@ -1089,6 +1124,10 @@ class PatchController(PatchService): DeployState.register_event_listener(ReleaseState.deploy_updated) DeployState.register_event_listener(self.create_clean_up_deployment_alarm) + # VIM notifications + DeployState.register_event_listener(self._notify_vim_on_state_change) + # TODO(jkraitbe): Add host-deploy when that becomes async + @property def release_collection(self): swrc = get_SWReleaseCollection() diff --git a/software/software/sysinv_utils.py b/software/software/sysinv_utils.py index 5e3e40305..fb2fdf72f 100644 --- a/software/software/sysinv_utils.py +++ b/software/software/sysinv_utils.py @@ -107,6 +107,18 @@ def update_host_sw_version(hostname, sw_version): raise +def trigger_vim_host_audit(hostname): + """Trigger for the sysinv function vim_host_audit.""" + token, endpoint = utils.get_endpoints_token() + sysinv_client = get_sysinv_client(token=token, endpoint=endpoint) + try: + host = sysinv_client.ihost.get(hostname) + sysinv_client.ihost.vim_host_audit(host.uuid) + except Exception as err: + LOG.error("Failed to trigger VIM host audit for %s: %s", hostname, err) + raise + + def get_service_parameter(service=None, section=None, name=None): """return a list of dictionaries with keys uuid, service, section, name, personality, resource, value diff --git a/software/software/tests/test_software_controller_vim_notification.py b/software/software/tests/test_software_controller_vim_notification.py new file mode 100644 index 000000000..6024c70dc --- /dev/null +++ b/software/software/tests/test_software_controller_vim_notification.py @@ -0,0 +1,111 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (c) 2025 Wind River Systems, Inc. +# + +import unittest +from unittest.mock import MagicMock +from unittest.mock import patch + +from software.software_controller import PatchController +from software.states import DEPLOY_STATES + +# This import has to be first +from software.tests import base # pylint: disable=unused-import # noqa: F401 + + +class TestSoftwareControllerVimNotification(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + @patch("software.software_controller.PatchController.__init__", return_value=None) + @patch("software.software_controller.trigger_vim_host_audit") + @patch("socket.gethostname", return_value="controller-0") + def test_notify_vim_on_state_change_supported_states( + self, mock_gethostname, mock_trigger_vim_host_audit, mock_init + ): # pylint: disable=unused-argument + """Test that VIM is notified for supported states.""" + controller = PatchController() + controller.pre_bootstrap = False + + # Test all supported states + supported_states = [ + DEPLOY_STATES.START_DONE, + DEPLOY_STATES.START_FAILED, + DEPLOY_STATES.ACTIVATE_DONE, + DEPLOY_STATES.ACTIVATE_FAILED, + DEPLOY_STATES.ACTIVATE_ROLLBACK_DONE, + DEPLOY_STATES.ACTIVATE_ROLLBACK_FAILED, + ] + + for state in supported_states: + mock_trigger_vim_host_audit.reset_mock() + # pylint: disable=protected-access + controller._notify_vim_on_state_change(state) + mock_gethostname.assert_called_with() + mock_trigger_vim_host_audit.assert_called_once_with("controller-0") + + @patch("software.software_controller.PatchController.__init__", return_value=None) + @patch("software.software_controller.trigger_vim_host_audit") + @patch("socket.gethostname") + def test_notify_vim_on_state_change_unsupported_states( + self, mock_gethostname, mock_trigger_vim_host_audit, mock_init + ): # pylint: disable=unused-argument + """Test that VIM is not notified for unsupported states.""" + controller = PatchController() + controller.pre_bootstrap = False + + # Test some unsupported states + unsupported_states = [ + "HELLO?", + DEPLOY_STATES.START, + DEPLOY_STATES.ACTIVATE, + ] + + for state in unsupported_states: + # pylint: disable=protected-access + controller._notify_vim_on_state_change(state) + mock_gethostname.assert_not_called() + mock_trigger_vim_host_audit.assert_not_called() + + @patch("software.software_controller.PatchController.__init__", return_value=None) + @patch("software.software_controller.DeployState") + def test_register_deploy_state_change_listeners( + self, mock_deploy_state, mock_init + ): # pylint: disable=unused-argument + """Test that the VIM notification listener is registered.""" + controller = PatchController() + + # Mock other methods that are called during registration + controller._state_changed_sync = MagicMock() # pylint: disable=protected-access + # pylint: disable=protected-access + controller._state_changed_notify = MagicMock() + controller.create_clean_up_deployment_alarm = MagicMock() + + # Call the method + controller.register_deploy_state_change_listeners() + + # Verify that _notify_vim_on_state_change is registered as a listener + # pylint: disable=protected-access + mock_deploy_state.register_event_listener.assert_any_call( + controller._notify_vim_on_state_change + ) + + @patch("software.software_controller.PatchController.__init__", return_value=None) + @patch("software.software_controller.trigger_vim_host_audit") + @patch("socket.gethostname") + def test_notify_vim_on_state_change_prebootstrap( + self, mock_gethostname, mock_trigger_vim_host_audit, mock_init + ): # pylint: disable=unused-argument + """Test that VIM is not notified during prebootstrap.""" + controller = PatchController() + controller.pre_bootstrap = True + + # pylint: disable=protected-access + controller._notify_vim_on_state_change(DEPLOY_STATES.START_DONE) + mock_gethostname.assert_not_called() + mock_trigger_vim_host_audit.assert_not_called() diff --git a/software/software/tests/test_sysinv_utils.py b/software/software/tests/test_sysinv_utils.py new file mode 100644 index 000000000..c98b4540e --- /dev/null +++ b/software/software/tests/test_sysinv_utils.py @@ -0,0 +1,63 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (c) 2025 Wind River Systems, Inc. +# + +import unittest +from unittest.mock import MagicMock +from unittest.mock import patch + +from software.sysinv_utils import trigger_vim_host_audit + + +class TestSysinvUtils(unittest.TestCase): + HOSTNAME = "test-host" + HOST_UUID = "test-uuid" + + def setUp(self): + # Create shared mock host + self.mock_host = MagicMock() + self.mock_host.hostname = self.HOSTNAME + self.mock_host.uuid = self.HOST_UUID + + @patch("software.sysinv_utils.utils.get_endpoints_token") + @patch("software.sysinv_utils.get_sysinv_client") + @patch("software.sysinv_utils.get_ihost_list") + def test_trigger_vim_host_audit( + self, mock_get_ihost_list, mock_get_sysinv_client, mock_get_endpoints_token + ): + mock_sysinv_client = MagicMock() + mock_get_sysinv_client.return_value = mock_sysinv_client + mock_sysinv_client.ihost.get.return_value = self.mock_host + mock_get_endpoints_token.return_value = ("fake_token", "fake_endpoint") + mock_get_ihost_list.return_value = [self.mock_host] + + trigger_vim_host_audit(self.HOSTNAME) + + mock_get_endpoints_token.assert_called_once() + mock_get_sysinv_client.assert_called_once_with( + token="fake_token", endpoint="fake_endpoint" + ) + mock_sysinv_client.ihost.get.assert_called_once_with(self.HOSTNAME) + mock_sysinv_client.ihost.vim_host_audit.assert_called_once_with(self.HOST_UUID) + + @patch("software.sysinv_utils.utils.get_endpoints_token") + @patch("software.sysinv_utils.get_sysinv_client") + @patch("software.sysinv_utils.get_ihost_list") + def test_trigger_vim_host_audit_sysinv_call_fails( + self, mock_get_ihost_list, mock_get_sysinv_client, mock_get_endpoints_token + ): + mock_sysinv_client = MagicMock() + mock_get_sysinv_client.return_value = mock_sysinv_client + mock_get_endpoints_token.return_value = ("fake_token", "fake_endpoint") + mock_get_ihost_list.return_value = [self.mock_host] + + # Configure vim_host_audit to raise an exception + mock_sysinv_client.ihost.vim_host_audit.side_effect = Exception( + "VIM audit failed" + ) + + msg = "Failed to trigger VIM host audit: VIM audit failed" + with self.assertRaises(Exception, msg=msg): # noqa: H202 + trigger_vim_host_audit(self.HOSTNAME) diff --git a/software/tox.ini b/software/tox.ini index 890cc1ba5..b161b7f21 100644 --- a/software/tox.ini +++ b/software/tox.ini @@ -37,6 +37,7 @@ deps = -r{toxinidir}/test-requirements.txt -e{[tox]stxdir}/fault/fm-api/source -e{[tox]stxdir}/config/tsconfig/tsconfig + -e{[tox]stxdir}/update/software -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/starlingx/root/raw/branch/master/build-tools/requirements/debian/upper-constraints.txt} passenv =