]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Added volume type description for volume type API
authorGloria Gu <gloria.gu@hp.com>
Fri, 5 Dec 2014 01:59:10 +0000 (17:59 -0800)
committerGloria Gu <gloria.gu@hp.com>
Wed, 17 Dec 2014 02:13:24 +0000 (18:13 -0800)
- 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

18 files changed:
cinder/api/contrib/types_manage.py
cinder/api/contrib/volume_type_access.py
cinder/api/openstack/wsgi.py
cinder/api/v2/types.py
cinder/api/views/types.py
cinder/db/api.py
cinder/db/sqlalchemy/api.py
cinder/db/sqlalchemy/migrate_repo/versions/034_sqlite_downgrade.sql [new file with mode: 0644]
cinder/db/sqlalchemy/migrate_repo/versions/034_volume_type_add_desc_column.py [new file with mode: 0644]
cinder/db/sqlalchemy/models.py
cinder/exception.py
cinder/tests/api/contrib/test_types_manage.py
cinder/tests/api/contrib/test_volume_type_access.py
cinder/tests/api/v1/test_types.py
cinder/tests/api/v2/test_types.py
cinder/tests/test_migrations.py
cinder/tests/test_volume_types.py
cinder/volume/volume_types.py

index 96e47f95e8e4055de80bc18608b17986f7218fd0..2839fd93977367a6429e1347b9ab66f256af7939 100644 (file)
@@ -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)
index 5371316a88dca546223656e051ed33b83be41d1d..50f6131f26a2cfe741ce101efcb494b9c36bc041 100644 (file)
@@ -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):
index 84ca382ed41b033860e74c6d7e3888767efb57d8..648a88407e24635a730c7c3dc447070e427a2bca 100644 (file)
@@ -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, [])
 
index c1b2dfc796e207837ecdc9b8b02db935970be4bd..5710df97a98b38d57e583b2b99b76c1a899e79c3 100644 (file)
@@ -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)
 
index d542c9f9ab64c03a0c4357786e1e11861eef29f4..78058df0a3df7f443d6844f1079cd6ae3d508e51 100644 (file)
@@ -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):
index 36c66b8316f966bea986091d7bccb2cf35c8ab5e..4a393e60d752dd2f8741da9edbb5bf17c4771db3 100644 (file)
@@ -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.
 
index ee23660e60c026ad57929341f1d52e714be08292..7eb6ddac32e33d515123ce47107ef7533140c966 100644 (file)
@@ -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).\
diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/034_sqlite_downgrade.sql b/cinder/db/sqlalchemy/migrate_repo/versions/034_sqlite_downgrade.sql
new file mode 100644 (file)
index 0000000..9d4d1a8
--- /dev/null
@@ -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;
diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/034_volume_type_add_desc_column.py b/cinder/db/sqlalchemy/migrate_repo/versions/034_volume_type_add_desc_column.py
new file mode 100644 (file)
index 0000000..b336aae
--- /dev/null
@@ -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)
index c7a53efd089460cd85a843f5881df2ddd4db659b..5667f119e9fcd496083899979b16be389c2ef6ca 100644 (file)
@@ -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'))
index b87b734e74c3a4cb2b6c792bf8e265f1e54458be..7c746cf93881af9c4131417c7727dbdd8ad8be23 100755 (executable)
@@ -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")
 
index 2fd4de350a16d7fc327c4485f60aba26d7223a82..fadc96bf56832e32f5307b57310c2978d81689b3 100644 (file)
@@ -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'])
\ No newline at end of file
index 9bb62693ca239015e77c71b581c9d4255f19602f..218ccd83575c688b4d52d5b4f50920652aca4003 100644 (file)
@@ -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()
index 7b8b873cbb31d94ceeffcab3549692c03952fbc2..13c8836d51e223f6a5ab0d4f6e2242c3745aeed8 100644 (file)
@@ -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)
 
index 8b120a6c37f5b403c582131ca1790d095fcbb43e..66bd9cedc3c0d2008485a797ae272192fba4861e 100644 (file)
@@ -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]
index 94a1d96eda809947543bc7f7eb85f79a5eb15ad5..1cd8c42d7d3948d8fbb58d8d852c57b1612218cf 100644 (file)
@@ -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)
 
index 3fc7c779ba1754506df2fdd7c0a5ae704f69aef9..393ee6b52be21651da816f95022402b62ebff3f0 100644 (file)
@@ -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,
index c196ff1e51dc47748d3aff86cf8dce44d740f190..5bff32ba5bc9d844ddaa8e9842085023429ecf23 100644 (file)
@@ -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