Refactor metering dashboard

- check date range for validity
- use django forms
- treat times according to users timezone

Change-Id: I68182ddb4b00d19e54011722ebc770d9aa8e725c
Closes-bug: #1331427
Closes-bug: #1331434
Closes-bug: #1288512
This commit is contained in:
Matthias Runge 2014-10-14 11:26:42 +02:00
parent d471bae2cc
commit ca2e10c3e8
10 changed files with 499 additions and 493 deletions

View File

@ -0,0 +1,68 @@
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from django.forms import ValidationError # noqa
from django.utils.translation import ugettext_lazy as _
from horizon import forms
class UsageReportForm(forms.SelfHandlingForm):
PERIOD_CHOICES = (("1", _("Last day")),
("7", _("Last week")),
(str(datetime.date.today().day), _("Month to date")),
("15", _("Last 15 days")),
("30", _("Last 30 days")),
("365", _("Last year")),
("other", _("Other")),
)
period = forms.ChoiceField(label=_("Period"),
required=True,
choices=PERIOD_CHOICES)
date_from = forms.DateField(label=_("From"), required=False,
widget=forms.TextInput(
attrs={'data-line-chart-command':
'date_picker_change'}))
date_to = forms.DateField(label=_("To"), required=False,
widget=forms.TextInput(
attrs={'data-line-chart-command':
'date_picker_change'}))
def clean_date_from(self):
period = self.cleaned_data['period']
date_from = self.cleaned_data['date_from']
if period == 'other' and date_from is None:
raise ValidationError(_('Must specify start of period'))
return date_from
def clean_date_to(self):
data = super(UsageReportForm, self).clean()
date_from = data.get('date_from')
date_to = data.get('date_to')
period = data.get('period')
if (period == 'other' and date_to is not None
and date_from is not None and date_to < date_from):
raise ValidationError(_("Start must be earlier "
"than end of period."))
else:
return date_to
def handle(self, request, data):
if hasattr(request, 'session'):
request.session['date_from'] = data['date_from']
request.session['date_to'] = data['date_to']
request.session['period'] = data['period']
return data

View File

@ -21,6 +21,42 @@ def show_date(datum):
return datum.split('T')[0] return datum.split('T')[0]
class ModifyUsageReportParameters(tables.LinkAction):
name = "create"
verbose_name = _("Modify Usage Report Parameters")
url = "horizon:admin:metering:create"
classes = ("btn-create",)
icon = "edit"
class CreateCSVUsageReport(tables.LinkAction):
name = "csv"
verbose_name = _("Download CSV Summary")
url = "horizon:admin:metering:csvreport"
classes = ("btn-create",)
icon = "download"
class ReportTable(tables.DataTable):
project = tables.Column('project', verbose_name=_('Project'))
service = tables.Column('service', verbose_name=_('Service'))
meter = tables.Column('meter', verbose_name=_('Meter'))
description = tables.Column('description', verbose_name=_('Description'))
time = tables.Column('time', verbose_name=_('Day'),
filters=[show_date])
value = tables.Column('value', verbose_name=_('Value (Avg)'),
filters=[humanize.intcomma])
def get_object_id(self, obj):
return "%s-%s-%s" % (obj['project'], obj['service'], obj['meter'])
class Meta:
name = 'report_table'
verbose_name = _("Daily Usage Report")
table_actions = (ModifyUsageReportParameters, CreateCSVUsageReport)
multi_select = False
class UsageTable(tables.DataTable): class UsageTable(tables.DataTable):
service = tables.Column('service', verbose_name=_('Service')) service = tables.Column('service', verbose_name=_('Service'))
meter = tables.Column('meter', verbose_name=_('Meter')) meter = tables.Column('meter', verbose_name=_('Meter'))
@ -30,6 +66,10 @@ class UsageTable(tables.DataTable):
value = tables.Column('value', verbose_name=_('Value (Avg)'), value = tables.Column('value', verbose_name=_('Value (Avg)'),
filters=[humanize.intcomma]) filters=[humanize.intcomma])
def __init__(self, request, *args, **kwargs):
super(UsageTable, self).__init__(request, *args, **kwargs)
self.title = getattr(self, 'title', None)
def get_object_id(self, datum): def get_object_id(self, datum):
return datum['time'] + datum['meter'] return datum['time'] + datum['meter']

View File

@ -10,30 +10,27 @@
# 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 django import template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import messages from horizon import messages
from horizon import tabs from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.api import ceilometer from openstack_dashboard.api import ceilometer
from openstack_dashboard.dashboards.admin.metering import \
tables as metering_tables
class GlobalStatsTab(tabs.Tab): from openstack_dashboard.utils import metering
class GlobalStatsTab(tabs.TableTab):
name = _("Stats") name = _("Stats")
slug = "stats" slug = "stats"
template_name = ("admin/metering/stats.html") template_name = ("admin/metering/stats.html")
preload = False preload = False
table_classes = (metering_tables.UsageTable,)
@staticmethod
def _get_flavor_names(request):
try:
flavors = api.nova.flavor_list(request, None)
return [f.name for f in flavors]
except Exception:
return ['m1.tiny', 'm1.small', 'm1.medium',
'm1.large', 'm1.xlarge']
def get_context_data(self, request): def get_context_data(self, request):
meters = ceilometer.Meters(request) meters = ceilometer.Meters(request)
@ -53,17 +50,66 @@ class GlobalStatsTab(tabs.Tab):
return context return context
class DailyReportTab(tabs.Tab): class UsageReportTab(tabs.TableTab):
name = _("Daily Report") name = _("Usage Report")
slug = "daily_report" slug = "usage_report"
template_name = ("admin/metering/daily.html") template_name = "horizon/common/_detail_table.html"
table_classes = (metering_tables.ReportTable,)
def get_context_data(self, request): def get_report_table_data(self):
context = template.RequestContext(request) meters = ceilometer.Meters(self.request)
return context services = {
_('Nova'): meters.list_nova(),
_('Neutron'): meters.list_neutron(),
_('Glance'): meters.list_glance(),
_('Cinder'): meters.list_cinder(),
_('Swift_meters'): meters.list_swift(),
_('Kwapi'): meters.list_kwapi(),
}
report_rows = []
date_options = self.request.session.get('period', 7)
date_from = self.request.session.get('date_from', '')
date_to = self.request.session.get('date_to', '')
try:
date_from, date_to = metering.calc_date_args(date_from,
date_to,
date_options)
except Exception:
exceptions.handle(self.request, _('Dates cannot be recognized.'))
try:
project_aggregates = metering.ProjectAggregatesQuery(self.request,
date_from,
date_to,
3600 * 24)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve project list.'))
for meter in meters._cached_meters.values():
service = None
for name, m_list in services.items():
if meter in m_list:
service = name
break
res, unit = project_aggregates.query(meter.name)
for re in res:
values = re.get_meter(meter.name.replace(".", "_"))
if values:
for value in values:
row = {"name": 'none',
"project": re.id,
"meter": meter.name,
"description": meter.description,
"service": service,
"time": value._apiresource.period_end,
"value": value._apiresource.avg}
report_rows.append(row)
return report_rows
class CeilometerOverviewTabs(tabs.TabGroup): class CeilometerOverviewTabs(tabs.TabGroup):
slug = "ceilometer_overview" slug = "ceilometer_overview"
tabs = (DailyReportTab, GlobalStatsTab, ) tabs = (UsageReportTab, GlobalStatsTab,)
sticky = True sticky = True

View File

@ -0,0 +1,57 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}create_usage_report_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:metering:create' %}{% endblock %}
{% block modal_id %}create_usage_report_modal{% endblock %}
{% block modal-header %}{% trans "Modify Usage Report Parameters" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Select a pre-defined period or specify date." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "View Usage Report" %}" />
<a href="{% url 'horizon:admin:metering:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}
{% block modal-js %}
<script type="text/javascript">
if (typeof $ !== 'undefined') {
show_hide_datepickers();
} else {
addHorizonLoadEvent(function() {
show_hide_datepickers();
});
}
function show_hide_datepickers() {
$("#id_period").change(function(evt) {
// Enhancing behaviour of selectbox, on 'other' value selected, I don't
// want to refresh, but show hide the date fields
if ($(this).find("option:selected").val() === "other"){
evt.stopPropagation();
$("#date_from .controls input, #date_to .controls input").val('');
$("#id_date_from, #id_date_to").parent().parent().show();
} else {
$("#id_date_from, #id_date_to").parent().parent().hide();
}
});
if ($("#id_period").find("option:selected").val() === "other"){
$("#id_date_from, #id_date_to").parent().parent().show();
} else {
$("#id_date_from, #id_date_to").parent().parent().hide();
}
}
</script>
{% endblock %}

View File

@ -1,74 +1,11 @@
{% extends 'base.html' %}
{% load i18n %} {% load i18n %}
<div id="ceilometer-report"> {% block title %}{% trans "Modify Usage Report Parameters" %}{% endblock %}
<form class="form-horizontal" action="{% url 'horizon:admin:metering:report' %}" method="POST">
{% csrf_token %}
<div class="form-group">
<label for="report_date_options" class="control-label col-sm-2">{% trans "Period:" %}</label>
<div class="col-sm-2">
<select data-line-chart-command="select_box_change"
id="report_date_options" name="date_options" class="form-control">
<option value="1">{% trans "Last day" %}</option>
<option value="7" selected="selected">{% trans "Last week" %}</option>
<option value="{% now 'j' %}">{% trans "Month to date" %}</option>
<option value="15">{% trans "Last 15 days" %}</option>
<option value="30">{% trans "Last 30 days" %}</option>
<option value="365">{% trans "Last year" %}</option>
<option value="other">{% trans "Other" %}</option>
</select>
</div>
</div>
<div class="form-group" id="report_date_from">
<label for="date_from" class="control-label col-sm-2">{% trans "From:" %}</label>
<div class="col-sm-2">
<input data-line-chart-command="date_picker_change"
type="text" id="date_from" name="date_from" class="form-control example"/>
</div>
</div>
<div class="form-group" id="report_date_to">
<label for="date_to" class="control-label col-sm-2">{% trans "To:" %}</label>
<div class="col-sm-2">
<input data-line-chart-command="date_picker_change"
type="text" name="date_to" class="form-control example"/>
</div>
</div>
<div class="form-group">
<label for="limit" class="control-label col-sm-2">{% trans "Limit project count:" %}</label>
<div class="col-sm-2">
<input type="text" name="limit" class="form-control example" value="20"/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default btn-sm">{% trans 'Generate Report' %}</button>
</div>
</div>
</form>
</div>
<script type="text/javascript">
if (typeof $ !== 'undefined') {
show_hide_datepickers();
} else {
addHorizonLoadEvent(function() {
show_hide_datepickers();
});
}
function show_hide_datepickers() { {% block page_header %}
$("#report_date_options").change(function(evt) { {% include "horizon/common/_page_header.html" with title=_("Modify Usage Report Parameters") %}
// Enhancing behaviour of selectbox, on 'other' value selected, I don't {% endblock page_header %}
// want to refresh, but show hide the date fields
if ($(this).find("option:selected").val() == "other"){ {% block main %}
evt.stopPropagation(); {% include "admin/metering/_daily.html" %}
$("#date_from input, #date_to input").val(''); {% endblock %}
$("#report_date_from, #report_date_to").show();
} else {
$("#report_date_from, #report_date_to").hide();
}
});
if ($("#report_date_options").find("option:selected").val() == "other"){
$("#report_date_from, #report_date_to").show();
} else {
$("#report_date_from, #report_date_to").hide();
}
}
</script>

View File

@ -1,24 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans 'Usage Report' %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Daily Usage Report Per Project")%}
{% endblock page_header %}
{% block main %}
<div class="table_actions">
<a href="{{ csv_url }}"><button class="btn btn-default btn-sm" ><span class="glyphicon glyphicon-download"></span>{% trans 'Download CSV Summary' %}</button></a>
<a href="{% url 'horizon:admin:metering:index' %}"><button class="btn btn-default btn-sm">{% trans 'Back' %}</button></a>
</div>
{% for table in tables %}
{{ table.render }}
{% endfor %}
{% if not tables %}
<p>
{% trans 'No data available' %}
</p>
{% endif %}
{% endblock %}

View File

@ -10,41 +10,56 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json import json
import uuid
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from mox import IsA # noqa from mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.metering import tabs
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
from openstack_dashboard.test.test_data import utils as test_utils
INDEX_URL = reverse("horizon:admin:metering:index")
class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests): INDEX_URL = reverse('horizon:admin:metering:index')
def test_stats_page(self): CREATE_URL = reverse('horizon:admin:metering:create')
meters = self.meters.list() SAMPLES_URL = reverse('horizon:admin:metering:samples')
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.meters = self.mox.CreateMockAnything()
ceilometerclient.meters.list(None).AndReturn(meters)
self.mox.ReplayAll() class MeteringViewTests(test.BaseAdminViewTests):
def test_create_report_page(self):
# getting all resources and with statistics formData = {'period': 7}
res = self.client.get(reverse('horizon:admin:metering:index') + res = self.client.get(CREATE_URL)
"?tab=ceilometer_overview__stats")
self.assertTemplateUsed(res, 'admin/metering/index.html')
self.assertTemplateUsed(res, 'admin/metering/stats.html')
def test_report_page(self):
# getting report page with no api access
res = self.client.get(reverse('horizon:admin:metering:index') +
"?tab=ceilometer_overview__daily_report")
self.assertTemplateUsed(res, 'admin/metering/index.html')
self.assertTemplateUsed(res, 'admin/metering/daily.html') self.assertTemplateUsed(res, 'admin/metering/daily.html')
res = self.client.post(CREATE_URL, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_create_report_dates_messed_up(self):
# dates are swapped in create report form
formData = {'period': 'other',
'date_to': '2014-01-01',
'date_from': '2014-02-02'}
res = self.client.post(CREATE_URL, formData)
self.assertFormError(res, "form", "date_to",
['Start must be earlier than end of period.'])
def test_create_report_date_missing(self):
formData = {'period': 'other',
'date_to': '2014-01-01',
'date_from': ''}
res = self.client.post(CREATE_URL, formData)
self.assertFormError(res, "form", "date_from",
['Must specify start of period'])
class MeteringLineChartTabTests(test.BaseAdminViewTests):
def setUp(self):
test.BaseAdminViewTests.setUp(self)
self.testdata = test_utils.TestData()
test_utils.load_test_data(self.testdata)
def _verify_series(self, series, value, date, expected_names): def _verify_series(self, series, value, date, expected_names):
data = json.loads(series) data = json.loads(series)
@ -61,22 +76,22 @@ class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
self.assertEqual(data.get('settings'), {}) self.assertEqual(data.get('settings'), {})
@test.create_stubs({api.keystone: ('tenant_list',)}) @test.create_stubs({api.keystone: ('tenant_list',),
api.ceilometer: ('meter_list',
'statistic_list',
), })
def test_stats_for_line_chart(self): def test_stats_for_line_chart(self):
statistics = self.statistics.list() api.ceilometer.meter_list(IsA(http.HttpRequest))\
.AndReturn(self.testdata.meters.list())
api.ceilometer.statistic_list(IsA(http.HttpRequest),
'memory',
period=IsA(int),
query=IsA(list)).MultipleTimes()\
.AndReturn(self.testdata.statistics.list())
api.keystone.tenant_list(IsA(http.HttpRequest), api.keystone.tenant_list(IsA(http.HttpRequest),
domain=None, domain=None,
paginate=False) \ paginate=False) \
.AndReturn([self.tenants.list(), False]) .AndReturn([self.testdata.tenants.list(), False])
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.statistics = self.mox.CreateMockAnything()
# check that list is called twice for one resource and 2 meters
ceilometerclient.statistics.list(meter_name="memory",
period=IsA(int), q=IsA(list)).\
MultipleTimes().\
AndReturn(statistics)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -93,22 +108,21 @@ class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
self._verify_series(res._container[0], 4.55, '2012-12-21T11:00:55', self._verify_series(res._container[0], 4.55, '2012-12-21T11:00:55',
expected_names) expected_names)
@test.create_stubs({api.keystone: ('tenant_list',)}) @test.create_stubs({api.keystone: ('tenant_list',),
api.ceilometer: ('meter_list',
'statistic_list',
), })
def test_stats_for_line_chart_attr_max(self): def test_stats_for_line_chart_attr_max(self):
statistics = self.statistics.list() api.ceilometer.meter_list(IsA(http.HttpRequest))\
.AndReturn(self.testdata.meters.list())
api.ceilometer.statistic_list(IsA(http.HttpRequest),
'memory', period=IsA(int),
query=IsA(list))\
.MultipleTimes().AndReturn(self.testdata.statistics.list())
api.keystone.tenant_list(IsA(http.HttpRequest), api.keystone.tenant_list(IsA(http.HttpRequest),
domain=None, domain=None,
paginate=False) \ paginate=False) \
.AndReturn([self.tenants.list(), False]) .AndReturn([self.testdata.tenants.list(), False])
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.statistics = self.mox.CreateMockAnything()
# check that list is called twice for one resource and 2 meters
ceilometerclient.statistics.list(meter_name="memory",
period=IsA(int), q=IsA(list)).\
MultipleTimes().\
AndReturn(statistics)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -122,131 +136,6 @@ class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
expected_names = ['test_tenant', expected_names = ['test_tenant',
'disabled_tenant', 'disabled_tenant',
u'\u4e91\u89c4\u5219'] u'\u4e91\u89c4\u5219']
self._verify_series(res._container[0], 9.0, '2012-12-21T11:00:55', self._verify_series(res._container[0], 9.0, '2012-12-21T11:00:55',
expected_names) expected_names)
def test_stats_for_line_chart_no_group_by(self):
resources = self.resources.list()
statistics = self.statistics.list()
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.resources = self.mox.CreateMockAnything()
ceilometerclient.resources.list(q=[]).AndReturn(resources)
ceilometerclient.statistics = self.mox.CreateMockAnything()
ceilometerclient.statistics.list(meter_name="storage.objects",
period=IsA(int), q=IsA(list)).\
MultipleTimes().\
AndReturn(statistics)
self.mox.ReplayAll()
# getting all resources and with statistics, I have only
# 'storage.objects' defined in test data
res = self.client.get(
reverse('horizon:admin:metering:samples') +
"?meter=storage.objects&stats_attr=avg&date_options=7")
self.assertEqual(res._headers['content-type'],
('Content-Type', 'application/json'))
expected_names = ['fake_resource_id',
'fake_resource_id2']
self._verify_series(res._container[0], 4.55, '2012-12-21T11:00:55',
expected_names)
@test.create_stubs({api.keystone: ('tenant_list',)})
def test_report(self):
meters = self.meters.list()
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.meters = self.mox.CreateMockAnything()
ceilometerclient.meters.list(None).AndReturn(meters)
api.keystone.tenant_list(IsA(http.HttpRequest),
domain=None,
paginate=False). \
MultipleTimes()\
.AndReturn([self.tenants.list(), False])
statistics = self.statistics.list()
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.statistics = self.mox.CreateMockAnything()
ceilometerclient.statistics.list(meter_name="instance",
period=IsA(int), q=IsA(list)).\
MultipleTimes().\
AndReturn(statistics)
ceilometerclient.statistics.list(meter_name="disk.read.bytes",
period=IsA(int), q=IsA(list)).\
MultipleTimes().\
AndReturn(statistics)
ceilometerclient.statistics.list(meter_name="disk.write.bytes",
period=IsA(int), q=IsA(list)).\
MultipleTimes().\
AndReturn(statistics)
self.mox.ReplayAll()
# generate report with mock data
res = self.client.post(reverse('horizon:admin:metering:report'),
data={"date_options": "7"})
self.assertTemplateUsed(res, 'admin/metering/report.html')
class MeteringStatsTabTests(test.APITestCase):
@test.create_stubs({api.nova: ('flavor_list',),
})
def test_stats_hover_hints(self):
class Struct(object):
def __init__(self, d):
self.__dict__.update(d)
def _get_link(meter):
link = ('http://localhost:8777/v2/meters/%s?'
'q.field=resource_id&q.value=ignored')
return dict(href=link % meter, rel=meter)
flavors = ['m1.tiny', 'm1.massive', 'm1.secret']
resources = [
Struct(dict(resource_id=uuid.uuid4(),
project_id='fake_project_id',
user_id='fake_user_id',
timestamp='2013-10-22T12:42:37',
metadata=dict(ramdisk_id='fake_image_id'),
links=[_get_link('instance:%s' % f),
_get_link('instance'),
_get_link('cpu')])) for f in flavors
]
request = self.mox.CreateMock(http.HttpRequest)
api.nova.flavor_list(request, None).AndReturn(self.flavors.list())
ceilometerclient = self.stub_ceilometerclient()
meters = []
for r in resources:
for link in r.links:
meters.append(Struct(dict(resource_id=r.resource_id,
project_id=r.project_id,
user_id=r.user_id,
timestamp=r.timestamp,
name=link['rel'])))
ceilometerclient.meters = self.mox.CreateMockAnything()
ceilometerclient.meters.list(None).AndReturn(meters)
self.mox.ReplayAll()
tab = tabs.GlobalStatsTab(None)
context_data = tab.get_context_data(request)
self.assertTrue('nova_meters' in context_data)
meter_hints = {}
for d in context_data['nova_meters']:
meter_hints[d.name] = d.description
expected_meters = ['instance:%s' % f for f in flavors]
expected_meters.extend(['instance', 'cpu'])
for meter in expected_meters:
self.assertTrue(meter in meter_hints)
self.assertNotEqual(meter_hints[meter], '')

View File

@ -18,6 +18,6 @@ from openstack_dashboard.dashboards.admin.metering import views
urlpatterns = patterns( urlpatterns = patterns(
'openstack_dashboard.dashboards.admin.metering.views', 'openstack_dashboard.dashboards.admin.metering.views',
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateUsageReport.as_view(), name='create'),
url(r'^samples$', views.SamplesView.as_view(), name='samples'), url(r'^samples$', views.SamplesView.as_view(), name='samples'),
url(r'^report$', views.ReportView.as_view(), name='report'),
url(r'^report/csv$', views.CsvReportView.as_view(), name='csvreport')) url(r'^report/csv$', views.CsvReportView.as_view(), name='csvreport'))

View File

@ -10,29 +10,25 @@
# 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 datetime import datetime # noqa
from datetime import timedelta # noqa
import json import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse # noqa from django.http import HttpResponse # noqa
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django.views import django.views
from horizon import exceptions from horizon import exceptions
from horizon import tables from horizon import forms
from horizon import tabs from horizon import tabs
from horizon.utils import csvbase from horizon.utils import csvbase
from openstack_dashboard import api
from openstack_dashboard.api import ceilometer from openstack_dashboard.api import ceilometer
from openstack_dashboard.dashboards.admin.metering import tables as \ from openstack_dashboard.dashboards.admin.metering import forms as \
metering_tables metering_forms
from openstack_dashboard.dashboards.admin.metering import tabs as \ from openstack_dashboard.dashboards.admin.metering import tabs as \
metering_tabs metering_tabs
from openstack_dashboard.utils import metering as utils_metering
class IndexView(tabs.TabbedTableView): class IndexView(tabs.TabbedTableView):
@ -40,6 +36,12 @@ class IndexView(tabs.TabbedTableView):
template_name = 'admin/metering/index.html' template_name = 'admin/metering/index.html'
class CreateUsageReport(forms.ModalFormView):
form_class = metering_forms.UsageReportForm
template_name = 'admin/metering/daily.html'
success_url = reverse_lazy('horizon:admin:metering:index')
class SamplesView(django.views.generic.TemplateView): class SamplesView(django.views.generic.TemplateView):
template_name = "admin/metering/samples.csv" template_name = "admin/metering/samples.csv"
@ -76,19 +78,29 @@ class SamplesView(django.views.generic.TemplateView):
stats_attr = request.GET.get('stats_attr', 'avg') stats_attr = request.GET.get('stats_attr', 'avg')
group_by = request.GET.get('group_by', None) group_by = request.GET.get('group_by', None)
resources, unit = query_data(request, try:
date_from, date_to = utils_metering.calc_date_args(date_from,
date_to,
date_options)
except Exception:
exceptions.handle(self.request, _('Dates cannot be recognized.'))
if group_by == 'project':
query = utils_metering.ProjectAggregatesQuery(request,
date_from, date_from,
date_to, date_to,
date_options, 3600 * 24)
group_by, else:
meter) query = utils_metering.MeterQuery(request, date_from,
date_to, 3600 * 24)
resources, unit = query.query(meter_name)
resource_name = 'id' if group_by == "project" else 'resource_id' resource_name = 'id' if group_by == "project" else 'resource_id'
series = self._series_for_meter(resources, series = self._series_for_meter(resources,
resource_name, resource_name,
meter_name, meter_name,
stats_attr, stats_attr,
unit) unit)
ret = {} ret = {}
ret['series'] = series ret['series'] = series
ret['settings'] = {} ret['settings'] = {}
@ -97,39 +109,6 @@ class SamplesView(django.views.generic.TemplateView):
content_type='application/json') content_type='application/json')
class ReportView(tables.MultiTableView):
template_name = 'admin/metering/report.html'
def get_tables(self):
if self._tables:
return self._tables
project_data = load_report_data(self.request)
table_instances = []
limit = int(self.request.POST.get('limit', '1000'))
for project in project_data.keys():
table = metering_tables.UsageTable(self.request,
data=project_data[project],
kwargs=self.kwargs.copy())
table.title = project
t = (table.name, table)
table_instances.append(t)
if len(table_instances) == limit:
break
self._tables = SortedDict(table_instances)
self.project_data = project_data
return self._tables
def handle_table(self, table):
name = table.name
handled = self._tables[name].maybe_handle()
return handled
def get_context_data(self, **kwargs):
context = {'tables': self.get_tables().values()}
context['csv_url'] = reverse('horizon:admin:metering:csvreport')
return context
class CsvReportView(django.views.generic.View): class CsvReportView(django.views.generic.View):
def get(self, request, **response_kwargs): def get(self, request, **response_kwargs):
render_class = ReportCsvRenderer render_class = ReportCsvRenderer
@ -160,150 +139,6 @@ class ReportCsvRenderer(csvbase.BaseCsvResponse):
u["value"]) u["value"])
def _calc_period(date_from, date_to):
if date_from and date_to:
if date_to < date_from:
# TODO(lsmola) propagate the Value error through Horizon
# handler to the client with verbose message.
raise ValueError("Date to must be bigger than date "
"from.")
# get the time delta in seconds
delta = date_to - date_from
if delta.days <= 0:
# it's one day
delta_in_seconds = 3600 * 24
else:
delta_in_seconds = delta.days * 24 * 3600 + delta.seconds
# Lets always show 400 samples in the chart. Know that it is
# maximum amount of samples and it can be lower.
number_of_samples = 400
period = delta_in_seconds / number_of_samples
else:
# If some date is missing, just set static window to one day.
period = 3600 * 24
return period
def _calc_date_args(date_from, date_to, date_options):
# TODO(lsmola) all timestamps should probably work with
# current timezone. And also show the current timezone in chart.
if (date_options == "other"):
try:
if date_from:
date_from = datetime.strptime(date_from,
"%Y-%m-%d")
else:
# TODO(lsmola) there should be probably the date
# of the first sample as default, so it correctly
# counts the time window. Though I need ordering
# and limit of samples to obtain that.
pass
if date_to:
date_to = datetime.strptime(date_to,
"%Y-%m-%d")
# It return beginning of the day, I want the and of
# the day, so i will add one day without a second.
date_to = (date_to + timedelta(days=1) -
timedelta(seconds=1))
else:
date_to = datetime.now()
except Exception:
raise ValueError("The dates are not "
"recognized.")
else:
try:
date_from = datetime.now() - timedelta(days=int(date_options))
date_to = datetime.now()
except Exception:
raise ValueError("The time delta must be an "
"integer representing days.")
return date_from, date_to
def query_data(request,
date_from,
date_to,
date_options,
group_by,
meter,
period=None,
additional_query=None):
date_from, date_to = _calc_date_args(date_from,
date_to,
date_options)
if not period:
period = _calc_period(date_from, date_to)
if additional_query is None:
additional_query = []
if date_from:
additional_query += [{'field': 'timestamp',
'op': 'ge',
'value': date_from}]
if date_to:
additional_query += [{'field': 'timestamp',
'op': 'le',
'value': date_to}]
# TODO(lsmola) replace this by logic implemented in I1 in bugs
# 1226479 and 1226482, this is just a quick fix for RC1
try:
meter_list = [m for m in ceilometer.meter_list(request)
if m.name == meter]
unit = meter_list[0].unit
except Exception:
unit = ""
if group_by == "project":
try:
tenants, more = api.keystone.tenant_list(
request,
domain=None,
paginate=False)
except Exception:
tenants = []
exceptions.handle(request,
_('Unable to retrieve project list.'))
queries = {}
for tenant in tenants:
tenant_query = [{
"field": "project_id",
"op": "eq",
"value": tenant.id}]
queries[tenant.name] = tenant_query
ceilometer_usage = ceilometer.CeilometerUsage(request)
resources = ceilometer_usage.resource_aggregates_with_statistics(
queries, [meter], period=period, stats_attr=None,
additional_query=additional_query)
else:
query = []
def filter_by_meter_name(resource):
"""Function for filtering of the list of resources.
Will pick the right resources according to currently selected
meter.
"""
for link in resource.links:
if link['rel'] == meter:
# If resource has the currently chosen meter.
return True
return False
ceilometer_usage = ceilometer.CeilometerUsage(request)
try:
resources = ceilometer_usage.resources_with_statistics(
query, [meter], period=period, stats_attr=None,
additional_query=additional_query,
filter_func=filter_by_meter_name)
except Exception:
resources = []
exceptions.handle(request,
_('Unable to retrieve statistics.'))
return resources, unit
def load_report_data(request): def load_report_data(request):
meters = ceilometer.Meters(request) meters = ceilometer.Meters(request)
services = { services = {
@ -318,21 +153,27 @@ def load_report_data(request):
date_options = request.GET.get('date_options', 7) date_options = request.GET.get('date_options', 7)
date_from = request.GET.get('date_from') date_from = request.GET.get('date_from')
date_to = request.GET.get('date_to') date_to = request.GET.get('date_to')
try:
date_from, date_to = utils_metering.calc_date_args(date_from,
date_to,
date_options)
except Exception:
exceptions.handle(request, _('Dates cannot be recognised.'))
try:
project_aggregates = utils_metering.ProjectAggregatesQuery(request,
date_from,
date_to,
3600 * 24)
except Exception:
exceptions.handle(request,
_('Unable to retrieve project list.'))
for meter in meters._cached_meters.values(): for meter in meters._cached_meters.values():
service = None service = None
for name, m_list in services.items(): for name, m_list in services.items():
if meter in m_list: if meter in m_list:
service = name service = name
break break
# show detailed samples res, unit = project_aggregates.query(meter.name)
# samples = ceilometer.sample_list(request, meter.name)
res, unit = query_data(request,
date_from,
date_to,
date_options,
"project",
meter.name,
3600 * 24)
for re in res: for re in res:
values = re.get_meter(meter.name.replace(".", "_")) values = re.get_meter(meter.name.replace(".", "_"))
if values: if values:

View File

@ -0,0 +1,152 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard import api
def calc_period(date_from, date_to):
if date_from and date_to:
if date_to < date_from:
# TODO(lsmola) propagate the Value error through Horizon
# handler to the client with verbose message.
raise ValueError(_("To date to must be greater than From date."))
# get the time delta in seconds
delta = date_to - date_from
if delta.days <= 0:
# it's one day
delta_in_seconds = 3600 * 24
else:
delta_in_seconds = delta.days * 24 * 3600 + delta.seconds
# Lets always show 400 samples in the chart. Know that it is
# maximum amount of samples and it can be lower.
number_of_samples = 400
period = delta_in_seconds / number_of_samples
else:
# If some date is missing, just set static window to one day.
period = 3600 * 24
return period
def calc_date_args(date_from, date_to, date_options):
if date_options == "other":
if date_from and not isinstance(date_from, datetime.date):
try:
date_from = datetime.datetime.strptime(date_from,
"%Y-%m-%d")
except Exception:
raise ValueError(_("From-date is not recognized"))
if date_to:
if not isinstance(date_to, datetime.date):
try:
date_to = datetime.datetime.strptime(date_to,
"%Y-%m-%d")
except Exception:
raise ValueError(_("To-date is not recognized"))
else:
date_to = timezone.now()
else:
try:
date_to = timezone.now()
date_from = date_to - datetime.timedelta(days=int(date_options))
except Exception:
raise ValueError(_("The time delta must be an "
"integer representing days."))
return date_from, date_to
class ProjectAggregatesQuery(object):
def __init__(self, request, date_from, date_to,
period=None, additional_query=[]):
if not period:
period = calc_period(date_from, date_to)
if date_from:
additional_query.append({'field': 'timestamp',
'op': 'ge',
'value': date_from})
if date_to:
additional_query.append({'field': 'timestamp',
'op': 'le',
'value': date_to})
self.request = request
self.period = period
self.additional_query = additional_query
tenants, more = api.keystone.tenant_list(request,
domain=None,
paginate=False)
self.queries = {}
for tenant in tenants:
tenant_query = [{
"field": "project_id",
"op": "eq",
"value": tenant.id}]
self.queries[tenant.name] = tenant_query
def query(self, meter):
meter_list = [m for m in api.ceilometer.meter_list(self.request)
if m.name == meter]
unit = ""
if len(meter_list) > 0:
unit = meter_list[0].unit
ceilometer_usage = api.ceilometer.CeilometerUsage(self.request)
resources = ceilometer_usage.resource_aggregates_with_statistics(
self.queries, [meter], period=self.period,
stats_attr=None,
additional_query=self.additional_query)
return resources, unit
class MeterQuery(ProjectAggregatesQuery):
def __init__(self, *args, **kwargs):
# pop filterfunc and add it later to self.
filterfunc = kwargs.pop('filterfunc', None)
super(MeterQuery, self).__init__(*args, **kwargs)
self.filterfunc = filterfunc
def query(self, meter):
def filter_by_meter_name(resource):
"""Function for filtering of the list of resources.
Will pick the right resources according to currently selected
meter.
"""
for link in resource.links:
if link['rel'] == meter:
# If resource has the currently chosen meter.
return True
return False
meter_list = [m for m in api.ceilometer.meter_list(self.request)
if m.name == meter]
unit = ""
if len(meter_list) > 0:
unit = meter_list[0].unit
ceilometer_usage = api.ceilometer.CeilometerUsage(self.request)
resources = ceilometer_usage.resources_with_statistics(
self.queries, [meter],
period=self.period,
stats_attr=None,
additional_query=self.additional_query,
filter_func=filter_by_meter_name)
return resources, unit