diff --git a/tuskar_ui/infrastructure/nodes/templates/nodes/details.html b/tuskar_ui/infrastructure/nodes/templates/nodes/details.html index 99d5a124e..f807f46db 100644 --- a/tuskar_ui/infrastructure/nodes/templates/nodes/details.html +++ b/tuskar_ui/infrastructure/nodes/templates/nodes/details.html @@ -9,41 +9,136 @@ {% block main %}
-

{% trans "Info" %}

-
-
{% trans "MAC Addresses" %}
-
{{ node.addresses|join:", "|default:"—" }}
-
{% trans "UUID" %}
-
{{ node.uuid|default:"—" }}
-
{% trans "Instance UUID" %}
-
{{ node.instance_uuid|default:"—" }}
-
{% trans "Driver" %}
-
{{ node.driver|default:"—" }}
-
{% trans "Power state" %}
-
{{ node.power_state|default:"—" }}
-
+

{% trans "Info" %}

+
+
{% trans "MAC Addresses" %}
+
{{ node.addresses|join:", "|default:"—" }}
+
{% trans "UUID" %}
+
{{ node.uuid|default:"—" }}
+
{% trans "Instance UUID" %}
+
{{ node.instance_uuid|default:"—" }}
+
{% trans "Driver" %}
+
{{ node.driver|default:"—" }}
+
{% trans "Power state" %}
+
{{ node.power_state|default:"—" }}
+
-

{% trans "Driver Info" %}

-
-
{% trans "IPMI address" %}
-
{{ node.driver_info.ipmi_address|default:"—" }}
-
{% trans "IPMI username" %}
-
{{ node.driver_info.ipmi_username|default:"—" }}
-
+

{% trans "Driver Info" %}

+
+
{% trans "IPMI address" %}
+
{{ node.driver_info.ipmi_address|default:"—" }}
+
{% trans "IPMI username" %}
+
{{ node.driver_info.ipmi_username|default:"—" }}
+
-

{% trans "Properties" %}

-
-
{% trans "Local disk" %}
-
{{ node.properties.local_disk|filesizeformat|default:"—" }}
-
{% trans "RAM" %}
-
{{ node.properties.ram|filesizeformat|default:"—" }}
-
{% trans "CPU" %}
-
{{ node.properties.cpu|default:"—" }}
-
+

{% trans "Properties" %}

+
+
{% trans "Local disk" %}
+
{{ node.properties.local_disk|filesizeformat|default:"—" }}
+
{% trans "RAM" %}
+
{{ node.properties.ram|filesizeformat|default:"—" }}
+
{% trans "CPU" %}
+
{{ node.properties.cpu|default:"—" }}
+
+ +

{% trans "Performance and Capacity" %}

+ +{% if meters %} +
+{% url 'horizon:infrastructure:nodes:performance' node.id as node_perf_url %} + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + +
+
+ + {% for meter_name, meter_label in meters %} + {% if forloop.counter0|divisibleby:"4" %} + + {% endif %} + + {% if forloop.counter|divisibleby:"4" %} + + {% endif %} + {% endfor %} +
+ {% include "infrastructure/_performance_chart.html" with label=meter_label url=node_perf_url|add:"?meter="|add:meter_name only %} +
+
+{% else %} + Metering service is not enabled. +{% endif %} {% endblock %} diff --git a/tuskar_ui/infrastructure/nodes/tests.py b/tuskar_ui/infrastructure/nodes/tests.py index ef298c2d0..838f9d96c 100644 --- a/tuskar_ui/infrastructure/nodes/tests.py +++ b/tuskar_ui/infrastructure/nodes/tests.py @@ -13,11 +13,13 @@ # under the License. import contextlib +import json from django.core import urlresolvers from mock import patch, call # noqa +from openstack_dashboard.test import helpers from openstack_dashboard.test.test_data import utils from tuskar_ui import api as api from tuskar_ui.handle_errors import handle_errors # noqa @@ -28,11 +30,12 @@ from tuskar_ui.test.test_data import tuskar_data INDEX_URL = urlresolvers.reverse('horizon:infrastructure:nodes:index') REGISTER_URL = urlresolvers.reverse('horizon:infrastructure:nodes:register') DETAIL_VIEW = 'horizon:infrastructure:nodes:detail' +PERFORMANCE_VIEW = 'horizon:infrastructure:nodes:performance' TEST_DATA = utils.TestDataContainer() tuskar_data.data(TEST_DATA) -class NodesTests(test.BaseAdminViewTests): +class NodesTests(test.BaseAdminViewTests, helpers.APITestCase): @handle_errors("Error!", []) def _raise_tuskar_exception(self, request, *args, **kwargs): raise self.exceptions.tuskar @@ -244,3 +247,30 @@ class NodesTests(test.BaseAdminViewTests): self.assertEqual(mock.get.call_count, 1) self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_performance(self): + node = api.Node(self.ironicclient_nodes.list()[0]) + meters = self.meters.list() + resources = self.resources.list() + + ceilometerclient = self.stub_ceilometerclient() + ceilometerclient.resources = self.mox.CreateMockAnything() + ceilometerclient.resources.list(q=[]).AndReturn(resources) + ceilometerclient.meters = self.mox.CreateMockAnything() + ceilometerclient.meters.list(None).AndReturn(meters) + + self.mox.ReplayAll() + + with patch('tuskar_ui.api.Node', **{ + 'spec_set': ['get'], + 'get.return_value': node, + }): + url = urlresolvers.reverse(PERFORMANCE_VIEW, args=(node.uuid,)) + url += '?meter=cpu&date_options=7' + res = self.client.get(url) + + json_content = json.loads(res.content) + self.assertEqual(res.status_code, 200) + self.assertIn('series', json_content) + self.assertIn('settings', json_content) + self.assertIn('stats', json_content) diff --git a/tuskar_ui/infrastructure/nodes/urls.py b/tuskar_ui/infrastructure/nodes/urls.py index ee16a89e7..3b9b890d5 100644 --- a/tuskar_ui/infrastructure/nodes/urls.py +++ b/tuskar_ui/infrastructure/nodes/urls.py @@ -24,4 +24,6 @@ urlpatterns = urls.patterns( name='register'), urls.url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), + urls.url(r'^(?P[^/]+)/performance/$', + views.PerformanceView.as_view(), name='performance'), ) diff --git a/tuskar_ui/infrastructure/nodes/views.py b/tuskar_ui/infrastructure/nodes/views.py index cb99f647f..ddeca2a8e 100644 --- a/tuskar_ui/infrastructure/nodes/views.py +++ b/tuskar_ui/infrastructure/nodes/views.py @@ -11,13 +11,20 @@ # 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 json from django.core.urlresolvers import reverse_lazy +from django import http +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import base from horizon import forms as horizon_forms from horizon import tabs as horizon_tabs from horizon import views as horizon_views +from openstack_dashboard.api import base as api_base +from openstack_dashboard.dashboards.admin.metering import views as metering + from tuskar_ui import api from tuskar_ui.infrastructure.nodes import forms from tuskar_ui.infrastructure.nodes import tabs @@ -71,4 +78,100 @@ class DetailView(horizon_views.APIView): redirect = reverse_lazy('horizon:infrastructure:nodes:index') node = api.Node.get(request, node_uuid, _error_redirect=redirect) context['node'] = node + + if api_base.is_service_enabled(request, 'metering'): + context['meters'] = ( + ('cpu', _('CPU')), + ('disk', _('Disk')), + ('network', _('Network Bandwidth (In)')), + ('energy', _('Energy')), + ('memory', _('Memory')), + ('swap', _('Swap')), + ('network-out', _('Network Bandwidth (Out)')), + ('power', _('Power')), + ) + return context + + +class PerformanceView(base.TemplateView): + def get(self, request, *args, **kwargs): + meter = request.GET.get('meter') + date_options = request.GET.get('date_options') + date_from = request.GET.get('date_from') + date_to = request.GET.get('date_to') + stats_attr = request.GET.get('stats_attr', 'avg') + group_by = request.GET.get('group_by') + + meter_name = meter.replace(".", "_") + resource_name = 'id' if group_by == "project" else 'resource_id' + node_uuid = kwargs.get('node_uuid') + + additional_query = [{'field': 'resource_id', + 'op': 'eq', + 'value': node_uuid}] + + resources, unit = metering.query_data( + request=request, + date_from=date_from, + date_to=date_to, + date_options=date_options, + group_by=group_by, + meter=meter, + additional_query=additional_query) + series = metering.SamplesView._series_for_meter(resources, + resource_name, + meter_name, + stats_attr, + unit) + + average = used = 0 + tooltip_average = '' + + if series: + values = [point['y'] for point in series[0]['data']] + average = sum(values) / len(values) + used = values[-1] + first_date = series[0]['data'][0]['x'] + last_date = series[0]['data'][-1]['x'] + tooltip_average = _( + 'Average %(average)s %(unit)s
From: %(first_date)s, to: ' + '%(last_date)s' + ) % (dict(average=average, unit=unit, first_date=first_date, + last_date=last_date) + ) + + ret = { + 'series': series, + 'settings': { + 'renderer': 'StaticAxes', + 'yMin': 0, + 'yMax': 100, + 'higlight_last_point': True, + 'auto_size': False, + 'auto_resize': False, + 'axes_x': False, + 'axes_y': False, + 'bar_chart_settings': { + 'orientation': 'vertical', + 'used_label_placement': 'left', + 'width': 30, + 'color_scale_domain': [0, 80, 80, 100], + 'color_scale_range': [ + '#0000FF', + '#0000FF', + '#FF0000', + '#FF0000' + ], + 'average_color_scale_domain': [0, 100], + 'average_color_scale_range': ['#0000FF', '#0000FF'] + } + }, + 'stats': { + 'average': average, + 'used': used, + 'tooltip_average': tooltip_average, + } + } + + return http.HttpResponse(json.dumps(ret), mimetype='application/json') diff --git a/tuskar_ui/infrastructure/templates/infrastructure/_performance_chart.html b/tuskar_ui/infrastructure/templates/infrastructure/_performance_chart.html new file mode 100644 index 000000000..8bf04b42a --- /dev/null +++ b/tuskar_ui/infrastructure/templates/infrastructure/_performance_chart.html @@ -0,0 +1,16 @@ +

{{ label }}

+
+
+
+
+
+
+
+
+
+