From ca2e10c3e8efa6d5ae07a04ea0cd6a029dad7319 Mon Sep 17 00:00:00 2001 From: Matthias Runge Date: Tue, 14 Oct 2014 11:26:42 +0200 Subject: [PATCH] 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 --- .../dashboards/admin/metering/forms.py | 68 +++++ .../dashboards/admin/metering/tables.py | 40 +++ .../dashboards/admin/metering/tabs.py | 86 ++++-- .../metering/templates/metering/_daily.html | 57 ++++ .../metering/templates/metering/daily.html | 81 +----- .../metering/templates/metering/report.html | 24 -- .../dashboards/admin/metering/tests.py | 235 +++++------------ .../dashboards/admin/metering/urls.py | 2 +- .../dashboards/admin/metering/views.py | 247 ++++-------------- openstack_dashboard/utils/metering.py | 152 +++++++++++ 10 files changed, 499 insertions(+), 493 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/metering/forms.py create mode 100644 openstack_dashboard/dashboards/admin/metering/templates/metering/_daily.html delete mode 100644 openstack_dashboard/dashboards/admin/metering/templates/metering/report.html create mode 100644 openstack_dashboard/utils/metering.py diff --git a/openstack_dashboard/dashboards/admin/metering/forms.py b/openstack_dashboard/dashboards/admin/metering/forms.py new file mode 100644 index 0000000000..31fe953cbe --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/forms.py @@ -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 diff --git a/openstack_dashboard/dashboards/admin/metering/tables.py b/openstack_dashboard/dashboards/admin/metering/tables.py index 0b16d246cf..583a44b092 100644 --- a/openstack_dashboard/dashboards/admin/metering/tables.py +++ b/openstack_dashboard/dashboards/admin/metering/tables.py @@ -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'] diff --git a/openstack_dashboard/dashboards/admin/metering/tabs.py b/openstack_dashboard/dashboards/admin/metering/tabs.py index 113826eaae..31c9308a12 100644 --- a/openstack_dashboard/dashboards/admin/metering/tabs.py +++ b/openstack_dashboard/dashboards/admin/metering/tabs.py @@ -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 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..a19572c049 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/templates/metering/_daily.html @@ -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 %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "Select a pre-defined period or specify date." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} + +{% block modal-js %} + +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html b/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html index ecec65b975..89a62ebe44 100644 --- a/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html +++ b/openstack_dashboard/dashboards/admin/metering/templates/metering/daily.html @@ -1,74 +1,11 @@ +{% extends 'base.html' %} {% load i18n %} -
-
- {% csrf_token %} -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
-
- +{% 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 %} diff --git a/openstack_dashboard/dashboards/admin/metering/templates/metering/report.html b/openstack_dashboard/dashboards/admin/metering/templates/metering/report.html deleted file mode 100644 index acb9f8ef31..0000000000 --- a/openstack_dashboard/dashboards/admin/metering/templates/metering/report.html +++ /dev/null @@ -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 %} -
- - -
-{% for table in tables %} - {{ table.render }} -{% endfor %} -{% if not tables %} -

- {% trans 'No data available' %} -

-{% endif %} -{% endblock %} - diff --git a/openstack_dashboard/dashboards/admin/metering/tests.py b/openstack_dashboard/dashboards/admin/metering/tests.py index 4f781690e5..ba0a08a8bd 100644 --- a/openstack_dashboard/dashboards/admin/metering/tests.py +++ b/openstack_dashboard/dashboards/admin/metering/tests.py @@ -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], '') diff --git a/openstack_dashboard/dashboards/admin/metering/urls.py b/openstack_dashboard/dashboards/admin/metering/urls.py index 07a636c8ff..33279ebd19 100644 --- a/openstack_dashboard/dashboards/admin/metering/urls.py +++ b/openstack_dashboard/dashboards/admin/metering/urls.py @@ -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')) diff --git a/openstack_dashboard/dashboards/admin/metering/views.py b/openstack_dashboard/dashboards/admin/metering/views.py index dd84f7cd1f..e97e5aaa32 100644 --- a/openstack_dashboard/dashboards/admin/metering/views.py +++ b/openstack_dashboard/dashboards/admin/metering/views.py @@ -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: diff --git a/openstack_dashboard/utils/metering.py b/openstack_dashboard/utils/metering.py new file mode 100644 index 0000000000..c245d81ff1 --- /dev/null +++ b/openstack_dashboard/utils/metering.py @@ -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