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:
parent
699926413c
commit
142a6e97fe
@ -20,8 +20,6 @@
|
||||
|
||||
from django import forms
|
||||
from django.forms.forms import NON_FIELD_ERRORS
|
||||
from django.utils import dates
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class SelfHandlingMixin(object):
|
||||
@ -49,13 +47,11 @@ class SelfHandlingForm(SelfHandlingMixin, forms.Form):
|
||||
|
||||
|
||||
class DateForm(forms.Form):
|
||||
""" A simple form for selecting a start date. """
|
||||
month = forms.ChoiceField(choices=dates.MONTHS.items())
|
||||
year = forms.ChoiceField()
|
||||
""" A simple form for selecting a range of time. """
|
||||
start = forms.DateField(input_formats=("%Y-%m-%d",))
|
||||
end = forms.DateField(input_formats=("%Y-%m-%d",))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DateForm, self).__init__(*args, **kwargs)
|
||||
years = [(year, year) for year
|
||||
in xrange(2009, timezone.now().year + 1)]
|
||||
years.reverse()
|
||||
self.fields['year'].choices = years
|
||||
self.fields['start'].widget.attrs['data-date-format'] = "yyyy-mm-dd"
|
||||
self.fields['end'].widget.attrs['data-date-format'] = "yyyy-mm-dd"
|
||||
|
@ -10,6 +10,7 @@ horizon.forms = {
|
||||
$volSize.val($option.data("size"));
|
||||
});
|
||||
},
|
||||
|
||||
handle_image_source: function() {
|
||||
$("div.table_wrapper, #modal_wrapper").on("change", "select#id_image_source", function(evt) {
|
||||
var $option = $(this).find("option:selected");
|
||||
@ -19,6 +20,27 @@ horizon.forms = {
|
||||
var $volSize = $form.find('input#id_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_image_source();
|
||||
horizon.forms.datepicker();
|
||||
|
||||
// Bind event handlers to confirm dangerous actions.
|
||||
$("body").on("click", "form button.btn-danger", function (evt) {
|
||||
|
@ -2,18 +2,19 @@
|
||||
|
||||
<div class="usage_info_wrapper">
|
||||
<form action="" method="get" id="date_form" class="form-horizontal">
|
||||
<h3>{% trans "Select a month to query its usage" %}: </h3>
|
||||
<div class="form-row">
|
||||
{{ form.month }}
|
||||
{{ form.year }}
|
||||
<h3>{% trans "Select a period of time to query its usage" %}: </h3>
|
||||
<div class="datepicker">
|
||||
<span>{% trans "From" %}: {{ form.start }} </span>
|
||||
<span>{% trans "To" %}: {{ form.end }} </span>
|
||||
<button class="btn btn-small" type="submit">{% trans "Submit" %}</button>
|
||||
<small>{% trans "The date should be in YYYY-mm-dd format." %}</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p id="activity">
|
||||
<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 "This Month'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 VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span>
|
||||
<span><strong>{% trans "This Period's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from django.utils import timezone
|
||||
|
||||
from mox import Func
|
||||
from mox import IsA
|
||||
|
||||
from horizon.templatetags.sizeformat import mbformat
|
||||
@ -47,10 +46,14 @@ class UsageViewTests(test.BaseAdminViewTests):
|
||||
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn([self.tenants.list(), False])
|
||||
api.nova.usage_list(IsA(http.HttpRequest),
|
||||
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
|
||||
Func(usage.almost_now)) \
|
||||
.AndReturn([usage_obj])
|
||||
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
datetime.datetime(now.year,
|
||||
now.month,
|
||||
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)) \
|
||||
.AndReturn(self.limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(reverse('horizon:admin:overview:index'))
|
||||
@ -78,9 +81,13 @@ class UsageViewTests(test.BaseAdminViewTests):
|
||||
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn([self.tenants.list(), False])
|
||||
api.nova.usage_list(IsA(http.HttpRequest),
|
||||
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
|
||||
Func(usage.almost_now)) \
|
||||
.AndReturn(usage_obj)
|
||||
datetime.datetime(now.year,
|
||||
now.month,
|
||||
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))\
|
||||
.AndReturn(self.limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
|
@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from django.utils import timezone
|
||||
|
||||
from mox import Func
|
||||
from mox import IsA
|
||||
|
||||
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, 'tenant_absolute_limits')
|
||||
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
|
||||
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
|
||||
Func(usage.almost_now)) \
|
||||
datetime.datetime(now.year,
|
||||
now.month,
|
||||
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))\
|
||||
.AndReturn(self.limits['absolute'])
|
||||
@ -60,8 +63,12 @@ class UsageViewTests(test.TestCase):
|
||||
self.mox.StubOutWithMock(api.nova, 'usage_get')
|
||||
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
|
||||
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
|
||||
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
|
||||
Func(usage.almost_now)) \
|
||||
datetime.datetime(now.year,
|
||||
now.month,
|
||||
now.day, 0, 0, 0, 0),
|
||||
datetime.datetime(now.year,
|
||||
now.month,
|
||||
now.day, 23, 59, 59, 0)) \
|
||||
.AndRaise(exc)
|
||||
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.limits['absolute'])
|
||||
@ -78,14 +85,13 @@ class UsageViewTests(test.TestCase):
|
||||
usage_obj = api.nova.NovaUsage(self.usages.first())
|
||||
self.mox.StubOutWithMock(api.nova, 'usage_get')
|
||||
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),
|
||||
self.tenant.id,
|
||||
timestamp,
|
||||
Func(usage.almost_now)) \
|
||||
.AndReturn(usage_obj)
|
||||
start, end).AndReturn(usage_obj)
|
||||
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.limits['absolute'])
|
||||
.AndReturn(self.limits['absolute'])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(reverse('horizon:project:overview:index') +
|
||||
@ -97,14 +103,14 @@ class UsageViewTests(test.TestCase):
|
||||
now = timezone.now()
|
||||
self.mox.StubOutWithMock(api.nova, 'usage_get')
|
||||
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),
|
||||
self.tenant.id,
|
||||
timestamp,
|
||||
Func(usage.almost_now)) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
start, end).AndRaise(self.exceptions.nova)
|
||||
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.limits['absolute'])
|
||||
.AndReturn(self.limits['absolute'])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
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())
|
||||
self.mox.StubOutWithMock(api.nova, 'usage_get')
|
||||
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),
|
||||
self.tenant.id,
|
||||
timestamp,
|
||||
Func(usage.almost_now)) \
|
||||
.AndReturn(usage_obj)
|
||||
start, end).AndReturn(usage_obj)
|
||||
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.AndRaise(self.exceptions.nova)
|
||||
self.mox.ReplayAll()
|
||||
@ -135,14 +140,13 @@ class UsageViewTests(test.TestCase):
|
||||
usage_obj = api.nova.NovaUsage(self.usages.first())
|
||||
self.mox.StubOutWithMock(api.nova, 'usage_get')
|
||||
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),
|
||||
self.tenant.id,
|
||||
timestamp,
|
||||
Func(usage.almost_now)) \
|
||||
.AndReturn(usage_obj)
|
||||
start, end).AndReturn(usage_obj)
|
||||
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.limits['absolute'])
|
||||
.AndReturn(self.limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:project:overview:index'))
|
||||
|
@ -6,6 +6,7 @@
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*/
|
||||
|
||||
.datepicker {
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -179,4 +180,4 @@
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
@ -724,6 +724,19 @@ form label {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.datepicker input {
|
||||
width: 65px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.datepicker .btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
form.horizontal .form-field {
|
||||
float: left;
|
||||
}
|
||||
@ -2102,4 +2115,4 @@ div.network {
|
||||
#info_box h3 {font-size:9pt;line-height:20px;}
|
||||
#info_box p {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;}
|
@ -14,7 +14,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from openstack_dashboard.usage.base import almost_now
|
||||
from openstack_dashboard.usage.base import BaseUsage
|
||||
from openstack_dashboard.usage.base import GlobalUsage
|
||||
from openstack_dashboard.usage.base import ProjectUsage
|
||||
@ -26,7 +25,6 @@ from openstack_dashboard.usage.views import UsageView
|
||||
assert BaseUsage
|
||||
assert ProjectUsage
|
||||
assert GlobalUsage
|
||||
assert almost_now
|
||||
assert UsageView
|
||||
assert BaseUsageTable
|
||||
assert ProjectUsageTable
|
||||
|
@ -1,8 +1,8 @@
|
||||
from __future__ import division
|
||||
|
||||
from calendar import monthrange
|
||||
from csv import DictWriter
|
||||
from csv import writer
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from StringIO import StringIO
|
||||
@ -24,12 +24,6 @@ from openstack_dashboard.usage import quotas
|
||||
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):
|
||||
show_terminated = False
|
||||
|
||||
@ -46,21 +40,14 @@ class BaseUsage(object):
|
||||
return timezone.now()
|
||||
|
||||
@staticmethod
|
||||
def get_start(year, month, day=1):
|
||||
def get_start(year, month, day):
|
||||
start = datetime.datetime(year, month, day, 0, 0, 0)
|
||||
return timezone.make_aware(start, timezone.utc)
|
||||
|
||||
@staticmethod
|
||||
def get_end(year, month, day=1):
|
||||
days_in_month = monthrange(year, month)[1]
|
||||
period = datetime.timedelta(days=days_in_month)
|
||||
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_end(year, month, day):
|
||||
end = datetime.datetime(year, month, day, 23, 59, 59)
|
||||
return timezone.make_aware(end, timezone.utc)
|
||||
|
||||
def get_instances(self):
|
||||
instance_list = []
|
||||
@ -69,24 +56,50 @@ class BaseUsage(object):
|
||||
|
||||
def get_date_range(self):
|
||||
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()
|
||||
if form.is_valid():
|
||||
args = (int(form.cleaned_data['year']),
|
||||
int(form.cleaned_data['month']))
|
||||
self.start = self.get_start(*args)
|
||||
self.end = self.get_end(*args)
|
||||
start = form.cleaned_data['start']
|
||||
end = form.cleaned_data['end']
|
||||
args_start = (start.year,
|
||||
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
|
||||
|
||||
def get_form(self):
|
||||
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
|
||||
self.form = forms.DateForm(self.request.GET)
|
||||
else:
|
||||
# non-bound form
|
||||
self.form = forms.DateForm(initial={'month': self.today.month,
|
||||
'year': self.today.year})
|
||||
init = self.init_form()
|
||||
self.form = forms.DateForm(initial={'start': init[0],
|
||||
'end': init[1]})
|
||||
return self.form
|
||||
|
||||
def get_limits(self):
|
||||
@ -100,7 +113,7 @@ class BaseUsage(object):
|
||||
raise NotImplementedError("You must define a get_usage method.")
|
||||
|
||||
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
|
||||
# to naive UTC just for this last step.
|
||||
start = timezone.make_naive(start, timezone.utc)
|
||||
@ -110,10 +123,14 @@ class BaseUsage(object):
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve usage information.'))
|
||||
else:
|
||||
messages.info(self.request,
|
||||
_("You are viewing data for the future, "
|
||||
"which may or may not exist."))
|
||||
elif end < start:
|
||||
messages.error(self.request,
|
||||
_("Invalid time period. The end date should be "
|
||||
"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:
|
||||
project_summary = project_usage.get_summary()
|
||||
@ -130,11 +147,13 @@ class BaseUsage(object):
|
||||
|
||||
def csv_link(self):
|
||||
form = self.get_form()
|
||||
data = {}
|
||||
if hasattr(form, "cleaned_data"):
|
||||
data = form.cleaned_data
|
||||
else:
|
||||
data = {"month": self.today.month, "year": self.today.year}
|
||||
return "?month=%s&year=%s&format=csv" % (data['month'], data['year'])
|
||||
if not ('start' in data and 'end' in data):
|
||||
data = {"start": self.today.date(), "end": self.today.date()}
|
||||
return "?start=%s&end=%s&format=csv" % (data['start'],
|
||||
data['end'])
|
||||
|
||||
|
||||
class GlobalUsage(BaseUsage):
|
||||
|
Loading…
Reference in New Issue
Block a user