From 9e28b6b5fd79224c74e2da633ce8b8f25ad5e0cb Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 14 Feb 2014 04:26:19 -0500 Subject: [PATCH] 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 --- tuskar_ui/api.py | 2 +- .../overcloud/templates/overcloud/detail.html | 3 +- .../templates/overcloud/node_counts.html | 32 +++++++++ .../overcloud/scale_node_counts.html | 5 ++ .../overcloud/undeployed_overview.html | 27 +------- tuskar_ui/infrastructure/overcloud/tests.py | 69 +++++++++++++++++++ tuskar_ui/infrastructure/overcloud/urls.py | 2 + tuskar_ui/infrastructure/overcloud/views.py | 36 +++++++++- .../overcloud/workflows/scale.py | 48 +++++++++++++ .../overcloud/workflows/scale_node_counts.py | 39 +++++++++++ 10 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html create mode 100644 tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html create mode 100644 tuskar_ui/infrastructure/overcloud/workflows/scale.py create mode 100644 tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py diff --git a/tuskar_ui/api.py b/tuskar_ui/api.py index 549e71415..7de3419dc 100644 --- a/tuskar_ui/api.py +++ b/tuskar_ui/api.py @@ -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) diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html index 64606ef5e..c67eb6d49 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html @@ -17,7 +17,8 @@ {% trans "Undeploy" %} - {% trans "Scale deployment" %} diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html new file mode 100644 index 000000000..aa18e55da --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html @@ -0,0 +1,32 @@ +{% load i18n %} + +{% include 'horizon/common/_form_errors.html' with form=form %} + + + + + + {% if show_change %} + + {% endif %} + + {% for role_id, label, fields in form.roles_fieldset %} + + {% for field in fields %} + + {% if forloop.first %} + + {% endif %} + + + {% if show_change %} + + {% endif %} + + {% endfor %} + + {% endfor %} +
{% trans "Name" %}{% trans "Node profiles" %}{% trans "Nodes" %}{% trans "Change" %}
+ + {{ label }} + {{ field.label }}{{ field }}no change
diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html new file mode 100644 index 000000000..56031a103 --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html @@ -0,0 +1,5 @@ +{% load i18n %} + + +{% include 'infrastructure/overcloud/node_counts.html' with form=form show_change=True %} + diff --git a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html b/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html index 506a8a170..0ae385242 100644 --- a/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html +++ b/tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html @@ -9,35 +9,12 @@

{% trans "Roles" %}

- {% include 'horizon/common/_form_errors.html' with form=form %} - - - - - - - {% for role, label, fields in form.roles_fieldset %} - - {% for field in fields %} - - {% if forloop.first %} - - {% endif %} - - - - {% endfor %} - - {% endfor %} -
{% trans "Name" %}{% trans "Node profiles" %}{% trans "Nodes" %}
- - {{ label }} - {{ field.label }}{{ field }}
+ {% include 'infrastructure/overcloud/node_counts.html' with form=form %}
+

Configuration

-

Configuration

{% trans "Configuration options will be auto-detected." %}

{% trans "See and change defaults." %}

diff --git a/tuskar_ui/infrastructure/overcloud/tests.py b/tuskar_ui/infrastructure/overcloud/tests.py index 9b1595f5f..9fa4a6108 100644 --- a/tuskar_ui/infrastructure/overcloud/tests.py +++ b/tuskar_ui/infrastructure/overcloud/tests.py @@ -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) diff --git a/tuskar_ui/infrastructure/overcloud/urls.py b/tuskar_ui/infrastructure/overcloud/urls.py index 209a54a2f..7f6107e7c 100644 --- a/tuskar_ui/infrastructure/overcloud/urls.py +++ b/tuskar_ui/infrastructure/overcloud/urls.py @@ -24,6 +24,8 @@ urlpatterns = defaults.patterns( name='create'), defaults.url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), + defaults.url(r'^(?P[^/]+)/scale$', + views.Scale.as_view(), name='scale'), defaults.url(r'^(?P[^/]+)/role/' '(?P[^/]+)$', views.OvercloudRoleView.as_view(), diff --git a/tuskar_ui/infrastructure/overcloud/views.py b/tuskar_ui/infrastructure/overcloud/views.py index fc0f49693..64726127e 100644 --- a/tuskar_ui/infrastructure/overcloud/views.py +++ b/tuskar_ui/infrastructure/overcloud/views.py @@ -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' diff --git a/tuskar_ui/infrastructure/overcloud/workflows/scale.py b/tuskar_ui/infrastructure/overcloud/workflows/scale.py new file mode 100644 index 000000000..b94c0850d --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/workflows/scale.py @@ -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,)) diff --git a/tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py b/tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py new file mode 100644 index 000000000..e5a723d98 --- /dev/null +++ b/tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py @@ -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