Add a deployment scaling dialog
Creates a dialog for scaling the deployment. We are still missing the API calls to initiate the actual deployment update. I also still need to make the "Change" column work with JS. Change-Id: I9d843916ff92017467a9accc44ae7a029179ded9 Implements: blueprint tripleo-deployment-scaling-dialog
This commit is contained in:
parent
932d8e2556
commit
9e28b6b5fd
@ -86,7 +86,7 @@ def image_get(request, image_id):
|
||||
|
||||
|
||||
class Overcloud(base.APIResourceWrapper):
|
||||
_attrs = ('id', 'stack_id', 'name', 'description')
|
||||
_attrs = ('id', 'stack_id', 'name', 'description', 'counts', 'attributes')
|
||||
|
||||
def __init__(self, apiresource, request=None):
|
||||
super(Overcloud, self).__init__(apiresource)
|
||||
|
@ -17,7 +17,8 @@
|
||||
<i class="icon-fire icon-white"></i>
|
||||
{% trans "Undeploy" %}
|
||||
</a>
|
||||
<a href="#" class="btn
|
||||
<a href="{% url 'horizon:infrastructure:overcloud:scale' overcloud_id %}"
|
||||
class="btn ajax-modal
|
||||
{% if not overcloud.is_deployed %}disabled{% endif %}">
|
||||
<i class="icon-resize-full"></i>
|
||||
{% trans "Scale deployment" %}
|
||||
|
@ -0,0 +1,32 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% include 'horizon/common/_form_errors.html' with form=form %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Node profiles" %}</th>
|
||||
<th>{% trans "Nodes" %}</th>
|
||||
{% if show_change %}
|
||||
<th>{% trans "Change" %}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for role_id, label, fields in form.roles_fieldset %}
|
||||
<tbody>
|
||||
{% for field in fields %}
|
||||
<tr>
|
||||
{% if forloop.first %}
|
||||
<td rowspan="{{ fields|length }}">
|
||||
<i class="icon-pencil"></i>
|
||||
{{ label }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ field.label }}</td>
|
||||
<td>{{ field }}</td>
|
||||
{% if show_change %}
|
||||
<td class="changed"><span class="muted">no change</span></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
|
||||
{% include 'infrastructure/overcloud/node_counts.html' with form=form show_change=True %}
|
||||
|
@ -9,35 +9,12 @@
|
||||
</div>
|
||||
<h2>{% trans "Roles" %}</h2>
|
||||
<div class="widget">
|
||||
{% include 'horizon/common/_form_errors.html' with form=form %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Node profiles" %}</th>
|
||||
<th>{% trans "Nodes" %}</th>
|
||||
</tr>
|
||||
{% for role, label, fields in form.roles_fieldset %}
|
||||
<tbody>
|
||||
{% for field in fields %}
|
||||
<tr>
|
||||
{% if forloop.first %}
|
||||
<td rowspan="{{ fields|length }}">
|
||||
<i class="icon-pencil"></i>
|
||||
{{ label }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ field.label }}</td>
|
||||
<td>{{ field }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% include 'infrastructure/overcloud/node_counts.html' with form=form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<div class="widget">
|
||||
<h2>Configuration</h2>
|
||||
<div class="widget">
|
||||
<p>{% trans "Configuration options will be auto-detected." %}</p>
|
||||
<p><a href="#undeployed_overcloud__deployed_configuration"
|
||||
data-toggle="tab">{% trans "See and change defaults." %}</a></p>
|
||||
|
@ -194,3 +194,72 @@ class OvercloudTests(test.BaseAdminViewTests):
|
||||
}):
|
||||
res = self.client.post(DELETE_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_scale_get(self):
|
||||
oc = None
|
||||
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.OvercloudRole', **{
|
||||
'spec_set': ['list'],
|
||||
'list.return_value': roles,
|
||||
}),
|
||||
patch('tuskar_ui.api.Overcloud', **{
|
||||
'spec_set': ['get', 'id', 'counts'],
|
||||
'get.side_effect': lambda *args: oc,
|
||||
'id': 1,
|
||||
'counts': [
|
||||
{"overcloud_role_id": role.id, "num_nodes": 0}
|
||||
for role in roles
|
||||
],
|
||||
}),
|
||||
) as (OvercloudRole, Overcloud):
|
||||
oc = Overcloud
|
||||
url = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:scale', args=(oc.id,))
|
||||
res = self.client.get(url)
|
||||
self.assertTemplateUsed(
|
||||
res, 'infrastructure/overcloud/scale_node_counts.html')
|
||||
|
||||
def test_scale_post(self):
|
||||
oc = None
|
||||
roles = TEST_DATA.tuskarclient_overcloud_roles.list()
|
||||
data = {
|
||||
'overcloud_id': '1',
|
||||
'count__1__default': '1',
|
||||
'count__2__default': '0',
|
||||
'count__3__default': '0',
|
||||
'count__4__default': '0',
|
||||
}
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.OvercloudRole', **{
|
||||
'spec_set': ['list'],
|
||||
'list.return_value': roles,
|
||||
}),
|
||||
patch('tuskar_ui.api.Overcloud', **{
|
||||
'spec_set': ['update', 'id', 'get', 'counts'],
|
||||
'get.side_effect': lambda *args: oc,
|
||||
'update.side_effect': lambda *args: oc,
|
||||
'id': 1,
|
||||
'counts': [
|
||||
{"overcloud_role_id": role.id, "num_nodes": 0}
|
||||
for role in roles
|
||||
],
|
||||
}),
|
||||
) as (OvercloudRole, Overcloud):
|
||||
oc = Overcloud
|
||||
url = urlresolvers.reverse(
|
||||
'horizon:infrastructure:overcloud:scale', args=(oc.id,))
|
||||
res = self.client.post(url, data)
|
||||
# TODO(rdopieralski) Check it when it's actually called.
|
||||
#request = Overcloud.update.call_args_list[0][0][0]
|
||||
#self.assertListEqual(
|
||||
# Overcloud.update.call_args_list,
|
||||
# [
|
||||
# call(request, {
|
||||
# ('1', 'default'): 1,
|
||||
# ('2', 'default'): 0,
|
||||
# ('3', 'default'): 0,
|
||||
# ('4', 'default'): 0,
|
||||
# }),
|
||||
# ])
|
||||
self.assertRedirectsNoFollow(res, DETAIL_URL)
|
||||
|
@ -24,6 +24,8 @@ urlpatterns = defaults.patterns(
|
||||
name='create'),
|
||||
defaults.url(r'^(?P<overcloud_id>[^/]+)/$',
|
||||
views.DetailView.as_view(), name='detail'),
|
||||
defaults.url(r'^(?P<overcloud_id>[^/]+)/scale$',
|
||||
views.Scale.as_view(), name='scale'),
|
||||
defaults.url(r'^(?P<overcloud_id>[^/]+)/role/'
|
||||
'(?P<role_id>[^/]+)$',
|
||||
views.OvercloudRoleView.as_view(),
|
||||
|
@ -26,9 +26,28 @@ from tuskar_ui import api
|
||||
from tuskar_ui.infrastructure.overcloud import forms
|
||||
from tuskar_ui.infrastructure.overcloud import tables
|
||||
from tuskar_ui.infrastructure.overcloud import tabs
|
||||
from tuskar_ui.infrastructure.overcloud.workflows import scale
|
||||
from tuskar_ui.infrastructure.overcloud.workflows import undeployed
|
||||
|
||||
|
||||
INDEX_URL = 'horizon:infrastructure:overcloud:index'
|
||||
|
||||
|
||||
class OvercloudMixin(object):
|
||||
@memoized.memoized
|
||||
def get_overcloud(self, redirect=None):
|
||||
if redirect is None:
|
||||
redirect = reverse(INDEX_URL)
|
||||
overcloud_id = self.kwargs['overcloud_id']
|
||||
try:
|
||||
overcloud = api.Overcloud.get(self.request, overcloud_id)
|
||||
except Exception:
|
||||
msg = _("Unable to retrieve deployment.")
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
|
||||
return overcloud
|
||||
|
||||
|
||||
class IndexView(base_views.RedirectView):
|
||||
permanent = False
|
||||
|
||||
@ -74,7 +93,7 @@ class DetailView(horizon_tabs.TabView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
context['overcloud'] = self.get_data()
|
||||
context['overcloud_id'] = self.kwargs['overcloud_id']
|
||||
return context
|
||||
|
||||
|
||||
@ -97,6 +116,21 @@ class UndeployConfirmationView(horizon.forms.ModalFormView):
|
||||
return initial
|
||||
|
||||
|
||||
class Scale(horizon.workflows.WorkflowView, OvercloudMixin):
|
||||
workflow_class = scale.Workflow
|
||||
|
||||
def get_initial(self):
|
||||
overcloud = self.get_overcloud()
|
||||
role_counts = dict((
|
||||
(count['overcloud_role_id'], 'default'),
|
||||
count['num_nodes'],
|
||||
) for count in overcloud.counts)
|
||||
return {
|
||||
'overcloud_id': overcloud.id,
|
||||
'role_counts': role_counts,
|
||||
}
|
||||
|
||||
|
||||
class OvercloudRoleView(horizon_tables.DataTableView):
|
||||
table_class = tables.OvercloudRoleNodeTable
|
||||
template_name = 'infrastructure/overcloud/overcloud_role.html'
|
||||
|
48
tuskar_ui/infrastructure/overcloud/workflows/scale.py
Normal file
48
tuskar_ui/infrastructure/overcloud/workflows/scale.py
Normal file
@ -0,0 +1,48 @@
|
||||
# -*- 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.overcloud.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):
|
||||
success = True
|
||||
overcloud_id = self.context['overcloud_id']
|
||||
try:
|
||||
# TODO(rdopieralski) Actually update it when possible.
|
||||
overcloud = api.Overcloud.get(request, overcloud_id) # noqa
|
||||
# overcloud.update(self.request, context['role_counts'])
|
||||
pass
|
||||
except Exception:
|
||||
success = False
|
||||
exceptions.handle(request, _('Unable to update deployment.'))
|
||||
return success
|
||||
|
||||
def get_success_url(self):
|
||||
overcloud_id = self.context.get('overcloud_id')
|
||||
return reverse('horizon:infrastructure:overcloud:detail',
|
||||
args=(overcloud_id,))
|
@ -0,0 +1,39 @@
|
||||
# -*- 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 tuskar_ui.infrastructure.overcloud.workflows import undeployed_overview
|
||||
|
||||
|
||||
class Action(undeployed_overview.Action):
|
||||
overcloud_id = django.forms.IntegerField(widget=django.forms.HiddenInput)
|
||||
|
||||
class Meta:
|
||||
slug = 'scale_node_counts'
|
||||
name = _("Node Counts")
|
||||
|
||||
|
||||
class Step(horizon.workflows.Step):
|
||||
action_class = Action
|
||||
contributes = ('role_counts', 'overcloud_id')
|
||||
template_name = 'infrastructure/overcloud/scale_node_counts.html'
|
||||
|
||||
def prepare_action_context(self, request, context):
|
||||
for (role_id, profile_id), count in context['role_counts'].items():
|
||||
name = 'count__%s__%s' % (role_id, profile_id)
|
||||
context[name] = count
|
||||
return context
|
Loading…
x
Reference in New Issue
Block a user