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:
Gloria Gu 2014-12-04 17:59:10 -08:00
parent 4623514158
commit cf73815982
18 changed files with 425 additions and 67 deletions

View File

@ -36,9 +36,16 @@ class VolumeTypesManageController(wsgi.Controller):
_view_builder_class = views_types.ViewBuilder _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) 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.action("create")
@wsgi.serializers(xml=types.VolumeTypeTemplate) @wsgi.serializers(xml=types.VolumeTypeTemplate)
def _create(self, req, body): def _create(self, req, body):
@ -51,36 +58,79 @@ class VolumeTypesManageController(wsgi.Controller):
vol_type = body['volume_type'] vol_type = body['volume_type']
name = vol_type.get('name', None) name = vol_type.get('name', None)
description = vol_type.get('description')
specs = vol_type.get('extra_specs', {}) specs = vol_type.get('extra_specs', {})
is_public = vol_type.get('os-volume-type-access:is_public', True) is_public = vol_type.get('os-volume-type-access:is_public', True)
if name is None or name == "": if name is None or len(name.strip()) == 0:
raise webob.exc.HTTPBadRequest() msg = _("Volume type name can not be empty.")
raise webob.exc.HTTPBadRequest(explanation=msg)
try: 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) vol_type = volume_types.get_volume_type_by_name(context, name)
req.cache_resource(vol_type, name='types') req.cache_resource(vol_type, name='types')
notifier_info = dict(volume_types=vol_type) self._notify_volume_type_info(
rpc.get_notifier('volumeType').info(context, 'volume_type.create', context, 'volume_type.create', vol_type)
notifier_info)
except exception.VolumeTypeExists as err: except exception.VolumeTypeExists as err:
notifier_err = dict(volume_types=vol_type, error_message=err) self._notify_volume_type_error(
self._notify_volume_type_error(context, context, 'volume_type.create', err, volume_type=vol_type)
'volume_type.create',
notifier_err)
raise webob.exc.HTTPConflict(explanation=six.text_type(err)) raise webob.exc.HTTPConflict(explanation=six.text_type(err))
except exception.NotFound as err: except exception.NotFound as err:
notifier_err = dict(volume_types=vol_type, error_message=err) self._notify_volume_type_error(
self._notify_volume_type_error(context, context, 'volume_type.create', err, name=name)
'volume_type.create',
notifier_err)
raise webob.exc.HTTPNotFound() raise webob.exc.HTTPNotFound()
return self._view_builder.show(req, vol_type) 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") @wsgi.action("delete")
def _delete(self, req, id): def _delete(self, req, id):
"""Deletes an existing volume type.""" """Deletes an existing volume type."""
@ -90,23 +140,16 @@ class VolumeTypesManageController(wsgi.Controller):
try: try:
vol_type = volume_types.get_volume_type(context, id) vol_type = volume_types.get_volume_type(context, id)
volume_types.destroy(context, vol_type['id']) volume_types.destroy(context, vol_type['id'])
notifier_info = dict(volume_types=vol_type) self._notify_volume_type_info(
rpc.get_notifier('volumeType').info(context, context, 'volume_type.delete', vol_type)
'volume_type.delete',
notifier_info)
except exception.VolumeTypeInUse as err: except exception.VolumeTypeInUse as err:
notifier_err = dict(id=id, error_message=err) self._notify_volume_type_error(
self._notify_volume_type_error(context, context, 'volume_type.delete', err, volume_type=vol_type)
'volume_type.delete',
notifier_err)
msg = _('Target volume type is still in use.') msg = _('Target volume type is still in use.')
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
except exception.NotFound as err: except exception.NotFound as err:
notifier_err = dict(id=id, error_message=err) self._notify_volume_type_error(
self._notify_volume_type_error(context, context, 'volume_type.delete', err, id=id)
'volume_type.delete',
notifier_err)
raise webob.exc.HTTPNotFound() raise webob.exc.HTTPNotFound()
return webob.Response(status_int=202) return webob.Response(status_int=202)

View File

@ -117,8 +117,9 @@ class VolumeTypeActionController(wsgi.Controller):
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
def _extend_vol_type(self, vol_type_rval, vol_type_ref): def _extend_vol_type(self, vol_type_rval, vol_type_ref):
key = "%s:is_public" % (Volume_type_access.alias) if vol_type_ref:
vol_type_rval[key] = vol_type_ref['is_public'] key = "%s:is_public" % (Volume_type_access.alias)
vol_type_rval[key] = vol_type_ref.get('is_public', True)
@wsgi.extends @wsgi.extends
def show(self, req, resp_obj, id): def show(self, req, resp_obj, id):

View File

@ -22,6 +22,7 @@ from xml.parsers import expat
from lxml import etree from lxml import etree
from oslo.serialization import jsonutils from oslo.serialization import jsonutils
from oslo.utils import excutils
import six import six
import webob import webob
@ -1075,11 +1076,15 @@ class Resource(wsgi.Application):
meth = getattr(self, action) meth = getattr(self, action)
else: else:
meth = getattr(self.controller, action) meth = getattr(self.controller, action)
except AttributeError: except AttributeError as e:
if (not self.wsgi_actions or with excutils.save_and_reraise_exception(e) as ctxt:
action not in ['action', 'create', 'delete']): if (not self.wsgi_actions or action not in ['action',
# Propagate the error 'create',
raise 'delete',
'update']):
LOG.exception(six.text_type(e))
else:
ctxt.reraise = False
else: else:
return meth, self.wsgi_extensions.get(action, []) return meth, self.wsgi_extensions.get(action, [])

View File

@ -30,6 +30,7 @@ from cinder.volume import volume_types
def make_voltype(elem): def make_voltype(elem):
elem.set('id') elem.set('id')
elem.set('name') elem.set('name')
elem.set('description')
extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs')
elem.append(extra_specs) elem.append(extra_specs)
@ -67,12 +68,20 @@ class VolumeTypesController(wsgi.Controller):
"""Return a single volume type item.""" """Return a single volume type item."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
try: # get default volume type
vol_type = volume_types.get_volume_type(context, id) 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') req.cache_resource(vol_type, name='types')
except exception.NotFound: else:
msg = _("Volume type not found") try:
raise exc.HTTPNotFound(explanation=msg) 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) return self._view_builder.show(req, vol_type)

View File

@ -22,7 +22,8 @@ class ViewBuilder(common.ViewBuilder):
"""Trim away extraneous volume type attributes.""" """Trim away extraneous volume type attributes."""
trimmed = dict(id=volume_type.get('id'), trimmed = dict(id=volume_type.get('id'),
name=volume_type.get('name'), 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) return trimmed if brief else dict(volume_type=trimmed)
def index(self, request, volume_types): def index(self, request, volume_types):

View File

@ -374,6 +374,10 @@ def volume_type_create(context, values, projects=None):
return IMPL.volume_type_create(context, values, projects) 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): def volume_type_get_all(context, inactive=False, filters=None):
"""Get all volume types. """Get all volume types.

View File

@ -1936,6 +1936,24 @@ def _volume_type_get_query(context, session=None, read_deleted=None,
return query 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 @require_context
def volume_type_get_all(context, inactive=False, filters=None): def volume_type_get_all(context, inactive=False, filters=None):
"""Returns a dict describing all volume_types with name as key.""" """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) 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 @require_context
def _volume_type_get_by_name(context, name, session=None): def _volume_type_get_by_name(context, name, session=None):
result = model_query(context, models.VolumeTypes, session=session).\ result = model_query(context, models.VolumeTypes, session=session).\

View File

@ -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;

View File

@ -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)

View File

@ -200,6 +200,7 @@ class VolumeTypes(BASE, CinderBase):
__tablename__ = "volume_types" __tablename__ = "volume_types"
id = Column(String(36), primary_key=True) id = Column(String(36), primary_key=True)
name = Column(String(255)) name = Column(String(255))
description = Column(String(255))
# A reference to qos_specs entity # A reference to qos_specs entity
qos_specs_id = Column(String(36), qos_specs_id = Column(String(36),
ForeignKey('quality_of_service_specs.id')) ForeignKey('quality_of_service_specs.id'))

View File

@ -459,6 +459,10 @@ class VolumeTypeCreateFailed(CinderException):
"name %(name)s and specs %(extra_specs)s") "name %(name)s and specs %(extra_specs)s")
class VolumeTypeUpdateFailed(CinderException):
message = _("Cannot update volume_type %(id)s")
class UnknownCmd(VolumeDriverException): class UnknownCmd(VolumeDriverException):
message = _("Unknown or unsupported command %(cmd)s") message = _("Unknown or unsupported command %(cmd)s")

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import six
import webob import webob
from cinder.api.contrib import types_manage from cinder.api.contrib import types_manage
@ -29,7 +30,24 @@ def stub_volume_type(id):
"key3": "value3", "key3": "value3",
"key4": "value4", "key4": "value4",
"key5": "value5"} "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): def return_volume_types_get_volume_type(context, id):
@ -50,20 +68,54 @@ def return_volume_types_with_volumes_destroy(context, id):
pass pass
def return_volume_types_create(context, name, specs, is_public): def return_volume_types_create(context,
name,
specs,
is_public,
description):
pass 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) 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): def return_volume_types_get_by_name(context, name):
if name == "777": if name == "777":
raise exception.VolumeTypeNotFoundByName(volume_type_name=name) raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
return stub_volume_type(int(name.split("_")[2])) 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): class VolumeTypesManageApiTest(test.TestCase):
def setUp(self): def setUp(self):
super(VolumeTypesManageApiTest, self).setUp() super(VolumeTypesManageApiTest, self).setUp()
@ -83,9 +135,9 @@ class VolumeTypesManageApiTest(test.TestCase):
return_volume_types_destroy) return_volume_types_destroy)
req = fakes.HTTPRequest.blank('/v2/fake/types/1') 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.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): def test_volume_types_delete_not_found(self):
self.stubs.Set(volume_types, 'get_volume_type', self.stubs.Set(volume_types, 'get_volume_type',
@ -93,11 +145,11 @@ class VolumeTypesManageApiTest(test.TestCase):
self.stubs.Set(volume_types, 'destroy', self.stubs.Set(volume_types, 'destroy',
return_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') req = fakes.HTTPRequest.blank('/v2/fake/types/777')
self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete, self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete,
req, '777') req, '777')
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1) self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
def test_volume_types_with_volumes_destroy(self): def test_volume_types_with_volumes_destroy(self):
self.stubs.Set(volume_types, 'get_volume_type', self.stubs.Set(volume_types, 'get_volume_type',
@ -105,9 +157,9 @@ class VolumeTypesManageApiTest(test.TestCase):
self.stubs.Set(volume_types, 'destroy', self.stubs.Set(volume_types, 'destroy',
return_volume_types_with_volumes_destroy) return_volume_types_with_volumes_destroy)
req = fakes.HTTPRequest.blank('/v2/fake/types/1') 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.controller._delete(req, 1)
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1) self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
def test_create(self): def test_create(self):
self.stubs.Set(volume_types, 'create', self.stubs.Set(volume_types, 'create',
@ -119,12 +171,12 @@ class VolumeTypesManageApiTest(test.TestCase):
"extra_specs": {"key1": "value1"}}} "extra_specs": {"key1": "value1"}}}
req = fakes.HTTPRequest.blank('/v2/fake/types') 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) res_dict = self.controller._create(req, body)
self.assertEqual(len(fake_notifier.NOTIFICATIONS), 1) self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
self.assertEqual(1, len(res_dict)) self._check_test_results(res_dict, {
self.assertEqual('vol_type_1', res_dict['volume_type']['name']) 'expected_name': 'vol_type_1', 'expected_desc': 'vol_type_desc_1'})
def test_create_duplicate_type_fail(self): def test_create_duplicate_type_fail(self):
self.stubs.Set(volume_types, 'create', self.stubs.Set(volume_types, 'create',
@ -154,3 +206,65 @@ class VolumeTypesManageApiTest(test.TestCase):
def test_create_malformed_entity(self): def test_create_malformed_entity(self):
body = {'volume_type': 'string'} body = {'volume_type': 'string'}
self._create_volume_type_bad_body(body=body) 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'])

View File

@ -233,9 +233,6 @@ class VolumeTypeAccessTest(test.TestCase):
self.type_action_controller.show(self.req, resp, '0') self.type_action_controller.show(self.req, resp, '0')
self.assertEqual({'id': '0', 'os-volume-type-access:is_public': True}, self.assertEqual({'id': '0', 'os-volume-type-access:is_public': True},
resp.obj['volume_type']) 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): def test_detail(self):
resp = FakeResponse() resp = FakeResponse()

View File

@ -118,6 +118,7 @@ class VolumeTypesApiTest(test.TestCase):
updated_at=now, updated_at=now,
extra_specs={}, extra_specs={},
deleted_at=None, deleted_at=None,
description=None,
id=42) id=42)
request = fakes.HTTPRequest.blank("/v1") request = fakes.HTTPRequest.blank("/v1")
@ -126,6 +127,7 @@ class VolumeTypesApiTest(test.TestCase):
self.assertIn('volume_type', output) self.assertIn('volume_type', output)
expected_volume_type = dict(name='new_type', expected_volume_type = dict(name='new_type',
extra_specs={}, extra_specs={},
description=None,
id=42) id=42)
self.assertDictMatch(output['volume_type'], expected_volume_type) self.assertDictMatch(output['volume_type'], expected_volume_type)
@ -141,6 +143,7 @@ class VolumeTypesApiTest(test.TestCase):
updated_at=now, updated_at=now,
extra_specs={}, extra_specs={},
deleted_at=None, deleted_at=None,
description=None,
id=42 + i)) id=42 + i))
request = fakes.HTTPRequest.blank("/v1") request = fakes.HTTPRequest.blank("/v1")
@ -150,7 +153,8 @@ class VolumeTypesApiTest(test.TestCase):
for i in range(0, 10): for i in range(0, 10):
expected_volume_type = dict(name='new_type', expected_volume_type = dict(name='new_type',
extra_specs={}, extra_specs={},
id=42 + i) id=42 + i,
description=None)
self.assertDictMatch(output['volume_types'][i], self.assertDictMatch(output['volume_types'][i],
expected_volume_type) expected_volume_type)

View File

@ -17,6 +17,7 @@ import uuid
from lxml import etree from lxml import etree
from oslo.utils import timeutils from oslo.utils import timeutils
import six
import webob import webob
from cinder.api.v2 import types from cinder.api.v2 import types
@ -37,7 +38,8 @@ def stub_volume_type(id):
} }
return dict( return dict(
id=id, 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, extra_specs=specs,
) )
@ -66,6 +68,14 @@ def return_volume_types_get_by_name(context, name):
return stub_volume_type(int(name.split("_")[2])) 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): class VolumeTypesApiTest(test.TestCase):
def setUp(self): def setUp(self):
super(VolumeTypesApiTest, self).setUp() super(VolumeTypesApiTest, self).setUp()
@ -116,12 +126,33 @@ class VolumeTypesApiTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
req, '777') 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): def test_view_builder_show(self):
view_builder = views_types.ViewBuilder() view_builder = views_types.ViewBuilder()
now = timeutils.isotime() now = timeutils.isotime()
raw_volume_type = dict( raw_volume_type = dict(
name='new_type', name='new_type',
description='new_type_desc',
deleted=False, deleted=False,
created_at=now, created_at=now,
updated_at=now, updated_at=now,
@ -136,6 +167,7 @@ class VolumeTypesApiTest(test.TestCase):
self.assertIn('volume_type', output) self.assertIn('volume_type', output)
expected_volume_type = dict( expected_volume_type = dict(
name='new_type', name='new_type',
description='new_type_desc',
extra_specs={}, extra_specs={},
id=42, id=42,
) )
@ -150,6 +182,7 @@ class VolumeTypesApiTest(test.TestCase):
raw_volume_types.append( raw_volume_types.append(
dict( dict(
name='new_type', name='new_type',
description='new_type_desc',
deleted=False, deleted=False,
created_at=now, created_at=now,
updated_at=now, updated_at=now,
@ -166,6 +199,7 @@ class VolumeTypesApiTest(test.TestCase):
for i in range(0, 10): for i in range(0, 10):
expected_volume_type = dict( expected_volume_type = dict(
name='new_type', name='new_type',
description='new_type_desc',
extra_specs={}, extra_specs={},
id=42 + i id=42 + i
) )
@ -177,6 +211,7 @@ class VolumeTypesSerializerTest(test.TestCase):
def _verify_volume_type(self, vtype, tree): def _verify_volume_type(self, vtype, tree):
self.assertEqual('volume_type', tree.tag) self.assertEqual('volume_type', tree.tag)
self.assertEqual(vtype['name'], tree.get('name')) self.assertEqual(vtype['name'], tree.get('name'))
self.assertEqual(vtype['description'], tree.get('description'))
self.assertEqual(str(vtype['id']), tree.get('id')) self.assertEqual(str(vtype['id']), tree.get('id'))
self.assertEqual(1, len(tree)) self.assertEqual(1, len(tree))
extra_specs = tree[0] extra_specs = tree[0]

View File

@ -694,6 +694,16 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
encryptions = db_utils.get_table(engine, 'encryption') encryptions = db_utils.get_table(engine, 'encryption')
self.assertNotIn('encryption_id', encryptions.c) 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): def test_walk_versions(self):
self.walk_versions(True, False) self.walk_versions(True, False)

View File

@ -49,20 +49,25 @@ class VolumeTypeTestCase(test.TestCase):
size="300", size="300",
rpm="7200", rpm="7200",
visible="True") visible="True")
self.vol_type1_description = self.vol_type1_name + '_desc'
def test_volume_type_create_then_destroy(self): def test_volume_type_create_then_destroy(self):
"""Ensure volume types can be created and deleted.""" """Ensure volume types can be created and deleted."""
prev_all_vtypes = volume_types.get_all_types(self.ctxt) prev_all_vtypes = volume_types.get_all_types(self.ctxt)
# create
type_ref = volume_types.create(self.ctxt, type_ref = volume_types.create(self.ctxt,
self.vol_type1_name, 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, new = volume_types.get_volume_type_by_name(self.ctxt,
self.vol_type1_name) self.vol_type1_name)
LOG.info(_("Given data: %s"), self.vol_type1_specs) LOG.info(_("Given data: %s"), self.vol_type1_specs)
LOG.info(_("Result data: %s"), new) LOG.info(_("Result data: %s"), new)
self.assertEqual(self.vol_type1_description, new['description'])
for k, v in self.vol_type1_specs.iteritems(): for k, v in self.vol_type1_specs.iteritems():
self.assertEqual(v, new['extra_specs'][k], self.assertEqual(v, new['extra_specs'][k],
'one of fields does not match') 'one of fields does not match')
@ -72,6 +77,14 @@ class VolumeTypeTestCase(test.TestCase):
len(new_all_vtypes), len(new_all_vtypes),
'drive type was not created') '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']) volume_types.destroy(self.ctxt, type_ref['id'])
new_all_vtypes = volume_types.get_all_types(self.ctxt) new_all_vtypes = volume_types.get_all_types(self.ctxt)
self.assertEqual(prev_all_vtypes, self.assertEqual(prev_all_vtypes,

View File

@ -22,6 +22,7 @@
from oslo.config import cfg from oslo.config import cfg
from oslo.db import exception as db_exc from oslo.db import exception as db_exc
import six
from cinder import context from cinder import context
from cinder import db from cinder import db
@ -34,7 +35,12 @@ CONF = cfg.CONF
LOG = logging.getLogger(__name__) 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.""" """Creates volume types."""
extra_specs = extra_specs or {} extra_specs = extra_specs or {}
projects = projects 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, type_ref = db.volume_type_create(context,
dict(name=name, dict(name=name,
extra_specs=extra_specs, extra_specs=extra_specs,
is_public=is_public), is_public=is_public,
description=description),
projects=projects) projects=projects)
except db_exc.DBError as e: 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, raise exception.VolumeTypeCreateFailed(name=name,
extra_specs=extra_specs) extra_specs=extra_specs)
return type_ref 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): def destroy(context, id):
"""Marks volume types as deleted.""" """Marks volume types as deleted."""
if id is None: 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 # Couldn't find volume type with the name in default_volume_type
# flag, record this issue and move on # flag, record this issue and move on
#TODO(zhiteng) consider add notification to warn admin #TODO(zhiteng) consider add notification to warn admin
LOG.exception(_LE('Default volume type is not found, ' LOG.exception(_LE('Default volume type is not found,'
'please check default_volume_type ' 'please check default_volume_type config: %s') %
'config: %s'), e) six.text_type(e))
return vol_type return vol_type