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:
parent
087d497cef
commit
4522ae2fec
|
@ -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'
|
|
@ -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
|
||||
|
|
|
@ -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" %}: </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" %}: </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" %}: </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" %}: </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>
|
|
@ -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 %}
|
||||
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue