diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index cf01ca1bdc..bdfc6ef7c5 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -22,7 +22,7 @@ import horizon class SystemPanels(horizon.PanelGroup): slug = "admin" name = _("System Panel") - panels = ('overview', 'hypervisors', 'instances', 'volumes', + panels = ('overview', 'metering', 'hypervisors', 'instances', 'volumes', 'flavors', 'images', 'networks', 'routers', 'defaults', 'info') diff --git a/openstack_dashboard/dashboards/admin/metering/__init__.py b/openstack_dashboard/dashboards/admin/metering/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/metering/panel.py b/openstack_dashboard/dashboards/admin/metering/panel.py new file mode 100644 index 0000000000..24af3845f5 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/panel.py @@ -0,0 +1,27 @@ +# 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.translation import ugettext_lazy as _ # noqa + +import horizon +from openstack_dashboard.dashboards.admin import dashboard + + +class Metering(horizon.Panel): + name = _("Resource Usage") + slug = 'metering' + permissions = ('openstack.services.metering', 'openstack.roles.admin', ) + + +dashboard.Admin.register(Metering) diff --git a/openstack_dashboard/dashboards/admin/metering/tables.py b/openstack_dashboard/dashboards/admin/metering/tables.py new file mode 100644 index 0000000000..2e7eef4285 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/tables.py @@ -0,0 +1,217 @@ +# 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. + +import logging + +from django.template.defaultfilters import filesizeformat # noqa +from django.utils.translation import ugettext_lazy as _ # noqa + +from horizon import exceptions +from horizon import tables +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class CommonFilterAction(tables.FilterAction): + def filter(self, table, resources, filter_string): + q = filter_string.lower() + return [resource for resource in resources + if q in resource.resource.lower() or + q in resource.tenant.lower() or + q in resource.user.lower()] + + +def get_status(fields): + # TODO(lsmola) it should periodically renew the tables I guess + def transform(datum): + if any([getattr(datum, field, None) is 0 or getattr(datum, field, None) + for field in fields]): + return _("up") + else: + return _("none") + return transform + + +class GlobalUsageTable(tables.DataTable): + tenant = tables.Column("tenant", verbose_name=_("Tenant"), sortable=True, + filters=(lambda (t): getattr(t, 'name', ""),)) + user = tables.Column("user", verbose_name=_("User"), sortable=True, + filters=(lambda (u): getattr(u, 'name', ""),)) + instance = tables.Column("resource", + verbose_name=_("Resource"), + sortable=True) + + +class GlobalDiskUsageTable(tables.DataTable): + tenant = tables.Column("id", verbose_name=_("Tenant"), sortable=True) + disk_read_bytes = tables.Column("disk_read_bytes", + filters=(filesizeformat,), + verbose_name=_("Disk Read Bytes"), + sortable=True) + disk_read_requests = tables.Column("disk_read_requests", + verbose_name=_("Disk Read Requests"), + sortable=True) + disk_write_bytes = tables.Column("disk_write_bytes", + verbose_name=_("Disk Write Bytes"), + filters=(filesizeformat,), + sortable=True) + disk_write_requests = tables.Column("disk_write_requests", + verbose_name=_("Disk Write Requests"), + sortable=True) + + class Meta: + name = "global_disk_usage" + verbose_name = _("Global Disk Usage (average of last 30 days)") + table_actions = (CommonFilterAction,) + multi_select = False + + +class GlobalNetworkTrafficUsageTable(tables.DataTable): + tenant = tables.Column("id", verbose_name=_("Tenant"), sortable=True) + network_incoming_bytes = tables\ + .Column("network_incoming_bytes", + verbose_name=_("Network incoming Bytes"), + filters=(filesizeformat,), + sortable=True) + network_incoming_packets = tables\ + .Column("network_incoming_packets", + verbose_name=_("Network incoming Packets"), + sortable=True) + network_outgoing_bytes = tables\ + .Column("network_outgoing_bytes", + verbose_name=_("Network Outgoing Bytes"), + filters=(filesizeformat,), + sortable=True) + network_outgoing_packets = tables\ + .Column("network_outgoing_packets", + verbose_name=_("Network Outgoing Packets"), + sortable=True) + + class Meta: + name = "global_network_traffic_usage" + verbose_name = _("Global Network Traffic Usage (average " + "of last 30 days)") + table_actions = (CommonFilterAction,) + multi_select = False + + +class GlobalNetworkUsageTable(tables.DataTable): + tenant = tables.Column("id", verbose_name=_("Tenant"), sortable=True) + network_duration = tables.Column("network", + verbose_name=_("Network Duration"), + sortable=True) + network_creation_requests = tables\ + .Column("network_create", + verbose_name=_("Network Creation Requests"), + sortable=True) + subnet_duration = tables.Column("subnet", + verbose_name=_("Subnet Duration"), + sortable=True) + subnet_creation = tables.Column("subnet_create", + verbose_name=_("Subnet Creation Requests"), + sortable=True) + port_duration = tables.Column("port", + verbose_name=_("Port Duration"), + sortable=True) + port_creation = tables.Column("port_create", + verbose_name=_("Port Creation Requests"), + sortable=True) + router_duration = tables.Column("router", + verbose_name=_("Router Duration"), + sortable=True) + router_creation = tables.Column("router_create", + verbose_name=_("Router Creation Requests"), + sortable=True) + port_duration = tables.Column("port", + verbose_name=_("Port Duration"), + sortable=True) + port_creation = tables.Column("port_create", + verbose_name=_("Port Creation Requests"), + sortable=True) + ip_floating_duration = tables\ + .Column("ip_floating", + verbose_name=_("Floating IP Duration"), + sortable=True) + ip_floating_creation = tables\ + .Column("ip_floating_create", + verbose_name=_("Floating IP Creation Requests"), + sortable=True) + + class Meta: + name = "global_network_usage" + verbose_name = _("Global Network Usage (average of last 30 days)") + table_actions = (CommonFilterAction,) + multi_select = False + + +class GlobalObjectStoreUsageUpdateRow(tables.Row): + ajax = True + + def get_data(self, request, object_id): + ceilometer_usage = api.ceilometer.CeilometerUsage(request) + + query = ceilometer_usage.query_from_object_id(object_id) + try: + data = ceilometer_usage.global_object_store_usage( + query, + with_statistics=True) + except Exception: + data = [] + exceptions.handle(request, + _('Unable to retrieve statistics.')) + return None + return data[0] + + +class GlobalObjectStoreUsageTable(tables.DataTable): + tenant = tables.Column("tenant", verbose_name=_("Tenant"), sortable=True, + filters=(lambda (t): getattr(t, 'name', ""),)) + status = tables.Column(get_status(["storage_objects", + "storage_objects_size", + "storage_objects_incoming_bytes", + "storage_objects_outgoing_bytes"]), + verbose_name=_("Status"), + hidden=True) + resource = tables.Column("resource", + verbose_name=_("Resource"), + sortable=True) + storage_incoming_bytes = tables.Column( + "storage_objects_incoming_bytes", + verbose_name=_("Object Storage Incoming Bytes"), + filters=(filesizeformat,), + sortable=True) + storage_outgoing_bytes = tables.Column( + "storage_objects_outgoing_bytes", + verbose_name=_("Object Storage Outgoing Bytes"), + filters=(filesizeformat,), + sortable=True) + storage_objects = tables.Column( + "storage_objects", + verbose_name=_("Total Number of Objects"), + sortable=True) + storage_objects_size = tables.Column( + "storage_objects_size", + filters=(filesizeformat,), + verbose_name=_("Total Size of Objects "), + sortable=True) + + class Meta: + name = "global_object_store_usage" + verbose_name = _("Global Object Store Usage (average of last 30 days)") + table_actions = (CommonFilterAction,) + row_class = GlobalObjectStoreUsageUpdateRow + status_columns = ["status"] + multi_select = False diff --git a/openstack_dashboard/dashboards/admin/metering/tabs.py b/openstack_dashboard/dashboards/admin/metering/tabs.py new file mode 100644 index 0000000000..890cd0ce46 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/tabs.py @@ -0,0 +1,223 @@ +# 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 datetime import datetime # noqa +from datetime import timedelta # noqa + +from django.utils.translation import ugettext_lazy as _ # noqa + +from horizon import exceptions +from horizon import tabs +from openstack_dashboard import api +from openstack_dashboard.api import ceilometer + +from openstack_dashboard.dashboards.admin.metering import tables + + +def make_tenant_queries(request, days_before=30): + try: + tenants, more = api.keystone.tenant_list( + request, + domain=None, + paginate=True, + marker="tenant_marker") + except Exception: + tenants = [] + exceptions.handle(request, + _('Unable to retrieve tenant list.')) + queries = {} + for tenant in tenants: + tenant_query = [{ + "field": "project_id", + "op": "eq", + "value": tenant.id}] + + queries[tenant.name] = tenant_query + + # TODO(lsmola) Just show last 30 days, should be switchable somewhere + # above the table. + date_from = datetime.now() - timedelta(days_before) + date_to = datetime.now() + additional_query = [{'field': 'timestamp', + 'op': 'ge', + 'value': date_from}, + {'field': 'timestamp', + 'op': 'le', + 'value': date_to}] + + return queries, additional_query + + +def list_of_resource_aggregates(request, meters, stats_attr="avg"): + queries, additional_query = make_tenant_queries(request) + + ceilometer_usage = ceilometer.CeilometerUsage(request) + try: + resource_aggregates = ceilometer_usage.\ + resource_aggregates_with_statistics( + queries, meters, stats_attr="avg", + additional_query=additional_query) + except Exception: + resource_aggregates = [] + exceptions.handle(request, + _('Unable to retrieve statistics.')) + + return resource_aggregates + + +class GlobalDiskUsageTab(tabs.TableTab): + table_classes = (tables.GlobalDiskUsageTable,) + name = _("Global Disk Usage") + slug = "global_disk_usage" + template_name = ("horizon/common/_detail_table.html") + preload = False + + def get_global_disk_usage_data(self): + """ Disk usage table data aggregated by project """ + request = self.tab_group.request + return list_of_resource_aggregates(request, + ceilometer.GlobalDiskUsage.meters) + + +class GlobalNetworkTrafficUsageTab(tabs.TableTab): + table_classes = (tables.GlobalNetworkTrafficUsageTable,) + name = _("Global Network Traffic Usage") + slug = "global_network_traffic_usage" + template_name = ("horizon/common/_detail_table.html") + preload = False + + def get_global_network_traffic_usage_data(self): + request = self.tab_group.request + return list_of_resource_aggregates(request, + ceilometer.GlobalNetworkTrafficUsage.meters) + + +class GlobalNetworkUsageTab(tabs.TableTab): + table_classes = (tables.GlobalNetworkUsageTable,) + name = _("Global Network Usage") + slug = "global_network_usage" + template_name = ("horizon/common/_detail_table.html") + preload = False + + def get_global_network_usage_data(self): + request = self.tab_group.request + return list_of_resource_aggregates(request, + ceilometer.GlobalNetworkUsage.meters) + + def allowed(self, request): + permissions = ("openstack.services.network",) + return request.user.has_perms(permissions) + + +class GlobalObjectStoreUsageTab(tabs.TableTab): + table_classes = (tables.GlobalObjectStoreUsageTable,) + name = _("Global Object Store Usage") + slug = "global_object_store_usage" + template_name = ("horizon/common/_detail_table.html") + preload = False + + def get_global_object_store_usage_data(self): + request = self.tab_group.request + ceilometer_usage = ceilometer.CeilometerUsage(request) + + date_from = datetime.now() - timedelta(30) + date_to = datetime.now() + additional_query = [{'field': 'timestamp', + 'op': 'ge', + 'value': date_from}, + {'field': 'timestamp', + 'op': 'le', + 'value': date_to}] + try: + result = ceilometer_usage.global_object_store_usage( + with_statistics=True, additional_query=additional_query) + except Exception: + result = [] + exceptions.handle(request, + _('Unable to retrieve statistics.')) + return result + + def allowed(self, request): + permissions = ("openstack.services.object-store",) + return request.user.has_perms(permissions) + + +class GlobalStatsTab(tabs.Tab): + name = _("Stats") + slug = "stats" + template_name = ("admin/metering/stats.html") + preload = False + + def get_context_data(self, request): + query = [{"field": "metadata.OS-EXT-AZ:availability_zone", + "op": "eq", + "value": "nova"}] + try: + resources = ceilometer.resource_list(request, query, + ceilometer_usage_object=None) + except Exception: + resources = [] + exceptions.handle(request, + _('Unable to retrieve Nova Ceilometer ' + 'resources.')) + try: + resource = resources[0] + meters = [link['rel'] for link in resource.links + if link['rel'] != "self"] + except IndexError: + resource = None + meters = [] + + meter_titles = {"instance": _("Duration of instance"), + "instance:": _("Duration of instance " + " (openstack types)"), + "memory": _("Volume of RAM in MB"), + "cpu": _("CPU time used"), + "cpu_util": _("Average CPU utilisation"), + "vcpus": _("Number of VCPUs"), + "disk.read.requests": _("Number of read requests"), + "disk.write.requests": _("Number of write requests"), + "disk.read.bytes": _("Volume of read in B"), + "disk.write.bytes": _("Volume of write in B"), + "disk.root.size": _("Size of root disk in GB"), + "disk.ephemeral.size": _("Size of ephemeral disk " + "in GB"), + "network.incoming.bytes": _("number of incoming bytes " + "on the network for a VM interface"), + "network.outgoing.bytes": _("number of outgoing bytes " + "on the network for a VM interface"), + "network.incoming.packets": _("number of incoming " + "packets for a VM interface"), + "network.outgoing.packets": _("number of outgoing " + "packets for a VM interface")} + + class MetersWrap(object): + """ A quick wrapper for meter and associated titles. """ + def __init__(self, meter, meter_titles): + self.name = meter + self.title = meter_titles.get(meter, "") + + meters_objs = [] + for meter in meters: + meters_objs.append(MetersWrap(meter, meter_titles)) + + context = {'meters': meters_objs} + return context + + +class CeilometerOverviewTabs(tabs.TabGroup): + slug = "ceilometer_overview" + tabs = (GlobalDiskUsageTab, GlobalNetworkTrafficUsageTab, + GlobalObjectStoreUsageTab, GlobalNetworkUsageTab, GlobalStatsTab,) + sticky = True diff --git a/openstack_dashboard/dashboards/admin/metering/templates/metering/index.html b/openstack_dashboard/dashboards/admin/metering/templates/metering/index.html new file mode 100644 index 0000000000..db9130757b --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/templates/metering/index.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Resources usage Overview" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Resources usage Overview")%} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/metering/templates/metering/stats.html b/openstack_dashboard/dashboards/admin/metering/templates/metering/stats.html new file mode 100644 index 0000000000..176ffcbe6d --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/templates/metering/stats.html @@ -0,0 +1,180 @@ +{% load i18n %} +{% load url from future %} + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+
+

{% trans "Statistics of all resources" %}

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/openstack_dashboard/dashboards/admin/metering/tests.py b/openstack_dashboard/dashboards/admin/metering/tests.py new file mode 100644 index 0000000000..2f8ea99140 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/tests.py @@ -0,0 +1,345 @@ +# 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.core.urlresolvers import reverse # noqa +from django import http # noqa +from mox import IsA # noqa + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +INDEX_URL = reverse("horizon:admin:metering:index") + + +class MeteringViewTests(test.APITestCase, test.BaseAdminViewTests): + @test.create_stubs({api.keystone: ('tenant_list',)}) + def test_disk_usage(self): + statistics = self.statistics.list() + + api.keystone.tenant_list(IsA(http.HttpRequest), + domain=None, + marker='tenant_marker', + paginate=True) \ + .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=IsA(str), + period=None, q=IsA(list)).\ + MultipleTimes().\ + AndReturn(statistics) + + self.mox.ReplayAll() + + # getting all resources and with statistics + res = self.client.get(reverse('horizon:admin:metering:index')) + self.assertTemplateUsed(res, 'admin/metering/index.html') + table_stats = res.context['table'].data + + first = table_stats[0] + self.assertEqual(first.id, 'test_tenant') + self.assertEqual(first.disk_write_requests, 4.55) + self.assertEqual(first.disk_read_bytes, 4.55) + self.assertEqual(first.disk_write_bytes, 4.55) + self.assertEqual(first.disk_read_bytes, 4.55) + + second = table_stats[1] + self.assertEqual(second.id, 'disabled_tenant') + self.assertEqual(second.disk_write_requests, 4.55) + self.assertEqual(second.disk_read_bytes, 4.55) + self.assertEqual(second.disk_write_bytes, 4.55) + self.assertEqual(second.disk_read_bytes, 4.55) + + # check there is as many rows as tenants + self.assertEqual(len(table_stats), + len(self.tenants.list())) + self.assertIsInstance(first, api.ceilometer.ResourceAggregate) + + @test.create_stubs({api.keystone: ('tenant_list',)}) + def test_global_network_traffic_usage(self): + statistics = self.statistics.list() + + api.keystone.tenant_list(IsA(http.HttpRequest), + domain=None, + marker='tenant_marker', + paginate=True) \ + .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=IsA(str), + period=None, q=IsA(list)).\ + MultipleTimes().\ + AndReturn(statistics) + + self.mox.ReplayAll() + + # getting all resources and with statistics + res = self.client.get(reverse('horizon:admin:metering:index') + + "?tab=ceilometer_overview__global_network_traffic_usage") + self.assertTemplateUsed(res, 'admin/metering/index.html') + table_stats = res.context['table'].data + + first = table_stats[0] + self.assertEqual(first.id, 'test_tenant') + self.assertEqual(first.network_incoming_bytes, 4.55) + self.assertEqual(first.network_incoming_packets, 4.55) + self.assertEqual(first.network_outgoing_bytes, 4.55) + self.assertEqual(first.network_outgoing_packets, 4.55) + + second = table_stats[1] + self.assertEqual(second.id, 'disabled_tenant') + self.assertEqual(second.network_incoming_bytes, 4.55) + self.assertEqual(second.network_incoming_packets, 4.55) + self.assertEqual(second.network_outgoing_bytes, 4.55) + self.assertEqual(second.network_outgoing_packets, 4.55) + + # check there is as many rows as tenants + self.assertEqual(len(table_stats), + len(self.tenants.list())) + self.assertIsInstance(first, api.ceilometer.ResourceAggregate) + + @test.create_stubs({api.keystone: ('tenant_list',)}) + def test_global_network_usage(self): + statistics = self.statistics.list() + + api.keystone.tenant_list(IsA(http.HttpRequest), + domain=None, + marker='tenant_marker', + paginate=True) \ + .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=IsA(str), + period=None, q=IsA(list)).\ + MultipleTimes().\ + AndReturn(statistics) + + self.mox.ReplayAll() + + # getting all resources and with statistics + res = self.client.get(reverse('horizon:admin:metering:index') + + "?tab=ceilometer_overview__global_network_usage") + self.assertTemplateUsed(res, 'admin/metering/index.html') + table_stats = res.context['table'].data + + first = table_stats[0] + self.assertEqual(first.id, 'test_tenant') + self.assertEqual(first.network, 4.55) + self.assertEqual(first.network_create, 4.55) + self.assertEqual(first.subnet, 4.55) + self.assertEqual(first.subnet_create, 4.55) + self.assertEqual(first.port, 4.55) + self.assertEqual(first.port_create, 4.55) + self.assertEqual(first.router, 4.55) + self.assertEqual(first.router_create, 4.55) + self.assertEqual(first.ip_floating, 4.55) + self.assertEqual(first.ip_floating_create, 4.55) + + second = table_stats[1] + self.assertEqual(second.id, 'disabled_tenant') + self.assertEqual(second.network, 4.55) + self.assertEqual(second.network_create, 4.55) + self.assertEqual(second.subnet, 4.55) + self.assertEqual(second.subnet_create, 4.55) + self.assertEqual(second.port, 4.55) + self.assertEqual(second.port_create, 4.55) + self.assertEqual(second.router, 4.55) + self.assertEqual(second.router_create, 4.55) + self.assertEqual(second.ip_floating, 4.55) + self.assertEqual(second.ip_floating_create, 4.55) + + # check there is as many rows as tenants + self.assertEqual(len(table_stats), + len(self.tenants.list())) + self.assertIsInstance(first, api.ceilometer.ResourceAggregate) + + @test.create_stubs({api.ceilometer.CeilometerUsage: ("get_user", + "get_tenant")}) + def test_global_object_store_usage(self): + resources = self.resources.list() + statistics = self.statistics.list() + user = self.ceilometer_users.list()[0] + tenant = self.ceilometer_tenants.list()[0] + + ceilometerclient = self.stub_ceilometerclient() + ceilometerclient.resources = self.mox.CreateMockAnything() + ceilometerclient.resources.list(q=None).AndReturn(resources) + + ceilometerclient.statistics = self.mox.CreateMockAnything() + ceilometerclient.statistics.list(meter_name=IsA(str), + period=None, q=IsA(list)).\ + MultipleTimes().\ + AndReturn(statistics) + + api.ceilometer.CeilometerUsage\ + .get_user(IsA(str)).MultipleTimes().AndReturn(user) + api.ceilometer.CeilometerUsage\ + .get_tenant(IsA(str)).MultipleTimes().AndReturn(tenant) + + self.mox.ReplayAll() + + # getting all resources and with statistics + res = self.client.get(reverse('horizon:admin:metering:index') + + "?tab=ceilometer_overview__global_object_store_usage") + self.assertTemplateUsed(res, 'admin/metering/index.html') + table_stats = res.context['table'].data + + first = table_stats[0] + self.assertEqual(first.id, 'fake_project_id__fake_user_id__' + 'fake_resource_id') + self.assertEqual(first.user.name, 'user') + self.assertEqual(first.tenant.name, 'test_tenant') + self.assertEqual(first.resource, 'fake_resource_id') + + self.assertEqual(first.storage_objects, 4.55) + self.assertEqual(first.storage_objects_size, 4.55) + self.assertEqual(first.storage_objects_incoming_bytes, 4.55) + self.assertEqual(first.storage_objects_outgoing_bytes, 4.55) + + self.assertEqual(len(table_stats), len(resources)) + self.assertIsInstance(first, api.ceilometer.GlobalObjectStoreUsage) + + def test_stats_page(self): + resources = self.resources.list() + ceilometerclient = self.stub_ceilometerclient() + ceilometerclient.resources = self.mox.CreateMockAnything() + # I am returning only 1 resource + ceilometerclient.resources.list(q=IsA(list)).AndReturn(resources[:1]) + + 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') + + @test.create_stubs({api.keystone: ('tenant_list',)}) + def test_stats_for_line_chart(self): + statistics = self.statistics.list() + + api.keystone.tenant_list(IsA(http.HttpRequest), + domain=None, + marker='tenant_marker', + paginate=True) \ + .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) + + self.mox.ReplayAll() + + # get all statistics of project aggregates + res = self.client.get(reverse('horizon:admin:metering:samples') + + "?meter=memory&group_by=project&stats_attr=avg&date_options=7") + + self.assertEqual(res._headers['content-type'], + ('Content-Type', 'application/json')) + self.assertEqual(res._container, + ['{"series": [{"data": [{"y": 4, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "test_tenant", "unit": ""}, ' + '{"data": [{"y": 4, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "disabled_tenant", ' + '"unit": ""}, ' + '{"data": [{"y": 4, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "\\u4e91\\u89c4\\u5219", ' + '"unit": ""}], ' + '"settings": {}}']) + + @test.create_stubs({api.keystone: ('tenant_list',)}) + def test_stats_for_line_chart_attr_max(self): + statistics = self.statistics.list() + + api.keystone.tenant_list(IsA(http.HttpRequest), + domain=None, + marker='tenant_marker', + paginate=True) \ + .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) + + self.mox.ReplayAll() + + # get all statistics of project aggregates + res = self.client.get(reverse('horizon:admin:metering:samples') + + "?meter=memory&group_by=project&stats_attr=max&date_options=7") + + self.assertEqual(res._headers['content-type'], + ('Content-Type', 'application/json')) + self.assertEqual(res._container, + ['{"series": [{"data": [{"y": 9, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "test_tenant", "unit": ""}, ' + '{"data": [{"y": 9, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "disabled_tenant", ' + '"unit": ""}, ' + '{"data": [{"y": 9, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "\\u4e91\\u89c4\\u5219", ' + '"unit": ""}], ' + '"settings": {}}']) + + 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=IsA(list)).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')) + self.assertEqual(res._container, + ['{"series": [{"data": [{"y": 4, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "fake_resource_id", ' + '"unit": ""}, ' + '{"data": [{"y": 4, ' + '"x": "2012-12-21T11:00:55"}], ' + '"name": "fake_resource_id2", ' + '"unit": ""}], ' + '"settings": {}}']) diff --git a/openstack_dashboard/dashboards/admin/metering/urls.py b/openstack_dashboard/dashboards/admin/metering/urls.py new file mode 100644 index 0000000000..bb22c99ad4 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/urls.py @@ -0,0 +1,22 @@ +# 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.conf.urls import patterns # noqa +from django.conf.urls import url # noqa + +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')) diff --git a/openstack_dashboard/dashboards/admin/metering/views.py b/openstack_dashboard/dashboards/admin/metering/views.py new file mode 100644 index 0000000000..08fa3e2459 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/metering/views.py @@ -0,0 +1,152 @@ +# 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 datetime import datetime # noqa +from datetime import timedelta # noqa + +import json +import logging + +from django.http import HttpResponse # noqa +from django.utils.translation import ugettext_lazy as _ # noqa +from django.views.generic import TemplateView # noqa + +from horizon import exceptions +from horizon import tabs + +from openstack_dashboard import api +from openstack_dashboard.api import ceilometer + +from openstack_dashboard.dashboards.admin.metering import tabs as \ + metering_tabs + +LOG = logging.getLogger(__name__) + + +class IndexView(tabs.TabbedTableView): + tab_group_class = metering_tabs.CeilometerOverviewTabs + template_name = 'admin/metering/index.html' + + +class SamplesView(TemplateView): + template_name = "admin/metering/samples.csv" + + def get(self, request, *args, **kwargs): + meter = request.GET.get('meter', None) + meter_name = meter.replace(".", "_") + date_options = request.GET.get('date_options', None) + date_from = request.GET.get('date_from', None) + date_to = request.GET.get('date_to', None) + resource = request.GET.get('resource', None) + stats_attr = request.GET.get('stats_attr', 'avg') + + if (date_options == "other"): + try: + if date_from: + date_from = datetime.strptime(date_from, + "%Y-%m-%d") + if date_to: + date_to = datetime.strptime(date_to, + "%Y-%m-%d") + except ValueError: + raise exceptions.NotFound + else: + date_from = datetime.now() - timedelta(days=int(date_options)) + date_to = datetime.now() + + query = [{"field": "metadata.OS-EXT-AZ:availability_zone", + "op": "eq", + "value": "nova"}] + + 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}] + + if request.GET.get('group_by', None) == "project": + try: + tenants, more = api.keystone.tenant_list( + request, + domain=None, + paginate=True, + marker="tenant_marker") + except Exception: + tenants = [] + exceptions.handle(request, + _('Unable to retrieve tenant 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=1000, stats_attr=None, + additional_query=additional_query) + + series = [] + for resource in resources: + name = resource.id + if getattr(resource, meter_name): + serie = {'unit': getattr(getattr(resource, meter_name)[0], + 'unit', ""), + 'name': name, + 'data': []} + + for statistic in getattr(resource, meter_name): + date = statistic.duration_end[:19] + value = int(getattr(statistic, stats_attr)) + serie['data'].append({'x': date, 'y': value}) + + series.append(serie) + else: + ceilometer_usage = ceilometer.CeilometerUsage(request) + try: + resources = ceilometer_usage.resources_with_statistics( + query, [meter], period=1000, stats_attr=None, + additional_query=additional_query) + except Exception: + resources = [] + exceptions.handle(request, + _('Unable to retrieve statistics.')) + + series = [] + for resource in resources: + if getattr(resource, meter_name): + serie = {'unit': getattr(getattr(resource, meter_name)[0], + 'unit', ""), + 'name': resource.resource_id, + 'data': []} + for statistic in getattr(resource, meter_name): + date = statistic.duration_end[:19] + value = int(getattr(statistic, stats_attr)) + serie['data'].append({'x': date, 'y': value}) + + series.append(serie) + + ret = {} + ret['series'] = series + ret['settings'] = {} + + return HttpResponse(json.dumps(ret), + mimetype='application/json') diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less index 2c8fbffdf2..2c790ae761 100644 --- a/openstack_dashboard/static/dashboard/less/horizon.less +++ b/openstack_dashboard/static/dashboard/less/horizon.less @@ -2031,6 +2031,7 @@ label.log-length { top: -100px; } + /**** Resource Topology CSS ****/ .link {stroke: #999;stroke-width: 1.5px;} .node {cursor:pointer;} @@ -2049,3 +2050,24 @@ label.log-length { #info_box p {margin:0;font-size:9pt;line-height:14px;} #info_box a {margin:0;font-size:9pt;line-height:14px;} #info_box .error {color:darkred;} + +#ceilometer-stats .form-horizontal { + .control-label { + width: auto; + } + .controls { + float: left; + margin-left: 0; + } + .control-group { + float: left; + margin-right: 20px; + } + .btn { + float: left; + margin-right: 20px; + margin-bottom: 18px; + } +} + + diff --git a/openstack_dashboard/test/test_data/ceilometer_data.py b/openstack_dashboard/test/test_data/ceilometer_data.py index 228e2158bc..8e03070138 100644 --- a/openstack_dashboard/test/test_data/ceilometer_data.py +++ b/openstack_dashboard/test/test_data/ceilometer_data.py @@ -36,6 +36,7 @@ def data(TEST): TEST.global_network_usages = utils.TestDataContainer() TEST.global_network_traffic_usages = utils.TestDataContainer() TEST.global_object_store_usages = utils.TestDataContainer() + TEST.statistics_array = utils.TestDataContainer() # users ceilometer_user_dict1 = {'id': "1", @@ -93,6 +94,7 @@ def data(TEST): user_id="fake_user_id", timestamp='2012-07-02T10:42:00.000000', metadata={'tag': 'self.counter3', 'display_name': 'test-server'}, + links=[{'url': 'test_url', 'rel': 'storage.objects'}], ) resource_dict_2 = dict( resource_id='fake_resource_id2', @@ -100,6 +102,7 @@ def data(TEST): user_id="fake_user_id", timestamp='2012-07-02T10:42:00.000000', metadata={'tag': 'self.counter3', 'display_name': 'test-server'}, + links=[{'url': 'test_url', 'rel': 'storage.objects'}], ) resource_1 = resources.Resource(resources.ResourceManager(None), resource_dict_1)