Port volume_attachments extension to v2.1 API
This patch ports v2 volume_attachments & volume_attachment_update extension to v2.1. Unittest code modified to share testing with both v2/v2.1. API sample file and their tests have been added. Partially implements blueprint v2-on-v3-api Change-Id: Icf63529f317bc61debc665641d1c9d9f2e45bcec
This commit is contained in:
parent
90dee8d431
commit
d2367ea1d2
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"device": "/dev/vdd"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"device": "/dev/vdd",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"serverId": "0c92f3f6-c253-4c9b-bd43-e880a8d2eb0a",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"volumeAttachments": [
|
||||
{
|
||||
"device": "/dev/sdd",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
},
|
||||
{
|
||||
"device": "/dev/sdc",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f804",
|
||||
"serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804"
|
||||
}
|
||||
]
|
||||
}
|
6
doc/v3/api_samples/os-volumes/update-volume-req.json
Normal file
6
doc/v3/api_samples/os-volumes/update-volume-req.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f805",
|
||||
"device": "/dev/sdd"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"device": "/dev/sdd",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"serverId": "2390fb4d-1693-45d7-b309-e29c4af16538",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
}
|
||||
}
|
@ -295,6 +295,12 @@
|
||||
"compute_extension:volume_attachments:delete": "",
|
||||
"compute_extension:v3:os-volumes": "",
|
||||
"compute_extension:v3:os-volumes:discoverable": "",
|
||||
"compute_extension:v3:os-volumes-attachments:index": "",
|
||||
"compute_extension:v3:os-volumes-attachments:show": "",
|
||||
"compute_extension:v3:os-volumes-attachments:create": "",
|
||||
"compute_extension:v3:os-volumes-attachments:update": "",
|
||||
"compute_extension:v3:os-volumes-attachments:delete": "",
|
||||
"compute_extension:v3:os-volumes-attachments:discoverable": "",
|
||||
"compute_extension:volumetypes": "",
|
||||
"compute_extension:availability_zone:list": "",
|
||||
"compute_extension:v3:os-availability-zone:list": "",
|
||||
|
@ -23,11 +23,17 @@ from nova.api.openstack.compute.schemas.v3 import volumes as volumes_schema
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api import validation
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova import objects
|
||||
from nova.openstack.common import uuidutils
|
||||
from nova import volume
|
||||
|
||||
ALIAS = "os-volumes"
|
||||
authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
|
||||
authorize_attach = extensions.extension_authorizer('compute',
|
||||
'v3:os-volumes-attachments')
|
||||
|
||||
|
||||
def _translate_volume_detail_view(context, vol):
|
||||
@ -203,6 +209,252 @@ def _translate_attachment_summary_view(volume_id, instance_uuid, mountpoint):
|
||||
return d
|
||||
|
||||
|
||||
class VolumeAttachmentController(wsgi.Controller):
|
||||
"""The volume attachment API controller for the OpenStack API.
|
||||
|
||||
A child resource of the server. Note that we use the volume id
|
||||
as the ID of the attachment (though this is not guaranteed externally)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.compute_api = compute.API()
|
||||
self.volume_api = volume.API()
|
||||
super(VolumeAttachmentController, self).__init__()
|
||||
|
||||
@extensions.expected_errors(404)
|
||||
def index(self, req, server_id):
|
||||
"""Returns the list of volume attachments for a given instance."""
|
||||
context = req.environ['nova.context']
|
||||
authorize_attach(context, action='index')
|
||||
return self._items(req, server_id,
|
||||
entity_maker=_translate_attachment_summary_view)
|
||||
|
||||
@extensions.expected_errors(404)
|
||||
def show(self, req, server_id, id):
|
||||
"""Return data about the given volume attachment."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
authorize_attach(context, action='show')
|
||||
|
||||
volume_id = id
|
||||
instance = common.get_instance(self.compute_api, context, server_id,
|
||||
want_objects=True)
|
||||
|
||||
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
|
||||
context, instance['uuid'])
|
||||
|
||||
if not bdms:
|
||||
msg = _("Instance %s is not attached.") % server_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
assigned_mountpoint = None
|
||||
|
||||
for bdm in bdms:
|
||||
if bdm.volume_id == volume_id:
|
||||
assigned_mountpoint = bdm.device_name
|
||||
break
|
||||
|
||||
if assigned_mountpoint is None:
|
||||
msg = _("volume_id not found: %s") % volume_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return {'volumeAttachment': _translate_attachment_detail_view(
|
||||
volume_id,
|
||||
instance['uuid'],
|
||||
assigned_mountpoint)}
|
||||
|
||||
def _validate_volume_id(self, volume_id):
|
||||
if not uuidutils.is_uuid_like(volume_id):
|
||||
msg = _("Bad volumeId format: volumeId is "
|
||||
"not in proper format (%s)") % volume_id
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
@extensions.expected_errors((400, 404, 409))
|
||||
def create(self, req, server_id, body):
|
||||
"""Attach a volume to an instance."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
authorize_attach(context, action='create')
|
||||
|
||||
if not self.is_valid_body(body, 'volumeAttachment'):
|
||||
msg = _("volumeAttachment not specified")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
try:
|
||||
volume_id = body['volumeAttachment']['volumeId']
|
||||
except KeyError:
|
||||
msg = _("volumeId must be specified.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
device = body['volumeAttachment'].get('device')
|
||||
|
||||
self._validate_volume_id(volume_id)
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, server_id,
|
||||
want_objects=True)
|
||||
try:
|
||||
device = self.compute_api.attach_volume(context, instance,
|
||||
volume_id, device)
|
||||
except exception.VolumeNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=e.format_message())
|
||||
except exception.InstanceIsLocked as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
'attach_volume', server_id)
|
||||
except (exception.InvalidVolume,
|
||||
exception.InvalidDevicePath) as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
|
||||
# The attach is async
|
||||
attachment = {}
|
||||
attachment['id'] = volume_id
|
||||
attachment['serverId'] = server_id
|
||||
attachment['volumeId'] = volume_id
|
||||
attachment['device'] = device
|
||||
|
||||
# NOTE(justinsb): And now, we have a problem...
|
||||
# The attach is async, so there's a window in which we don't see
|
||||
# the attachment (until the attachment completes). We could also
|
||||
# get problems with concurrent requests. I think we need an
|
||||
# attachment state, and to write to the DB here, but that's a bigger
|
||||
# change.
|
||||
# For now, we'll probably have to rely on libraries being smart
|
||||
|
||||
# TODO(justinsb): How do I return "accepted" here?
|
||||
return {'volumeAttachment': attachment}
|
||||
|
||||
@wsgi.response(202)
|
||||
@extensions.expected_errors((400, 404, 409))
|
||||
def update(self, req, server_id, id, body):
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
authorize_attach(context, action='update')
|
||||
|
||||
if not self.is_valid_body(body, 'volumeAttachment'):
|
||||
msg = _("volumeAttachment not specified")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
old_volume_id = id
|
||||
try:
|
||||
old_volume = self.volume_api.get(context, old_volume_id)
|
||||
|
||||
try:
|
||||
new_volume_id = body['volumeAttachment']['volumeId']
|
||||
except KeyError:
|
||||
msg = _("volumeId must be specified.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
self._validate_volume_id(new_volume_id)
|
||||
new_volume = self.volume_api.get(context, new_volume_id)
|
||||
except exception.VolumeNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=e.format_message())
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, server_id,
|
||||
want_objects=True)
|
||||
|
||||
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
|
||||
context, instance.uuid)
|
||||
found = False
|
||||
try:
|
||||
for bdm in bdms:
|
||||
if bdm.volume_id != old_volume_id:
|
||||
continue
|
||||
try:
|
||||
self.compute_api.swap_volume(context, instance, old_volume,
|
||||
new_volume)
|
||||
found = True
|
||||
break
|
||||
except exception.VolumeUnattached:
|
||||
# The volume is not attached. Treat it as NotFound
|
||||
# by falling through.
|
||||
pass
|
||||
except exception.InvalidVolume as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
except exception.InstanceIsLocked as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
'swap_volume', server_id)
|
||||
|
||||
if not found:
|
||||
msg = _("The volume was either invalid or not attached to the "
|
||||
"instance.")
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
@wsgi.response(202)
|
||||
@extensions.expected_errors((400, 403, 404, 409))
|
||||
def delete(self, req, server_id, id):
|
||||
"""Detach a volume from an instance."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
authorize_attach(context, action='delete')
|
||||
|
||||
volume_id = id
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, server_id,
|
||||
want_objects=True)
|
||||
|
||||
try:
|
||||
volume = self.volume_api.get(context, volume_id)
|
||||
except exception.VolumeNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=e.format_message())
|
||||
|
||||
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
|
||||
context, instance['uuid'])
|
||||
if not bdms:
|
||||
msg = _("Instance %s is not attached.") % server_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
found = False
|
||||
try:
|
||||
for bdm in bdms:
|
||||
if bdm.volume_id != volume_id:
|
||||
continue
|
||||
if bdm.is_root:
|
||||
msg = _("Can't detach root device volume")
|
||||
raise exc.HTTPForbidden(explanation=msg)
|
||||
try:
|
||||
self.compute_api.detach_volume(context, instance, volume)
|
||||
found = True
|
||||
break
|
||||
except exception.VolumeUnattached:
|
||||
# The volume is not attached. Treat it as NotFound
|
||||
# by falling through.
|
||||
pass
|
||||
except exception.InvalidVolume as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
|
||||
except exception.InstanceIsLocked as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(state_error,
|
||||
'detach_volume', server_id)
|
||||
|
||||
if not found:
|
||||
msg = _("volume_id not found: %s") % volume_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
def _items(self, req, server_id, entity_maker):
|
||||
"""Returns a list of attachments, transformed through entity_maker."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, server_id,
|
||||
want_objects=True)
|
||||
|
||||
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
|
||||
context, instance['uuid'])
|
||||
limited_list = common.limited(bdms, req)
|
||||
results = []
|
||||
|
||||
for bdm in limited_list:
|
||||
if bdm.volume_id:
|
||||
results.append(entity_maker(bdm.volume_id,
|
||||
bdm.instance_uuid,
|
||||
bdm.device_name))
|
||||
|
||||
return {'volumeAttachments': results}
|
||||
|
||||
|
||||
def _translate_snapshot_detail_view(context, vol):
|
||||
"""Maps keys for snapshots details view."""
|
||||
|
||||
@ -322,6 +574,13 @@ class Volumes(extensions.V3APIExtensionBase):
|
||||
inherits='servers')
|
||||
resources.append(res)
|
||||
|
||||
res = extensions.ResourceExtension('os-volume_attachments',
|
||||
VolumeAttachmentController(),
|
||||
parent=dict(
|
||||
member_name='server',
|
||||
collection_name='servers'))
|
||||
resources.append(res)
|
||||
|
||||
res = extensions.ResourceExtension(
|
||||
'os-snapshots', SnapshotController(),
|
||||
collection_actions={'detail': 'GET'})
|
||||
|
@ -340,9 +340,9 @@ class VolumeApiTestV2(VolumeApiTestV21):
|
||||
return fakes.wsgi_app()
|
||||
|
||||
|
||||
class VolumeAttachTests(test.TestCase):
|
||||
class VolumeAttachTestsV21(test.TestCase):
|
||||
def setUp(self):
|
||||
super(VolumeAttachTests, self).setUp()
|
||||
super(VolumeAttachTestsV21, self).setUp()
|
||||
self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
|
||||
fake_bdms_get_all_by_instance)
|
||||
self.stubs.Set(compute_api.API, 'get', fake_get_instance)
|
||||
@ -354,9 +354,10 @@ class VolumeAttachTests(test.TestCase):
|
||||
'id': FAKE_UUID_A,
|
||||
'volumeId': FAKE_UUID_A
|
||||
}}
|
||||
self.ext_mgr = extensions.ExtensionManager()
|
||||
self.ext_mgr.extensions = {}
|
||||
self.attachments = volumes.VolumeAttachmentController(self.ext_mgr)
|
||||
self._set_up_controller()
|
||||
|
||||
def _set_up_controller(self):
|
||||
self.attachments = volumes_v3.VolumeAttachmentController()
|
||||
|
||||
def test_show(self):
|
||||
req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
|
||||
@ -423,7 +424,14 @@ class VolumeAttachTests(test.TestCase):
|
||||
req.environ['nova.context'] = self.context
|
||||
|
||||
result = self.attachments.delete(req, FAKE_UUID, FAKE_UUID_A)
|
||||
self.assertEqual('202 Accepted', result.status)
|
||||
# NOTE: on v2.1, http status code is set as wsgi_code of API
|
||||
# method instead of status_int in a response object.
|
||||
if isinstance(self.attachments,
|
||||
volumes_v3.VolumeAttachmentController):
|
||||
status_int = self.attachments.delete.wsgi_code
|
||||
else:
|
||||
status_int = result.status_int
|
||||
self.assertEqual(202, status_int)
|
||||
|
||||
def test_detach_vol_not_found(self):
|
||||
self.stubs.Set(compute_api.API,
|
||||
@ -545,7 +553,8 @@ class VolumeAttachTests(test.TestCase):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create,
|
||||
req, FAKE_UUID, body=body)
|
||||
|
||||
def _test_swap(self, uuid=FAKE_UUID_A, fake_func=None, body=None):
|
||||
def _test_swap(self, attachments, uuid=FAKE_UUID_A,
|
||||
fake_func=None, body=None):
|
||||
fake_func = fake_func or fake_swap_volume
|
||||
self.stubs.Set(compute_api.API,
|
||||
'swap_volume',
|
||||
@ -558,40 +567,57 @@ class VolumeAttachTests(test.TestCase):
|
||||
req.body = jsonutils.dumps({})
|
||||
req.headers['content-type'] = 'application/json'
|
||||
req.environ['nova.context'] = self.context
|
||||
return self.attachments.update(req, FAKE_UUID, uuid, body=body)
|
||||
return attachments.update(req, FAKE_UUID, uuid, body=body)
|
||||
|
||||
def test_swap_volume_for_locked_server(self):
|
||||
self.ext_mgr.extensions['os-volume-attachment-update'] = True
|
||||
|
||||
def fake_swap_volume_for_locked_server(self, context, instance,
|
||||
old_volume, new_volume):
|
||||
raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
|
||||
|
||||
self.ext_mgr.extensions['os-volume-attachment-update'] = True
|
||||
self.assertRaises(webob.exc.HTTPConflict, self._test_swap,
|
||||
self.attachments,
|
||||
fake_func=fake_swap_volume_for_locked_server)
|
||||
|
||||
def test_swap_volume_no_extension(self):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap)
|
||||
|
||||
def test_swap_volume(self):
|
||||
self.ext_mgr.extensions['os-volume-attachment-update'] = True
|
||||
result = self._test_swap()
|
||||
self.assertEqual('202 Accepted', result.status)
|
||||
result = self._test_swap(self.attachments)
|
||||
# NOTE: on v2.1, http status code is set as wsgi_code of API
|
||||
# method instead of status_int in a response object.
|
||||
if isinstance(self.attachments,
|
||||
volumes_v3.VolumeAttachmentController):
|
||||
status_int = self.attachments.update.wsgi_code
|
||||
else:
|
||||
status_int = result.status_int
|
||||
self.assertEqual(202, status_int)
|
||||
|
||||
def test_swap_volume_no_attachment(self):
|
||||
self.ext_mgr.extensions['os-volume-attachment-update'] = True
|
||||
|
||||
self.assertRaises(exc.HTTPNotFound, self._test_swap, FAKE_UUID_C)
|
||||
self.assertRaises(exc.HTTPNotFound, self._test_swap,
|
||||
self.attachments, FAKE_UUID_C)
|
||||
|
||||
def test_swap_volume_without_volumeId(self):
|
||||
self.ext_mgr.extensions['os-volume-attachment-update'] = True
|
||||
body = {'volumeAttachment': {'device': '/dev/fake'}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self._test_swap,
|
||||
self.attachments,
|
||||
body=body)
|
||||
|
||||
|
||||
class VolumeAttachTestsV2(VolumeAttachTestsV21):
|
||||
|
||||
def _set_up_controller(self):
|
||||
ext_mgr = extensions.ExtensionManager()
|
||||
ext_mgr.extensions = {'os-volume-attachment-update'}
|
||||
self.attachments = volumes.VolumeAttachmentController(ext_mgr)
|
||||
ext_mgr_no_update = extensions.ExtensionManager()
|
||||
ext_mgr_no_update.extensions = {}
|
||||
self.attachments_no_update = volumes.VolumeAttachmentController(
|
||||
ext_mgr_no_update)
|
||||
|
||||
def test_swap_volume_no_extension(self):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap,
|
||||
self.attachments_no_update)
|
||||
|
||||
|
||||
class VolumeSerializerTest(test.TestCase):
|
||||
def _verify_volume_attachment(self, attach, tree):
|
||||
for attr in ('id', 'volumeId', 'serverId', 'device'):
|
||||
|
@ -315,6 +315,11 @@ policy_data = """
|
||||
"compute_extension:volume_attachments:update": "",
|
||||
"compute_extension:volume_attachments:delete": "",
|
||||
"compute_extension:v3:os-volumes": "",
|
||||
"compute_extension:v3:os-volumes-attachments:index": "",
|
||||
"compute_extension:v3:os-volumes-attachments:show": "",
|
||||
"compute_extension:v3:os-volumes-attachments:create": "",
|
||||
"compute_extension:v3:os-volumes-attachments:update": "",
|
||||
"compute_extension:v3:os-volumes-attachments:delete": "",
|
||||
"compute_extension:volumetypes": "",
|
||||
"compute_extension:zones": "",
|
||||
"compute_extension:availability_zone:list": "",
|
||||
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "%(volume_id)s",
|
||||
"device": "%(device)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"device": "%(device)s",
|
||||
"id": "%(volume_id)s",
|
||||
"serverId": "%(uuid)s",
|
||||
"volumeId": "%(volume_id)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"volumeAttachments": [
|
||||
{
|
||||
"device": "/dev/sdd",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"serverId": "%(uuid)s",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
},
|
||||
{
|
||||
"device": "/dev/sdc",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f804",
|
||||
"serverId": "%(uuid)s",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "%(volume_id)s",
|
||||
"device": "%(device)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"device": "/dev/sdd",
|
||||
"id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"serverId": "%(uuid)s",
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
}
|
||||
}
|
@ -15,7 +15,14 @@
|
||||
|
||||
import datetime
|
||||
|
||||
from nova.compute import api as compute_api
|
||||
from nova.compute import manager as compute_manager
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import objects
|
||||
from nova.tests.unit.api.openstack import fakes
|
||||
from nova.tests.unit import fake_block_device
|
||||
from nova.tests.unit import fake_instance
|
||||
from nova.tests.unit.integrated.v3 import api_sample_base
|
||||
from nova.tests.unit.integrated.v3 import test_servers
|
||||
from nova.volume import cinder
|
||||
@ -182,3 +189,124 @@ class VolumesSampleJsonTest(test_servers.ServersSampleBase):
|
||||
response = self._do_delete('os-volumes/%s' % vol_id)
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertEqual(response.content, '')
|
||||
|
||||
|
||||
class VolumeAttachmentsSampleBase(test_servers.ServersSampleBase):
|
||||
def _stub_db_bdms_get_all_by_instance(self, server_id):
|
||||
|
||||
def fake_bdms_get_all_by_instance(context, instance_uuid,
|
||||
use_slave=False):
|
||||
bdms = [
|
||||
fake_block_device.FakeDbBlockDeviceDict(
|
||||
{'id': 1, 'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f803',
|
||||
'instance_uuid': server_id, 'source_type': 'volume',
|
||||
'destination_type': 'volume', 'device_name': '/dev/sdd'}),
|
||||
fake_block_device.FakeDbBlockDeviceDict(
|
||||
{'id': 2, 'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f804',
|
||||
'instance_uuid': server_id, 'source_type': 'volume',
|
||||
'destination_type': 'volume', 'device_name': '/dev/sdc'})
|
||||
]
|
||||
return bdms
|
||||
|
||||
self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
|
||||
fake_bdms_get_all_by_instance)
|
||||
|
||||
def _stub_compute_api_get(self):
|
||||
|
||||
def fake_compute_api_get(self, context, instance_id,
|
||||
want_objects=False, expected_attrs=None):
|
||||
if want_objects:
|
||||
return fake_instance.fake_instance_obj(
|
||||
context, **{'uuid': instance_id})
|
||||
else:
|
||||
return {'uuid': instance_id}
|
||||
|
||||
self.stubs.Set(compute_api.API, 'get', fake_compute_api_get)
|
||||
|
||||
|
||||
class VolumeAttachmentsSampleJsonTest(VolumeAttachmentsSampleBase):
|
||||
extension_name = "os-volumes"
|
||||
|
||||
def test_attach_volume_to_server(self):
|
||||
self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get)
|
||||
self.stubs.Set(cinder.API, 'check_attach', lambda *a, **k: None)
|
||||
self.stubs.Set(cinder.API, 'reserve_volume', lambda *a, **k: None)
|
||||
device_name = '/dev/vdd'
|
||||
bdm = objects.BlockDeviceMapping()
|
||||
bdm['device_name'] = device_name
|
||||
self.stubs.Set(compute_manager.ComputeManager,
|
||||
"reserve_block_device_name",
|
||||
lambda *a, **k: bdm)
|
||||
self.stubs.Set(compute_manager.ComputeManager,
|
||||
'attach_volume',
|
||||
lambda *a, **k: None)
|
||||
self.stubs.Set(objects.BlockDeviceMapping, 'get_by_volume_id',
|
||||
classmethod(lambda *a, **k: None))
|
||||
|
||||
volume = fakes.stub_volume_get(None, context.get_admin_context(),
|
||||
'a26887c6-c47b-4654-abb5-dfadf7d3f803')
|
||||
subs = {
|
||||
'volume_id': volume['id'],
|
||||
'device': device_name
|
||||
}
|
||||
server_id = self._post_server()
|
||||
response = self._do_post('servers/%s/os-volume_attachments'
|
||||
% server_id,
|
||||
'attach-volume-to-server-req', subs)
|
||||
|
||||
subs.update(self._get_regexes())
|
||||
self._verify_response('attach-volume-to-server-resp', subs,
|
||||
response, 200)
|
||||
|
||||
def test_list_volume_attachments(self):
|
||||
server_id = self._post_server()
|
||||
|
||||
self._stub_db_bdms_get_all_by_instance(server_id)
|
||||
|
||||
response = self._do_get('servers/%s/os-volume_attachments'
|
||||
% server_id)
|
||||
subs = self._get_regexes()
|
||||
self._verify_response('list-volume-attachments-resp', subs,
|
||||
response, 200)
|
||||
|
||||
def test_volume_attachment_detail(self):
|
||||
server_id = self._post_server()
|
||||
attach_id = "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
self._stub_db_bdms_get_all_by_instance(server_id)
|
||||
self._stub_compute_api_get()
|
||||
response = self._do_get('servers/%s/os-volume_attachments/%s'
|
||||
% (server_id, attach_id))
|
||||
subs = self._get_regexes()
|
||||
self._verify_response('volume-attachment-detail-resp', subs,
|
||||
response, 200)
|
||||
|
||||
def test_volume_attachment_delete(self):
|
||||
server_id = self._post_server()
|
||||
attach_id = "a26887c6-c47b-4654-abb5-dfadf7d3f803"
|
||||
self._stub_db_bdms_get_all_by_instance(server_id)
|
||||
self._stub_compute_api_get()
|
||||
self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get)
|
||||
self.stubs.Set(compute_api.API, 'detach_volume', lambda *a, **k: None)
|
||||
response = self._do_delete('servers/%s/os-volume_attachments/%s'
|
||||
% (server_id, attach_id))
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertEqual(response.content, '')
|
||||
|
||||
def test_volume_attachment_update(self):
|
||||
self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get)
|
||||
subs = {
|
||||
'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f805',
|
||||
'device': '/dev/sdd'
|
||||
}
|
||||
server_id = self._post_server()
|
||||
attach_id = 'a26887c6-c47b-4654-abb5-dfadf7d3f803'
|
||||
self._stub_db_bdms_get_all_by_instance(server_id)
|
||||
self._stub_compute_api_get()
|
||||
self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get)
|
||||
self.stubs.Set(compute_api.API, 'swap_volume', lambda *a, **k: None)
|
||||
response = self._do_put('servers/%s/os-volume_attachments/%s'
|
||||
% (server_id, attach_id),
|
||||
'update-volume-req',
|
||||
subs)
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertEqual(response.content, '')
|
||||
|
Loading…
Reference in New Issue
Block a user