diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 579d18c5d5..211b9ca8da 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -17,6 +17,7 @@ import logging from django.conf import settings from django.core import urlresolvers +from django.http import HttpResponse from django import shortcuts from django import template from django.template.defaultfilters import title @@ -390,6 +391,36 @@ class LaunchLink(tables.LinkAction): kwargs['preempt'] = True super(LaunchLink, self).__init__(attrs, **kwargs) + def allowed(self, request, datum): + try: + limits = api.nova.tenant_absolute_limits(request, reserved=True) + + instances_available = limits['maxTotalInstances'] \ + - limits['totalInstancesUsed'] + cores_available = limits['maxTotalCores'] \ + - limits['totalCoresUsed'] + ram_available = limits['maxTotalRAMSize'] - limits['totalRAMUsed'] + + if instances_available <= 0 or cores_available <= 0 \ + or ram_available <= 0: + if "disabled" not in self.classes: + self.classes = [c for c in self.classes] + ['disabled'] + self.verbose_name = string_concat(self.verbose_name, ' ', + _("(Quota exceeded)")) + else: + self.verbose_name = _("Launch Instance") + classes = [c for c in self.classes if c != "disabled"] + self.classes = classes + except Exception: + LOG.exception("Failed to retrieve quota information") + # If we can't get the quota information, leave it to the + # API to check when launching + return True # The action should always be displayed + + def single(self, table, request, object_id=None): + self.allowed(request, None) + return HttpResponse(self.render(is_table_action=True)) + class LaunchLinkNG(LaunchLink): name = "launch-ng" diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 45b33a1019..9b57ef6b1b 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -149,6 +149,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported( @@ -182,6 +184,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): .AndReturn(images) api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndRaise(self.exceptions.nova) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) self.mox.ReplayAll() @@ -217,6 +221,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): .AndRaise(self.exceptions.nova) api.glance.image_list_detailed(IgnoreArg()) \ .AndReturn((self.images.list(), False, False)) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported( @@ -261,6 +267,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported( @@ -1444,6 +1452,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported( @@ -3339,6 +3349,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(limits) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported( @@ -3358,6 +3370,54 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): self.assertEqual((('compute', 'os_compute_api:servers:create'),), launch_action.policy_rules) + @helpers.create_stubs({ + api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', + 'extension_supported', 'is_feature_available',), + api.glance: ('image_list_detailed',), + api.neutron: ('floating_ip_simple_associate_supported', + 'floating_ip_supported',), + api.network: ('servers_update_addresses',), + }) + def test_launch_button_disabled_when_quota_exceeded(self): + servers = self.servers.list() + limits = self.limits['absolute'] + limits['totalInstancesUsed'] = limits['maxTotalInstances'] + + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) + api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(self.flavors.list()) + api.glance.image_list_detailed(IgnoreArg()) \ + .AndReturn((self.images.list(), False, False)) + search_opts = {'marker': None, 'paginate': True} + api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ + .AndReturn([servers, False]) + api.network.servers_update_addresses(IsA(http.HttpRequest), servers) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(limits) + api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) + api.neutron.floating_ip_simple_associate_supported( + IsA(http.HttpRequest)).MultipleTimes().AndReturn(True) + + self.mox.ReplayAll() + + tables.LaunchLink() + res = self.client.get(INDEX_URL) + + launch_action = self.getAndAssertTableAction( + res, 'instances', 'launch-ng') + + self.assertIn('disabled', launch_action.classes, + 'The launch button should be disabled') + self.assertEqual('Launch Instance (Quota exceeded)', + six.text_type(launch_action.verbose_name)) + @helpers.create_stubs({api.glance: ('image_list_detailed',), api.neutron: ('network_list', 'port_list_with_trunk_types', @@ -3492,6 +3552,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported( @@ -3969,6 +4031,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.network.servers_update_addresses( IsA(http.HttpRequest), servers[page_size:]) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ + .MultipleTimes().AndReturn(self.limits['absolute']) api.neutron.floating_ip_supported(IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.floating_ip_simple_associate_supported(