From fde88906b34d82443bd9d5615ea1e9297fe26692 Mon Sep 17 00:00:00 2001 From: Julie Pichon Date: Mon, 26 Aug 2013 16:32:37 +0100 Subject: [PATCH] Show Neutron floating IPs quotas on Overview Display the correct limits and usage when Neutron is in use and the quotas extension is enabled. If Neutron is enabled but the quotas extensions is not supported, assume the floating IPs quota is unlimited (a floating IP quota is expected to exist in other places, e.g. Security and Access panel) Because quotas may not be configured or enabled even if the extension is available, add an 'enable_quotas' setting. Partial-Bug: #1109140 Change-Id: Id6345f4700f0ff45be8ce8acb69cca0d4e05e14a --- openstack_dashboard/api/base.py | 7 +- openstack_dashboard/api/neutron.py | 22 +++++ .../dashboards/admin/overview/tests.py | 12 ++- .../dashboards/project/overview/tests.py | 85 ++++++++++++++++++- .../local/local_settings.py.example | 7 +- .../test/api_tests/neutron_tests.py | 11 +++ openstack_dashboard/test/settings.py | 3 +- .../test/test_data/neutron_data.py | 27 +++++- openstack_dashboard/usage/base.py | 42 ++++++++- 9 files changed, 203 insertions(+), 13 deletions(-) diff --git a/openstack_dashboard/api/base.py b/openstack_dashboard/api/base.py index d2e63b402f..2d9c595644 100644 --- a/openstack_dashboard/api/base.py +++ b/openstack_dashboard/api/base.py @@ -158,7 +158,12 @@ class QuotaSet(Sequence): def __init__(self, apiresource=None): self.items = [] if apiresource: - for k, v in apiresource._info.items(): + if hasattr(apiresource, '_info'): + items = apiresource._info.items() + else: + items = apiresource.items() + + for k, v in items: if k == 'id': continue self[k] = v diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index 0e24ccd132..cb0857aba6 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -602,3 +602,25 @@ def router_add_gateway(request, router_id, network_id): def router_remove_gateway(request, router_id): neutronclient(request).remove_gateway_router(router_id) + + +def tenant_quota_get(request, tenant_id): + return base.QuotaSet(neutronclient(request).show_quota(tenant_id)['quota']) + + +def list_extensions(request): + extensions_list = neutronclient(request).list_extensions() + if 'extensions' in extensions_list: + return extensions_list['extensions'] + else: + return {} + + +def is_extension_supported(request, extension_alias): + extensions = list_extensions(request) + + for extension in extensions: + if extension['alias'] == extension_alias: + return True + else: + return False diff --git a/openstack_dashboard/dashboards/admin/overview/tests.py b/openstack_dashboard/dashboards/admin/overview/tests.py index e227509096..08bc2d2849 100644 --- a/openstack_dashboard/dashboards/admin/overview/tests.py +++ b/openstack_dashboard/dashboards/admin/overview/tests.py @@ -39,7 +39,8 @@ INDEX_URL = reverse('horizon:project:overview:index') class UsageViewTests(test.BaseAdminViewTests): @test.create_stubs({api.nova: ('usage_list', 'tenant_absolute_limits', ), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + api.network: ('tenant_floating_ip_list',)}) def test_usage(self): now = timezone.now() usage_obj = api.nova.NovaUsage(self.usages.first()) @@ -55,7 +56,10 @@ class UsageViewTests(test.BaseAdminViewTests): .AndReturn([usage_obj]) api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \ .AndReturn(self.limits['absolute']) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() + res = self.client.get(reverse('horizon:admin:overview:index')) self.assertTemplateUsed(res, 'admin/overview/usage.html') self.assertTrue(isinstance(res.context['usage'], usage.GlobalUsage)) @@ -74,7 +78,8 @@ class UsageViewTests(test.BaseAdminViewTests): usage_obj.total_local_gb_usage)) @test.create_stubs({api.nova: ('usage_list', 'tenant_absolute_limits', ), - api.keystone: ('tenant_list',)}) + api.keystone: ('tenant_list',), + api.network: ('tenant_floating_ip_list',)}) def test_usage_csv(self): now = timezone.now() usage_obj = [api.nova.NovaUsage(u) for u in self.usages.list()] @@ -90,7 +95,10 @@ class UsageViewTests(test.BaseAdminViewTests): .AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() + csv_url = reverse('horizon:admin:overview:index') + "?format=csv" res = self.client.get(csv_url) self.assertTemplateUsed(res, 'admin/overview/usage.csv') diff --git a/openstack_dashboard/dashboards/project/overview/tests.py b/openstack_dashboard/dashboards/project/overview/tests.py index cc4dde8471..79f50a2821 100644 --- a/openstack_dashboard/dashboards/project/overview/tests.py +++ b/openstack_dashboard/dashboards/project/overview/tests.py @@ -22,6 +22,7 @@ import datetime from django.core.urlresolvers import reverse # noqa from django import http +from django.test.utils import override_settings # noqa from django.utils import timezone from mox import IsA # noqa @@ -40,6 +41,7 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, datetime.datetime(now.year, now.month, @@ -48,20 +50,52 @@ class UsageViewTests(test.TestCase): now.month, now.day, 23, 59, 59, 0)) \ .AndReturn(usage_obj) - api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ + api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \ .AndReturn(self.limits['absolute']) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) + usages = res.context['usage'] self.assertTemplateUsed(res, 'project/overview/usage.html') - self.assertTrue(isinstance(res.context['usage'], usage.ProjectUsage)) + self.assertTrue(isinstance(usages, usage.ProjectUsage)) self.assertContains(res, 'form-horizontal') + self.assertEqual(usages.limits['maxTotalFloatingIps'], float("inf")) + + def test_usage_nova_network(self): + now = timezone.now() + usage_obj = api.nova.NovaUsage(self.usages.first()) + self.mox.StubOutWithMock(api.nova, 'usage_get') + self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.base, 'is_service_enabled') + api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, + datetime.datetime(now.year, + now.month, + now.day, 0, 0, 0, 0), + datetime.datetime(now.year, + now.month, + now.day, 23, 59, 59, 0)) \ + .AndReturn(usage_obj) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \ + .AndReturn(self.limits['absolute']) + api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ + .AndReturn(False) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:project:overview:index')) + usages = res.context['usage'] + self.assertTemplateUsed(res, 'project/overview/usage.html') + self.assertTrue(isinstance(usages, usage.ProjectUsage)) + self.assertContains(res, 'form-horizontal') + self.assertEqual(usages.limits['maxTotalFloatingIps'], 10) def test_unauthorized(self): exc = self.exceptions.nova_unauthorized now = timezone.now() self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, datetime.datetime(now.year, now.month, @@ -72,6 +106,8 @@ class UsageViewTests(test.TestCase): .AndRaise(exc) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() url = reverse('horizon:project:overview:index') @@ -85,6 +121,7 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), @@ -92,7 +129,8 @@ class UsageViewTests(test.TestCase): start, end).AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) - + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index') + "?format=csv") @@ -103,6 +141,7 @@ class UsageViewTests(test.TestCase): now = timezone.now() self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), @@ -110,7 +149,8 @@ class UsageViewTests(test.TestCase): start, end).AndRaise(self.exceptions.nova) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) - + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) @@ -122,6 +162,7 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), @@ -129,6 +170,8 @@ class UsageViewTests(test.TestCase): start, end).AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndRaise(self.exceptions.nova) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) @@ -140,6 +183,7 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), @@ -147,8 +191,41 @@ class UsageViewTests(test.TestCase): start, end).AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) self.assertTemplateUsed(res, 'project/overview/usage.html') self.assertTrue(isinstance(res.context['usage'], usage.ProjectUsage)) + + @override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_quotas': True}) + def test_usage_with_neutron_floating_ips(self): + now = timezone.now() + usage_obj = api.nova.NovaUsage(self.usages.first()) + self.mox.StubOutWithMock(api.nova, 'usage_get') + self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') + self.mox.StubOutWithMock(api.neutron, 'is_extension_supported') + self.mox.StubOutWithMock(api.neutron, 'tenant_quota_get') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) + end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) + api.nova.usage_get(IsA(http.HttpRequest), + self.tenant.id, + start, end).AndReturn(usage_obj) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ + .AndReturn(self.limits['absolute']) + api.neutron.is_extension_supported(IsA(http.HttpRequest), 'quotas') \ + .AndReturn(True) + api.neutron.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \ + .AndReturn(self.neutron_quotas.first()) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:project:overview:index')) + self.assertContains(res, 'Floating IPs') + + # Make sure the floating IPs limit comes from Neutron (50 vs. 10) + max_floating_ips = res.context['usage'].limits['maxTotalFloatingIps'] + self.assertEqual(max_floating_ips, 50) diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 089b33f051..55c7e9726a 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -155,11 +155,12 @@ OPENSTACK_HYPERVISOR_FEATURES = { } # The OPENSTACK_NEUTRON_NETWORK settings can be used to enable optional -# services provided by neutron. Currently only the load balancer service -# is available. +# services provided by neutron. Options currenly available are load +# balancer service, security groups, quotas. OPENSTACK_NEUTRON_NETWORK = { - 'enable_security_group': True, 'enable_lb': False, + 'enable_quotas': True, + 'enable_security_group': True, } # OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints diff --git a/openstack_dashboard/test/api_tests/neutron_tests.py b/openstack_dashboard/test/api_tests/neutron_tests.py index 8ee1038570..10c2b4bfed 100644 --- a/openstack_dashboard/test/api_tests/neutron_tests.py +++ b/openstack_dashboard/test/api_tests/neutron_tests.py @@ -270,3 +270,14 @@ class NeutronApiTests(test.APITestCase): api.neutron.router_remove_interface( self.request, router_id, port_id=fake_port) + + def test_is_extension_supported(self): + neutronclient = self.stub_neutronclient() + neutronclient.list_extensions().MultipleTimes() \ + .AndReturn(self.api_extensions.first()) + self.mox.ReplayAll() + + self.assertTrue( + api.neutron.is_extension_supported(self.request, 'quotas')) + self.assertFalse( + api.neutron.is_extension_supported(self.request, 'doesntexist')) diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 3944e765ff..d700591dba 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -83,7 +83,8 @@ OPENSTACK_KEYSTONE_BACKEND = { } OPENSTACK_NEUTRON_NETWORK = { - 'enable_lb': True + 'enable_lb': True, + 'enable_quotas': False # Enabled in specific tests only } OPENSTACK_HYPERVISOR_FEATURES = { diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index b53338b614..f2fa2728bd 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -15,8 +15,8 @@ import copy import uuid +from openstack_dashboard.api import base from openstack_dashboard.api import lbaas - from openstack_dashboard.api import neutron from openstack_dashboard.test.test_data import utils @@ -35,6 +35,7 @@ def data(TEST): TEST.vips = utils.TestDataContainer() TEST.members = utils.TestDataContainer() TEST.monitors = utils.TestDataContainer() + TEST.neutron_quotas = utils.TestDataContainer() # data return by neutronclient TEST.api_networks = utils.TestDataContainer() @@ -48,6 +49,7 @@ def data(TEST): TEST.api_vips = utils.TestDataContainer() TEST.api_members = utils.TestDataContainer() TEST.api_monitors = utils.TestDataContainer() + TEST.api_extensions = utils.TestDataContainer() #------------------------------------------------------------ # 1st network @@ -448,3 +450,26 @@ def data(TEST): 'admin_state_up': True} TEST.api_monitors.add(monitor_dict) TEST.monitors.add(lbaas.PoolMonitor(monitor_dict)) + + #------------------------------------------------------------ + # Quotas + quota_data = dict(floatingip='50', + network='10', + port='50', + router='10', + security_groups='10', + security_group_rules='100', + subnet='10') + TEST.neutron_quotas.add(base.QuotaSet(quota_data)) + + #------------------------------------------------------------ + # Extensions + extension_1 = {"name": "security-group", + "alias": "security-group", + "description": "The security groups extension."} + extension_2 = {"name": "Quota management support", + "alias": "quotas", + "description": "Expose functions for quotas management"} + extensions = {} + extensions['extensions'] = [extension_1, extension_2] + TEST.api_extensions.add(extensions) diff --git a/openstack_dashboard/usage/base.py b/openstack_dashboard/usage/base.py index abf4c157d9..5b1c91795d 100644 --- a/openstack_dashboard/usage/base.py +++ b/openstack_dashboard/usage/base.py @@ -7,6 +7,7 @@ import datetime import logging from StringIO import StringIO # noqa +from django.conf import settings # noqa from django.http import HttpResponse # noqa from django import template as django_template from django.utils import timezone @@ -102,6 +103,43 @@ class BaseUsage(object): 'end': init[1]}) return self.form + def get_neutron_limits(self): + if not api.base.is_service_enabled(self.request, 'network'): + return + + # Retrieve number of floating IPs currently allocated + try: + floating_ips_current = len( + api.network.tenant_floating_ip_list(self.request)) + except Exception: + floating_ips_current = 0 + msg = _('Unable to retrieve floating IP addresses.') + exceptions.handle(self.request, msg) + + self.limits['totalFloatingIpsUsed'] = floating_ips_current + + # Quotas are an optional extension in Neutron. If it isn't + # enabled, assume the floating IP limit is infinite. + network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) + if not network_config.get('enable_quotas', False) or \ + not api.neutron.is_extension_supported(self.request, 'quotas'): + self.limits['maxTotalFloatingIps'] = float("inf") + return + + try: + neutron_quotas = api.neutron.tenant_quota_get(self.request, + self.project_id) + floating_ips_max = getattr(neutron_quotas.get('floatingip'), + 'limit', float("inf")) + if floating_ips_max == -1: + floating_ips_max = float("inf") + except Exception: + floating_ips_max = float("inf") + msg = _('Unable to retrieve network quota information.') + exceptions.handle(self.request, msg) + + self.limits['maxTotalFloatingIps'] = floating_ips_max + def get_limits(self): try: self.limits = api.nova.tenant_absolute_limits(self.request) @@ -109,8 +147,10 @@ class BaseUsage(object): exceptions.handle(self.request, _("Unable to retrieve limit information.")) + self.get_neutron_limits() + def get_usage_list(self, start, end): - raise NotImplementedError("You must define a get_usage method.") + raise NotImplementedError("You must define a get_usage_list method.") def summarize(self, start, end): if start <= end and start <= self.today: