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:
Radomir Dopieralski 2014-02-14 04:26:19 -05:00
parent 932d8e2556
commit 9e28b6b5fd
10 changed files with 235 additions and 28 deletions

View File

@ -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)

View File

@ -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" %}

View File

@ -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>

View File

@ -0,0 +1,5 @@
{% load i18n %}
<noscript><h3>{{ step }}</h3></noscript>
{% include 'infrastructure/overcloud/node_counts.html' with form=form show_change=True %}

View File

@ -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">
<h2>Configuration</h2>
<div class="widget">
<h2>Configuration</h2>
<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>

View File

@ -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)

View File

@ -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(),

View File

@ -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'

View 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,))

View File

@ -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