Remove attach/detach/swap from V2.1 extended_volumes

V2.1 must be identical with V2 and must not contain any new attributes
and new method. And those methods have same methods in the volumes
extensions. Those methods are added in the early stage of v3 works,
because the in early stage v3 works decides to delete any volume related
proxy API, then adds those method instead of them. This commit remove
these methods from V2.1 API.

Partially implements blueprint v2-on-v3-api

Change-Id: I20e0498ec187e9feafd2bfb1aa9646426b3609af
This commit is contained in:
He Jie Xu 2015-03-01 12:25:13 +08:00
parent 0476bed304
commit 7007ab20f6
10 changed files with 0 additions and 567 deletions

View File

@ -1,8 +0,0 @@
{
"attach": {
"volume_id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
"device": "/dev/vdd",
"disk_bus": "ide",
"device_type": "cdrom"
}
}

View File

@ -1,5 +0,0 @@
{
"detach": {
"volume_id": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
}
}

View File

@ -1,6 +0,0 @@
{
"swap_volume_attachment": {
"old_volume_id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
"new_volume_id": "a26887c6-c47b-4654-abb5-dfadf7d3f805"
}
}

View File

@ -13,27 +13,14 @@
# under the License.
"""The Extended Volumes API extension."""
from webob import exc
from nova.api.openstack import common
from nova.api.openstack.compute.schemas.v3 import extended_volumes
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 import volume
ALIAS = "os-extended-volumes"
authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
authorize_attach = extensions.extension_authorizer('compute',
'v3:%s:attach' % ALIAS)
authorize_detach = extensions.extension_authorizer('compute',
'v3:%s:detach' % ALIAS)
authorize_swap = extensions.extension_authorizer('compute',
'v3:%s:swap' % ALIAS)
class ExtendedVolumesController(wsgi.Controller):
@ -49,55 +36,6 @@ class ExtendedVolumesController(wsgi.Controller):
key = "%s:volumes_attached" % ExtendedVolumes.alias
server[key] = [{'id': volume_id} for volume_id in volume_ids]
@wsgi.response(202)
@extensions.expected_errors((400, 404, 409))
@wsgi.action('swap_volume_attachment')
@validation.schema(extended_volumes.swap_volume_attachment)
def swap(self, req, id, body):
context = req.environ['nova.context']
authorize_swap(context)
old_volume_id = body['swap_volume_attachment']['old_volume_id']
new_volume_id = body['swap_volume_attachment']['new_volume_id']
try:
old_volume = self.volume_api.get(context, old_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, 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',
id)
if not found:
msg = _("The volume was either invalid or not attached to the "
"instance.")
raise exc.HTTPNotFound(explanation=msg)
@wsgi.extends
def show(self, req, resp_obj, id):
context = req.environ['nova.context']
@ -119,91 +57,6 @@ class ExtendedVolumesController(wsgi.Controller):
# the core API adding it in its 'detail' method.
self._extend_server(context, server, db_instance)
@extensions.expected_errors((400, 404, 409))
@wsgi.response(202)
@wsgi.action('attach')
@validation.schema(extended_volumes.attach)
def attach(self, req, id, body):
server_id = id
context = req.environ['nova.context']
authorize_attach(context)
volume_id = body['attach']['volume_id']
device = body['attach'].get('device')
disk_bus = body['attach'].get('disk_bus')
device_type = body['attach'].get('device_type')
instance = common.get_instance(self.compute_api, context, server_id,
want_objects=True)
try:
self.compute_api.attach_volume(context, instance,
volume_id, device,
disk_bus=disk_bus,
device_type=device_type)
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 as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
except exception.InvalidDevicePath as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
@extensions.expected_errors((400, 403, 404, 409))
@wsgi.response(202)
@wsgi.action('detach')
@validation.schema(extended_volumes.detach)
def detach(self, req, id, body):
server_id = id
context = req.environ['nova.context']
authorize_detach(context)
volume_id = body['detach']['volume_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 = _("Volume %(volume_id)s is not attached to the "
"instance %(server_id)s") % {'server_id': server_id,
'volume_id': volume_id}
raise exc.HTTPNotFound(explanation=msg)
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)
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)
else:
msg = _("Volume %(volume_id)s is not attached to the "
"instance %(server_id)s") % {'server_id': server_id,
'volume_id': volume_id}
raise exc.HTTPNotFound(explanation=msg)
class ExtendedVolumes(extensions.V3APIExtensionBase):
"""Extended Volumes support."""

View File

@ -1,79 +0,0 @@
# Copyright 2013 NEC Corporation. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova.api.validation import parameter_types
swap_volume_attachment = {
'type': 'object',
'properties': {
'swap_volume_attachment': {
'type': 'object',
'properties': {
'old_volume_id': parameter_types.volume_id,
'new_volume_id': parameter_types.volume_id
},
'required': ['old_volume_id', 'new_volume_id'],
'additionalProperties': False,
},
},
'required': ['swap_volume_attachment'],
'additionalProperties': False,
}
attach = {
'type': 'object',
'properties': {
'attach': {
'type': 'object',
'properties': {
'volume_id': parameter_types.volume_id,
'device': {
'type': 'string',
# NOTE: The validation pattern from match_device() in
# nova/block_device.py.
'pattern': '(^/dev/x{0,1}[a-z]{0,1}d{0,1})([a-z]+)[0-9]*$'
},
'disk_bus': {
'type': 'string'
},
'device_type': {
'type': 'string',
}
},
'required': ['volume_id'],
'additionalProperties': False,
},
},
'required': ['attach'],
'additionalProperties': False,
}
detach = {
'type': 'object',
'properties': {
'detach': {
'type': 'object',
'properties': {
'volume_id': parameter_types.volume_id
},
'required': ['volume_id'],
'additionalProperties': False,
},
},
'required': ['detach'],
'additionalProperties': False,
}

View File

@ -1,8 +0,0 @@
{
"attach": {
"volume_id": "%(volume_id)s",
"device": "%(device)s",
"disk_bus": "%(disk_bus)s",
"device_type": "%(device_type)s"
}
}

View File

@ -1,5 +0,0 @@
{
"detach": {
"volume_id": "%(volume_id)s"
}
}

View File

@ -1,6 +0,0 @@
{
"swap_volume_attachment": {
"old_volume_id": "%(old_volume_id)s",
"new_volume_id": "%(new_volume_id)s"
}
}

View File

@ -14,15 +14,11 @@
# under the License.
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.functional.v3 import test_servers
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.volume import cinder
class ExtendedVolumesSampleJsonTests(test_servers.ServersSampleBase):
@ -76,76 +72,3 @@ class ExtendedVolumesSampleJsonTests(test_servers.ServersSampleBase):
subs['id'] = uuid
subs['hostid'] = '[a-f0-9]+'
self._verify_response('servers-detail-resp', subs, response, 200)
def test_attach_volume(self):
bdm = objects.BlockDeviceMapping()
device_name = '/dev/vdd'
bdm['device_name'] = device_name
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)
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)
volume = fakes.stub_volume_get(None, context.get_admin_context(),
'a26887c6-c47b-4654-abb5-dfadf7d3f803')
subs = {
'volume_id': volume['id'],
'device': device_name,
'disk_bus': 'ide',
'device_type': 'cdrom'
}
server_id = self._post_server()
response = self._do_post('servers/%s/action'
% server_id,
'attach-volume-req', subs)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.content, '')
def test_detach_volume(self):
server_id = self._post_server()
attach_id = "a26887c6-c47b-4654-abb5-dfadf7d3f803"
self._stub_compute_api_get_instance_bdms(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)
subs = {
'volume_id': attach_id,
}
response = self._do_post('servers/%s/action'
% server_id, 'detach-volume-req', subs)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.content, '')
def test_swap_volume(self):
server_id = self._post_server()
old_volume_id = "a26887c6-c47b-4654-abb5-dfadf7d3f803"
old_new_volume = 'a26887c6-c47b-4654-abb5-dfadf7d3f805'
self._stub_compute_api_get_instance_bdms(server_id)
def stub_volume_get(self, context, volume_id):
if volume_id == old_volume_id:
return fakes.stub_volume(volume_id, instance_uuid=server_id)
else:
return fakes.stub_volume(volume_id, instance_uuid=None,
attach_status='detached')
self.stubs.Set(cinder.API, 'get', stub_volume_get)
self.stubs.Set(cinder.API, 'begin_detaching', lambda *a, **k: None)
self.stubs.Set(cinder.API, 'check_attach', lambda *a, **k: None)
self.stubs.Set(cinder.API, 'check_detach', lambda *a, **k: None)
self.stubs.Set(cinder.API, 'reserve_volume', lambda *a, **k: None)
self.stubs.Set(compute_manager.ComputeManager, 'swap_volume',
lambda *a, **k: None)
subs = {
'old_volume_id': old_volume_id,
'new_volume_id': old_new_volume
}
response = self._do_post('servers/%s/action' % server_id,
'swap-volume-req', subs)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.content, '')

View File

@ -13,16 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo_serialization import jsonutils
import webob
from nova.api.openstack.compute.plugins.v3 import (extended_volumes
as extended_volumes_v21)
from nova import compute
from nova import context
from nova import db
from nova import exception
from nova import objects
from nova.objects import instance as instance_obj
from nova import test
@ -58,15 +55,6 @@ def fake_bdms_get_all_by_instance(*args, **kwargs):
'destination_type': 'volume', 'id': 2})]
def fake_attach_volume(self, context, instance, volume_id,
device, disk_bus, device_type):
pass
def fake_detach_volume(self, context, instance, volume):
pass
def fake_volume_get(*args, **kwargs):
pass
@ -95,8 +83,6 @@ class ExtendedVolumesTestV21(test.TestCase):
def _setUp(self):
self.Controller = extended_volumes_v21.ExtendedVolumesController()
self.stubs.Set(volume.cinder.API, 'get', fake_volume_get)
self.stubs.Set(compute.api.API, 'detach_volume', fake_detach_volume)
self.stubs.Set(compute.api.API, 'attach_volume', fake_attach_volume)
self.action_url = "/%s/action" % UUID1
def _make_request(self, url, body=None):
@ -132,218 +118,6 @@ class ExtendedVolumesTestV21(test.TestCase):
self.assertEqual(self.exp_volumes, actual)
class ExtendedVolumesAdditionTestV21(ExtendedVolumesTestV21):
def test_show(self):
pass
def test_detail(self):
pass
def test_detach(self):
res = self._make_request(self.action_url,
{"detach": {"volume_id": UUID1}})
self.assertEqual(202, res.status_int)
def test_detach_volume_from_locked_server(self):
self.stubs.Set(compute.api.API, 'detach_volume',
fakes.fake_actions_to_locked_server)
res = self._make_request(self.action_url,
{"detach": {"volume_id": UUID1}})
self.assertEqual(409, res.status_int)
@mock.patch('nova.volume.cinder.API.get',
side_effect=exception.VolumeNotFound(volume_id=UUID1))
def test_detach_with_non_existed_vol(self, mock_detach):
res = self._make_request(self.action_url,
{"detach": {"volume_id": UUID2}})
self.assertEqual(404, res.status_int)
@mock.patch('nova.compute.api.API.get',
side_effect=exception.InstanceNotFound(instance_id=UUID1))
def test_detach_with_non_existed_instance(self, mock_detach):
res = self._make_request(self.action_url,
{"detach": {"volume_id": UUID2}})
self.assertEqual(404, res.status_int)
@mock.patch('nova.compute.api.API.detach_volume',
side_effect=exception.InvalidVolume(reason=''))
def test_detach_with_invalid_vol(self, mock_detach):
res = self._make_request(self.action_url,
{"detach": {"volume_id": UUID2}})
self.assertEqual(400, res.status_int)
def test_detach_with_bad_id(self):
res = self._make_request(self.action_url,
{"detach": {"volume_id": 'xxx'}})
self.assertEqual(400, res.status_int)
def test_detach_without_id(self):
res = self._make_request(self.action_url, {"detach": {}})
self.assertEqual(400, res.status_int)
def test_detach_volume_with_invalid_request(self):
res = self._make_request(self.action_url,
{"detach": None})
self.assertEqual(400, res.status_int)
@mock.patch('nova.objects.BlockDeviceMapping.is_root',
new_callable=mock.PropertyMock)
def test_detach_volume_root(self, mock_isroot):
mock_isroot.return_value = True
res = self._make_request(self.action_url,
{"detach": {"volume_id": UUID1}})
self.assertEqual(403, res.status_int)
def test_attach_volume(self):
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1}})
self.assertEqual(202, res.status_int)
def test_attach_volume_to_locked_server(self):
self.stubs.Set(compute.api.API, 'attach_volume',
fakes.fake_actions_to_locked_server)
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1}})
self.assertEqual(409, res.status_int)
def test_attach_volume_disk_bus_and_disk_dev(self):
self._make_request(self.action_url,
{"attach": {"volume_id": UUID1,
"device": "/dev/vdb",
"disk_bus": "ide",
"device_type": "cdrom"}})
def test_attach_volume_with_bad_id(self):
res = self._make_request(self.action_url,
{"attach": {"volume_id": 'xxx'}})
self.assertEqual(400, res.status_int)
def test_attach_volume_without_id(self):
res = self._make_request(self.action_url, {"attach": {}})
self.assertEqual(400, res.status_int)
def test_attach_volume_with_invalid_request(self):
res = self._make_request(self.action_url,
{"attach": None})
self.assertEqual(400, res.status_int)
@mock.patch('nova.compute.api.API.attach_volume',
side_effect=exception.VolumeNotFound(volume_id=UUID1))
def test_attach_volume_with_non_existe_vol(self, mock_attach):
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1}})
self.assertEqual(404, res.status_int)
@mock.patch('nova.compute.api.API.get',
side_effect=exception.InstanceNotFound(instance_id=UUID1))
def test_attach_volume_with_non_existed_instance(self, mock_attach):
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1}})
self.assertEqual(404, res.status_int)
@mock.patch('nova.compute.api.API.attach_volume',
side_effect=exception.InvalidDevicePath(path='xxx'))
def test_attach_volume_with_invalid_device_path(self, mock_attach):
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1,
'device': 'xxx'}})
self.assertEqual(400, res.status_int)
@mock.patch('nova.compute.api.API.attach_volume',
side_effect=exception.InstanceInvalidState(instance_uuid=UUID1,
state='',
method='', attr=''))
def test_attach_volume_with_instance_invalid_state(self, mock_attach):
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1}})
self.assertEqual(409, res.status_int)
@mock.patch('nova.compute.api.API.attach_volume',
side_effect=exception.InvalidVolume(reason=''))
def test_attach_volume_with_invalid_volume(self, mock_attach):
res = self._make_request(self.action_url,
{"attach": {"volume_id": UUID1}})
self.assertEqual(400, res.status_int)
@mock.patch('nova.compute.api.API.attach_volume',
side_effect=exception.InvalidVolume(reason=''))
def test_attach_volume_with_invalid_request_body(self, mock_attach):
res = self._make_request(self.action_url, {"attach": None})
self.assertEqual(400, res.status_int)
def _test_swap(self, uuid=UUID1, body=None):
body = body or {'swap_volume_attachment': {'old_volume_id': uuid,
'new_volume_id': UUID2}}
req = fakes.HTTPRequest.blank('/v2/fake/servers/%s/action' % UUID1)
req.method = 'PUT'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = context.get_admin_context()
return self.Controller.swap(req, UUID1, body=body)
@mock.patch('nova.compute.api.API.swap_volume')
def test_swap_volume(self, mock_swap):
# Check any exceptions don't happen and status code
self._test_swap()
self.assertEqual(202, self.Controller.swap.wsgi_code)
@mock.patch('nova.compute.api.API.swap_volume',
side_effect=exception.InstanceIsLocked(instance_uuid=UUID1))
def test_swap_volume_for_locked_server(self, mock_swap):
self.assertRaises(webob.exc.HTTPConflict, self._test_swap)
@mock.patch('nova.compute.api.API.swap_volume',
side_effect=exception.InstanceIsLocked(instance_uuid=UUID1))
def test_swap_volume_for_locked_server_new(self, mock_swap):
self.assertRaises(webob.exc.HTTPConflict, self._test_swap)
@mock.patch('nova.compute.api.API.get',
side_effect=exception.InstanceNotFound(instance_id=UUID1))
def test_swap_volume_instance_not_found(self, mock_swap):
self.assertRaises(webob.exc.HTTPNotFound, self._test_swap)
@mock.patch('nova.compute.api.API.swap_volume')
def test_swap_volume_with_bad_action(self, mock_swap):
body = {'swap_volume_attachment_bad_action': None}
self.assertRaises(exception.ValidationError, self._test_swap,
body=body)
@mock.patch('nova.compute.api.API.swap_volume')
def test_swap_volume_with_invalid_body(self, mock_swap):
body = {'swap_volume_attachment': {'bad_volume_id_body': UUID1,
'new_volume_id': UUID2}}
self.assertRaises(exception.ValidationError, self._test_swap,
body=body)
@mock.patch('nova.compute.api.API.swap_volume',
side_effect=exception.InvalidVolume(reason=''))
def test_swap_volume_with_invalid_volume(self, mock_swap):
self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap)
@mock.patch('nova.compute.api.API.swap_volume',
side_effect=exception.VolumeUnattached(volume_id='fake'))
def test_swap_volume_with_unattached_volume(self, mock_swap):
self.assertRaises(webob.exc.HTTPNotFound, self._test_swap)
@mock.patch('nova.compute.api.API.swap_volume',
side_effect=exception.InstanceInvalidState(instance_uuid=UUID1,
state='',
method='', attr=''))
def test_swap_volume_with_bad_state_instance(self, mock_swap):
self.assertRaises(webob.exc.HTTPConflict, self._test_swap)
@mock.patch('nova.compute.api.API.swap_volume')
def test_swap_volume_no_attachment(self, mock_swap):
self.assertRaises(webob.exc.HTTPNotFound, self._test_swap, UUID3)
@mock.patch('nova.compute.api.API.swap_volume')
@mock.patch('nova.volume.cinder.API.get',
side_effect=exception.VolumeNotFound(volume_id=UUID1))
def test_swap_volume_not_found(self, mock_swap, mock_cinder_get):
self.assertRaises(webob.exc.HTTPNotFound, self._test_swap)
class ExtendedVolumesTestV2(ExtendedVolumesTestV21):
def _setup_app(self):