Added Overview panel
The overview panel is a combination of the former overcloud and plan panels. The assumption here is that a plan already exists; different view states will be determined by whether an associated stack exists. Future enhancements will include a reorganization of the templates used and updating the mock data to simulate the creation of a Heat stack Change-Id: I37d0943d3dbe0cb2849af82a459bc5b2af17881c
This commit is contained in:
parent
907210928c
commit
222891cf83
@ -20,8 +20,7 @@ class BasePanels(horizon.PanelGroup):
|
||||
slug = "infrastructure"
|
||||
name = _("Infrastructure")
|
||||
panels = (
|
||||
'overcloud',
|
||||
'plans',
|
||||
'overview',
|
||||
'parameters',
|
||||
'roles',
|
||||
'nodes',
|
||||
@ -37,7 +36,7 @@ class Infrastructure(horizon.Dashboard):
|
||||
panels = (
|
||||
BasePanels,
|
||||
)
|
||||
default_panel = 'overcloud'
|
||||
default_panel = 'overview'
|
||||
permissions = ('openstack.roles.admin',)
|
||||
|
||||
|
||||
|
@ -1,27 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
from tuskar_ui.infrastructure import dashboard
|
||||
|
||||
|
||||
class Overcloud(horizon.Panel):
|
||||
name = _("OpenStack Deployment")
|
||||
slug = "overcloud"
|
||||
|
||||
|
||||
dashboard.Infrastructure.register(Overcloud)
|
@ -1,35 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
|
||||
class ConfigurationTable(tables.DataTable):
|
||||
|
||||
key = tables.Column(lambda parameter: parameter[0],
|
||||
verbose_name=_("Attribute Name"))
|
||||
value = tables.Column(lambda parameter: parameter[1],
|
||||
verbose_name=_("Attribute Value"))
|
||||
|
||||
class Meta:
|
||||
name = "configuration"
|
||||
verbose_name = _("Configuration")
|
||||
multi_select = False
|
||||
table_actions = ()
|
||||
row_actions = ()
|
||||
|
||||
def get_object_id(self, datum):
|
||||
return datum[0]
|
@ -1,161 +0,0 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import heatclient
|
||||
from horizon import tabs
|
||||
|
||||
from tuskar_ui.infrastructure.overcloud import tables
|
||||
from tuskar_ui.utils import utils
|
||||
|
||||
|
||||
def _get_role_data(stack, role):
|
||||
"""Gathers data about a single deployment role from the related Overcloud
|
||||
and OvercloudRole objects, and presents it in the form convenient for use
|
||||
from the template.
|
||||
|
||||
:param stack: Stack object
|
||||
:type stack: tuskar_ui.api.heat.Stack
|
||||
:param role: Role object
|
||||
:type role: tuskar_ui.api.tuskar.OvercloudRole
|
||||
:return: dict with information about the role, to be used by template
|
||||
:rtype: dict
|
||||
"""
|
||||
resources = stack.resources_by_role(role, with_joins=True)
|
||||
nodes = [r.node for r in resources]
|
||||
node_count = len(nodes)
|
||||
|
||||
data = {
|
||||
'role': role,
|
||||
'name': role.name,
|
||||
'total_node_count': node_count,
|
||||
}
|
||||
deployed_node_count = 0
|
||||
deploying_node_count = 0
|
||||
error_node_count = 0
|
||||
waiting_node_count = node_count
|
||||
|
||||
if nodes:
|
||||
deployed_node_count = sum(1 for node in nodes
|
||||
if node.instance.status == 'ACTIVE')
|
||||
deploying_node_count = sum(1 for node in nodes
|
||||
if node.instance.status == 'BUILD')
|
||||
error_node_count = sum(1 for node in nodes
|
||||
if node.instance.status == 'ERROR')
|
||||
waiting_node_count = (node_count - deployed_node_count -
|
||||
deploying_node_count - error_node_count)
|
||||
|
||||
data.update({
|
||||
'deployed_node_count': deployed_node_count,
|
||||
'deploying_node_count': deploying_node_count,
|
||||
'waiting_node_count': waiting_node_count,
|
||||
'error_node_count': error_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"
|
||||
preload = False
|
||||
|
||||
def get_context_data(self, request, **kwargs):
|
||||
stack = self.tab_group.kwargs['stack']
|
||||
roles = stack.plan.role_list
|
||||
role_data = [_get_role_data(stack, role) for role in roles]
|
||||
total = sum(d['total_node_count'] for d in role_data)
|
||||
progress = 100 * sum(d.get('deployed_node_count', 0)
|
||||
for d in role_data) // (total or 1)
|
||||
|
||||
events = stack.events
|
||||
last_failed_events = [e for e in events
|
||||
if e.resource_status == 'CREATE_FAILED'][-3:]
|
||||
|
||||
return {
|
||||
'stack': stack,
|
||||
'plan': stack.plan,
|
||||
'roles': role_data,
|
||||
'progress': max(5, progress),
|
||||
'dashboard_urls': stack.dashboard_urls,
|
||||
'last_failed_events': last_failed_events,
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
stack = self.tab_group.kwargs['stack']
|
||||
|
||||
# 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:
|
||||
resources_count = len(
|
||||
stack.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.
|
||||
resources_count = total_num_nodes_count
|
||||
|
||||
# TODO(lsmola) same as hack above
|
||||
total_num_nodes_count = max(resources_count, total_num_nodes_count)
|
||||
|
||||
delete_progress = max(
|
||||
5, 100 * (total_num_nodes_count - resources_count))
|
||||
|
||||
events = stack.events
|
||||
last_failed_events = [e for e in events
|
||||
if e.resource_status == 'DELETE_FAILED'][-3:]
|
||||
return {
|
||||
'stack': stack,
|
||||
'plan': stack.plan,
|
||||
'progress': delete_progress,
|
||||
'last_failed_events': last_failed_events,
|
||||
}
|
||||
|
||||
|
||||
class ConfigurationTab(tabs.TableTab):
|
||||
table_classes = (tables.ConfigurationTable,)
|
||||
name = _("Configuration")
|
||||
slug = "configuration"
|
||||
template_name = "horizon/common/_detail_table.html"
|
||||
preload = False
|
||||
|
||||
def get_configuration_data(self):
|
||||
stack = self.tab_group.kwargs['stack']
|
||||
|
||||
return [(utils.de_camel_case(key), value) for key, value in
|
||||
stack.parameters.items()]
|
||||
|
||||
|
||||
class UndeployInProgressTabs(tabs.TabGroup):
|
||||
slug = "undeploy_in_progress"
|
||||
tabs = (UndeployInProgressTab,)
|
||||
sticky = True
|
||||
|
||||
|
||||
class DetailTabs(tabs.TabGroup):
|
||||
slug = "detail"
|
||||
tabs = (OverviewTab, ConfigurationTab,)
|
||||
sticky = True
|
@ -1,31 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import urls
|
||||
|
||||
from tuskar_ui.infrastructure.overcloud import views
|
||||
|
||||
|
||||
urlpatterns = urls.patterns(
|
||||
'',
|
||||
urls.url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/undeploy-in-progress$',
|
||||
views.UndeployInProgressView.as_view(),
|
||||
name='undeploy_in_progress'),
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/$', views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
urls.url(r'^(?P<stack_id>[^/]+)/undeploy-confirmation$',
|
||||
views.UndeployConfirmationView.as_view(),
|
||||
name='undeploy_confirmation'),
|
||||
)
|
@ -1,144 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import base as base_views
|
||||
|
||||
from horizon import exceptions as horizon_exceptions
|
||||
import horizon.forms
|
||||
from horizon import messages
|
||||
from horizon import tabs as horizon_tabs
|
||||
from horizon.utils import memoized
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.overcloud import forms
|
||||
from tuskar_ui.infrastructure.overcloud import tabs
|
||||
|
||||
|
||||
INDEX_URL = 'horizon:infrastructure:overcloud:index'
|
||||
DETAIL_URL = 'horizon:infrastructure:overcloud:detail'
|
||||
PLAN_CREATE_URL = 'horizon:infrastructure:plans:create'
|
||||
UNDEPLOY_IN_PROGRESS_URL = (
|
||||
'horizon:infrastructure:overcloud:undeploy_in_progress')
|
||||
|
||||
|
||||
class StackMixin(object):
|
||||
@memoized.memoized
|
||||
def get_stack(self, redirect=None):
|
||||
if redirect is None:
|
||||
redirect = reverse(INDEX_URL)
|
||||
stack_id = self.kwargs['stack_id']
|
||||
stack = api.heat.Stack.get(self.request, stack_id,
|
||||
_error_redirect=redirect)
|
||||
return stack
|
||||
|
||||
|
||||
class IndexView(base_views.RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self):
|
||||
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
|
||||
|
||||
redirect = reverse(PLAN_CREATE_URL)
|
||||
if plan is not None:
|
||||
stacks = api.heat.Stack.list(self.request)
|
||||
for stack in stacks:
|
||||
if stack.plan.id == plan.id:
|
||||
break
|
||||
else:
|
||||
stack = None
|
||||
|
||||
if stack is not None:
|
||||
if stack.is_deleting or stack.is_delete_failed:
|
||||
redirect = reverse(UNDEPLOY_IN_PROGRESS_URL,
|
||||
args=(stack.id,))
|
||||
else:
|
||||
redirect = reverse(DETAIL_URL,
|
||||
args=(stack.id,))
|
||||
return redirect
|
||||
|
||||
|
||||
class DetailView(horizon_tabs.TabView, StackMixin):
|
||||
tab_group_class = tabs.DetailTabs
|
||||
template_name = 'infrastructure/overcloud/detail.html'
|
||||
|
||||
def get_tabs(self, request, **kwargs):
|
||||
stack = self.get_stack()
|
||||
return self.tab_group_class(request, stack=stack, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
context['stack'] = self.get_stack()
|
||||
context['plan'] = self.get_stack().plan
|
||||
return context
|
||||
|
||||
|
||||
class UndeployConfirmationView(horizon.forms.ModalFormView):
|
||||
form_class = forms.UndeployOvercloud
|
||||
template_name = 'infrastructure/overcloud/undeploy_confirmation.html'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(INDEX_URL)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UndeployConfirmationView,
|
||||
self).get_context_data(**kwargs)
|
||||
context['stack_id'] = self.kwargs['stack_id']
|
||||
return context
|
||||
|
||||
def get_initial(self, **kwargs):
|
||||
initial = super(UndeployConfirmationView, self).get_initial(**kwargs)
|
||||
initial['stack_id'] = self.kwargs['stack_id']
|
||||
return initial
|
||||
|
||||
|
||||
class UndeployInProgressView(horizon_tabs.TabView, StackMixin, ):
|
||||
tab_group_class = tabs.UndeployInProgressTabs
|
||||
template_name = 'infrastructure/overcloud/detail.html'
|
||||
|
||||
def get_stack_or_redirect(self):
|
||||
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
|
||||
stack = None
|
||||
|
||||
if plan is not None:
|
||||
stack = None
|
||||
stacks = api.heat.Stack.list(self.request)
|
||||
for s in stacks:
|
||||
if s.plan.id == plan.id:
|
||||
stack = s
|
||||
break
|
||||
|
||||
if stack is None:
|
||||
redirect = reverse(INDEX_URL)
|
||||
messages.success(self.request,
|
||||
_("Undeploying of the Overcloud has finished."))
|
||||
raise horizon_exceptions.Http302(redirect)
|
||||
elif stack.is_deleting or stack.is_delete_failed:
|
||||
return stack
|
||||
else:
|
||||
messages.error(self.request,
|
||||
_("Overcloud is not being undeployed."))
|
||||
redirect = reverse(DETAIL_URL,
|
||||
args=(stack.id,))
|
||||
raise horizon_exceptions.Http302(redirect)
|
||||
|
||||
def get_tabs(self, request, **kwargs):
|
||||
stack = self.get_stack_or_redirect()
|
||||
return self.tab_group_class(request, stack=stack, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UndeployInProgressView,
|
||||
self).get_context_data(**kwargs)
|
||||
context['stack'] = self.get_stack_or_redirect()
|
||||
return context
|
@ -19,10 +19,9 @@ import horizon
|
||||
from tuskar_ui.infrastructure import dashboard
|
||||
|
||||
|
||||
class Plans(horizon.Panel):
|
||||
name = _("Plans")
|
||||
slug = "plans"
|
||||
nav = False
|
||||
class Overview(horizon.Panel):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
|
||||
|
||||
dashboard.Infrastructure.register(Plans)
|
||||
dashboard.Infrastructure.register(Overview)
|
@ -3,7 +3,7 @@
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}provision_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' stack_id %}{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:overview:undeploy_confirmation' %}{% endblock %}
|
||||
|
||||
{% block modal_id %}provision_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Provisioning Confirmation" %}{% endblock %}
|
||||
@ -21,5 +21,5 @@
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary" type="submit" value="{% trans "Undeploy" %}" />
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
<a href="{% url 'horizon:infrastructure:overview:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -41,7 +41,7 @@
|
||||
</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>
|
||||
<a href="{% url 'horizon:infrastructure:history:index' %}" class="pull-right">See full log</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -19,14 +19,22 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="actions page-actions pull-right">
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:undeploy_confirmation' stack.id %}"
|
||||
<a href="{% url 'horizon:infrastructure:overview:undeploy_confirmation' %}"
|
||||
class="btn btn-danger ajax-modal
|
||||
{% if not stack.is_deployed and not stack.is_failed %}disabled{% endif %}">
|
||||
<i class="icon-fire icon-white"></i>
|
||||
{% trans "Undeploy" %}
|
||||
</a>
|
||||
</div>
|
||||
{{ tab_group.render }}
|
||||
{% if stack %}
|
||||
{% if stack.is_deleting or stack.is_delete_failed %}
|
||||
{% include "infrastructure/overview/_undeploy_in_progress.html" %}
|
||||
{% else %}
|
||||
{% include "infrastructure/overview/_plan_overview.html" %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include "infrastructure/overview/_role_nodes.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -7,5 +7,5 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block infrastructure_main %}
|
||||
{% include "infrastructure/overcloud/_undeploy_confirmation.html" %}
|
||||
{% include "infrastructure/overview/_undeploy_confirmation.html" %}
|
||||
{% endblock %}
|
@ -20,29 +20,15 @@ from openstack_dashboard.test.test_data import utils
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.test import helpers as test
|
||||
from tuskar_ui.test.test_data import flavor_data
|
||||
from tuskar_ui.test.test_data import heat_data
|
||||
from tuskar_ui.test.test_data import node_data
|
||||
from tuskar_ui.test.test_data import tuskar_data
|
||||
|
||||
|
||||
INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:index')
|
||||
DETAIL_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:detail', args=('stack-id-1',))
|
||||
UNDEPLOY_IN_PROGRESS_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:undeploy_in_progress',
|
||||
args=('overcloud',))
|
||||
DETAIL_URL_CONFIGURATION_TAB = (DETAIL_URL +
|
||||
"?tab=detail__configuration")
|
||||
'horizon:infrastructure:overview:index')
|
||||
DELETE_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:undeploy_confirmation',
|
||||
args=('stack-id-1',))
|
||||
PLAN_CREATE_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:plans:create')
|
||||
'horizon:infrastructure:overview:undeploy_confirmation')
|
||||
TEST_DATA = utils.TestDataContainer()
|
||||
flavor_data.data(TEST_DATA)
|
||||
node_data.data(TEST_DATA)
|
||||
heat_data.data(TEST_DATA)
|
||||
tuskar_data.data(TEST_DATA)
|
||||
|
||||
@ -79,80 +65,42 @@ def _mock_plan(**kwargs):
|
||||
|
||||
class OvercloudTests(test.BaseAdminViewTests):
|
||||
|
||||
def test_index_overcloud_undeployed_get(self):
|
||||
with _mock_plan(**{'get_the_plan.side_effect': None,
|
||||
'get_the_plan.return_value': None}):
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertRedirectsNoFollow(res, PLAN_CREATE_URL)
|
||||
|
||||
def test_index_overcloud_deployed_stack_not_created(self):
|
||||
def test_index_stack_not_created(self):
|
||||
with contextlib.nested(
|
||||
_mock_plan(),
|
||||
patch('tuskar_ui.api.heat.Stack.is_deployed',
|
||||
return_value=False),
|
||||
patch('tuskar_ui.api.heat.Stack.list',
|
||||
return_value=[]),
|
||||
):
|
||||
res = self.client.get(INDEX_URL)
|
||||
request = api.tuskar.OvercloudPlan.get_the_plan. \
|
||||
call_args_list[0][0][0]
|
||||
self.assertListEqual(
|
||||
api.tuskar.OvercloudPlan.get_the_plan.call_args_list,
|
||||
[call(request)])
|
||||
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
||||
[call(request), call(request)])
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overview/index.html')
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overview/_role_nodes.html')
|
||||
|
||||
def test_index_overcloud_deployed(self):
|
||||
with _mock_plan() as OvercloudPlan:
|
||||
def test_index_stack_deployed(self):
|
||||
with contextlib.nested(
|
||||
_mock_plan(),
|
||||
patch('tuskar_ui.api.heat.Stack.get',
|
||||
return_value=None),
|
||||
patch('tuskar_ui.api.heat.Stack.events',
|
||||
return_value=[]),
|
||||
) as (OvercloudPlan, stack_get_mock, stack_events_mock):
|
||||
res = self.client.get(INDEX_URL)
|
||||
request = OvercloudPlan.get_the_plan.call_args_list[0][0][0]
|
||||
self.assertListEqual(OvercloudPlan.get_the_plan.call_args_list,
|
||||
[call(request)])
|
||||
|
||||
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
||||
|
||||
def test_detail_get(self):
|
||||
roles = [api.tuskar.OvercloudRole(role)
|
||||
for role in TEST_DATA.tuskarclient_roles.list()]
|
||||
|
||||
with contextlib.nested(
|
||||
_mock_plan(),
|
||||
patch('tuskar_ui.api.tuskar.OvercloudRole', **{
|
||||
'spec_set': ['list'],
|
||||
'list.return_value': roles,
|
||||
}),
|
||||
patch('tuskar_ui.api.heat.Stack.events',
|
||||
return_value=[]),
|
||||
):
|
||||
res = self.client.get(DETAIL_URL)
|
||||
[call(request), call(request)])
|
||||
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/detail.html')
|
||||
self.assertTemplateNotUsed(
|
||||
res, 'horizon/common/_detail_table.html')
|
||||
res, 'infrastructure/overview/index.html')
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/_detail_overview.html')
|
||||
res, 'infrastructure/overview/_plan_overview.html')
|
||||
|
||||
def test_detail_get_configuration_tab(self):
|
||||
with _mock_plan():
|
||||
res = self.client.get(DETAIL_URL_CONFIGURATION_TAB)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/detail.html')
|
||||
self.assertTemplateNotUsed(
|
||||
res, 'infrastructure/overcloud/_detail_overview.html')
|
||||
self.assertTemplateUsed(
|
||||
res, 'horizon/common/_detail_table.html')
|
||||
|
||||
def test_delete_get(self):
|
||||
res = self.client.get(DELETE_URL)
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/undeploy_confirmation.html')
|
||||
|
||||
def test_delete_post(self):
|
||||
with _mock_plan():
|
||||
res = self.client.post(DELETE_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_undeploy_in_progress(self):
|
||||
def test_index_stack_undeploy_in_progress(self):
|
||||
with contextlib.nested(
|
||||
_mock_plan(),
|
||||
patch('tuskar_ui.api.heat.Stack.is_deleting',
|
||||
@ -162,24 +110,19 @@ class OvercloudTests(test.BaseAdminViewTests):
|
||||
patch('tuskar_ui.api.heat.Stack.events',
|
||||
return_value=[]),
|
||||
):
|
||||
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/detail.html')
|
||||
res, 'infrastructure/overview/index.html')
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/_undeploy_in_progress.html')
|
||||
self.assertTemplateNotUsed(
|
||||
res, 'horizon/common/_detail_table.html')
|
||||
res, 'infrastructure/overview/_undeploy_in_progress.html')
|
||||
|
||||
def test_undeploy_in_progress_finished(self):
|
||||
with _mock_plan(**{'get_the_plan.side_effect': None,
|
||||
'get_the_plan.return_value': None}):
|
||||
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
|
||||
def test_delete_get(self):
|
||||
res = self.client.get(DELETE_URL)
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overview/undeploy_confirmation.html')
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_undeploy_in_progress_invalid(self):
|
||||
def test_delete_post(self):
|
||||
with _mock_plan():
|
||||
res = self.client.get(UNDEPLOY_IN_PROGRESS_URL)
|
||||
|
||||
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
||||
res = self.client.post(DELETE_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
@ -14,13 +14,13 @@
|
||||
|
||||
from django.conf import urls
|
||||
|
||||
from tuskar_ui.infrastructure.plans import views
|
||||
from tuskar_ui.infrastructure.overview import views
|
||||
|
||||
|
||||
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<plan_id>[^/]+)/scale$', views.Scale.as_view(),
|
||||
name='scale'),
|
||||
urls.url(r'^undeploy-confirmation$',
|
||||
views.UndeployConfirmationView.as_view(),
|
||||
name='undeploy_confirmation'),
|
||||
)
|
157
tuskar_ui/infrastructure/overview/views.py
Normal file
157
tuskar_ui/infrastructure/overview/views.py
Normal file
@ -0,0 +1,157 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
import heatclient
|
||||
|
||||
import horizon.forms
|
||||
from horizon.utils import memoized
|
||||
from horizon import views as horizon_views
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.overview import forms
|
||||
|
||||
|
||||
INDEX_URL = 'horizon:infrastructure:overview:index'
|
||||
|
||||
|
||||
def _get_role_data(stack, role):
|
||||
"""Gathers data about a single deployment role from the related Overcloud
|
||||
and OvercloudRole objects, and presents it in the form convenient for use
|
||||
from the template.
|
||||
|
||||
:param stack: Stack object
|
||||
:type stack: tuskar_ui.api.heat.Stack
|
||||
:param role: Role object
|
||||
:type role: tuskar_ui.api.tuskar.OvercloudRole
|
||||
:return: dict with information about the role, to be used by template
|
||||
:rtype: dict
|
||||
"""
|
||||
resources = stack.resources_by_role(role, with_joins=True)
|
||||
nodes = [r.node for r in resources]
|
||||
node_count = len(nodes)
|
||||
|
||||
data = {
|
||||
'role': role,
|
||||
'name': role.name,
|
||||
'total_node_count': node_count,
|
||||
}
|
||||
deployed_node_count = 0
|
||||
deploying_node_count = 0
|
||||
error_node_count = 0
|
||||
waiting_node_count = node_count
|
||||
|
||||
if nodes:
|
||||
deployed_node_count = sum(1 for node in nodes
|
||||
if node.instance.status == 'ACTIVE')
|
||||
deploying_node_count = sum(1 for node in nodes
|
||||
if node.instance.status == 'BUILD')
|
||||
error_node_count = sum(1 for node in nodes
|
||||
if node.instance.status == 'ERROR')
|
||||
waiting_node_count = (node_count - deployed_node_count -
|
||||
deploying_node_count - error_node_count)
|
||||
|
||||
data.update({
|
||||
'deployed_node_count': deployed_node_count,
|
||||
'deploying_node_count': deploying_node_count,
|
||||
'waiting_node_count': waiting_node_count,
|
||||
'error_node_count': error_node_count,
|
||||
})
|
||||
# TODO(rdopieralski) get this from ceilometer
|
||||
# data['capacity'] = 20
|
||||
return data
|
||||
|
||||
|
||||
class StackMixin(object):
|
||||
@memoized.memoized
|
||||
def get_stack(self, redirect=None):
|
||||
if redirect is None:
|
||||
redirect = reverse(INDEX_URL)
|
||||
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
|
||||
stack = api.heat.Stack.get_by_plan(self.request, plan)
|
||||
|
||||
return stack
|
||||
|
||||
|
||||
class IndexView(horizon_views.APIView, StackMixin):
|
||||
template_name = 'infrastructure/overview/index.html'
|
||||
|
||||
def get_data(self, request, context, *args, **kwargs):
|
||||
plan = api.tuskar.OvercloudPlan.get_the_plan(request)
|
||||
stack = self.get_stack()
|
||||
|
||||
context['plan'] = plan
|
||||
context['stack'] = stack
|
||||
|
||||
if stack:
|
||||
context['last_failed_events'] = [
|
||||
e for e in stack.events
|
||||
if e.resource_status == 'DELETE_FAILED'][-3:]
|
||||
|
||||
if stack.is_deleting or stack.is_delete_failed:
|
||||
# stack is being deleted
|
||||
|
||||
# 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:
|
||||
resources_count = len(
|
||||
stack.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.
|
||||
resources_count = total_num_nodes_count
|
||||
|
||||
# TODO(lsmola) same as hack above
|
||||
total_num_nodes_count = max(
|
||||
resources_count, total_num_nodes_count)
|
||||
|
||||
context['progress'] = max(
|
||||
5, 100 * (total_num_nodes_count - resources_count))
|
||||
else:
|
||||
# stack is active
|
||||
roles = [_get_role_data(stack, role)
|
||||
for role in stack.plan.role_list]
|
||||
total = sum(d['total_node_count'] for d in roles)
|
||||
|
||||
context['roles'] = roles
|
||||
context['progress'] = 100 * sum(d.get('deployed_node_count', 0)
|
||||
for d in roles) // (total or 1)
|
||||
context['dashboard_urls'] = stack.dashboard_urls
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class UndeployConfirmationView(horizon.forms.ModalFormView, StackMixin):
|
||||
form_class = forms.UndeployOvercloud
|
||||
template_name = 'infrastructure/overview/undeploy_confirmation.html'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(INDEX_URL)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UndeployConfirmationView,
|
||||
self).get_context_data(**kwargs)
|
||||
context['stack_id'] = self.get_stack().id
|
||||
return context
|
||||
|
||||
def get_initial(self, **kwargs):
|
||||
initial = super(UndeployConfirmationView, self).get_initial(**kwargs)
|
||||
initial['stack_id'] = self.get_stack().id
|
||||
return initial
|
@ -1,66 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import django.forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import horizon.exceptions
|
||||
import horizon.forms
|
||||
import horizon.messages
|
||||
from openstack_dashboard import api as horizon_api
|
||||
|
||||
from tuskar_ui import api
|
||||
|
||||
|
||||
def get_flavor_choices(request):
|
||||
empty = [('', '----')]
|
||||
try:
|
||||
flavors = horizon_api.nova.flavor_list(request, None)
|
||||
except Exception:
|
||||
horizon.exceptions.handle(request,
|
||||
_('Unable to retrieve flavor list.'))
|
||||
return empty
|
||||
return empty + [(flavor.id, flavor.name) for flavor in flavors]
|
||||
|
||||
|
||||
class OvercloudRoleForm(horizon.forms.SelfHandlingForm):
|
||||
id = django.forms.IntegerField(
|
||||
widget=django.forms.HiddenInput)
|
||||
name = django.forms.CharField(
|
||||
label=_("Name"), required=False,
|
||||
widget=django.forms.TextInput(
|
||||
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
|
||||
description = django.forms.CharField(
|
||||
label=_("Description"), required=False,
|
||||
widget=django.forms.Textarea(
|
||||
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
|
||||
image_name = django.forms.CharField(
|
||||
label=_("Image"), required=False,
|
||||
widget=django.forms.TextInput(
|
||||
attrs={'readonly': 'readonly', 'disabled': 'disabled'}))
|
||||
flavor_id = django.forms.ChoiceField(
|
||||
label=_("Flavor"), required=False, choices=())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OvercloudRoleForm, self).__init__(*args, **kwargs)
|
||||
self.fields['flavor_id'].choices = get_flavor_choices(self.request)
|
||||
|
||||
def handle(self, request, context):
|
||||
try:
|
||||
role = api.tuskar.OvercloudRole.get(request, context['id'])
|
||||
role.update(request, flavor_id=context['flavor_id'])
|
||||
except Exception:
|
||||
horizon.exceptions.handle(request,
|
||||
_('Unable to update the role.'))
|
||||
return False
|
||||
return True
|
@ -1,35 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
|
||||
class ConfigurationTable(tables.DataTable):
|
||||
|
||||
key = tables.Column(lambda parameter: parameter[0],
|
||||
verbose_name=_("Attribute Name"))
|
||||
value = tables.Column(lambda parameter: parameter[1],
|
||||
verbose_name=_("Attribute Value"))
|
||||
|
||||
class Meta:
|
||||
name = "configuration"
|
||||
verbose_name = _("Configuration")
|
||||
multi_select = False
|
||||
table_actions = ()
|
||||
row_actions = ()
|
||||
|
||||
def get_object_id(self, datum):
|
||||
return datum[0]
|
@ -1,9 +0,0 @@
|
||||
{% load i18n %}
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
<p>{{ step.get_help_text }}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% include 'horizon/common/_form_fields.html' with form=form %}
|
||||
</div>
|
||||
</div>
|
@ -1,16 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load url from future%}
|
||||
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
<div class="widget muted">{{ step.get_help_text }}</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<h2>{% trans "Roles" %}</h2>
|
||||
<div class="widget">
|
||||
<td class="actions">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,5 +0,0 @@
|
||||
{% load i18n %}
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
|
||||
{% include 'infrastructure/overcloud/node_counts.html' with form=form show_change=True %}
|
||||
|
@ -1,65 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# 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
|
||||
from openstack_dashboard.test.test_data import utils
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.test import helpers as test
|
||||
from tuskar_ui.test.test_data import flavor_data
|
||||
from tuskar_ui.test.test_data import heat_data
|
||||
from tuskar_ui.test.test_data import node_data
|
||||
from tuskar_ui.test.test_data import tuskar_data
|
||||
|
||||
|
||||
INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:plans:index')
|
||||
CREATE_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:plans:create')
|
||||
OVERCLOUD_INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:index')
|
||||
|
||||
TEST_DATA = utils.TestDataContainer()
|
||||
flavor_data.data(TEST_DATA)
|
||||
node_data.data(TEST_DATA)
|
||||
heat_data.data(TEST_DATA)
|
||||
tuskar_data.data(TEST_DATA)
|
||||
|
||||
|
||||
class OvercloudTests(test.BaseAdminViewTests):
|
||||
|
||||
def test_index_no_plan_get(self):
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
|
||||
return_value=None),
|
||||
):
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, CREATE_URL)
|
||||
|
||||
def test_index_with_plan_get(self):
|
||||
plan = api.tuskar.OvercloudPlan(TEST_DATA.tuskarclient_plans.first())
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.tuskar.OvercloudPlan.get_the_plan',
|
||||
return_value=plan),
|
||||
):
|
||||
res = self.client.get(INDEX_URL)
|
||||
request = api.tuskar.OvercloudPlan.get_the_plan. \
|
||||
call_args_list[0][0][0]
|
||||
self.assertListEqual(
|
||||
api.tuskar.OvercloudPlan.get_the_plan.call_args_list,
|
||||
[call(request)])
|
||||
self.assertRedirectsNoFollow(res, OVERCLOUD_INDEX_URL)
|
@ -1,91 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.generic import base as base_views
|
||||
|
||||
from horizon.utils import memoized
|
||||
import horizon.workflows
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.plans.workflows import create
|
||||
from tuskar_ui.infrastructure.plans.workflows import scale
|
||||
|
||||
|
||||
INDEX_URL = 'horizon:infrastructure:plans:index'
|
||||
CREATE_URL = 'horizon:infrastructure:plans:create'
|
||||
OVERCLOUD_INDEX_URL = 'horizon:infrastructure:overcloud:index'
|
||||
|
||||
|
||||
class OvercloudPlanMixin(object):
|
||||
@memoized.memoized
|
||||
def get_plan(self, redirect=None):
|
||||
if redirect is None:
|
||||
redirect = reverse(INDEX_URL)
|
||||
plan_id = self.kwargs['plan_id']
|
||||
plan = api.tuskar.OvercloudPlan.get(self.request, plan_id,
|
||||
_error_redirect=redirect)
|
||||
return plan
|
||||
|
||||
|
||||
class OvercloudRoleMixin(object):
|
||||
@memoized.memoized
|
||||
def get_role(self, redirect=None):
|
||||
role_id = self.kwargs['role_id']
|
||||
role = api.tuskar.OvercloudRole.get(self.request, role_id,
|
||||
_error_redirect=redirect)
|
||||
return role
|
||||
|
||||
|
||||
class IndexView(base_views.RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self):
|
||||
plan = api.tuskar.OvercloudPlan.get_the_plan(self.request)
|
||||
|
||||
if plan is None:
|
||||
redirect = reverse(CREATE_URL)
|
||||
else:
|
||||
redirect = reverse(OVERCLOUD_INDEX_URL)
|
||||
|
||||
return redirect
|
||||
|
||||
|
||||
class CreateView(horizon.workflows.WorkflowView):
|
||||
workflow_class = create.Workflow
|
||||
template_name = 'infrastructure/_fullscreen_workflow_base.html'
|
||||
|
||||
|
||||
class Scale(horizon.workflows.WorkflowView, OvercloudPlanMixin):
|
||||
workflow_class = scale.Workflow
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(Scale, self).get_context_data(**kwargs)
|
||||
context['plan_id'] = self.kwargs['plan_id']
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
plan = self.get_plan()
|
||||
overcloud_roles = dict((overcloud_role.id, overcloud_role)
|
||||
for overcloud_role in
|
||||
api.tuskar.OvercloudRole.list(self.request))
|
||||
|
||||
role_counts = dict((
|
||||
(count['overcloud_role_id'],
|
||||
overcloud_roles[count['overcloud_role_id']].flavor_id),
|
||||
count['num_nodes'],
|
||||
) for count in plan.counts)
|
||||
return {
|
||||
'plan_id': plan.id,
|
||||
'role_counts': role_counts,
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
|
||||
import django.forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import horizon.workflows
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.plans.workflows import create_configuration
|
||||
from tuskar_ui.infrastructure.plans.workflows import create_overview
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Workflow(horizon.workflows.Workflow):
|
||||
slug = 'create_plan'
|
||||
name = _("My OpenStack Deployment Plan")
|
||||
default_steps = (
|
||||
create_overview.Step,
|
||||
create_configuration.Step,
|
||||
)
|
||||
finalize_button_name = _("Deploy")
|
||||
success_message = _("OpenStack deployment launched")
|
||||
success_url = 'horizon:infrastructure:overcloud:index'
|
||||
|
||||
def handle(self, request, context):
|
||||
try:
|
||||
api.tuskar.OvercloudPlan.create(
|
||||
self.request, 'overcloud', 'overcloud')
|
||||
except Exception as e:
|
||||
# Showing error in both workflow tabs, because from the exception
|
||||
# type we can't recognize where it should show
|
||||
msg = unicode(e)
|
||||
self.add_error_to_step(msg, 'create_overview')
|
||||
self.add_error_to_step(msg, 'create_configuration')
|
||||
LOG.exception('Error creating overcloud plan')
|
||||
raise django.forms.ValidationError(msg)
|
||||
return True
|
@ -1,89 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import django.forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import horizon.workflows
|
||||
|
||||
from openstack_dashboard.api import neutron
|
||||
from tuskar_ui.utils import utils
|
||||
|
||||
|
||||
def make_field(name, Type, NoEcho, Default, Description, AllowedValues=None,
|
||||
**kwargs):
|
||||
"""Create a form field using the parameters from a Heat template."""
|
||||
|
||||
label = utils.de_camel_case(name)
|
||||
Widget = django.forms.TextInput
|
||||
attrs = {}
|
||||
widget_kwargs = {}
|
||||
if Default == 'unset':
|
||||
attrs['placeholder'] = _("auto-generate")
|
||||
if Type == 'Json':
|
||||
# TODO(lsmola) this should eventually be a textarea
|
||||
Field = django.forms.CharField
|
||||
else:
|
||||
# TODO(lsmola) we should use Horizon code for generating of the form.
|
||||
# There it should have list of all supported types according to Heat
|
||||
# specification.
|
||||
Field = django.forms.CharField
|
||||
|
||||
if NoEcho == 'true':
|
||||
Widget = django.forms.PasswordInput
|
||||
widget_kwargs['render_value'] = True
|
||||
if AllowedValues is not None:
|
||||
return django.forms.ChoiceField(initial=Default, choices=[
|
||||
(value, value) for value in AllowedValues
|
||||
], help_text=Description, required=False, label=label)
|
||||
return Field(widget=Widget(attrs=attrs, **widget_kwargs), initial=Default,
|
||||
help_text=Description, required=False, label=label)
|
||||
|
||||
|
||||
class Action(horizon.workflows.Action):
|
||||
class Meta:
|
||||
slug = 'deployed_configuration'
|
||||
name = _("Configuration")
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(Action, self).__init__(request, *args, **kwargs)
|
||||
params = []
|
||||
params.sort()
|
||||
|
||||
for name, data in params:
|
||||
# workaround for this parameter, which needs a preset taken from
|
||||
# neutron
|
||||
if name == 'NeutronControlPlaneID':
|
||||
networks = neutron.network_list(request)
|
||||
for network in networks:
|
||||
if network.name == 'ctlplane':
|
||||
data['Default'] = network.id
|
||||
break
|
||||
|
||||
self.fields[name] = make_field(name, **data)
|
||||
|
||||
def clean(self):
|
||||
# this is a workaround for a single parameter
|
||||
if 'GlanceLogFile' in self.cleaned_data:
|
||||
if not self.cleaned_data['GlanceLogFile']:
|
||||
self.cleaned_data['GlanceLogFile'] = u"''"
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class Step(horizon.workflows.Step):
|
||||
action_class = Action
|
||||
contributes = ('configuration',)
|
||||
template_name = 'infrastructure/plans/create_configuration.html'
|
||||
|
||||
def contribute(self, data, context):
|
||||
context['configuration'] = data
|
||||
return context
|
@ -1,53 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import forms
|
||||
import horizon.workflows
|
||||
|
||||
from tuskar_ui import api
|
||||
|
||||
|
||||
class Action(horizon.workflows.Action):
|
||||
|
||||
role_ids = forms.MultipleChoiceField(
|
||||
label=_("Roles"),
|
||||
required=True,
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
help_text=_("Select roles for this plan."))
|
||||
|
||||
class Meta:
|
||||
slug = 'create_overview'
|
||||
name = _("Overview")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Action, self).__init__(*args, **kwargs)
|
||||
|
||||
role_ids_choices = []
|
||||
roles = api.tuskar.OvercloudRole.list(self.request)
|
||||
for r in roles:
|
||||
role_ids_choices.append((r.id, r.name))
|
||||
self.fields['role_ids'].choices = sorted(
|
||||
role_ids_choices)
|
||||
|
||||
|
||||
class Step(horizon.workflows.Step):
|
||||
action_class = Action
|
||||
contributes = ('role_ids',)
|
||||
template_name = 'infrastructure/plans/create_overview.html'
|
||||
help_text = _("Nothing deployed yet. Design your first deployment.")
|
||||
|
||||
def contribute(self, data, context):
|
||||
context = super(Step, self).contribute(data, context)
|
||||
return context
|
@ -1,47 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import exceptions
|
||||
import horizon.workflows
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.plans.workflows import scale_node_counts
|
||||
|
||||
|
||||
class Workflow(horizon.workflows.Workflow):
|
||||
slug = 'scale_overcloud'
|
||||
name = _("Scale Deployment")
|
||||
default_steps = (
|
||||
scale_node_counts.Step,
|
||||
)
|
||||
finalize_button_name = _("Apply Changes")
|
||||
|
||||
def handle(self, request, context):
|
||||
plan_id = context['plan_id']
|
||||
try:
|
||||
# TODO(lsmola) when updates are fixed in Heat, figure out whether
|
||||
# we need to send also parameters, right now we send {}
|
||||
api.tuskar.OvercloudPlan.update(request, plan_id,
|
||||
context['role_counts'], {})
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to update deployment.'))
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_success_url(self):
|
||||
plan_id = self.context.get('plan_id', 1)
|
||||
return reverse('horizon:infrastructure:overcloud:detail',
|
||||
args=(plan_id,))
|
@ -1,35 +0,0 @@
|
||||
# -*- coding: utf8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from tuskar_ui.infrastructure.plans.workflows import create_overview
|
||||
|
||||
|
||||
class Action(create_overview.Action):
|
||||
class Meta:
|
||||
slug = 'scale_node_counts'
|
||||
name = _("Node Counts")
|
||||
|
||||
|
||||
class Step(create_overview.Step):
|
||||
action_class = Action
|
||||
contributes = ('role_counts', 'plan_id')
|
||||
template_name = 'infrastructure/plans/scale_node_counts.html'
|
||||
|
||||
def prepare_action_context(self, request, context):
|
||||
for (role_id, flavor_id), count in context['role_counts'].items():
|
||||
name = 'count__%s__%s' % (role_id, flavor_id)
|
||||
context[name] = count
|
||||
return context
|
Loading…
x
Reference in New Issue
Block a user