add virtual media GET api
Closes-Bug: 2072307 Change-Id: I6020a7904639f5b6628bcabb5a861ecc397a8b05 Signed-off-by: Himanshu Roy <hroy@redhat.com>
This commit is contained in:
parent
8b296e242b
commit
c9cf2347ea
21
api-ref/source/baremetal-api-v1-get-vmedia.inc
Normal file
21
api-ref/source/baremetal-api-v1-get-vmedia.inc
Normal 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
|
@ -2220,6 +2220,23 @@ class NodeVmediaController(rest.RestController, GetNodeAndTopicMixin):
|
|||||||
def __init__(self, node_ident):
|
def __init__(self, node_ident):
|
||||||
self.node_ident = 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')
|
@METRICS.timer('NodeVmediaController.post')
|
||||||
@method.expose(status_code=http_client.NO_CONTENT)
|
@method.expose(status_code=http_client.NO_CONTENT)
|
||||||
@method.body('vmedia')
|
@method.body('vmedia')
|
||||||
|
@ -2209,3 +2209,8 @@ def allow_port_name():
|
|||||||
def allow_attach_detach_vmedia():
|
def allow_attach_detach_vmedia():
|
||||||
"""Check if we should support virtual media actions."""
|
"""Check if we should support virtual media actions."""
|
||||||
return api.request.version.minor >= versions.MINOR_89_ATTACH_DETACH_VMEDIA
|
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
|
||||||
|
@ -130,6 +130,7 @@ BASE_VERSION = 1
|
|||||||
# v1.90: Accept ovn vtep switch metadata schema to port.local_link_connection
|
# v1.90: Accept ovn vtep switch metadata schema to port.local_link_connection
|
||||||
# v1.91: Remove special treatment of .json for API objects
|
# v1.91: Remove special treatment of .json for API objects
|
||||||
# v1.92: Add runbooks API
|
# v1.92: Add runbooks API
|
||||||
|
# v1.93: Add GET API for virtual media
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -224,6 +225,7 @@ MINOR_89_ATTACH_DETACH_VMEDIA = 89
|
|||||||
MINOR_90_OVN_VTEP = 90
|
MINOR_90_OVN_VTEP = 90
|
||||||
MINOR_91_DOT_JSON = 91
|
MINOR_91_DOT_JSON = 91
|
||||||
MINOR_92_RUNBOOKS = 92
|
MINOR_92_RUNBOOKS = 92
|
||||||
|
MINOR_93_GET_VMEDIA = 93
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -231,7 +233,7 @@ MINOR_92_RUNBOOKS = 92
|
|||||||
# explanation of what changed in the new version
|
# explanation of what changed in the new version
|
||||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
# - 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
|
# String representations of the minor and maximum versions
|
||||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -1088,6 +1088,15 @@ node_policies = [
|
|||||||
{'path': '/nodes/{node_ident}/vmedia', 'method': 'DELETE'}
|
{'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 = """
|
deprecated_port_reason = """
|
||||||
|
@ -709,8 +709,8 @@ RELEASE_MAPPING = {
|
|||||||
# make it below. To release, we will preserve a version matching
|
# make it below. To release, we will preserve a version matching
|
||||||
# the release as a separate block of text, like above.
|
# the release as a separate block of text, like above.
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.92',
|
'api': '1.93',
|
||||||
'rpc': '1.60',
|
'rpc': '1.61',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Allocation': ['1.1'],
|
'Allocation': ['1.1'],
|
||||||
'BIOSSetting': ['1.1'],
|
'BIOSSetting': ['1.1'],
|
||||||
|
@ -97,7 +97,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
||||||
# NOTE(pas-ha): This also must be in sync with
|
# NOTE(pas-ha): This also must be in sync with
|
||||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||||
RPC_API_VERSION = '1.60'
|
RPC_API_VERSION = '1.61'
|
||||||
|
|
||||||
target = messaging.Target(version=RPC_API_VERSION)
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
@ -3851,6 +3851,31 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
action='service', node=node.uuid,
|
action='service', node=node.uuid,
|
||||||
state=node.provision_state)
|
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')
|
@METRICS.timer('ConductorManager.attach_virtual_media')
|
||||||
@messaging.expected_exceptions(exception.InvalidParameterValue,
|
@messaging.expected_exceptions(exception.InvalidParameterValue,
|
||||||
exception.NoFreeConductorWorker,
|
exception.NoFreeConductorWorker,
|
||||||
|
@ -159,12 +159,13 @@ class ConductorAPI(object):
|
|||||||
| 1.58 - Added support for json-rpc port usage
|
| 1.58 - Added support for json-rpc port usage
|
||||||
| 1.59 - Added support for attaching/detaching virtual media
|
| 1.59 - Added support for attaching/detaching virtual media
|
||||||
| 1.60 - Added continue_node_service
|
| 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(rloo): This must be in sync with manager.ConductorManager's.
|
||||||
# NOTE(pas-ha): This also must be in sync with
|
# NOTE(pas-ha): This also must be in sync with
|
||||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||||
RPC_API_VERSION = '1.60'
|
RPC_API_VERSION = '1.61'
|
||||||
|
|
||||||
def __init__(self, topic=None):
|
def __init__(self, topic=None):
|
||||||
super(ConductorAPI, self).__init__()
|
super(ConductorAPI, self).__init__()
|
||||||
@ -1506,3 +1507,21 @@ class ConductorAPI(object):
|
|||||||
context, 'detach_virtual_media',
|
context, 'detach_virtual_media',
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
device_types=device_types)
|
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)
|
||||||
|
@ -1302,6 +1302,16 @@ class ManagementInterface(BaseInterface):
|
|||||||
raise exception.UnsupportedDriverExtension(
|
raise exception.UnsupportedDriverExtension(
|
||||||
driver=task.node.driver, extension='get_mac_addresses')
|
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):
|
def attach_virtual_media(self, task, device_type, image_url):
|
||||||
"""Attach a virtual media device to the node.
|
"""Attach a virtual media device to the node.
|
||||||
|
|
||||||
|
@ -197,6 +197,57 @@ def _has_vmedia_via_manager(manager):
|
|||||||
return False
|
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):
|
def _insert_vmedia(task, managers, boot_url, boot_device):
|
||||||
"""Insert bootable ISO image into virtual CD or DVD
|
"""Insert bootable ISO image into virtual CD or DVD
|
||||||
|
|
||||||
@ -340,6 +391,23 @@ def _eject_vmedia(task, managers, boot_device=None):
|
|||||||
return found
|
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):
|
def _eject_vmedia_from_resource(task, resource, boot_device=None):
|
||||||
"""Eject virtual media from a given redfish resource (System/Manager)
|
"""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
|
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):
|
def insert_vmedia(task, image_url, device_type):
|
||||||
"""Insert virtual CDs and DVDs
|
"""Insert virtual CDs and DVDs
|
||||||
|
|
||||||
|
@ -1340,6 +1340,18 @@ class RedfishManagement(base.ManagementInterface):
|
|||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.RedfishError(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
|
@task_manager.require_exclusive_lock
|
||||||
def attach_virtual_media(self, task, device_type, image_url):
|
def attach_virtual_media(self, task, device_type, image_url):
|
||||||
"""Attach a virtual media device to the node.
|
"""Attach a virtual media device to the node.
|
||||||
|
@ -8908,9 +8908,33 @@ class TestNodeVmedia(test_api_base.BaseApiTest):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.version = "1.89"
|
self.version = "1.93"
|
||||||
self.node = obj_utils.create_test_node(self.context)
|
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',
|
@mock.patch.object(rpcapi.ConductorAPI, 'attach_virtual_media',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_attach(self, mock_attach):
|
def test_attach(self, mock_attach):
|
||||||
|
@ -1866,6 +1866,12 @@ class RedfishManagementTestCase(db_base.DbTestCase):
|
|||||||
shared=True) as task:
|
shared=True) as task:
|
||||||
self.assertIsNone(task.driver.management.get_mac_addresses(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)
|
@mock.patch.object(redfish_boot, 'insert_vmedia', autospec=True)
|
||||||
def test_attach_virtual_media(self, mock_insert_vmedia):
|
def test_attach_virtual_media(self, mock_insert_vmedia):
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user