Skip to content

Commit

Permalink
Volume Progress Bar & Fixes For Quota
Browse files Browse the repository at this point in the history
When a volume creation exceed the allocation quota
a vague error message was returned that offered the
user no guidance as to what went wrong. This has
been fixed.

Fixes Bug #1012883

This change also abstracts the Quota javascript to
allow it to be used anywhere on the site for any
progress bars that are to be shown in the future.

Implements blueprint progress-bar-javascript

On top of this I have renamed all "can_haz" filters
in the code, as they are really sort of embarassing.

Lastly, I have added the ability to append JS
events to the window load when a modal is loaded as
a static page, OR when it is loaded as an AJAX
modal.

Change-Id: I4b0cefa160cafbbd07d4b0981f62febaed051871
  • Loading branch information
John Postlethwait committed Jun 27, 2012
1 parent 0ffa674 commit 406cb5d
Show file tree
Hide file tree
Showing 24 changed files with 521 additions and 197 deletions.
18 changes: 13 additions & 5 deletions horizon/api/nova.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,18 +407,19 @@ def usage_list(request, start, end):
def tenant_quota_usages(request):
"""
Builds a dictionary of current usage against quota for the current
tenant.
project.
"""
# TODO(tres): Make this capture floating_ips and volumes as well.
instances = server_list(request)
floating_ips = tenant_floating_ip_list(request)
quotas = tenant_quota_get(request, request.user.tenant_id)
flavors = dict([(f.id, f) for f in flavor_list(request)])
volumes = volume_list(request)

usages = {'instances': {'flavor_fields': [], 'used': len(instances)},
'cores': {'flavor_fields': ['vcpus'], 'used': 0},
'gigabytes': {'used': 0,
'flavor_fields': ['disk',
'OS-FLV-EXT-DATA:ephemeral']},
'gigabytes': {'used': sum([int(v.size) for v in volumes]),
'flavor_fields': []},
'volumes': {'used': len(volumes), 'flavor_fields': []},
'ram': {'flavor_fields': ['ram'], 'used': 0},
'floating_ips': {'flavor_fields': [], 'used': len(floating_ips)}}

Expand All @@ -427,11 +428,18 @@ def tenant_quota_usages(request):
for flavor_field in usages[usage]['flavor_fields']:
usages[usage]['used'] += getattr(
flavors[instance.flavor['id']], flavor_field, 0)

usages[usage]['quota'] = getattr(quotas, usage)

if usages[usage]['quota'] is None:
usages[usage]['quota'] = float("inf")
usages[usage]['available'] = float("inf")
elif type(usages[usage]['quota']) is str:
usages[usage]['quota'] = int(usages[usage]['quota'])
else:
if type(usages[usage]['used']) is str:
usages[usage]['used'] = int(usages[usage]['used'])

usages[usage]['available'] = usages[usage]['quota'] - \
usages[usage]['used']

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,3 @@
{% block dash_main %}
{% include 'nova/images_and_snapshots/snapshots/_create.html' %}
{% endblock %}


Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n horizon %}
{% load i18n horizon humanize %}

<p>{% blocktrans %}Specify the details for launching an instance.{% endblocktrans %}</p>
<p>{% blocktrans %}The chart below shows the resources used by this project in relation to the project's quotas.{% endblocktrans %}</p>
Expand All @@ -17,55 +17,37 @@ <h4>{% trans "Flavor Details" %}</h4>

<div class="quota-dynamic">
<h4>{% trans "Project Quotas" %}</h4>
<div class="quota_title">
<strong>{% trans "Instance Count" %} <span>({{ usages.instances.used }})</span></strong>
<p>{{ usages.instances.available|quota }}</p>
<div class="quota_title clearfix">
<strong>{% trans "Number of Instances" %} <span>({{ usages.instances.used|intcomma }})</span></strong>
<p>{{ usages.instances.available|quota|intcomma }}</p>
</div>
<div class="clearfix"></div>
<div id="quota_instances" class="quota_bar">{% horizon_progress_bar usages.instances.used usages.instances.quota %}</div>

<div class="quota_title">
<strong>{% trans "VCPUs" %} <span>({{ usages.cores.used }})</span></strong>
<p>{{ usages.cores.available|quota }}</p>
<div id="quota_instances" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.instances.quota }}" data-quota-used="{{ usages.instances.used }}">
{% horizon_progress_bar usages.instances.used usages.instances.quota %}
</div>
<div class="clearfix"></div>
<div id="quota_cores" class="quota_bar">{% horizon_progress_bar usages.cores.used usages.cores.quota %}</div>

<div class="quota_title">
<strong>{% trans "Disk" %} <span>({{ usages.gigabytes.used }} {% trans "GB" %})</span></strong>
<p>{{ usages.gigabytes.available|quota:"GB" }}</p>
<div class="quota_title clearfix">
<strong>{% trans "Number of VCPUs" %} <span>({{ usages.cores.used|intcomma }})</span></strong>
<p>{{ usages.cores.available|quota|intcomma }}</p>
</div>
<div id="quota_vcpus" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.cores.quota }}" data-quota-used="{{ usages.cores.used }}">
{% horizon_progress_bar usages.cores.used usages.cores.quota %}
</div>
<div class="clearfix"></div>
<div id="quota_disk" class="quota_bar">{% horizon_progress_bar usages.gigabytes.used usages.gigabytes.quota %}</div>

<div class="quota_title">
<strong>{% trans "Memory" %} <span>({{ usages.ram.used }} {% trans "MB" %})</span></strong>
<p>{{ usages.ram.available|quota:"MB" }}</p>
<div class="quota_title clearfix">
<strong>{% trans "Total Memory" %} <span>({{ usages.ram.used|intcomma }} {% trans "MB" %})</span></strong>
<p>{{ usages.ram.available|quota:"MB"|intcomma }}</p>
</div>
<div id="quota_ram" data-progress-indicator-flavor data-quota-limit="{{ usages.ram.quota }}" data-quota-used="{{ usages.ram.used }}" class="quota_bar">
{% horizon_progress_bar usages.ram.used usages.ram.quota %}
</div>
<div class="clearfix"></div>
<div id="quota_ram" class="quota_bar">{% horizon_progress_bar usages.ram.used usages.ram.quota %}</div>
</div>

<script type="text/javascript" charset="utf-8">
var horizon_flavors = {{ flavors|safe }};
var horizon_usages = {{ usages_json|safe }};

// FIXME(gabriel): move this function into a horizon primitive when we have
// one constructed at the head of the document. :-/
(function () {
function fire_change(el) {
if ("fireEvent" in el) {
el.fireEvent("onchange");
}
else
{
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", true, true);
el.dispatchEvent(evt);
}
}
fire_change(document.getElementById('id_flavor'));
fire_change(document.getElementById('id_source_type'));
fire_change(document.getElementById('id_volume_type'));
})();
if(typeof horizon !== 'undefined') {
horizon.Quota.initWithFlavors({{ flavors|safe }});
} else {
addHorizonLoadEvent(function() {
horizon.Quota.initWithFlavors({{ flavors|safe }});
});
}
</script>
Original file line number Diff line number Diff line change
@@ -1,22 +1,3 @@
{% load i18n horizon %}

<p>{% blocktrans %}An instance can be launched with varying types of attached storage. You may select from those options here.{% endblocktrans %}</p>

<script type="text/javascript" charset="utf-8">
// FIXME(gabriel): move this function into a horizon primitive when we have
// one constructed at the head of the document. :-/
(function () {
function fire_change(el) {
if ("fireEvent" in el) {
el.fireEvent("onchange");
}
else
{
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", true, true);
el.dispatchEvent(evt);
}
}
fire_change(document.getElementById('id_volume_type'));
})();
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load i18n horizon humanize %}

{% block form_id %}{% endblock %}
{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create %}{% endblock %}
Expand All @@ -8,15 +8,47 @@
{% block modal-header %}{% trans "Create Volume" %}{% endblock %}

{% block modal-body %}
<div class="left">
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
</div>

<div class="right quota-dynamic">
<h3>{% trans "Description" %}:</h3>

<p>{% trans "Volumes are block devices that can be attached to instances." %}</p>
</div>

<h3>{% trans "Volume Quotas" %}</h3>

<div class="quota_title clearfix">
<strong>{% trans "Total Gigabytes" %} <span>({{ usages.gigabytes.used|intcomma }} GB)</span></strong>
<p>{{ usages.gigabytes.available|quota:"GB"|intcomma }}</p>
</div>

<div id="quota_size" data-progress-indicator-for="id_size" data-quota-limit="{{ usages.gigabytes.quota }}" data-quota-used="{{ usages.gigabytes.used }}" class="quota_bar">
{% horizon_progress_bar usages.gigabytes.used usages.gigabytes.quota %}
</div>

<div class="quota_title clearfix">
<strong>{% trans "Number of Volumes" %} <span>({{ usages.volumes.used|intcomma }})</span></strong>
<p>{{ usages.volumes.available|quota|intcomma }}</p>
</div>

<div id="quota_volumes" data-progress-indicator-step-by="1" data-quota-limit="{{ usages.volumes.quota }}" data-quota-used="{{ usages.volumes.used }}" class="quota_bar">
{% horizon_progress_bar usages.volumes.used usages.volumes.quota %}
</div>
</div>

<script type="text/javascript" charset="utf-8">
if(typeof horizon !== 'undefined') {
horizon.Quota.init();
} else {
addHorizonLoadEvent(function() {
horizon.Quota.init();
});
}
</script>
{% endblock %}

{% block modal-footer %}
Expand Down
30 changes: 28 additions & 2 deletions horizon/dashboards/nova/instances_and_volumes/volumes/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from django import shortcuts
from django.contrib import messages
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _

from horizon import api
Expand All @@ -26,13 +27,38 @@ class CreateForm(forms.SelfHandlingForm):

def handle(self, request, data):
try:
# FIXME(johnp): Nova (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 Nova to try and create.
usages = api.tenant_quota_usages(request)

if type(data['size']) is str:
data['size'] = int(data['size'])

if usages['gigabytes']['available'] < data['size']:
error_message = _('A volume of %iGB cannot be created as you'
' only have %iGB of your quota available.'
% (data['size'],
usages['gigabytes']['available'],))
raise ValidationError(error_message)
elif usages['volumes']['available'] <= 0:
error_message = _('You are already using all of your available'
' volumes.')
raise ValidationError(error_message)

api.volume_create(request, data['size'], data['name'],
data['description'])
message = 'Creating volume "%s"' % data['name']

messages.info(request, message)
except ValidationError, e:
return self.api_error(e.messages[0])
except:
exceptions.handle(request,
_("Unable to create volume."))
exceptions.handle(request, ignore=True)

return self.api_error(_("Unable to create volume."))

return shortcuts.redirect("horizon:nova:instances_and_volumes:index")


Expand Down
63 changes: 63 additions & 0 deletions horizon/dashboards/nova/instances_and_volumes/volumes/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,69 @@


class VolumeViewTests(test.TestCase):
@test.create_stubs({api: ('tenant_quota_usages', 'volume_create',)})
def test_create_volume(self):
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
formData = {'name': u'A Volume I Am Making',
'description': u'This is a volume I am making for a test.',
'method': u'CreateForm',
'size': 50}

api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.volume_create(IsA(http.HttpRequest),
formData['size'],
formData['name'],
formData['description'])

self.mox.ReplayAll()

url = reverse('horizon:nova:instances_and_volumes:volumes:create')
res = self.client.post(url, formData)

redirect_url = reverse('horizon:nova:instances_and_volumes:index')
self.assertRedirectsNoFollow(res, redirect_url)

@test.create_stubs({api: ('tenant_quota_usages',)})
def test_create_volume_gb_used_over_alloted_quota(self):
usage = {'gigabytes': {'available': 100, 'used': 20}}
formData = {'name': u'This Volume Is Huge!',
'description': u'This is a volume that is just too big!',
'method': u'CreateForm',
'size': 5000}

api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)

self.mox.ReplayAll()

url = reverse('horizon:nova:instances_and_volumes:volumes:create')
res = self.client.post(url, formData)

expected_error = [u'A volume of 5000GB cannot be created as you only'
' have 100GB of your quota available.']
self.assertEqual(res.context['form'].errors['__all__'], expected_error)

@test.create_stubs({api: ('tenant_quota_usages',)})
def test_create_volume_number_over_alloted_quota(self):
usage = {'gigabytes': {'available': 100, 'used': 20},
'volumes': {'available': 0}}
formData = {'name': u'Too Many...',
'description': u'We have no volumes left!',
'method': u'CreateForm',
'size': 10}

api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)

self.mox.ReplayAll()

url = reverse('horizon:nova:instances_and_volumes:volumes:create')
res = self.client.post(url, formData)

expected_error = [u'You are already using all of your available'
' volumes.']
self.assertEqual(res.context['form'].errors['__all__'], expected_error)

@test.create_stubs({api: ('volume_get',), api.nova: ('server_list',)})
def test_edit_attachments(self):
volume = self.volumes.first()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ class CreateView(forms.ModalFormView):
form_class = CreateForm
template_name = 'nova/instances_and_volumes/volumes/create.html'

def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
try:
context['usages'] = api.tenant_quota_usages(self.request)
except:
exceptions.handle(self.request)

return context


class CreateSnapshotView(forms.ModalFormView):
form_class = CreateSnapshotForm
Expand Down
3 changes: 2 additions & 1 deletion horizon/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def get_traceback_frame_variables(self, request, tb_frame):
sensitive_variables = None
while current_frame is not None:
if (current_frame.f_code.co_name == 'sensitive_variables_wrapper'
and 'sensitive_variables_wrapper' in current_frame.f_locals):
and 'sensitive_variables_wrapper'
in current_frame.f_locals):
# The sensitive_variables decorator was used, so we take note
# of the sensitive variables' names.
wrapper = current_frame.f_locals['sensitive_variables_wrapper']
Expand Down

0 comments on commit 406cb5d

Please sign in to comment.