Add performance charts to node details page
Implements blueprint: tripleo-live-graphs Change-Id: I55066f51aaa78e49bc69351a5b7d74788b546872
This commit is contained in:
parent
d028589933
commit
8bd9df83f3
@ -46,4 +46,99 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2>{% trans "Performance and Capacity" %}</h2>
|
||||||
|
|
||||||
|
{% if meters %}
|
||||||
|
<br />
|
||||||
|
{% url 'horizon:infrastructure:nodes:performance' node.id as node_perf_url %}
|
||||||
|
|
||||||
|
<div id="ceilometer-stats">
|
||||||
|
<form class="form-horizontal" id="linechart_general_form">
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="date_options" class="control-label">{% trans "Period" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<select data-line-chart-command="select_box_change"
|
||||||
|
id="date_options"
|
||||||
|
name="date_options"
|
||||||
|
class="span2">
|
||||||
|
<option value="1">{% trans "Last day" %}</option>
|
||||||
|
<option value="7" selected="selected">{% trans "Last week" %}</option>
|
||||||
|
<option value="{% now 'j' %}">{% trans "Month to date" %}</option>
|
||||||
|
<option value="15">{% trans "Last 15 days" %}</option>
|
||||||
|
<option value="30">{% trans "Last 30 days" %}</option>
|
||||||
|
<option value="365">{% trans "Last year" %}</option>
|
||||||
|
<option value="other">{% trans "Other" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" id="date_from">
|
||||||
|
<label for="date_from" class="control-label">{% trans "From" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<input data-line-chart-command="date_picker_change"
|
||||||
|
type="text"
|
||||||
|
id="date_from"
|
||||||
|
name="date_from"
|
||||||
|
class="span2 example"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" id="date_to">
|
||||||
|
<label for="date_to" class="control-label">{% trans "To" %}: </label>
|
||||||
|
<div class="controls">
|
||||||
|
<input data-line-chart-command="date_picker_change"
|
||||||
|
type="text"
|
||||||
|
name="date_to"
|
||||||
|
class="span2 example"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
show_hide_datepickers();
|
||||||
|
} else {
|
||||||
|
addHorizonLoadEvent(function() {
|
||||||
|
show_hide_datepickers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_hide_datepickers() {
|
||||||
|
var date_options = $("#date_options");
|
||||||
|
date_options.change(function(evt) {
|
||||||
|
if ($(this).find("option:selected").val() == "other"){
|
||||||
|
evt.stopPropagation();
|
||||||
|
$("#date_from .controls input, #date_to .controls input").val('');
|
||||||
|
$("#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>
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
{% for meter_name, meter_label in meters %}
|
||||||
|
{% if forloop.counter0|divisibleby:"4" %}
|
||||||
|
<tr>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{% include "infrastructure/_performance_chart.html" with label=meter_label url=node_perf_url|add:"?meter="|add:meter_name only %}
|
||||||
|
</td>
|
||||||
|
{% if forloop.counter|divisibleby:"4" %}
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
Metering service is not enabled.
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,11 +13,13 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import json
|
||||||
|
|
||||||
from django.core import urlresolvers
|
from django.core import urlresolvers
|
||||||
|
|
||||||
from mock import patch, call # noqa
|
from mock import patch, call # noqa
|
||||||
|
|
||||||
|
from openstack_dashboard.test import helpers
|
||||||
from openstack_dashboard.test.test_data import utils
|
from openstack_dashboard.test.test_data import utils
|
||||||
from tuskar_ui import api as api
|
from tuskar_ui import api as api
|
||||||
from tuskar_ui.handle_errors import handle_errors # noqa
|
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')
|
INDEX_URL = urlresolvers.reverse('horizon:infrastructure:nodes:index')
|
||||||
REGISTER_URL = urlresolvers.reverse('horizon:infrastructure:nodes:register')
|
REGISTER_URL = urlresolvers.reverse('horizon:infrastructure:nodes:register')
|
||||||
DETAIL_VIEW = 'horizon:infrastructure:nodes:detail'
|
DETAIL_VIEW = 'horizon:infrastructure:nodes:detail'
|
||||||
|
PERFORMANCE_VIEW = 'horizon:infrastructure:nodes:performance'
|
||||||
TEST_DATA = utils.TestDataContainer()
|
TEST_DATA = utils.TestDataContainer()
|
||||||
tuskar_data.data(TEST_DATA)
|
tuskar_data.data(TEST_DATA)
|
||||||
|
|
||||||
|
|
||||||
class NodesTests(test.BaseAdminViewTests):
|
class NodesTests(test.BaseAdminViewTests, helpers.APITestCase):
|
||||||
@handle_errors("Error!", [])
|
@handle_errors("Error!", [])
|
||||||
def _raise_tuskar_exception(self, request, *args, **kwargs):
|
def _raise_tuskar_exception(self, request, *args, **kwargs):
|
||||||
raise self.exceptions.tuskar
|
raise self.exceptions.tuskar
|
||||||
@ -244,3 +247,30 @@ class NodesTests(test.BaseAdminViewTests):
|
|||||||
self.assertEqual(mock.get.call_count, 1)
|
self.assertEqual(mock.get.call_count, 1)
|
||||||
|
|
||||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
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)
|
||||||
|
@ -24,4 +24,6 @@ urlpatterns = urls.patterns(
|
|||||||
name='register'),
|
name='register'),
|
||||||
urls.url(r'^(?P<node_uuid>[^/]+)/$', views.DetailView.as_view(),
|
urls.url(r'^(?P<node_uuid>[^/]+)/$', views.DetailView.as_view(),
|
||||||
name='detail'),
|
name='detail'),
|
||||||
|
urls.url(r'^(?P<node_uuid>[^/]+)/performance/$',
|
||||||
|
views.PerformanceView.as_view(), name='performance'),
|
||||||
)
|
)
|
||||||
|
@ -11,13 +11,20 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import json
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
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 forms as horizon_forms
|
||||||
from horizon import tabs as horizon_tabs
|
from horizon import tabs as horizon_tabs
|
||||||
from horizon import views as horizon_views
|
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 import api
|
||||||
from tuskar_ui.infrastructure.nodes import forms
|
from tuskar_ui.infrastructure.nodes import forms
|
||||||
from tuskar_ui.infrastructure.nodes import tabs
|
from tuskar_ui.infrastructure.nodes import tabs
|
||||||
@ -71,4 +78,100 @@ class DetailView(horizon_views.APIView):
|
|||||||
redirect = reverse_lazy('horizon:infrastructure:nodes:index')
|
redirect = reverse_lazy('horizon:infrastructure:nodes:index')
|
||||||
node = api.Node.get(request, node_uuid, _error_redirect=redirect)
|
node = api.Node.get(request, node_uuid, _error_redirect=redirect)
|
||||||
context['node'] = node
|
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
|
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<br> 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')
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
<h4>{{ label }}</h4>
|
||||||
|
<div class="overview_chart">
|
||||||
|
<div class="chart_container">
|
||||||
|
<div class="chart"
|
||||||
|
data-chart-type="line_chart"
|
||||||
|
data-url="{{ url }}"
|
||||||
|
data-form-selector='#linechart_general_form'
|
||||||
|
data-settings='{ "auto_size": false, "axes_x" : false, "axes_y" : false }'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bar_chart_container">
|
||||||
|
<div class="chart"
|
||||||
|
data-chart-type="overview_bar_chart">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
x
Reference in New Issue
Block a user