Merge "Add a deployment scaling dialog"
This commit is contained in:
		| @@ -86,7 +86,7 @@ def image_get(request, image_id): | |||||||
|  |  | ||||||
|  |  | ||||||
| class Overcloud(base.APIResourceWrapper): | class Overcloud(base.APIResourceWrapper): | ||||||
|     _attrs = ('id', 'stack_id', 'name', 'description') |     _attrs = ('id', 'stack_id', 'name', 'description', 'counts', 'attributes') | ||||||
|  |  | ||||||
|     def __init__(self, apiresource, request=None): |     def __init__(self, apiresource, request=None): | ||||||
|         super(Overcloud, self).__init__(apiresource) |         super(Overcloud, self).__init__(apiresource) | ||||||
|   | |||||||
| @@ -17,7 +17,8 @@ | |||||||
|             <i class="icon-fire icon-white"></i> |             <i class="icon-fire icon-white"></i> | ||||||
|             {% trans "Undeploy" %} |             {% trans "Undeploy" %} | ||||||
|         </a> |         </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 %}"> |             {% if not overcloud.is_deployed %}disabled{% endif %}"> | ||||||
|             <i class="icon-resize-full"></i> |             <i class="icon-resize-full"></i> | ||||||
|             {% trans "Scale deployment" %} |             {% 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> |     </div> | ||||||
|     <h2>{% trans "Roles" %}</h2> |     <h2>{% trans "Roles" %}</h2> | ||||||
|     <div class="widget"> |     <div class="widget"> | ||||||
|       {% include 'horizon/common/_form_errors.html' with form=form %} |     {% include 'infrastructure/overcloud/node_counts.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> |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="span4"> |   <div class="span4"> | ||||||
|  |     <h2>Configuration</h2> | ||||||
|     <div class="widget"> |     <div class="widget"> | ||||||
|       <h2>Configuration</h2> |  | ||||||
|       <p>{% trans "Configuration options will be auto-detected." %}</p> |       <p>{% trans "Configuration options will be auto-detected." %}</p> | ||||||
|       <p><a href="#undeployed_overcloud__deployed_configuration" |       <p><a href="#undeployed_overcloud__deployed_configuration" | ||||||
|             data-toggle="tab">{% trans "See and change defaults." %}</a></p> |             data-toggle="tab">{% trans "See and change defaults." %}</a></p> | ||||||
|   | |||||||
| @@ -202,3 +202,72 @@ class OvercloudTests(test.BaseAdminViewTests): | |||||||
|         }): |         }): | ||||||
|             res = self.client.post(DELETE_URL) |             res = self.client.post(DELETE_URL) | ||||||
|         self.assertRedirectsNoFollow(res, INDEX_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'), |                  name='create'), | ||||||
|     defaults.url(r'^(?P<overcloud_id>[^/]+)/$', |     defaults.url(r'^(?P<overcloud_id>[^/]+)/$', | ||||||
|                  views.DetailView.as_view(), name='detail'), |                  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/' |     defaults.url(r'^(?P<overcloud_id>[^/]+)/role/' | ||||||
|                  '(?P<role_id>[^/]+)$', |                  '(?P<role_id>[^/]+)$', | ||||||
|                  views.OvercloudRoleView.as_view(), |                  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 forms | ||||||
| from tuskar_ui.infrastructure.overcloud import tables | from tuskar_ui.infrastructure.overcloud import tables | ||||||
| from tuskar_ui.infrastructure.overcloud import tabs | from tuskar_ui.infrastructure.overcloud import tabs | ||||||
|  | from tuskar_ui.infrastructure.overcloud.workflows import scale | ||||||
| from tuskar_ui.infrastructure.overcloud.workflows import undeployed | 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): | class IndexView(base_views.RedirectView): | ||||||
|     permanent = False |     permanent = False | ||||||
|  |  | ||||||
| @@ -74,7 +93,7 @@ class DetailView(horizon_tabs.TabView): | |||||||
|  |  | ||||||
|     def get_context_data(self, **kwargs): |     def get_context_data(self, **kwargs): | ||||||
|         context = super(DetailView, self).get_context_data(**kwargs) |         context = super(DetailView, self).get_context_data(**kwargs) | ||||||
|         context['overcloud'] = self.get_data() |         context['overcloud_id'] = self.kwargs['overcloud_id'] | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -97,6 +116,21 @@ class UndeployConfirmationView(horizon.forms.ModalFormView): | |||||||
|         return initial |         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): | class OvercloudRoleView(horizon_tables.DataTableView): | ||||||
|     table_class = tables.OvercloudRoleNodeTable |     table_class = tables.OvercloudRoleNodeTable | ||||||
|     template_name = 'infrastructure/overcloud/overcloud_role.html' |     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 | ||||||
		Reference in New Issue
	
	Block a user
	 Jenkins
					Jenkins