File ceilometer-horizon.patch of Package openstack-dashboard
--- horizon/static/horizon/js/horizon.ceilometer.js
+++ horizon/static/horizon/js/horizon.ceilometer.js
@@ -0,0 +1,221 @@
+horizon.ceilometer = {
+ initVariables: function () {
+ var self = this;
+ self.margin = {
+ top: 40,
+ right: 100,
+ bottom: 80,
+ left: 200
+ };
+ self.width = 1100 - self.margin.left - self.margin.right;
+ self.height = 480 - self.margin.top - self.margin.bottom;
+ self.svg = undefined;
+ self.x = d3.time.scale().range([0, self.width]);
+ self.y = d3.scale.linear().range([self.height, 0]);
+ self.xAxis = d3.svg.axis().scale(self.x).orient("bottom");
+ self.yAxis = d3.svg.axis().scale(self.y).orient("left");
+ self.line = d3.svg.line()
+ .x(function (d) {
+ return self.x(d.date);
+ })
+ .y(function (d) {
+ return self.y(d.value);
+ });
+
+ self.$meter = $("#meter");
+ self.$resource = $("#resource");
+ self.$date_from = $("#date_from");
+ self.$date_to = $("#date_to");
+ self.$get_samples = $("#samples_url");
+ self.$chart_container = $("#chart_container");
+ self.parseDate = d3.time.format("%Y-%m-%dT%H:%M:%S").parse;
+ },
+ getResources: function (meters) {
+ // Get ``meters`` variable from django template.
+ var self = this;
+ if (meters) {
+ self.meters = meters;
+ }
+ },
+
+ updateResourceOptions: function () {
+ // Update resource select field on stats.html template.
+ var self = this,
+ meter_name = self.$meter.val(),
+ i,
+ option;
+ self.$resource.empty();
+ if (self.meters) {
+ for (i = 0; i < self.meters.length; i = i + 1) {
+ if (meter_name === self.meters[i].name) {
+ option = '<option value="' + self.meters[i].resource_id + '">';
+ option += self.meters[i].resource_id + '</option>';
+ self.$resource.append(option);
+ }
+ }
+ }
+ },
+
+ loadChartData: function () {
+ var self = this,
+ meter = self.$meter.val(),
+ resource = self.$resource.val(),
+ from = self.$date_from.val(),
+ to = self.$date_to.val(),
+ horizon_samples_url = self.$get_samples.attr("url");
+ d3.select("svg").remove();
+
+ self.svg = d3.select("#chart_container").append("svg")
+ .attr("width", self.width + self.margin.left + self.margin.right)
+ .attr("height", self.height + self.margin.top + self.margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + self.margin.left + "," + self.margin.top + ")");
+
+ if (meter && resource) {
+ d3.csv(horizon_samples_url + "?meter=" + meter + "&resource=" + resource + "&from=" + from + "&to=" + to,
+ function (error, data) {
+ var chart_title = self.$meter.val() + " ";
+ chart_title += gettext("for resource") + " " + self.$resource.val();
+ chart_title += " (" + gettext("From") + " " + self.$date_to.val() + " " + gettext("to") + " ";
+ chart_title += self.$date_to.val() + ")";
+
+ // read selected option
+ var option = self.$meter.find("option").filter(":selected");
+ var meter_text = gettext("Value");
+ if (option) {
+ var unit = option[0].getAttribute("data-unit");
+ meter_text = meter_text + " (" + unit + ")";
+ }
+
+ data.forEach(function (d) {
+ d.date = self.parseDate(d.date);
+ d.value = +d.value;
+ });
+
+ self.x.domain(d3.extent(data, function (d) {
+ return d.date;
+ }));
+
+ var min_value = d3.min(data, function(d){return d.value});
+ var max_value = d3.max(data, function(d){return d.value});
+ if (min_value === max_value) {
+ self.y.domain([0, min_value * 2]);
+ } else {
+ self.y.domain(d3.extent(data, function (d) {
+ return d.value;
+ }));
+
+ }
+
+ self.svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + self.height + ")")
+ .call(self.xAxis);
+ self.svg.append("g")
+ .attr("class", "y axis")
+ .call(self.yAxis)
+ .append("text")
+ .attr("transform", "rotate(-90)")
+ .attr("y", 6)
+ .attr("dy", ".71em")
+ .style("text-anchor", "end")
+ .text(meter_text);
+
+ self.svg.append("path")
+ .datum(data)
+ .attr("class", "line")
+ .style("stroke", "steelblue")
+ .style("stroke-width", 1.5)
+ .style("fill", "none")
+ .attr("d", self.line);
+
+ self.svg.append("text")
+ .attr("x", (self.width / 2))
+ .attr("y", -(self.margin.top / 2))
+ .attr("text-anchor", "middle")
+ .style("font-size", "14px")
+ .style("text-decoration", "none")
+ .text(chart_title);
+
+ d3.selectAll("path.domain")
+ .style("fill", "none")
+ .style("stroke", "black")
+ .style("stroke-width", 1);
+
+ d3.selectAll(".axis path")
+ .style("fill", "none")
+ .style("stroke", "black")
+ .style("shape-rendering", "crispEdges");
+
+ d3.selectAll(".axis")
+ .style("font-size", "10px");
+
+ d3.selectAll(".axis line")
+ .style("fill", "none")
+ .style("stroke", "black")
+ .style("shape-rendering", "crispEdges");
+ });
+ }
+ },
+
+ refreshPickers: function (date_interval) {
+ var now;
+ var targetDate;
+ var self = this;
+ now = new Date();
+ self.$date_to.datepicker('setValue', now);
+ targetDate = new Date();
+ targetDate.setDate(now.getDate() - date_interval);
+ self.$date_from.datepicker('setValue', targetDate);
+ },
+
+ init: function () {
+ var self = this,
+ to,
+ from;
+ self.initVariables();
+ from = self.$date_from.datepicker({format: "yyyy-mm-dd"})
+ .on('changeDate', function (ev) {
+ if (ev.date.valueOf() > to.date.valueOf()) {
+ var newDate = new Date(ev.date);
+ newDate.setDate(newDate.getDate() + 1);
+ to.setValue(newDate);
+ }
+ from.hide();
+ self.$date_to[0].focus();
+ }).data('datepicker');
+ to = self.$date_to.datepicker({
+ format: "yyyy-mm-dd",
+ onRender: function (date) { return date.valueOf() <= from.date.valueOf() ? 'disabled' : ''; }
+ }).on('changeDate', function () {
+ to.hide();
+ self.loadChartData();
+ }).data('datepicker');
+
+ $(".action_display_chart").click(function () {
+ self.loadChartData();
+ });
+
+ self.$meter.change(function () {
+ self.updateResourceOptions();
+ self.loadChartData();
+ });
+ self.$resource.change(function () {
+ self.loadChartData();
+ });
+
+ $("#date_options").change(function () {
+ var current = $(this).val();
+ if (current) {
+ self.refreshPickers(current);
+ self.loadChartData();
+ }
+ });
+ self.refreshPickers(1);
+ self.loadChartData();
+ }
+};
+
+horizon.addInitFunction(function () {
+ horizon.ceilometer.init();
+});
--- openstack_dashboard/api/ceilometer.py
+++ openstack_dashboard/api/ceilometer.py
@@ -0,0 +1,303 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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 itertools
+import logging
+
+from ceilometerclient import client as ceilometer_client
+from django.conf import settings
+
+from .base import APIDictWrapper
+from .base import APIResourceWrapper
+from .base import url_for
+
+import keystone
+
+
+LOG = logging.getLogger(__name__)
+
+
+class Meter(APIResourceWrapper):
+ _attrs = ['name', 'type', 'unit', 'resource_id', 'user_id',
+ 'project_id']
+
+
+class Resource(APIResourceWrapper):
+ _attrs = ['resource_id', "source", "user_id", "project_id", "metadata"]
+
+ @property
+ def name(self):
+ name = self.metadata.get("name", None)
+ display_name = self.metadata.get("display_name", None)
+ return name or display_name or ""
+
+
+class Sample(APIResourceWrapper):
+ _attrs = ['counter_name', 'user_id', 'resource_id', 'timestamp',
+ 'resource_metadata', 'source', 'counter_unit', 'counter_volume',
+ 'project_id', 'counter_type', 'resource_metadata']
+
+ @property
+ def instance(self):
+ if 'display_name' in self.resource_metadata:
+ return self.resource_metadata['display_name']
+ elif 'instance_id' in self.resource_metadata:
+ return self.resource_metadata['instance_id']
+ else:
+ return None
+
+ @property
+ def name(self):
+ name = self.resource_metadata.get("name", None)
+ display_name = self.resource_metadata.get("display_name", None)
+ return name or display_name or ""
+
+
+class GlobalObjectStoreUsage(APIDictWrapper):
+ _attrs = ["id", "tenant", "user", "resource", "storage_objects",
+ "storage_objects_size", "storage_objects_outgoing_bytes",
+ "storage_objects_incoming_bytes"]
+
+
+class GlobalDiskUsage(APIDictWrapper):
+ _attrs = ["id", "tenant", "user", "resource", "disk_read_bytes",
+ "disk_read_requests", "disk_write_bytes",
+ "disk_write_requests"]
+
+
+class GlobalNetworkTrafficUsage(APIDictWrapper):
+ _attrs = ["id", "tenant", "user", "resource", "network_incoming_bytes",
+ "network_incoming_packets", "network_outgoing_bytes",
+ "network_outgoing_packets"]
+
+
+class GlobalNetworkUsage(APIDictWrapper):
+ _attrs = ["id", "tenant", "user", "resource", "network", "network_create",
+ "subnet", "subnet_create", "port", "port_create", "router",
+ "router_create", "ip_floating", "ip_floating_create"]
+
+
+class Statistic(APIResourceWrapper):
+ _attrs = ['period', 'period_start', 'period_end',
+ 'count', 'min', 'max', 'sum', 'avg',
+ 'duration', 'duration_start', 'duration_end']
+
+
+def ceilometerclient(request):
+ endpoint = url_for(request, 'metering')
+ insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
+ LOG.debug('ceilometerclient connection created using token "%s" '
+ 'and endpoint "%s"' % (request.user.token.id, endpoint))
+ return ceilometer_client.Client('2', endpoint, token=request.user.token.id,
+ insecure=insecure)
+
+
+def sample_list(request, meter_name, query=[]):
+ """List the samples for this meters."""
+ samples = ceilometerclient(request).samples.list(meter_name=meter_name,
+ q=query)
+ return [Sample(s) for s in samples]
+
+
+def meter_list(request, query=[]):
+ """List the user's meters."""
+ meters = ceilometerclient(request).meters.list(query)
+ return [Meter(m) for m in meters]
+
+
+def resource_list(request, query=[]):
+ """List the resources."""
+ resources = ceilometerclient(request).\
+ resources.list(q=query)
+ return [Resource(r) for r in resources]
+
+
+def statistic_list(request, meter_name, query=[]):
+ statistics = ceilometerclient(request).\
+ statistics.list(meter_name=meter_name, q=query)
+ return [Statistic(s) for s in statistics]
+
+
+def global_object_store_usage(request):
+ result_list = global_usage_preload(request,
+ ["storage.objects",
+ "storage.objects.size",
+ "storage.objects.incoming.bytes",
+ "storage.objects.outgoing.bytes"])
+ return [GlobalObjectStoreUsage(u) for u in result_list]
+
+
+def global_object_store_usage_get(request, usage_id):
+ meter_names = ["storage.objects",
+ "storage.objects.size",
+ "storage.objects.incoming.bytes",
+ "storage.objects.outgoing.bytes"]
+ usage = global_usage_get(request, meter_names, usage_id)
+ return GlobalObjectStoreUsage(usage)
+
+
+def global_disk_usage(request):
+ result_list = global_usage_preload(request, ["disk.read.bytes",
+ "disk.read.requests",
+ "disk.write.bytes",
+ "disk.write.requests"])
+ return [GlobalDiskUsage(u) for u in result_list]
+
+
+def global_disk_usage_get(request, usage_id):
+ meter_names = ["disk.read.bytes",
+ "disk.read.requests",
+ "disk.write.bytes",
+ "disk.write.requests"]
+ usage = global_usage_get(request, meter_names, usage_id)
+ return GlobalDiskUsage(usage)
+
+
+def global_network_traffic_usage(request):
+ result_list = global_usage_preload(request, ["network.incoming.bytes",
+ "network.incoming.packets",
+ "network.outgoing.bytes",
+ "network.outgoing.packets"])
+ return [GlobalNetworkTrafficUsage(u) for u in result_list]
+
+
+def global_network_traffic_usage_get(request, usage_id):
+ meter_names = ["network.incoming.bytes",
+ "network.incoming.packets",
+ "network.outgoing.bytes",
+ "network.outgoing.packets"]
+ usage = global_usage_get(request, meter_names, usage_id)
+ return GlobalNetworkTrafficUsage(usage)
+
+
+def global_network_usage(request):
+ result_list = global_usage_preload(request,
+ ["network", "network_create",
+ "subnet", "subnet_create",
+ "port", "port_create",
+ "router", "router_create",
+ "ip_floating", "ip_floating_create"])
+ return [GlobalNetworkUsage(u) for u in result_list]
+
+
+def global_network_usage_get(request, usage_id):
+ meter_names = ["network", "network_create",
+ "subnet", "subnet_create",
+ "port", "port_create",
+ "router", "router_create",
+ "ip_floating", "ip_floating_create"]
+ usage = global_usage_get(request, meter_names, usage_id)
+ return GlobalNetworkUsage(usage)
+
+
+def global_usage_get(request, meter_names, usage_id):
+ try:
+ tenant_id, user_id, resource_id = usage_id.split("__")
+ except ValueError:
+ return []
+
+ query = []
+ if user_id and user_id != 'None':
+ query.append({"field": "user", "op": "eq", "value": user_id})
+ if tenant_id and tenant_id != 'None':
+ query.append({"field": "project", "op": "eq", "value": tenant_id})
+ if resource_id and resource_id != 'None':
+ query.append({"field": "resource", "op": "eq", "value": resource_id})
+
+ usage_list = []
+ usage = dict(id=usage_id,
+ tenant=CachedResources.get_tenant_name(request,
+ tenant_id),
+ user=CachedResources.get_user_name(request,
+ user_id),
+ resource=resource_id)
+ for meter in meter_names:
+ statistics = statistic_list(request, meter,
+ query=query)
+ meter = meter.replace(".", "_")
+ if statistics:
+ usage.setdefault(meter, statistics[0].max)
+ else:
+ usage.setdefault(meter, 0)
+
+ usage_list.append(usage)
+
+ usage_list = itertools.groupby(
+ usage_list,
+ lambda x: x["id"],
+ )
+ usage_list = map(lambda x: list(x[1]), usage_list)
+ usage_list = reduce(lambda x, y: x.update(y) or x, usage_list)
+ return usage_list[0]
+
+
+def global_usage_preload(request, fields):
+ """
+ Get "user", "tenant", "resource" for a horizon table datum
+ without actually data.
+ The data will be loaded asynchronously via ``global_usage_get``.
+ """
+ meters = CachedResources.get_meter_list(request)
+
+ filtered = filter(lambda m: m.name in fields, meters)
+
+ usage_list = []
+ for m in filtered:
+ usage_list.append({
+ "id": "%s__%s__%s" % (m.project_id, m.user_id, m.resource_id),
+ "tenant": CachedResources.get_tenant_name(request,
+ m.project_id),
+ "user": CachedResources.get_user_name(request,
+ m.user_id),
+ "resource": m.resource_id
+ })
+ # To remove redundent usage.
+ tupled_usage_list = [tuple(d.items()) for d in usage_list]
+ unique_usage_list = [dict(t) for t in set(tupled_usage_list)]
+ return unique_usage_list
+
+
+class CachedResources(object):
+ """
+ Cached users, tenants and meters.
+ """
+ _users = None
+ _tenants = None
+ _meters = None
+
+ @classmethod
+ def get_meter_list(cls, request, query=None):
+ if not cls._meters:
+ cls._meters = meter_list(request, query)
+ return cls._meters
+
+ @classmethod
+ def get_user_name(cls, request, user_id):
+ if not cls._users:
+ cls._users = keystone.user_list(request)
+ for u in cls._users:
+ if u.id == user_id:
+ return u.name
+ return None
+
+ @classmethod
+ def get_tenant_name(cls, request, tenant_id):
+ if not cls._tenants:
+ cls._tenants, more = keystone.tenant_list(request, admin=True)
+ for t in cls._tenants:
+ if t.id == tenant_id:
+ return t.name
+ return None
--- openstack_dashboard/api/__init__.py
+++ openstack_dashboard/api/__init__.py
@@ -34,6 +34,7 @@
Keystone/Nova/Glance/Swift et. al.
"""
from openstack_dashboard.api import base
+from openstack_dashboard.api import ceilometer
from openstack_dashboard.api import cinder
from openstack_dashboard.api import glance
from openstack_dashboard.api import keystone
--- openstack_dashboard/dashboards/admin/dashboard.py
+++ openstack_dashboard/dashboards/admin/dashboard.py
@@ -22,7 +22,7 @@
class SystemPanels(horizon.PanelGroup):
slug = "admin"
name = _("System Panel")
- panels = ('overview', 'instances', 'volumes', 'flavors',
+ panels = ('overview', 'metering', 'instances', 'volumes', 'flavors',
'images', 'projects', 'users', 'networks', 'routers', 'info')
--- openstack_dashboard/dashboards/admin/metering/__init__.py
+++ openstack_dashboard/dashboards/admin/metering/__init__.py
@@ -0,0 +1 @@
+# empty file
--- openstack_dashboard/dashboards/admin/metering/panel.py
+++ openstack_dashboard/dashboards/admin/metering/panel.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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 _
+
+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)
--- openstack_dashboard/dashboards/admin/metering/tables.py
+++ openstack_dashboard/dashboards/admin/metering/tables.py
@@ -0,0 +1,257 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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
+from django.utils.translation import ugettext_lazy as _
+
+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):
+ def transform(datum):
+ if any([datum.get(field) is 0 or datum.get(field)
+ for field in fields]):
+ return "up"
+ else:
+ return "none"
+ return transform
+
+
+class GlobalDiskUsageUpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, object_id):
+ return api.ceilometer.global_disk_usage_get(request, object_id)
+
+
+class GlobalDiskUsageTable(tables.DataTable):
+ tenant = tables.Column("tenant", verbose_name=_("Tenant"), sortable=True)
+ user = tables.Column("user", verbose_name=_("User"), sortable=True)
+ status = tables.Column(get_status(["disk_read_bytes",
+ "disk_write_bytes",
+ "disk_read_requests",
+ "disk_write_requests"
+ ]),
+ verbose_name=_("Status"),
+ hidden=True)
+ instance = tables.Column("resource",
+ verbose_name=_("Resource"),
+ 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")
+ status_columns = ["status"]
+ table_actions = (CommonFilterAction,)
+ row_class = GlobalDiskUsageUpdateRow
+ multi_select = False
+
+
+class GlobalNetworkTrafficUsageUpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, object_id):
+ return api.ceilometer\
+ .global_network_traffic_usage_get(request,
+ object_id)
+
+
+class GlobalNetworkTrafficUsageTable(tables.DataTable):
+ tenant = tables.Column("tenant", verbose_name=_("Tenant"))
+ user = tables.Column("user", verbose_name=_("User"), sortable=True)
+ status = tables.Column(get_status(["network_incoming_bytes",
+ "network_incoming_packets",
+ "network_outgoing_bytes",
+ "network_outgoing_packets"]),
+ verbose_name=_("Status"),
+ hidden=True)
+ instance = tables.Column("resource",
+ verbose_name=_("Resource"),
+ 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")
+ table_actions = (CommonFilterAction,)
+ row_class = GlobalNetworkTrafficUsageUpdateRow
+ status_columns = ["status"]
+ multi_select = False
+
+
+class GlobalNetworkUsageUpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, object_id):
+ return api.ceilometer.global_network_usage_get(request,
+ object_id)
+
+
+class GlobalNetworkUsageTable(tables.DataTable):
+ tenant = tables.Column("tenant", verbose_name=_("Tenant"))
+ user = tables.Column("user", verbose_name=_("User"), sortable=True)
+ status = tables.Column(get_status(["network", "network_create",
+ "subnet", "subnet_create",
+ "port", "port_create",
+ "router", "router_create",
+ "ip_floating", "ip_floating_create"]),
+ verbose_name=_("Status"),
+ hidden=True)
+ instance = tables.Column("resource",
+ verbose_name=_("Resource"),
+ 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")
+ table_actions = (CommonFilterAction,)
+ row_class = GlobalNetworkUsageUpdateRow
+ status_columns = ["status"]
+ multi_select = False
+
+
+class GlobalObjectStoreUsageUpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, object_id):
+ return api.ceilometer.global_object_store_usage_get(request,
+ object_id)
+
+
+class GlobalObjectStoreUsageTable(tables.DataTable):
+ tenant = tables.Column("tenant", verbose_name=_("Tenant"))
+ user = tables.Column("user", verbose_name=_("User"), sortable=True)
+ 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")
+ table_actions = (CommonFilterAction,)
+ row_class = GlobalObjectStoreUsageUpdateRow
+ status_columns = ["status"]
+ multi_select = False
--- openstack_dashboard/dashboards/admin/metering/tabs.py
+++ openstack_dashboard/dashboards/admin/metering/tabs.py
@@ -0,0 +1,100 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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 _
+
+from horizon import tabs
+from openstack_dashboard.api import ceilometer
+
+from .tables import GlobalDiskUsageTable
+from .tables import GlobalNetworkTrafficUsageTable
+from .tables import GlobalNetworkUsageTable
+from .tables import GlobalObjectStoreUsageTable
+
+
+class GlobalDiskUsageTab(tabs.TableTab):
+ table_classes = (GlobalDiskUsageTable,)
+ name = _("Global Disk Usage")
+ slug = "global_disk_usage"
+ template_name = ("horizon/common/_detail_table.html")
+
+ def get_global_disk_usage_data(self):
+ request = self.tab_group.request
+ result = ceilometer.global_disk_usage(request)
+ return result
+
+
+class GlobalNetworkTrafficUsageTab(tabs.TableTab):
+ table_classes = (GlobalNetworkTrafficUsageTable,)
+ name = _("Global Network Traffic Usage")
+ slug = "global_network_traffic_usage"
+ template_name = ("horizon/common/_detail_table.html")
+
+ def get_global_network_traffic_usage_data(self):
+ request = self.tab_group.request
+ result = ceilometer.global_network_traffic_usage(request)
+ return result
+
+
+class GlobalNetworkUsageTab(tabs.TableTab):
+ table_classes = (GlobalNetworkUsageTable,)
+ name = _("Global Network Usage")
+ slug = "global_network_usage"
+ template_name = ("horizon/common/_detail_table.html")
+
+ def get_global_network_usage_data(self):
+ request = self.tab_group.request
+ result = ceilometer.global_network_usage(request)
+ return result
+
+ def allowed(self, request):
+ permissions = ("openstack.services.network",)
+ return request.user.has_perms(permissions)
+
+
+class GlobalObjectStoreUsageTab(tabs.TableTab):
+ table_classes = (GlobalObjectStoreUsageTable,)
+ name = _("Global Object Store Usage")
+ slug = "global_object_store_usage"
+ template_name = ("horizon/common/_detail_table.html")
+
+ def get_global_object_store_usage_data(self):
+ request = self.tab_group.request
+ result = ceilometer.global_object_store_usage(request)
+ 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")
+
+ def get_context_data(self, request):
+ meters = ceilometer.CachedResources.get_meter_list(request)
+ # Now only 'cpu_utl' data is useful for line chart.
+ meters = filter(lambda m: m.name == "cpu_util", meters)
+ context = {'meters': meters}
+ return context
+
+
+class CeilometerOverviewTabs(tabs.TabGroup):
+ slug = "ceilometer_overview"
+ tabs = (GlobalDiskUsageTab, GlobalNetworkTrafficUsageTab,
+ GlobalNetworkUsageTab, GlobalObjectStoreUsageTab, GlobalStatsTab,)
+ sticky = True
--- openstack_dashboard/dashboards/admin/metering/templates/metering/index.html
+++ openstack_dashboard/dashboards/admin/metering/templates/metering/index.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Resources usage Overview" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Resources usage Overview")%}
+{% endblock page_header %}
+
+{% block main %}
+<div class="row-fluid">
+ <div class="span12">
+ {{ tab_group.render }}
+ </div>
+</div>
+{% endblock %}
--- openstack_dashboard/dashboards/admin/metering/templates/metering/samples.csv
+++ openstack_dashboard/dashboards/admin/metering/templates/metering/samples.csv
@@ -0,0 +1,5 @@
+date,value
+{% spaceless %}
+{% for sample in samples %}{{ sample.0 }},{{ sample.1 }}
+{% endfor %}
+{% endspaceless %}
--- openstack_dashboard/dashboards/admin/metering/templates/metering/stats.html
+++ openstack_dashboard/dashboards/admin/metering/templates/metering/stats.html
@@ -0,0 +1,81 @@
+{% load i18n %}
+{% load url from future %}
+{% spaceless %}
+ <script type="text/javascript">
+ (function(){
+ var init_ceilometer = function(){
+ var meters = [];
+ {% for m in meters %}
+ meters.push({
+ "name": "{{ m.name }}",
+ "resource_id": "{{ m.resource_id }}",
+ "user_id": "{{ m.user_id }}",
+ "project_id": "{{ m.project_id }}",
+ "unit": "{{ m.unit }}",
+ "type": "{{ m.type }}"
+ });
+ {% endfor %}
+ horizon.ceilometer.getResources(meters);
+ horizon.ceilometer.updateResourceOptions();
+ horizon.ceilometer.loadChartData();
+ };
+ if (typeof horizon.ceilometer !== "undefined"){
+ init_ceilometer();
+ } else {
+ addHorizonLoadEvent(init_ceilometer);
+ }
+ }());
+ </script>
+{% endspaceless %}
+<div id="samples_url" url="{% url "horizon:admin:metering:samples" %}"></div>
+<div id="ceilometer-stats">
+ <form class="form-horizontal">
+ <div class="control-group">
+ <label for="meter" class="control-label">{% trans "Metric" %}: </label>
+ <div class="controls">
+ <select name="meter" id="meter" class="span2 example">
+ {% for meter in meters %}
+ <option value="{{ meter.name }}" data-unit="{{ meter.unit }}">
+ {{ meter.name }}
+ </option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ <div class="control-group">
+ <label for="resource" class="control-label">{% trans "Resource" %}: </label>
+ <div class="controls">
+ <select name="resource" id="resource" class="span4 example">
+ <option value="">{% trans "Select a meter to load options." %}</option>
+ </select>
+ </div>
+ </div>
+ <div class="control-group">
+ <label for="date_options" class="control-label">{% trans "Period" %}: </label>
+ <div class="controls">
+ <select id="date_options" name="date_options" class="span2">
+ <option value="1" selected>{% trans "Last day" %}</option>
+ <option value="7">{% 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>
+ </select>
+ </div>
+ </div>
+ <div class="control-group">
+ <label for="date_from" class="control-label">{% trans "From" %}: </label>
+ <div class="controls">
+ <input type="text" id="date_from" name="date_from" class="span2 example"/>
+ </div>
+ </div>
+ <div class="control-group">
+ <label for="date_to" class="control-label">{% trans "To" %}: </label>
+ <div class="controls">
+ <input type="text" id="date_to" name="date_to" class="span2 example"/>
+ </div>
+ </div>
+ <button type="button" id="action_display_chart"
+ class="btn btn-small btn-search action_display_chart">{% trans "Refresh" %}</button>
+ </form>
+</div>
+<div id="chart_container"></div>
--- openstack_dashboard/dashboards/admin/metering/tests.py
+++ openstack_dashboard/dashboards/admin/metering/tests.py
@@ -0,0 +1,108 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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
+from django import http
+from mox import IsA
+
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+
+from .views import reduce_metrics
+from .views import to_days
+from .views import to_hours
+
+
+INDEX_URL = reverse("horizon:admin:metering:index")
+
+
+class MeteringViewTests(test.BaseAdminViewTests):
+ @test.create_stubs({api.ceilometer: ("global_disk_usage",
+ "global_network_traffic_usage",
+ "global_network_usage",
+ "global_object_store_usage"),
+ api.ceilometer.CachedResources: ("get_meter_list",)})
+ def test_index_view(self):
+ global_disk_usages = self.global_disk_usages.list()
+ global_network_usages = self.global_network_usages.list()
+ global_network_traffic_usages = self.global_network_traffic_usages\
+ .list()
+ global_object_store_usages = self.global_object_store_usages.list()
+ meters = self.meters.list()
+
+ api.ceilometer.global_disk_usage(IsA(http.HttpRequest))\
+ .AndReturn(global_disk_usages)
+ api.ceilometer.global_network_usage(IsA(http.HttpRequest))\
+ .AndReturn(global_network_usages)
+ api.ceilometer.global_network_traffic_usage(IsA(http.HttpRequest))\
+ .AndReturn(global_network_traffic_usages)
+ api.ceilometer.global_object_store_usage(IsA(http.HttpRequest))\
+ .AndReturn(global_object_store_usages)
+ api.ceilometer.CachedResources.get_meter_list(IsA(http.HttpRequest))\
+ .AndReturn(meters)
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, "admin/metering/index.html")
+
+ @test.create_stubs({api.ceilometer: ("sample_list",)})
+ def test_sample_views(self):
+ samples = self.samples.list()
+ api.ceilometer.sample_list(IsA(http.HttpRequest), IsA(basestring),
+ IsA(list)).AndReturn(samples)
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse("horizon:admin:metering:samples") +
+ '?meter=meter&from=2012-10-10&'
+ 'to=2012-10-11&resource=resouce')
+ self.assertTemplateUsed(res, "admin/metering/samples.csv")
+
+ def test_sample_views_without_request_args(self):
+ res = self.client.get(reverse("horizon:admin:metering:samples"))
+ self.assertEqual(res.status_code, 404)
+
+ def test_sample_views_wrong_dates(self):
+ res = self.client.get(reverse("horizon:admin:metering:samples"),
+ dict(meter="meter",
+ date_from="cannot be parsed",
+ date_to="cannot be parsed",
+ resource="resource")
+ )
+ self.assertEqual(res.status_code, 404)
+
+ def test_to_days_to_hours(self):
+ test_data = (["2001-01-01T01:01:01", 123],
+ ["1999-12-12T00:00:00", 321],
+ ["9999-12-12T12:59:59", 0])
+ for test in test_data:
+ date, value = to_days(test)
+ self.assertEqual(date, test[0][:11] + "00:00:00")
+ self.assertEqual(value, test[1])
+ for test in test_data:
+ date, value = to_hours(test)
+ self.assertEqual(date, test[0][:14] + "00:00")
+ self.assertEqual(value, test[1])
+
+ def test_reduce_metrics(self):
+ test_data = [["2001-01-01T00:00:00", 123],
+ ["2001-01-01T00:00:00", 321],
+ ["2001-01-01T00:00:00", 0],
+ ["2001-01-02T00:00:00", 2],
+ ["2001-01-02T00:00:00", 2],
+ ["2001-01-02T00:00:00", 2]]
+ result = reduce_metrics(test_data)
+ self.assertEqual(result, [["2001-01-01T00:00:00", 444 / 3],
+ ["2001-01-02T00:00:00", 6 / 3]])
--- openstack_dashboard/dashboards/admin/metering/urls.py
+++ openstack_dashboard/dashboards/admin/metering/urls.py
@@ -0,0 +1,26 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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
+from django.conf.urls import url
+
+from .views import IndexView
+from .views import SamplesView
+
+
+urlpatterns = patterns('openstack_dashboard.dashboards.admin.metering.views',
+ url(r'^$', IndexView.as_view(), name='index'),
+ url(r'^samples$', SamplesView.as_view(), name='samples'))
--- openstack_dashboard/dashboards/admin/metering/views.py
+++ openstack_dashboard/dashboards/admin/metering/views.py
@@ -0,0 +1,128 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Canonical Ltd.
+#
+# 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
+import itertools
+import logging
+import operator
+
+from django.views.generic import TemplateView
+
+from horizon import exceptions
+from horizon import tabs
+from openstack_dashboard.api import ceilometer
+
+from .tabs import CeilometerOverviewTabs
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(tabs.TabbedTableView):
+ tab_group_class = CeilometerOverviewTabs
+ template_name = 'admin/metering/index.html'
+
+
+def to_hours(item):
+ """
+ Convert the time of a sample to hour level
+ to strip the minute and second information.
+ It's used to preprocess data before interpolation.
+ """
+ date_obj = datetime.strptime(item[0], '%Y-%m-%dT%H:%M:%S')
+ new_date_str = date_obj.strftime("%Y-%m-%dT%H:00:00")
+ return new_date_str, item[1]
+
+
+def to_days(item):
+ """
+ Convert the time of a sample to day level
+ to strip the hour, minute and second information.
+ It's used to preprocess data before interpolation.
+ """
+ date_obj = datetime.strptime(item[0], '%Y-%m-%dT%H:%M:%S')
+ new_date_str = date_obj.strftime("%Y-%m-%dT00:00:00")
+ return new_date_str, item[1]
+
+
+def reduce_metrics(samples):
+ """
+ Given a set of samples, group them by time and calculate the average
+ value for that time point.
+ """
+ new_samples = []
+ for key, items in itertools.groupby(samples, operator.itemgetter(0)):
+ items = list(items)
+ if len(items) > 0:
+ avg = sum(map(lambda x: x[1], items)) / len(items)
+ else:
+ avg = 0
+ new_samples.append([key, avg])
+ return new_samples
+
+
+class SamplesView(TemplateView):
+ template_name = "admin/metering/samples.csv"
+
+ def get(self, request, *args, **kwargs):
+ meter = request.GET.get('meter', None)
+ date_from = request.GET.get('from', None)
+ date_to = request.GET.get('to', None)
+ resource = request.GET.get('resource', None)
+
+ if not(meter and date_from and date_to and resource):
+ raise exceptions.NotFound
+ else:
+ date_from += "T00:00:00"
+ date_to += "T23:59:59"
+ try:
+ date_from_obj = datetime.strptime(date_from,
+ "%Y-%m-%dT%H:%M:%S")
+ date_to_obj = datetime.strptime(date_to, "%Y-%m-%dT%H:%M:%S")
+ except ValueError:
+ raise exceptions.NotFound
+
+ query = [{'field': 'timestamp',
+ 'op': 'ge',
+ 'value': date_from},
+ {'field': 'timestamp',
+ 'op': 'le',
+ 'value': date_to},
+ {'field': 'resource',
+ 'op': 'eq',
+ 'value': resource}]
+ sample_list = ceilometer.sample_list(self.request, meter, query)
+ samples = []
+ for sample_data in sample_list:
+ samples.append([sample_data.timestamp[:19],
+ sample_data.counter_volume])
+
+ # if requested period is too long interpolate data
+ delta = (date_to_obj - date_from_obj).days
+
+ if delta >= 365:
+ samples = map(to_days, samples)
+ samples = reduce_metrics(samples)
+ elif delta >= 30:
+ # reduce metrics to hours
+ samples = map(to_hours, samples)
+ samples = reduce_metrics(samples)
+
+ context = dict(samples=samples)
+ response_kwargs = dict(mimetype='text/csv')
+ return self.render_to_response(
+ context=context,
+ **response_kwargs)
--- static/horizon/js/horizon.ceilometer.js
+++ static/horizon/js/horizon.ceilometer.js
@@ -0,0 +1,221 @@
+horizon.ceilometer = {
+ initVariables: function () {
+ var self = this;
+ self.margin = {
+ top: 40,
+ right: 100,
+ bottom: 80,
+ left: 200
+ };
+ self.width = 1100 - self.margin.left - self.margin.right;
+ self.height = 480 - self.margin.top - self.margin.bottom;
+ self.svg = undefined;
+ self.x = d3.time.scale().range([0, self.width]);
+ self.y = d3.scale.linear().range([self.height, 0]);
+ self.xAxis = d3.svg.axis().scale(self.x).orient("bottom");
+ self.yAxis = d3.svg.axis().scale(self.y).orient("left");
+ self.line = d3.svg.line()
+ .x(function (d) {
+ return self.x(d.date);
+ })
+ .y(function (d) {
+ return self.y(d.value);
+ });
+
+ self.$meter = $("#meter");
+ self.$resource = $("#resource");
+ self.$date_from = $("#date_from");
+ self.$date_to = $("#date_to");
+ self.$get_samples = $("#samples_url");
+ self.$chart_container = $("#chart_container");
+ self.parseDate = d3.time.format("%Y-%m-%dT%H:%M:%S").parse;
+ },
+ getResources: function (meters) {
+ // Get ``meters`` variable from django template.
+ var self = this;
+ if (meters) {
+ self.meters = meters;
+ }
+ },
+
+ updateResourceOptions: function () {
+ // Update resource select field on stats.html template.
+ var self = this,
+ meter_name = self.$meter.val(),
+ i,
+ option;
+ self.$resource.empty();
+ if (self.meters) {
+ for (i = 0; i < self.meters.length; i = i + 1) {
+ if (meter_name === self.meters[i].name) {
+ option = '<option value="' + self.meters[i].resource_id + '">';
+ option += self.meters[i].resource_id + '</option>';
+ self.$resource.append(option);
+ }
+ }
+ }
+ },
+
+ loadChartData: function () {
+ var self = this,
+ meter = self.$meter.val(),
+ resource = self.$resource.val(),
+ from = self.$date_from.val(),
+ to = self.$date_to.val(),
+ horizon_samples_url = self.$get_samples.attr("url");
+ d3.select("svg").remove();
+
+ self.svg = d3.select("#chart_container").append("svg")
+ .attr("width", self.width + self.margin.left + self.margin.right)
+ .attr("height", self.height + self.margin.top + self.margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + self.margin.left + "," + self.margin.top + ")");
+
+ if (meter && resource) {
+ d3.csv(horizon_samples_url + "?meter=" + meter + "&resource=" + resource + "&from=" + from + "&to=" + to,
+ function (error, data) {
+ var chart_title = self.$meter.val() + " ";
+ chart_title += gettext("for resource") + " " + self.$resource.val();
+ chart_title += " (" + gettext("From") + " " + self.$date_to.val() + " " + gettext("to") + " ";
+ chart_title += self.$date_to.val() + ")";
+
+ // read selected option
+ var option = self.$meter.find("option").filter(":selected");
+ var meter_text = gettext("Value");
+ if (option) {
+ var unit = option[0].getAttribute("data-unit");
+ meter_text = meter_text + " (" + unit + ")";
+ }
+
+ data.forEach(function (d) {
+ d.date = self.parseDate(d.date);
+ d.value = +d.value;
+ });
+
+ self.x.domain(d3.extent(data, function (d) {
+ return d.date;
+ }));
+
+ var min_value = d3.min(data, function(d){return d.value});
+ var max_value = d3.max(data, function(d){return d.value});
+ if (min_value === max_value) {
+ self.y.domain([0, min_value * 2]);
+ } else {
+ self.y.domain(d3.extent(data, function (d) {
+ return d.value;
+ }));
+
+ }
+
+ self.svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + self.height + ")")
+ .call(self.xAxis);
+ self.svg.append("g")
+ .attr("class", "y axis")
+ .call(self.yAxis)
+ .append("text")
+ .attr("transform", "rotate(-90)")
+ .attr("y", 6)
+ .attr("dy", ".71em")
+ .style("text-anchor", "end")
+ .text(meter_text);
+
+ self.svg.append("path")
+ .datum(data)
+ .attr("class", "line")
+ .style("stroke", "steelblue")
+ .style("stroke-width", 1.5)
+ .style("fill", "none")
+ .attr("d", self.line);
+
+ self.svg.append("text")
+ .attr("x", (self.width / 2))
+ .attr("y", -(self.margin.top / 2))
+ .attr("text-anchor", "middle")
+ .style("font-size", "14px")
+ .style("text-decoration", "none")
+ .text(chart_title);
+
+ d3.selectAll("path.domain")
+ .style("fill", "none")
+ .style("stroke", "black")
+ .style("stroke-width", 1);
+
+ d3.selectAll(".axis path")
+ .style("fill", "none")
+ .style("stroke", "black")
+ .style("shape-rendering", "crispEdges");
+
+ d3.selectAll(".axis")
+ .style("font-size", "10px");
+
+ d3.selectAll(".axis line")
+ .style("fill", "none")
+ .style("stroke", "black")
+ .style("shape-rendering", "crispEdges");
+ });
+ }
+ },
+
+ refreshPickers: function (date_interval) {
+ var now;
+ var targetDate;
+ var self = this;
+ now = new Date();
+ self.$date_to.datepicker('setValue', now);
+ targetDate = new Date();
+ targetDate.setDate(now.getDate() - date_interval);
+ self.$date_from.datepicker('setValue', targetDate);
+ },
+
+ init: function () {
+ var self = this,
+ to,
+ from;
+ self.initVariables();
+ from = self.$date_from.datepicker({format: "yyyy-mm-dd"})
+ .on('changeDate', function (ev) {
+ if (ev.date.valueOf() > to.date.valueOf()) {
+ var newDate = new Date(ev.date);
+ newDate.setDate(newDate.getDate() + 1);
+ to.setValue(newDate);
+ }
+ from.hide();
+ self.$date_to[0].focus();
+ }).data('datepicker');
+ to = self.$date_to.datepicker({
+ format: "yyyy-mm-dd",
+ onRender: function (date) { return date.valueOf() <= from.date.valueOf() ? 'disabled' : ''; }
+ }).on('changeDate', function () {
+ to.hide();
+ self.loadChartData();
+ }).data('datepicker');
+
+ $(".action_display_chart").click(function () {
+ self.loadChartData();
+ });
+
+ self.$meter.change(function () {
+ self.updateResourceOptions();
+ self.loadChartData();
+ });
+ self.$resource.change(function () {
+ self.loadChartData();
+ });
+
+ $("#date_options").change(function () {
+ var current = $(this).val();
+ if (current) {
+ self.refreshPickers(current);
+ self.loadChartData();
+ }
+ });
+ self.refreshPickers(1);
+ self.loadChartData();
+ }
+};
+
+horizon.addInitFunction(function () {
+ horizon.ceilometer.init();
+});