Merge "Added VIM host-audit to sysinv API/CLI"

This commit is contained in:
Zuul
2025-07-09 14:41:16 +00:00
committed by Gerrit Code Review
15 changed files with 379 additions and 6 deletions

View File

@@ -13683,3 +13683,55 @@ Will reply with updated kernel value
"kernel_provisioned": "lowlatency", "kernel_provisioned": "lowlatency",
"kernel_running": "standard" "kernel_running": "standard"
} }
-------------------------
Host VIM Actions
-------------------------
These APIs allow the user to trigger actions in VIM.
Supported actions:
- host-audit
********************
Trigger Action
********************
.. rest_method:: POST /v1/ihosts/{ihost_uuid}/vim
**Normal response codes**
200
**Error response codes**
computeFault (400, 500, ...), serviceUnavailable (503),
unauthorized (401), forbidden (403), itemNotFound (404)
**Request parameters**
.. csv-table::
:header: "Parameter", "Style", "Type", "Description"
:widths: 20, 20, 20, 60
"ihost_uuid", "URI", "csapi:UUID", "The unique identifier of the host"
"vim_event", "plain", "xsd:string", "The action to trigger (host-audit)"
**Response parameters**
.. csv-table::
:header: "Parameter", "Style", "Type", "Description"
:widths: 20, 20, 20, 60
"ihost_uuid", "plain", "csapi:UUID", "The unique identifier of the host"
"hostname", "plain", "xsd:string", "The host name"
"vim_event", "plain", "xsd:string", "The action that was triggered"
::
{
"ihost_uuid": "e551b1f0-ab6d-43a9-8eb1-05c39025a161",
"hostname": "controller-0",
"vim_event": "host-audit",
}

View File

@@ -15,7 +15,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
# #
# Copyright (c) 2013-2023 Wind River Systems, Inc. # Copyright (c) 2013-2023,2025 Wind River Systems, Inc.
# #
@@ -61,6 +61,7 @@ KERNEL = {'ihost_uuid': IHOST['uuid'],
UPDATED_KERNEL = copy.deepcopy(KERNEL) UPDATED_KERNEL = copy.deepcopy(KERNEL)
NEW_KERNEL = 'lowlatency' NEW_KERNEL = 'lowlatency'
UPDATED_KERNEL['kernel_provisioned'] = NEW_KERNEL UPDATED_KERNEL['kernel_provisioned'] = NEW_KERNEL
VIM_HOST_AUDIT_RESPONSE = {"vim_event": "host-audit"}
fixtures = { fixtures = {
'/v1/ihosts': '/v1/ihosts':
@@ -107,6 +108,13 @@ fixtures = {
UPDATED_KERNEL, UPDATED_KERNEL,
), ),
}, },
'/v1/ihosts/%s/vim' % IHOST['uuid']:
{
'POST': (
{},
VIM_HOST_AUDIT_RESPONSE,
),
},
} }
@@ -182,3 +190,11 @@ class HostManagerTest(testtools.TestCase):
self.assertEqual(self.api.calls, expect) self.assertEqual(self.api.calls, expect)
self.assertEqual(kernel.kernel_provisioned, 'standard') self.assertEqual(kernel.kernel_provisioned, 'standard')
self.assertEqual(kernel.kernel_running, 'standard') self.assertEqual(kernel.kernel_running, 'standard')
def test_vim_host_audit(self):
self.mgr.vim_host_audit(hostid=IHOST['uuid'])
response = {"vim_event": "host-audit"}
expect = [
('POST', f'/v1/ihosts/{IHOST["uuid"]}/vim', {}, response),
]
self.assertEqual(expect, self.api.calls)

View File

@@ -250,3 +250,11 @@ class HostTest(test_shell.ShellTest):
FAKE_KERNEL['kernel_provisioned']) FAKE_KERNEL['kernel_provisioned'])
self.assertEqual(kernel['kernel_running'], self.assertEqual(kernel['kernel_running'],
FAKE_KERNEL['kernel_running']) FAKE_KERNEL['kernel_running'])
@mock.patch('cgtsclient.v1.ihost.ihostManager.vim_host_audit')
def test_vim_host_audit(self, mock_vim_host_audit):
self.make_env()
mock_vim_host_audit.return_value = None
results = self.shell(f"vim-host-audit {FAKE_IHOST['hostname']}")
self.assertIn("Host audit initiated successfully", results)
mock_vim_host_audit.assert_called_once_with(FAKE_IHOST['uuid'])

View File

@@ -1,5 +1,5 @@
# #
# Copyright (c) 2013-2024 Wind River Systems, Inc. # Copyright (c) 2013-2025 Wind River Systems, Inc.
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
@@ -686,3 +686,17 @@ def do_host_kernel_show(cc, args):
except exc.HTTPNotFound: except exc.HTTPNotFound:
raise exc.CommandError('Host not found: %s' % args.hostnameorid) raise exc.CommandError('Host not found: %s' % args.hostnameorid)
_print_kernel_show(kernel, args.format) _print_kernel_show(kernel, args.format)
@utils.arg('hostnameorid', metavar='<hostname or id>',
help="Name or ID of host")
def do_vim_host_audit(cc, args):
"""Perform host audit operation on specified host."""
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
try:
cc.ihost.vim_host_audit(ihost.uuid)
print(f"Host audit initiated successfully: {ihost.hostname}")
except exc.HTTPNotFound:
print("Host audit failed: host not found")
except Exception as e:
print(f"Host audit failed: {e}")

View File

@@ -1,5 +1,5 @@
# #
# Copyright (c) 2013-2023 Wind River Systems, Inc. # Copyright (c) 2013-2023,2025 Wind River Systems, Inc.
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
@@ -11,6 +11,7 @@ from cgtsclient.common import base
from cgtsclient.common import utils from cgtsclient.common import utils
from cgtsclient import exc from cgtsclient import exc
from cgtsclient.v1 import icpu from cgtsclient.v1 import icpu
from sysinv.common import constants
CREATION_ATTRIBUTES = ['hostname', 'personality', 'subfunctions', 'mgmt_mac', CREATION_ATTRIBUTES = ['hostname', 'personality', 'subfunctions', 'mgmt_mac',
@@ -32,6 +33,11 @@ class ihost_kernel(base.Resource):
return "<kernel %s>" % self._info return "<kernel %s>" % self._info
class ihost_vim(base.Resource):
def __repr__(self):
return "<vim %s>" % self._info
class ihostManager(base.Manager): class ihostManager(base.Manager):
resource_class = ihost resource_class = ihost
@@ -152,6 +158,13 @@ class ihostManager(base.Manager):
resp, body = self.api.json_request('GET', url) resp, body = self.api.json_request('GET', url)
return ihost_kernel(self, body) return ihost_kernel(self, body)
def vim_host_audit(self, hostid):
# path = self._path(hostid) + "/vim"
url = self._path(hostid) + "/vim"
body = {"vim_event": constants.HOST_AUDIT_ACTION}
resp, body = self.api.json_request('POST', url, body=body)
return ihost_vim(self, body)
def _find_ihost(cc, ihost_id): def _find_ihost(cc, ihost_id):
if ihost_id.isdigit() or utils.is_uuid_like(ihost_id): if ihost_id.isdigit() or utils.is_uuid_like(ihost_id):

View File

@@ -22,6 +22,7 @@ install_command = pip install \
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
-e{[tox]stxdir}/config/tsconfig/tsconfig -e{[tox]stxdir}/config/tsconfig/tsconfig
-e{[tox]stxdir}/config/sysinv/sysinv/sysinv
commands = commands =
find {toxinidir} -not -path '{toxinidir}/.tox/*' -name '*.py[c|o]' -delete find {toxinidir} -not -path '{toxinidir}/.tox/*' -name '*.py[c|o]' -delete

View File

@@ -312,6 +312,9 @@ class V1(base.APIBase):
evaluate_apps_reapply = [link.Link] evaluate_apps_reapply = [link.Link]
"Links to the evaluate_apps_reapply resource" "Links to the evaluate_apps_reapply resource"
vim = [link.Link]
"Links to the VIM resource"
@classmethod @classmethod
def convert(self): def convert(self):
v1 = V1() v1 = V1()
@@ -949,6 +952,13 @@ class V1(base.APIBase):
'evaluate_apps_reapply', '', 'evaluate_apps_reapply', '',
bookmark=True)] bookmark=True)]
v1.vim = [link.Link.make_link('self', pecan.request.host_url,
'vim', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'vim', '',
bookmark=True)]
return v1 return v1

View File

@@ -88,6 +88,7 @@ from sysinv.api.controllers.v1 import patch_api
from sysinv.api.controllers.v1 import ptp_instance from sysinv.api.controllers.v1 import ptp_instance
from sysinv.api.controllers.v1 import ptp_interface from sysinv.api.controllers.v1 import ptp_interface
from sysinv.api.controllers.v1 import kernel from sysinv.api.controllers.v1 import kernel
from sysinv.api.controllers.v1 import vim
from sysinv.api.policies import ihosts as ihosts_policy from sysinv.api.policies import ihosts as ihosts_policy
from sysinv.common import ceph from sysinv.common import ceph
from sysinv.common import constants from sysinv.common import constants
@@ -1175,6 +1176,9 @@ class HostController(rest.RestController):
kernel = kernel.KernelController() kernel = kernel.KernelController()
"Expose kernel as a sub-element of ihosts" "Expose kernel as a sub-element of ihosts"
vim = vim.VIMController()
"Expose vim as a sub-element of ihosts"
_custom_actions = { _custom_actions = {
'detail': ['GET'], 'detail': ['GET'],
'bulk_add': ['POST'], 'bulk_add': ['POST'],

View File

@@ -0,0 +1,83 @@
# Copyright (c) 2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import pecan
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from oslo_log import log
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import types
from sysinv.common import constants
from sysinv.common import exception
from sysinv.api.controllers.v1 import vim_api
LOG = log.getLogger(__name__)
class VIMHostAudit(base.APIBase):
"""API representation of a host audit operation."""
vim_event = wtypes.text
"The VIM event"
def __init__(self, **kwargs):
self.fields = ['vim_event']
for k in self.fields:
setattr(self, k, kwargs.get(k))
class VIMHostAuditResponse(base.APIBase):
"""API representation of a host audit operation."""
hostname = wtypes.text
"The hostname of the host being audited"
ihost_uuid = types.uuid
"The UUID of the host being audited"
vim_event = wtypes.text
"The VIM event"
def __init__(self, **kwargs):
self.fields = ['hostname', 'ihost_uuid', 'vim_event']
for k in self.fields:
setattr(self, k, kwargs.get(k))
class VIMController(rest.RestController):
"""REST controller for VIM operations."""
def __init__(self):
self._api_token = None
# POST ihosts/<uuid>/vim
@wsme_pecan.wsexpose(VIMHostAuditResponse, types.uuid, body=VIMHostAudit)
def post(self, host_uuid, event_request):
"""Perform host audit operation on specified hosts."""
host = pecan.request.dbapi.ihost_get(host_uuid)
if event_request.vim_event != constants.HOST_AUDIT_ACTION:
raise exception.InvalidVIMAction(vim_event=event_request.vim_event)
try:
vim_api.vim_host_action(
token=self._api_token,
uuid=host_uuid,
hostname=host.hostname,
action=constants.HOST_AUDIT_ACTION,
timeout=constants.VIM_DEFAULT_TIMEOUT_IN_SECS,
)
except Exception as e:
raise exception.CannotTriggerVIMHostAudit(hostname=host.hostname) from e
return VIMHostAuditResponse(
hostname=host.hostname,
ihost_uuid=host.uuid,
vim_event=constants.HOST_AUDIT_ACTION,
)

View File

@@ -1,5 +1,5 @@
# #
# Copyright (c) 2015-2020 Wind River Systems, Inc. # Copyright (c) 2015-2020,2025 Wind River Systems, Inc.
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
@@ -102,7 +102,8 @@ def vim_host_action(token, uuid, hostname, action, timeout):
_valid_actions = [constants.UNLOCK_ACTION, _valid_actions = [constants.UNLOCK_ACTION,
constants.LOCK_ACTION, constants.LOCK_ACTION,
constants.FORCE_LOCK_ACTION, constants.FORCE_LOCK_ACTION,
constants.FORCE_UNSAFE_LOCK_ACTION] constants.FORCE_UNSAFE_LOCK_ACTION,
constants.HOST_AUDIT_ACTION]
if action not in _valid_actions: if action not in _valid_actions:
LOG.error("Unrecognized vim_host_action=%s" % action) LOG.error("Unrecognized vim_host_action=%s" % action)

View File

@@ -86,6 +86,7 @@ FORCE_UNLOCK_ACTION = 'force-unlock'
LOCK_ACTION = 'lock' LOCK_ACTION = 'lock'
FORCE_LOCK_ACTION = 'force-lock' FORCE_LOCK_ACTION = 'force-lock'
FORCE_UNSAFE_LOCK_ACTION = 'force-unsafe-lock' FORCE_UNSAFE_LOCK_ACTION = 'force-unsafe-lock'
HOST_AUDIT_ACTION = 'host-audit'
REBOOT_ACTION = 'reboot' REBOOT_ACTION = 'reboot'
RESET_ACTION = 'reset' RESET_ACTION = 'reset'
REINSTALL_ACTION = 'reinstall' REINSTALL_ACTION = 'reinstall'

View File

@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2013-2024 Wind River Systems, Inc. # Copyright (c) 2013-2025 Wind River Systems, Inc.
# Copyright 2010 United States Government as represented by the # Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration. # Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved. # All Rights Reserved.
@@ -275,6 +275,11 @@ class ManagedIPAddress(Invalid):
"modified.") "modified.")
class InvalidVIMAction(Invalid):
message = _("Unsupported action: %(vim_event)s, "
"only host-audit action is supported")
class AddressAlreadyExists(Conflict): class AddressAlreadyExists(Conflict):
message = _("Address %(address)s/%(prefix)s already " message = _("Address %(address)s/%(prefix)s already "
"exists on this interface.") "exists on this interface.")
@@ -1725,3 +1730,7 @@ class UnexpectedEvent(SysinvException):
class CannotQueryPlatformUpgrade(SysinvException): class CannotQueryPlatformUpgrade(SysinvException):
message = _("Failed to query platform upgrade state") message = _("Failed to query platform upgrade state")
class CannotTriggerVIMHostAudit(SysinvException):
message = _("Failed to trigger VIM host-audit for %(hostname)s")

View File

@@ -0,0 +1,68 @@
# Copyright (c) 2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
"""
Tests for the API /ihosts/<uuid>/vim methods.
"""
import mock
from six.moves import http_client
from sysinv.common import constants
from sysinv.tests.api import base
from sysinv.tests.db import base as dbbase
class TestVIM(base.FunctionalTest, dbbase.BaseHostTestCase):
# API_HEADERS are a generic header passed to most API calls
API_HEADERS = {'User-Agent': 'sysinv-test'}
def _get_path(self, host_uuid):
return f'/ihosts/{host_uuid}/vim'
def _create_host(self, personality, subfunction=None,
mgmt_mac=None, mgmt_ip=None,
admin=None,
invprovision=constants.PROVISIONED, **kw):
host = self._create_test_host(personality=personality,
subfunction=subfunction,
administrative=(admin or
constants.ADMIN_UNLOCKED),
invprovision=invprovision,
**kw)
return host
class VIMHostAuditTestCase(TestVIM):
@mock.patch('sysinv.api.controllers.v1.vim_api.vim_host_action')
def test_vim_host_audit(self, mock_vim_host_action):
worker = self._create_host(constants.WORKER,
admin=constants.ADMIN_LOCKED)
host_uuid = worker['uuid']
data = {"vim_event": "host-audit"}
response = self.post_json(self._get_path(host_uuid), data, headers=self.API_HEADERS)
self.assertEqual(http_client.OK, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertEqual(response.json['vim_event'], constants.HOST_AUDIT_ACTION)
self.assertEqual(response.json['ihost_uuid'], host_uuid)
self.assertEqual(response.json['hostname'], worker['hostname'])
mock_vim_host_action.assert_called_once_with(
token=mock.ANY,
uuid=worker["uuid"],
hostname=worker["hostname"],
action=constants.HOST_AUDIT_ACTION,
timeout=constants.VIM_DEFAULT_TIMEOUT_IN_SECS
)
@mock.patch('sysinv.api.controllers.v1.vim_api.vim_host_action')
def test_vim_host_audit_invalid_action(self, mock_vim_host_action):
worker = self._create_host(constants.WORKER,
admin=constants.ADMIN_LOCKED)
host_uuid = worker['uuid']
data = {"vim_event": "invalid-action"}
response = self.post_json(self._get_path(host_uuid), data, headers=self.API_HEADERS,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertIn("Unsupported action", response.json['error_message'])
mock_vim_host_action.assert_not_called()

View File

@@ -0,0 +1,92 @@
# Copyright (c) 2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import mock
import json
from sysinv.common import constants
from sysinv.tests.api import base
from sysinv.api.controllers.v1 import vim_api
class VimApiTestCase(base.FunctionalTest):
def setUp(self):
super(VimApiTestCase, self).setUp()
@mock.patch('sysinv.api.controllers.v1.vim_api.rest_api_request')
def test_vim_host_action_audit(self, mock_rest_api_request):
# Mock the rest_api_request response
mock_rest_api_request.return_value = {'status': 'success'}
# Test parameters
token = None
uuid = '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'
hostname = 'controller-0'
action = constants.HOST_AUDIT_ACTION
timeout = constants.VIM_DEFAULT_TIMEOUT_IN_SECS
# Call the function
result = vim_api.vim_host_action(token, uuid, hostname, action, timeout)
# Verify the result
self.assertEqual(result, {'status': 'success'})
# Verify rest_api_request was called with the correct parameters
expected_url = "http://localhost:30001/nfvi-plugins/v1/hosts/%s" % uuid
expected_headers = {
'Content-type': 'application/json',
'User-Agent': 'sysinv/1.0'
}
expected_payload = {
'uuid': uuid,
'hostname': hostname,
'action': action
}
mock_rest_api_request.assert_called_once_with(
token,
"PATCH",
expected_url,
expected_headers,
json.dumps(expected_payload),
timeout
)
def test_vim_host_action_invalid_action(self):
# Test with an invalid action
token = None
uuid = '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'
hostname = 'controller-0'
action = 'invalid-action'
timeout = constants.VIM_DEFAULT_TIMEOUT_IN_SECS
# Call the function
result = vim_api.vim_host_action(token, uuid, hostname, action, timeout)
# Verify the result is None for invalid action
self.assertIsNone(result)
@mock.patch('sysinv.api.controllers.v1.vim_api.rest_api_request')
def test_vim_host_action_valid_actions(self, mock_rest_api_request):
# Test that all valid actions are accepted
mock_rest_api_request.return_value = {'status': 'success'}
token = None
uuid = '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'
hostname = 'controller-0'
timeout = constants.VIM_DEFAULT_TIMEOUT_IN_SECS
valid_actions = [
constants.UNLOCK_ACTION,
constants.LOCK_ACTION,
constants.FORCE_LOCK_ACTION,
constants.FORCE_UNSAFE_LOCK_ACTION,
constants.HOST_AUDIT_ACTION
]
for action in valid_actions:
result = vim_api.vim_host_action(token, uuid, hostname, action, timeout)
self.assertEqual(result, {'status': 'success'})

View File

@@ -42,6 +42,7 @@ deps = -r{toxinidir}/requirements.txt
-e{[tox]stxdir}/fault/fm-api/source -e{[tox]stxdir}/fault/fm-api/source
-e{[tox]stxdir}/fault/python-fmclient/fmclient -e{[tox]stxdir}/fault/python-fmclient/fmclient
-e{[tox]stxdir}/config/controllerconfig/controllerconfig -e{[tox]stxdir}/config/controllerconfig/controllerconfig
-e{[tox]stxdir}/config/sysinv/sysinv/sysinv
-e{[tox]stxdir}/update/sw-patch/cgcs-patch -e{[tox]stxdir}/update/sw-patch/cgcs-patch
-e{[tox]stxdir}/utilities/utilities/platform-util/platform-util -e{[tox]stxdir}/utilities/utilities/platform-util/platform-util
-e{[tox]stxdir}/utilities/ceph/python-cephclient/python-cephclient -e{[tox]stxdir}/utilities/ceph/python-cephclient/python-cephclient