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_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
# 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)
NEW_KERNEL = 'lowlatency'
UPDATED_KERNEL['kernel_provisioned'] = NEW_KERNEL
VIM_HOST_AUDIT_RESPONSE = {"vim_event": "host-audit"}
fixtures = {
'/v1/ihosts':
@@ -107,6 +108,13 @@ fixtures = {
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(kernel.kernel_provisioned, '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'])
self.assertEqual(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
#
@@ -686,3 +686,17 @@ def do_host_kernel_show(cc, args):
except exc.HTTPNotFound:
raise exc.CommandError('Host not found: %s' % args.hostnameorid)
_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
#
@@ -11,6 +11,7 @@ from cgtsclient.common import base
from cgtsclient.common import utils
from cgtsclient import exc
from cgtsclient.v1 import icpu
from sysinv.common import constants
CREATION_ATTRIBUTES = ['hostname', 'personality', 'subfunctions', 'mgmt_mac',
@@ -32,6 +33,11 @@ class ihost_kernel(base.Resource):
return "<kernel %s>" % self._info
class ihost_vim(base.Resource):
def __repr__(self):
return "<vim %s>" % self._info
class ihostManager(base.Manager):
resource_class = ihost
@@ -152,6 +158,13 @@ class ihostManager(base.Manager):
resp, body = self.api.json_request('GET', url)
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):
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
-r{toxinidir}/test-requirements.txt
-e{[tox]stxdir}/config/tsconfig/tsconfig
-e{[tox]stxdir}/config/sysinv/sysinv/sysinv
commands =
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]
"Links to the evaluate_apps_reapply resource"
vim = [link.Link]
"Links to the VIM resource"
@classmethod
def convert(self):
v1 = V1()
@@ -949,6 +952,13 @@ class V1(base.APIBase):
'evaluate_apps_reapply', '',
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

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_interface
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.common import ceph
from sysinv.common import constants
@@ -1175,6 +1176,9 @@ class HostController(rest.RestController):
kernel = kernel.KernelController()
"Expose kernel as a sub-element of ihosts"
vim = vim.VIMController()
"Expose vim as a sub-element of ihosts"
_custom_actions = {
'detail': ['GET'],
'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
#
@@ -102,7 +102,8 @@ def vim_host_action(token, uuid, hostname, action, timeout):
_valid_actions = [constants.UNLOCK_ACTION,
constants.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:
LOG.error("Unrecognized vim_host_action=%s" % action)

View File

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

View File

@@ -1,6 +1,6 @@
# 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
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
@@ -275,6 +275,11 @@ class ManagedIPAddress(Invalid):
"modified.")
class InvalidVIMAction(Invalid):
message = _("Unsupported action: %(vim_event)s, "
"only host-audit action is supported")
class AddressAlreadyExists(Conflict):
message = _("Address %(address)s/%(prefix)s already "
"exists on this interface.")
@@ -1725,3 +1730,7 @@ class UnexpectedEvent(SysinvException):
class CannotQueryPlatformUpgrade(SysinvException):
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/python-fmclient/fmclient
-e{[tox]stxdir}/config/controllerconfig/controllerconfig
-e{[tox]stxdir}/config/sysinv/sysinv/sysinv
-e{[tox]stxdir}/update/sw-patch/cgcs-patch
-e{[tox]stxdir}/utilities/utilities/platform-util/platform-util
-e{[tox]stxdir}/utilities/ceph/python-cephclient/python-cephclient