Improve consistency of quota checking in forms

Add quota based validations to floating_ips and instance forms. Unify
the implementation of disabled/enabled button for instance and
floating_ips table. Add disabled/enalbed button to volumes table.

Change-Id: I488e86e467369f35244bdcd34ecaaee32e56ba2d
fixes: bug #1070899
This commit is contained in:
Imre Farkas 2013-05-30 14:52:00 +02:00
parent e8b3360038
commit 32c863e844
12 changed files with 145 additions and 28 deletions

View File

@ -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

View File

@ -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);
});

View File

@ -26,6 +26,7 @@ from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.usage import quotas
class FloatingIpAllocate(forms.SelfHandlingForm):
@ -38,6 +39,14 @@ class FloatingIpAllocate(forms.SelfHandlingForm):
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,

View File

@ -20,6 +20,7 @@ import logging
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 messages
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"

View File

@ -39,6 +39,6 @@
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right {% if usages.floating_ips.used >= usages.floating_ips.quota %}disabled" type="button"{% else %}" type="submit"{% endif %} value="{% trans "Allocate IP" %}" />
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Allocate IP" %}" />
<a href="{% url 'horizon:project:access_and_security:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -29,6 +29,7 @@ from horizon.workflows.views import WorkflowView
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 @@ class AccessAndSecurityTests(test.TestCase):
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 @@ class AccessAndSecurityTests(test.TestCase):
.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()

View File

@ -33,6 +33,7 @@ from horizon.workflows.views import WorkflowView
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 @@ class InstanceTests(test.TestCase):
'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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
'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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
'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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
'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 @@ class InstanceTests(test.TestCase):
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 @@ class InstanceTests(test.TestCase):
instance_count=IsA(int),
admin_pass='password') \
.AndRaise(self.exceptions.keystone)
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(quota_usages)
self.mox.ReplayAll()

View File

@ -24,6 +24,8 @@ import logging
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 horizon import workflows
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 @@ class SetInstanceDetailsAction(workflows.Action):
'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):

View File

@ -161,10 +161,6 @@ class CreateForm(forms.SelfHandlingForm):
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

View File

@ -21,13 +21,16 @@ from django.core.urlresolvers import reverse
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"

View File

@ -27,7 +27,9 @@ from mox import IsA
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 @@ class VolumeViewTests(test.TestCase):
@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 @@ class VolumeViewTests(test.TestCase):
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 @@ class VolumeViewTests(test.TestCase):
@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 @@ class VolumeViewTests(test.TestCase):
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 @@ class VolumeViewTests(test.TestCase):
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 @@ class VolumeViewTests(test.TestCase):
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 = "<a href='%s' title='%s' class='%s disabled' "\
"id='volumes__action_create'>%s</a>" \
% (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):

View File

@ -348,7 +348,11 @@ def data(TEST):
'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']))