Add Daily Usage tab to Resource Usage panel

Added a new tab to Resource Usage panel. This tab prompts the user for
timeframe and then generates a report showing daily usage per project over the
indicated time frame with one table per project. This report shows all meters
collected summarized per day.

Originally I generated report with default settings initially but opted to only show
form first so that panel can load quickly.

Screenshots are available from
https://a248.e.akamai.net/cdn.hpcloudsvc.com/h6ede74dc88eef6ce6a4bd3a0697deb3a/prodaw2/report.png

Change-Id: I56103dc90b4931a5ab1a5c99bff28cb2d81fc522
Implements: blueprint daily-usage-report
This commit is contained in:
Rob Raymond 2014-02-10 12:46:16 -07:00
parent 087d497cef
commit 4522ae2fec
7 changed files with 275 additions and 3 deletions

View File

@ -0,0 +1,49 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# 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.
from django.utils import text
from django.utils.translation import ugettext_lazy as _
from horizon import tables
def show_date(datum):
return datum.split('T')[0]
class UsageTable(tables.DataTable):
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)'))
def get_object_id(self, datum):
return datum['time'] + datum['meter']
# since these tables are dynamically created and named, we use title
@property
def name(self):
# slugify was introduced in Django 1.5
if hasattr(text, 'slugify'):
return text.slugify(unicode(self.title))
else:
return self.title
def __unicode__(self):
return self.title
class Meta:
name = 'daily'

View File

@ -12,7 +12,7 @@
# 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 messages
@ -55,7 +55,17 @@ class GlobalStatsTab(tabs.Tab):
return context
class DailyReportTab(tabs.Tab):
name = _("Daily Report")
slug = "daily_report"
template_name = ("admin/metering/daily.html")
def get_context_data(self, request):
context = template.RequestContext(request)
return context
class CeilometerOverviewTabs(tabs.TabGroup):
slug = "ceilometer_overview"
tabs = (GlobalStatsTab,)
tabs = (DailyReportTab, GlobalStatsTab, )
sticky = True

View File

@ -0,0 +1,70 @@
{% load i18n %}
<div id="ceilometer-report">
<form class="form-horizontal" action="{% url 'horizon:admin:metering:report' %}" method="POST">
{% csrf_token %}
<div class="control-group">
<label for="report_date_options" class="control-label">{% trans "Period" %}:&nbsp;</label>
<div class="controls">
<select data-line-chart-command="select_box_change"
id="report_date_options" name="date_options" class="span2">
<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="control-group" id="report_date_from">
<label for="date_from" class="control-label">{% trans "From" %}:&nbsp;</label>
<div class="controls">
<input data-line-chart-command="date_picker_change"
type="text" id="date_from" name="date_from" class="span2 example"/>
</div>
</div>
<div class="control-group" id="report_date_to">
<label for="date_to" class="control-label">{% trans "To" %}:&nbsp;</label>
<div class="controls">
<input data-line-chart-command="date_picker_change"
type="text" name="date_to" class="span2 example"/>
</div>
</div>
<div class="control-group">
<label for="limit" class="control-label">{% trans "Limit project count" %}:&nbsp;</label>
<div class="controls">
<input type="text" name="limit" class="span2 example" value="20"/>
</div>
</div>
<button type="submit" class="btn btn-small">{% trans 'Generate Report' %}</button>
</form>
</div>
<script type="text/javascript">
if (typeof $ !== 'undefined') {
show_hide_datepickers();
} else {
addHorizonLoadEvent(function() {
show_hide_datepickers();
});
}
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 .controls input, #date_to .controls 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>

View File

@ -0,0 +1,16 @@
{% 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 %}
<a href="{% url 'horizon:admin:metering:index' %}"><button class="btn btn-small">{% trans 'Back' %}</button></a>
{% for table in tables %}
{{ table.render }}
{% endfor %}
{% endblock %}

View File

@ -41,6 +41,13 @@ class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests):
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')
def _verify_series(self, series, value, date, expected_names):
expected_names.reverse()
data = json.loads(series)
@ -146,6 +153,44 @@ 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',)})
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):

View File

@ -19,4 +19,5 @@ 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'^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'))

View File

@ -18,15 +18,19 @@ from datetime import timedelta # noqa
import json
from django.http import HttpResponse # noqa
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView # noqa
from horizon import exceptions
from horizon import tables
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
from openstack_dashboard.dashboards.admin.metering import tabs as \
metering_tabs
@ -93,6 +97,83 @@ class SamplesView(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 = self.load_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 load_data(self, request):
meters = ceilometer.Meters(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(),
}
project_rows = {}
date_options = request.POST.get('date_options', None)
date_from = request.POST.get('date_from', None)
date_to = request.POST.get('date_to', None)
for meter in meters._cached_meters.values():
for name, m_list in services.items():
if meter in m_list:
service = name
# 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)
for re in res:
values = getattr(re, 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}
if re.id not in project_rows:
project_rows[re.id] = [row]
else:
project_rows[re.id].append(row)
return project_rows
def get_context_data(self, **kwargs):
context = {}
context['tables'] = self.get_tables().values()
return context
def _calc_period(date_from, date_to):
if date_from and date_to:
if date_to < date_from: