Handle quota exceed exception

This patch solves two problems:

1. Code duplication about handling quota exceed exceptions.

2. When extending a volume, if OverQuota is caught, Cinder
raised VolumeSizeExceedsAvailableQuota with data from
quota gigabytes. This is not correct, as it may exceed
gigabytes of volume type.

This patch is to transfer quota data from gigabytes or
[volume_type_name]gigabytes to VolumeSizeExceedsAvailableQuota.

3. Raise new exception if overs are not expected.

Change-Id: I811ce51a4aefe8a99f0f5197ab071212322c584d
Closes-bug: 1546890
This commit is contained in:
lisali
2016-05-31 16:14:50 +08:00
parent 82c352649b
commit 87c5749a3f
11 changed files with 233 additions and 235 deletions

View File

@@ -33,11 +33,12 @@ from cinder.backup import rpcapi as backup_rpcapi
from cinder import context
from cinder.db import base
from cinder import exception
from cinder.i18n import _, _LI, _LW
from cinder.i18n import _, _LI
from cinder import objects
from cinder.objects import fields
import cinder.policy
from cinder import quota
from cinder import quota_utils
from cinder import utils
import cinder.volume
from cinder.volume import utils as volume_utils
@@ -279,37 +280,10 @@ class API(base.Base):
'backup_gigabytes': volume['size']}
reservations = QUOTAS.reserve(context, **reserve_opts)
except exception.OverQuota as e:
overs = e.kwargs['overs']
usages = e.kwargs['usages']
quotas = e.kwargs['quotas']
def _consumed(resource_name):
return (usages[resource_name]['reserved'] +
usages[resource_name]['in_use'])
for over in overs:
if 'gigabytes' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_size)sG backup (%(d_consumed)dG of "
"%(d_quota)dG already consumed)")
LOG.warning(msg, {'s_pid': context.project_id,
's_size': volume['size'],
'd_consumed': _consumed(over),
'd_quota': quotas[over]})
raise exception.VolumeBackupSizeExceedsAvailableQuota(
requested=volume['size'],
consumed=_consumed('backup_gigabytes'),
quota=quotas['backup_gigabytes'])
elif 'backups' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"backups (%(d_consumed)d backups "
"already consumed)")
LOG.warning(msg, {'s_pid': context.project_id,
'd_consumed': _consumed(over)})
raise exception.BackupLimitExceeded(
allowed=quotas[over])
quota_utils.process_reserve_over_quota(
context, e,
resource='backups',
size=volume.size)
# Find the latest backup and use it as the parent backup to do an
# incremental backup.
latest_backup = None

View File

@@ -540,6 +540,10 @@ class SnapshotLimitExceeded(QuotaError):
message = _("Maximum number of snapshots allowed (%(allowed)d) exceeded")
class UnexpectedOverQuota(QuotaError):
message = _("Unexpected over quota on %(name)s.")
class BackupLimitExceeded(QuotaError):
message = _("Maximum number of backups allowed (%(allowed)d) exceeded")

View File

@@ -70,37 +70,9 @@ def get_volume_type_reservation(ctxt, volume, type_id,
project_id=project_id,
**reserve_opts)
except exception.OverQuota as e:
overs = e.kwargs['overs']
usages = e.kwargs['usages']
quotas = e.kwargs['quotas']
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
for over in overs:
if 'gigabytes' in over:
s_size = volume['size']
d_quota = quotas[over]
d_consumed = _consumed(over)
LOG.warning(
_LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_size)sG volume - (%(d_consumed)dG of "
"%(d_quota)dG already consumed)"),
{'s_pid': ctxt.project_id,
's_size': s_size,
'd_consumed': d_consumed,
'd_quota': d_quota})
raise exception.VolumeSizeExceedsAvailableQuota(
requested=s_size, quota=d_quota, consumed=d_consumed)
elif 'volumes' in over:
LOG.warning(
_LW("Quota exceeded for %(s_pid)s, tried to create "
"volume (%(d_consumed)d volumes "
"already consumed)"),
{'s_pid': ctxt.project_id,
'd_consumed': _consumed(over)})
raise exception.VolumeLimitExceeded(
allowed=quotas[over])
process_reserve_over_quota(ctxt, e,
resource='volumes',
size=volume.size)
return reservations
@@ -261,3 +233,65 @@ def _keystone_client(context, version=(3, 0)):
(CONF.keystone_authtoken.cafile or True))
return client.Client(auth_url=CONF.keystone_authtoken.auth_uri,
session=client_session, version=version)
OVER_QUOTA_RESOURCE_EXCEPTIONS = {'snapshots': exception.SnapshotLimitExceeded,
'backups': exception.BackupLimitExceeded,
'volumes': exception.VolumeLimitExceeded, }
def process_reserve_over_quota(context, over_quota_exception,
resource, size=None):
"""Handle OverQuota exception.
Analyze OverQuota exception, and raise new exception related to
resource type. If there are unexpected items in overs,
UnexpectedOverQuota is raised.
:param context: security context
:param over_quota_exception: OverQuota exception
:param resource: can be backups, snapshots, and volumes
:param size: requested size in reservation
"""
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
overs = over_quota_exception.kwargs['overs']
usages = over_quota_exception.kwargs['usages']
quotas = over_quota_exception.kwargs['quotas']
invalid_overs = []
for over in overs:
if 'gigabytes' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_size)dG %(s_resource)s (%(d_consumed)dG of "
"%(d_quota)dG already consumed).")
LOG.warning(msg, {'s_pid': context.project_id,
's_size': size,
's_resource': resource[:-1],
'd_consumed': _consumed(over),
'd_quota': quotas[over]})
if resource == 'backups':
exc = exception.VolumeBackupSizeExceedsAvailableQuota
else:
exc = exception.VolumeSizeExceedsAvailableQuota
raise exc(
name=over,
requested=size,
consumed=_consumed(over),
quota=quotas[over])
if (resource in OVER_QUOTA_RESOURCE_EXCEPTIONS.keys() and
resource in over):
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_resource)s (%(d_consumed)d %(s_resource)ss "
"already consumed).")
LOG.warning(msg, {'s_pid': context.project_id,
'd_consumed': _consumed(over),
's_resource': resource[:-1]})
raise OVER_QUOTA_RESOURCE_EXCEPTIONS[resource](
allowed=quotas[over],
name=over)
invalid_overs.append(over)
if invalid_overs:
raise exception.UnexpectedOverQuota(name=', '.join(invalid_overs))

View File

@@ -165,3 +165,80 @@ class QuotaUtilsTest(test.TestCase):
self.assertRaises(exception.CinderException,
quota_utils.validate_setup_for_nested_quota_use,
self.context, [], None)
def _process_reserve_over_quota(self, overs, usages, quotas,
expected_ex,
resource='volumes'):
ctxt = context.get_admin_context()
ctxt.project_id = 'fake'
size = 1
kwargs = {'overs': overs,
'usages': usages,
'quotas': quotas}
exc = exception.OverQuota(**kwargs)
self.assertRaises(expected_ex,
quota_utils.process_reserve_over_quota,
ctxt, exc,
resource=resource,
size=size)
def test_volume_size_exceed_quota(self):
overs = ['gigabytes']
usages = {'gigabytes': {'reserved': 1, 'in_use': 9}}
quotas = {'gigabytes': 10, 'snapshots': 10}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.VolumeSizeExceedsAvailableQuota)
def test_snapshot_limit_exceed_quota(self):
overs = ['snapshots']
usages = {'snapshots': {'reserved': 1, 'in_use': 9}}
quotas = {'gigabytes': 10, 'snapshots': 10}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.SnapshotLimitExceeded,
resource='snapshots')
def test_backup_gigabytes_exceed_quota(self):
overs = ['backup_gigabytes']
usages = {'backup_gigabytes': {'reserved': 1, 'in_use': 9}}
quotas = {'backup_gigabytes': 10}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.VolumeBackupSizeExceedsAvailableQuota,
resource='backups')
def test_backup_limit_quota(self):
overs = ['backups']
usages = {'backups': {'reserved': 1, 'in_use': 9}}
quotas = {'backups': 9}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.BackupLimitExceeded,
resource='backups')
def test_volumes_limit_quota(self):
overs = ['volumes']
usages = {'volumes': {'reserved': 1, 'in_use': 9}}
quotas = {'volumes': 9}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.VolumeLimitExceeded)
def test_unknown_quota(self):
overs = ['unknown']
usages = {'volumes': {'reserved': 1, 'in_use': 9}}
quotas = {'volumes': 9}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.UnexpectedOverQuota)
def test_unknown_quota2(self):
overs = ['volumes']
usages = {'volumes': {'reserved': 1, 'in_use': 9}}
quotas = {'volumes': 9}
self._process_reserve_over_quota(
overs, usages, quotas,
exception.UnexpectedOverQuota,
resource='snapshots')

View File

@@ -88,6 +88,12 @@ fake_opt = [
]
OVER_SNAPSHOT_QUOTA_EXCEPTION = exception.OverQuota(
overs=['snapshots'],
usages = {'snapshots': {'reserved': 1, 'in_use': 9}},
quotas = {'gigabytes': 10, 'snapshots': 10})
def create_snapshot(volume_id, size=1, metadata=None, ctxt=None,
**kwargs):
"""Create a snapshot object."""
@@ -3169,10 +3175,8 @@ class VolumeTestCase(BaseVolumeTestCase):
"""Test exception handling when create snapshot in db failed."""
test_volume = tests_utils.create_volume(
self.context,
**self.volume_params)
self.volume.create_volume(self.context, test_volume.id,
volume=test_volume)
test_volume['status'] = 'available'
status='available',
host=CONF.host)
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidSnapshot,
volume_api.create_snapshot,
@@ -3185,10 +3189,8 @@ class VolumeTestCase(BaseVolumeTestCase):
"""Test exception handling when create snapshot in maintenance."""
test_volume = tests_utils.create_volume(
self.context,
**self.volume_params)
self.volume.create_volume(self.context, test_volume.id,
volume=test_volume)
test_volume['status'] = 'maintenance'
status='maintenance',
host=CONF.host)
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.create_snapshot,
@@ -3204,10 +3206,8 @@ class VolumeTestCase(BaseVolumeTestCase):
"""Test exception handling when snapshot quota commit failed."""
test_volume = tests_utils.create_volume(
self.context,
**self.volume_params)
self.volume.create_volume(self.context, test_volume.id,
request_spec={}, volume=test_volume)
test_volume['status'] = 'available'
status='available',
host=CONF.host)
volume_api = cinder.volume.api.API()
self.assertRaises(exception.QuotaError,
volume_api.create_snapshot,
@@ -3216,6 +3216,40 @@ class VolumeTestCase(BaseVolumeTestCase):
'fake_name',
'fake_description')
@mock.patch.object(QUOTAS, 'reserve',
side_effect = OVER_SNAPSHOT_QUOTA_EXCEPTION)
def test_create_snapshot_failed_quota_reserve(self, mock_reserve):
"""Test exception handling when snapshot quota reserve failed."""
test_volume = tests_utils.create_volume(
self.context,
status='available',
host=CONF.host)
volume_api = cinder.volume.api.API()
self.assertRaises(exception.SnapshotLimitExceeded,
volume_api.create_snapshot,
self.context,
test_volume,
'fake_name',
'fake_description')
@mock.patch.object(QUOTAS, 'reserve',
side_effect = OVER_SNAPSHOT_QUOTA_EXCEPTION)
def test_create_snapshots_in_db_failed_quota_reserve(self, mock_reserve):
"""Test exception handling when snapshot quota reserve failed."""
test_volume = tests_utils.create_volume(
self.context,
status='available',
host=CONF.host)
volume_api = cinder.volume.api.API()
self.assertRaises(exception.SnapshotLimitExceeded,
volume_api.create_snapshots_in_db,
self.context,
[test_volume],
'fake_name',
'fake_description',
False,
fake.CONSISTENCY_GROUP_ID)
def test_cannot_delete_volume_in_use(self):
"""Test volume can't be deleted in in-use status."""
self._test_cannot_delete_volume('in-use')
@@ -4589,6 +4623,20 @@ class VolumeTestCase(BaseVolumeTestCase):
'is_snapshot': False}
self.assertEqual(expected_result, result)
@mock.patch.object(QUOTAS, 'reserve',
side_effect = OVER_SNAPSHOT_QUOTA_EXCEPTION)
def test_existing_snapshot_failed_quota_reserve(self, mock_reserve):
vol = tests_utils.create_volume(self.context)
snap = tests_utils.create_snapshot(self.context, vol.id)
with mock.patch.object(
self.volume.driver,
'manage_existing_snapshot_get_size') as mock_get_size:
mock_get_size.return_value = 1
self.assertRaises(exception.SnapshotLimitExceeded,
self.volume.manage_existing_snapshot,
self.context,
snap)
@ddt.ddt
class VolumeMigrationTestCase(BaseVolumeTestCase):

View File

@@ -816,20 +816,3 @@ class VolumeUtilsTestCase(test.TestCase):
self.assertEqual(
expected_dict,
volume_utils.convert_config_string_to_dict(test_string))
def test_process_reserve_over_quota(self):
ctxt = context.get_admin_context()
ctxt.project_id = 'fake'
overs_one = ['gigabytes']
over_two = ['snapshots']
usages = {'gigabytes': {'reserved': 1, 'in_use': 9},
'snapshots': {'reserved': 1, 'in_use': 9}}
quotas = {'gigabytes': 10, 'snapshots': 10}
size = 1
self.assertRaises(exception.VolumeSizeExceedsAvailableQuota,
volume_utils.process_reserve_over_quota,
ctxt, overs_one, usages, quotas, size)
self.assertRaises(exception.SnapshotLimitExceeded,
volume_utils.process_reserve_over_quota,
ctxt, over_two, usages, quotas, size)

View File

@@ -29,8 +29,9 @@ import six
from cinder.db import base
from cinder import exception
from cinder.i18n import _, _LE, _LI, _LW
from cinder.i18n import _, _LE, _LI
from cinder import quota
from cinder import quota_utils
from cinder.volume import api as volume_api
from cinder.volume import utils as volume_utils
@@ -178,35 +179,9 @@ class API(base.Base):
vol_ref.volume_type_id)
reservations = QUOTAS.reserve(context, **reserve_opts)
except exception.OverQuota as e:
overs = e.kwargs['overs']
usages = e.kwargs['usages']
quotas = e.kwargs['quotas']
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
for over in overs:
if 'gigabytes' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_size)sG volume (%(d_consumed)dG of "
"%(d_quota)dG already consumed)")
LOG.warning(msg, {'s_pid': context.project_id,
's_size': vol_ref['size'],
'd_consumed': _consumed(over),
'd_quota': quotas[over]})
raise exception.VolumeSizeExceedsAvailableQuota(
requested=vol_ref['size'],
consumed=_consumed(over),
quota=quotas[over])
elif 'volumes' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"volume (%(d_consumed)d volumes "
"already consumed)")
LOG.warning(msg, {'s_pid': context.project_id,
'd_consumed': _consumed(over)})
raise exception.VolumeLimitExceeded(allowed=quotas[over],
name=over)
quota_utils.process_reserve_over_quota(context, e,
resource='volumes',
size=vol_ref.size)
try:
donor_id = vol_ref['project_id']
reserve_opts = {'volumes': -1, 'gigabytes': -vol_ref.size}

View File

@@ -750,36 +750,10 @@ class API(base.Base):
volume.get('volume_type_id'))
reservations = QUOTAS.reserve(context, **reserve_opts)
except exception.OverQuota as e:
overs = e.kwargs['overs']
usages = e.kwargs['usages']
quotas = e.kwargs['quotas']
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
for over in overs:
if 'gigabytes' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to "
"create %(s_size)sG snapshot (%(d_consumed)d"
"G of %(d_quota)dG already consumed).")
LOG.warning(msg, {'s_pid': context.project_id,
's_size': volume['size'],
'd_consumed': _consumed(over),
'd_quota': quotas[over]})
raise exception.VolumeSizeExceedsAvailableQuota(
requested=volume['size'],
consumed=_consumed('gigabytes'),
quota=quotas['gigabytes'])
elif 'snapshots' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to "
"create snapshot (%(d_consumed)d snapshots "
"already consumed).")
LOG.warning(msg, {'s_pid': context.project_id,
'd_consumed': _consumed(over)})
raise exception.SnapshotLimitExceeded(
allowed=quotas[over])
quota_utils.process_reserve_over_quota(
context, e,
resource='snapshots',
size=volume.size)
self._check_metadata_properties(metadata)
snapshot = None
@@ -893,11 +867,9 @@ class API(base.Base):
total_reserve_opts[key] + value
reservations = QUOTAS.reserve(context, **total_reserve_opts)
except exception.OverQuota as e:
overs = e.kwargs['overs']
usages = e.kwargs['usages']
quotas = e.kwargs['quotas']
volume_utils.process_reserve_over_quota(context, overs, usages,
quotas, volume['size'])
quota_utils.process_reserve_over_quota(context, e,
resource='snapshots',
size=volume.size)
return reservations

View File

@@ -26,6 +26,7 @@ from cinder import objects
from cinder.objects import fields
from cinder import policy
from cinder import quota
from cinder import quota_utils
from cinder import utils
from cinder.volume.flows import common
from cinder.volume import utils as vol_utils
@@ -601,53 +602,9 @@ class QuotaReserveTask(flow_utils.CinderTask):
'reservations': reservations,
}
except exception.OverQuota as e:
overs = e.kwargs['overs']
quotas = e.kwargs['quotas']
usages = e.kwargs['usages']
def _consumed(name):
usage = usages[name]
return usage['reserved'] + usage['in_use'] + usage.get(
'allocated', 0)
def _get_over(name):
for over in overs:
if name in over:
return over
return None
over_name = _get_over('gigabytes')
exceeded_vol_limit_name = _get_over('volumes')
if over_name:
# TODO(mc_nair): improve error message for child -1 limit
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_size)sG volume (%(d_consumed)dG "
"of %(d_quota)dG already consumed)")
LOG.warning(msg, {'s_pid': context.project_id,
's_size': size,
'd_consumed': _consumed(over_name),
'd_quota': quotas[over_name]})
raise exception.VolumeSizeExceedsAvailableQuota(
name=over_name,
requested=size,
consumed=_consumed(over_name),
quota=quotas[over_name])
elif exceeded_vol_limit_name:
msg = _LW("Quota %(s_name)s exceeded for %(s_pid)s, tried "
"to create volume (%(d_consumed)d volume(s) "
"already consumed).")
LOG.warning(msg,
{'s_name': exceeded_vol_limit_name,
's_pid': context.project_id,
'd_consumed':
_consumed(exceeded_vol_limit_name)})
# TODO(mc_nair): improve error message for child -1 limit
raise exception.VolumeLimitExceeded(
allowed=quotas[exceeded_vol_limit_name],
name=exceeded_vol_limit_name)
else:
# If nothing was reraised, ensure we reraise the initial error
raise
quota_utils.process_reserve_over_quota(context, e,
resource='volumes',
size=size)
def revert(self, context, result, optional_args, **kwargs):
# We never produced a result and therefore can't destroy anything.

View File

@@ -23,6 +23,7 @@ from cinder import flow_utils
from cinder.i18n import _, _LE, _LI
from cinder import objects
from cinder import quota
from cinder import quota_utils
from cinder.volume.flows import common as flow_common
from cinder.volume import utils as volume_utils
@@ -153,11 +154,10 @@ class QuotaReserveTask(flow_utils.CinderTask):
'reservations': reservations,
}
except exception.OverQuota as e:
overs = e.kwargs['overs']
quotas = e.kwargs['quotas']
usages = e.kwargs['usages']
volume_utils.process_reserve_over_quota(context, overs, usages,
quotas, size)
quota_utils.process_reserve_over_quota(
context, e,
resource='snapshots',
size=size)
def revert(self, context, result, optional_args, **kwargs):
# We never produced a result and therefore can't destroy anything.

View File

@@ -710,29 +710,3 @@ def convert_config_string_to_dict(config_string):
{'config_string': config_string})
return resultant_dict
def process_reserve_over_quota(context, overs, usages, quotas, size):
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
for over in overs:
if 'gigabytes' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"%(s_size)sG snapshot (%(d_consumed)dG of "
"%(d_quota)dG already consumed).")
LOG.warning(msg, {'s_pid': context.project_id,
's_size': size,
'd_consumed': _consumed(over),
'd_quota': quotas[over]})
raise exception.VolumeSizeExceedsAvailableQuota(
requested=size,
consumed=_consumed('gigabytes'),
quota=quotas['gigabytes'])
elif 'snapshots' in over:
msg = _LW("Quota exceeded for %(s_pid)s, tried to create "
"snapshot (%(d_consumed)d snapshots "
"already consumed).")
LOG.warning(msg, {'s_pid': context.project_id,
'd_consumed': _consumed(over)})
raise exception.SnapshotLimitExceeded(allowed=quotas[over])