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:
Ladislav Smola 2013-08-28 10:37:26 +02:00 committed by Kieran Spear
parent 8ac4267a8d
commit bef4ee0489
12 changed files with 1207 additions and 1 deletions

View File

@ -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')

View 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)

View 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

View 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

View File

@ -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 %}

View File

@ -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" %}:&nbsp;</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" %}:&nbsp;</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" %}:&nbsp;</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" %}:&nbsp;</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" %}:&nbsp;</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" %}:&nbsp;</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>

View 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": {}}'])

View 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'))

View 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')

View File

@ -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;
}
}

View File

@ -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)