From: Cory Stone Date: Wed, 22 May 2013 15:31:13 +0000 (-0500) Subject: Quotas by Volume Type X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=afa27908c5ef558ae8822c89a6f95d8150fc6b47;p=openstack-build%2Fcinder-build.git Quotas by Volume Type 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_, gigabytes_, and snapshots_. Another patch will have changes to cinderclient for setting project quotas by volume type. blueprint quotas-limits-by-voltype DocImpact Change-Id: I88261676edcd6eb5b7cea40654a931f32c00815c --- diff --git a/cinder/db/api.py b/cinder/db/api.py index 15e4a3f78..dbf3230c6 100644 --- a/cinder/db/api.py +++ b/cinder/db/api.py @@ -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) diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index c5735fc85..af6da757a 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -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) diff --git a/cinder/quota.py b/cinder/quota.py index fdf73138c..16f77a510 100644 --- a/cinder/quota.py +++ b/cinder/quota.py @@ -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_', 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() diff --git a/cinder/tests/test_quota.py b/cinder/tests/test_quota.py index 6a5e1653c..58de0b398 100644 --- a/cinder/tests/test_quota.py +++ b/cinder/tests/test_quota.py @@ -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', diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 7d1510e40..aded3ea99 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -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'], diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 4f4b3d8ad..fc5a63bf5 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -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"))