]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Quotas by Volume Type
authorCory Stone <corystone@gmail.com>
Wed, 22 May 2013 15:31:13 +0000 (10:31 -0500)
committerCory Stone <corystone@gmail.com>
Thu, 27 Jun 2013 15:29:04 +0000 (10:29 -0500)
The VolumeTypeQuotaEngine creates a volume, snapshot, and gigabyte
quota for each volume type that exists, in addition to the existing
global quotas for each. These are queried every time a quota operation
happens.

The resources for creating quotas are named: volumes_<vtype>,
gigabytes_<vtype>, and snapshots_<vtype>.

Another patch will have changes to cinderclient for setting project
quotas by volume type.

blueprint quotas-limits-by-voltype
DocImpact

Change-Id: I88261676edcd6eb5b7cea40654a931f32c00815c

cinder/db/api.py
cinder/db/sqlalchemy/api.py
cinder/quota.py
cinder/tests/test_quota.py
cinder/volume/api.py
cinder/volume/manager.py

index 15e4a3f78bfefe1c8c7d6d4699f24a8f8f05d708..dbf3230c677eef46aca52361f946e00bf91578ab 100644 (file)
@@ -216,10 +216,12 @@ def volume_data_get_for_host(context, host, session=None):
                                          session)
 
 
-def volume_data_get_for_project(context, project_id, session=None):
+def volume_data_get_for_project(context, project_id, volume_type_id=None,
+                                session=None):
     """Get (volume_count, gigabytes) for project."""
     return IMPL.volume_data_get_for_project(context,
                                             project_id,
+                                            volume_type_id,
                                             session)
 
 
@@ -316,10 +318,12 @@ def snapshot_update(context, snapshot_id, values):
     return IMPL.snapshot_update(context, snapshot_id, values)
 
 
-def snapshot_data_get_for_project(context, project_id, session=None):
+def snapshot_data_get_for_project(context, project_id, volume_type_id=None,
+                                  session=None):
     """Get count and gigabytes used for snapshots for specified project."""
     return IMPL.snapshot_data_get_for_project(context,
                                               project_id,
+                                              volume_type_id,
                                               session)
 
 
index c5735fc85de43f09dda6d6905c2d0a2a652bccc8..af6da757af349204e246c36da811a933cf2ca00f 100644 (file)
@@ -987,14 +987,19 @@ def volume_data_get_for_host(context, host, session=None):
 
 
 @require_admin_context
-def volume_data_get_for_project(context, project_id, session=None):
-    result = model_query(context,
-                         func.count(models.Volume.id),
-                         func.sum(models.Volume.size),
-                         read_deleted="no",
-                         session=session).\
-        filter_by(project_id=project_id).\
-        first()
+def volume_data_get_for_project(context, project_id, volume_type_id=None,
+                                session=None):
+    query = model_query(context,
+                        func.count(models.Volume.id),
+                        func.sum(models.Volume.size),
+                        read_deleted="no",
+                        session=session).\
+        filter_by(project_id=project_id)
+
+    if volume_type_id:
+        query = query.filter_by(volume_type_id=volume_type_id)
+
+    result = query.first()
 
     # NOTE(vish): convert None to 0
     return (result[0] or 0, result[1] or 0)
@@ -1282,15 +1287,20 @@ def snapshot_get_all_by_project(context, project_id):
 
 
 @require_context
-def snapshot_data_get_for_project(context, project_id, session=None):
+def snapshot_data_get_for_project(context, project_id, volume_type_id=None,
+                                  session=None):
     authorize_project_context(context, project_id)
-    result = model_query(context,
-                         func.count(models.Snapshot.id),
-                         func.sum(models.Snapshot.volume_size),
-                         read_deleted="no",
-                         session=session).\
-        filter_by(project_id=project_id).\
-        first()
+    query = model_query(context,
+                        func.count(models.Snapshot.id),
+                        func.sum(models.Snapshot.volume_size),
+                        read_deleted="no",
+                        session=session).\
+        filter_by(project_id=project_id)
+
+    if volume_type_id:
+        query = query.join('volume').filter_by(volume_type_id=volume_type_id)
+
+    result = query.first()
 
     # NOTE(vish): convert None to 0
     return (result[0] or 0, result[1] or 0)
index fdf73138c9260773986d9f04701850536de3f4a1..16f77a5107a180f6bbf51425b3c14b4cf383285f 100644 (file)
@@ -23,6 +23,7 @@ import datetime
 
 from oslo.config import cfg
 
+from cinder import context
 from cinder import db
 from cinder import exception
 from cinder.openstack.common import importutils
@@ -70,15 +71,21 @@ class DbQuotaDriver(object):
     database.
     """
 
-    def get_by_project(self, context, project_id, resource):
+    def get_by_project(self, context, project_id, resource_name):
         """Get a specific quota by project."""
 
-        return db.quota_get(context, project_id, resource)
+        return db.quota_get(context, project_id, resource_name)
 
-    def get_by_class(self, context, quota_class, resource):
+    def get_by_class(self, context, quota_class, resource_name):
         """Get a specific quota by quota class."""
 
-        return db.quota_class_get(context, quota_class, resource)
+        return db.quota_class_get(context, quota_class, resource_name)
+
+    def get_default(self, context, resource):
+        """Get a specific default quota for a resource."""
+
+        default_quotas = db.quota_class_get_default(context)
+        return default_quotas.get(resource.name, resource.default)
 
     def get_defaults(self, context, resources):
         """Given a list of resources, retrieve the default quotas.
@@ -121,11 +128,18 @@ class DbQuotaDriver(object):
         """
 
         quotas = {}
+        default_quotas = {}
         class_quotas = db.quota_class_get_all_by_name(context, quota_class)
+        if defaults:
+            default_quotas = db.quota_class_get_default(context)
         for resource in resources.values():
-            if defaults or resource.name in class_quotas:
-                quotas[resource.name] = class_quotas.get(resource.name,
-                                                         resource.default)
+            if resource.name in class_quotas:
+                quotas[resource.name] = class_quotas[resource.name]
+                continue
+
+            if defaults:
+                quotas[resource.name] = default_quotas.get(resource.name,
+                                                           resource.default)
 
         return quotas
 
@@ -460,7 +474,7 @@ class BaseResource(object):
                 pass
 
         # OK, return the default
-        return self.default
+        return driver.get_default(context, self)
 
     @property
     def default(self):
@@ -551,6 +565,64 @@ class CountableResource(AbsoluteResource):
         self.count = count
 
 
+class VolumeTypeResource(ReservableResource):
+    """ReservableResource for a specific volume type."""
+
+    def __init__(self, part_name, volume_type):
+        """
+        Initializes a VolumeTypeResource.
+
+        :param part_name: The kind of resource, i.e., "volumes".
+        :param volume_type: The volume type for this resource.
+        """
+
+        try:
+            method = getattr(self, '_sync_%s' % part_name)
+        except AttributeError:
+            raise ValueError('Invalid resource: %s' % part_name)
+
+        self.volume_type_name = volume_type['name']
+        self.volume_type_id = volume_type['id']
+        name = "%s_%s" % (part_name, self.volume_type_name)
+        super(VolumeTypeResource, self).__init__(name, method)
+
+    def _sync_snapshots(self, context, project_id, session):
+        """Sync snapshots for this specific volume type."""
+        (snapshots, gigs) = db.snapshot_data_get_for_project(
+            context,
+            project_id,
+            volume_type_id=self.volume_type_id,
+            session=session)
+        return {'snapshots_%s' % self.volume_type_name: snapshots}
+
+    def _sync_volumes(self, context, project_id, session):
+        """Sync volumes for this specific volume type."""
+        (volumes, gigs) = db.volume_data_get_for_project(
+            context,
+            project_id,
+            volume_type_id=self.volume_type_id,
+            session=session)
+        return {'volumes_%s' % self.volume_type_name: volumes}
+
+    def _sync_gigabytes(self, context, project_id, session):
+        """Sync gigabytes for this specific volume type."""
+        key = 'gigabytes_%s' % self.volume_type_name
+        (_junk, vol_gigs) = db.volume_data_get_for_project(
+            context,
+            project_id,
+            volume_type_id=self.volume_type_id,
+            session=session)
+        if CONF.no_snapshot_gb_quota:
+            return {key: vol_gigs}
+
+        (_junk, snap_gigs) = db.snapshot_data_get_for_project(
+            context,
+            project_id,
+            volume_type_id=self.volume_type_id,
+            session=session)
+        return {key: vol_gigs + snap_gigs}
+
+
 class QuotaEngine(object):
     """Represent the set of recognized quotas."""
 
@@ -567,7 +639,7 @@ class QuotaEngine(object):
         self._driver = quota_driver_class
 
     def __contains__(self, resource):
-        return resource in self._resources
+        return resource in self.resources
 
     def register_resource(self, resource):
         """Register a resource."""
@@ -580,15 +652,20 @@ class QuotaEngine(object):
         for resource in resources:
             self.register_resource(resource)
 
-    def get_by_project(self, context, project_id, resource):
+    def get_by_project(self, context, project_id, resource_name):
         """Get a specific quota by project."""
 
-        return self._driver.get_by_project(context, project_id, resource)
+        return self._driver.get_by_project(context, project_id, resource_name)
 
-    def get_by_class(self, context, quota_class, resource):
+    def get_by_class(self, context, quota_class, resource_name):
         """Get a specific quota by quota class."""
 
-        return self._driver.get_by_class(context, quota_class, resource)
+        return self._driver.get_by_class(context, quota_class, resource_name)
+
+    def get_default(self, context, resource):
+        """Get a specific default quota for a resource."""
+
+        return self._driver.get_default(context, resource)
 
     def get_defaults(self, context):
         """Retrieve the default quotas.
@@ -596,7 +673,7 @@ class QuotaEngine(object):
         :param context: The request context, for access checks.
         """
 
-        return self._driver.get_defaults(context, self._resources)
+        return self._driver.get_defaults(context, self.resources)
 
     def get_class_quotas(self, context, quota_class, defaults=True):
         """Retrieve the quotas for the given quota class.
@@ -609,7 +686,7 @@ class QuotaEngine(object):
                          resource.
         """
 
-        return self._driver.get_class_quotas(context, self._resources,
+        return self._driver.get_class_quotas(context, self.resources,
                                              quota_class, defaults=defaults)
 
     def get_project_quotas(self, context, project_id, quota_class=None,
@@ -629,7 +706,7 @@ class QuotaEngine(object):
                        will also be returned.
         """
 
-        return self._driver.get_project_quotas(context, self._resources,
+        return self._driver.get_project_quotas(context, self.resources,
                                                project_id,
                                                quota_class=quota_class,
                                                defaults=defaults,
@@ -648,7 +725,7 @@ class QuotaEngine(object):
         """
 
         # Get the resource
-        res = self._resources.get(resource)
+        res = self.resources.get(resource)
         if not res or not hasattr(res, 'count'):
             raise exception.QuotaResourceUnknown(unknown=[resource])
 
@@ -679,7 +756,7 @@ class QuotaEngine(object):
                            common user's tenant.
         """
 
-        return self._driver.limit_check(context, self._resources, values,
+        return self._driver.limit_check(context, self.resources, values,
                                         project_id=project_id)
 
     def reserve(self, context, expire=None, project_id=None, **deltas):
@@ -717,7 +794,7 @@ class QuotaEngine(object):
                            common user's tenant.
         """
 
-        reservations = self._driver.reserve(context, self._resources, deltas,
+        reservations = self._driver.reserve(context, self.resources, deltas,
                                             expire=expire,
                                             project_id=project_id)
 
@@ -788,9 +865,64 @@ class QuotaEngine(object):
 
         self._driver.expire(context)
 
+    def add_volume_type_opts(self, context, opts, volume_type_id):
+        """Add volume type resource options.
+
+        Adds elements to the opts hash for volume type quotas.
+        If a resource is being reserved ('gigabytes', etc) and the volume
+        type is set up for its own quotas, these reservations are copied
+        into keys for 'gigabytes_<volume type name>', etc.
+
+        :param context: The request context, for access checks.
+        :param opts: The reservations options hash.
+        :param volume_type_id: The volume type id for this reservation.
+        """
+        if not volume_type_id:
+            return
+        volume_type = db.volume_type_get(context, volume_type_id)
+        for quota in ('volumes', 'gigabytes', 'snapshots'):
+            if quota in opts:
+                vtype_quota = "%s_%s" % (quota, volume_type['name'])
+                opts[vtype_quota] = opts[quota]
+
+    @property
+    def resource_names(self):
+        return sorted(self.resources.keys())
+
+    @property
+    def resources(self):
+        return self._resources
+
+
+class VolumeTypeQuotaEngine(QuotaEngine):
+    """Represent the set of all quotas."""
+
     @property
     def resources(self):
-        return sorted(self._resources.keys())
+        """Fetches all possible quota resources."""
+
+        result = {}
+        # Global quotas.
+        argses = [('volumes', _sync_volumes, 'quota_volumes'),
+                  ('snapshots', _sync_snapshots, 'quota_snapshots'),
+                  ('gigabytes', _sync_gigabytes, 'quota_gigabytes'), ]
+        for args in argses:
+            resource = ReservableResource(*args)
+            result[resource.name] = resource
+
+        # Volume type quotas.
+        volume_types = db.volume_type_get_all(context.get_admin_context())
+        for volume_type in volume_types.values():
+            for part_name in ('volumes', 'gigabytes', 'snapshots'):
+                resource = VolumeTypeResource(part_name, volume_type)
+                result[resource.name] = resource
+        return result
+
+    def register_resource(self, resource):
+        raise NotImplementedError(_("Cannot register resource"))
+
+    def register_resources(self, resources):
+        raise NotImplementedError(_("Cannot register resources"))
 
 
 def _sync_volumes(context, project_id, session):
@@ -820,13 +952,4 @@ def _sync_gigabytes(context, project_id, session):
     return {'gigabytes': vol_gigs + snap_gigs}
 
 
-QUOTAS = QuotaEngine()
-
-
-resources = [
-    ReservableResource('volumes', _sync_volumes, 'quota_volumes'),
-    ReservableResource('snapshots', _sync_snapshots, 'quota_snapshots'),
-    ReservableResource('gigabytes', _sync_gigabytes, 'quota_gigabytes'), ]
-
-
-QUOTAS.register_resources(resources)
+QUOTAS = VolumeTypeQuotaEngine()
index 6a5e1653c9a493380c8936435158061a24b90863..58de0b398fec766e931093654e91b585a77b2da6 100644 (file)
@@ -41,13 +41,15 @@ class QuotaIntegrationTestCase(test.TestCase):
 
     def setUp(self):
         super(QuotaIntegrationTestCase, self).setUp()
+        self.volume_type_name = CONF.default_volume_type
+        self.volume_type = db.volume_type_create(
+            context.get_admin_context(),
+            dict(name=self.volume_type_name))
+
         self.flags(quota_volumes=2,
                    quota_snapshots=2,
                    quota_gigabytes=20)
 
-        # Apparently needed by the RPC tests...
-        #self.network = self.start_service('network')
-
         self.user_id = 'admin'
         self.project_id = 'admin'
         self.context = context.RequestContext(self.user_id,
@@ -61,16 +63,19 @@ class QuotaIntegrationTestCase(test.TestCase):
         self.stubs.Set(rpc, 'call', rpc_call_wrapper)
 
     def tearDown(self):
+        db.volume_type_destroy(context.get_admin_context(),
+                               self.volume_type['id'])
         super(QuotaIntegrationTestCase, self).tearDown()
         cinder.tests.image.fake.FakeImageService_reset()
 
-    def _create_volume(self, size=10):
+    def _create_volume(self, size=1):
         """Create a test volume."""
         vol = {}
         vol['user_id'] = self.user_id
         vol['project_id'] = self.project_id
         vol['size'] = size
         vol['status'] = 'available'
+        vol['volume_type_id'] = self.volume_type['id']
         return db.volume_create(self.context, vol)
 
     def _create_snapshot(self, volume):
@@ -87,19 +92,52 @@ class QuotaIntegrationTestCase(test.TestCase):
         for i in range(CONF.quota_volumes):
             vol_ref = self._create_volume()
             volume_ids.append(vol_ref['id'])
-        self.assertRaises(exception.QuotaError,
+        self.assertRaises(exception.VolumeLimitExceeded,
                           volume.API().create,
-                          self.context, 10, '', '', None)
+                          self.context, 1, '', '',
+                          volume_type=self.volume_type)
         for volume_id in volume_ids:
             db.volume_destroy(self.context, volume_id)
 
+    def test_too_many_volumes_of_type(self):
+        resource = 'volumes_%s' % self.volume_type_name
+        db.quota_class_create(self.context, 'default', resource, 1)
+        flag_args = {
+            'quota_volumes': 2000,
+            'quota_gigabytes': 2000
+        }
+        self.flags(**flag_args)
+        vol_ref = self._create_volume()
+        self.assertRaises(exception.VolumeLimitExceeded,
+                          volume.API().create,
+                          self.context, 1, '', '',
+                          volume_type=self.volume_type)
+        db.volume_destroy(self.context, vol_ref['id'])
+
+    def test_too_many_snapshots_of_type(self):
+        resource = 'snapshots_%s' % self.volume_type_name
+        db.quota_class_create(self.context, 'default', resource, 1)
+        flag_args = {
+            'quota_volumes': 2000,
+            'quota_gigabytes': 2000,
+        }
+        self.flags(**flag_args)
+        vol_ref = self._create_volume()
+        snap_ref = self._create_snapshot(vol_ref)
+        self.assertRaises(exception.SnapshotLimitExceeded,
+                          volume.API().create_snapshot,
+                          self.context, vol_ref, '', '')
+        db.snapshot_destroy(self.context, snap_ref['id'])
+        db.volume_destroy(self.context, vol_ref['id'])
+
     def test_too_many_gigabytes(self):
         volume_ids = []
         vol_ref = self._create_volume(size=20)
         volume_ids.append(vol_ref['id'])
-        self.assertRaises(exception.QuotaError,
+        self.assertRaises(exception.VolumeSizeExceedsAvailableQuota,
                           volume.API().create,
-                          self.context, 10, '', '', None)
+                          self.context, 1, '', '',
+                          volume_type=self.volume_type)
         for volume_id in volume_ids:
             db.volume_destroy(self.context, volume_id)
 
@@ -131,8 +169,6 @@ class QuotaIntegrationTestCase(test.TestCase):
         self.assertEqual(reservations.get('gigabytes'), None)
 
         # Make sure the snapshot volume_size isn't included in usage.
-        vol_type = db.volume_type_create(self.context,
-                                         dict(name=CONF.default_volume_type))
         vol_ref2 = volume.API().create(self.context, 10, '', '')
         usages = db.quota_usage_get_all_by_project(self.context,
                                                    self.project_id)
@@ -142,7 +178,21 @@ class QuotaIntegrationTestCase(test.TestCase):
         db.snapshot_destroy(self.context, snap_ref2['id'])
         db.volume_destroy(self.context, vol_ref['id'])
         db.volume_destroy(self.context, vol_ref2['id'])
-        db.volume_type_destroy(self.context, vol_type['id'])
+
+    def test_too_many_gigabytes_of_type(self):
+        resource = 'gigabytes_%s' % self.volume_type_name
+        db.quota_class_create(self.context, 'default', resource, 10)
+        flag_args = {
+            'quota_volumes': 2000,
+            'quota_gigabytes': 2000,
+        }
+        self.flags(**flag_args)
+        vol_ref = self._create_volume(size=10)
+        self.assertRaises(exception.VolumeSizeExceedsAvailableQuota,
+                          volume.API().create,
+                          self.context, 1, '', '',
+                          volume_type=self.volume_type)
+        db.volume_destroy(self.context, vol_ref['id'])
 
 
 class FakeContext(object):
@@ -179,6 +229,10 @@ class FakeDriver(object):
         except KeyError:
             raise exception.QuotaClassNotFound(class_name=quota_class)
 
+    def get_default(self, context, resource):
+        self.called.append(('get_default', context, resource))
+        return resource.default
+
     def get_defaults(self, context, resources):
         self.called.append(('get_defaults', context, resources))
         return resources
@@ -310,24 +364,35 @@ class BaseResourceTestCase(test.TestCase):
         self.assertEqual(quota_value, 20)
 
 
+class VolumeTypeResourceTestCase(test.TestCase):
+    def test_name_and_flag(self):
+        volume_type_name = 'foo'
+        volume = {'name': volume_type_name, 'id': 'myid'}
+        resource = quota.VolumeTypeResource('volumes', volume)
+
+        self.assertEqual(resource.name, 'volumes_%s' % volume_type_name)
+        self.assertEqual(resource.flag, None)
+        self.assertEqual(resource.default, -1)
+
+
 class QuotaEngineTestCase(test.TestCase):
     def test_init(self):
         quota_obj = quota.QuotaEngine()
 
-        self.assertEqual(quota_obj._resources, {})
+        self.assertEqual(quota_obj.resources, {})
         self.assertTrue(isinstance(quota_obj._driver, quota.DbQuotaDriver))
 
     def test_init_override_string(self):
         quota_obj = quota.QuotaEngine(
             quota_driver_class='cinder.tests.test_quota.FakeDriver')
 
-        self.assertEqual(quota_obj._resources, {})
+        self.assertEqual(quota_obj.resources, {})
         self.assertTrue(isinstance(quota_obj._driver, FakeDriver))
 
     def test_init_override_obj(self):
         quota_obj = quota.QuotaEngine(quota_driver_class=FakeDriver)
 
-        self.assertEqual(quota_obj._resources, {})
+        self.assertEqual(quota_obj.resources, {})
         self.assertEqual(quota_obj._driver, FakeDriver)
 
     def test_register_resource(self):
@@ -335,7 +400,7 @@ class QuotaEngineTestCase(test.TestCase):
         resource = quota.AbsoluteResource('test_resource')
         quota_obj.register_resource(resource)
 
-        self.assertEqual(quota_obj._resources, dict(test_resource=resource))
+        self.assertEqual(quota_obj.resources, dict(test_resource=resource))
 
     def test_register_resources(self):
         quota_obj = quota.QuotaEngine()
@@ -345,7 +410,7 @@ class QuotaEngineTestCase(test.TestCase):
             quota.AbsoluteResource('test_resource3'), ]
         quota_obj.register_resources(resources)
 
-        self.assertEqual(quota_obj._resources,
+        self.assertEqual(quota_obj.resources,
                          dict(test_resource1=resources[0],
                               test_resource2=resources[1],
                               test_resource3=resources[2], ))
@@ -428,8 +493,8 @@ class QuotaEngineTestCase(test.TestCase):
 
         self.assertEqual(driver.called, [('get_defaults',
                                           context,
-                                          quota_obj._resources), ])
-        self.assertEqual(result, quota_obj._resources)
+                                          quota_obj.resources), ])
+        self.assertEqual(result, quota_obj.resources)
 
     def test_get_class_quotas(self):
         context = FakeContext(None, None)
@@ -441,13 +506,13 @@ class QuotaEngineTestCase(test.TestCase):
         self.assertEqual(driver.called, [
             ('get_class_quotas',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              'test_class', True),
             ('get_class_quotas',
-             context, quota_obj._resources,
+             context, quota_obj.resources,
              'test_class', False), ])
-        self.assertEqual(result1, quota_obj._resources)
-        self.assertEqual(result2, quota_obj._resources)
+        self.assertEqual(result1, quota_obj.resources)
+        self.assertEqual(result2, quota_obj.resources)
 
     def test_get_project_quotas(self):
         context = FakeContext(None, None)
@@ -462,20 +527,20 @@ class QuotaEngineTestCase(test.TestCase):
         self.assertEqual(driver.called, [
             ('get_project_quotas',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              'test_project',
              None,
              True,
              True),
             ('get_project_quotas',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              'test_project',
              'test_class',
              False,
              False), ])
-        self.assertEqual(result1, quota_obj._resources)
-        self.assertEqual(result2, quota_obj._resources)
+        self.assertEqual(result1, quota_obj.resources)
+        self.assertEqual(result2, quota_obj.resources)
 
     def test_count_no_resource(self):
         context = FakeContext(None, None)
@@ -518,7 +583,7 @@ class QuotaEngineTestCase(test.TestCase):
         self.assertEqual(driver.called, [
             ('limit_check',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              dict(
                  test_resource1=4,
                  test_resource2=3,
@@ -546,7 +611,7 @@ class QuotaEngineTestCase(test.TestCase):
         self.assertEqual(driver.called, [
             ('reserve',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              dict(
                  test_resource1=4,
                  test_resource2=3,
@@ -556,7 +621,7 @@ class QuotaEngineTestCase(test.TestCase):
              None),
             ('reserve',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              dict(
                  test_resource1=1,
                  test_resource2=2,
@@ -566,7 +631,7 @@ class QuotaEngineTestCase(test.TestCase):
              None),
             ('reserve',
              context,
-             quota_obj._resources,
+             quota_obj.resources,
              dict(
                  test_resource1=1,
                  test_resource2=2,
@@ -634,14 +699,33 @@ class QuotaEngineTestCase(test.TestCase):
 
         self.assertEqual(driver.called, [('expire', context), ])
 
-    def test_resources(self):
+    def test_resource_names(self):
         quota_obj = self._make_quota_obj(None)
 
-        self.assertEqual(quota_obj.resources,
+        self.assertEqual(quota_obj.resource_names,
                          ['test_resource1', 'test_resource2',
                           'test_resource3', 'test_resource4'])
 
 
+class VolumeTypeQuotaEngineTestCase(test.TestCase):
+    def test_default_resources(self):
+        engine = quota.VolumeTypeQuotaEngine()
+        self.assertEqual(engine.resource_names,
+                         ['gigabytes', 'snapshots', 'volumes'])
+
+    def test_volume_type_resources(self):
+        ctx = context.RequestContext('admin', 'admin', is_admin=True)
+        vtype = db.volume_type_create(ctx, {'name': 'type1'})
+        vtype2 = db.volume_type_create(ctx, {'name': 'type_2'})
+        engine = quota.VolumeTypeQuotaEngine()
+        self.assertEqual(engine.resource_names,
+                         ['gigabytes', 'gigabytes_type1', 'gigabytes_type_2',
+                          'snapshots', 'snapshots_type1', 'snapshots_type_2',
+                          'volumes', 'volumes_type1', 'volumes_type_2'])
+        db.volume_type_destroy(ctx, vtype['id'])
+        db.volume_type_destroy(ctx, vtype2['id'])
+
+
 class DbQuotaDriverTestCase(test.TestCase):
     def setUp(self):
         super(DbQuotaDriverTestCase, self).setUp()
@@ -667,7 +751,7 @@ class DbQuotaDriverTestCase(test.TestCase):
     def test_get_defaults(self):
         # Use our pre-defined resources
         self._stub_quota_class_get_default()
-        result = self.driver.get_defaults(None, quota.QUOTAS._resources)
+        result = self.driver.get_defaults(None, quota.QUOTAS.resources)
 
         self.assertEqual(
             result,
@@ -695,7 +779,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_class_quotas(self):
         self._stub_quota_class_get_all_by_name()
-        result = self.driver.get_class_quotas(None, quota.QUOTAS._resources,
+        result = self.driver.get_class_quotas(None, quota.QUOTAS.resources,
                                               'test_class')
 
         self.assertEqual(self.calls, ['quota_class_get_all_by_name'])
@@ -705,7 +789,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_class_quotas_no_defaults(self):
         self._stub_quota_class_get_all_by_name()
-        result = self.driver.get_class_quotas(None, quota.QUOTAS._resources,
+        result = self.driver.get_class_quotas(None, quota.QUOTAS.resources,
                                               'test_class', False)
 
         self.assertEqual(self.calls, ['quota_class_get_all_by_name'])
@@ -736,7 +820,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_by_project()
         result = self.driver.get_project_quotas(
             FakeContext('test_project', 'test_class'),
-            quota.QUOTAS._resources, 'test_project')
+            quota.QUOTAS.resources, 'test_project')
 
         self.assertEqual(self.calls, ['quota_get_all_by_project',
                                       'quota_usage_get_all_by_project',
@@ -756,7 +840,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_by_project()
         result = self.driver.get_project_quotas(
             FakeContext('other_project', 'other_class'),
-            quota.QUOTAS._resources, 'test_project')
+            quota.QUOTAS.resources, 'test_project')
 
         self.assertEqual(self.calls, ['quota_get_all_by_project',
                                       'quota_usage_get_all_by_project',
@@ -775,7 +859,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_by_project()
         result = self.driver.get_project_quotas(
             FakeContext('other_project', 'other_class'),
-            quota.QUOTAS._resources, 'test_project', quota_class='test_class')
+            quota.QUOTAS.resources, 'test_project', quota_class='test_class')
 
         self.assertEqual(self.calls, ['quota_get_all_by_project',
                                       'quota_usage_get_all_by_project',
@@ -795,7 +879,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_by_project()
         result = self.driver.get_project_quotas(
             FakeContext('test_project', 'test_class'),
-            quota.QUOTAS._resources, 'test_project', defaults=False)
+            quota.QUOTAS.resources, 'test_project', defaults=False)
 
         self.assertEqual(self.calls, ['quota_get_all_by_project',
                                       'quota_usage_get_all_by_project',
@@ -816,7 +900,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_by_project()
         result = self.driver.get_project_quotas(
             FakeContext('test_project', 'test_class'),
-            quota.QUOTAS._resources, 'test_project', usages=False)
+            quota.QUOTAS.resources, 'test_project', usages=False)
 
         self.assertEqual(self.calls, ['quota_get_all_by_project',
                                       'quota_class_get_all_by_name',
@@ -840,7 +924,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         self.assertRaises(exception.QuotaResourceUnknown,
                           self.driver._get_quotas,
-                          None, quota.QUOTAS._resources,
+                          None, quota.QUOTAS.resources,
                           ['unknown'], True)
         self.assertEqual(self.calls, [])
 
@@ -848,7 +932,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         self.assertRaises(exception.QuotaResourceUnknown,
                           self.driver._get_quotas,
-                          None, quota.QUOTAS._resources,
+                          None, quota.QUOTAS.resources,
                           ['unknown'], False)
         self.assertEqual(self.calls, [])
 
@@ -856,7 +940,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         self.assertRaises(exception.QuotaResourceUnknown,
                           self.driver._get_quotas,
-                          None, quota.QUOTAS._resources,
+                          None, quota.QUOTAS.resources,
                           ['metadata_items'], True)
         self.assertEqual(self.calls, [])
 
@@ -864,7 +948,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         self.assertRaises(exception.QuotaResourceUnknown,
                           self.driver._get_quotas,
-                          None, quota.QUOTAS._resources,
+                          None, quota.QUOTAS.resources,
                           ['volumes'], False)
         self.assertEqual(self.calls, [])
 
@@ -872,7 +956,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         result = self.driver._get_quotas(FakeContext('test_project',
                                                      'test_class'),
-                                         quota.QUOTAS._resources,
+                                         quota.QUOTAS.resources,
                                          ['volumes', 'gigabytes'],
                                          True)
 
@@ -893,7 +977,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self.assertRaises(exception.InvalidReservationExpiration,
                           self.driver.reserve,
                           FakeContext('test_project', 'test_class'),
-                          quota.QUOTAS._resources,
+                          quota.QUOTAS.resources,
                           dict(volumes=2), expire='invalid')
         self.assertEqual(self.calls, [])
 
@@ -901,7 +985,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         self._stub_quota_reserve()
         result = self.driver.reserve(FakeContext('test_project', 'test_class'),
-                                     quota.QUOTAS._resources,
+                                     quota.QUOTAS.resources,
                                      dict(volumes=2))
 
         expire = timeutils.utcnow() + datetime.timedelta(seconds=86400)
@@ -913,7 +997,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_get_project_quotas()
         self._stub_quota_reserve()
         result = self.driver.reserve(FakeContext('test_project', 'test_class'),
-                                     quota.QUOTAS._resources,
+                                     quota.QUOTAS.resources,
                                      dict(volumes=2), expire=3600)
 
         expire = timeutils.utcnow() + datetime.timedelta(seconds=3600)
@@ -926,7 +1010,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_quota_reserve()
         expire_delta = datetime.timedelta(seconds=60)
         result = self.driver.reserve(FakeContext('test_project', 'test_class'),
-                                     quota.QUOTAS._resources,
+                                     quota.QUOTAS.resources,
                                      dict(volumes=2), expire=expire_delta)
 
         expire = timeutils.utcnow() + expire_delta
@@ -939,7 +1023,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self._stub_quota_reserve()
         expire = timeutils.utcnow() + datetime.timedelta(seconds=120)
         result = self.driver.reserve(FakeContext('test_project', 'test_class'),
-                                     quota.QUOTAS._resources,
+                                     quota.QUOTAS.resources,
                                      dict(volumes=2), expire=expire)
 
         self.assertEqual(self.calls, ['get_project_quotas',
@@ -952,7 +1036,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self.flags(until_refresh=500)
         expire = timeutils.utcnow() + datetime.timedelta(seconds=120)
         result = self.driver.reserve(FakeContext('test_project', 'test_class'),
-                                     quota.QUOTAS._resources,
+                                     quota.QUOTAS.resources,
                                      dict(volumes=2), expire=expire)
 
         self.assertEqual(self.calls, ['get_project_quotas',
@@ -965,7 +1049,7 @@ class DbQuotaDriverTestCase(test.TestCase):
         self.flags(max_age=86400)
         expire = timeutils.utcnow() + datetime.timedelta(seconds=120)
         result = self.driver.reserve(FakeContext('test_project', 'test_class'),
-                                     quota.QUOTAS._resources,
+                                     quota.QUOTAS.resources,
                                      dict(volumes=2), expire=expire)
 
         self.assertEqual(self.calls, ['get_project_quotas',
index 7d1510e4088c9e408dd2f75059c9b7aa1927c9f2..aded3ea99e2194b650da59b0f7fa32675d924873 100644 (file)
@@ -159,8 +159,18 @@ class API(base.Base):
                 msg = _('Image minDisk size is larger than the volume size.')
                 raise exception.InvalidInput(reason=msg)
 
+        if not volume_type and not source_volume:
+            volume_type = volume_types.get_default_volume_type()
+
+        if not volume_type and source_volume:
+            volume_type_id = source_volume['volume_type_id']
+        else:
+            volume_type_id = volume_type.get('id')
+
         try:
-            reservations = QUOTAS.reserve(context, volumes=1, gigabytes=size)
+            reserve_opts = {'volumes': 1, 'gigabytes': size}
+            QUOTAS.add_volume_type_opts(context, reserve_opts, volume_type_id)
+            reservations = QUOTAS.reserve(context, **reserve_opts)
         except exception.OverQuota as e:
             overs = e.kwargs['overs']
             usages = e.kwargs['usages']
@@ -169,36 +179,29 @@ class API(base.Base):
             def _consumed(name):
                 return (usages[name]['reserved'] + usages[name]['in_use'])
 
-            if 'gigabytes' in overs:
-                msg = _("Quota exceeded for %(s_pid)s, tried to create "
-                        "%(s_size)sG volume (%(d_consumed)dG of %(d_quota)dG "
-                        "already consumed)")
-                LOG.warn(msg % {'s_pid': context.project_id,
-                                's_size': size,
-                                'd_consumed': _consumed('gigabytes'),
-                                'd_quota': quotas['gigabytes']})
-                raise exception.VolumeSizeExceedsAvailableQuota()
-            elif 'volumes' in overs:
-                msg = _("Quota exceeded for %(s_pid)s, tried to create "
-                        "volume (%(d_consumed)d volumes "
-                        "already consumed)")
-                LOG.warn(msg % {'s_pid': context.project_id,
-                                'd_consumed': _consumed('volumes')})
-                raise exception.VolumeLimitExceeded(allowed=quotas['volumes'])
+            for over in overs:
+                if 'gigabytes' in over:
+                    msg = _("Quota exceeded for %(s_pid)s, tried to create "
+                            "%(s_size)sG volume (%(d_consumed)dG of "
+                            "%(d_quota)dG already consumed)")
+                    LOG.warn(msg % {'s_pid': context.project_id,
+                                    's_size': size,
+                                    'd_consumed': _consumed(over),
+                                    'd_quota': quotas[over]})
+                    raise exception.VolumeSizeExceedsAvailableQuota()
+                elif 'volumes' in over:
+                    msg = _("Quota exceeded for %(s_pid)s, tried to create "
+                            "volume (%(d_consumed)d volumes"
+                            "already consumed)")
+                    LOG.warn(msg % {'s_pid': context.project_id,
+                                    'd_consumed': _consumed(over)})
+                    raise exception.VolumeLimitExceeded(allowed=quotas[over])
 
         if availability_zone is None:
             availability_zone = CONF.storage_availability_zone
         else:
             self._check_availabilty_zone(availability_zone)
 
-        if not volume_type and not source_volume:
-            volume_type = volume_types.get_default_volume_type()
-
-        if not volume_type and source_volume:
-            volume_type_id = source_volume['volume_type_id']
-        else:
-            volume_type_id = volume_type.get('id')
-
         self._check_metadata_properties(context, metadata)
         options = {'size': size,
                    'user_id': context.user_id,
@@ -328,10 +331,13 @@ class API(base.Base):
             # NOTE(vish): scheduling failed, so delete it
             # Note(zhiteng): update volume quota reservation
             try:
+                reserve_opts = {'volumes': -1, 'gigabytes': -volume['size']}
+                QUOTAS.add_volume_type_opts(context,
+                                            reserve_opts,
+                                            volume['volume_type_id'])
                 reservations = QUOTAS.reserve(context,
                                               project_id=project_id,
-                                              volumes=-1,
-                                              gigabytes=-volume['size'])
+                                              **reserve_opts)
             except Exception:
                 reservations = None
                 LOG.exception(_("Failed to update quota for deleting volume"))
@@ -559,10 +565,13 @@ class API(base.Base):
 
         try:
             if CONF.no_snapshot_gb_quota:
-                reservations = QUOTAS.reserve(context, snapshots=1)
+                reserve_opts = {'snapshots': 1}
             else:
-                reservations = QUOTAS.reserve(context, snapshots=1,
-                                              gigabytes=volume['size'])
+                reserve_opts = {'snapshots': 1, 'gigabytes': volume['size']}
+            QUOTAS.add_volume_type_opts(context,
+                                        reserve_opts,
+                                        volume.get('volume_type_id'))
+            reservations = QUOTAS.reserve(context, **reserve_opts)
         except exception.OverQuota as e:
             overs = e.kwargs['overs']
             usages = e.kwargs['usages']
@@ -571,24 +580,25 @@ class API(base.Base):
             def _consumed(name):
                 return (usages[name]['reserved'] + usages[name]['in_use'])
 
-            if 'gigabytes' in overs:
-                msg = _("Quota exceeded for %(s_pid)s, tried to create "
-                        "%(s_size)sG snapshot (%(d_consumed)dG of "
-                        "%(d_quota)dG already consumed)")
-                LOG.warn(msg % {'s_pid': context.project_id,
-                                's_size': volume['size'],
-                                'd_consumed': _consumed('gigabytes'),
-                                'd_quota': quotas['gigabytes']})
-                raise exception.VolumeSizeExceedsAvailableQuota()
-            elif 'snapshots' in overs:
-                msg = _("Quota exceeded for %(s_pid)s, tried to create "
-                        "snapshot (%(d_consumed)d snapshots "
-                        "already consumed)")
-
-                LOG.warn(msg % {'s_pid': context.project_id,
-                                'd_consumed': _consumed('snapshots')})
-                raise exception.SnapshotLimitExceeded(
-                    allowed=quotas['snapshots'])
+            for over in overs:
+                if 'gigabytes' in over:
+                    msg = _("Quota exceeded for %(s_pid)s, tried to create "
+                            "%(s_size)sG snapshot (%(d_consumed)dG of "
+                            "%(d_quota)dG already consumed)")
+                    LOG.warn(msg % {'s_pid': context.project_id,
+                                    's_size': volume['size'],
+                                    'd_consumed': _consumed(over),
+                                    'd_quota': quotas[over]})
+                    raise exception.VolumeSizeExceedsAvailableQuota()
+                elif 'snapshots' in over:
+                    msg = _("Quota exceeded for %(s_pid)s, tried to create "
+                            "snapshot (%(d_consumed)d snapshots "
+                            "already consumed)")
+
+                    LOG.warn(msg % {'s_pid': context.project_id,
+                                    'd_consumed': _consumed(over)})
+                    raise exception.SnapshotLimitExceeded(
+                        allowed=quotas[over])
 
         self._check_metadata_properties(context, metadata)
         options = {'volume_id': volume['id'],
index 4f4b3d8ad4e14e1f0f23e208efa37d8d2e5f6aad..fc5a63bf53cce4a68a02746a4211e3eebb76f2c4 100644 (file)
@@ -444,10 +444,13 @@ class VolumeManager(manager.SchedulerDependentManager):
 
         # Get reservations
         try:
+            reserve_opts = {'volumes': -1, 'gigabytes': -volume_ref['size']}
+            QUOTAS.add_volume_type_opts(context,
+                                        reserve_opts,
+                                        volume_ref.get('volume_type_id'))
             reservations = QUOTAS.reserve(context,
                                           project_id=project_id,
-                                          volumes=-1,
-                                          gigabytes=-volume_ref['size'])
+                                          **reserve_opts)
         except Exception:
             reservations = None
             LOG.exception(_("Failed to update usages deleting volume"))
@@ -501,15 +504,11 @@ class VolumeManager(manager.SchedulerDependentManager):
         """Deletes and unexports snapshot."""
         context = context.elevated()
         snapshot_ref = self.db.snapshot_get(context, snapshot_id)
+        project_id = snapshot_ref['project_id']
         LOG.info(_("snapshot %s: deleting"), snapshot_ref['name'])
         self._notify_about_snapshot_usage(
             context, snapshot_ref, "delete.start")
 
-        if context.project_id != snapshot_ref['project_id']:
-            project_id = snapshot_ref['project_id']
-        else:
-            project_id = context.project_id
-
         try:
             LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name'])
             self.driver.delete_snapshot(snapshot_ref)
@@ -529,15 +528,19 @@ class VolumeManager(manager.SchedulerDependentManager):
         # Get reservations
         try:
             if CONF.no_snapshot_gb_quota:
-                reservations = QUOTAS.reserve(context,
-                                              project_id=project_id,
-                                              snapshots=-1)
+                reserve_opts = {'snapshots': -1}
             else:
-                reservations = QUOTAS.reserve(
-                    context,
-                    project_id=project_id,
-                    snapshots=-1,
-                    gigabytes=-snapshot_ref['volume_size'])
+                reserve_opts = {
+                    'snapshots': -1,
+                    'gigabytes': -snapshot_ref['volume_size'],
+                }
+            volume_ref = self.db.volume_get(context, snapshot_ref['volume_id'])
+            QUOTAS.add_volume_type_opts(context,
+                                        reserve_opts,
+                                        volume_ref.get('volume_type_id'))
+            reservations = QUOTAS.reserve(context,
+                                          project_id=project_id,
+                                          **reserve_opts)
         except Exception:
             reservations = None
             LOG.exception(_("Failed to update usages deleting snapshot"))