diff --git a/horizon/static/horizon/js/horizon.instances.js b/horizon/static/horizon/js/horizon.instances.js index f23abc204a1..98a7d95c122 100644 --- a/horizon/static/horizon/js/horizon.instances.js +++ b/horizon/static/horizon/js/horizon.instances.js @@ -33,17 +33,6 @@ horizon.instances = { }); }, - disable_launch_button: function() { - var launch_button = "#instances__action_launch"; - - $(launch_button).click(function(e) { - if ($(launch_button).hasClass("disabled")) { - e.preventDefault(); - e.stopPropagation(); - } - }); - }, - /* * Gets the html select element associated with a given * network id for network_id. @@ -167,9 +156,6 @@ horizon.addInitFunction(function () { evt.preventDefault(); }); - // Disable the launch button if required - horizon.instances.disable_launch_button(); - /* Launch instance workflow */ // Handle field toggles for the Launch Instance source type field diff --git a/horizon/static/horizon/js/horizon.tables.js b/horizon/static/horizon/js/horizon.tables.js index 468213c1081..970300cd411 100644 --- a/horizon/static/horizon/js/horizon.tables.js +++ b/horizon/static/horizon/js/horizon.tables.js @@ -196,6 +196,13 @@ $.tablesorter.addParser({ type: 'numeric' }); +horizon.datatables.disable_buttons = function() { + $("table .table_actions").on("click", ".btn.disabled", function(event){ + event.preventDefault(); + event.stopPropagation(); + }); +}; + horizon.datatables.update_footer_count = function (el, modifier) { var $el = $(el), $browser, $footer, row_count, footer_text_template, footer_text; @@ -347,6 +354,7 @@ horizon.datatables.set_table_fixed_filter = function (parent) { horizon.addInitFunction(function() { horizon.datatables.validate_button(); + horizon.datatables.disable_buttons(); $('table.datatable').each(function (idx, el) { horizon.datatables.update_footer_count($(el), 0); }); diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py index 40918f7877d..a4cf690d7e4 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py @@ -26,6 +26,7 @@ from horizon import messages from openstack_dashboard import api +from openstack_dashboard.usage import quotas class FloatingIpAllocate(forms.SelfHandlingForm): @@ -38,6 +39,14 @@ def __init__(self, *args, **kwargs): def handle(self, request, data): try: + # Prevent allocating more IP than the quota allows + usages = quotas.tenant_quota_usages(request) + if usages['floating_ips']['available'] <= 0: + error_message = _('You are already using all of your available' + ' floating IPs.') + self.api_error(error_message) + return False + fip = api.network.tenant_floating_ip_allocate(request, pool=data['pool']) messages.success(request, diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py index 751ce681050..4137b655f41 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py @@ -20,6 +20,7 @@ from django.core import urlresolvers from django import shortcuts from django.utils.http import urlencode +from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ from horizon import exceptions @@ -27,6 +28,7 @@ from horizon import tables from openstack_dashboard import api +from openstack_dashboard.usage import quotas from openstack_dashboard.utils.filters import get_int_or_uuid @@ -42,6 +44,19 @@ class AllocateIP(tables.LinkAction): def single(self, data_table, request, *args): return shortcuts.redirect('horizon:project:access_and_security:index') + def allowed(self, request, volume=None): + usages = quotas.tenant_quota_usages(request) + if usages['floating_ips']['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 = _("Allocate IP To Project") + classes = [c for c in self.classes if c != "disabled"] + self.classes = classes + return True + class ReleaseIPs(tables.BatchAction): name = "release" diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/_allocate.html b/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/_allocate.html index b8d9d4fd1e2..fae32807d23 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/_allocate.html +++ b/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/_allocate.html @@ -39,6 +39,6 @@

{% trans "Project Quotas" %}

{% endblock %} {% block modal-footer %} - + {% trans "Cancel" %} {% endblock %} diff --git a/openstack_dashboard/dashboards/project/access_and_security/tests.py b/openstack_dashboard/dashboards/project/access_and_security/tests.py index 5f2ec562c30..3f6e7bbb701 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/tests.py @@ -29,6 +29,7 @@ from openstack_dashboard import api from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas class AccessAndSecurityTests(test.TestCase): @@ -39,10 +40,12 @@ def test_index(self): keypairs = self.keypairs.list() sec_groups = self.security_groups.list() floating_ips = self.floating_ips.list() + quota_data = self.quota_usages.first() self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.network, 'security_group_list') self.mox.StubOutWithMock(api.nova, 'keypair_list') self.mox.StubOutWithMock(api.nova, 'server_list') + self.mox.StubOutWithMock(quotas, 'tenant_quota_usages') api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn([self.servers.list(), False]) @@ -51,6 +54,8 @@ def test_index(self): .AndReturn(floating_ips) api.network.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(sec_groups) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes()\ + .AndReturn(quota_data) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 0e51d94a1c3..bc5bf69c0e4 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -33,6 +33,7 @@ from openstack_dashboard import api from openstack_dashboard.api import cinder from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas from openstack_dashboard.dashboards.project.instances.tables import LaunchLink from openstack_dashboard.dashboards.project.instances.tabs \ @@ -876,7 +877,8 @@ def test_launch_instance_get(self): 'server_create',), api.network: ('security_group_list',), cinder: ('volume_list', - 'volume_snapshot_list',)}) + 'volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) def test_launch_instance_post(self): flavor = self.flavors.first() image = self.images.first() @@ -886,6 +888,7 @@ def test_launch_instance_post(self): avail_zone = self.availability_zones.first() customization_script = 'user data' nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] + quota_usages = self.quota_usages.first() api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -925,6 +928,8 @@ def test_launch_instance_post(self): availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'') + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ + .AndReturn(quota_usages) self.mox.ReplayAll() @@ -1035,7 +1040,8 @@ def test_launch_instance_post_boot_from_volume_with_image(self): 'server_create',), api.network: ('security_group_list',), cinder: ('volume_list', - 'volume_snapshot_list',)}) + 'volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) def test_launch_instance_post_boot_from_volume(self): flavor = self.flavors.first() keypair = self.keypairs.first() @@ -1048,6 +1054,7 @@ def test_launch_instance_post_boot_from_volume(self): volume_choice = "%s:vol" % volume.id block_device_mapping = {device_name: u"%s::0" % volume_choice} nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] + quota_usages = self.quota_usages.first() api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -1087,6 +1094,8 @@ def test_launch_instance_post_boot_from_volume(self): availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'') + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ + .AndReturn(quota_usages) self.mox.ReplayAll() @@ -1118,7 +1127,8 @@ def test_launch_instance_post_boot_from_volume(self): 'availability_zone_list',), api.network: ('security_group_list',), cinder: ('volume_list', - 'volume_snapshot_list',)}) + 'volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) def test_launch_instance_post_no_images_available_boot_from_volume(self): flavor = self.flavors.first() keypair = self.keypairs.first() @@ -1131,6 +1141,7 @@ def test_launch_instance_post_no_images_available_boot_from_volume(self): volume_choice = "%s:vol" % volume.id block_device_mapping = {device_name: u"%s::0" % volume_choice} nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] + quota_usages = self.quota_usages.first() api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -1158,6 +1169,8 @@ def test_launch_instance_post_no_images_available_boot_from_volume(self): cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ + .AndReturn(quota_usages) api.nova.server_create(IsA(http.HttpRequest), server.name, @@ -1324,7 +1337,8 @@ def test_launch_flavorlist_error(self): 'server_create',), api.network: ('security_group_list',), cinder: ('volume_list', - 'volume_snapshot_list',)}) + 'volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) def test_launch_form_keystone_exception(self): flavor = self.flavors.first() image = self.images.first() @@ -1334,6 +1348,7 @@ def test_launch_form_keystone_exception(self): avail_zone = self.availability_zones.first() customization_script = 'userData' nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] + quota_usages = self.quota_usages.first() cinder.volume_snapshot_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) @@ -1372,6 +1387,8 @@ def test_launch_form_keystone_exception(self): instance_count=IsA(int), admin_pass='password') \ .AndRaise(self.exceptions.keystone) + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ + .AndReturn(quota_usages) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index d95c0391a53..53acfaeadcb 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -24,6 +24,8 @@ from django.conf import settings from django.utils.text import normalize_newlines from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + from django.views.decorators.debug import sensitive_variables from horizon import exceptions @@ -33,6 +35,7 @@ from openstack_dashboard import api from openstack_dashboard.api import cinder +from openstack_dashboard.usage import quotas from openstack_dashboard.dashboards.project.images_and_snapshots.utils \ import get_available_images @@ -232,6 +235,21 @@ def clean(self): 'images and instance snapshots.') raise forms.ValidationError(msg) + # Prevent launching more instances than the quota allows + usages = quotas.tenant_quota_usages(self.request) + available_count = usages['instances']['available'] + if available_count < count: + error_message = ungettext_lazy('The requested instance ' + 'cannot be launched as you only have %(avail)i ' + 'of your quota available.', + 'The requested %(req)i instances ' + 'cannot be launched as you only have %(avail)i ' + 'of your quota available.', + count) + params = {'req': count, + 'avail': available_count} + raise forms.ValidationError(error_message % params) + return cleaned_data def _init_images_cache(self): diff --git a/openstack_dashboard/dashboards/project/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/forms.py index 5a5c0d76404..05d50fcb4d4 100644 --- a/openstack_dashboard/dashboards/project/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/forms.py @@ -161,10 +161,6 @@ def __init__(self, request, *args, **kwargs): def handle(self, request, data): try: - # FIXME(johnp): cinderclient currently returns a useless - # error message when the quota is exceeded when trying to create - # a volume, so we need to check for that scenario here before we - # send it off to try and create. usages = cinder.tenant_absolute_limits(self.request) volumes = cinder.volume_list(self.request) total_size = sum([getattr(volume, 'size', 0) for volume diff --git a/openstack_dashboard/dashboards/project/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/tables.py index 7a61ed0c62b..5928215b026 100644 --- a/openstack_dashboard/dashboards/project/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/tables.py @@ -21,13 +21,16 @@ from django.template.defaultfilters import title from django.utils.html import strip_tags from django.utils import safestring +from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ + from horizon import exceptions from horizon import tables from openstack_dashboard import api from openstack_dashboard.api import cinder +from openstack_dashboard.usage import quotas LOG = logging.getLogger(__name__) @@ -63,6 +66,20 @@ class CreateVolume(tables.LinkAction): url = "horizon:project:volumes:create" classes = ("ajax-modal", "btn-create") + def allowed(self, request, volume=None): + usages = quotas.tenant_quota_usages(request) + if usages['gigabytes']['available'] <= 0 or\ + usages['volumes']['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 = _("Create Volume") + classes = [c for c in self.classes if c != "disabled"] + self.classes = classes + return True + class EditAttachments(tables.LinkAction): name = "attachments" diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index c474b4ab50a..1fc51b424de 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -27,7 +27,9 @@ from openstack_dashboard import api from openstack_dashboard.api import cinder +from openstack_dashboard.dashboards.project.volumes.tables import CreateVolume from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas class VolumeViewTests(test.TestCase): @@ -608,12 +610,12 @@ def test_create_volume_cannot_encrypt(self): @test.create_stubs({cinder: ('volume_list', 'volume_delete',), - api.nova: ('server_list',)}) + api.nova: ('server_list',), + quotas: ('tenant_quota_usages',)}) def test_delete_volume(self): volume = self.volumes.first() formData = {'action': 'volumes__delete__%s' % volume.id} - cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn(self.volumes.list()) cinder.volume_delete(IsA(http.HttpRequest), volume.id) @@ -623,6 +625,8 @@ def test_delete_volume(self): AndReturn(self.volumes.list()) api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([self.servers.list(), False]) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes().\ + AndReturn(self.quota_usages.first()) self.mox.ReplayAll() @@ -633,7 +637,8 @@ def test_delete_volume(self): @test.create_stubs({cinder: ('volume_list', 'volume_delete',), - api.nova: ('server_list',)}) + api.nova: ('server_list',), + quotas: ('tenant_quota_usages',)}) def test_delete_volume_error_existing_snapshot(self): volume = self.volumes.first() formData = {'action': @@ -651,6 +656,8 @@ def test_delete_volume_error_existing_snapshot(self): AndReturn(self.volumes.list()) api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn([self.servers.list(), False]) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes().\ + AndReturn(self.quota_usages.first()) self.mox.ReplayAll() @@ -705,7 +712,8 @@ def test_edit_attachments_cannot_set_mount_point(self): settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = PREV @test.create_stubs({cinder: ('volume_get',), - api.nova: ('server_get', 'server_list',)}) + api.nova: ('server_get', 'server_list',), + quotas: ('tenant_quota_usages',)}) def test_edit_attachments_attached_volume(self): servers = [s for s in self.servers.list() if s.tenant_id == self.request.user.tenant_id] @@ -731,6 +739,40 @@ def test_edit_attachments_attached_volume(self): server.id) self.assertEqual(res.status_code, 200) + @test.create_stubs({cinder: ('volume_list',), + api.nova: ('server_list',), + quotas: ('tenant_quota_usages',)}) + def test_create_button_disabled_when_quota_exceeded(self): + quota_usages = self.quota_usages.first() + quota_usages['volumes']['available'] = 0 + + cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\ + .AndReturn(self.volumes.list()) + api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\ + .AndReturn([self.servers.list(), False]) + quotas.tenant_quota_usages(IsA(http.HttpRequest))\ + .MultipleTimes().AndReturn(quota_usages) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:project:volumes:index')) + self.assertTemplateUsed(res, 'project/volumes/index.html') + + volumes = res.context['volumes_table'].data + self.assertItemsEqual(volumes, self.volumes.list()) + + create_link = CreateVolume() + 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") + @test.create_stubs({cinder: ('volume_get',), api.nova: ('server_get',)}) def test_detail_view(self): diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 326683d83d8..7f96dcaf966 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -348,7 +348,11 @@ def get_id(is_uuid): 'ram': {'used': 0, 'quota': 10000}, 'cores': {'used': 0, - 'quota': 20}} + 'quota': 20}, + 'floating_ips': {'used': 0, + 'quota': 10}, + 'volumes': {'used': 0, + 'quota': 10}} quota_usage = QuotaUsage() for k, v in quota_usage_data.items(): quota_usage.add_quota(Quota(k, v['quota']))