Add metering panel to admin console
Now it has usage tables for disk usage, network traffic usage, network(neutron) usage, objectstore usage, all agregates of project. A multiseries linechart with choosable meters, group by, value and time span. Reasonable amount of tests implemented over all resource usage tabs. Change-Id: I21342d595ee08da45707e909f3d9802707d912cc Implements: blueprint admin-resource-usage-page
This commit is contained in:
parent
8ac4267a8d
commit
bef4ee0489
@ -22,7 +22,7 @@ import horizon
|
|||||||
class SystemPanels(horizon.PanelGroup):
|
class SystemPanels(horizon.PanelGroup):
|
||||||
slug = "admin"
|
slug = "admin"
|
||||||
name = _("System Panel")
|
name = _("System Panel")
|
||||||
panels = ('overview', 'hypervisors', 'instances', 'volumes',
|
panels = ('overview', 'metering', 'hypervisors', 'instances', 'volumes',
|
||||||
'flavors', 'images', 'networks', 'routers', 'defaults', 'info')
|
'flavors', 'images', 'networks', 'routers', 'defaults', 'info')
|
||||||
|
|
||||||
|
|
||||||
|
27
openstack_dashboard/dashboards/admin/metering/panel.py
Normal file
27
openstack_dashboard/dashboards/admin/metering/panel.py
Normal file
@ -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)
|
217
openstack_dashboard/dashboards/admin/metering/tables.py
Normal file
217
openstack_dashboard/dashboards/admin/metering/tables.py
Normal file
@ -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
|
223
openstack_dashboard/dashboards/admin/metering/tabs.py
Normal file
223
openstack_dashboard/dashboards/admin/metering/tabs.py
Normal file
@ -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:<type>": _("Duration of instance <type>"
|
||||||
|
" (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
|
@ -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 %}
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12">
|
||||||
|
{{ tab_group.render }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,180 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load url from future %}
|
||||||
|
|
||||||
|
<div id="samples_url" url="{% url "horizon:admin:metering:samples" %}"></div>
|
||||||
|
<div id="ceilometer-stats">
|
||||||
|
<form class="form-horizontal"
|
||||||
|
id="linechart_general_form">
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="meter" class="control-label">{% trans "Metric" %}: </label>
|
||||||
|
<div class="controls line_chart_time_picker">
|
||||||
|
<select data-line-chart-command="select_box_change"
|
||||||
|
name="meter" id="meter" class="span2 example">
|
||||||
|
<optgroup label='{% trans "Compute (Nova)" %}'>
|
||||||
|
{% for meter in meters %}
|
||||||
|
<option title="{{ meter.title }}" value="{{ meter.name }}" data-unit="">
|
||||||
|
{{ meter.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label='{% trans "Network (Neutron)" %}'>
|
||||||
|
<option title='{% trans "Duration of network" %}' value="network" data-unit="">network</option>
|
||||||
|
<option title='{% trans "Creation requests for this network" %}' value="network.create" data-unit="">network.create</option>
|
||||||
|
<option title='{% trans "Update requests for this network" %}' value="network.update" data-unit="">network.update</option>
|
||||||
|
<option title='{% trans "Duration of subnet" %}' value="subnet" data-unit="">subnet</option>
|
||||||
|
<option title='{% trans "Creation requests for this subnet" %}' value="subnet.create" data-unit="">subnet.create</option>
|
||||||
|
<option title='{% trans "Update requests for this subnet" %}' value="subnet.update" data-unit="">subnet.update</option>
|
||||||
|
<option title='{% trans "Creation requests for this port" %}' value="port.create" data-unit="">port.create</option>
|
||||||
|
<option title='{% trans "Update requests for this port" %}' value="port.update" data-unit="">port.update</option>
|
||||||
|
<option title='{% trans "Duration of router" %}' value="router" data-unit="">router</option>
|
||||||
|
<option title='{% trans "Creation requests for this router" %}' value="router.create" data-unit="">router.create</option>
|
||||||
|
<option title='{% trans "Update requests for this router" %}' value="router.update" data-unit="">router.update</option>
|
||||||
|
<option title='{% trans "Duration of floating ip" %}' value="ip.floating" data-unit="">ip.floating</option>
|
||||||
|
<option title='{% trans "Creation requests for this floating ip" %}' value="ip.floating.create" data-unit="">ip.floating.create</option>
|
||||||
|
<option title='{% trans "Update requests for this floating ip" %}' value="ip.floating.update" data-unit="">ip.floating.update</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label='{% trans "Image (Glance)" %}'>
|
||||||
|
<option title='{% trans "Uploaded image size" %}' value="image.size" data-unit="">image.size</option>
|
||||||
|
<option title='{% trans "Number of update on the image" %}' value="image.update " data-unit="">image.update </option>
|
||||||
|
<option title='{% trans "Number of upload of the image" %}' value="image.upload " data-unit="">image.upload </option>
|
||||||
|
<option title='{% trans "Number of delete on the image" %}' value="image.delete " data-unit="">image.delete </option>
|
||||||
|
<option title='{% trans "Image is downloaded" %}' value="image.download" data-unit="">image.download</option>
|
||||||
|
<option title='{% trans "Image is served out" %}' value="image.serve" data-unit="">image.serve</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label='{% trans "Volume (Cinder)" %}'>
|
||||||
|
<option title='{% trans "Duration of volume" %}' value="volume" data-unit="">volume</option>
|
||||||
|
<option title='{% trans "Size of volume" %}' value="volume.size" data-unit="">volume.size</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label='{% trans "Object Storage (Swift)" %}'>
|
||||||
|
<option title='{% trans "Number of objects" %}' value="storage.objects" data-unit="">storage.objects</option>
|
||||||
|
<option title='{% trans "Total size of stored objects" %}' value="storage.objects.size" data-unit="">storage.objects.size</option>
|
||||||
|
<option title='{% trans "Number of containers" %}' value="" data-unit="storage.objects.containers">storage.objects.containers</option>
|
||||||
|
<option title='{% trans "Number of incoming bytes" %}' value="storage.objects.incoming.bytes" data-unit="">storage.objects.incoming.bytes</option>
|
||||||
|
<option title='{% trans "Number of outgoing bytes" %}' value="storage.objects.outgoing.bytes" data-unit="">storage.objects.outgoing.bytes</option>
|
||||||
|
<option title='{% trans "Number of API requests against swift" %}' value="storage.api.request" data-unit="">storage.api.request</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label='{% trans "Energy (Kwapi)" %}'>
|
||||||
|
<option title='{% trans "Amount of energy" %}' value="energy" data-unit="">energy</option>
|
||||||
|
<option title='{% trans "Power consumption" %}' value="power" data-unit="">power</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="group_by" class="control-label">{% trans "Group by" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<select data-line-chart-command="select_box_change"
|
||||||
|
id="group_by" name="group_by" class="span2">
|
||||||
|
<option value="" selected="selected">{% trans "--" %}</option>
|
||||||
|
<option selected="selected" value="project" selected>{% trans "Project" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="stats_attr" class="control-label">{% trans "Value" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<select data-line-chart-command="select_box_change"
|
||||||
|
id="stats_attr" name="stats_attr" class="span2">
|
||||||
|
|
||||||
|
<option selected="selected" value="avg">{% trans "Avg." %}</option>
|
||||||
|
<option value="min">{% trans "Min." %}</option>
|
||||||
|
<option value="max">{% trans "Max." %}</option>
|
||||||
|
<option value="sum">{% trans "Sum." %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="date_options" class="control-label">{% trans "Period" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<select data-line-chart-command="select_box_change"
|
||||||
|
id="date_options" name="date_options" class="span2">
|
||||||
|
<option value="1">{% trans "Last day" %}</option>
|
||||||
|
<option value="7" selected="selected">{% trans "Last week" %}</option>
|
||||||
|
<option value="15">{% trans "Last 15 days" %}</option>
|
||||||
|
<option value="30">{% trans "Last 30 days" %}</option>
|
||||||
|
<option value="365">{% trans "Last year" %}</option>
|
||||||
|
<option value="other">{% trans "Other" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" id="date_from">
|
||||||
|
<label for="date_from" class="control-label">{% trans "From" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<input data-line-chart-command="date_picker_change"
|
||||||
|
type="text" id="date_from" name="date_from" class="span2 example"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" id="date_to">
|
||||||
|
<label for="date_to" class="control-label">{% trans "To" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<input data-line-chart-command="date_picker_change"
|
||||||
|
type="text" name="date_to" class="span2 example"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info row-fluid detail">
|
||||||
|
<div class="span12">
|
||||||
|
<h4>{% trans "Statistics of all resources" %}</h4>
|
||||||
|
<hr class="header_rule" />
|
||||||
|
<div class="info row-fluid detail">
|
||||||
|
<div class="span9 chart_container">
|
||||||
|
<div class="chart"
|
||||||
|
data-chart-type="line_chart"
|
||||||
|
data-url="{% url 'horizon:admin:metering:samples'%}"
|
||||||
|
data-form-selector='#linechart_general_form'
|
||||||
|
data-legend-selector="#legend"
|
||||||
|
data-smoother-selector="#smoother"
|
||||||
|
data-slider-selector="#slider">
|
||||||
|
</div>
|
||||||
|
<div id="slider"></div>
|
||||||
|
</div>
|
||||||
|
<div class="span3 legend_container">
|
||||||
|
<div id="smoother" title="Smoothing"></div>
|
||||||
|
<div id="legend"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
if (typeof horizon.d3_line_chart !== 'undefined') {
|
||||||
|
horizon.d3_line_chart.init("div[data-chart-type='line_chart']");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
show_hide_datepickers();
|
||||||
|
} else {
|
||||||
|
addHorizonLoadEvent(function() {
|
||||||
|
show_hide_datepickers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_hide_datepickers() {
|
||||||
|
$("#date_options").change(function(evt) {
|
||||||
|
// Enhancing behaviour of selectbox, on 'other' value selected, I don't
|
||||||
|
// want to refresh, but show hide the date fields
|
||||||
|
if ($(this).find("option:selected").val() == "other"){
|
||||||
|
evt.stopPropagation();
|
||||||
|
$("#date_from, #date_to").show();
|
||||||
|
} else {
|
||||||
|
$("#date_from, #date_to").hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ($("#date_options").find("option:selected").val() == "other"){
|
||||||
|
$("#date_from, #date_to").show();
|
||||||
|
} else {
|
||||||
|
$("#date_from, #date_to").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
345
openstack_dashboard/dashboards/admin/metering/tests.py
Normal file
345
openstack_dashboard/dashboards/admin/metering/tests.py
Normal file
@ -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": {}}'])
|
22
openstack_dashboard/dashboards/admin/metering/urls.py
Normal file
22
openstack_dashboard/dashboards/admin/metering/urls.py
Normal file
@ -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'))
|
152
openstack_dashboard/dashboards/admin/metering/views.py
Normal file
152
openstack_dashboard/dashboards/admin/metering/views.py
Normal file
@ -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')
|
@ -2031,6 +2031,7 @@ label.log-length {
|
|||||||
top: -100px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**** Resource Topology CSS ****/
|
/**** Resource Topology CSS ****/
|
||||||
.link {stroke: #999;stroke-width: 1.5px;}
|
.link {stroke: #999;stroke-width: 1.5px;}
|
||||||
.node {cursor:pointer;}
|
.node {cursor:pointer;}
|
||||||
@ -2049,3 +2050,24 @@ label.log-length {
|
|||||||
#info_box p {margin:0;font-size:9pt;line-height:14px;}
|
#info_box p {margin:0;font-size:9pt;line-height:14px;}
|
||||||
#info_box a {margin:0;font-size:9pt;line-height:14px;}
|
#info_box a {margin:0;font-size:9pt;line-height:14px;}
|
||||||
#info_box .error {color:darkred;}
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ def data(TEST):
|
|||||||
TEST.global_network_usages = utils.TestDataContainer()
|
TEST.global_network_usages = utils.TestDataContainer()
|
||||||
TEST.global_network_traffic_usages = utils.TestDataContainer()
|
TEST.global_network_traffic_usages = utils.TestDataContainer()
|
||||||
TEST.global_object_store_usages = utils.TestDataContainer()
|
TEST.global_object_store_usages = utils.TestDataContainer()
|
||||||
|
TEST.statistics_array = utils.TestDataContainer()
|
||||||
|
|
||||||
# users
|
# users
|
||||||
ceilometer_user_dict1 = {'id': "1",
|
ceilometer_user_dict1 = {'id': "1",
|
||||||
@ -93,6 +94,7 @@ def data(TEST):
|
|||||||
user_id="fake_user_id",
|
user_id="fake_user_id",
|
||||||
timestamp='2012-07-02T10:42:00.000000',
|
timestamp='2012-07-02T10:42:00.000000',
|
||||||
metadata={'tag': 'self.counter3', 'display_name': 'test-server'},
|
metadata={'tag': 'self.counter3', 'display_name': 'test-server'},
|
||||||
|
links=[{'url': 'test_url', 'rel': 'storage.objects'}],
|
||||||
)
|
)
|
||||||
resource_dict_2 = dict(
|
resource_dict_2 = dict(
|
||||||
resource_id='fake_resource_id2',
|
resource_id='fake_resource_id2',
|
||||||
@ -100,6 +102,7 @@ def data(TEST):
|
|||||||
user_id="fake_user_id",
|
user_id="fake_user_id",
|
||||||
timestamp='2012-07-02T10:42:00.000000',
|
timestamp='2012-07-02T10:42:00.000000',
|
||||||
metadata={'tag': 'self.counter3', 'display_name': 'test-server'},
|
metadata={'tag': 'self.counter3', 'display_name': 'test-server'},
|
||||||
|
links=[{'url': 'test_url', 'rel': 'storage.objects'}],
|
||||||
)
|
)
|
||||||
resource_1 = resources.Resource(resources.ResourceManager(None),
|
resource_1 = resources.Resource(resources.ResourceManager(None),
|
||||||
resource_dict_1)
|
resource_dict_1)
|
||||||
|
Loading…
Reference in New Issue
Block a user