Query a period of time for usage summary

Allows the user to pick a range of time
to query services usage data.

Fallback mode: the user will have to input
dates by hand if JavaScript is disabled.

Change-Id: I4cf4d4fd56fd450743b9d624dc066d6ed74df881
Fixes: bug #1102448
This commit is contained in:
Victoria Martinez de la Cruz 2013-04-02 14:23:43 -03:00 committed by Victoria Martínez de la Cruz
parent 699926413c
commit 142a6e97fe
9 changed files with 147 additions and 85 deletions

View File

@ -20,8 +20,6 @@
from django import forms from django import forms
from django.forms.forms import NON_FIELD_ERRORS from django.forms.forms import NON_FIELD_ERRORS
from django.utils import dates
from django.utils import timezone
class SelfHandlingMixin(object): class SelfHandlingMixin(object):
@ -49,13 +47,11 @@ class SelfHandlingForm(SelfHandlingMixin, forms.Form):
class DateForm(forms.Form): class DateForm(forms.Form):
""" A simple form for selecting a start date. """ """ A simple form for selecting a range of time. """
month = forms.ChoiceField(choices=dates.MONTHS.items()) start = forms.DateField(input_formats=("%Y-%m-%d",))
year = forms.ChoiceField() end = forms.DateField(input_formats=("%Y-%m-%d",))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DateForm, self).__init__(*args, **kwargs) super(DateForm, self).__init__(*args, **kwargs)
years = [(year, year) for year self.fields['start'].widget.attrs['data-date-format'] = "yyyy-mm-dd"
in xrange(2009, timezone.now().year + 1)] self.fields['end'].widget.attrs['data-date-format'] = "yyyy-mm-dd"
years.reverse()
self.fields['year'].choices = years

View File

@ -10,6 +10,7 @@ horizon.forms = {
$volSize.val($option.data("size")); $volSize.val($option.data("size"));
}); });
}, },
handle_image_source: function() { handle_image_source: function() {
$("div.table_wrapper, #modal_wrapper").on("change", "select#id_image_source", function(evt) { $("div.table_wrapper, #modal_wrapper").on("change", "select#id_image_source", function(evt) {
var $option = $(this).find("option:selected"); var $option = $(this).find("option:selected");
@ -19,6 +20,27 @@ horizon.forms = {
var $volSize = $form.find('input#id_size'); var $volSize = $form.find('input#id_size');
$volSize.val($option.data("size")); $volSize.val($option.data("size"));
}); });
},
datepicker: function() {
var startDate = $('input#id_start').datepicker()
.on('changeDate', function(ev) {
if (ev.date.valueOf() > endDate.date.valueOf()) {
var newDate = new Date(ev.date)
newDate.setDate(newDate.getDate() + 1);
endDate.setValue(newDate);
$('input#id_end')[0].focus();
}
startDate.hide();
}).data('datepicker');
var endDate = $('input#id_end').datepicker({
onRender: function(date) {
return date.valueOf() < startDate.date.valueOf() ? 'disabled' : '';
}
}).on('changeDate', function(ev) {
endDate.hide();
}).data('datepicker');
} }
}; };
@ -78,6 +100,7 @@ horizon.addInitFunction(function () {
horizon.forms.handle_snapshot_source(); horizon.forms.handle_snapshot_source();
horizon.forms.handle_image_source(); horizon.forms.handle_image_source();
horizon.forms.datepicker();
// Bind event handlers to confirm dangerous actions. // Bind event handlers to confirm dangerous actions.
$("body").on("click", "form button.btn-danger", function (evt) { $("body").on("click", "form button.btn-danger", function (evt) {

View File

@ -2,18 +2,19 @@
<div class="usage_info_wrapper"> <div class="usage_info_wrapper">
<form action="" method="get" id="date_form" class="form-horizontal"> <form action="" method="get" id="date_form" class="form-horizontal">
<h3>{% trans "Select a month to query its usage" %}: </h3> <h3>{% trans "Select a period of time to query its usage" %}: </h3>
<div class="form-row"> <div class="datepicker">
{{ form.month }} <span>{% trans "From" %}: {{ form.start }} </span>
{{ form.year }} <span>{% trans "To" %}: {{ form.end }} </span>
<button class="btn btn-small" type="submit">{% trans "Submit" %}</button> <button class="btn btn-small" type="submit">{% trans "Submit" %}</button>
<small>{% trans "The date should be in YYYY-mm-dd format." %}</small>
</div> </div>
</form> </form>
<p id="activity"> <p id="activity">
<span><strong>{% trans "Active Instances" %}:</strong> {{ usage.summary.instances|default:'-' }}</span> <span><strong>{% trans "Active Instances" %}:</strong> {{ usage.summary.instances|default:'-' }}</span>
<span><strong>{% trans "Active RAM" %}:</strong> {{ usage.summary.memory_mb|mbformat|default:'-' }}</span> <span><strong>{% trans "Active RAM" %}:</strong> {{ usage.summary.memory_mb|mbformat|default:'-' }}</span>
<span><strong>{% trans "This Month's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span> <span><strong>{% trans "This Period's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span>
<span><strong>{% trans "This Month's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span> <span><strong>{% trans "This Period's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span>
</p> </p>
</div> </div>

View File

@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse
from django import http from django import http
from django.utils import timezone from django.utils import timezone
from mox import Func
from mox import IsA from mox import IsA
from horizon.templatetags.sizeformat import mbformat from horizon.templatetags.sizeformat import mbformat
@ -47,10 +46,14 @@ class UsageViewTests(test.BaseAdminViewTests):
api.keystone.tenant_list(IsA(http.HttpRequest)) \ api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False]) .AndReturn([self.tenants.list(), False])
api.nova.usage_list(IsA(http.HttpRequest), api.nova.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0), datetime.datetime(now.year,
Func(usage.almost_now)) \ now.month,
.AndReturn([usage_obj]) now.day, 0, 0, 0, 0),
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndReturn([usage_obj])
api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:overview:index')) res = self.client.get(reverse('horizon:admin:overview:index'))
@ -78,9 +81,13 @@ class UsageViewTests(test.BaseAdminViewTests):
api.keystone.tenant_list(IsA(http.HttpRequest)) \ api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False]) .AndReturn([self.tenants.list(), False])
api.nova.usage_list(IsA(http.HttpRequest), api.nova.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0), datetime.datetime(now.year,
Func(usage.almost_now)) \ now.month,
.AndReturn(usage_obj) now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
self.mox.ReplayAll() self.mox.ReplayAll()

View File

@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse
from django import http from django import http
from django.utils import timezone from django.utils import timezone
from mox import Func
from mox import IsA from mox import IsA
from openstack_dashboard import api from openstack_dashboard import api
@ -42,8 +41,12 @@ class UsageViewTests(test.TestCase):
self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0), datetime.datetime(now.year,
Func(usage.almost_now)) \ now.month,
now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndReturn(usage_obj) .AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
@ -60,8 +63,12 @@ class UsageViewTests(test.TestCase):
self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0), datetime.datetime(now.year,
Func(usage.almost_now)) \ now.month,
now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndRaise(exc) .AndRaise(exc)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
@ -78,14 +85,13 @@ class UsageViewTests(test.TestCase):
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
timestamp, start, end).AndReturn(usage_obj)
Func(usage.almost_now)) \
.AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index') + res = self.client.get(reverse('horizon:project:overview:index') +
@ -97,14 +103,14 @@ class UsageViewTests(test.TestCase):
now = timezone.now() now = timezone.now()
self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
timestamp, start, end).AndRaise(self.exceptions.nova)
Func(usage.almost_now)) \
.AndRaise(self.exceptions.nova)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index')) res = self.client.get(reverse('horizon:project:overview:index'))
@ -116,12 +122,11 @@ class UsageViewTests(test.TestCase):
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
timestamp, start, end).AndReturn(usage_obj)
Func(usage.almost_now)) \
.AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.nova) .AndRaise(self.exceptions.nova)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -135,14 +140,13 @@ class UsageViewTests(test.TestCase):
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
timestamp, start, end).AndReturn(usage_obj)
Func(usage.almost_now)) \
.AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute']) .AndReturn(self.limits['absolute'])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index')) res = self.client.get(reverse('horizon:project:overview:index'))

View File

@ -6,6 +6,7 @@
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
*/ */
.datepicker { .datepicker {
top: 0; top: 0;
left: 0; left: 0;
@ -179,4 +180,4 @@
cursor: pointer; cursor: pointer;
width: 16px; width: 16px;
height: 16px; height: 16px;
} }

View File

@ -724,6 +724,19 @@ form label {
width: 150px; width: 150px;
} }
.datepicker {
margin-top: 10px;
}
.datepicker input {
width: 65px;
margin-right: 10px;
}
.datepicker .btn {
margin-right: 10px;
}
form.horizontal .form-field { form.horizontal .form-field {
float: left; float: left;
} }
@ -2102,4 +2115,4 @@ div.network {
#info_box h3 {font-size:9pt;line-height:20px;} #info_box h3 {font-size:9pt;line-height:20px;}
#info_box p {margin:0;font-size:9pt;line-height:14px;} #info_box p {margin:0;font-size:9pt;line-height:14px;}
#info_box a {margin:0;font-size:9pt;line-height:14px;} #info_box a {margin:0;font-size:9pt;line-height:14px;}
#info_box .error {color:darkred;} #info_box .error {color:darkred;}

View File

@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from openstack_dashboard.usage.base import almost_now
from openstack_dashboard.usage.base import BaseUsage from openstack_dashboard.usage.base import BaseUsage
from openstack_dashboard.usage.base import GlobalUsage from openstack_dashboard.usage.base import GlobalUsage
from openstack_dashboard.usage.base import ProjectUsage from openstack_dashboard.usage.base import ProjectUsage
@ -26,7 +25,6 @@ from openstack_dashboard.usage.views import UsageView
assert BaseUsage assert BaseUsage
assert ProjectUsage assert ProjectUsage
assert GlobalUsage assert GlobalUsage
assert almost_now
assert UsageView assert UsageView
assert BaseUsageTable assert BaseUsageTable
assert ProjectUsageTable assert ProjectUsageTable

View File

@ -1,8 +1,8 @@
from __future__ import division from __future__ import division
from calendar import monthrange
from csv import DictWriter from csv import DictWriter
from csv import writer from csv import writer
import datetime import datetime
import logging import logging
from StringIO import StringIO from StringIO import StringIO
@ -24,12 +24,6 @@ from openstack_dashboard.usage import quotas
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def almost_now(input_time):
now = timezone.make_naive(timezone.now(), timezone.utc)
# If we're less than a minute apart we'll assume success here.
return now - input_time < datetime.timedelta(seconds=30)
class BaseUsage(object): class BaseUsage(object):
show_terminated = False show_terminated = False
@ -46,21 +40,14 @@ class BaseUsage(object):
return timezone.now() return timezone.now()
@staticmethod @staticmethod
def get_start(year, month, day=1): def get_start(year, month, day):
start = datetime.datetime(year, month, day, 0, 0, 0) start = datetime.datetime(year, month, day, 0, 0, 0)
return timezone.make_aware(start, timezone.utc) return timezone.make_aware(start, timezone.utc)
@staticmethod @staticmethod
def get_end(year, month, day=1): def get_end(year, month, day):
days_in_month = monthrange(year, month)[1] end = datetime.datetime(year, month, day, 23, 59, 59)
period = datetime.timedelta(days=days_in_month) return timezone.make_aware(end, timezone.utc)
end = BaseUsage.get_start(year, month, day) + period
# End our calculation at midnight of the given day.
date_end = datetime.datetime.combine(end, datetime.time(0, 0, 0))
date_end = timezone.make_aware(date_end, timezone.utc)
if date_end > timezone.now():
date_end = timezone.now()
return date_end
def get_instances(self): def get_instances(self):
instance_list = [] instance_list = []
@ -69,24 +56,50 @@ class BaseUsage(object):
def get_date_range(self): def get_date_range(self):
if not hasattr(self, "start") or not hasattr(self, "end"): if not hasattr(self, "start") or not hasattr(self, "end"):
args = (self.today.year, self.today.month) args_start = args_end = (self.today.year,
self.today.month,
self.today.day)
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
args = (int(form.cleaned_data['year']), start = form.cleaned_data['start']
int(form.cleaned_data['month'])) end = form.cleaned_data['end']
self.start = self.get_start(*args) args_start = (start.year,
self.end = self.get_end(*args) start.month,
start.day)
args_end = (end.year,
end.month,
end.day)
elif form.is_bound:
messages.error(self.request,
_("Invalid date format: "
"Using today as default."))
self.start = self.get_start(*args_start)
self.end = self.get_end(*args_end)
return self.start, self.end
def init_form(self):
today = datetime.date.today()
first = datetime.date(day=1, month=today.month, year=today.year)
if today.day in range(5):
self.end = first - datetime.timedelta(days=1)
self.start = datetime.date(day=1,
month=self.end.month,
year=self.end.year)
else:
self.end = today
self.start = first
return self.start, self.end return self.start, self.end
def get_form(self): def get_form(self):
if not hasattr(self, 'form'): if not hasattr(self, 'form'):
if any(key in ['month', 'year'] for key in self.request.GET): if any(key in ['start', 'end'] for key in self.request.GET):
# bound form # bound form
self.form = forms.DateForm(self.request.GET) self.form = forms.DateForm(self.request.GET)
else: else:
# non-bound form # non-bound form
self.form = forms.DateForm(initial={'month': self.today.month, init = self.init_form()
'year': self.today.year}) self.form = forms.DateForm(initial={'start': init[0],
'end': init[1]})
return self.form return self.form
def get_limits(self): def get_limits(self):
@ -100,7 +113,7 @@ class BaseUsage(object):
raise NotImplementedError("You must define a get_usage method.") raise NotImplementedError("You must define a get_usage method.")
def summarize(self, start, end): def summarize(self, start, end):
if start <= end <= self.today: if start <= end and start <= self.today:
# The API can't handle timezone aware datetime, so convert back # The API can't handle timezone aware datetime, so convert back
# to naive UTC just for this last step. # to naive UTC just for this last step.
start = timezone.make_naive(start, timezone.utc) start = timezone.make_naive(start, timezone.utc)
@ -110,10 +123,14 @@ class BaseUsage(object):
except: except:
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve usage information.')) _('Unable to retrieve usage information.'))
else: elif end < start:
messages.info(self.request, messages.error(self.request,
_("You are viewing data for the future, " _("Invalid time period. The end date should be "
"which may or may not exist.")) "more recent than the start date."))
elif start > self.today:
messages.error(self.request,
_("Invalid time period. You are requesting "
"data from the future which may not exist."))
for project_usage in self.usage_list: for project_usage in self.usage_list:
project_summary = project_usage.get_summary() project_summary = project_usage.get_summary()
@ -130,11 +147,13 @@ class BaseUsage(object):
def csv_link(self): def csv_link(self):
form = self.get_form() form = self.get_form()
data = {}
if hasattr(form, "cleaned_data"): if hasattr(form, "cleaned_data"):
data = form.cleaned_data data = form.cleaned_data
else: if not ('start' in data and 'end' in data):
data = {"month": self.today.month, "year": self.today.year} data = {"start": self.today.date(), "end": self.today.date()}
return "?month=%s&year=%s&format=csv" % (data['month'], data['year']) return "?start=%s&end=%s&format=csv" % (data['start'],
data['end'])
class GlobalUsage(BaseUsage): class GlobalUsage(BaseUsage):