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 %}
+
+
+
+
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: