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']))