Add virtual media attachment and detachment support
This change introduces methods for attaching and detaching virtual media devices to/from nodes, enhancing the capabilities of the baremetal API. Changes include: - Added `VMEDIA_VERSION` constant in `_common.py` for API versioning. - Introduced `attach_vmedia` and `detach_vmedia` methods in the `Node` class. - Added `attach_vmedia_to_node` and `detach_vmedia_from_node` methods in the `Proxy` class. - Added unit and functional test for the features. Change-Id: Id41d45ad78b07f8ce9cca92444e1603f3882fe53
This commit is contained in:
@@ -58,6 +58,12 @@ Chassis Operations
|
||||
:members: chassis, find_chassis, get_chassis,
|
||||
create_chassis, update_chassis, patch_chassis, delete_chassis
|
||||
|
||||
Virtual Media Operations
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. autoclass:: openstack.baremetal.v1._proxy.Proxy
|
||||
:noindex:
|
||||
:members: attach_vmedia_to_node, detach_vmedia_from_node
|
||||
|
||||
VIF Operations
|
||||
^^^^^^^^^^^^^^
|
||||
.. autoclass:: openstack.baremetal.v1._proxy.Proxy
|
||||
|
||||
@@ -95,6 +95,9 @@ CHANGE_BOOT_MODE_VERSION = '1.76'
|
||||
FIRMWARE_VERSION = '1.86'
|
||||
"""API version in which firmware components of a node can be accessed"""
|
||||
|
||||
VMEDIA_VERSION = '1.89'
|
||||
"""API version in which the virtual media operations were introduced."""
|
||||
|
||||
RUNBOOKS_VERSION = '1.92'
|
||||
"""API version in which a runbook can be used in place of arbitrary steps
|
||||
for provisioning"""
|
||||
|
||||
@@ -1087,6 +1087,55 @@ class Proxy(proxy.Proxy):
|
||||
_portgroup.PortGroup, port_group, ignore_missing=ignore_missing
|
||||
)
|
||||
|
||||
# ========== Virtual Media ==========
|
||||
|
||||
def attach_vmedia_to_node(
|
||||
self,
|
||||
node,
|
||||
device_type,
|
||||
image_url,
|
||||
image_download_source=None,
|
||||
retry_on_conflict=True,
|
||||
):
|
||||
"""Attach virtual media device to a node.
|
||||
|
||||
:param node: The value can be either the name or ID of a node or
|
||||
a :class:`~openstack.baremetal.v1.node.Node` instance.
|
||||
:param device_type: The type of virtual media device.
|
||||
:param image_url: The URL of the image to attach.
|
||||
:param image_download_source: The source of the image download.
|
||||
:param retry_on_conflict: Whether to retry HTTP CONFLICT errors.
|
||||
This can happen when either the virtual media is already used on
|
||||
a node or the node is locked. Since the latter happens more often,
|
||||
the default value is True.
|
||||
:return: ``None``
|
||||
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
|
||||
does not support the VMEDIA API.
|
||||
"""
|
||||
res = self._get_resource(_node.Node, node)
|
||||
res.attach_vmedia(
|
||||
self,
|
||||
device_type=device_type,
|
||||
image_url=image_url,
|
||||
image_download_source=image_download_source,
|
||||
retry_on_conflict=retry_on_conflict,
|
||||
)
|
||||
|
||||
def detach_vmedia_from_node(self, node, device_types=None):
|
||||
"""Detach virtual media from the node.
|
||||
|
||||
:param node: The value can be either the name or ID of a node or
|
||||
a :class:`~openstack.baremetal.v1.node.Node` instance.
|
||||
:param device_types: A list with the types of virtual media
|
||||
devices to detach.
|
||||
:return: ``True`` if the virtual media was detached,
|
||||
otherwise ``False``.
|
||||
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
|
||||
does not support the VMEDIA API.
|
||||
"""
|
||||
res = self._get_resource(_node.Node, node)
|
||||
return res.detach_vmedia(self, device_types=device_types)
|
||||
|
||||
# ========== VIFs ==========
|
||||
|
||||
def attach_vif_to_node(
|
||||
|
||||
@@ -212,7 +212,7 @@ class Node(_common.Resource):
|
||||
#: unit of a specific type of resource. Added in API microversion 1.21.
|
||||
resource_class = resource.Body("resource_class")
|
||||
#: A string representing the current service step being executed upon.
|
||||
#: Added in API microversion 1.87.
|
||||
#: Added in API microversion 1.89.
|
||||
service_step = resource.Body("service_step")
|
||||
#: A string representing the uuid or logical name of a runbook as an
|
||||
#: alternative to providing ``clean_steps`` or ``service_steps``.
|
||||
@@ -812,6 +812,100 @@ class Node(_common.Resource):
|
||||
if wait:
|
||||
self.wait_for_power_state(session, expected, timeout=timeout)
|
||||
|
||||
def attach_vmedia(
|
||||
self,
|
||||
session,
|
||||
device_type,
|
||||
image_url,
|
||||
image_download_source=None,
|
||||
retry_on_conflict=True,
|
||||
):
|
||||
"""Attach virtual media device to a node.
|
||||
|
||||
:param session: The session to use for making this request.
|
||||
:type session: :class:`~keystoneauth1.adapter.Adapter`
|
||||
:param device_type: The type of virtual media device.
|
||||
:param image_url: The URL of the image to attach.
|
||||
:param image_download_source: The source of the image download.
|
||||
:param retry_on_conflict: Whether to retry HTTP CONFLICT errors.
|
||||
This can happen when either the virtual media is already used on
|
||||
a node or the node is locked. Since the latter happens more often,
|
||||
the default value is True.
|
||||
:return: ``None``
|
||||
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
|
||||
does not support the VMEDIA API.
|
||||
|
||||
"""
|
||||
session = self._get_session(session)
|
||||
version = self._assert_microversion_for(
|
||||
session,
|
||||
_common.VMEDIA_VERSION,
|
||||
error_message=("Cannot use virtual media API"),
|
||||
)
|
||||
# Prepare the request and create the request body
|
||||
request = self._prepare_request(requires_id=True)
|
||||
request.url = utils.urljoin(request.url, 'vmedia')
|
||||
body = {"device_type": device_type, "image_url": image_url}
|
||||
if image_download_source:
|
||||
body["image_download_source"] = image_download_source
|
||||
retriable_status_codes = _common.RETRIABLE_STATUS_CODES
|
||||
if not retry_on_conflict:
|
||||
retriable_status_codes = list(set(retriable_status_codes) - {409})
|
||||
response = session.post(
|
||||
request.url,
|
||||
json=body,
|
||||
headers=request.headers,
|
||||
microversion=version,
|
||||
retriable_status_codes=retriable_status_codes,
|
||||
)
|
||||
|
||||
msg = f"Failed to attach Virtual Media to bare metal node {self.id}"
|
||||
exceptions.raise_from_response(response, error_message=msg)
|
||||
|
||||
def detach_vmedia(self, session, device_types=None):
|
||||
"""Detach virtual media from a node
|
||||
|
||||
:param session: The session to use for making this request.
|
||||
:type session: :class:`~keystoneauth1.adapter.Adapter`
|
||||
:param device_types: A list with the types of virtual media
|
||||
devices to detach.
|
||||
:return: ``True`` if the virtual media was detached,
|
||||
otherwise ``False``.
|
||||
:raises: :exc:`~openstack.exceptions.NotSupported` if the server
|
||||
does not support the VMEDIA API
|
||||
"""
|
||||
session = self._get_session(session)
|
||||
version = self._assert_microversion_for(
|
||||
session,
|
||||
_common.VMEDIA_VERSION,
|
||||
error_message=("Cannot use virtual media API"),
|
||||
)
|
||||
|
||||
request = self._prepare_request(requires_id=True)
|
||||
request.url = utils.urljoin(request.url, 'vmedia')
|
||||
|
||||
delete_kwargs = {
|
||||
'headers': request.headers,
|
||||
'microversion': version,
|
||||
'retriable_status_codes': _common.RETRIABLE_STATUS_CODES,
|
||||
}
|
||||
|
||||
if device_types:
|
||||
delete_kwargs['json'] = {
|
||||
'device_types': _common.comma_separated_list(device_types)
|
||||
}
|
||||
|
||||
response = session.delete(request.url, **delete_kwargs)
|
||||
|
||||
if response.status_code == 400:
|
||||
session.log.debug(
|
||||
"Virtual media doesn't exist for node %(node)s",
|
||||
{'node': self.id},
|
||||
)
|
||||
|
||||
msg = f"Failed to detach virtual media from bare metal node {self.id}"
|
||||
exceptions.raise_from_response(response, error_message=msg)
|
||||
|
||||
def attach_vif(
|
||||
self,
|
||||
session: adapter.Adapter,
|
||||
|
||||
@@ -467,6 +467,38 @@ class TestBareMetalVif(base.BaseBaremetalTest):
|
||||
)
|
||||
|
||||
|
||||
class TestBareMetalVirtualMedia(base.BaseBaremetalTest):
|
||||
min_microversion = '1.89'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.node = self.create_node(network_interface='noop')
|
||||
self.device_type = "CDROM"
|
||||
self.image_url = "http://image"
|
||||
|
||||
def test_node_vmedia_attach_detach(self):
|
||||
self.conn.baremetal.attach_vmedia_to_node(
|
||||
self.node, self.device_type, self.image_url
|
||||
)
|
||||
res = self.conn.baremetal.detach_vmedia_from_node(self.node)
|
||||
self.assertNone(res)
|
||||
|
||||
def test_node_vmedia_negative(self):
|
||||
uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971"
|
||||
self.assertRaises(
|
||||
exceptions.ResourceNotFound,
|
||||
self.conn.baremetal.attach_vmedia_to_node,
|
||||
uuid,
|
||||
self.device_type,
|
||||
self.image_url,
|
||||
)
|
||||
self.assertRaises(
|
||||
exceptions.ResourceNotFound,
|
||||
self.conn.baremetal.detach_vmedia_from_node,
|
||||
uuid,
|
||||
)
|
||||
|
||||
|
||||
class TestTraits(base.BaseBaremetalTest):
|
||||
min_microversion = '1.37'
|
||||
|
||||
|
||||
@@ -736,6 +736,92 @@ class TestNodeVif(base.TestCase):
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
|
||||
@mock.patch.object(node.Node, '_get_session', lambda self, x: x)
|
||||
class TestNodeVmedia(base.TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.session = mock.Mock(spec=adapter.Adapter)
|
||||
self.session.default_microversion = '1.89'
|
||||
self.session.log = mock.Mock()
|
||||
self.node = node.Node(
|
||||
id='c29db401-b6a7-4530-af8e-20a720dee946', driver=FAKE['driver']
|
||||
)
|
||||
self.device_type = "CDROM"
|
||||
self.image_url = "http://image"
|
||||
|
||||
def test_attach_vmedia(self):
|
||||
self.assertIsNone(
|
||||
self.node.attach_vmedia(
|
||||
self.session, self.device_type, self.image_url
|
||||
)
|
||||
)
|
||||
self.session.post.assert_called_once_with(
|
||||
f'nodes/{self.node.id}/vmedia',
|
||||
json={
|
||||
'device_type': self.device_type,
|
||||
'image_url': self.image_url,
|
||||
},
|
||||
headers=mock.ANY,
|
||||
microversion='1.89',
|
||||
retriable_status_codes=[409, 503],
|
||||
)
|
||||
|
||||
def test_attach_vmedia_no_retries(self):
|
||||
self.assertIsNone(
|
||||
self.node.attach_vmedia(
|
||||
self.session,
|
||||
self.device_type,
|
||||
self.image_url,
|
||||
retry_on_conflict=False,
|
||||
)
|
||||
)
|
||||
self.session.post.assert_called_once_with(
|
||||
f'nodes/{self.node.id}/vmedia',
|
||||
json={
|
||||
'device_type': self.device_type,
|
||||
'image_url': self.image_url,
|
||||
},
|
||||
headers=mock.ANY,
|
||||
microversion='1.89',
|
||||
retriable_status_codes=[503],
|
||||
)
|
||||
|
||||
def test_detach_vmedia_existing(self):
|
||||
self.assertIsNone(self.node.detach_vmedia(self.session))
|
||||
self.session.delete.assert_called_once_with(
|
||||
f'nodes/{self.node.id}/vmedia',
|
||||
headers=mock.ANY,
|
||||
microversion='1.89',
|
||||
retriable_status_codes=_common.RETRIABLE_STATUS_CODES,
|
||||
)
|
||||
|
||||
def test_detach_vmedia_missing(self):
|
||||
self.session.delete.return_value.status_code = 400
|
||||
self.assertIsNone(self.node.detach_vmedia(self.session))
|
||||
self.session.delete.assert_called_once_with(
|
||||
f'nodes/{self.node.id}/vmedia',
|
||||
headers=mock.ANY,
|
||||
microversion='1.89',
|
||||
retriable_status_codes=_common.RETRIABLE_STATUS_CODES,
|
||||
)
|
||||
|
||||
def test_incompatible_microversion(self):
|
||||
self.session.default_microversion = '1.1'
|
||||
self.assertRaises(
|
||||
exceptions.NotSupported,
|
||||
self.node.attach_vmedia,
|
||||
self.session,
|
||||
self.device_type,
|
||||
self.image_url,
|
||||
)
|
||||
self.assertRaises(
|
||||
exceptions.NotSupported,
|
||||
self.node.detach_vmedia,
|
||||
self.session,
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
|
||||
@mock.patch.object(node.Node, '_get_session', lambda self, x: x)
|
||||
class TestNodeValidate(base.TestCase):
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Implements virtual media attach/detach API for bare metal nodes.
|
||||
Reference in New Issue
Block a user