Skip to content

Commit

Permalink
Improve consistency of quota checking in forms
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ifarkas committed Aug 9, 2013
1 parent e8b3360 commit 32c863e
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 28 deletions.
14 changes: 0 additions & 14 deletions horizon/static/horizon/js/horizon.instances.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions horizon/static/horizon/js/horizon.tables.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
Expand Down
Expand Up @@ -26,6 +26,7 @@
from horizon import messages

from openstack_dashboard import api
from openstack_dashboard.usage import quotas


class FloatingIpAllocate(forms.SelfHandlingForm):
Expand All @@ -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,
Expand Down
Expand Up @@ -20,13 +20,15 @@
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
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


Expand All @@ -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"
Expand Down
Expand Up @@ -39,6 +39,6 @@ <h3>{% trans "Project Quotas" %}</h3>
{% 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 %}
Expand Up @@ -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):
Expand All @@ -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])
Expand All @@ -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()

Expand Down
25 changes: 21 additions & 4 deletions openstack_dashboard/dashboards/project/instances/tests.py
Expand Up @@ -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 \
Expand Down Expand Up @@ -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()
Expand All @@ -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())
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -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())
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -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())
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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())
Expand Down Expand Up @@ -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()

Expand Down
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 0 additions & 4 deletions openstack_dashboard/dashboards/project/volumes/forms.py
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions openstack_dashboard/dashboards/project/volumes/tables.py
Expand Up @@ -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__)
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 32c863e

Please sign in to comment.