diff --git a/openstack_dashboard/dashboards/admin/metering/tables.py b/openstack_dashboard/dashboards/admin/metering/tables.py new file mode 100644 index 0000000000..d013b02583 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/tables.py @@ -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' diff --git a/openstack_dashboard/dashboards/admin/metering/tabs.py b/openstack_dashboard/dashboards/admin/metering/tabs.py index 59ffc75ce3..36f2491b4d 100644 --- a/openstack_dashboard/dashboards/admin/metering/tabs.py +++ b/openstack_dashboard/dashboards/admin/metering/tabs.py @@ -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 diff --git a/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html b/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html new file mode 100644 index 0000000000..ea85ea64d4 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html @@ -0,0 +1,70 @@ +{% load i18n %} +
+
+ {% csrf_token %} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ diff --git a/openstack_dashboard/dashboards/admin/metering/templates/metering/report.html b/openstack_dashboard/dashboards/admin/metering/templates/metering/report.html new file mode 100644 index 0000000000..041ffe6063 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/templates/metering/report.html @@ -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 %} + +{% for table in tables %} + {{ table.render }} +{% endfor %} +{% endblock %} + diff --git a/openstack_dashboard/dashboards/admin/metering/tests.py b/openstack_dashboard/dashboards/admin/metering/tests.py index 8542dad888..ab57bf0504 100644 --- a/openstack_dashboard/dashboards/admin/metering/tests.py +++ b/openstack_dashboard/dashboards/admin/metering/tests.py @@ -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): diff --git a/openstack_dashboard/dashboards/admin/metering/urls.py b/openstack_dashboard/dashboards/admin/metering/urls.py index bb22c99ad4..f98d722a63 100644 --- a/openstack_dashboard/dashboards/admin/metering/urls.py +++ b/openstack_dashboard/dashboards/admin/metering/urls.py @@ -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')) diff --git a/openstack_dashboard/dashboards/admin/metering/views.py b/openstack_dashboard/dashboards/admin/metering/views.py index d4fb1456a2..213a9d7c36 100644 --- a/openstack_dashboard/dashboards/admin/metering/views.py +++ b/openstack_dashboard/dashboards/admin/metering/views.py @@ -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: