Merge "Implement the deployment overview and progress page"

This commit is contained in:
Jenkins 2014-02-13 09:55:12 +00:00 committed by Gerrit Code Review
commit da892bc265
6 changed files with 285 additions and 84 deletions

View File

@ -194,6 +194,26 @@ class Overcloud(base.APIDictWrapper):
return self.stack.stack_status in ('CREATE_COMPLETE',
'UPDATE_COMPLETE')
@cached_property
def is_deploying(self):
"""Check if this Overcloud is currently deploying or updating.
:return: True if deployment is in progress, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('CREATE_IN_PROGRESS',
'UPDATE_IN_PROGRESS')
@cached_property
def is_failed(self):
"""Check if this Overcloud failed to update or deploy.
:return: True if deployment there was an error, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('CREATE_FAILED',
'UPDATE_FAILED')
@memoized.memoized
def all_resources(self, with_joins=True):
"""Return a list of all Overcloud Resources
@ -249,6 +269,11 @@ class Overcloud(base.APIDictWrapper):
return filtered_resources
@cached_property
def dashboard_url(self):
# TODO(rdopieralski) Implement this.
return "http://horizon.example.com"
class Node(base.APIResourceWrapper):
# FIXME(lsmola) uncomment this and delete equivalent methods

View File

@ -15,6 +15,7 @@
# under the License.
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tabs
@ -23,47 +24,66 @@ from tuskar_ui import api
from tuskar_ui.infrastructure.overcloud import tables
def _get_role_data(overcloud, role):
resources = overcloud.resources(role, with_joins=True)
nodes = [r.node for r in resources]
node_count = len(nodes)
data = {
'role': role,
'name': role.name,
'node_count': node_count,
}
if nodes:
running_node_count = sum(1 for node in nodes
if node.instance.status == 'ACTIVE')
error_node_count = sum(1 for node in nodes
if node.instance.status == 'ERROR')
deploying_node_count = (node_count - error_node_count -
running_node_count)
data.update({
'running_node_count': running_node_count,
'error_node_count': error_node_count,
'error_node_message': ungettext_lazy("node is down",
"nodes are down",
error_node_count),
'deploying_node_count': deploying_node_count,
'deploying_node_message': ungettext_lazy("node is deploying",
"nodes are deploying",
deploying_node_count),
})
# TODO(rdopieralski) get this from ceilometer
# data['capacity'] = 20
return data
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("infrastructure/overcloud/_detail_overview.html")
def get_context_data(self, request):
context = {}
def get_context_data(self, request, **kwargs):
overcloud = self.tab_group.kwargs['overcloud']
try:
roles = api.OvercloudRole.list(request)
except Exception:
roles = {}
roles = []
exceptions.handle(request,
_('Unable to retrieve overcloud roles.'))
context['overcloud'] = overcloud
context['roles'] = []
for role in roles:
context['roles'].append(
self._get_role_data(overcloud, role))
# also get expected node counts
return context
def _get_role_data(self, overcloud, role):
resources = overcloud.resources(role, with_joins=True)
nodes = [r.node for r in resources]
role.node_count = len(nodes)
if role.node_count > 0:
role.running_node_count = len(
[n for n in nodes if n.instance.status == 'ACTIVE'])
role.error_node_count = len(
[n for n in nodes if n.instance.status == 'ERROR'])
role.other_node_count = role.node_count - \
(role.running_node_count +
role.error_node_count)
role.running_node_percentage = 100 * \
role.running_node_count / role.node_count
return role
_("Unable to retrieve overcloud roles."))
role_data = [_get_role_data(overcloud, role) for role in roles]
total = sum(d['node_count'] for d in role_data)
progress = 100 * sum(d.get('running_node_count', 0)
for d in role_data) // (total or 1)
try:
last_event = overcloud.stack_events[-1]
except IndexError:
last_event = None
return {
'overcloud': overcloud,
'roles': role_data,
'progress': progress,
'dashboard_url': overcloud.dashboard_url,
'last_event': last_event,
}
class ConfigurationTab(tabs.Tab):

View File

@ -1,28 +1,109 @@
{% load i18n %}
{% load url from future%}
<div class="row-fluid">
<div class="span6">
<h3>{% trans "Roles" %}</h3>
<hr />
{% for role in roles %}
<h4><a href="{% url 'horizon:infrastructure:overcloud:role' overcloud.id role.id%}">{{ role.node_count }} {{ role.name }}</a></h4>
{% if role.error_node_count > 0 %}
{{ role.error_node_count }} {% trans "nodes are down" %}
<br />
{% if overcloud.is_deploying or overcloud.is_failed %}
{% if overcloud.is_deploying %}
<div class="alert alert-info">
<div class="row-fluid">
<div class="span2">
<strong>Deploying...</strong>
<div class="progress progress-striped active progress-info">
<div class="bar bar-info" style="width:{{ progress }}%"></div>
</div>
</div>
{% else %}
<div class="alert alert-error">
<div class="row-fluid">
<div class="span2">
<strong>Deployment failed</strong>
<div class="progress progress-striped progress-danger">
<div class="bar bar-info" style="width:{{ progress }}%"></div>
</div>
</div>
{% endif %}
<div class="span10">
{% if last_event %}
<strong>Last update:</strong>
<p>
<time datetime="{{ last_event.event_time }}">{{ last_event.event_time }}</time>
{{ last_event.resource_name }}
{{ last_event.resource_status }}
</p>
{% endif %}
{% if role.other_node_count > 0 %}
{{ role.other_node_count }} {% trans "nodes are building" %}
<br />
{% endif %}
{% if role.running_node_percentage %}
{{ role.running_node_percentage }} {% trans "% of nodes are running" %}
<br />
{% endif %}
<hr />
{% endfor %}
</div>
<div class="span6">
<h3>{% trans "Role Distribution" %}</h3>
<a href="?tab=detail__log" data-toggle="tab" data-target="#detail__log" class="pull-right">See full log</a>
</div>
</div>
</div>
{% endif %}
<div class="row-fluid">
<div class="span8">
<h2>{% trans "Deployment Roles" %}</h2>
<div class="widget">
<table class="table">
{% for role in roles %}
<tr>
<td><a
href="{% url 'horizon:infrastructure:overcloud:role' overcloud.id role.role.id %}"
>{{ role.name }} <span class="badge">({{ role.node_count }})</span></a>
</td><td>
{% if role.deploying_node_count %}
<div class="text-warning">
<i class="icon-cog"></i>
{{ role.deploying_node_count }} / {{ role.node_count }}
{{ role.deploying_node_message }}
</div>
{% elif role.error_node_count %}
<div class="text-error">
<i class="icon-warning"></i>
{{ role.error_node_count }} / {{ role.node_count }}
{{ role.error_node_message }}
</div>
{% elif role.node_count %}
<div class="text-success">
<i class="icon-ok"></i>
{% trans "All nodes run correctly" %}
</div>
{% else %}
<div class="muted">
<i class="icon-minus"></i>
{% trans "No nodes" %}
</div>
{% endif %}
</td><td>
{% if role.capacity %}
<div class="row-fluid">
<div class="span6"><p>{{ role.capacity }}%</p></div>
<div class="span6">
<div class="progress active progress-info">
<div class="bar bar-info" style="width:{{ role.capacity }}%"></div>
</div>
</div>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="span4">
<div class="widget">
<h2>{% trans "Deployment Role Distribution" %}</h2>
<p>{% trans "Statistics currently not available." %}</p>
</div>
<div class="widget">
<h2>{% trans "Horizon UI Connection" %}</h2>
<p><a href="{{ dashboard_url }}"
>{% trans "Horizon UI" %} ({{ dashboard_url }}).</a></p>
</div>
</div>
</div>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
$('div > a[data-target="#detail__log"]').click(function () {
$('li > a[data-target="#detail__log"]').tab('show');
});
});
</script>

View File

@ -10,6 +10,18 @@
{% block main %}
<div class="row-fluid">
<div class="span12">
<div class="actions pull-right">
<a href="#"class="btn btn-danger
{% if not overcloud.is_deployed %}disabled{% endif %}">
<i class="icon-fire icon-white"></i>
{% trans "Undeploy" %}
</a>
<a href="#" class="btn
{% if not overcloud.is_deployed %}disabled{% endif %}">
<i class="icon-resize-full"></i>
{% trans "Scale deployment" %}
</a>
</div>
{{ tab_group.render }}
</div>
</div>

View File

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
from django.core import urlresolvers
from mock import patch, call # noqa
@ -36,8 +38,17 @@ class OvercloudTests(test.BaseAdminViewTests):
def test_index_overcloud_undeployed_get(self):
oc = None
with patch('tuskar_ui.api.Overcloud', **{
'spec_set': ['get', 'stack'],
'spec_set': [
'get',
'is_deployed',
'is_deploying',
'is_failed',
'stack',
],
'stack': None,
'is_deployed': False,
'is_deploying': False,
'is_failed': False,
'get.side_effect': lambda request, overcloud_id: oc,
}) as Overcloud:
oc = api.Overcloud
@ -47,9 +58,48 @@ class OvercloudTests(test.BaseAdminViewTests):
[call(request, 1)])
self.assertRedirectsNoFollow(res, CREATE_URL)
def test_create_overcloud_undeployed_post(self):
def test_index_overcloud_deployed(self):
oc = None
stack = TEST_DATA.heatclient_stacks.first()
with patch('tuskar_ui.api.Overcloud', **{
'spec_set': [
'get',
'is_deployed',
'is_deploying',
'is_failed',
'id',
'stack',
],
'stack': stack,
'is_deployed': True,
'is_deploying': False,
'is_failed': False,
'id': 1,
'get.side_effect': lambda request, overcloud_id: oc,
}) as Overcloud:
oc = Overcloud
res = self.client.get(INDEX_URL)
request = Overcloud.get.call_args_list[0][0][0] # This is a hack.
self.assertListEqual(Overcloud.get.call_args_list,
[call(request, 1)])
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_create_get(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
with patch('tuskar_ui.api.OvercloudRole', **{
'spec_set': ['list'],
'list.side_effect': lambda request: roles,
}):
res = self.client.get(CREATE_URL)
self.assertTemplateUsed(
res, 'infrastructure/_fullscreen_workflow_base.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/undeployed_overview.html')
def test_create_post(self):
oc = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first())
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
data = {
'count__1__default': '1',
'count__2__default': '0',
@ -87,33 +137,36 @@ class OvercloudTests(test.BaseAdminViewTests):
])
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_index_overcloud_deployed(self):
def test_detail_get(self):
oc = None
stack = TEST_DATA.heatclient_stacks.first()
with patch('tuskar_ui.api.Overcloud', **{
'spec_set': ['get', 'stack', 'id'],
'stack': stack,
'id': 1,
'get.side_effect': lambda request, overcloud_id: oc,
}) as Overcloud:
oc = Overcloud
res = self.client.get(INDEX_URL)
request = Overcloud.get.call_args_list[0][0][0] # This is a hack.
self.assertListEqual(Overcloud.get.call_args_list,
[call(request, 1)])
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_create_get(self):
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
with patch('tuskar_ui.api.OvercloudRole', **{
with contextlib.nested(patch('tuskar_ui.api.Overcloud', **{
'spec_set': [
'get',
'is_deployed',
'is_deploying',
'is_failed',
'resources',
'dashboard_url',
'stack_events',
],
'is_deployed': True,
'is_deploying': False,
'is_failed': False,
'get.side_effect': lambda request, overcloud_id: oc,
'resources.return_value': [],
'dashboard_url': '',
'stack_events': [],
}), patch('tuskar_ui.api.OvercloudRole', **{
'spec_set': ['list'],
'list.side_effect': lambda request: roles,
}):
res = self.client.get(CREATE_URL)
})) as (Overcloud, OvercloudRole):
oc = Overcloud
res = self.client.get(DETAIL_URL)
self.assertTemplateUsed(
res, 'infrastructure/_fullscreen_workflow_base.html')
res, 'infrastructure/overcloud/detail.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/undeployed_overview.html')
res, 'infrastructure/overcloud/_detail_overview.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/_detail_configuration.html')

View File

@ -33,7 +33,11 @@ class IndexView(base_views.RedirectView):
def get_redirect_url(self):
overcloud = api.Overcloud.get(self.request, 1)
if overcloud is not None and overcloud.stack is not None:
if overcloud is not None and overcloud.stack is not None and any([
overcloud.is_deployed,
overcloud.is_deploying,
overcloud.is_failed,
]):
redirect = reverse('horizon:infrastructure:overcloud:detail',
args=(overcloud.id,))
else:
@ -50,19 +54,25 @@ class DetailView(horizon_tabs.TabView):
tab_group_class = tabs.DetailTabs
template_name = 'infrastructure/overcloud/detail.html'
def get_data(self, request, **kwargs):
overcloud_id = kwargs['overcloud_id']
@memoized.memoized_method
def get_data(self):
overcloud_id = self.kwargs['overcloud_id']
try:
return api.Overcloud.get(request, overcloud_id)
return api.Overcloud.get(self.request, overcloud_id)
except Exception:
msg = _("Unable to retrieve deployment.")
redirect = reverse('horizon:infrastructure:overcloud:index')
exceptions.handle(request, msg, redirect=redirect)
exceptions.handle(self.request, msg, redirect=redirect)
def get_tabs(self, request, **kwargs):
overcloud = self.get_data(request, **kwargs)
overcloud = self.get_data()
return self.tab_group_class(request, overcloud=overcloud, **kwargs)
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context['overcloud'] = self.get_data()
return context
class OvercloudRoleView(horizon_tables.DataTableView):
table_class = tables.OvercloudRoleNodeTable