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]
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):
service = tables.Column('service', verbose_name=_('Service'))
meter = tables.Column('meter', verbose_name=_('Meter'))
@ -30,6 +66,10 @@ class UsageTable(tables.DataTable):
value = tables.Column('value', verbose_name=_('Value (Avg)'),
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):
return datum['time'] + datum['meter']

View File

@ -10,30 +10,27 @@
# License for the specific language governing permissions and limitations
# under the License.
from django import template
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import messages
from horizon import tabs
from openstack_dashboard import api
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")
slug = "stats"
template_name = ("admin/metering/stats.html")
preload = False
@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']
table_classes = (metering_tables.UsageTable,)
def get_context_data(self, request):
meters = ceilometer.Meters(request)
@ -53,17 +50,66 @@ class GlobalStatsTab(tabs.Tab):
return context
class DailyReportTab(tabs.Tab):
name = _("Daily Report")
slug = "daily_report"
template_name = ("admin/metering/daily.html")
class UsageReportTab(tabs.TableTab):
name = _("Usage Report")
slug = "usage_report"
template_name = "horizon/common/_detail_table.html"
table_classes = (metering_tables.ReportTable,)
def get_context_data(self, request):
context = template.RequestContext(request)
return context
def get_report_table_data(self):
meters = ceilometer.Meters(self.request)
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):
slug = "ceilometer_overview"
tabs = (DailyReportTab, GlobalStatsTab, )
tabs = (UsageReportTab, GlobalStatsTab,)
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 %}
<div id="ceilometer-report">
<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();
});
}
{% block title %}{% trans "Modify Usage Report Parameters" %}{% endblock %}
function show_hide_datepickers() {
$("#report_date_options").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 input, #date_to input").val('');
$("#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>
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Modify Usage Report Parameters") %}
{% endblock page_header %}
{% block main %}
{% include "admin/metering/_daily.html" %}
{% endblock %}

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
# under the License.
import json
import uuid
from django.core.urlresolvers import reverse
from django import http
from mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.metering import tabs
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse("horizon:admin:metering:index")
from openstack_dashboard.test.test_data import utils as test_utils
class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
def test_stats_page(self):
meters = self.meters.list()
INDEX_URL = reverse('horizon:admin:metering:index')
CREATE_URL = reverse('horizon:admin:metering:create')
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()
# getting all resources and with statistics
res = self.client.get(reverse('horizon:admin:metering:index') +
"?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')
class MeteringViewTests(test.BaseAdminViewTests):
def test_create_report_page(self):
formData = {'period': 7}
res = self.client.get(CREATE_URL)
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):
data = json.loads(series)
@ -61,22 +76,22 @@ class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
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):
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),
domain=None,
paginate=False) \
.AndReturn([self.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)
.AndReturn([self.testdata.tenants.list(), False])
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',
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):
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),
domain=None,
paginate=False) \
.AndReturn([self.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)
.AndReturn([self.testdata.tenants.list(), False])
self.mox.ReplayAll()
@ -122,131 +136,6 @@ class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
expected_names = ['test_tenant',
'disabled_tenant',
u'\u4e91\u89c4\u5219']
self._verify_series(res._container[0], 9.0, '2012-12-21T11:00:55',
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(
'openstack_dashboard.dashboards.admin.metering.views',
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'^report$', views.ReportView.as_view(), name='report'),
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
# under the License.
from datetime import datetime # noqa
from datetime import timedelta # noqa
import json
from django.core.urlresolvers import reverse
from django.http import HttpResponse # noqa
from django.utils.datastructures import SortedDict
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse # noqa
from django.utils.translation import ugettext_lazy as _
import django.views
from horizon import exceptions
from horizon import tables
from horizon import forms
from horizon import tabs
from horizon.utils import csvbase
from openstack_dashboard import api
from openstack_dashboard.api import ceilometer
from openstack_dashboard.dashboards.admin.metering import tables as \
metering_tables
from openstack_dashboard.dashboards.admin.metering import forms as \
metering_forms
from openstack_dashboard.dashboards.admin.metering import tabs as \
metering_tabs
from openstack_dashboard.utils import metering as utils_metering
class IndexView(tabs.TabbedTableView):
@ -40,6 +36,12 @@ class IndexView(tabs.TabbedTableView):
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):
template_name = "admin/metering/samples.csv"
@ -76,19 +78,29 @@ class SamplesView(django.views.generic.TemplateView):
stats_attr = request.GET.get('stats_attr', 'avg')
group_by = request.GET.get('group_by', None)
resources, unit = query_data(request,
date_from,
date_to,
date_options,
group_by,
meter)
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_to,
3600 * 24)
else:
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'
series = self._series_for_meter(resources,
resource_name,
meter_name,
stats_attr,
unit)
ret = {}
ret['series'] = series
ret['settings'] = {}
@ -97,39 +109,6 @@ class SamplesView(django.views.generic.TemplateView):
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):
def get(self, request, **response_kwargs):
render_class = ReportCsvRenderer
@ -160,150 +139,6 @@ class ReportCsvRenderer(csvbase.BaseCsvResponse):
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):
meters = ceilometer.Meters(request)
services = {
@ -318,21 +153,27 @@ def load_report_data(request):
date_options = request.GET.get('date_options', 7)
date_from = request.GET.get('date_from')
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():
service = None
for name, m_list in services.items():
if meter in m_list:
service = name
break
# show detailed samples
# samples = ceilometer.sample_list(request, meter.name)
res, unit = query_data(request,
date_from,
date_to,
date_options,
"project",
meter.name,
3600 * 24)
res, unit = project_aggregates.query(meter.name)
for re in res:
values = re.get_meter(meter.name.replace(".", "_"))
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