add virtual media GET api

Closes-Bug: 2072307
Change-Id: I6020a7904639f5b6628bcabb5a861ecc397a8b05
Signed-off-by: Himanshu Roy <hroy@redhat.com>
This commit is contained in:
Himanshu Roy 2024-06-10 16:38:38 +05:30
parent 8b296e242b
commit c9cf2347ea
14 changed files with 243 additions and 6 deletions

View File

@ -0,0 +1,21 @@
.. -*- rst -*-
=====================================
Get Virtual Media (nodes)
=====================================
.. versionadded:: 1.93
Get a list of virtual media devices attached to a node using
the ``v1/nodes/{node_ident}/vmedia`` endpoint.
Get virtual media devices attached to a node
================================
.. rest_method:: GET /v1/nodes/{node_ident}/vmedia
Get virtual media devices attached to a node.
Normal response code: 200
Error codes: 400,401,403,404,409

View File

@ -2220,6 +2220,23 @@ class NodeVmediaController(rest.RestController, GetNodeAndTopicMixin):
def __init__(self, node_ident):
self.node_ident = node_ident
@METRICS.timer('NodeVmediaController.get')
@method.expose(status_code=http_client.OK)
def get(self):
"""Get virtual media details for this node
"""
# NOTE(hroyrh) checking for api version here
# rather than separating the get function into
# a different controller
if not api_utils.allow_get_vmedia():
pecan.abort(http_client.NOT_FOUND)
rpc_node, topic = self._get_node_and_topic(
'baremetal:node:vmedia:get')
return api.request.rpcapi.get_virtual_media(
api.request.context, rpc_node.uuid,
topic=topic)
@METRICS.timer('NodeVmediaController.post')
@method.expose(status_code=http_client.NO_CONTENT)
@method.body('vmedia')

View File

@ -2209,3 +2209,8 @@ def allow_port_name():
def allow_attach_detach_vmedia():
"""Check if we should support virtual media actions."""
return api.request.version.minor >= versions.MINOR_89_ATTACH_DETACH_VMEDIA
def allow_get_vmedia():
"""Check if we should support get virtual media action."""
return api.request.version.minor >= versions.MINOR_93_GET_VMEDIA

View File

@ -130,6 +130,7 @@ BASE_VERSION = 1
# v1.90: Accept ovn vtep switch metadata schema to port.local_link_connection
# v1.91: Remove special treatment of .json for API objects
# v1.92: Add runbooks API
# v1.93: Add GET API for virtual media
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -224,6 +225,7 @@ MINOR_89_ATTACH_DETACH_VMEDIA = 89
MINOR_90_OVN_VTEP = 90
MINOR_91_DOT_JSON = 91
MINOR_92_RUNBOOKS = 92
MINOR_93_GET_VMEDIA = 93
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -231,7 +233,7 @@ MINOR_92_RUNBOOKS = 92
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_92_RUNBOOKS
MINOR_MAX_VERSION = MINOR_93_GET_VMEDIA
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -1088,6 +1088,15 @@ node_policies = [
{'path': '/nodes/{node_ident}/vmedia', 'method': 'DELETE'}
],
),
policy.DocumentedRuleDefault(
name='baremetal:node:vmedia:get',
check_str=SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description='Get virtual media device details from a node',
operations=[
{'path': '/nodes/{node_ident}/vmedia', 'method': 'GET'}
],
),
]
deprecated_port_reason = """

View File

@ -709,8 +709,8 @@ RELEASE_MAPPING = {
# make it below. To release, we will preserve a version matching
# the release as a separate block of text, like above.
'master': {
'api': '1.92',
'rpc': '1.60',
'api': '1.93',
'rpc': '1.61',
'objects': {
'Allocation': ['1.1'],
'BIOSSetting': ['1.1'],

View File

@ -97,7 +97,7 @@ class ConductorManager(base_manager.BaseConductorManager):
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
# NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.60'
RPC_API_VERSION = '1.61'
target = messaging.Target(version=RPC_API_VERSION)
@ -3851,6 +3851,31 @@ class ConductorManager(base_manager.BaseConductorManager):
action='service', node=node.uuid,
state=node.provision_state)
@METRICS.timer('ConductorManager.get_virtual_media')
@messaging.expected_exceptions(exception.InvalidParameterValue,
exception.NodeLocked,
exception.UnsupportedDriverExtension)
def get_virtual_media(self, context, node_id):
"""Get all virtual media devices from the node.
:param context: request context.
:param node_id: node ID or UUID.
:raises: UnsupportedDriverExtension if the driver does not support
this call.
:raises: InvalidParameterValue if validation of management driver
interface failed.
:raises: NodeLocked if node is locked by another conductor.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
"""
LOG.debug("RPC get_virtual_media called for node %(node)s ",
{'node': node_id})
with task_manager.acquire(context, node_id,
purpose='get virtual media devices') as task:
task.driver.management.validate(task)
return task.driver.management.get_virtual_media(task)
@METRICS.timer('ConductorManager.attach_virtual_media')
@messaging.expected_exceptions(exception.InvalidParameterValue,
exception.NoFreeConductorWorker,

View File

@ -159,12 +159,13 @@ class ConductorAPI(object):
| 1.58 - Added support for json-rpc port usage
| 1.59 - Added support for attaching/detaching virtual media
| 1.60 - Added continue_node_service
| 1.61 - Added get virtual media support
"""
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
# NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.60'
RPC_API_VERSION = '1.61'
def __init__(self, topic=None):
super(ConductorAPI, self).__init__()
@ -1506,3 +1507,21 @@ class ConductorAPI(object):
context, 'detach_virtual_media',
node_id=node_id,
device_types=device_types)
def get_virtual_media(self, context, node_id, topic=None):
"""Get all virtual media devices from the node.
:param context: request context.
:param node_id: node ID or UUID.
:param topic: RPC topic. Defaults to self.topic.
:raises: UnsupportedDriverExtension if the driver does not support
this call.
:raises: InvalidParameterValue if validation of management driver
interface failed.
:raises: NodeLocked if node is locked by another conductor.
"""
cctxt = self._prepare_call(topic=topic, version='1.61')
return cctxt.call(
context, 'get_virtual_media',
node_id=node_id)

View File

@ -1302,6 +1302,16 @@ class ManagementInterface(BaseInterface):
raise exception.UnsupportedDriverExtension(
driver=task.node.driver, extension='get_mac_addresses')
def get_virtual_media(self, task):
"""Get all virtual media devices from the node.
:param task: A TaskManager instance containing the node to act on.
:raises: UnsupportedDriverExtension
"""
raise exception.UnsupportedDriverExtension(
driver=task.node.driver, extension='get_virtual_media')
def attach_virtual_media(self, task, device_type, image_url):
"""Attach a virtual media device to the node.

View File

@ -197,6 +197,57 @@ def _has_vmedia_via_manager(manager):
return False
def _get_vmedia(task, managers):
"""GET virtual media details
:param task: A task from Task Manager.
:param managers: A list of System managers.
:raises: InvalidParameterValue, if no suitable virtual CD or DVD is
found on the node.
"""
err_msgs = []
vmedia_list = []
system = redfish_utils.get_system(task.node)
if _has_vmedia_via_systems(system):
vmedia_get = _get_vmedia_resources(task, system, err_msgs)
if vmedia_get:
for vmedia in vmedia_get:
media_type_list = []
for media_type in vmedia.media_types:
media_type_list.append(media_type.value)
media = {
"media_types": media_type_list,
"inserted": vmedia.inserted,
"image": vmedia.image
}
vmedia_list.append(media)
return vmedia_list
else:
for manager in managers:
vmedia_get = _get_vmedia_resources(task, manager, err_msgs)
if vmedia_get:
for vmedia in vmedia_get:
media_type_list = []
for media_type in vmedia.media_types:
media_type_list.append(media_type.value)
media = {
"media_types": media_type_list,
"inserted": vmedia.inserted,
"image": vmedia.image
}
vmedia_list.append(media)
return vmedia_list
if err_msgs:
exc_msg = ("All virtual media GET attempts failed. "
"Most recent error: ", err_msgs[-1])
else:
exc_msg = 'No suitable virtual media device found'
raise exception.InvalidParameterValue(exc_msg)
def _insert_vmedia(task, managers, boot_url, boot_device):
"""Insert bootable ISO image into virtual CD or DVD
@ -340,6 +391,23 @@ def _eject_vmedia(task, managers, boot_device=None):
return found
@tenacity.retry(retry=tenacity.retry_if_exception(_test_retry),
stop=tenacity.stop_after_attempt(3),
wait=tenacity.wait_fixed(3),
reraise=True)
def _get_vmedia_resources(task, resource, err_msgs):
"""Get virtual media for a given redfish resource (System/Manager)
:param task: A task from TaskManager.
:param resource: A redfish resource either a System or Manager.
:param err_msgs: A list that will contain all errors found
"""
LOG.info("Get virtual media details for node=%(node)s",
{'node': task.node.uuid})
return resource.virtual_media.get_members()
def _eject_vmedia_from_resource(task, resource, boot_device=None):
"""Eject virtual media from a given redfish resource (System/Manager)
@ -383,6 +451,20 @@ def _eject_vmedia_from_resource(task, resource, boot_device=None):
return found
def get_vmedia(task):
"""Get the attached virtual CDs and DVDs for a node
:param task: A task from TaskManager.
:raises: InvalidParameterValue, if no suitable virtual CD or DVD is
found on the node.
"""
LOG.info('Called redfish.boot.get_vmedia, for '
'node=%(node)s',
{'node': task.node.uuid})
system = redfish_utils.get_system(task.node)
return _get_vmedia(task, system.managers)
def insert_vmedia(task, image_url, device_type):
"""Insert virtual CDs and DVDs

View File

@ -1340,6 +1340,18 @@ class RedfishManagement(base.ManagementInterface):
LOG.error(msg)
raise exception.RedfishError(error=msg)
@task_manager.require_exclusive_lock
def get_virtual_media(self, task):
"""Get all virtual media devices from the node.
:param task: A task from TaskManager.
"""
LOG.info('Called redfish.management.get_virtual_media,'
'for the node=%(node)s',
{'node': task.node.uuid})
return redfish_boot.get_vmedia(task)
@task_manager.require_exclusive_lock
def attach_virtual_media(self, task, device_type, image_url):
"""Attach a virtual media device to the node.

View File

@ -8908,9 +8908,33 @@ class TestNodeVmedia(test_api_base.BaseApiTest):
def setUp(self):
super().setUp()
self.version = "1.89"
self.version = "1.93"
self.node = obj_utils.create_test_node(self.context)
@mock.patch.object(rpcapi.ConductorAPI, 'get_virtual_media',
autospec=True)
def test_get(self, mock_get):
mock_vmedia_list = [
{'media_types': ['CD', 'DVD'],
'inserted': 'false',
'image': ''},
{'media_types': ['Floppy', 'USBStick'],
'inserted': 'false',
'image': ''}
]
mock_get.return_value = mock_vmedia_list
ret = self.get_json('/nodes/%s/vmedia' % self.node.uuid,
headers={api_base.Version.string: self.version})
self.assertEqual(mock_vmedia_list, ret)
mock_get.assert_called_once_with(
mock.ANY, mock.ANY, self.node.uuid, topic='test-topic')
def test_get_wrong_version(self):
ret = self.get_json('/nodes/%s/vmedia' % self.node.uuid,
headers={api_base.Version.string: "1.92"},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_int)
@mock.patch.object(rpcapi.ConductorAPI, 'attach_virtual_media',
autospec=True)
def test_attach(self, mock_attach):

View File

@ -1866,6 +1866,12 @@ class RedfishManagementTestCase(db_base.DbTestCase):
shared=True) as task:
self.assertIsNone(task.driver.management.get_mac_addresses(task))
@mock.patch.object(redfish_boot, 'get_vmedia', autospec=True)
def test_get_virtual_media(self, mock_get_vmedia):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.management.get_virtual_media(task)
mock_get_vmedia.assert_called_once_with(task)
@mock.patch.object(redfish_boot, 'insert_vmedia', autospec=True)
def test_attach_virtual_media(self, mock_insert_vmedia):
with task_manager.acquire(self.context, self.node.uuid) as task:

View File

@ -0,0 +1,5 @@
---
features:
Adds a new capability allowing to fetch the list
of virtual media devices attached to a node by
making a GET request.