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:
parent
e8b3360038
commit
32c863e844
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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']))
|
||||
|
|
Loading…
Reference in New Issue