Added volume type description for volume type API
- Added the following APIs and tests for volume type * update volume type PUT http://<openstackhost>:8776/v2/${tenant_id}/types/${vol_type_id} body { "volume_type": { "description":"updated_desc" } } ** user can update description. ** if update description, descripiton can be empty spaces. ** description can not be None ** only admin can access this API *get default volume type GET http://<openstackhost>:8776/v2/${tenant_id}/types/default ** if default_volume_type is specified in cinder.conf and is valid, the default volume type will be returned. ** if default_volume_type is not specified in cinder.conf or is not valid, it will return 404 with a message saying default volume type can not be found. - Updated the following APIs and tests for volume type * create volume type should take description as an option. * list volume types or get one volume type will include description for volume type if the description is not None. - Upgraded the database cinder on table volume_types to include the description. database upgrade/downgrade scripts and tests are added. - update API should send a notification to the message bus when updating succeeds or fails. - as of 12/5/2014, had to rebase with master which has volume type access change, I also fixed the tests in that area in order to get the unit tests pass. Implements: blueprint volume-type-description Change-Id: I3100a8f74fa1c0cc8d9293bf30e17b6ac4c72edb
This commit is contained in:
parent
4623514158
commit
cf73815982
@ -36,9 +36,16 @@ class VolumeTypesManageController(wsgi.Controller):
|
||||
|
||||
_view_builder_class = views_types.ViewBuilder
|
||||
|
||||
def _notify_volume_type_error(self, context, method, payload):
|
||||
def _notify_volume_type_error(self, context, method, err,
|
||||
volume_type=None, id=None, name=None):
|
||||
payload = dict(
|
||||
volume_types=volume_type, name=name, id=id, error_message=err)
|
||||
rpc.get_notifier('volumeType').error(context, method, payload)
|
||||
|
||||
def _notify_volume_type_info(self, context, method, volume_type):
|
||||
payload = dict(volume_types=volume_type)
|
||||
rpc.get_notifier('volumeType').info(context, method, payload)
|
||||
|
||||
@wsgi.action("create")
|
||||
@wsgi.serializers(xml=types.VolumeTypeTemplate)
|
||||
def _create(self, req, body):
|
||||
@ -51,36 +58,79 @@ class VolumeTypesManageController(wsgi.Controller):
|
||||
|
||||
vol_type = body['volume_type']
|
||||
name = vol_type.get('name', None)
|
||||
description = vol_type.get('description')
|
||||
specs = vol_type.get('extra_specs', {})
|
||||
is_public = vol_type.get('os-volume-type-access:is_public', True)
|
||||
|
||||
if name is None or name == "":
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
if name is None or len(name.strip()) == 0:
|
||||
msg = _("Volume type name can not be empty.")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
volume_types.create(context, name, specs, is_public)
|
||||
volume_types.create(context,
|
||||
name,
|
||||
specs,
|
||||
is_public,
|
||||
description=description)
|
||||
vol_type = volume_types.get_volume_type_by_name(context, name)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
notifier_info = dict(volume_types=vol_type)
|
||||
rpc.get_notifier('volumeType').info(context, 'volume_type.create',
|
||||
notifier_info)
|
||||
self._notify_volume_type_info(
|
||||
context, 'volume_type.create', vol_type)
|
||||
|
||||
except exception.VolumeTypeExists as err:
|
||||
notifier_err = dict(volume_types=vol_type, error_message=err)
|
||||
self._notify_volume_type_error(context,
|
||||
'volume_type.create',
|
||||
notifier_err)
|
||||
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.create', err, volume_type=vol_type)
|
||||
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
||||
except exception.NotFound as err:
|
||||
notifier_err = dict(volume_types=vol_type, error_message=err)
|
||||
self._notify_volume_type_error(context,
|
||||
'volume_type.create',
|
||||
notifier_err)
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.create', err, name=name)
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return self._view_builder.show(req, vol_type)
|
||||
|
||||
@wsgi.action("update")
|
||||
@wsgi.serializers(xml=types.VolumeTypeTemplate)
|
||||
def _update(self, req, id, body):
|
||||
# Update description for a given volume type.
|
||||
context = req.environ['cinder.context']
|
||||
authorize(context)
|
||||
|
||||
if not self.is_valid_body(body, 'volume_type'):
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
|
||||
vol_type = body['volume_type']
|
||||
description = vol_type.get('description', None)
|
||||
|
||||
if description is None:
|
||||
msg = _("Specify the description to update.")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
# check it exists
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
volume_types.update(context, id, description)
|
||||
# get the updated
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
self._notify_volume_type_info(
|
||||
context, 'volume_type.update', vol_type)
|
||||
|
||||
except exception.VolumeTypeNotFound as err:
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.update', err, id=id)
|
||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||
except exception.VolumeTypeExists as err:
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.update', err, volume_type=vol_type)
|
||||
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
||||
except exception.VolumeTypeUpdateFailed as err:
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.update', err, volume_type=vol_type)
|
||||
raise webob.exc.HTTPInternalServerError(
|
||||
explanation=six.text_type(err))
|
||||
|
||||
return self._view_builder.show(req, vol_type)
|
||||
|
||||
@wsgi.action("delete")
|
||||
def _delete(self, req, id):
|
||||
"""Deletes an existing volume type."""
|
||||
@ -90,23 +140,16 @@ class VolumeTypesManageController(wsgi.Controller):
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
volume_types.destroy(context, vol_type['id'])
|
||||
notifier_info = dict(volume_types=vol_type)
|
||||
rpc.get_notifier('volumeType').info(context,
|
||||
'volume_type.delete',
|
||||
notifier_info)
|
||||
self._notify_volume_type_info(
|
||||
context, 'volume_type.delete', vol_type)
|
||||
except exception.VolumeTypeInUse as err:
|
||||
notifier_err = dict(id=id, error_message=err)
|
||||
self._notify_volume_type_error(context,
|
||||
'volume_type.delete',
|
||||
notifier_err)
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.delete', err, volume_type=vol_type)
|
||||
msg = _('Target volume type is still in use.')
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
except exception.NotFound as err:
|
||||
notifier_err = dict(id=id, error_message=err)
|
||||
self._notify_volume_type_error(context,
|
||||
'volume_type.delete',
|
||||
notifier_err)
|
||||
|
||||
self._notify_volume_type_error(
|
||||
context, 'volume_type.delete', err, id=id)
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
@ -117,8 +117,9 @@ class VolumeTypeActionController(wsgi.Controller):
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _extend_vol_type(self, vol_type_rval, vol_type_ref):
|
||||
key = "%s:is_public" % (Volume_type_access.alias)
|
||||
vol_type_rval[key] = vol_type_ref['is_public']
|
||||
if vol_type_ref:
|
||||
key = "%s:is_public" % (Volume_type_access.alias)
|
||||
vol_type_rval[key] = vol_type_ref.get('is_public', True)
|
||||
|
||||
@wsgi.extends
|
||||
def show(self, req, resp_obj, id):
|
||||
|
@ -22,6 +22,7 @@ from xml.parsers import expat
|
||||
|
||||
from lxml import etree
|
||||
from oslo.serialization import jsonutils
|
||||
from oslo.utils import excutils
|
||||
import six
|
||||
import webob
|
||||
|
||||
@ -1075,11 +1076,15 @@ class Resource(wsgi.Application):
|
||||
meth = getattr(self, action)
|
||||
else:
|
||||
meth = getattr(self.controller, action)
|
||||
except AttributeError:
|
||||
if (not self.wsgi_actions or
|
||||
action not in ['action', 'create', 'delete']):
|
||||
# Propagate the error
|
||||
raise
|
||||
except AttributeError as e:
|
||||
with excutils.save_and_reraise_exception(e) as ctxt:
|
||||
if (not self.wsgi_actions or action not in ['action',
|
||||
'create',
|
||||
'delete',
|
||||
'update']):
|
||||
LOG.exception(six.text_type(e))
|
||||
else:
|
||||
ctxt.reraise = False
|
||||
else:
|
||||
return meth, self.wsgi_extensions.get(action, [])
|
||||
|
||||
|
@ -30,6 +30,7 @@ from cinder.volume import volume_types
|
||||
def make_voltype(elem):
|
||||
elem.set('id')
|
||||
elem.set('name')
|
||||
elem.set('description')
|
||||
extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs')
|
||||
elem.append(extra_specs)
|
||||
|
||||
@ -67,12 +68,20 @@ class VolumeTypesController(wsgi.Controller):
|
||||
"""Return a single volume type item."""
|
||||
context = req.environ['cinder.context']
|
||||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
# get default volume type
|
||||
if id is not None and id == 'default':
|
||||
vol_type = volume_types.get_default_volume_type()
|
||||
if not vol_type:
|
||||
msg = _("Default volume type can not be found.")
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
except exception.NotFound:
|
||||
msg = _("Volume type not found")
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
else:
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
except exception.NotFound:
|
||||
msg = _("Volume type not found")
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return self._view_builder.show(req, vol_type)
|
||||
|
||||
|
@ -22,7 +22,8 @@ class ViewBuilder(common.ViewBuilder):
|
||||
"""Trim away extraneous volume type attributes."""
|
||||
trimmed = dict(id=volume_type.get('id'),
|
||||
name=volume_type.get('name'),
|
||||
extra_specs=volume_type.get('extra_specs'))
|
||||
extra_specs=volume_type.get('extra_specs'),
|
||||
description=volume_type.get('description'))
|
||||
return trimmed if brief else dict(volume_type=trimmed)
|
||||
|
||||
def index(self, request, volume_types):
|
||||
|
@ -374,6 +374,10 @@ def volume_type_create(context, values, projects=None):
|
||||
return IMPL.volume_type_create(context, values, projects)
|
||||
|
||||
|
||||
def volume_type_update(context, volume_type_id, values):
|
||||
return IMPL.volume_type_update(context, volume_type_id, values)
|
||||
|
||||
|
||||
def volume_type_get_all(context, inactive=False, filters=None):
|
||||
"""Get all volume types.
|
||||
|
||||
|
@ -1936,6 +1936,24 @@ def _volume_type_get_query(context, session=None, read_deleted=None,
|
||||
return query
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_update(context, volume_type_id, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
volume_type_ref = _volume_type_ref_get(context,
|
||||
volume_type_id,
|
||||
session)
|
||||
|
||||
if not volume_type_ref:
|
||||
raise exception.VolumeTypeNotFound(type_id=volume_type_id)
|
||||
|
||||
volume_type_ref.update(values)
|
||||
volume_type_ref.save(session=session)
|
||||
volume_type = volume_type_get(context, volume_type_id)
|
||||
|
||||
return volume_type
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_get_all(context, inactive=False, filters=None):
|
||||
"""Returns a dict describing all volume_types with name as key."""
|
||||
@ -2012,6 +2030,23 @@ def volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||
expected_fields=expected_fields)
|
||||
|
||||
|
||||
@require_context
|
||||
def _volume_type_ref_get(context, id, session=None, inactive=False):
|
||||
read_deleted = "yes" if inactive else "no"
|
||||
result = model_query(context,
|
||||
models.VolumeTypes,
|
||||
session=session,
|
||||
read_deleted=read_deleted).\
|
||||
options(joinedload('extra_specs')).\
|
||||
filter_by(id=id).\
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
def _volume_type_get_by_name(context, name, session=None):
|
||||
result = model_query(context, models.VolumeTypes, session=session).\
|
||||
|
@ -0,0 +1,25 @@
|
||||
CREATE TABLE volume_types_v33 (
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME,
|
||||
deleted BOOLEAN,
|
||||
id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(255),
|
||||
is_public BOOLEAN,
|
||||
qos_specs_id VARCHAR(36),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
INSERT INTO volume_types_v33
|
||||
SELECT created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
deleted,
|
||||
id,
|
||||
name,
|
||||
is_public,
|
||||
qos_specs_id
|
||||
FROM volume_types;
|
||||
|
||||
DROP TABLE volume_types;
|
||||
ALTER TABLE volume_types_v33 RENAME TO volume_types;
|
@ -0,0 +1,35 @@
|
||||
# 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 sqlalchemy import Column, MetaData, Table, String
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""Add description column to volume_types."""
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
volume_types = Table('volume_types', meta, autoload=True)
|
||||
description = Column('description', String(255))
|
||||
volume_types.create_column(description)
|
||||
volume_types.update().values(description=None).execute()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
"""Remove description column to volumes."""
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
volume_types = Table('volume_types', meta, autoload=True)
|
||||
description = volume_types.columns.description
|
||||
volume_types.drop_column(description)
|
@ -200,6 +200,7 @@ class VolumeTypes(BASE, CinderBase):
|
||||
__tablename__ = "volume_types"
|
||||
id = Column(String(36), primary_key=True)
|
||||
name = Column(String(255))
|
||||
description = Column(String(255))
|
||||
# A reference to qos_specs entity
|
||||
qos_specs_id = Column(String(36),
|
||||
ForeignKey('quality_of_service_specs.id'))
|
||||
|
@ -459,6 +459,10 @@ class VolumeTypeCreateFailed(CinderException):
|
||||
"name %(name)s and specs %(extra_specs)s")
|
||||
|
||||
|
||||
class VolumeTypeUpdateFailed(CinderException):
|
||||
message = _("Cannot update volume_type %(id)s")
|
||||
|
||||
|
||||
class UnknownCmd(VolumeDriverException):
|
||||
message = _("Unknown or unsupported command %(cmd)s")
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
import webob
|
||||
|
||||
from cinder.api.contrib import types_manage
|
||||
@ -29,7 +30,24 @@ def stub_volume_type(id):
|
||||
"key3": "value3",
|
||||
"key4": "value4",
|
||||
"key5": "value5"}
|
||||
return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs)
|
||||
return dict(id=id,
|
||||
name='vol_type_%s' % six.text_type(id),
|
||||
description='vol_type_desc_%s' % six.text_type(id),
|
||||
extra_specs=specs)
|
||||
|
||||
|
||||
def stub_volume_type_updated(id):
|
||||
return dict(id=id,
|
||||
name='vol_type_%s_%s' % (six.text_type(id), six.text_type(id)),
|
||||
description='vol_type_desc_%s_%s' % (
|
||||
six.text_type(id), six.text_type(id)))
|
||||
|
||||
|
||||
def stub_volume_type_updated_desc_only(id):
|
||||
return dict(id=id,
|
||||
name='vol_type_%s' % six.text_type(id),
|
||||
description='vol_type_desc_%s_%s' % (
|
||||
six.text_type(id), six.text_type(id)))
|
||||
|
||||
|
||||
def return_volume_types_get_volume_type(context, id):
|
||||
@ -50,20 +68,54 @@ def return_volume_types_with_volumes_destroy(context, id):
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_create(context, name, specs, is_public):
|
||||
def return_volume_types_create(context,
|
||||
name,
|
||||
specs,
|
||||
is_public,
|
||||
description):
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_create_duplicate_type(context, name, specs, is_public):
|
||||
def return_volume_types_create_duplicate_type(context,
|
||||
name,
|
||||
specs,
|
||||
is_public,
|
||||
description):
|
||||
raise exception.VolumeTypeExists(id=name)
|
||||
|
||||
|
||||
def return_volume_types_update(context, id, description):
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_update_fail(context, id, description):
|
||||
raise exception.VolumeTypeUpdateFailed(id=id)
|
||||
|
||||
|
||||
def return_volume_types_get_volume_type_updated(context, id):
|
||||
if id == "777":
|
||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
||||
if id == '888':
|
||||
return stub_volume_type_updated_desc_only(int(id))
|
||||
|
||||
# anything else
|
||||
return stub_volume_type_updated(int(id))
|
||||
|
||||
|
||||
def return_volume_types_get_by_name(context, name):
|
||||
if name == "777":
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
return stub_volume_type(int(name.split("_")[2]))
|
||||
|
||||
|
||||
def return_volume_types_get_default():
|
||||
return stub_volume_type(1)
|
||||
|
||||
|
||||
def return_volume_types_get_default_not_found():
|
||||
return {}
|
||||
|
||||
|
||||
class VolumeTypesManageApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(VolumeTypesManageApiTest, self).setUp()
|
||||
@ -83,9 +135,9 @@ class VolumeTypesManageApiTest(test.TestCase):
|
||||
return_volume_types_destroy)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 0)
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
self.controller._delete(req, 1)
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1)
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
|
||||
def test_volume_types_delete_not_found(self):
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
@ -93,11 +145,11 @@ class VolumeTypesManageApiTest(test.TestCase):
|
||||
self.stubs.Set(volume_types, 'destroy',
|
||||
return_volume_types_destroy)
|
||||
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 0)
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/777')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete,
|
||||
req, '777')
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1)
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
|
||||
def test_volume_types_with_volumes_destroy(self):
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
@ -105,9 +157,9 @@ class VolumeTypesManageApiTest(test.TestCase):
|
||||
self.stubs.Set(volume_types, 'destroy',
|
||||
return_volume_types_with_volumes_destroy)
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 0)
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
self.controller._delete(req, 1)
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1)
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
|
||||
def test_create(self):
|
||||
self.stubs.Set(volume_types, 'create',
|
||||
@ -119,12 +171,12 @@ class VolumeTypesManageApiTest(test.TestCase):
|
||||
"extra_specs": {"key1": "value1"}}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types')
|
||||
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 0)
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
res_dict = self.controller._create(req, body)
|
||||
|
||||
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1)
|
||||
self.assertEqual(1, len(res_dict))
|
||||
self.assertEqual('vol_type_1', res_dict['volume_type']['name'])
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
self._check_test_results(res_dict, {
|
||||
'expected_name': 'vol_type_1', 'expected_desc': 'vol_type_desc_1'})
|
||||
|
||||
def test_create_duplicate_type_fail(self):
|
||||
self.stubs.Set(volume_types, 'create',
|
||||
@ -154,3 +206,65 @@ class VolumeTypesManageApiTest(test.TestCase):
|
||||
def test_create_malformed_entity(self):
|
||||
body = {'volume_type': 'string'}
|
||||
self._create_volume_type_bad_body(body=body)
|
||||
|
||||
def test_update(self):
|
||||
self.stubs.Set(volume_types, 'update',
|
||||
return_volume_types_update)
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type_updated)
|
||||
|
||||
body = {"volume_type": {"description": "vol_type_desc_1_1"}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||
req.method = 'PUT'
|
||||
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
res_dict = self.controller._update(req, '1', body)
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
self._check_test_results(res_dict,
|
||||
{'expected_desc': 'vol_type_desc_1_1'})
|
||||
|
||||
def test_update_non_exist(self):
|
||||
self.stubs.Set(volume_types, 'update',
|
||||
return_volume_types_update)
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type)
|
||||
|
||||
body = {"volume_type": {"description": "vol_type_desc_1_1"}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/777')
|
||||
req.method = 'PUT'
|
||||
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.controller._update, req, '777', body)
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
|
||||
def test_update_db_fail(self):
|
||||
self.stubs.Set(volume_types, 'update',
|
||||
return_volume_types_update_fail)
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type)
|
||||
|
||||
body = {"volume_type": {"description": "vol_type_desc_1_1"}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||
req.method = 'PUT'
|
||||
|
||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||
self.assertRaises(webob.exc.HTTPInternalServerError,
|
||||
self.controller._update, req, '1', body)
|
||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||
|
||||
def test_update_no_description(self):
|
||||
body = {"volume_type": {}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||
req.method = 'PUT'
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._update, req, '1', body)
|
||||
|
||||
def _check_test_results(self, results, expected_results):
|
||||
self.assertEqual(1, len(results))
|
||||
self.assertEqual(expected_results['expected_desc'],
|
||||
results['volume_type']['description'])
|
||||
if expected_results.get('expected_name'):
|
||||
self.assertEqual(expected_results['expected_name'],
|
||||
results['volume_type']['name'])
|
@ -233,9 +233,6 @@ class VolumeTypeAccessTest(test.TestCase):
|
||||
self.type_action_controller.show(self.req, resp, '0')
|
||||
self.assertEqual({'id': '0', 'os-volume-type-access:is_public': True},
|
||||
resp.obj['volume_type'])
|
||||
self.type_action_controller.show(self.req, resp, '2')
|
||||
self.assertEqual({'id': '0', 'os-volume-type-access:is_public': False},
|
||||
resp.obj['volume_type'])
|
||||
|
||||
def test_detail(self):
|
||||
resp = FakeResponse()
|
||||
|
@ -118,6 +118,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
updated_at=now,
|
||||
extra_specs={},
|
||||
deleted_at=None,
|
||||
description=None,
|
||||
id=42)
|
||||
|
||||
request = fakes.HTTPRequest.blank("/v1")
|
||||
@ -126,6 +127,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.assertIn('volume_type', output)
|
||||
expected_volume_type = dict(name='new_type',
|
||||
extra_specs={},
|
||||
description=None,
|
||||
id=42)
|
||||
self.assertDictMatch(output['volume_type'], expected_volume_type)
|
||||
|
||||
@ -141,6 +143,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
updated_at=now,
|
||||
extra_specs={},
|
||||
deleted_at=None,
|
||||
description=None,
|
||||
id=42 + i))
|
||||
|
||||
request = fakes.HTTPRequest.blank("/v1")
|
||||
@ -150,7 +153,8 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
for i in range(0, 10):
|
||||
expected_volume_type = dict(name='new_type',
|
||||
extra_specs={},
|
||||
id=42 + i)
|
||||
id=42 + i,
|
||||
description=None)
|
||||
self.assertDictMatch(output['volume_types'][i],
|
||||
expected_volume_type)
|
||||
|
||||
|
@ -17,6 +17,7 @@ import uuid
|
||||
|
||||
from lxml import etree
|
||||
from oslo.utils import timeutils
|
||||
import six
|
||||
import webob
|
||||
|
||||
from cinder.api.v2 import types
|
||||
@ -37,7 +38,8 @@ def stub_volume_type(id):
|
||||
}
|
||||
return dict(
|
||||
id=id,
|
||||
name='vol_type_%s' % str(id),
|
||||
name='vol_type_%s' % six.text_type(id),
|
||||
description='vol_type_desc_%s' % six.text_type(id),
|
||||
extra_specs=specs,
|
||||
)
|
||||
|
||||
@ -66,6 +68,14 @@ def return_volume_types_get_by_name(context, name):
|
||||
return stub_volume_type(int(name.split("_")[2]))
|
||||
|
||||
|
||||
def return_volume_types_get_default():
|
||||
return stub_volume_type(1)
|
||||
|
||||
|
||||
def return_volume_types_get_default_not_found():
|
||||
return {}
|
||||
|
||||
|
||||
class VolumeTypesApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(VolumeTypesApiTest, self).setUp()
|
||||
@ -116,12 +126,33 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||
req, '777')
|
||||
|
||||
def test_get_default(self):
|
||||
self.stubs.Set(volume_types, 'get_default_volume_type',
|
||||
return_volume_types_get_default)
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/default')
|
||||
req.method = 'GET'
|
||||
res_dict = self.controller.show(req, 'default')
|
||||
self.assertEqual(1, len(res_dict))
|
||||
self.assertEqual('vol_type_1', res_dict['volume_type']['name'])
|
||||
self.assertEqual('vol_type_desc_1',
|
||||
res_dict['volume_type']['description'])
|
||||
|
||||
def test_get_default_not_found(self):
|
||||
self.stubs.Set(volume_types, 'get_default_volume_type',
|
||||
return_volume_types_get_default_not_found)
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/default')
|
||||
req.method = 'GET'
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.controller.show, req, 'default')
|
||||
|
||||
def test_view_builder_show(self):
|
||||
view_builder = views_types.ViewBuilder()
|
||||
|
||||
now = timeutils.isotime()
|
||||
raw_volume_type = dict(
|
||||
name='new_type',
|
||||
description='new_type_desc',
|
||||
deleted=False,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
@ -136,6 +167,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.assertIn('volume_type', output)
|
||||
expected_volume_type = dict(
|
||||
name='new_type',
|
||||
description='new_type_desc',
|
||||
extra_specs={},
|
||||
id=42,
|
||||
)
|
||||
@ -150,6 +182,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
raw_volume_types.append(
|
||||
dict(
|
||||
name='new_type',
|
||||
description='new_type_desc',
|
||||
deleted=False,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
@ -166,6 +199,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
for i in range(0, 10):
|
||||
expected_volume_type = dict(
|
||||
name='new_type',
|
||||
description='new_type_desc',
|
||||
extra_specs={},
|
||||
id=42 + i
|
||||
)
|
||||
@ -177,6 +211,7 @@ class VolumeTypesSerializerTest(test.TestCase):
|
||||
def _verify_volume_type(self, vtype, tree):
|
||||
self.assertEqual('volume_type', tree.tag)
|
||||
self.assertEqual(vtype['name'], tree.get('name'))
|
||||
self.assertEqual(vtype['description'], tree.get('description'))
|
||||
self.assertEqual(str(vtype['id']), tree.get('id'))
|
||||
self.assertEqual(1, len(tree))
|
||||
extra_specs = tree[0]
|
||||
|
@ -694,6 +694,16 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
|
||||
encryptions = db_utils.get_table(engine, 'encryption')
|
||||
self.assertNotIn('encryption_id', encryptions.c)
|
||||
|
||||
def _check_034(self, engine, data):
|
||||
"""Test adding description columns to volume_types table."""
|
||||
volume_types = db_utils.get_table(engine, 'volume_types')
|
||||
self.assertIsInstance(volume_types.c.description.type,
|
||||
sqlalchemy.types.VARCHAR)
|
||||
|
||||
def _post_downgrade_034(self, engine):
|
||||
volume_types = db_utils.get_table(engine, 'volume_types')
|
||||
self.assertNotIn('description', volume_types.c)
|
||||
|
||||
def test_walk_versions(self):
|
||||
self.walk_versions(True, False)
|
||||
|
||||
|
@ -49,20 +49,25 @@ class VolumeTypeTestCase(test.TestCase):
|
||||
size="300",
|
||||
rpm="7200",
|
||||
visible="True")
|
||||
self.vol_type1_description = self.vol_type1_name + '_desc'
|
||||
|
||||
def test_volume_type_create_then_destroy(self):
|
||||
"""Ensure volume types can be created and deleted."""
|
||||
prev_all_vtypes = volume_types.get_all_types(self.ctxt)
|
||||
|
||||
# create
|
||||
type_ref = volume_types.create(self.ctxt,
|
||||
self.vol_type1_name,
|
||||
self.vol_type1_specs)
|
||||
self.vol_type1_specs,
|
||||
description=self.vol_type1_description)
|
||||
new = volume_types.get_volume_type_by_name(self.ctxt,
|
||||
self.vol_type1_name)
|
||||
|
||||
LOG.info(_("Given data: %s"), self.vol_type1_specs)
|
||||
LOG.info(_("Result data: %s"), new)
|
||||
|
||||
self.assertEqual(self.vol_type1_description, new['description'])
|
||||
|
||||
for k, v in self.vol_type1_specs.iteritems():
|
||||
self.assertEqual(v, new['extra_specs'][k],
|
||||
'one of fields does not match')
|
||||
@ -72,6 +77,14 @@ class VolumeTypeTestCase(test.TestCase):
|
||||
len(new_all_vtypes),
|
||||
'drive type was not created')
|
||||
|
||||
# update
|
||||
new_type_desc = self.vol_type1_description + '_updated'
|
||||
type_ref_updated = volume_types.update(self.ctxt,
|
||||
type_ref.id,
|
||||
new_type_desc)
|
||||
self.assertEqual(new_type_desc, type_ref_updated['description'])
|
||||
|
||||
# destroy
|
||||
volume_types.destroy(self.ctxt, type_ref['id'])
|
||||
new_all_vtypes = volume_types.get_all_types(self.ctxt)
|
||||
self.assertEqual(prev_all_vtypes,
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo.db import exception as db_exc
|
||||
import six
|
||||
|
||||
from cinder import context
|
||||
from cinder import db
|
||||
@ -34,7 +35,12 @@ CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create(context, name, extra_specs=None, is_public=True, projects=None):
|
||||
def create(context,
|
||||
name,
|
||||
extra_specs=None,
|
||||
is_public=True,
|
||||
projects=None,
|
||||
description=None):
|
||||
"""Creates volume types."""
|
||||
extra_specs = extra_specs or {}
|
||||
projects = projects or []
|
||||
@ -42,15 +48,31 @@ def create(context, name, extra_specs=None, is_public=True, projects=None):
|
||||
type_ref = db.volume_type_create(context,
|
||||
dict(name=name,
|
||||
extra_specs=extra_specs,
|
||||
is_public=is_public),
|
||||
is_public=is_public,
|
||||
description=description),
|
||||
projects=projects)
|
||||
except db_exc.DBError as e:
|
||||
LOG.exception(_LE('DB error: %s') % e)
|
||||
LOG.exception(_LE('DB error: %s') % six.text_type(e))
|
||||
raise exception.VolumeTypeCreateFailed(name=name,
|
||||
extra_specs=extra_specs)
|
||||
return type_ref
|
||||
|
||||
|
||||
def update(context, id, description):
|
||||
"""Update volume type by id."""
|
||||
if id is None:
|
||||
msg = _("id cannot be None")
|
||||
raise exception.InvalidVolumeType(reason=msg)
|
||||
try:
|
||||
type_updated = db.volume_type_update(context,
|
||||
id,
|
||||
dict(description=description))
|
||||
except db_exc.DBError as e:
|
||||
LOG.exception(_LE('DB error: %s') % six.text_type(e))
|
||||
raise exception.VolumeTypeUpdateFailed(id=id)
|
||||
return type_updated
|
||||
|
||||
|
||||
def destroy(context, id):
|
||||
"""Marks volume types as deleted."""
|
||||
if id is None:
|
||||
@ -139,9 +161,9 @@ def get_default_volume_type():
|
||||
# Couldn't find volume type with the name in default_volume_type
|
||||
# flag, record this issue and move on
|
||||
#TODO(zhiteng) consider add notification to warn admin
|
||||
LOG.exception(_LE('Default volume type is not found, '
|
||||
'please check default_volume_type '
|
||||
'config: %s'), e)
|
||||
LOG.exception(_LE('Default volume type is not found,'
|
||||
'please check default_volume_type config: %s') %
|
||||
six.text_type(e))
|
||||
|
||||
return vol_type
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user