Notify vim on state change

USM has several async operations.  This change allows software to notify
VIM when those async operations complete.

It does this by using the existing listener functionality to trigger a
host-audit event in VIM via the sysinv API.

TEST PLAN
PASS: AIO-SX patch upgrade
PASS: AIO-SX patch downgrade
PASS: AIO-SX patch upgrade, pre-bootstrap
PASS: AIO-DX patch upgrade

Story: 2011045
Task: 52380
Depends-On: https://review.opendev.org/c/starlingx/config/+/952590
Change-Id: Id7b22f58040cbbf02a2e616a529a2b72a3970e80
Signed-off-by: Joshua Kraitberg <joshua.kraitberg@windriver.com>
This commit is contained in:
Joshua Kraitberg
2025-06-13 12:22:29 -04:00
parent 45d22a068e
commit 9ce9a82cdb
5 changed files with 226 additions and 0 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 =