diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py index 97ce4e2631..4ac11e8849 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py @@ -182,42 +182,51 @@ class FloatingIpNeutronViewTests(FloatingIpViewTests): @test.create_stubs({api.nova: ('tenant_quota_get', 'flavor_list', 'server_list'), - api.cinder: ('tenant_quota_get', 'volume_list', - 'volume_snapshot_list',), api.network: ('floating_ip_pools_list', 'floating_ip_supported', + 'security_group_list', 'tenant_floating_ip_list'), api.neutron: ('is_extension_supported', - 'tenant_quota_get')}) + 'tenant_quota_get', + 'network_list', + 'router_list'), + api.base: ('is_service_enabled',)}) @test.update_settings(OPENSTACK_NEUTRON_NETWORK={'enable_quotas': True}) def test_correct_quotas_displayed(self): + quota_data = self.quota_usages.first() + quota_data['floating_ips']['quota'] = 50 + servers = [s for s in self.servers.list() if s.tenant_id == self.request.user.tenant_id] + api.base.is_service_enabled(IsA(http.HttpRequest), 'volume') \ + .AndReturn(False) + api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ + .MultipleTimes().AndReturn(True) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(self.quotas.first()) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn([servers, False]) - api.cinder.volume_list(IsA(http.HttpRequest)) \ - .AndReturn(self.volumes.list()) - api.cinder.volume_snapshot_list(IsA(http.HttpRequest)) \ - .AndReturn(self.snapshots.list()) - api.cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \ - .AndReturn(self.cinder_quotas.first()) api.neutron.is_extension_supported( IsA(http.HttpRequest), 'security-group').AndReturn(True) 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.neutron.router_list(IsA(http.HttpRequest)) \ + .AndReturn(self.routers.list()) + api.neutron.network_list(IsA(http.HttpRequest), shared=False) \ + .AndReturn(self.networks.list()) api.network.floating_ip_supported(IsA(http.HttpRequest)) \ .AndReturn(True) api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(self.floating_ips.list()) api.network.floating_ip_pools_list(IsA(http.HttpRequest)) \ .AndReturn(self.pools.list()) + api.network.security_group_list(IsA(http.HttpRequest)) \ + .AndReturn(self.security_groups.list()) self.mox.ReplayAll() url = reverse('%s:allocate' % NAMESPACE) diff --git a/openstack_dashboard/dashboards/project/access_and_security/security_groups/tables.py b/openstack_dashboard/dashboards/project/access_and_security/security_groups/tables.py index ba107e8fe4..f3af030af8 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/security_groups/tables.py +++ b/openstack_dashboard/dashboards/project/access_and_security/security_groups/tables.py @@ -21,6 +21,7 @@ from horizon import tables from openstack_dashboard import api from openstack_dashboard import policy +from openstack_dashboard.usage import quotas from openstack_dashboard.utils import filters @@ -77,6 +78,15 @@ class CreateGroup(tables.LinkAction): else: policy = (("compute", "compute_extension:security_groups"),) + usages = quotas.tenant_quota_usages(request) + if usages['security_groups']['available'] <= 0: + if "disabled" not in self.classes: + self.classes = [c for c in self.classes] + ["disabled"] + self.verbose_name = _("Create Security Group (Quota exceeded)") + else: + self.verbose_name = _("Create Security Group") + self.classes = [c for c in self.classes if c != "disabled"] + return POLICY_CHECK(policy, request, target={}) diff --git a/openstack_dashboard/dashboards/project/access_and_security/tests.py b/openstack_dashboard/dashboards/project/access_and_security/tests.py index 5661933952..40625af178 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/tests.py @@ -26,9 +26,13 @@ from horizon.workflows import views from openstack_dashboard import api from openstack_dashboard.dashboards.project.access_and_security \ import api_access +from openstack_dashboard.dashboards.project.access_and_security \ + .security_groups import tables from openstack_dashboard.test import helpers as test from openstack_dashboard.usage import quotas +INDEX_URL = reverse('horizon:project:access_and_security:index') + class AccessAndSecurityTests(test.TestCase): def setUp(self): @@ -46,6 +50,7 @@ class AccessAndSecurityTests(test.TestCase): sec_groups = self.security_groups.list() floating_ips = self.floating_ips.list() quota_data = self.quota_usages.first() + quota_data['security_groups']['available'] = 10 api.nova.server_list( IsA(http.HttpRequest)) \ @@ -77,8 +82,7 @@ class AccessAndSecurityTests(test.TestCase): self.mox.ReplayAll() - url = reverse('horizon:project:access_and_security:index') - res = self.client.get(url) + res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'project/access_and_security/index.html') self.assertItemsEqual(res.context['keypairs_table'].data, keypairs) @@ -140,3 +144,81 @@ class AccessAndSecurityNeutronProxyTests(AccessAndSecurityTests): def setUp(self): super(AccessAndSecurityNeutronProxyTests, self).setUp() self.floating_ips = self.floating_ips_uuid + + +class SecurityGroupTabTests(test.TestCase): + def setUp(self): + super(SecurityGroupTabTests, self).setUp() + + @test.create_stubs({api.network: ('floating_ip_supported', + 'tenant_floating_ip_list', + 'security_group_list', + 'floating_ip_pools_list',), + api.nova: ('keypair_list', + 'server_list',), + quotas: ('tenant_quota_usages',), + api.base: ('is_service_enabled',)}) + def _test_create_button_disabled_when_quota_exceeded(self, + network_enabled): + keypairs = self.keypairs.list() + floating_ips = self.floating_ips.list() + floating_pools = self.pools.list() + sec_groups = self.security_groups.list() + quota_data = self.quota_usages.first() + quota_data['security_groups']['available'] = 0 + + api.network.floating_ip_supported( + IsA(http.HttpRequest)) \ + .AndReturn(True) + api.network.tenant_floating_ip_list( + IsA(http.HttpRequest)) \ + .AndReturn(floating_ips) + api.network.floating_ip_pools_list( + IsA(http.HttpRequest)) \ + .AndReturn(floating_pools) + api.network.security_group_list( + IsA(http.HttpRequest)) \ + .AndReturn(sec_groups) + api.nova.keypair_list( + IsA(http.HttpRequest)) \ + .AndReturn(keypairs) + api.nova.server_list( + IsA(http.HttpRequest)) \ + .AndReturn([self.servers.list(), False]) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)).MultipleTimes() \ + .AndReturn(quota_data) + + api.base.is_service_enabled( + IsA(http.HttpRequest), 'network').MultipleTimes() \ + .AndReturn(network_enabled) + api.base.is_service_enabled( + IsA(http.HttpRequest), 'ec2').MultipleTimes() \ + .AndReturn(False) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL + + "?tab=access_security_tabs__security_groups_tab") + + security_groups = res.context['security_groups_table'].data + self.assertItemsEqual(security_groups, self.security_groups.list()) + + create_link = tables.CreateGroup() + url = create_link.get_link_url() + classes = list(create_link.get_default_classes())\ + + list(create_link.classes) + link_name = "%s (%s)" % (unicode(create_link.verbose_name), + "Quota exceeded") + expected_string = "" \ + "%s" \ + % (url, link_name, " ".join(classes), link_name) + self.assertContains(res, expected_string, html=True, + msg_prefix="The create button is not disabled") + + def test_create_button_disabled_when_quota_exceeded_neutron_disabled(self): + self._test_create_button_disabled_when_quota_exceeded(False) + + def test_create_button_disabled_when_quota_exceeded_neutron_enabled(self): + self._test_create_button_disabled_when_quota_exceeded(True) diff --git a/openstack_dashboard/dashboards/project/networks/tables.py b/openstack_dashboard/dashboards/project/networks/tables.py index fcd662a2f5..2f436865c4 100644 --- a/openstack_dashboard/dashboards/project/networks/tables.py +++ b/openstack_dashboard/dashboards/project/networks/tables.py @@ -23,6 +23,8 @@ from horizon import tables from openstack_dashboard import api from openstack_dashboard import policy +from openstack_dashboard.usage import quotas + LOG = logging.getLogger(__name__) @@ -70,6 +72,18 @@ class CreateNetwork(tables.LinkAction): icon = "plus" policy_rules = (("network", "create_network"),) + def allowed(self, request, datum=None): + usages = quotas.tenant_quota_usages(request) + if usages['networks']['available'] <= 0: + if "disabled" not in self.classes: + self.classes = [c for c in self.classes] + ["disabled"] + self.verbose_name = _("Create Network (Quota exceeded)") + else: + self.verbose_name = _("Create Network") + self.classes = [c for c in self.classes if c != "disabled"] + + return True + class EditNetwork(policy.PolicyTargetMixin, CheckNetworkEditable, tables.LinkAction): diff --git a/openstack_dashboard/dashboards/project/networks/tests.py b/openstack_dashboard/dashboards/project/networks/tests.py index 802f5df5fd..492c1cbfbc 100644 --- a/openstack_dashboard/dashboards/project/networks/tests.py +++ b/openstack_dashboard/dashboards/project/networks/tests.py @@ -21,10 +21,10 @@ from horizon.workflows import views from mox import IsA # noqa from openstack_dashboard import api -from openstack_dashboard.test import helpers as test - +from openstack_dashboard.dashboards.project.networks import tables from openstack_dashboard.dashboards.project.networks import workflows - +from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas INDEX_URL = reverse('horizon:project:networks:index') @@ -95,8 +95,11 @@ def _str_host_routes(host_routes): class NetworkTests(test.TestCase): - @test.create_stubs({api.neutron: ('network_list',)}) + @test.create_stubs({api.neutron: ('network_list',), + quotas: ('tenant_quota_usages',)}) def test_index(self): + quota_data = self.quota_usages.first() + quota_data['networks']['available'] = 5 api.neutron.network_list( IsA(http.HttpRequest), tenant_id=self.tenant.id, @@ -104,21 +107,29 @@ class NetworkTests(test.TestCase): api.neutron.network_list( IsA(http.HttpRequest), shared=True).AndReturn([]) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(INDEX_URL) - self.assertTemplateUsed(res, 'project/networks/index.html') networks = res.context['networks_table'].data self.assertItemsEqual(networks, self.networks.list()) - @test.create_stubs({api.neutron: ('network_list',)}) + @test.create_stubs({api.neutron: ('network_list',), + quotas: ('tenant_quota_usages',)}) def test_index_network_list_exception(self): + quota_data = self.quota_usages.first() + quota_data['networks']['available'] = 5 api.neutron.network_list( IsA(http.HttpRequest), tenant_id=self.tenant.id, - shared=False).AndRaise(self.exceptions.neutron) + shared=False).MultipleTimes().AndRaise(self.exceptions.neutron) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -1741,3 +1752,44 @@ class NetworkPortTests(test.TestCase): redir_url = reverse('horizon:project:networks:detail', args=[port.network_id]) self.assertRedirectsNoFollow(res, redir_url) + + +class NetworkViewTests(test.TestCase): + + @test.create_stubs({api.neutron: ('network_list',), + quotas: ('tenant_quota_usages',)}) + def test_create_button_disabled_when_quota_exceeded(self): + quota_data = self.quota_usages.first() + quota_data['networks']['available'] = 0 + + api.neutron.network_list( + IsA(http.HttpRequest), + tenant_id=self.tenant.id, + shared=False).AndReturn(self.networks.list()) + api.neutron.network_list( + IsA(http.HttpRequest), + shared=True).AndReturn([]) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'project/networks/index.html') + + networks = res.context['networks_table'].data + self.assertItemsEqual(networks, self.networks.list()) + + create_link = tables.CreateNetwork() + url = create_link.get_link_url() + classes = list(create_link.get_default_classes())\ + + list(create_link.classes) + link_name = "%s (%s)" % (unicode(create_link.verbose_name), + "Quota exceeded") + expected_string = "" \ + "%s" \ + % (url, link_name, " ".join(classes), link_name) + self.assertContains(res, expected_string, html=True, + msg_prefix="The create button is not disabled") diff --git a/openstack_dashboard/dashboards/project/routers/tables.py b/openstack_dashboard/dashboards/project/routers/tables.py index 7924f06001..a0ca486197 100644 --- a/openstack_dashboard/dashboards/project/routers/tables.py +++ b/openstack_dashboard/dashboards/project/routers/tables.py @@ -23,8 +23,11 @@ from neutronclient.common import exceptions as q_ext from horizon import exceptions from horizon import messages from horizon import tables + from openstack_dashboard import api from openstack_dashboard import policy +from openstack_dashboard.usage import quotas + LOG = logging.getLogger(__name__) @@ -77,6 +80,18 @@ class CreateRouter(tables.LinkAction): icon = "plus" policy_rules = (("network", "create_router"),) + def allowed(self, request, datum=None): + usages = quotas.tenant_quota_usages(request) + if usages['routers']['available'] <= 0: + if "disabled" not in self.classes: + self.classes = [c for c in self.classes] + ["disabled"] + self.verbose_name = _("Create Router (Quota exceeded)") + else: + self.verbose_name = _("Create Router") + self.classes = [c for c in self.classes if c != "disabled"] + + return True + class EditRouter(policy.PolicyTargetMixin, tables.LinkAction): name = "update" diff --git a/openstack_dashboard/dashboards/project/routers/tests.py b/openstack_dashboard/dashboards/project/routers/tests.py index 5706de0a3a..2b6a6fcf56 100644 --- a/openstack_dashboard/dashboards/project/routers/tests.py +++ b/openstack_dashboard/dashboards/project/routers/tests.py @@ -21,7 +21,9 @@ from mox import IsA # noqa from openstack_dashboard import api from openstack_dashboard.dashboards.project.routers.extensions.routerrules\ import rulemanager +from openstack_dashboard.dashboards.project.routers import tables from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas class RouterTests(test.TestCase): @@ -45,12 +47,18 @@ class RouterTests(test.TestCase): api.neutron.network_get(IsA(http.HttpRequest), ext_net_id, expand_subnet=False).AndReturn(ext_net) - @test.create_stubs({api.neutron: ('router_list', 'network_list')}) + @test.create_stubs({api.neutron: ('router_list', 'network_list'), + quotas: ('tenant_quota_usages',)}) def test_index(self): + quota_data = self.quota_usages.first() + quota_data['routers']['available'] = 5 api.neutron.router_list( IsA(http.HttpRequest), tenant_id=self.tenant.id, search_opts=None).AndReturn(self.routers.list()) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) self._mock_external_network_list() self.mox.ReplayAll() @@ -60,12 +68,18 @@ class RouterTests(test.TestCase): routers = res.context['table'].data self.assertItemsEqual(routers, self.routers.list()) - @test.create_stubs({api.neutron: ('router_list', 'network_list')}) + @test.create_stubs({api.neutron: ('router_list', 'network_list'), + quotas: ('tenant_quota_usages',)}) def test_index_router_list_exception(self): + quota_data = self.quota_usages.first() + quota_data['routers']['available'] = 5 api.neutron.router_list( IsA(http.HttpRequest), tenant_id=self.tenant.id, - search_opts=None).AndRaise(self.exceptions.neutron) + search_opts=None).MultipleTimes().AndRaise(self.exceptions.neutron) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) self._mock_external_network_list() self.mox.ReplayAll() @@ -75,13 +89,19 @@ class RouterTests(test.TestCase): self.assertEqual(len(res.context['table'].data), 0) self.assertMessageCount(res, error=1) - @test.create_stubs({api.neutron: ('router_list', 'network_list')}) + @test.create_stubs({api.neutron: ('router_list', 'network_list'), + quotas: ('tenant_quota_usages',)}) def test_set_external_network_empty(self): router = self.routers.first() + quota_data = self.quota_usages.first() + quota_data['routers']['available'] = 5 api.neutron.router_list( IsA(http.HttpRequest), tenant_id=self.tenant.id, - search_opts=None).AndReturn([router]) + search_opts=None).MultipleTimes().AndReturn([router]) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) self._mock_external_network_list(alter_ids=True) self.mox.ReplayAll() @@ -679,3 +699,53 @@ class RouterRuleTests(test.TestCase): url = reverse(self.DETAIL_PATH, args=[pre_router.id]) res = self.client.post(url, form_data) self.assertNoFormErrors(res) + + +class RouterViewTests(test.TestCase): + DASHBOARD = 'project' + INDEX_URL = reverse('horizon:%s:routers:index' % DASHBOARD) + + def _mock_external_network_list(self, alter_ids=False): + search_opts = {'router:external': True} + ext_nets = [n for n in self.networks.list() if n['router:external']] + if alter_ids: + for ext_net in ext_nets: + ext_net.id += 'some extra garbage' + api.neutron.network_list( + IsA(http.HttpRequest), + **search_opts).AndReturn(ext_nets) + + @test.create_stubs({api.neutron: ('router_list', 'network_list'), + quotas: ('tenant_quota_usages',)}) + def test_create_button_disabled_when_quota_exceeded(self): + quota_data = self.quota_usages.first() + quota_data['routers']['available'] = 0 + api.neutron.router_list( + IsA(http.HttpRequest), + tenant_id=self.tenant.id, + search_opts=None).AndReturn(self.routers.list()) + quotas.tenant_quota_usages( + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(quota_data) + + self._mock_external_network_list() + self.mox.ReplayAll() + + res = self.client.get(self.INDEX_URL) + self.assertTemplateUsed(res, 'project/routers/index.html') + + routers = res.context['Routers_table'].data + self.assertItemsEqual(routers, self.routers.list()) + + create_link = tables.CreateRouter() + url = create_link.get_link_url() + classes = list(create_link.get_default_classes())\ + + list(create_link.classes) + link_name = "%s (%s)" % (unicode(create_link.verbose_name), + "Quota exceeded") + expected_string = "" \ + "%s" \ + % (url, link_name, " ".join(classes), link_name) + self.assertContains(res, expected_string, html=True, + msg_prefix="The create button is not disabled") diff --git a/openstack_dashboard/usage/quotas.py b/openstack_dashboard/usage/quotas.py index 9bd67807c1..5edc1837ea 100644 --- a/openstack_dashboard/usage/quotas.py +++ b/openstack_dashboard/usage/quotas.py @@ -167,18 +167,48 @@ def get_tenant_quota_data(request, disabled_quotas=None, tenant_id=None): # TODO(jpichon): There is no API to get the default system quotas # in Neutron (cf. LP#1204956), so for now handle tenant quotas here. # This should be handled in _get_quota_data() eventually. - if disabled_quotas and 'floating_ips' in disabled_quotas: + if not disabled_quotas: + return qs + + # Check if neutron is enabled by looking for network and router + if 'network' and 'router' not in disabled_quotas: + tenant_id = tenant_id or request.user.tenant_id + neutron_quotas = neutron.tenant_quota_get(request, tenant_id) + if 'floating_ips' in disabled_quotas: # Neutron with quota extension disabled if 'floatingip' in disabled_quotas: qs.add(base.QuotaSet({'floating_ips': -1})) # Neutron with quota extension enabled else: - tenant_id = tenant_id or request.user.tenant_id - neutron_quotas = neutron.tenant_quota_get(request, tenant_id) # Rename floatingip to floating_ips since that's how it's # expected in some places (e.g. Security & Access' Floating IPs) fips_quota = neutron_quotas.get('floatingip').limit qs.add(base.QuotaSet({'floating_ips': fips_quota})) + if 'security_groups' in disabled_quotas: + if 'security_group' in disabled_quotas: + qs.add(base.QuotaSet({'security_groups': -1})) + # Neutron with quota extension enabled + else: + # Rename security_group to security_groups since that's how it's + # expected in some places (e.g. Security & Access' Security Groups) + sec_quota = neutron_quotas.get('security_group').limit + qs.add(base.QuotaSet({'security_groups': sec_quota})) + if 'network' in disabled_quotas: + for item in qs.items: + if item.name == 'networks': + qs.items.remove(item) + break + else: + net_quota = neutron_quotas.get('network').limit + qs.add(base.QuotaSet({'networks': net_quota})) + if 'router' in disabled_quotas: + for item in qs.items: + if item.name == 'routers': + qs.items.remove(item) + break + else: + router_quota = neutron_quotas.get('router').limit + qs.add(base.QuotaSet({'routers': router_quota})) return qs @@ -247,6 +277,21 @@ def tenant_quota_usages(request): usages.tally('instances', len(instances)) usages.tally('floating_ips', len(floating_ips)) + if 'security_group' not in disabled_quotas: + security_groups = [] + security_groups = network.security_group_list(request) + usages.tally('security_groups', len(security_groups)) + + if 'network' not in disabled_quotas: + networks = [] + networks = neutron.network_list(request, shared=False) + usages.tally('networks', len(networks)) + + if 'router' not in disabled_quotas: + routers = [] + routers = neutron.router_list(request) + usages.tally('routers', len(routers)) + if 'volumes' not in disabled_quotas: volumes = cinder.volume_list(request) snapshots = cinder.volume_snapshot_list(request)