]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Update quota when volume type renames
authorLisaLi <xiaoyan.li@intel.com>
Fri, 15 Jan 2016 08:12:45 +0000 (16:12 +0800)
committerLisaLi <xiaoyan.li@intel.com>
Mon, 15 Feb 2016 03:22:21 +0000 (11:22 +0800)
When customers rename volume type, the corresponding quota_usages
should be changed. Or else quota_usage shows incorrect data.

Meanwhile, it deletes quota_usages whose corresponding volume
types don't exist. These invalid data are left when renaming
volume types in old versions.

Closes-bug: #1473183

Change-Id: I9071821f8c1a95fccef214868e5cea026fed9657
Co-Authored-By: wanghao <wanghao749@huawei.com>
cinder/db/api.py
cinder/db/sqlalchemy/api.py
cinder/quota.py
cinder/tests/unit/api/contrib/test_types_manage.py
cinder/tests/unit/test_db_api.py
cinder/tests/unit/test_quota.py
cinder/tests/unit/test_volume_types.py
cinder/volume/volume_types.py

index 2859f99ee66579ac5cf7d627cf3bee9d4cb6ae3c..71674ff430d73611b792e5ff16afdf42f102e2e2 100644 (file)
@@ -752,6 +752,11 @@ def quota_update(context, project_id, resource, limit):
     return IMPL.quota_update(context, project_id, resource, limit)
 
 
+def quota_update_resource(context, old_res, new_res):
+    """Update resource of quotas."""
+    return IMPL.quota_update_resource(context, old_res, new_res)
+
+
 def quota_destroy(context, project_id, resource):
     """Destroy the quota or raise if it does not exist."""
     return IMPL.quota_destroy(context, project_id, resource)
@@ -785,6 +790,11 @@ def quota_class_update(context, class_name, resource, limit):
     return IMPL.quota_class_update(context, class_name, resource, limit)
 
 
+def quota_class_update_resource(context, resource, new_resource):
+    """Update resource name in quota_class."""
+    return IMPL.quota_class_update_resource(context, resource, new_resource)
+
+
 def quota_class_destroy(context, class_name, resource):
     """Destroy the quota class or raise if it does not exist."""
     return IMPL.quota_class_destroy(context, class_name, resource)
@@ -840,6 +850,11 @@ def reservation_expire(context):
     return IMPL.reservation_expire(context)
 
 
+def quota_usage_update_resource(context, old_res, new_res):
+    """Update resource field in quota_usages."""
+    return IMPL.quota_usage_update_resource(context, old_res, new_res)
+
+
 ###################
 
 
index f859bb7159ce9643a93353172d85d76176491715..1fc0cfbaffd6c31bf44150559f85685b09a95f39 100644 (file)
@@ -535,6 +535,15 @@ def quota_allocated_get_all_by_project(context, project_id):
     return result
 
 
+@require_context
+def _quota_get_by_resource(context, resource, session=None):
+    rows = model_query(context, models.Quota,
+                       session=session,
+                       read_deleted='no').filter_by(
+        resource=resource).all()
+    return rows
+
+
 @require_admin_context
 def quota_create(context, project_id, resource, limit, allocated):
     quota_ref = models.Quota()
@@ -559,6 +568,15 @@ def quota_update(context, project_id, resource, limit):
         return quota_ref
 
 
+@require_context
+def quota_update_resource(context, old_res, new_res):
+    session = get_session()
+    with session.begin():
+        quotas = _quota_get_by_resource(context, old_res, session=session)
+        for quota in quotas:
+            quota.resource = new_res
+
+
 @require_admin_context
 def quota_allocated_update(context, project_id, resource, allocated):
     session = get_session()
@@ -625,6 +643,17 @@ def quota_class_get_all_by_name(context, class_name):
     return result
 
 
+@require_context
+def _quota_class_get_all_by_resource(context, resource, session):
+    result = model_query(context, models.QuotaClass,
+                         session=session,
+                         read_deleted="no").\
+        filter_by(resource=resource).\
+        all()
+
+    return result
+
+
 @require_admin_context
 def quota_class_create(context, class_name, resource, limit):
     quota_class_ref = models.QuotaClass()
@@ -648,6 +677,16 @@ def quota_class_update(context, class_name, resource, limit):
         return quota_class_ref
 
 
+@require_context
+def quota_class_update_resource(context, old_res, new_res):
+    session = get_session()
+    with session.begin():
+        quota_class_list = _quota_class_get_all_by_resource(
+            context, old_res, session)
+        for quota_class in quota_class_list:
+            quota_class.resource = new_res
+
+
 @require_admin_context
 def quota_class_destroy(context, class_name, resource):
     session = get_session()
@@ -751,6 +790,27 @@ def _get_quota_usages(context, session, project_id):
     return {row.resource: row for row in rows}
 
 
+def _get_quota_usages_by_resource(context, session, resource):
+    rows = model_query(context, models.QuotaUsage,
+                       deleted="no",
+                       session=session).\
+        filter_by(resource=resource).\
+        with_lockmode('update').\
+        all()
+    return rows
+
+
+@require_context
+@_retry_on_deadlock
+def quota_usage_update_resource(context, old_res, new_res):
+    session = get_session()
+    with session.begin():
+        usages = _get_quota_usages_by_resource(context, session, old_res)
+        for usage in usages:
+            usage.resource = new_res
+            usage.until_refresh = 1
+
+
 @require_context
 @_retry_on_deadlock
 def quota_reserve(context, resources, quotas, deltas, expire,
@@ -903,15 +963,20 @@ def _quota_reservations(session, context, reservations):
         all()
 
 
+def _dict_with_usage_id(usages):
+    return {row.id: row for row in usages.values()}
+
+
 @require_context
 @_retry_on_deadlock
 def reservation_commit(context, reservations, project_id=None):
     session = get_session()
     with session.begin():
         usages = _get_quota_usages(context, session, project_id)
+        usages = _dict_with_usage_id(usages)
 
         for reservation in _quota_reservations(session, context, reservations):
-            usage = usages[reservation.resource]
+            usage = usages[reservation.usage_id]
             if reservation.delta >= 0:
                 usage.reserved -= reservation.delta
             usage.in_use += reservation.delta
@@ -925,9 +990,9 @@ def reservation_rollback(context, reservations, project_id=None):
     session = get_session()
     with session.begin():
         usages = _get_quota_usages(context, session, project_id)
-
+        usages = _dict_with_usage_id(usages)
         for reservation in _quota_reservations(session, context, reservations):
-            usage = usages[reservation.resource]
+            usage = usages[reservation.usage_id]
             if reservation.delta >= 0:
                 usage.reserved -= reservation.delta
 
index f32931db2ed9bff011a9ef59703f662a32e9c924..1226077de7ed866d590b40f4bd68429d56f3791f 100644 (file)
@@ -942,6 +942,30 @@ class VolumeTypeQuotaEngine(QuotaEngine):
     def register_resources(self, resources):
         raise NotImplementedError(_("Cannot register resources"))
 
+    def update_quota_resource(self, context, old_type_name, new_type_name):
+        """Update resource in quota.
+
+        This is to update resource in quotas, quota_classes, and
+        quota_usages once the name of a volume type is changed.
+
+        :param context: The request context, for access checks.
+        :param old_type_name: old name of volume type.
+        :param new_type_name: new name of volume type.
+        """
+
+        for quota in ('volumes', 'gigabytes', 'snapshots'):
+            old_res = "%s_%s" % (quota, old_type_name)
+            new_res = "%s_%s" % (quota, new_type_name)
+            db.quota_usage_update_resource(context,
+                                           old_res,
+                                           new_res)
+            db.quota_class_update_resource(context,
+                                           old_res,
+                                           new_res)
+            db.quota_update_resource(context,
+                                     old_res,
+                                     new_res)
+
 
 class CGQuotaEngine(QuotaEngine):
     """Represent the consistencygroup quotas."""
index 629b2b2bc2628733737ad9a994e4e2a1815637fc..c39d75bec6ab24563bd984a82e2c074fb3a754a0 100644 (file)
@@ -18,6 +18,7 @@ import six
 import webob
 
 from cinder.api.contrib import types_manage
+from cinder import context
 from cinder import exception
 from cinder import test
 from cinder.tests.unit.api import fakes
@@ -365,19 +366,26 @@ class VolumeTypesManageApiTest(test.TestCase):
         self.assertRaises(webob.exc.HTTPBadRequest,
                           self.controller._update, req, '1', body)
 
-    @mock.patch('cinder.volume.volume_types.update')
     @mock.patch('cinder.volume.volume_types.get_volume_type')
-    def test_update_only_name(self, mock_get, mock_update):
+    @mock.patch('cinder.db.volume_type_update')
+    @mock.patch('cinder.quota.VolumeTypeQuotaEngine.'
+                'update_quota_resource')
+    def test_update_only_name(self, mock_update_quota,
+                              mock_update, mock_get):
         mock_get.return_value = return_volume_types_get_volume_type_updated(
             '999')
 
-        body = {"volume_type": {"name": "vol_type_999_999"}}
+        ctxt = context.RequestContext('admin', 'fake', True)
+        body = {"volume_type": {"name": "vol_type_999"}}
         req = fakes.HTTPRequest.blank('/v2/fake/types/999')
         req.method = 'PUT'
+        req.environ['cinder.context'] = ctxt
 
         self.assertEqual(0, len(self.notifier.notifications))
         res_dict = self.controller._update(req, '999', body)
         self.assertEqual(1, len(self.notifier.notifications))
+        mock_update_quota.assert_called_once_with(ctxt, 'vol_type_999_999',
+                                                  'vol_type_999')
         self._check_test_results(res_dict,
                                  {'expected_name': 'vol_type_999_999',
                                   'expected_desc': 'vol_type_desc_999'})
index 058d895b0573189d80e5a72239625ea58a00fa1b..3dfa4e0d9edf9184834f5c6cbf1d2966340f0787 100644 (file)
@@ -1710,6 +1710,15 @@ class DBAPIQuotaClassTestCase(BaseTest):
         updated = db.quota_class_get(self.ctxt, 'test_qc', 'test_resource')
         self.assertEqual(43, updated['hard_limit'])
 
+    def test_quota_class_update_resource(self):
+        old = db.quota_class_get(self.ctxt, 'test_qc', 'test_resource')
+        db.quota_class_update_resource(self.ctxt,
+                                       'test_resource',
+                                       'test_resource1')
+        new = db.quota_class_get(self.ctxt, 'test_qc', 'test_resource1')
+        self.assertEqual(old.id, new.id)
+        self.assertEqual('test_resource1', new.resource)
+
     def test_quota_class_destroy_all_by_name(self):
         db.quota_class_create(self.ctxt, 'test2', 'res1', 43)
         db.quota_class_create(self.ctxt, 'test2', 'res2', 44)
@@ -1752,6 +1761,13 @@ class DBAPIQuotaTestCase(BaseTest):
         self.assertEqual('resource1', quota.resource)
         self.assertEqual('project1', quota.project_id)
 
+    def test_quota_update_resource(self):
+        old = db.quota_create(self.ctxt, 'project1', 'resource1', 41)
+        db.quota_update_resource(self.ctxt, 'resource1', 'resource2')
+        new = db.quota_get(self.ctxt, 'project1', 'resource2')
+        self.assertEqual(old.id, new.id)
+        self.assertEqual('resource2', new.resource)
+
     def test_quota_update_nonexistent(self):
         self.assertRaises(exception.ProjectQuotaNotFound,
                           db.quota_update,
index 11f792d44da4846524d1275c7abb12daf0eac481..22a5e4204609d65ec58ec9d0b36864858c678398 100644 (file)
@@ -885,6 +885,12 @@ class VolumeTypeQuotaEngineTestCase(test.TestCase):
         db.volume_type_destroy(ctx, vtype['id'])
         db.volume_type_destroy(ctx, vtype2['id'])
 
+    def test_update_quota_resource(self):
+        ctx = context.RequestContext('admin', 'admin', is_admin=True)
+
+        engine = quota.VolumeTypeQuotaEngine()
+        engine.update_quota_resource(ctx, 'type1', 'type2')
+
 
 class DbQuotaDriverTestCase(test.TestCase):
     def setUp(self):
index 747f4fd3e40b69382c4301e0f651860017c98cf9..5016bb820dd3bf669c61985c4c9b272172638b8d 100644 (file)
@@ -15,6 +15,7 @@
 
 
 import datetime
+import mock
 import time
 
 from oslo_config import cfg
@@ -84,6 +85,23 @@ class VolumeTypeTestCase(test.TestCase):
                          new_all_vtypes,
                          'drive type was not deleted')
 
+    @mock.patch('cinder.quota.VolumeTypeQuotaEngine.'
+                'update_quota_resource')
+    def test_update_volume_type_name(self, mock_update_quota):
+        type_ref = volume_types.create(self.ctxt,
+                                       self.vol_type1_name,
+                                       self.vol_type1_specs,
+                                       description=self.vol_type1_description)
+        new_type_name = self.vol_type1_name + '_updated'
+        volume_types.update(self.ctxt,
+                            type_ref.id,
+                            new_type_name,
+                            None)
+        mock_update_quota.assert_called_once_with(self.ctxt,
+                                                  self.vol_type1_name,
+                                                  new_type_name)
+        volume_types.destroy(self.ctxt, type_ref.id)
+
     def test_create_volume_type_with_invalid_params(self):
         """Ensure exception will be returned."""
         vol_type_invalid_specs = "invalid_extra_specs"
index 57ef554cae545de5135660ed8dc46fa33defdcc0..a800d156112ef99bbb1ff92398f95b1028ba3319 100644 (file)
@@ -28,10 +28,11 @@ from cinder import context
 from cinder import db
 from cinder import exception
 from cinder.i18n import _, _LE
-
+from cinder import quota
 
 CONF = cfg.CONF
 LOG = logging.getLogger(__name__)
+QUOTAS = quota.QUOTAS
 
 
 def create(context,
@@ -62,12 +63,20 @@ def update(context, id, name, description, is_public=None):
     if id is None:
         msg = _("id cannot be None")
         raise exception.InvalidVolumeType(reason=msg)
+    old_volume_type = get_volume_type(context, id)
     try:
         type_updated = db.volume_type_update(context,
                                              id,
                                              dict(name=name,
                                                   description=description,
                                                   is_public=is_public))
+        # Rename resource in quota if volume type name is changed.
+        if name:
+            old_type_name = old_volume_type.get('name')
+            if old_type_name != name:
+                QUOTAS.update_quota_resource(context,
+                                             old_type_name,
+                                             name)
     except db_exc.DBError:
         LOG.exception(_LE('DB error:'))
         raise exception.VolumeTypeUpdateFailed(id=id)