quota: retrieve quota (limit) and usage at once

Previously tenant_quota_usages() uses list API operations
to count usages. It is not efficient. This commit changes
the limit APIs from nova and cinder to retrieve usages.

blueprint make-quotas-great-again
Change-Id: I2c9a479758a1dfe134e5fabf16ab02831338718d
This commit is contained in:
Akihiro Motoki 2017-10-22 22:50:42 +00:00
parent 34fb3e5b0e
commit df0a18e7a3
9 changed files with 222 additions and 239 deletions

View File

@ -967,8 +967,8 @@ def qos_specs_list(request):
@profiler.trace @profiler.trace
@memoized @memoized
def tenant_absolute_limits(request): def tenant_absolute_limits(request, tenant_id=None):
limits = cinderclient(request).limits.get().absolute limits = cinderclient(request).limits.get(tenant_id=tenant_id).absolute
limits_dict = {} limits_dict = {}
for limit in limits: for limit in limits:
if limit.value < 0: if limit.value < 0:

View File

@ -908,8 +908,13 @@ def migrate_host(request, host, live_migrate=False, disk_over_commit=False,
@profiler.trace @profiler.trace
def tenant_absolute_limits(request, reserved=False): def tenant_absolute_limits(request, reserved=False, tenant_id=None):
limits = novaclient(request).limits.get(reserved=reserved).absolute # Nova does not allow to specify tenant_id for non-admin users
# even if tenant_id matches a tenant_id of the user.
if tenant_id == request.user.tenant_id:
tenant_id = None
limits = novaclient(request).limits.get(reserved=reserved,
tenant_id=tenant_id).absolute
limits_dict = {} limits_dict = {}
for limit in limits: for limit in limits:
if limit.value < 0: if limit.value < 0:

View File

@ -52,8 +52,8 @@ class DeleteKeyPairs(tables.DeleteAction):
class QuotaKeypairMixin(object): class QuotaKeypairMixin(object):
def allowed(self, request, datum=None): def allowed(self, request, datum=None):
usages = quotas.tenant_quota_usages(request, targets=('key_pairs', )) usages = quotas.tenant_quota_usages(request, targets=('key_pairs', ))
count = len(self.table.data) usages.tally('key_pairs', len(self.table.data))
if (usages.get('key_pairs') and usages['key_pairs']['quota'] <= count): if usages['key_pairs']['available'] <= 0:
if "disabled" not in self.classes: if "disabled" not in self.classes:
self.classes = [c for c in self.classes] + ['disabled'] self.classes = [c for c in self.classes] + ['disabled']
self.verbose_name = string_concat(self.verbose_name, ' ', self.verbose_name = string_concat(self.verbose_name, ' ',

View File

@ -337,9 +337,9 @@ class UsageViewTests(test.TestCase):
self.assertTemplateUsed(res, 'project/overview/usage.html') self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertIsInstance(usages, usage.ProjectUsage) self.assertIsInstance(usages, usage.ProjectUsage)
if cinder_enabled: if cinder_enabled:
self.assertEqual(usages.limits['totalVolumesUsed'], 1) self.assertEqual(usages.limits['totalVolumesUsed'], 4)
self.assertEqual(usages.limits['maxTotalVolumes'], 10) self.assertEqual(usages.limits['maxTotalVolumes'], 20)
self.assertEqual(usages.limits['totalGigabytesUsed'], 5) self.assertEqual(usages.limits['totalGigabytesUsed'], 400)
self.assertEqual(usages.limits['maxTotalVolumeGigabytes'], 1000) self.assertEqual(usages.limits['maxTotalVolumeGigabytes'], 1000)
else: else:
self.assertNotIn('totalVolumesUsed', usages.limits) self.assertNotIn('totalVolumesUsed', usages.limits)

View File

@ -333,10 +333,17 @@ def data(TEST):
) )
) )
# Cinder Limits # Cinder Limits
limits = {"absolute": {"totalVolumesUsed": 1, limits = {
"totalGigabytesUsed": 5, "absolute": {
"maxTotalVolumeGigabytes": 1000, "totalVolumesUsed": 4,
"maxTotalVolumes": 10}} "totalGigabytesUsed": 400,
'totalSnapshotsUsed': 3,
"maxTotalVolumes": 20,
"maxTotalVolumeGigabytes": 1000,
'maxTotalSnapshots': 10,
}
}
TEST.cinder_limits = limits TEST.cinder_limits = limits
# QOS Specs # QOS Specs

View File

@ -340,10 +340,10 @@ def data(TEST):
"maxTotalInstances": 10, "maxTotalInstances": 10,
"maxTotalKeypairs": 100, "maxTotalKeypairs": 100,
"maxTotalRAMSize": 10000, "maxTotalRAMSize": 10000,
"totalCoresUsed": 0, "totalCoresUsed": 2,
"totalInstancesUsed": 0, "totalInstancesUsed": 2,
"totalKeyPairsUsed": 0, "totalKeyPairsUsed": 0,
"totalRAMUsed": 0, "totalRAMUsed": 1024,
"totalSecurityGroupsUsed": 0}} "totalSecurityGroupsUsed": 0}}
TEST.limits = limits TEST.limits = limits

View File

@ -341,7 +341,8 @@ class ComputeApiTests(test.APITestCase):
novaclient = self.stub_novaclient() novaclient = self.stub_novaclient()
novaclient.limits = self.mox.CreateMockAnything() novaclient.limits = self.mox.CreateMockAnything()
novaclient.limits.get(reserved=True).AndReturn(limits) novaclient.limits.get(reserved=True,
tenant_id=None).AndReturn(limits)
self.mox.ReplayAll() self.mox.ReplayAll()
ret_val = api.nova.tenant_absolute_limits(self.request, reserved=True) ret_val = api.nova.tenant_absolute_limits(self.request, reserved=True)

View File

@ -67,6 +67,24 @@ class QuotaTests(test.APITestCase):
'quota': 1000}}) 'quota': 1000}})
return usages return usages
def get_usages_from_limits(self, with_volume=True, with_compute=True,
nova_quotas_enabled=True):
usages = {}
if with_compute and nova_quotas_enabled:
usages.update({
'instances': {'available': 8, 'used': 2, 'quota': 10},
'cores': {'available': 18, 'used': 2, 'quota': 20},
'ram': {'available': 8976, 'used': 1024, 'quota': 10000},
'key_pairs': {'quota': 100},
})
if with_volume:
usages.update({
'volumes': {'available': 16, 'used': 4, 'quota': 20},
'gigabytes': {'available': 600, 'used': 400, 'quota': 1000},
'snapshots': {'available': 7, 'used': 3, 'quota': 10},
})
return usages
def assertAvailableQuotasEqual(self, expected_usages, actual_usages): def assertAvailableQuotasEqual(self, expected_usages, actual_usages):
expected_available = {key: value['available'] for key, value in expected_available = {key: value['available'] for key, value in
expected_usages.items() if 'available' in value} expected_usages.items() if 'available' in value}
@ -74,12 +92,9 @@ class QuotaTests(test.APITestCase):
actual_usages.items() if 'available' in value} actual_usages.items() if 'available' in value}
self.assertEqual(expected_available, actual_available) self.assertEqual(expected_available, actual_available)
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
cinder: ('volume_list', 'volume_snapshot_list', cinder: ('tenant_absolute_limits',
'tenant_quota_get',
'is_volume_service_enabled')}) 'is_volume_service_enabled')})
def test_tenant_quota_usages_with_id(self): def test_tenant_quota_usages_with_id(self):
tenant_id = 3 tenant_id = 3
@ -88,48 +103,35 @@ class QuotaTests(test.APITestCase):
'network').AndReturn(False) 'network').AndReturn(False)
api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \ api.base.is_service_enabled(IsA(http.HttpRequest), 'compute') \
.MultipleTimes().AndReturn(True) .MultipleTimes().AndReturn(True)
servers = [s for s in self.servers.list() if s.tenant_id == tenant_id]
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
opts = {'tenant_id': tenant_id,
'all_tenants': True}
api.nova.server_list(IsA(http.HttpRequest), search_opts=opts) \
.AndReturn([servers, False])
api.nova.tenant_quota_get(IsA(http.HttpRequest), tenant_id) \
.AndReturn(self.quotas.first())
opts = {'all_tenants': 1, api.nova.tenant_absolute_limits(
'project_id': tenant_id} IsA(http.HttpRequest),
cinder.volume_list(IsA(http.HttpRequest), opts) \ reserved=True,
.AndReturn(self.volumes.list()) tenant_id=tenant_id).AndReturn(self.limits['absolute'])
cinder.volume_snapshot_list(IsA(http.HttpRequest), opts) \
.AndReturn(self.cinder_volume_snapshots.list()) api.cinder.tenant_absolute_limits(
cinder.tenant_quota_get(IsA(http.HttpRequest), tenant_id) \ IsA(http.HttpRequest),
.AndReturn(self.cinder_quotas.first()) tenant_id).AndReturn(self.cinder_limits['absolute'])
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request, quota_usages = quotas.tenant_quota_usages(self.request,
tenant_id=tenant_id) tenant_id=tenant_id)
expected_output = self.get_usages( expected_output = self.get_usages_from_limits(
nova_quotas_enabled=True, with_volume=True, with_volume=True, with_compute=True)
with_compute=True, tenant_id=tenant_id)
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.
self.assertItemsEqual(expected_output, quota_usages.usages) self.assertItemsEqual(expected_output, quota_usages.usages)
# Compare available resources # Compare available resources
self.assertAvailableQuotasEqual(expected_output, quota_usages.usages) self.assertAvailableQuotasEqual(expected_output, quota_usages.usages)
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
cinder: ('volume_list', 'volume_snapshot_list', cinder: ('tenant_absolute_limits',
'tenant_quota_get',
'is_volume_service_enabled')}) 'is_volume_service_enabled')})
def _test_tenant_quota_usages(self, nova_quotas_enabled=True, def _test_tenant_quota_usages(self, nova_quotas_enabled=True,
with_compute=True, with_volume=True): with_compute=True, with_volume=True):
tenant_id = '1'
cinder.is_volume_service_enabled(IsA(http.HttpRequest)).AndReturn( cinder.is_volume_service_enabled(IsA(http.HttpRequest)).AndReturn(
with_volume) with_volume)
api.base.is_service_enabled(IsA(http.HttpRequest), api.base.is_service_enabled(IsA(http.HttpRequest),
@ -139,30 +141,22 @@ class QuotaTests(test.APITestCase):
).MultipleTimes().AndReturn(with_compute) ).MultipleTimes().AndReturn(with_compute)
if with_compute: if with_compute:
if nova_quotas_enabled: if nova_quotas_enabled:
servers = [s for s in self.servers.list() api.nova.tenant_absolute_limits(
if s.tenant_id == self.request.user.tenant_id] IsA(http.HttpRequest),
api.nova.flavor_list(IsA(http.HttpRequest)) \ reserved=True,
.AndReturn(self.flavors.list()) tenant_id=tenant_id).AndReturn(self.limits['absolute'])
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([servers, False])
api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.quotas.first())
if with_volume: if with_volume:
opts = {'all_tenants': 1, api.cinder.tenant_absolute_limits(
'project_id': self.request.user.tenant_id} IsA(http.HttpRequest),
cinder.volume_list(IsA(http.HttpRequest), opts) \ tenant_id).AndReturn(self.cinder_limits['absolute'])
.AndReturn(self.volumes.list())
cinder.volume_snapshot_list(IsA(http.HttpRequest), opts) \
.AndReturn(self.cinder_volume_snapshots.list())
cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.cinder_quotas.first())
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request) quota_usages = quotas.tenant_quota_usages(self.request)
expected_output = self.get_usages( expected_output = self.get_usages_from_limits(
nova_quotas_enabled=nova_quotas_enabled, with_volume=with_volume, nova_quotas_enabled=nova_quotas_enabled,
with_volume=with_volume,
with_compute=with_compute) with_compute=with_compute)
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.
@ -198,14 +192,11 @@ class QuotaTests(test.APITestCase):
quotas.NOVA_QUOTA_FIELDS) quotas.NOVA_QUOTA_FIELDS)
self.assertItemsEqual(result_quotas, expected_quotas) self.assertItemsEqual(result_quotas, expected_quotas)
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
api.cinder: ('is_volume_service_enabled',)}) api.cinder: ('is_volume_service_enabled',)})
def test_tenant_quota_usages_without_volume(self): def test_tenant_quota_usages_without_volume(self):
servers = [s for s in self.servers.list() tenant_id = self.request.user.tenant_id
if s.tenant_id == self.request.user.tenant_id]
api.cinder.is_volume_service_enabled( api.cinder.is_volume_service_enabled(
IsA(http.HttpRequest) IsA(http.HttpRequest)
@ -214,17 +205,15 @@ class QuotaTests(test.APITestCase):
'network').AndReturn(False) 'network').AndReturn(False)
api.base.is_service_enabled(IsA(http.HttpRequest), api.base.is_service_enabled(IsA(http.HttpRequest),
'compute').MultipleTimes().AndReturn(True) 'compute').MultipleTimes().AndReturn(True)
api.nova.flavor_list(IsA(http.HttpRequest)) \ api.nova.tenant_absolute_limits(
.AndReturn(self.flavors.list()) IsA(http.HttpRequest),
api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ reserved=True,
.AndReturn(self.quotas.first()) tenant_id=tenant_id).AndReturn(self.limits['absolute'])
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([servers, False])
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request) quota_usages = quotas.tenant_quota_usages(self.request)
expected_output = self.get_usages(with_volume=False) expected_output = self.get_usages_from_limits(with_volume=False)
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.
self.assertItemsEqual(expected_output, quota_usages.usages) self.assertItemsEqual(expected_output, quota_usages.usages)
@ -234,9 +223,7 @@ class QuotaTests(test.APITestCase):
self.assertIn('ram', quota_usages) self.assertIn('ram', quota_usages)
self.assertIsNotNone(quota_usages.get('ram')) self.assertIsNotNone(quota_usages.get('ram'))
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
api.cinder: ('is_volume_service_enabled',)}) api.cinder: ('is_volume_service_enabled',)})
def test_tenant_quota_usages_no_instances_running(self): def test_tenant_quota_usages_no_instances_running(self):
@ -247,16 +234,15 @@ class QuotaTests(test.APITestCase):
'network').AndReturn(False) 'network').AndReturn(False)
api.base.is_service_enabled(IsA(http.HttpRequest), api.base.is_service_enabled(IsA(http.HttpRequest),
'compute').MultipleTimes().AndReturn(True) 'compute').MultipleTimes().AndReturn(True)
api.nova.flavor_list(IsA(http.HttpRequest)) \ api.nova.tenant_absolute_limits(
.AndReturn(self.flavors.list()) IsA(http.HttpRequest),
api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ reserved=True,
.AndReturn(self.quotas.first()) tenant_id='1').AndReturn(self.limits['absolute'])
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([[], False])
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request) quota_usages = quotas.tenant_quota_usages(self.request)
expected_output = self.get_usages(with_volume=False) expected_output = self.get_usages_from_limits(with_volume=False)
expected_output.update({ expected_output.update({
'ram': {'available': 10000, 'used': 0, 'quota': 10000}, 'ram': {'available': 10000, 'used': 0, 'quota': 10000},
@ -266,18 +252,14 @@ class QuotaTests(test.APITestCase):
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.
self.assertItemsEqual(expected_output, quota_usages.usages) self.assertItemsEqual(expected_output, quota_usages.usages)
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
cinder: ('volume_list', 'volume_snapshot_list', cinder: ('tenant_absolute_limits',
'tenant_quota_get',
'is_volume_service_enabled')}) 'is_volume_service_enabled')})
def test_tenant_quota_usages_unlimited_quota(self): def test_tenant_quota_usages_unlimited_quota(self):
tenant_id = '1'
inf_quota = self.quotas.first() inf_quota = self.quotas.first()
inf_quota['ram'] = -1 inf_quota['ram'] = -1
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
cinder.is_volume_service_enabled( cinder.is_volume_service_enabled(
IsA(http.HttpRequest) IsA(http.HttpRequest)
@ -286,23 +268,19 @@ class QuotaTests(test.APITestCase):
'network').AndReturn(False) 'network').AndReturn(False)
api.base.is_service_enabled(IsA(http.HttpRequest), api.base.is_service_enabled(IsA(http.HttpRequest),
'compute').MultipleTimes().AndReturn(True) 'compute').MultipleTimes().AndReturn(True)
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list()) api.nova.tenant_absolute_limits(
api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ IsA(http.HttpRequest),
.AndReturn(inf_quota) reserved=True,
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) tenant_id=tenant_id).AndReturn(self.limits['absolute'])
opts = {'all_tenants': 1, 'project_id': self.request.user.tenant_id} api.cinder.tenant_absolute_limits(
cinder.volume_list(IsA(http.HttpRequest), opts) \ IsA(http.HttpRequest),
.AndReturn(self.volumes.list()) tenant_id).AndReturn(self.cinder_limits['absolute'])
cinder.volume_snapshot_list(IsA(http.HttpRequest), opts) \
.AndReturn(self.cinder_volume_snapshots.list())
cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.cinder_quotas.first())
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request) quota_usages = quotas.tenant_quota_usages(self.request)
expected_output = self.get_usages() expected_output = self.get_usages_from_limits()
expected_output.update({'ram': {'available': float("inf"), expected_output.update({'ram': {'available': float("inf"),
'used': 1024, 'used': 1024,
'quota': float("inf")}}) 'quota': float("inf")}})
@ -310,17 +288,12 @@ class QuotaTests(test.APITestCase):
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.
self.assertItemsEqual(expected_output, quota_usages.usages) self.assertItemsEqual(expected_output, quota_usages.usages)
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
cinder: ('volume_list', 'volume_snapshot_list', cinder: ('tenant_absolute_limits',
'tenant_quota_get',
'is_volume_service_enabled')}) 'is_volume_service_enabled')})
def test_tenant_quota_usages_neutron_fip_disabled(self): def test_tenant_quota_usages_neutron_fip_disabled(self):
servers = [s for s in self.servers.list() tenant_id = '1'
if s.tenant_id == self.request.user.tenant_id]
cinder.is_volume_service_enabled( cinder.is_volume_service_enabled(
IsA(http.HttpRequest) IsA(http.HttpRequest)
).AndReturn(True) ).AndReturn(True)
@ -328,38 +301,22 @@ class QuotaTests(test.APITestCase):
'network').AndReturn(False) 'network').AndReturn(False)
api.base.is_service_enabled(IsA(http.HttpRequest), api.base.is_service_enabled(IsA(http.HttpRequest),
'compute').MultipleTimes().AndReturn(True) 'compute').MultipleTimes().AndReturn(True)
api.nova.flavor_list(IsA(http.HttpRequest)) \ api.nova.tenant_absolute_limits(
.AndReturn(self.flavors.list()) IsA(http.HttpRequest),
api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ reserved=True,
.AndReturn(self.quotas.first()) tenant_id=tenant_id).AndReturn(self.limits['absolute'])
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) api.cinder.tenant_absolute_limits(
opts = {'all_tenants': 1, 'project_id': self.request.user.tenant_id} IsA(http.HttpRequest),
cinder.volume_list(IsA(http.HttpRequest), opts) \ tenant_id).AndReturn(self.cinder_limits['absolute'])
.AndReturn(self.volumes.list())
cinder.volume_snapshot_list(IsA(http.HttpRequest), opts) \
.AndReturn(self.cinder_volume_snapshots.list())
cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.cinder_quotas.first())
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request) quota_usages = quotas.tenant_quota_usages(self.request)
expected_output = self.get_usages() expected_output = self.get_usages_from_limits()
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.
self.assertItemsEqual(expected_output, quota_usages.usages) self.assertItemsEqual(expected_output, quota_usages.usages)
@test.create_stubs({cinder: ('volume_list',),
exceptions: ('handle',)})
def test_get_tenant_volume_usages_cinder_exception(self):
cinder.volume_list(IsA(http.HttpRequest)) \
.AndRaise(cinder.cinder_exception.ClientException('test'))
exceptions.handle(IsA(http.HttpRequest),
_("Unable to retrieve volume limit information."))
self.mox.ReplayAll()
quotas._get_tenant_volume_usages(self.request, {}, set(), None)
@test.create_stubs({api.base: ('is_service_enabled',), @test.create_stubs({api.base: ('is_service_enabled',),
api.cinder: ('tenant_quota_get', api.cinder: ('tenant_quota_get',
'is_volume_service_enabled'), 'is_volume_service_enabled'),
@ -439,17 +396,15 @@ class QuotaTests(test.APITestCase):
targets=('instances', 'cores', 'ram', 'volumes', ), targets=('instances', 'cores', 'ram', 'volumes', ),
use_flavor_list=True, use_cinder_call=True) use_flavor_list=True, use_cinder_call=True)
@test.create_stubs({api.nova: ('server_list', @test.create_stubs({api.nova: ('tenant_absolute_limits',),
'flavor_list',
'tenant_quota_get',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
cinder: ('volume_list', 'volume_snapshot_list', cinder: ('tenant_absolute_limits',
'tenant_quota_get',
'is_volume_service_enabled')}) 'is_volume_service_enabled')})
def _test_tenant_quota_usages_with_target( def _test_tenant_quota_usages_with_target(
self, targets, self, targets,
use_compute_call=True, use_compute_call=True,
use_flavor_list=False, use_cinder_call=False): use_flavor_list=False, use_cinder_call=False):
tenant_id = self.request.user.tenant_id
cinder.is_volume_service_enabled(IsA(http.HttpRequest)).AndReturn(True) cinder.is_volume_service_enabled(IsA(http.HttpRequest)).AndReturn(True)
api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \
.AndReturn(False) .AndReturn(False)
@ -457,32 +412,22 @@ class QuotaTests(test.APITestCase):
.MultipleTimes().AndReturn(True) .MultipleTimes().AndReturn(True)
if use_compute_call: if use_compute_call:
api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ api.nova.tenant_absolute_limits(
.AndReturn(self.quotas.first()) IsA(http.HttpRequest),
servers = [s for s in self.servers.list() reserved=True,
if s.tenant_id == self.request.user.tenant_id] tenant_id=tenant_id).AndReturn(self.limits['absolute'])
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([servers, False])
if use_flavor_list:
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
if use_cinder_call: if use_cinder_call:
opts = {'all_tenants': 1, api.cinder.tenant_absolute_limits(
'project_id': self.request.user.tenant_id} IsA(http.HttpRequest),
cinder.volume_list(IsA(http.HttpRequest), opts) \ tenant_id).AndReturn(self.cinder_limits['absolute'])
.AndReturn(self.volumes.list())
cinder.volume_snapshot_list(IsA(http.HttpRequest), opts) \
.AndReturn(self.cinder_volume_snapshots.list())
cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.cinder_quotas.first())
self.mox.ReplayAll() self.mox.ReplayAll()
quota_usages = quotas.tenant_quota_usages(self.request, quota_usages = quotas.tenant_quota_usages(self.request,
targets=targets) targets=targets)
expected = self.get_usages() expected = self.get_usages_from_limits()
expected = dict((k, v) for k, v in expected.items() if k in targets) expected = dict((k, v) for k, v in expected.items() if k in targets)
# Compare internal structure of usages to expected. # Compare internal structure of usages to expected.

View File

@ -44,10 +44,38 @@ NOVA_COMPUTE_QUOTA_FIELDS = {
# are not considered. # are not considered.
NOVA_QUOTA_FIELDS = NOVA_COMPUTE_QUOTA_FIELDS NOVA_QUOTA_FIELDS = NOVA_COMPUTE_QUOTA_FIELDS
NOVA_QUOTA_LIMIT_MAP = {
'instances': {
'limit': 'maxTotalInstances',
'usage': 'totalInstancesUsed'
},
'cores': {
'limit': 'maxTotalCores',
'usage': 'totalCoresUsed'
},
'ram': {
'limit': 'maxTotalRAMSize',
'usage': 'totalRAMUsed'
},
'key_pairs': {
'limit': 'maxTotalKeypairs',
'usage': None
},
}
CINDER_QUOTA_FIELDS = {"volumes", CINDER_QUOTA_FIELDS = {"volumes",
"snapshots", "snapshots",
"gigabytes"} "gigabytes"}
CINDER_QUOTA_LIMIT_MAP = {
'volumes': {'usage': 'totalVolumesUsed',
'limit': 'maxTotalVolumes'},
'gigabytes': {'usage': 'totalGigabytesUsed',
'limit': 'maxTotalVolumeGigabytes'},
'snapshots': {'usage': 'totalSnapshotsUsed',
'limit': 'maxTotalSnapshots'},
}
NEUTRON_QUOTA_FIELDS = {"network", NEUTRON_QUOTA_FIELDS = {"network",
"subnet", "subnet",
"port", "port",
@ -198,6 +226,12 @@ def get_tenant_quota_data(request, disabled_quotas=None, tenant_id=None):
if not (NEUTRON_QUOTA_FIELDS - disabled_quotas): if not (NEUTRON_QUOTA_FIELDS - disabled_quotas):
return qs return qs
_get_neutron_quota_data(request, qs, disabled_quotas, tenant_id)
return qs
def _get_neutron_quota_data(request, qs, disabled_quotas, tenant_id):
tenant_id = tenant_id or request.user.tenant_id tenant_id = tenant_id or request.user.tenant_id
neutron_quotas = neutron.tenant_quota_get(request, tenant_id) neutron_quotas = neutron.tenant_quota_get(request, tenant_id)
@ -232,6 +266,11 @@ def get_tenant_quota_data(request, disabled_quotas=None, tenant_id=None):
return qs return qs
# TOOD(amotoki): Do not use neutron specific quota field names.
# At now, quota names from nova-network are used in the dashboard code,
# but get_disabled_quotas() returns quota names from neutron API.
# It is confusing and makes the code complicated. They should be push away.
# Check Identity Project panel and System Defaults panel too.
@profiler.trace @profiler.trace
def get_disabled_quotas(request): def get_disabled_quotas(request):
# We no longer supports nova network, so we always disable # We no longer supports nova network, so we always disable
@ -268,10 +307,19 @@ def get_disabled_quotas(request):
return disabled_quotas return disabled_quotas
def _add_usage_if_quota_enabled(usage, name, value, disabled_quotas): def _add_limit_and_usage(usages, name, limit, usage, disabled_quotas):
if name in disabled_quotas: if name not in disabled_quotas:
usages.add_quota(base.Quota(name, limit))
if usage is not None:
usages.tally(name, usage)
def _add_limit_and_usage_neutron(usages, name, quota_name,
detail, disabled_quotas):
if quota_name in disabled_quotas:
return return
usage.tally(name, value) usages.add_quota(base.Quota(name, detail['limit']))
usages.tally(name, detail['used'] + detail['reserved'])
@profiler.trace @profiler.trace
@ -280,50 +328,25 @@ def _get_tenant_compute_usages(request, usages, disabled_quotas, tenant_id):
if not enabled_compute_quotas: if not enabled_compute_quotas:
return return
# Unlike the other services it can be the case that nova is enabled but
# doesn't support quotas, in which case we still want to get usage info,
# so don't rely on '"instances" in disabled_quotas' as elsewhere
if not base.is_service_enabled(request, 'compute'): if not base.is_service_enabled(request, 'compute'):
return return
if tenant_id and tenant_id != request.user.project_id: try:
# all_tenants is required when querying about any project the user is limits = nova.tenant_absolute_limits(request, reserved=True,
# not currently scoped to tenant_id=tenant_id)
instances, has_more = nova.server_list( except nova.nova_exceptions.ClientException:
request, search_opts={'tenant_id': tenant_id, 'all_tenants': True}) msg = _("Unable to retrieve compute limit information.")
else: exceptions.handle(request, msg)
instances, has_more = nova.server_list(request)
_add_usage_if_quota_enabled(usages, 'instances', len(instances), for quota_name, limit_keys in NOVA_QUOTA_LIMIT_MAP.items():
disabled_quotas) if limit_keys['usage']:
usage = limits[limit_keys['usage']]
if {'cores', 'ram'} - disabled_quotas: else:
# Fetch deleted flavors if necessary. usage = None
flavors = dict([(f.id, f) for f in nova.flavor_list(request)]) _add_limit_and_usage(usages, quota_name,
missing_flavors = [instance.flavor['id'] for instance in instances limits[limit_keys['limit']],
if instance.flavor['id'] not in flavors] usage,
for missing in missing_flavors: disabled_quotas)
if missing not in flavors:
try:
flavors[missing] = nova.flavor_get(request, missing)
except Exception:
flavors[missing] = {}
exceptions.handle(request, ignore=True)
# Sum our usage based on the flavors of the instances.
for flavor in [flavors[instance.flavor['id']]
for instance in instances]:
_add_usage_if_quota_enabled(
usages, 'cores', getattr(flavor, 'vcpus', None),
disabled_quotas)
_add_usage_if_quota_enabled(
usages, 'ram', getattr(flavor, 'ram', None),
disabled_quotas)
# Initialize the tally if no instances have been launched yet
if len(instances) == 0:
_add_usage_if_quota_enabled(usages, 'cores', 0, disabled_quotas)
_add_usage_if_quota_enabled(usages, 'ram', 0, disabled_quotas)
@profiler.trace @profiler.trace
@ -332,6 +355,11 @@ def _get_tenant_network_usages(request, usages, disabled_quotas, tenant_id):
if not enabled_quotas: if not enabled_quotas:
return return
qs = base.QuotaSet()
_get_neutron_quota_data(request, qs, disabled_quotas, tenant_id)
for quota in qs:
usages.add_quota(quota)
# NOTE(amotoki): floatingip is Neutron quota and floating_ips is # NOTE(amotoki): floatingip is Neutron quota and floating_ips is
# Nova quota. We need to check both. # Nova quota. We need to check both.
if {'floatingip', 'floating_ips'} & enabled_quotas: if {'floatingip', 'floating_ips'} & enabled_quotas:
@ -367,41 +395,35 @@ def _get_tenant_network_usages(request, usages, disabled_quotas, tenant_id):
@profiler.trace @profiler.trace
def _get_tenant_volume_usages(request, usages, disabled_quotas, tenant_id): def _get_tenant_volume_usages(request, usages, disabled_quotas, tenant_id):
if CINDER_QUOTA_FIELDS - disabled_quotas: enabled_volume_quotas = CINDER_QUOTA_FIELDS - disabled_quotas
try: if not enabled_volume_quotas:
if tenant_id: return
opts = {'all_tenants': 1, 'project_id': tenant_id}
volumes = cinder.volume_list(request, opts) try:
snapshots = cinder.volume_snapshot_list(request, opts) limits = cinder.tenant_absolute_limits(request, tenant_id)
else: except cinder.cinder_exception.ClientException:
volumes = cinder.volume_list(request) msg = _("Unable to retrieve volume limit information.")
snapshots = cinder.volume_snapshot_list(request) exceptions.handle(request, msg)
volume_usage = sum([int(v.size) for v in volumes])
snapshot_usage = sum([int(s.size) for s in snapshots]) for quota_name, limit_keys in CINDER_QUOTA_LIMIT_MAP.items():
_add_usage_if_quota_enabled( _add_limit_and_usage(usages, quota_name,
usages, 'gigabytes', (snapshot_usage + volume_usage), limits[limit_keys['limit']],
disabled_quotas) limits[limit_keys['usage']],
_add_usage_if_quota_enabled( disabled_quotas)
usages, 'volumes', len(volumes), disabled_quotas)
_add_usage_if_quota_enabled(
usages, 'snapshots', len(snapshots), disabled_quotas)
except cinder.cinder_exception.ClientException:
msg = _("Unable to retrieve volume limit information.")
exceptions.handle(request, msg)
# Singular form key is used as quota field in the Neutron API.
# We convert it explicitly here.
# NOTE(amotoki): It is better to be converted in the horizon API wrapper
# layer. Ideally the REST APIs of back-end services are consistent.
NETWORK_QUOTA_API_KEY_MAP = { NETWORK_QUOTA_API_KEY_MAP = {
'floating_ips': ['floatingip'], 'floating_ips': ['floatingip'],
'security_groups': ['security_group'],
'security_group_rules': ['security_group_rule'],
# Singular form key is used as quota field in the Neutron API.
# We convert it explicitly here.
# NOTE(amotoki): It is better to be converted in the horizon API wrapper
# layer. Ideally the REST APIs of back-end services are consistent.
'networks': ['network'], 'networks': ['network'],
'subnets': ['subnet'],
'ports': ['port'], 'ports': ['port'],
'routers': ['router'], 'routers': ['router'],
'security_group_rules': ['security_group_rule'],
'security_groups': ['security_group'],
'subnets': ['subnet'],
} }
@ -418,6 +440,9 @@ def _convert_targets_to_quota_keys(targets):
return quota_keys return quota_keys
# TODO(amotoki): Merge tenant_quota_usages and tenant_limit_usages.
# These two functions are similar. There seems no reason to have both.
@profiler.trace @profiler.trace
@memoized @memoized
def tenant_quota_usages(request, tenant_id=None, targets=None): def tenant_quota_usages(request, tenant_id=None, targets=None):
@ -439,12 +464,6 @@ def tenant_quota_usages(request, tenant_id=None, targets=None):
enabled_quotas &= _convert_targets_to_quota_keys(targets) enabled_quotas &= _convert_targets_to_quota_keys(targets)
disabled_quotas = set(QUOTA_FIELDS) - enabled_quotas disabled_quotas = set(QUOTA_FIELDS) - enabled_quotas
for quota in get_tenant_quota_data(request,
disabled_quotas=disabled_quotas,
tenant_id=tenant_id):
usages.add_quota(quota)
# Get our usages.
_get_tenant_compute_usages(request, usages, disabled_quotas, tenant_id) _get_tenant_compute_usages(request, usages, disabled_quotas, tenant_id)
_get_tenant_network_usages(request, usages, disabled_quotas, tenant_id) _get_tenant_network_usages(request, usages, disabled_quotas, tenant_id)
_get_tenant_volume_usages(request, usages, disabled_quotas, tenant_id) _get_tenant_volume_usages(request, usages, disabled_quotas, tenant_id)
@ -472,6 +491,12 @@ def tenant_limit_usages(request):
msg = _("Unable to retrieve volume limit information.") msg = _("Unable to retrieve volume limit information.")
exceptions.handle(request, msg) exceptions.handle(request, msg)
# TODO(amotoki): Support neutron quota details extensions
# which returns limit/usage/reserved per resource.
# Note that the data format is different from nova/cinder limit API.
# https://developer.openstack.org/
# api-ref/network/v2/#quotas-details-extension-quota-details
return limits return limits