Fix incomplete error message of quota exceeded

When we boot and resize instance, if multiple requested
resource(core, ram and instances) exceeded quota,
only the detail of core resource will been outputed to
user in the exception, the loss of core and instances number
will make end user have no idea which flavor can be
used to boot instance successfully.

Fix this issue and update related test cases.

Change-Id: I969d73e2f222278ea8a2bb4c21474c13ab213d81
Closes-Bug: #1469942
This commit is contained in:
Rui Chen 2015-06-30 20:34:32 +08:00
parent e041f3ee62
commit 79fe4d8e07
6 changed files with 98 additions and 32 deletions

View File

@ -429,10 +429,12 @@ class API(base.Base):
msg = (_("Can only run %s more instances of this type.") %
allowed)
resource = overs[0]
used = quotas[resource] - headroom[resource]
total_allowed = quotas[resource]
overs = ','.join(overs)
num_instances = (str(min_count) if min_count == max_count else
"%s-%s" % (min_count, max_count))
requested = dict(instances=num_instances, cores=req_cores,
ram=req_ram)
(overs, reqs, total_alloweds, useds) = self._get_over_quota_detail(
headroom, overs, quotas, requested)
params = {'overs': overs, 'pid': context.project_id,
'min_count': min_count, 'max_count': max_count,
'msg': msg}
@ -446,18 +448,25 @@ class API(base.Base):
" tried to run between %(min_count)d and"
" %(max_count)d instances. %(msg)s"),
params)
num_instances = (str(min_count) if min_count == max_count else
"%s-%s" % (min_count, max_count))
requested = dict(instances=num_instances, cores=req_cores,
ram=req_ram)
raise exception.TooManyInstances(overs=overs,
req=requested[resource],
used=used, allowed=total_allowed,
resource=resource)
req=reqs,
used=useds,
allowed=total_alloweds)
return max_count, quotas
def _get_over_quota_detail(self, headroom, overs, quotas, requested):
reqs = []
useds = []
total_alloweds = []
for resource in overs:
reqs.append(str(requested[resource]))
useds.append(str(quotas[resource] - headroom[resource]))
total_alloweds.append(str(quotas[resource]))
(overs, reqs, useds, total_alloweds) = map(', '.join, (
overs, reqs, useds, total_alloweds))
return overs, reqs, total_alloweds, useds
def _check_metadata_properties_quota(self, context, metadata=None):
"""Enforce quota limits on metadata properties."""
if not metadata:
@ -2636,19 +2645,16 @@ class API(base.Base):
overs = exc.kwargs['overs']
usages = exc.kwargs['usages']
headroom = self._get_headroom(quotas, usages, deltas)
resource = overs[0]
used = quotas[resource] - headroom[resource]
total_allowed = used + headroom[resource]
overs = ','.join(overs)
(overs, reqs, total_alloweds,
useds) = self._get_over_quota_detail(headroom, overs, quotas,
deltas)
LOG.warning(_LW("%(overs)s quota exceeded for %(pid)s,"
" tried to resize instance."),
{'overs': overs, 'pid': context.project_id})
raise exception.TooManyInstances(overs=overs,
req=deltas[resource],
used=used,
allowed=total_allowed,
resource=resource)
req=reqs,
used=useds,
allowed=total_alloweds)
else:
quotas = objects.Quotas(context=context)

View File

@ -1305,7 +1305,7 @@ class QuotaError(NovaException):
class TooManyInstances(QuotaError):
msg_fmt = _("Quota exceeded for %(overs)s: Requested %(req)s,"
" but already used %(used)d of %(allowed)d %(resource)s")
" but already used %(used)s of %(allowed)s %(overs)s")
class FloatingIpLimitExceeded(QuotaError):

View File

@ -111,7 +111,7 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
def test_migrate_too_many_instances(self):
exc_info = exception.TooManyInstances(overs='', req='', used=0,
allowed=0, resource='')
allowed=0)
self._test_migrate_exception(exc_info, webob.exc.HTTPForbidden)
def _test_migrate_live_succeeded(self, param):

View File

@ -171,9 +171,8 @@ def stub_out_instance_quota(stubs, allowed, quota, resource='instances'):
usages = dict(instances=dict(in_use=0, reserved=0),
cores=dict(in_use=0, reserved=0),
ram=dict(in_use=0, reserved=0))
usages[resource]['in_use'] = (quotas[resource] * 0.9 -
allowed)
usages[resource]['reserved'] = quotas[resource] * 0.1
usages[resource]['in_use'] = (quotas[resource] * 9 // 10 - allowed)
usages[resource]['reserved'] = quotas[resource] // 10
raise exc.OverQuota(overs=[resource], quotas=quotas,
usages=usages)
stubs.Set(QUOTAS, 'reserve', fake_reserve)

View File

@ -1574,6 +1574,61 @@ class _ComputeAPIUnitTestMixIn(object):
fake_inst, flavor_id='flavor-id')
self.assertFalse(mock_save.called)
def test_check_instance_quota_exceeds_with_multiple_resources(self):
quotas = {'cores': 1, 'instances': 1, 'ram': 512}
usages = {'cores': dict(in_use=1, reserved=0),
'instances': dict(in_use=1, reserved=0),
'ram': dict(in_use=512, reserved=0)}
overs = ['cores', 'instances', 'ram']
over_quota_args = dict(quotas=quotas,
usages=usages,
overs=overs)
e = exception.OverQuota(**over_quota_args)
fake_flavor = self._create_flavor()
instance_num = 1
with mock.patch.object(objects.Quotas, 'reserve', side_effect=e):
try:
self.compute_api._check_num_instances_quota(self.context,
fake_flavor,
instance_num,
instance_num)
except exception.TooManyInstances as e:
self.assertEqual('cores, instances, ram', e.kwargs['overs'])
self.assertEqual('1, 1, 512', e.kwargs['req'])
self.assertEqual('1, 1, 512', e.kwargs['used'])
self.assertEqual('1, 1, 512', e.kwargs['allowed'])
else:
self.fail("Exception not raised")
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
@mock.patch.object(objects.Quotas, 'reserve')
def test_resize_instance_quota_exceeds_with_multiple_resources(
self, mock_reserve, mock_get_flavor):
quotas = {'cores': 1, 'ram': 512}
usages = {'cores': dict(in_use=1, reserved=0),
'ram': dict(in_use=512, reserved=0)}
overs = ['cores', 'ram']
over_quota_args = dict(quotas=quotas,
usages=usages,
overs=overs)
mock_reserve.side_effect = exception.OverQuota(**over_quota_args)
mock_get_flavor.return_value = self._create_flavor(id=333,
vcpus=3,
memory_mb=1536)
try:
self.compute_api.resize(self.context, self._create_instance_obj(),
'fake_flavor_id')
except exception.TooManyInstances as e:
self.assertEqual('cores, ram', e.kwargs['overs'])
self.assertEqual('2, 1024', e.kwargs['req'])
self.assertEqual('1, 512', e.kwargs['used'])
self.assertEqual('1, 512', e.kwargs['allowed'])
mock_get_flavor.assert_called_once_with('fake_flavor_id',
read_deleted="no")
else:
self.fail("Exception not raised")
def test_pause(self):
# Ensure instance can be paused.
instance = self._create_instance_obj()

View File

@ -87,9 +87,12 @@ class QuotaIntegrationTestCase(test.TestCase):
instance_type=inst_type,
image_href=image_uuid)
except exception.QuotaError as e:
expected_kwargs = {'code': 413, 'resource': 'cores', 'req': 1,
'used': 4, 'allowed': 4, 'overs': 'cores,instances'}
self.assertEqual(e.kwargs, expected_kwargs)
expected_kwargs = {'code': 413,
'req': '1, 1',
'used': '4, 2',
'allowed': '4, 2',
'overs': 'cores, instances'}
self.assertEqual(expected_kwargs, e.kwargs)
else:
self.fail('Expected QuotaError exception')
for instance_uuid in instance_uuids:
@ -104,9 +107,12 @@ class QuotaIntegrationTestCase(test.TestCase):
instance_type=inst_type,
image_href=image_uuid)
except exception.QuotaError as e:
expected_kwargs = {'code': 413, 'resource': 'cores', 'req': 1,
'used': 4, 'allowed': 4, 'overs': 'cores'}
self.assertEqual(e.kwargs, expected_kwargs)
expected_kwargs = {'code': 413,
'req': '1',
'used': '4',
'allowed': '4',
'overs': 'cores'}
self.assertEqual(expected_kwargs, e.kwargs)
else:
self.fail('Expected QuotaError exception')
db.instance_destroy(self.context, instance['uuid'])