diff options
author | Jenkins <jenkins@review.openstack.org> | 2014-04-09 16:01:05 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2014-04-09 16:01:05 +0000 |
commit | 36a0bc621b2149185c6cc82b621cda111e2b98d4 (patch) | |
tree | f1abdbfca9ed51aa4edd315e3a9c737eceb28170 /tuskar_ui | |
parent | e75b1f459c549000f325b255288f06c884df0753 (diff) | |
parent | 6311948afe77bd1a757f5772f1d11c6d81873ec1 (diff) | |
download | tuskar-ui-0.1.0.tar.gz |
Merge "Undeploy in progress page"0.1.0
Diffstat (limited to 'tuskar_ui')
-rw-r--r-- | tuskar_ui/api.py | 26 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overcloud/tabs.py | 46 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html | 13 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html | 57 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overcloud/tests.py | 49 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overcloud/urls.py | 3 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/overcloud/views.py | 72 |
7 files changed, 224 insertions, 42 deletions
diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py index 11fe6d56..db80595a 100644 --- a/tuskar_ui/api.py +++ b/tuskar_ui/api.py @@ -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 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 72e44dd5..cbcc7200 100644 --- a/tuskar_ui/infrastructure/overcloud/views.py +++ b/tuskar_ui/infrastructure/overcloud/views.py @@ -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() |