Merge "Undeploy in progress page"

This commit is contained in:
Jenkins 2014-04-09 16:01:05 +00:00 committed by Gerrit Code Review
commit 36a0bc621b
7 changed files with 224 additions and 42 deletions

View File

@ -370,7 +370,7 @@ class Overcloud(base.APIResourceWrapper):
the_overcloud = cls(object(), request=request)
# I need to mock attributes of overcloud that is being deleted.
the_overcloud.id = "deleting_in_progress"
the_overcloud.id = "overcloud"
if the_overcloud.stack and the_overcloud.is_deleting:
return the_overcloud
@ -398,12 +398,6 @@ class Overcloud(base.APIResourceWrapper):
found
:rtype: heatclient.v1.stacks.Stack or None
"""
# TODO(lsmola) load it properly, once the API has finished workflow
# and for example there can't be a situation when I delete Overcloud
# but Stack is still deleting. So the Overcloud will represent the
# state of all inner entities and operations correctly.
# Then also delete the try/except, it should not be caught on this
# level.
return heat.stack_get(self._request, 'overcloud')
@cached_property
@ -449,7 +443,7 @@ class Overcloud(base.APIResourceWrapper):
:rtype: bool
"""
return self.stack.stack_status in ('CREATE_FAILED',
'UPDATE_FAILED')
'UPDATE_FAILED',)
@cached_property
def is_deleting(self):
@ -460,6 +454,15 @@ class Overcloud(base.APIResourceWrapper):
"""
return self.stack.stack_status in ('DELETE_IN_PROGRESS', )
@cached_property
def is_delete_failed(self):
"""Check if this Overcloud deleting has failed.
:return: True if Overcloud deleting has failed, False otherwise.
:rtype: bool
"""
return self.stack.stack_status in ('DELETE_FAILED', )
@memoized.memoized
def all_resources(self, with_joins=True):
"""Return a list of all Overcloud Resources
@ -472,16 +475,9 @@ class Overcloud(base.APIResourceWrapper):
are none
:rtype: list of tuskar_ui.api.Resource
"""
# FIXME(lsmola) of this is a temporary hack. When I delete the stack
# there is a brief moment when list of resources throws an exception
# a second later, it does not. So the delete in progress page will
# need to be separated, because it is 'special'. Till then, this hack
# stays.
try:
resources = [r for r in heat.resources_list(self._request,
self.stack.stack_name)]
except heatclient.exc.HTTPNotFound:
resources = []
except heatclient.exc.HTTPInternalServerError:
# TODO(lsmola) There is a weird bug in heat, that after
# stack-create it returns 500 for a little while. This can be

View File

@ -13,6 +13,7 @@
# under the License.
from django.utils.translation import ugettext_lazy as _
import heatclient
from horizon import tabs
from tuskar_ui import api
@ -100,6 +101,45 @@ class OverviewTab(tabs.Tab):
}
class UndeployInProgressTab(tabs.Tab):
name = _("Overview")
slug = "undeploy_in_progress_tab"
template_name = "infrastructure/overcloud/_undeploy_in_progress.html"
preload = False
def get_context_data(self, request, **kwargs):
overcloud = self.tab_group.kwargs['overcloud']
# TODO(lsmola) since at this point we don't have total number of nodes
# we will hack this around, till API can show this information. So it
# will actually show progress like the total number is 10, or it will
# show progress of 5%. Ugly, but workable.
total_num_nodes_count = 10
try:
all_resources_count = len(
overcloud.all_resources(with_joins=False))
except heatclient.exc.HTTPNotFound:
# Immediately after undeploying has started, heat returns this
# exception so we can take it as kind of init of undeploying.
all_resources_count = total_num_nodes_count
# TODO(lsmola) same as hack above
total_num_nodes_count = max(all_resources_count, total_num_nodes_count)
delete_progress = max(
5, 100 * (total_num_nodes_count - all_resources_count))
events = overcloud.stack_events
last_failed_events = [e for e in events
if e.resource_status == 'DELETE_FAILED'][-3:]
return {
'overcloud': overcloud,
'progress': delete_progress,
'last_failed_events': last_failed_events,
}
class ConfigurationTab(tabs.TableTab):
table_classes = (tables.ConfigurationTable,)
name = _("Configuration")
@ -126,6 +166,12 @@ class LogTab(tabs.TableTab):
return overcloud.stack_events
class UndeployInProgressTabs(tabs.TabGroup):
slug = "undeploy_in_progress"
tabs = (UndeployInProgressTab, LogTab)
sticky = True
class DetailTabs(tabs.TabGroup):
slug = "detail"
tabs = (OverviewTab, ConfigurationTab, LogTab)

View File

@ -1,7 +1,7 @@
{% load i18n %}
{% load url from future%}
{% if overcloud.is_deploying or overcloud.is_failed or overcloud.is_deleting %}
{% if overcloud.is_deploying or overcloud.is_failed %}
{% if overcloud.is_deploying %}
<div class="alert alert-info">
<div class="row-fluid">
@ -11,15 +11,6 @@
<div class="bar bar-info" style="width:{{ progress }}%"></div>
</div>
</div>
{% elif overcloud.is_deleting %}
<div class="alert alert-error">
<div class="row-fluid">
<div class="span2">
<strong>Undeploying...</strong>
<div class="progress progress-striped progress-danger">
<div class="bar bar-info" style="width:{{ progress }}%"></div>
</div>
</div>
{% else %}
<div class="alert alert-error">
<div class="row-fluid">
@ -54,7 +45,7 @@
</div>
{% endif %}
{% if not dashboard_urls and not overcloud.is_deploying and not overcloud.is_failed and not overcloud.is_deleting %}
{% if not dashboard_urls and not overcloud.is_deploying and not overcloud.is_failed %}
<div class="row-fluid">
<div class="span8">
<p>Your OpenStack Deployment has been successfully deployed. It needs to be initialized before you will be able to use it.

View File

@ -0,0 +1,57 @@
{% load i18n %}
<div class="row-fluid">
<div class="span12">
<div class="actions pull-right">
</div>
{% if overcloud.is_deleting %}
<div class="alert alert-error">
<div class="row-fluid">
<div class="span2">
<strong>Undeploying...</strong>
<div class="progress progress-striped progress-danger">
<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>Undeploying 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_failed_events %}
<strong>{% trans "Last failed events:" %}</strong>
{% for event in last_failed_events %}
<div>
<dl>
<dt>{% trans "Timestamp" %}</dt>
<dd><time datetime="{{ event.event_time }}">{{ event.event_time }}</time></dd>
<dt>{% trans "Resource Name" %}</dt>
<dd>{{ event.resource_name }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ event.resource_status }}</dd>
<dt>{% trans "Reason" %}</dt>
<dd>{{ event.resource_status_reason }}</dd>
</dl>
</div>
{% endfor %}
{% endif %}
<a href="?tab=undeploy_in_progress__log" data-toggle="tab" data-target="#undeploy_in_progress__log" class="pull-right">See full log</a>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
$('div > a[data-target="#undeploy_in_progress__log"]').click(function () {
$('li > a[data-target="#undeploy_in_progress__log"]').tab('show');
});
});
</script>

View File

@ -29,6 +29,11 @@ CREATE_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:create')
DETAIL_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:detail', args=(1,))
UNDEPLOY_IN_PROGRESS_URL = urlresolvers.reverse(
'horizon:infrastructure:overcloud:undeploy_in_progress',
args=('overcloud',))
UNDEPLOY_IN_PROGRESS_URL_LOG_TAB = (
UNDEPLOY_IN_PROGRESS_URL + "?tab=undeploy_in_progress__log")
DETAIL_URL_CONFIGURATION_TAB = (DETAIL_URL +
"?tab=detail__configuration")
DETAIL_URL_LOG_TAB = (DETAIL_URL + "?tab=detail__log")
@ -70,7 +75,10 @@ def _mock_overcloud(**kwargs):
'id',
'is_deployed',
'is_deploying',
'is_deleting',
'is_delete_failed',
'is_failed',
'all_resources',
'resources',
'stack',
'stack_events',
@ -86,7 +94,10 @@ def _mock_overcloud(**kwargs):
'id': 1,
'is_deployed': True,
'is_deploying': False,
'is_deleting': False,
'is_delete_failed': False,
'is_failed': False,
'all_resources.return_value': [],
'resources.return_value': [],
'stack_events': [],
'stack': stack,
@ -102,7 +113,8 @@ def _mock_overcloud(**kwargs):
class OvercloudTests(test.BaseAdminViewTests):
def test_index_overcloud_undeployed_get(self):
with patch('tuskar_ui.api.Overcloud.list', return_value=[]):
with _mock_overcloud(**{'get_the_overcloud.side_effect': None,
'get_the_overcloud.return_value': None}):
res = self.client.get(INDEX_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
@ -305,6 +317,41 @@ class OvercloudTests(test.BaseAdminViewTests):
res = self.client.post(DELETE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_undeploy_in_progress(self):
with _mock_overcloud(is_deleting=True, is_deployed=False):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertTemplateUsed(
res, 'infrastructure/overcloud/detail.html')
self.assertTemplateUsed(
res, 'infrastructure/overcloud/_undeploy_in_progress.html')
self.assertTemplateNotUsed(
res, 'horizon/common/_detail_table.html')
def test_undeploy_in_progress_finished(self):
with _mock_overcloud(**{'get_the_overcloud.side_effect': None,
'get_the_overcloud.return_value': None}):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertRedirectsNoFollow(res, CREATE_URL)
def test_undeploy_in_progress_invalid(self):
with _mock_overcloud():
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
self.assertRedirectsNoFollow(res, DETAIL_URL)
def test_undeploy_in_progress_log_tab(self):
with _mock_overcloud(is_deleting=True, is_deployed=False):
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL_LOG_TAB)
self.assertTemplateUsed(
res, 'infrastructure/overcloud/detail.html')
self.assertTemplateNotUsed(
res, 'infrastructure/overcloud/_undeploy_in_progress.html')
self.assertTemplateUsed(
res, 'horizon/common/_detail_table.html')
def test_scale_get(self):
oc = None
roles = TEST_DATA.tuskarclient_overcloud_roles.list()

View File

@ -21,6 +21,9 @@ urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
urls.url(r'^(?P<overcloud_id>[^/]+)/undeploy-in-progress$',
views.UndeployInProgressView.as_view(),
name='undeploy_in_progress'),
urls.url(r'^create/role-edit/(?P<role_id>[^/]+)$',
views.OvercloudRoleEdit.as_view(), name='role_edit'),
urls.url(r'^(?P<overcloud_id>[^/]+)/$', views.DetailView.as_view(),

View File

@ -17,7 +17,10 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.views.generic import base as base_views
import heatclient
from horizon import exceptions as horizon_exceptions
import horizon.forms
from horizon import messages
from horizon import tables as horizon_tables
from horizon import tabs as horizon_tabs
from horizon.utils import memoized
@ -33,6 +36,10 @@ from tuskar_ui.infrastructure.overcloud.workflows import undeployed
INDEX_URL = 'horizon:infrastructure:overcloud:index'
DETAIL_URL = 'horizon:infrastructure:overcloud:detail'
CREATE_URL = 'horizon:infrastructure:overcloud:create'
UNDEPLOY_IN_PROGRESS_URL = (
'horizon:infrastructure:overcloud:undeploy_in_progress')
class OvercloudMixin(object):
@ -60,22 +67,21 @@ class IndexView(base_views.RedirectView):
def get_redirect_url(self):
try:
# TODO(lsmola) implement this properly when supported by API
# TODO(lsmola) implement this properly when supported by API
overcloud = api.Overcloud.get_the_overcloud(self.request)
except Exception:
except heatclient.exc.HTTPNotFound:
overcloud = None
if overcloud is not None:
# TODO(lsmola) there can be a short period when overcloud
# is created, but stack not. So we have to make sure we have
# missing stack under control as a new STATE
# Also when deleting now, it first deletes Overcloud then Stack
# because stack takes much longer to delete. But we can probably
# ignore it for now and fix the worflow on API side.
redirect = reverse('horizon:infrastructure:overcloud:detail',
redirect = None
if overcloud is None:
redirect = reverse(CREATE_URL)
elif overcloud.is_deleting or overcloud.is_delete_failed:
redirect = reverse(UNDEPLOY_IN_PROGRESS_URL,
args=(overcloud.id,))
else:
redirect = reverse('horizon:infrastructure:overcloud:create')
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
return redirect
@ -103,7 +109,7 @@ class UndeployConfirmationView(horizon.forms.ModalFormView):
template_name = 'infrastructure/overcloud/undeploy_confirmation.html'
def get_success_url(self):
return reverse('horizon:infrastructure:overcloud:index')
return reverse(INDEX_URL)
def get_context_data(self, **kwargs):
context = super(UndeployConfirmationView,
@ -117,6 +123,42 @@ class UndeployConfirmationView(horizon.forms.ModalFormView):
return initial
class UndeployInProgressView(horizon_tabs.TabView, OvercloudMixin, ):
tab_group_class = tabs.UndeployInProgressTabs
template_name = 'infrastructure/overcloud/detail.html'
def get_overcloud_or_redirect(self):
try:
# TODO(lsmola) implement this properly when supported by API
overcloud = api.Overcloud.get_the_overcloud(self.request)
except heatclient.exc.HTTPNotFound:
overcloud = None
if overcloud is None:
redirect = reverse(CREATE_URL)
messages.success(self.request,
_("Undeploying of the Overcloud has finished."))
raise horizon_exceptions.Http302(redirect)
elif overcloud.is_deleting or overcloud.is_delete_failed:
return overcloud
else:
messages.error(self.request,
_("Overcloud is not being undeployed."))
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
raise horizon_exceptions.Http302(redirect)
def get_tabs(self, request, **kwargs):
overcloud = self.get_overcloud_or_redirect()
return self.tab_group_class(request, overcloud=overcloud, **kwargs)
def get_context_data(self, **kwargs):
context = super(UndeployInProgressView,
self).get_context_data(**kwargs)
context['overcloud'] = self.get_overcloud_or_redirect()
return context
class Scale(horizon.workflows.WorkflowView, OvercloudMixin):
workflow_class = scale.Workflow
@ -154,7 +196,7 @@ class OvercloudRoleView(horizon_tables.DataTableView,
def get_data(self):
overcloud = self.get_overcloud()
redirect = reverse('horizon:infrastructure:overcloud:detail',
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
role = self.get_role(redirect)
return self._get_nodes(overcloud, role)
@ -163,7 +205,7 @@ class OvercloudRoleView(horizon_tables.DataTableView,
context = super(OvercloudRoleView, self).get_context_data(**kwargs)
overcloud = self.get_overcloud()
redirect = reverse('horizon:infrastructure:overcloud:detail',
redirect = reverse(DETAIL_URL,
args=(overcloud.id,))
role = self.get_role(redirect)
context['role'] = role
@ -185,7 +227,7 @@ class OvercloudRoleEdit(horizon.forms.ModalFormView, OvercloudRoleMixin):
template_name = 'infrastructure/overcloud/role_edit.html'
def get_success_url(self):
return reverse('horizon:infrastructure:overcloud:create')
return reverse(CREATE_URL)
def get_initial(self):
role = self.get_role()