summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLadislav Smola <lsmola@redhat.com>2014-04-04 16:08:13 +0200
committerLadislav Smola <lsmola@redhat.com>2014-04-09 17:26:11 +0200
commit6311948afe77bd1a757f5772f1d11c6d81873ec1 (patch)
tree3045b994c257b1b51f2adb6fccc7d4e3ecdf486f
parent0f0ea0996bb5a23eb05487baa345f780441456ac (diff)
downloadtuskar-ui-6311948afe77bd1a757f5772f1d11c6d81873ec1.tar.gz
Undeploy in progress page
-Separate undeploy in progress page, so we can deal with different behaviour when undeploying. Fixes-bug: #1284059 Change-Id: I93cd5a533d198ce11f75eefaf9e91824f5cd19ba
-rw-r--r--tuskar_ui/api.py26
-rw-r--r--tuskar_ui/infrastructure/overcloud/tabs.py46
-rw-r--r--tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html13
-rw-r--r--tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html57
-rw-r--r--tuskar_ui/infrastructure/overcloud/tests.py49
-rw-r--r--tuskar_ui/infrastructure/overcloud/urls.py3
-rw-r--r--tuskar_ui/infrastructure/overcloud/views.py72
7 files changed, 224 insertions, 42 deletions
diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py
index 11dcfbb1..0cee6368 100644
--- a/tuskar_ui/api.py
+++ b/tuskar_ui/api.py
@@ -367,7 +367,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
@@ -395,12 +395,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
@@ -446,7 +440,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):
@@ -457,6 +451,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
@@ -469,16 +472,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
diff --git a/tuskar_ui/infrastructure/overcloud/tabs.py b/tuskar_ui/infrastructure/overcloud/tabs.py
index 7c77c2f7..aa3e8b99 100644
--- a/tuskar_ui/infrastructure/overcloud/tabs.py
+++ b/tuskar_ui/infrastructure/overcloud/tabs.py
@@ -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)
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html
index b9f247f2..bcb85fba 100644
--- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html
+++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html
@@ -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.
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html
new file mode 100644
index 00000000..50abe0ed
--- /dev/null
+++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html
@@ -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>
diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py
index a995744f..57c28dc4 100644
--- a/tuskar_ui/infrastructure/overcloud/tests.py
+++ b/tuskar_ui/infrastructure/overcloud/tests.py
@@ -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()
diff --git a/tuskar_ui/infrastructure/overcloud/urls.py b/tuskar_ui/infrastructure/overcloud/urls.py
index 7756c6f1..d2244dcb 100644
--- a/tuskar_ui/infrastructure/overcloud/urls.py
+++ b/tuskar_ui/infrastructure/overcloud/urls.py
@@ -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(),
diff --git a/tuskar_ui/infrastructure/overcloud/views.py b/tuskar_ui/infrastructure/overcloud/views.py
index 1ee0982e..c124bd5a 100644
--- a/tuskar_ui/infrastructure/overcloud/views.py
+++ b/tuskar_ui/infrastructure/overcloud/views.py
@@ -16,7 +16,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
@@ -32,6 +35,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):
@@ -59,22 +66,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
@@ -102,7 +108,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,
@@ -116,6 +122,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
@@ -153,7 +195,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)
@@ -162,7 +204,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
@@ -182,7 +224,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()