Attaching a volume returns HTTP 202

Instead of returning an HTTP 200 and a `volumeAttachment` object,
attaching a volume to an instance returns HTTP 202 starting from API
version 2.101.

To keep the functionality for older API versions, we move the
`_attach_volume()` method from n-api to n-conductor and either do a call
or a cast depending on whether the API needs to return a value.
n-conductor then handles reserving the block_device_mapping's device by
calling n-compute before it starts the previously-already-async volume
attachment.

We have to move `_check_attach_and_reserve_volume` into compute utils,
because it's getting called in both conductor and compute api (for the
shelved offloaded attach).

The new RPC method in the conductor needs a long timeout when used with
API versions less than the new 2.101, because it waits for the call to
`reserve_block_device_name()` in nova-compute which already needs a long
timeout.

Updating the functional tests' `post_server_volume()` and
`_attach_volume()` to not return the attachment anymore is possible, as
no test uses the returned values.

Change-Id: I4d38c2679f0e88cca30055a9c8c45ba1dd6fb5ef
Implements: blueprint async-volume-attachments
Signed-off-by: Johannes Kulik <johannes.kulik@sap.com>
This commit is contained in:
Johannes Kulik
2025-12-15 16:27:49 +01:00
parent 0383085510
commit 2a51df2760
30 changed files with 675 additions and 349 deletions
+5 -1
View File
@@ -60,7 +60,7 @@ Attach a volume to an instance
Attach a volume to an instance. Attach a volume to an instance.
Normal response codes: 200 Normal response codes: 200 (microversions 2.0 - 2.100), 202 (microversion 2.101 - )
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), conflict(409) Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), conflict(409)
@@ -106,6 +106,10 @@ Request
Response Response
-------- --------
Starting with microversion 2.101, there is no body content for the response of
a successful POST operation. Until microversion 2.100, a response contains the
following parameters.
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- volumeAttachment: volumeAttachment - volumeAttachment: volumeAttachment
@@ -0,0 +1,7 @@
{
"volumeAttachment": {
"volumeId": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"tag": "foo",
"delete_on_termination": true
}
}
@@ -0,0 +1,22 @@
{
"volumeAttachments": [
{
"attachment_id": "979ce4f8-033a-409d-85e6-6b5c0f6a6302",
"delete_on_termination": false,
"device": "/dev/sdc",
"serverId": "7696780b-3f53-4688-ab25-019bfcbbd806",
"tag": null,
"volumeId": "227cc671-f30b-4488-96fd-7d0bf13648d8",
"bdm_uuid": "c088db45-92b8-49e8-81e2-a1b77a144b3b"
},
{
"attachment_id": "c5684109-0311-4fca-9814-350e46ab7d2a",
"delete_on_termination": true,
"device": "/dev/sdb",
"serverId": "7696780b-3f53-4688-ab25-019bfcbbd806",
"tag": "foo",
"volumeId": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"bdm_uuid": "1aa24536-6fb5-426c-8894-d627f39aa48b"
}
]
}
@@ -0,0 +1,10 @@
{
"volumeAttachment": {
"volumeId": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"id": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"serverId": "fddf0901-8caf-42c9-b496-133c570b171b",
"device": "/dev/sdb",
"tag": "foo",
"delete_on_termination": true
}
}
@@ -0,0 +1,11 @@
{
"volumeAttachment": {
"attachment_id": "721a5c82-5ebc-4c6a-8339-3d33d8d027ed",
"delete_on_termination": true,
"device": "/dev/sdb",
"serverId": "7ebed2ce-85b3-40b5-84ae-8cc725c37ed2",
"tag": "foo",
"volumeId": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"bdm_uuid": "c088db45-92b8-49e8-81e2-a1b77a144b3b"
}
}
@@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.100", "version": "2.101",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }
@@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.100", "version": "2.101",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }
+4 -1
View File
@@ -278,6 +278,9 @@ REST_API_VERSION_HISTORY = """REST API Version History:
``GET /servers/{server_id}``, ``GET /servers/detail``, ``GET /servers/{server_id}``, ``GET /servers/detail``,
``PUT /servers/{server_id}`` and ``PUT /servers/{server_id}`` and
``POST /server/{server_id}/action`` (rebuild) responses. ``POST /server/{server_id}/action`` (rebuild) responses.
* 2.101 - Attaching a volume via
``POST /servers/{server_id}/os-volume_attachments`` returns HTTP
202 Accepted instead of HTTP 200 and a volumeAttachment response.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@@ -286,7 +289,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = '2.1' _MIN_API_VERSION = '2.1'
_MAX_API_VERSION = '2.100' _MAX_API_VERSION = '2.101'
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal
@@ -1300,3 +1300,15 @@ The ``GET /servers/{server_id}``, ``GET /servers/detail``
``PUT /servers/{server_id}`` and ``POST /server/{server_id}/action`` ``PUT /servers/{server_id}`` and ``POST /server/{server_id}/action``
(rebuild) responses now include the scheduler hints provided during (rebuild) responses now include the scheduler hints provided during
server creation. server creation.
.. _microversion 2.101:
2.101 (Maximum in 2026.1 Gazpacho)
----------------------------------
Attaching a volume via ``POST /servers/{server_id}/os-volume_attachments``
returns an HTTP 202 Accepted response instead of the previous HTTP 200
containing a ``volumeAttachment`` object. The response is now fully aync and
does not wait for reserving a device name. Like before, callers need to watch
the volume's state and/or server actions to recognize when the
volume-attachment process finished.
@@ -229,6 +229,8 @@ create_response_v279['properties']['volumeAttachment'][
'required' 'required'
].append('device') ].append('device')
create_response_v2101 = {'type': 'null'}
update_response = {'type': 'null'} update_response = {'type': 'null'}
delete_response = {'type': 'null'} delete_response = {'type': 'null'}
@@ -16,6 +16,7 @@
"""The volume attachments extension.""" """The volume attachments extension."""
from oslo_utils import strutils from oslo_utils import strutils
import webob
from webob import exc from webob import exc
from nova.api.openstack import api_version_request from nova.api.openstack import api_version_request
@@ -179,14 +180,14 @@ class VolumeAttachmentController(wsgi.Controller):
) )
} }
# TODO(mriedem): This API should return a 202 instead of a 200 response.
@wsgi.expected_errors((400, 403, 404, 409)) @wsgi.expected_errors((400, 403, 404, 409))
@validation.schema(schema.create, '2.0', '2.48') @validation.schema(schema.create, '2.0', '2.48')
@validation.schema(schema.create_v249, '2.49', '2.78') @validation.schema(schema.create_v249, '2.49', '2.78')
@validation.schema(schema.create_v279, '2.79') @validation.schema(schema.create_v279, '2.79')
@validation.response_body_schema(schema.create_response, '2.0', '2.69') @validation.response_body_schema(schema.create_response, '2.0', '2.69')
@validation.response_body_schema(schema.create_response_v270, '2.70', '2.78') # noqa: E501 @validation.response_body_schema(schema.create_response_v270, '2.70', '2.78') # noqa: E501
@validation.response_body_schema(schema.create_response_v279, '2.79') @validation.response_body_schema(schema.create_response_v279, '2.79', '2.100') # noqa: E501
@validation.response_body_schema(schema.create_response_v2101, '2.101')
def create(self, req, server_id, body): def create(self, req, server_id, body):
"""Attach a volume to an instance.""" """Attach a volume to an instance."""
context = req.environ['nova.context'] context = req.environ['nova.context']
@@ -207,12 +208,16 @@ class VolumeAttachmentController(wsgi.Controller):
_check_request_version( _check_request_version(
req, '2.20', 'attach_volume', server_id, instance.vm_state) req, '2.20', 'attach_volume', server_id, instance.vm_state)
needs_device_returned = not api_version_request.is_supported(
req, '2.101')
try: try:
supports_multiattach = common.supports_multiattach_volume(req) supports_multiattach = common.supports_multiattach_volume(req)
device = self.compute_api.attach_volume( device = self.compute_api.attach_volume(
context, instance, volume_id, device, tag=tag, context, instance, volume_id, device, tag=tag,
supports_multiattach=supports_multiattach, supports_multiattach=supports_multiattach,
delete_on_termination=delete_on_termination) delete_on_termination=delete_on_termination,
needs_device_returned=needs_device_returned)
except exception.VolumeNotFound as e: except exception.VolumeNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message()) raise exc.HTTPNotFound(explanation=e.format_message())
except (exception.InstanceIsLocked, exception.DevicePathInUse) as e: except (exception.InstanceIsLocked, exception.DevicePathInUse) as e:
@@ -231,6 +236,14 @@ class VolumeAttachmentController(wsgi.Controller):
raise exc.HTTPBadRequest(explanation=e.format_message()) raise exc.HTTPBadRequest(explanation=e.format_message())
except exception.TooManyDiskDevices as e: except exception.TooManyDiskDevices as e:
raise exc.HTTPForbidden(explanation=e.format_message()) raise exc.HTTPForbidden(explanation=e.format_message())
except exception.ServiceTooOld:
raise exc.HTTPServiceUnavailable(explanation="nova-conductor is "
"not on the same version as nova-api and needs to be "
"upgraded.")
if not needs_device_returned:
# New return code starting from 2.101
return webob.Response(status_int=202)
# The attach is async # The attach is async
# NOTE(mriedem): It would be nice to use # NOTE(mriedem): It would be nice to use
@@ -246,8 +259,9 @@ class VolumeAttachmentController(wsgi.Controller):
attachment['tag'] = tag attachment['tag'] = tag
if api_version_request.is_supported(req, '2.79'): if api_version_request.is_supported(req, '2.79'):
attachment['delete_on_termination'] = delete_on_termination attachment['delete_on_termination'] = delete_on_termination
# TODO(stephenfin): We forgot to apply 2.89 here. We should return # NOTE(stephenfin): We forgot to apply 2.89 here, but starting with
# 'bdm_uuid' and 'attachment_id' and stop returning 'id' # 2.101 we don't return a body anymore and thus it will stay this way
# forever.
return {'volumeAttachment': attachment} return {'volumeAttachment': attachment}
def _update_volume_swap(self, req, instance, id, body): def _update_volume_swap(self, req, instance, id, body):
+36 -128
View File
@@ -2039,9 +2039,9 @@ class API:
volume = volumes[volume_id] volume = volumes[volume_id]
# We do not validate the instance and volume AZ here # We do not validate the instance and volume AZ here
# because that is done earlier by _provision_instances. # because that is done earlier by _provision_instances.
self._check_attach_and_reserve_volume( compute_utils.check_attach_and_reserve_volume(
context, volume, instance, bdm, supports_multiattach, context, self.volume_api, volume, instance, bdm,
validate_az=False) supports_multiattach, validate_az=False)
bdm.volume_size = volume.get('size') bdm.volume_size = volume.get('size')
except (exception.CinderConnectionFailed, except (exception.CinderConnectionFailed,
exception.InvalidVolume, exception.InvalidVolume,
@@ -5056,42 +5056,28 @@ class API:
"""Inject network info for the instance.""" """Inject network info for the instance."""
self.compute_rpcapi.inject_network_info(context, instance=instance) self.compute_rpcapi.inject_network_info(context, instance=instance)
def _create_volume_bdm(self, context, instance, device, volume, def _create_volume_bdm_locally(self, context, instance, device, volume,
disk_bus, device_type, is_local_creation=False, disk_bus, device_type, tag=None, delete_on_termination=False):
tag=None, delete_on_termination=False):
volume_id = volume['id'] volume_id = volume['id']
if is_local_creation: # when the creation is done locally we can't specify the device
# when the creation is done locally we can't specify the device # name as we do not have a way to check that the name specified is
# name as we do not have a way to check that the name specified is # a valid one.
# a valid one. # We leave the setting of that value when the actual attach
# We leave the setting of that value when the actual attach # happens on the compute manager
# happens on the compute manager # NOTE(artom) Local attach (to a shelved-offload instance) cannot
# NOTE(artom) Local attach (to a shelved-offload instance) cannot # support device tagging because we have no way to call the compute
# support device tagging because we have no way to call the compute # manager to check that it supports device tagging. In fact, we
# manager to check that it supports device tagging. In fact, we # don't even know which computer manager the instance will
# don't even know which computer manager the instance will # eventually end up on when it's unshelved.
# eventually end up on when it's unshelved. volume_bdm = objects.BlockDeviceMapping(
volume_bdm = objects.BlockDeviceMapping( context=context,
context=context, source_type='volume', destination_type='volume',
source_type='volume', destination_type='volume', instance_uuid=instance.uuid, boot_index=None,
instance_uuid=instance.uuid, boot_index=None, volume_id=volume_id,
volume_id=volume_id, device_name=None, guest_format=None,
device_name=None, guest_format=None, disk_bus=disk_bus, device_type=device_type,
disk_bus=disk_bus, device_type=device_type, delete_on_termination=delete_on_termination)
delete_on_termination=delete_on_termination) volume_bdm.create()
volume_bdm.create()
else:
# NOTE(vish): This is done on the compute host because we want
# to avoid a race where two devices are requested at
# the same time. When db access is removed from
# compute, the bdm will be created here and we will
# have to make sure that they are assigned atomically.
volume_bdm = self.compute_rpcapi.reserve_block_device_name(
context, instance, device, volume_id, disk_bus=disk_bus,
device_type=device_type, tag=tag,
multiattach=volume['multiattach'])
volume_bdm.delete_on_termination = delete_on_termination
volume_bdm.save()
return volume_bdm return volume_bdm
def _check_volume_already_attached( def _check_volume_already_attached(
@@ -5142,86 +5128,6 @@ class API:
msg = _("volume %s already attached") % volume['id'] msg = _("volume %s already attached") % volume['id']
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
def _check_attach_and_reserve_volume(self, context, volume, instance,
bdm, supports_multiattach=False,
validate_az=True):
"""Perform checks against the instance and volume before attaching.
If validation succeeds, the bdm is updated with an attachment_id which
effectively reserves it during the attach process in cinder.
:param context: nova auth RequestContext
:param volume: volume dict from cinder
:param instance: Instance object
:param bdm: BlockDeviceMapping object
:param supports_multiattach: True if the request supports multiattach
volumes, i.e. microversion >= 2.60, False otherwise
:param validate_az: True if the instance and volume availability zones
should be validated for cross_az_attach, False to not validate AZ
"""
volume_id = volume['id']
if validate_az:
self.volume_api.check_availability_zone(context, volume,
instance=instance)
# If volume.multiattach=True and the microversion to
# support multiattach is not used, fail the request.
if volume['multiattach'] and not supports_multiattach:
raise exception.MultiattachNotSupportedOldMicroversion()
attachment_id = self.volume_api.attachment_create(
context, volume_id, instance.uuid)['id']
bdm.attachment_id = attachment_id
# NOTE(ildikov): In case of boot from volume the BDM at this
# point is not yet created in a cell database, so we can't
# call save(). When attaching a volume to an existing
# instance, the instance is already in a cell and the BDM has
# been created in that same cell so updating here in that case
# is "ok".
if bdm.obj_attr_is_set('id'):
bdm.save()
# TODO(stephenfin): Fold this back in now that cells v1 no longer needs to
# override it.
def _attach_volume(self, context, instance, volume, device,
disk_bus, device_type, tag=None,
supports_multiattach=False,
delete_on_termination=False):
"""Attach an existing volume to an existing instance.
This method is separated to make it possible for cells version
to override it.
"""
try:
volume_bdm = self._create_volume_bdm(
context, instance, device, volume, disk_bus=disk_bus,
device_type=device_type, tag=tag,
delete_on_termination=delete_on_termination)
except oslo_exceptions.MessagingTimeout:
# The compute node might have already created the attachment but
# we never received the answer. In this case it is safe to delete
# the attachment as nobody will ever pick it up again.
with excutils.save_and_reraise_exception():
try:
objects.BlockDeviceMapping.get_by_volume_and_instance(
context, volume['id'], instance.uuid).destroy()
LOG.debug("Delete BDM after compute did not respond to "
f"attachment request for volume {volume['id']}")
except exception.VolumeBDMNotFound:
LOG.debug("BDM not found, ignoring removal. "
f"Error attaching volume {volume['id']}")
try:
self._check_attach_and_reserve_volume(context, volume, instance,
volume_bdm,
supports_multiattach)
self._record_action_start(
context, instance, instance_actions.ATTACH_VOLUME)
self.compute_rpcapi.attach_volume(context, instance, volume_bdm)
except Exception:
with excutils.save_and_reraise_exception():
volume_bdm.destroy()
return volume_bdm.device_name
def _attach_volume_shelved_offloaded(self, context, instance, volume, def _attach_volume_shelved_offloaded(self, context, instance, volume,
device, disk_bus, device_type, device, disk_bus, device_type,
delete_on_termination): delete_on_termination):
@@ -5252,13 +5158,13 @@ class API:
instance.uuid, instance.uuid,
dev) dev)
volume_bdm = self._create_volume_bdm( volume_bdm = self._create_volume_bdm_locally(
context, instance, device, volume, disk_bus=disk_bus, context, instance, device, volume, disk_bus=disk_bus,
device_type=device_type, is_local_creation=True, device_type=device_type,
delete_on_termination=delete_on_termination) delete_on_termination=delete_on_termination)
try: try:
self._check_attach_and_reserve_volume(context, volume, instance, compute_utils.check_attach_and_reserve_volume(context,
volume_bdm) self.volume_api, volume, instance, volume_bdm)
self._record_action_start( self._record_action_start(
context, instance, context, instance,
instance_actions.ATTACH_VOLUME) instance_actions.ATTACH_VOLUME)
@@ -5278,7 +5184,8 @@ class API:
def attach_volume(self, context, instance, volume_id, device=None, def attach_volume(self, context, instance, volume_id, device=None,
disk_bus=None, device_type=None, tag=None, disk_bus=None, device_type=None, tag=None,
supports_multiattach=False, supports_multiattach=False,
delete_on_termination=False): delete_on_termination=False,
needs_device_returned=False):
"""Attach an existing volume to an existing instance.""" """Attach an existing volume to an existing instance."""
# NOTE(vish): Fail fast if the device is not going to pass. This # NOTE(vish): Fail fast if the device is not going to pass. This
# will need to be removed along with the test if we # will need to be removed along with the test if we
@@ -5289,7 +5196,7 @@ class API:
# Make sure the volume isn't already attached to this instance # Make sure the volume isn't already attached to this instance
# because we'll use the v3.44 attachment flow in # because we'll use the v3.44 attachment flow in
# _check_attach_and_reserve_volume and Cinder will allow multiple # check_attach_and_reserve_volume and Cinder will allow multiple
# attachments between the same volume and instance but the old flow # attachments between the same volume and instance but the old flow
# API semantics don't allow that so we enforce it here. # API semantics don't allow that so we enforce it here.
# NOTE(lyarwood): Ensure that non multiattach volumes don't already # NOTE(lyarwood): Ensure that non multiattach volumes don't already
@@ -5322,10 +5229,11 @@ class API:
device_type, device_type,
delete_on_termination) delete_on_termination)
return self._attach_volume(context, instance, volume, device, return self.compute_task_api.attach_volume(context, instance,
disk_bus, device_type, tag=tag, volume, device, disk_bus, device_type, tag=tag,
supports_multiattach=supports_multiattach, supports_multiattach=supports_multiattach,
delete_on_termination=delete_on_termination) delete_on_termination=delete_on_termination,
do_cast=not needs_device_returned)
def _detach_volume_shelved_offloaded(self, context, instance, volume): def _detach_volume_shelved_offloaded(self, context, instance, volume):
"""Detach a volume from an instance in shelved offloaded state. """Detach a volume from an instance in shelved offloaded state.
+40
View File
@@ -1633,3 +1633,43 @@ def delete_arqs_if_needed(context, instance, arq_uuids=None):
{'instance': instance.uuid, {'instance': instance.uuid,
'uuid': arq_uuids}) 'uuid': arq_uuids})
cyclient.delete_arqs_by_uuid(arq_uuids) cyclient.delete_arqs_by_uuid(arq_uuids)
def check_attach_and_reserve_volume(context, volume_api, volume, instance,
bdm, supports_multiattach=False,
validate_az=True):
"""Perform checks against the instance and volume before attaching.
If validation succeeds, the bdm is updated with an attachment_id which
effectively reserves it during the attach process in cinder.
:param context: nova auth RequestContext
:param volume_api: cinder API object
:param volume: volume dict from cinder
:param instance: Instance object
:param bdm: BlockDeviceMapping object
:param supports_multiattach: True if the request supports multiattach
volumes, i.e. microversion >= 2.60, False otherwise
:param validate_az: True if the instance and volume availability zones
should be validated for cross_az_attach, False to not validate AZ
"""
volume_id = volume['id']
if validate_az:
volume_api.check_availability_zone(context, volume,
instance=instance)
# If volume.multiattach=True and the microversion to
# support multiattach is not used, fail the request.
if volume['multiattach'] and not supports_multiattach:
raise exception.MultiattachNotSupportedOldMicroversion()
attachment_id = volume_api.attachment_create(
context, volume_id, instance.uuid)['id']
bdm.attachment_id = attachment_id
# NOTE(ildikov): In case of boot from volume the BDM at this
# point is not yet created in a cell database, so we can't
# call save(). When attaching a volume to an existing
# instance, the instance is already in a cell and the BDM has
# been created in that same cell so updating here in that case
# is "ok".
if bdm.obj_attr_is_set('id'):
bdm.save()
+8
View File
@@ -188,3 +188,11 @@ class ComputeTaskAPI(object):
self, ctxt, instance, migration): self, ctxt, instance, migration):
self.conductor_compute_rpcapi.revert_snapshot_based_resize( self.conductor_compute_rpcapi.revert_snapshot_based_resize(
ctxt, instance, migration) ctxt, instance, migration)
def attach_volume(self, ctxt, instance, volume, device, disk_bus,
device_type, tag=None, supports_multiattach=False,
delete_on_termination=False, do_cast=False):
return self.conductor_compute_rpcapi.attach_volume(ctxt, instance,
volume, device, disk_bus, device_type, tag=tag,
supports_multiattach=supports_multiattach,
delete_on_termination=delete_on_termination, do_cast=do_cast)
+60 -1
View File
@@ -237,7 +237,7 @@ class ComputeTaskManager:
may involve coordinating activities on multiple compute nodes. may involve coordinating activities on multiple compute nodes.
""" """
target = messaging.Target(namespace='compute_task', version='1.25') target = messaging.Target(namespace='compute_task', version='1.26')
def __init__(self): def __init__(self):
self.compute_rpcapi = compute_rpcapi.ComputeAPI() self.compute_rpcapi = compute_rpcapi.ComputeAPI()
@@ -2217,3 +2217,62 @@ class ComputeTaskManager:
task = cross_cell_migrate.RevertResizeTask( task = cross_cell_migrate.RevertResizeTask(
context, instance, migration, self.notifier, self.compute_rpcapi) context, instance, migration, self.notifier, self.compute_rpcapi)
task.execute() task.execute()
def _create_volume_bdm(self, context, instance, device, volume,
disk_bus, device_type, tag=None,
delete_on_termination=False):
volume_id = volume['id']
# NOTE(vish): This is done on the compute host because we want
# to avoid a race where two devices are requested at
# the same time. When db access is removed from
# compute, the bdm will be created here and we will
# have to make sure that they are assigned atomically.
volume_bdm = self.compute_rpcapi.reserve_block_device_name(
context, instance, device, volume_id, disk_bus=disk_bus,
device_type=device_type, tag=tag,
multiattach=volume['multiattach'])
volume_bdm.delete_on_termination = delete_on_termination
volume_bdm.save()
return volume_bdm
@targets_cell
def attach_volume(self, context, instance, volume, device, disk_bus,
device_type, tag=None, supports_multiattach=False,
delete_on_termination=False):
"""Attach an existing volume to an existing instance.
This method lives in conductor so it can do the synchronous call to
compute for reserving a block-device mapping instead of having nova-api
do it starting with API version 2.101.
"""
try:
volume_bdm = self._create_volume_bdm(
context, instance, device, volume, disk_bus=disk_bus,
device_type=device_type, tag=tag,
delete_on_termination=delete_on_termination)
except messaging.exceptions.MessagingTimeout:
# The compute node might have already created the attachment but
# we never received the answer. In this case it is safe to delete
# the attachment as nobody will ever pick it up again.
with excutils.save_and_reraise_exception():
try:
objects.BlockDeviceMapping.get_by_volume_and_instance(
context, volume['id'], instance.uuid).destroy()
LOG.debug("Delete BDM after compute did not respond to "
f"attachment request for volume {volume['id']}")
except exception.VolumeBDMNotFound:
LOG.debug("BDM not found, ignoring removal. "
f"Error attaching volume {volume['id']}")
try:
compute_utils.check_attach_and_reserve_volume(
context, self.volume_api, volume, instance, volume_bdm,
supports_multiattach)
objects.InstanceAction.action_start(
context, instance.uuid, instance_actions.ATTACH_VOLUME,
want_result=False)
self.compute_rpcapi.attach_volume(context, instance, volume_bdm)
except Exception:
with excutils.save_and_reraise_exception():
volume_bdm.destroy()
return volume_bdm.device_name
+21
View File
@@ -288,6 +288,7 @@ class ComputeTaskAPI(object):
1.23 - Added revert_snapshot_based_resize() 1.23 - Added revert_snapshot_based_resize()
1.24 - Add reimage_boot_volume parameter to rebuild_instance() 1.24 - Add reimage_boot_volume parameter to rebuild_instance()
1.25 - Add target_state parameter to rebuild_instance() 1.25 - Add target_state parameter to rebuild_instance()
1.26 - Added attach_volume()
""" """
def __init__(self): def __init__(self):
@@ -494,3 +495,23 @@ class ComputeTaskAPI(object):
kw = {'instance': instance, 'migration': migration} kw = {'instance': instance, 'migration': migration}
cctxt = self.client.prepare(version=version) cctxt = self.client.prepare(version=version)
cctxt.cast(ctxt, 'revert_snapshot_based_resize', **kw) cctxt.cast(ctxt, 'revert_snapshot_based_resize', **kw)
def attach_volume(self, ctxt, instance, volume, device, disk_bus,
device_type, tag=None, supports_multiattach=False,
delete_on_termination=False, do_cast=False):
version = '1.26'
if not self.client.can_send_version(version):
raise exception.ServiceTooOld(_('nova-conductor too old'))
kw = {'instance': instance, 'volume': volume, 'device': device,
'disk_bus': disk_bus, 'device_type': device_type, 'tag': tag,
'supports_multiattach': supports_multiattach,
'delete_on_termination': delete_on_termination}
if do_cast:
cctxt = self.client.prepare(version=version)
return cctxt.cast(ctxt, 'attach_volume', **kw)
# NOTE(jkulik): We call nova-compute's reserve_block_device_name(),
# which uses long_rpc_timeout and thus we need a long_rpc_timeout, too.
cctxt = self.client.prepare(
version=version, call_monitor_timeout=CONF.rpc_response_timeout,
timeout=CONF.long_rpc_timeout)
return cctxt.call(ctxt, 'attach_volume', **kw)
+1 -1
View File
@@ -384,7 +384,7 @@ class TestOpenStackClient(object):
def post_server_volume(self, server_id, volume_attachment): def post_server_volume(self, server_id, volume_attachment):
return self.api_post('/servers/%s/os-volume_attachments' % return self.api_post('/servers/%s/os-volume_attachments' %
(server_id), volume_attachment (server_id), volume_attachment
).body['volumeAttachment'] )
def put_server_volume(self, server_id, original_volume_id, volume_id): def put_server_volume(self, server_id, original_volume_id, volume_id):
return self.api_put('/servers/%s/os-volume_attachments/%s' % return self.api_put('/servers/%s/os-volume_attachments/%s' %
@@ -0,0 +1,7 @@
{
"volumeAttachment": {
"volumeId": "%(volume_id)s",
"tag": "%(tag)s",
"delete_on_termination": true
}
}
@@ -0,0 +1,22 @@
{
"volumeAttachments": [
{
"device": "%(device)s",
"serverId": "%(uuid)s",
"tag": "%(tag)s",
"volumeId": "%(volume_id)s",
"delete_on_termination": true,
"attachment_id": "%(uuid)s",
"bdm_uuid": "%(uuid)s"
},
{
"device": "%(text)s",
"serverId": "%(uuid)s",
"tag": null,
"volumeId": "%(volume_id2)s",
"delete_on_termination": false,
"attachment_id": "%(uuid)s",
"bdm_uuid": "%(uuid)s"
}
]
}
@@ -0,0 +1,10 @@
{
"volumeAttachment": {
"volumeId": "%(volume_id)s",
"id": "%(volume_id)s",
"serverId": "%(server_id)s",
"device": "%(device)s",
"tag": "%(tag)s",
"delete_on_termination": true
}
}
@@ -0,0 +1,5 @@
{
"volumeAttachment": {
"volumeId": "%(new_volume_id)s"
}
}
@@ -0,0 +1,11 @@
{
"volumeAttachment": {
"device": "%(device)s",
"serverId": "%(uuid)s",
"tag": "%(tag)s",
"volumeId": "%(volume_id)s",
"delete_on_termination": true,
"attachment_id": "%(uuid)s",
"bdm_uuid": "%(uuid)s"
}
}
@@ -159,3 +159,22 @@ class VolumeAttachmentsSampleV289(VolumeAttachmentsSampleV285):
""" """
microversion = '2.89' microversion = '2.89'
scenarios = [('v2_89', {'api_major_version': 'v2.1'})] scenarios = [('v2_89', {'api_major_version': 'v2.1'})]
class VolumeAttachmentsSampleV2101(VolumeAttachmentsSampleV289):
"""Microversion 2.101 removes the body when creating an attachment"""
microversion = '2.101'
scenarios = [('v2_101', {'api_major_version': 'v2.1'})]
def test_attach_volume_to_server(self):
subs = {
'volume_id': self.OLD_VOLUME_ID,
'device': '/dev/sdb'
}
subs = self._get_vol_attachment_subs(subs)
response = self._do_post(
'servers/%s/os-volume_attachments' % self.server_id,
'attach-volume-to-server-req', subs)
self.assertEqual(202, response.status_code)
self.assertEqual("", response.text)
return subs
+1 -2
View File
@@ -585,14 +585,13 @@ class InstanceHelperMixin:
def _attach_volume(self, server, volume_id): def _attach_volume(self, server, volume_id):
"""attach a cinder volume to a server.""" """attach a cinder volume to a server."""
attachment = self.api.post_server_volume( self.api.post_server_volume(
server['id'], server['id'],
{'volumeAttachment': {'volumeId': volume_id}} {'volumeAttachment': {'volumeId': volume_id}}
) )
self._wait_for_volume_attach(server['id'], volume_id) self._wait_for_volume_attach(server['id'], volume_id)
self.notifier.wait_for_versioned_notifications( self.notifier.wait_for_versioned_notifications(
'instance.volume_attach.end') 'instance.volume_attach.end')
return attachment
def _rebuild_server(self, server, image_uuid, expected_state='ACTIVE'): def _rebuild_server(self, server, image_uuid, expected_state='ACTIVE'):
"""Rebuild a server.""" """Rebuild a server."""
@@ -266,14 +266,18 @@ class VolumeAttachTestsV21(test.NoDBTestCase):
self.stub_out( self.stub_out(
'nova.compute.api.API.attach_volume', 'nova.compute.api.API.attach_volume',
lambda self, context, instance, volume_id, device, tag=None, lambda self, context, instance, volume_id, device, tag=None,
supports_multiattach=False, delete_on_termination=False: None) supports_multiattach=False, delete_on_termination=False,
needs_device_returned=True: None)
body = { body = {
'volumeAttachment': { 'volumeAttachment': {
'volumeId': FAKE_UUID_A, 'volumeId': FAKE_UUID_A,
'device': '/dev/fake'}} 'device': '/dev/fake'}}
result = self.controller.create(self.req, FAKE_UUID, body=body) result = self.controller.create(self.req, FAKE_UUID, body=body)
self.assertEqual('00000000-aaaa-aaaa-aaaa-000000000000', if api_version_request.is_supported(self.req, '2.101'):
result['volumeAttachment']['id']) self.assertEqual("", result.text)
else:
self.assertEqual('00000000-aaaa-aaaa-aaaa-000000000000',
result['volumeAttachment']['id'])
@mock.patch.object(compute_api.API, 'attach_volume', @mock.patch.object(compute_api.API, 'attach_volume',
side_effect=exception.VolumeTaggedAttachNotSupported()) side_effect=exception.VolumeTaggedAttachNotSupported())
@@ -332,9 +336,13 @@ class VolumeAttachTestsV21(test.NoDBTestCase):
'volumeId': FAKE_UUID_A, 'volumeId': FAKE_UUID_A,
'device': None}} 'device': None}}
result = self.controller.create(self.req, FAKE_UUID, body=body) result = self.controller.create(self.req, FAKE_UUID, body=body)
self.assertEqual('00000000-aaaa-aaaa-aaaa-000000000000', if api_version_request.is_supported(self.req, '2.101'):
result['volumeAttachment']['id']) self.assertEqual("", result.text)
self.assertEqual('/dev/myfake', result['volumeAttachment']['device']) else:
self.assertEqual('00000000-aaaa-aaaa-aaaa-000000000000',
result['volumeAttachment']['id'])
self.assertEqual('/dev/myfake',
result['volumeAttachment']['device'])
@mock.patch.object(compute_api.API, 'attach_volume', @mock.patch.object(compute_api.API, 'attach_volume',
side_effect=exception.InstanceIsLocked( side_effect=exception.InstanceIsLocked(
@@ -348,11 +356,14 @@ class VolumeAttachTestsV21(test.NoDBTestCase):
self.req, FAKE_UUID, body=body) self.req, FAKE_UUID, body=body)
supports_multiattach = api_version_request.is_supported( supports_multiattach = api_version_request.is_supported(
self.req, '2.60') self.req, '2.60')
needs_device_returned = not api_version_request.is_supported(
self.req, '2.101')
mock_attach_volume.assert_called_once_with( mock_attach_volume.assert_called_once_with(
self.req.environ['nova.context'], self.req.environ['nova.context'],
test.MatchType(objects.Instance), FAKE_UUID_A, '/dev/fake', test.MatchType(objects.Instance), FAKE_UUID_A, '/dev/fake',
supports_multiattach=supports_multiattach, supports_multiattach=supports_multiattach,
delete_on_termination=False, tag=None) delete_on_termination=False, tag=None,
needs_device_returned=needs_device_returned)
def test_attach_volume_bad_id(self): def test_attach_volume_bad_id(self):
self.stub_out( self.stub_out(
@@ -772,7 +783,7 @@ class VolumeAttachTestsV279(VolumeAttachTestsV275):
mock_attach_volume.assert_called_once_with( mock_attach_volume.assert_called_once_with(
req.environ['nova.context'], test.MatchType(objects.Instance), req.environ['nova.context'], test.MatchType(objects.Instance),
FAKE_UUID_A, None, tag=None, supports_multiattach=True, FAKE_UUID_A, None, tag=None, supports_multiattach=True,
delete_on_termination=False) delete_on_termination=False, needs_device_returned=True)
@mock.patch('nova.compute.api.API.attach_volume', return_value=None) @mock.patch('nova.compute.api.API.attach_volume', return_value=None)
def test_attach_volume_with_delete_on_termination_default_value( def test_attach_volume_with_delete_on_termination_default_value(
@@ -784,11 +795,16 @@ class VolumeAttachTestsV279(VolumeAttachTestsV275):
body = {'volumeAttachment': {'volumeId': FAKE_UUID_A}} body = {'volumeAttachment': {'volumeId': FAKE_UUID_A}}
req = self._get_req(body) req = self._get_req(body)
result = self.controller.create(req, FAKE_UUID, body=body) result = self.controller.create(req, FAKE_UUID, body=body)
self.assertFalse(result['volumeAttachment']['delete_on_termination']) needs_device_returned = not api_version_request.is_supported(
self.req, '2.101')
if needs_device_returned:
self.assertFalse(
result['volumeAttachment']['delete_on_termination'])
mock_attach_volume.assert_called_once_with( mock_attach_volume.assert_called_once_with(
req.environ['nova.context'], test.MatchType(objects.Instance), req.environ['nova.context'], test.MatchType(objects.Instance),
FAKE_UUID_A, None, tag=None, supports_multiattach=True, FAKE_UUID_A, None, tag=None, supports_multiattach=True,
delete_on_termination=False) delete_on_termination=False,
needs_device_returned=needs_device_returned)
def test_create_volume_attach_invalid_delete_on_termination_empty(self): def test_create_volume_attach_invalid_delete_on_termination_empty(self):
body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, body = {'volumeAttachment': {'volumeId': FAKE_UUID_A,
@@ -824,11 +840,16 @@ class VolumeAttachTestsV279(VolumeAttachTestsV275):
'delete_on_termination': True}} 'delete_on_termination': True}}
req = self._get_req(body) req = self._get_req(body)
result = self.controller.create(req, FAKE_UUID, body=body) result = self.controller.create(req, FAKE_UUID, body=body)
self.assertTrue(result['volumeAttachment']['delete_on_termination']) needs_device_returned = not api_version_request.is_supported(
self.req, '2.101')
if needs_device_returned:
self.assertTrue(
result['volumeAttachment']['delete_on_termination'])
mock_attach_volume.assert_called_once_with( mock_attach_volume.assert_called_once_with(
req.environ['nova.context'], test.MatchType(objects.Instance), req.environ['nova.context'], test.MatchType(objects.Instance),
FAKE_UUID_A, None, tag=None, supports_multiattach=True, FAKE_UUID_A, None, tag=None, supports_multiattach=True,
delete_on_termination=True) delete_on_termination=True,
needs_device_returned=needs_device_returned)
def test_show_pre_v279(self): def test_show_pre_v279(self):
"""Before microversion 2.79, show a detail of a volume attachment """Before microversion 2.79, show a detail of a volume attachment
@@ -1318,6 +1339,26 @@ class VolumeAttachTestsV289(VolumeAttachTestsV285):
) )
class VolumeAttachTestsV2101(VolumeAttachTestsV289):
microversion = '2.101'
def setUp(self):
super().setUp()
self.controller = volume_attachments.VolumeAttachmentController()
@mock.patch('nova.compute.api.API.attach_volume', return_value=None)
def test_atttach_volume_v2101(self, mock_attach_volume):
body = {'volumeAttachment': {'volumeId': FAKE_UUID_A}}
req = self._get_req(body)
result = self.controller.create(req, FAKE_UUID, body=body)
self.assertEqual(202, result.status_int)
self.assertEqual("", result.text)
mock_attach_volume.assert_called_once_with(
req.environ['nova.context'], test.MatchType(objects.Instance),
FAKE_UUID_A, None, tag=None, supports_multiattach=True,
delete_on_termination=False, needs_device_returned=False)
class SwapVolumeMultiattachTestCase(test.NoDBTestCase): class SwapVolumeMultiattachTestCase(test.NoDBTestCase):
@mock.patch('nova.api.openstack.common.get_instance') @mock.patch('nova.api.openstack.common.get_instance')
+15 -120
View File
@@ -412,33 +412,6 @@ class _ComputeAPIUnitTestMixIn(object):
# 'get_pci_numa_policy_constraint' is only called in this method. # 'get_pci_numa_policy_constraint' is only called in this method.
mock_pci.assert_called_once() mock_pci.assert_called_once()
@mock.patch('nova.objects.BlockDeviceMapping.save')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
def test_create_volume_bdm_call_reserve_dev_name(self, mock_reserve,
mock_bdm_save):
bdm = objects.BlockDeviceMapping(
**fake_block_device.FakeDbBlockDeviceDict(
{
'id': 1,
'volume_id': 1,
'source_type': 'volume',
'destination_type': 'volume',
'device_name': 'vda',
'boot_index': 1,
}))
mock_reserve.return_value = bdm
instance = self._create_instance_obj()
volume = {'id': '1', 'multiattach': False}
result = self.compute_api._create_volume_bdm(self.context,
instance,
'vda',
volume,
None,
None)
self.assertTrue(mock_reserve.called)
self.assertEqual(result, bdm)
mock_bdm_save.assert_called_once_with()
@mock.patch.object(objects.BlockDeviceMapping, 'create') @mock.patch.object(objects.BlockDeviceMapping, 'create')
def test_create_volume_bdm_local_creation(self, bdm_create): def test_create_volume_bdm_local_creation(self, bdm_create):
instance = self._create_instance_obj() instance = self._create_instance_obj()
@@ -455,57 +428,42 @@ class _ComputeAPIUnitTestMixIn(object):
'disk_bus': None, 'disk_bus': None,
'device_type': None 'device_type': None
})) }))
result = self.compute_api._create_volume_bdm(self.context, result = self.compute_api._create_volume_bdm_locally(self.context,
instance, instance,
'/dev/vda', '/dev/vda',
{'id': volume_id}, {'id': volume_id},
None, None,
None, None)
is_local_creation=True)
self.assertEqual(result.instance_uuid, bdm.instance_uuid) self.assertEqual(result.instance_uuid, bdm.instance_uuid)
self.assertIsNone(result.device_name) self.assertIsNone(result.device_name)
self.assertEqual(result.volume_id, bdm.volume_id) self.assertEqual(result.volume_id, bdm.volume_id)
self.assertTrue(bdm_create.called) self.assertTrue(bdm_create.called)
@mock.patch.object(compute_api.API, '_record_action_start')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume') @mock.patch.object(nova.conductor.api.ComputeTaskAPI, 'attach_volume')
def test_attach_volume_new_flow( def test_attach_volume_new_flow(self, mock_attach, mock_get_by_volume):
self, mock_attach, mock_get_by_volume, mock_reserve, mock_record
):
mock_get_by_volume.side_effect = exception.VolumeBDMNotFound( mock_get_by_volume.side_effect = exception.VolumeBDMNotFound(
volume_id='fake-volume-id') volume_id='fake-volume-id')
instance = self._create_instance_obj() instance = self._create_instance_obj()
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol', volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None) None, None, None, None, None)
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_reserve.return_value = fake_bdm
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api', mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
mock.MagicMock(spec=cinder.API)) mock.MagicMock(spec=cinder.API))
with mock_volume_api as mock_v_api: with mock_volume_api as mock_v_api:
mock_v_api.get.return_value = volume mock_v_api.get.return_value = volume
mock_v_api.attachment_create.return_value = \
{'id': uuids.attachment_id}
self.compute_api.attach_volume( self.compute_api.attach_volume(
self.context, instance, volume['id']) self.context, instance, volume['id'])
mock_v_api.check_availability_zone.assert_called_once_with(
self.context, volume, instance=instance)
mock_v_api.attachment_create.assert_called_once_with(self.context,
volume['id'],
instance.uuid)
mock_attach.assert_called_once_with(self.context, mock_attach.assert_called_once_with(self.context,
instance, fake_bdm) instance, volume, None, None, None, tag=None,
supports_multiattach=False, delete_on_termination=False,
do_cast=True)
@mock.patch.object(compute_api.API, '_record_action_start')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume') @mock.patch.object(nova.conductor.api.ComputeTaskAPI, 'attach_volume')
def test_tagged_volume_attach_new_flow( def test_tagged_volume_attach_new_flow(
self, mock_attach, mock_get_by_volume, mock_reserve, mock_record self, mock_attach, mock_get_by_volume
): ):
mock_get_by_volume.side_effect = exception.VolumeBDMNotFound( mock_get_by_volume.side_effect = exception.VolumeBDMNotFound(
volume_id='fake-volume-id') volume_id='fake-volume-id')
@@ -513,9 +471,6 @@ class _ComputeAPIUnitTestMixIn(object):
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol', volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None) None, None, None, None, None)
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_reserve.return_value = fake_bdm
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api', mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
mock.MagicMock(spec=cinder.API)) mock.MagicMock(spec=cinder.API))
@@ -525,35 +480,22 @@ class _ComputeAPIUnitTestMixIn(object):
{'id': uuids.attachment_id} {'id': uuids.attachment_id}
self.compute_api.attach_volume( self.compute_api.attach_volume(
self.context, instance, volume['id'], tag='foo') self.context, instance, volume['id'], tag='foo')
mock_reserve.assert_called_once_with(self.context, instance, None,
volume['id'],
device_type=None,
disk_bus=None, tag='foo',
multiattach=False)
mock_v_api.check_availability_zone.assert_called_once_with(
self.context, volume, instance=instance)
mock_v_api.attachment_create.assert_called_once_with(
self.context, volume['id'], instance.uuid)
mock_attach.assert_called_once_with(self.context, mock_attach.assert_called_once_with(self.context,
instance, fake_bdm) instance, volume, None, None, None, tag='foo',
supports_multiattach=False, delete_on_termination=False,
do_cast=True)
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name') @mock.patch.object(nova.conductor.api.ComputeTaskAPI, 'attach_volume')
@mock.patch.object(
objects.BlockDeviceMapping, 'get_by_volume_and_instance')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
def test_attach_volume_reserve_bdm_timeout( def test_attach_volume_timeout(self, mock_get_by_volume, mock_attach):
self, mock_get_by_volume, mock_get_by_volume_and_instance,
mock_reserve):
mock_get_by_volume.side_effect = exception.VolumeBDMNotFound( mock_get_by_volume.side_effect = exception.VolumeBDMNotFound(
volume_id='fake-volume-id') volume_id='fake-volume-id')
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_get_by_volume_and_instance.return_value = fake_bdm
instance = self._create_instance_obj() instance = self._create_instance_obj()
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol', volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None) None, None, None, None, None)
mock_reserve.side_effect = oslo_exceptions.MessagingTimeout() mock_attach.side_effect = oslo_exceptions.MessagingTimeout()
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api', mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
mock.MagicMock(spec=cinder.API)) mock.MagicMock(spec=cinder.API))
@@ -563,40 +505,6 @@ class _ComputeAPIUnitTestMixIn(object):
self.assertRaises(oslo_exceptions.MessagingTimeout, self.assertRaises(oslo_exceptions.MessagingTimeout,
self.compute_api.attach_volume, self.compute_api.attach_volume,
self.context, instance, volume['id']) self.context, instance, volume['id'])
mock_get_by_volume_and_instance.assert_called_once_with(
self.context, volume['id'], instance.uuid)
fake_bdm.destroy.assert_called_once_with()
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
def test_attach_volume_attachment_create_fails(
self, mock_attach, mock_get_by_volume, mock_reserve
):
mock_get_by_volume.side_effect = exception.VolumeBDMNotFound(
volume_id='fake-volume-id')
instance = self._create_instance_obj()
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None)
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_reserve.return_value = fake_bdm
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
mock.MagicMock(spec=cinder.API))
with mock_volume_api as mock_v_api:
mock_v_api.get.return_value = volume
mock_v_api.attachment_create.side_effect = test.TestingException()
self.assertRaises(test.TestingException,
self.compute_api.attach_volume,
self.context, instance, volume['id'])
mock_v_api.check_availability_zone.assert_called_once_with(
self.context, volume, instance=instance)
mock_v_api.attachment_create.assert_called_once_with(
self.context, volume['id'], instance.uuid)
self.assertEqual(0, mock_attach.call_count)
fake_bdm.destroy.assert_called_once_with()
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
def test_attach_volume_bdm_exists(self, mock_by_volume): def test_attach_volume_bdm_exists(self, mock_by_volume):
@@ -7949,19 +7857,6 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
mock_record.assert_called_once_with( mock_record.assert_called_once_with(
self.context, instance, instance_actions.DETACH_INTERFACE) self.context, instance, instance_actions.DETACH_INTERFACE)
def test_check_attach_and_reserve_volume_multiattach_old_version(self):
"""Tests that _check_attach_and_reserve_volume fails if trying
to use a multiattach volume with a microversion<2.60.
"""
instance = self._create_instance_obj()
volume = {'id': uuids.volumeid, 'multiattach': True}
bdm = objects.BlockDeviceMapping(volume_id=uuids.volumeid,
instance_uuid=instance.uuid)
self.assertRaises(exception.MultiattachNotSupportedOldMicroversion,
self.compute_api._check_attach_and_reserve_volume,
self.context, volume, instance, bdm,
supports_multiattach=False)
@mock.patch('nova.volume.cinder.API.get', @mock.patch('nova.volume.cinder.API.get',
return_value={'id': uuids.volumeid, 'multiattach': True}) return_value={'id': uuids.volumeid, 'multiattach': True})
def test_attach_volume_shelved_offloaded_fails( def test_attach_volume_shelved_offloaded_fails(
+24 -75
View File
@@ -2208,8 +2208,8 @@ class ComputeTestCase(BaseTestCase,
bdms = [] bdms = []
def fake_rpc_reserve_block_device_name(self, context, instance, device, def fake_rpc_attach_volume(self, context, instance, volume, device,
volume_id, **kwargs): disk_bus, device_type, **kwargs):
bdm = objects.BlockDeviceMapping( bdm = objects.BlockDeviceMapping(
**{'context': context, **{'context': context,
'source_type': 'volume', 'source_type': 'volume',
@@ -2229,9 +2229,9 @@ class ComputeTestCase(BaseTestCase,
self.stub_out('nova.volume.cinder.API.terminate_connection', self.stub_out('nova.volume.cinder.API.terminate_connection',
fake_terminate_connection) fake_terminate_connection)
self.stub_out('nova.volume.cinder.API.detach', fake_detach) self.stub_out('nova.volume.cinder.API.detach', fake_detach)
self.stub_out('nova.compute.rpcapi.ComputeAPI.' self.stub_out('nova.conductor.rpcapi.ComputeTaskAPI.'
'reserve_block_device_name', 'attach_volume',
fake_rpc_reserve_block_device_name) fake_rpc_attach_volume)
self.compute_api.attach_volume(self.context, instance, 1, self.compute_api.attach_volume(self.context, instance, 1,
'/dev/vdc') '/dev/vdc')
@@ -11783,13 +11783,6 @@ class ComputeAPITestCase(BaseTestCase):
{'port_id': port_id, 'pci_req': pci_req}) {'port_id': port_id, 'pci_req': pci_req})
def test_attach_volume_new_flow(self): def test_attach_volume_new_flow(self):
fake_bdm = fake_block_device.FakeDbBlockDeviceDict(
{'source_type': 'volume', 'destination_type': 'volume',
'volume_id': uuids.volume_id, 'device_name': '/dev/vdb'})
bdm = block_device_obj.BlockDeviceMapping()._from_db_object(
self.context,
block_device_obj.BlockDeviceMapping(),
fake_bdm)
instance = self._create_fake_instance_obj() instance = self._create_fake_instance_obj()
instance.id = 42 instance.id = 42
fake_volume = {'id': uuids.volume, 'multiattach': False} fake_volume = {'id': uuids.volume, 'multiattach': False}
@@ -11798,46 +11791,23 @@ class ComputeAPITestCase(BaseTestCase):
mock.patch.object(cinder.API, 'get', return_value=fake_volume), mock.patch.object(cinder.API, 'get', return_value=fake_volume),
mock.patch.object(objects.BlockDeviceMapping, mock.patch.object(objects.BlockDeviceMapping,
'get_by_volume_and_instance'), 'get_by_volume_and_instance'),
mock.patch.object(cinder.API, 'check_availability_zone'), mock.patch.object(self.compute_api.compute_task_api,
mock.patch.object(cinder.API, 'attachment_create', 'attach_volume')
return_value={'id': uuids.attachment_id}), ) as (mock_get, mock_no_bdm, mock_attach):
mock.patch.object(objects.BlockDeviceMapping, 'save'),
mock.patch.object(compute_rpcapi.ComputeAPI,
'reserve_block_device_name', return_value=bdm),
mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
) as (mock_get, mock_no_bdm,
mock_check_availability_zone, mock_attachment_create,
mock_bdm_save, mock_reserve_bdm, mock_attach):
mock_no_bdm.side_effect = exception.VolumeBDMNotFound( mock_no_bdm.side_effect = exception.VolumeBDMNotFound(
volume_id=uuids.volume) volume_id=uuids.volume)
self.compute_api.attach_volume( self.compute_api.attach_volume(
self.context, instance, uuids.volume, self.context, instance, uuids.volume,
'/dev/vdb', 'ide', 'cdrom') '/dev/vdb', 'ide', 'cdrom')
mock_reserve_bdm.assert_called_once_with(
self.context, instance, '/dev/vdb', uuids.volume,
disk_bus='ide', device_type='cdrom', tag=None,
multiattach=False)
self.assertEqual(mock_get.call_args, self.assertEqual(mock_get.call_args,
mock.call(self.context, uuids.volume)) mock.call(self.context, uuids.volume))
self.assertEqual(mock_check_availability_zone.call_args, mock_attach.assert_called_once_with(self.context, instance,
mock.call( fake_volume, '/dev/vdb', 'ide', 'cdrom', tag=None,
self.context, fake_volume, instance=instance)) supports_multiattach=False, delete_on_termination=False,
mock_attachment_create.assert_called_once_with(self.context, do_cast=True)
uuids.volume,
instance.uuid)
a, kw = mock_attach.call_args
self.assertEqual(a[2].device_name, '/dev/vdb')
self.assertEqual(a[2].volume_id, uuids.volume_id)
def test_attach_volume_no_device_new_flow(self): def test_attach_volume_no_device_new_flow(self):
fake_bdm = fake_block_device.FakeDbBlockDeviceDict(
{'source_type': 'volume', 'destination_type': 'volume',
'device_name': '/dev/vdb', 'volume_id': uuids.volume_id})
bdm = block_device_obj.BlockDeviceMapping()._from_db_object(
self.context,
block_device_obj.BlockDeviceMapping(),
fake_bdm)
instance = self._create_fake_instance_obj() instance = self._create_fake_instance_obj()
instance.id = 42 instance.id = 42
fake_volume = {'id': uuids.volume, 'multiattach': False} fake_volume = {'id': uuids.volume, 'multiattach': False}
@@ -11847,16 +11817,9 @@ class ComputeAPITestCase(BaseTestCase):
mock.patch.object(objects.BlockDeviceMapping, mock.patch.object(objects.BlockDeviceMapping,
'get_by_volume_and_instance', 'get_by_volume_and_instance',
side_effect=exception.VolumeBDMNotFound), side_effect=exception.VolumeBDMNotFound),
mock.patch.object(cinder.API, 'check_availability_zone'), mock.patch.object(self.compute_api.compute_task_api,
mock.patch.object(cinder.API, 'attachment_create', 'attach_volume')
return_value={'id': uuids.attachment_id}), ) as (mock_get, mock_no_bdm, mock_attach):
mock.patch.object(objects.BlockDeviceMapping, 'save'),
mock.patch.object(compute_rpcapi.ComputeAPI,
'reserve_block_device_name', return_value=bdm),
mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
) as (mock_get, mock_no_bdm,
mock_check_availability_zone, mock_attachment_create,
mock_bdm_save, mock_reserve_bdm, mock_attach):
mock_no_bdm.side_effect = exception.VolumeBDMNotFound( mock_no_bdm.side_effect = exception.VolumeBDMNotFound(
volume_id=uuids.volume) volume_id=uuids.volume)
@@ -11864,20 +11827,10 @@ class ComputeAPITestCase(BaseTestCase):
self.context, instance, uuids.volume, self.context, instance, uuids.volume,
device=None) device=None)
mock_reserve_bdm.assert_called_once_with(
self.context, instance, None, uuids.volume,
disk_bus=None, device_type=None, tag=None,
multiattach=False)
self.assertEqual(mock_get.call_args, self.assertEqual(mock_get.call_args,
mock.call(self.context, uuids.volume)) mock.call(self.context, uuids.volume))
self.assertEqual(mock_check_availability_zone.call_args,
mock.call(
self.context, fake_volume, instance=instance))
mock_attachment_create.assert_called_once_with(self.context,
uuids.volume,
instance.uuid)
a, kw = mock_attach.call_args a, kw = mock_attach.call_args
self.assertEqual(a[2].volume_id, uuids.volume_id) self.assertEqual(a[2], fake_volume)
def test_attach_volume_shelved_offloaded(self): def test_attach_volume_shelved_offloaded(self):
instance = self._create_fake_instance_obj() instance = self._create_fake_instance_obj()
@@ -11891,10 +11844,10 @@ class ComputeAPITestCase(BaseTestCase):
'instance_uuid': instance.uuid})) 'instance_uuid': instance.uuid}))
with test.nested( with test.nested(
mock.patch.object(compute.API, mock.patch.object(compute.API,
'_create_volume_bdm', '_create_volume_bdm_locally',
return_value=fake_bdm), return_value=fake_bdm),
mock.patch.object(compute.API, mock.patch.object(compute_utils,
'_check_attach_and_reserve_volume'), 'check_attach_and_reserve_volume'),
mock.patch.object(cinder.API, 'attach'), mock.patch.object(cinder.API, 'attach'),
mock.patch.object(compute_utils, 'EventReporter') mock.patch.object(compute_utils, 'EventReporter')
) as (mock_bdm_create, mock_attach_and_reserve, mock_attach, ) as (mock_bdm_create, mock_attach_and_reserve, mock_attach,
@@ -11904,9 +11857,7 @@ class ComputeAPITestCase(BaseTestCase):
self.context, instance, volume, self.context, instance, volume,
'/dev/vdb', 'ide', 'cdrom', False) '/dev/vdb', 'ide', 'cdrom', False)
mock_attach_and_reserve.assert_called_once_with(self.context, mock_attach_and_reserve.assert_called_once_with(self.context,
volume, self.compute_api.volume_api, volume, instance, fake_bdm)
instance,
fake_bdm)
mock_attach.assert_called_once_with(self.context, mock_attach.assert_called_once_with(self.context,
uuids.volume, uuids.volume,
instance.uuid, instance.uuid,
@@ -11932,10 +11883,10 @@ class ComputeAPITestCase(BaseTestCase):
with test.nested( with test.nested(
mock.patch.object(compute.API, mock.patch.object(compute.API,
'_create_volume_bdm', '_create_volume_bdm_locally',
return_value=fake_bdm), return_value=fake_bdm),
mock.patch.object(compute.API, mock.patch.object(compute_utils,
'_check_attach_and_reserve_volume', 'check_attach_and_reserve_volume',
side_effect=fake_check_attach_and_reserve), side_effect=fake_check_attach_and_reserve),
mock.patch.object(cinder.API, 'attachment_complete') mock.patch.object(cinder.API, 'attachment_complete')
) as (mock_bdm_create, mock_attach_and_reserve, mock_attach_complete): ) as (mock_bdm_create, mock_attach_and_reserve, mock_attach_complete):
@@ -11944,9 +11895,7 @@ class ComputeAPITestCase(BaseTestCase):
self.context, instance, volume, self.context, instance, volume,
'/dev/vdb', 'ide', 'cdrom', False) '/dev/vdb', 'ide', 'cdrom', False)
mock_attach_and_reserve.assert_called_once_with(self.context, mock_attach_and_reserve.assert_called_once_with(self.context,
volume, self.compute_api.volume_api, volume, instance, fake_bdm)
instance,
fake_bdm)
mock_attach_complete.assert_called_once_with( mock_attach_complete.assert_called_once_with(
self.context, uuids.attachment_id) self.context, uuids.attachment_id)
+21
View File
@@ -53,6 +53,7 @@ from nova.tests.unit import fake_instance
from nova.tests.unit import fake_network from nova.tests.unit import fake_network
from nova.tests.unit import fake_server_actions from nova.tests.unit import fake_server_actions
from nova.tests.unit.objects import test_flavor from nova.tests.unit.objects import test_flavor
from nova.volume import cinder
FAKE_IMAGE_REF = uuids.image_ref FAKE_IMAGE_REF = uuids.image_ref
@@ -1779,3 +1780,23 @@ class AcceleratorRequestTestCase(test.NoDBTestCase):
compute_utils.delete_arqs_if_needed(self.context, instance, arq_uuids) compute_utils.delete_arqs_if_needed(self.context, instance, arq_uuids)
mock_del_inst.assert_called_once_with(instance.uuid) mock_del_inst.assert_called_once_with(instance.uuid)
mock_del_uuid.assert_called_once_with(arq_uuids) mock_del_uuid.assert_called_once_with(arq_uuids)
class CheckAttachAndReserverVolumeTestCase(test.NoDBTestCase):
def setUp(self):
super(CheckAttachAndReserverVolumeTestCase, self).setUp()
self.context = context.get_admin_context()
def test_check_attach_and_reserve_volume_multiattach_old_version(self):
"""Tests that _check_attach_and_reserve_volume fails if trying
to use a multiattach volume with a microversion<2.60.
"""
instance = fake_instance.fake_instance_obj(self.context)
volume = {'id': uuids.volumeid, 'multiattach': True}
bdm = objects.BlockDeviceMapping(volume_id=uuids.volumeid,
instance_uuid=instance.uuid)
mock_volume_api = mock.MagicMock(spec=cinder.API)
self.assertRaises(exception.MultiattachNotSupportedOldMicroversion,
compute_utils.check_attach_and_reserve_volume,
self.context, mock_volume_api, volume, instance, bdm,
supports_multiattach=False)
+216
View File
@@ -61,10 +61,12 @@ from nova.tests import fixtures
from nova.tests.fixtures import cyborg as cyborg_fixture from nova.tests.fixtures import cyborg as cyborg_fixture
from nova.tests.unit.api.openstack import fakes from nova.tests.unit.api.openstack import fakes
from nova.tests.unit.compute import test_compute from nova.tests.unit.compute import test_compute
from nova.tests.unit import fake_block_device
from nova.tests.unit import fake_build_request from nova.tests.unit import fake_build_request
from nova.tests.unit import fake_instance from nova.tests.unit import fake_instance
from nova.tests.unit import fake_request_spec from nova.tests.unit import fake_request_spec
from nova.tests.unit import fake_server_actions from nova.tests.unit import fake_server_actions
from nova.tests.unit import fake_volume
from nova.tests.unit import utils as test_utils from nova.tests.unit import utils as test_utils
from nova import utils from nova import utils
from nova.volume import cinder from nova.volume import cinder
@@ -4540,6 +4542,220 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase):
disk_over_commit=None, request_spec=reqspec) disk_over_commit=None, request_spec=reqspec)
mock_execute.assert_called_once_with() mock_execute.assert_called_once_with()
@mock.patch('nova.objects.BlockDeviceMapping.save')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
def test_create_volume_bdm_call_reserve_dev_name(self, mock_reserve,
mock_bdm_save):
bdm = objects.BlockDeviceMapping(
**fake_block_device.FakeDbBlockDeviceDict(
{
'id': 1,
'volume_id': 1,
'source_type': 'volume',
'destination_type': 'volume',
'device_name': 'vda',
'boot_index': 1,
}))
mock_reserve.return_value = bdm
instance = fake_instance.fake_instance_obj(self.context)
volume = {'id': '1', 'multiattach': False}
result = self.conductor._create_volume_bdm(self.context,
instance,
'vda',
volume,
None,
None)
self.assertTrue(mock_reserve.called)
self.assertEqual(result, bdm)
mock_bdm_save.assert_called_once_with()
def test_attach_volume_no_device_new_flow(self):
fake_bdm = fake_block_device.FakeDbBlockDeviceDict(
{'source_type': 'volume', 'destination_type': 'volume',
'device_name': '/dev/vdb', 'volume_id': uuids.volume_id})
bdm = block_device_obj.BlockDeviceMapping()._from_db_object(
self.context,
block_device_obj.BlockDeviceMapping(),
fake_bdm)
instance = self._create_fake_instance_obj()
instance.id = 42
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
uuids.volume, None, None, None, None)
with test.nested(
mock.patch.object(objects.BlockDeviceMapping,
'get_by_volume_and_instance',
side_effect=exc.VolumeBDMNotFound),
mock.patch.object(cinder.API, 'check_availability_zone'),
mock.patch.object(cinder.API, 'attachment_create',
return_value={'id': uuids.attachment_id}),
mock.patch.object(objects.BlockDeviceMapping, 'save'),
mock.patch.object(compute_rpcapi.ComputeAPI,
'reserve_block_device_name', return_value=bdm),
mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
) as (mock_no_bdm,
mock_check_availability_zone, mock_attachment_create,
mock_bdm_save, mock_reserve_bdm, mock_attach):
mock_no_bdm.side_effect = exc.VolumeBDMNotFound(
volume_id=uuids.volume)
self.conductor.attach_volume(
self.context, instance, volume,
device=None, disk_bus=None, device_type=None)
mock_reserve_bdm.assert_called_once_with(
self.context, instance, None, uuids.volume,
disk_bus=None, device_type=None, tag=None,
multiattach=False)
self.assertEqual(mock_check_availability_zone.call_args,
mock.call(
self.context, volume, instance=instance))
mock_attachment_create.assert_called_once_with(self.context,
uuids.volume,
instance.uuid)
a, kw = mock_attach.call_args
self.assertEqual(a[2].volume_id, uuids.volume_id)
def test_attach_volume_new_flow(self):
fake_bdm = fake_block_device.FakeDbBlockDeviceDict(
{'source_type': 'volume', 'destination_type': 'volume',
'volume_id': uuids.volume_id, 'device_name': '/dev/vdb'})
bdm = block_device_obj.BlockDeviceMapping()._from_db_object(
self.context,
block_device_obj.BlockDeviceMapping(),
fake_bdm)
instance = self._create_fake_instance_obj()
instance.id = 42
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
uuids.volume, None, None, None, None)
with test.nested(
mock.patch.object(objects.BlockDeviceMapping,
'get_by_volume_and_instance'),
mock.patch.object(cinder.API, 'check_availability_zone'),
mock.patch.object(cinder.API, 'attachment_create',
return_value={'id': uuids.attachment_id}),
mock.patch.object(objects.BlockDeviceMapping, 'save'),
mock.patch.object(compute_rpcapi.ComputeAPI,
'reserve_block_device_name', return_value=bdm),
mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
) as (mock_no_bdm,
mock_check_availability_zone, mock_attachment_create,
mock_bdm_save, mock_reserve_bdm, mock_attach):
mock_no_bdm.side_effect = exc.VolumeBDMNotFound(
volume_id=uuids.volume)
self.conductor.attach_volume(
self.context, instance, volume,
'/dev/vdb', 'ide', 'cdrom')
mock_reserve_bdm.assert_called_once_with(
self.context, instance, '/dev/vdb', uuids.volume,
disk_bus='ide', device_type='cdrom', tag=None,
multiattach=False)
self.assertEqual(mock_check_availability_zone.call_args,
mock.call(
self.context, volume, instance=instance))
mock_attachment_create.assert_called_once_with(self.context,
uuids.volume,
instance.uuid)
a, kw = mock_attach.call_args
self.assertEqual(a[2].device_name, '/dev/vdb')
self.assertEqual(a[2].volume_id, uuids.volume_id)
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
def test_attach_volume_attachment_create_fails(
self, mock_attach, mock_get_by_volume, mock_reserve
):
mock_get_by_volume.side_effect = exc.VolumeBDMNotFound(
volume_id='fake-volume-id')
instance = fake_instance.fake_instance_obj(self.context,
vm_state=vm_states.ACTIVE)
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None)
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_reserve.return_value = fake_bdm
mock_volume_api = mock.patch.object(self.conductor, 'volume_api',
mock.MagicMock(spec=cinder.API))
with mock_volume_api as mock_v_api:
mock_v_api.attachment_create.side_effect = test.TestingException()
self.assertRaises(test.TestingException,
self.conductor.attach_volume,
self.context, instance, volume, None, None, None)
mock_v_api.check_availability_zone.assert_called_once_with(
self.context, volume, instance=instance)
mock_v_api.attachment_create.assert_called_once_with(
self.context, volume['id'], instance.uuid)
self.assertEqual(0, mock_attach.call_count)
fake_bdm.destroy.assert_called_once_with()
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
@mock.patch.object(
objects.BlockDeviceMapping, 'get_by_volume_and_instance')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
def test_attach_volume_reserve_bdm_timeout(
self, mock_get_by_volume, mock_get_by_volume_and_instance,
mock_reserve):
mock_get_by_volume.side_effect = exc.VolumeBDMNotFound(
volume_id='fake-volume-id')
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_get_by_volume_and_instance.return_value = fake_bdm
instance = fake_instance.fake_instance_obj(self.context,
vm_state=vm_states.ACTIVE)
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None)
mock_reserve.side_effect = messaging.exceptions.MessagingTimeout()
self.assertRaises(messaging.exceptions.MessagingTimeout,
self.conductor.attach_volume,
self.context, instance, volume,
None, None, None)
mock_get_by_volume_and_instance.assert_called_once_with(
self.context, volume['id'], instance.uuid)
fake_bdm.destroy.assert_called_once_with()
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
def test_tagged_volume_attach_new_flow(
self, mock_attach, mock_get_by_volume, mock_reserve
):
mock_get_by_volume.side_effect = exc.VolumeBDMNotFound(
volume_id='fake-volume-id')
instance = fake_instance.fake_instance_obj(self.context,
vm_state=vm_states.ACTIVE)
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
None, None, None, None, None)
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
mock_reserve.return_value = fake_bdm
mock_volume_api = mock.patch.object(self.conductor, 'volume_api',
mock.MagicMock(spec=cinder.API))
with mock_volume_api as mock_v_api:
mock_v_api.attachment_create.return_value = \
{'id': uuids.attachment_id}
self.conductor.attach_volume(
self.context, instance, volume, None, None, None, tag='foo')
mock_reserve.assert_called_once_with(self.context, instance, None,
volume['id'],
device_type=None,
disk_bus=None, tag='foo',
multiattach=False)
mock_v_api.check_availability_zone.assert_called_once_with(
self.context, volume, instance=instance)
mock_v_api.attachment_create.assert_called_once_with(
self.context, volume['id'], instance.uuid)
mock_attach.assert_called_once_with(self.context,
instance, fake_bdm)
class ConductorTaskRPCAPITestCase(_BaseTaskTestCase, class ConductorTaskRPCAPITestCase(_BaseTaskTestCase,
test_compute.BaseTestCase): test_compute.BaseTestCase):
@@ -0,0 +1,10 @@
---
features:
- |
A new microversion has been added to allow fully-asynchronous attachment of
volumes to a server. Previously, calling ``POST
/servers/{server_id}/os-volume_attachments`` to attach a volume would
return HTTP 200 containing a ``volumeAttachment`` object, returning a
device name. With API version 2.101 this changes to HTTP 202. The reason
is, that the device name could not be assured by any of the current drivers
and reserving the device name could block the API thread for some time.