diff --git a/openstack_dashboard/api/heat.py b/openstack_dashboard/api/heat.py
index a8ab195a..8411c400 100644
--- a/openstack_dashboard/api/heat.py
+++ b/openstack_dashboard/api/heat.py
@@ -64,10 +64,20 @@ def stack_get(request, stack_id):
return heatclient(request).stacks.get(stack_id)
+def template_get(request, stack_id):
+ return heatclient(request).stacks.template(stack_id)
+
+
def stack_create(request, password=None, **kwargs):
return heatclient(request, password).stacks.create(**kwargs)
+def stack_update(request, stack_id, **kwargs):
+ if kwargs.get('password'):
+ kwargs.pop('password')
+ return heatclient(request).stacks.update(stack_id, **kwargs)
+
+
def events_list(request, stack_name):
return heatclient(request).events.list(stack_name)
diff --git a/openstack_dashboard/dashboards/project/stacks/forms.py b/openstack_dashboard/dashboards/project/stacks/forms.py
index c640c75d..4fa0ebac 100644
--- a/openstack_dashboard/dashboards/project/stacks/forms.py
+++ b/openstack_dashboard/dashboards/project/stacks/forms.py
@@ -145,18 +145,39 @@ class TemplateForm(forms.SelfHandlingForm):
return cleaned
- def handle(self, request, data):
+ def create_kwargs(self, data):
kwargs = {'parameters': data['template_validate'],
'template_data': data['template_data'],
'template_url': data['template_url']}
+ if data.get('stack_id'):
+ kwargs['stack_id'] = data['stack_id']
+ return kwargs
+
+ def handle(self, request, data):
+ kwargs = self.create_kwargs(data)
# NOTE (gabriel): This is a bit of a hack, essentially rewriting this
# request so that we can chain it as an input to the next view...
# but hey, it totally works.
request.method = 'GET'
+
return self.next_view.as_view()(request, **kwargs)
-class StackCreateForm(forms.SelfHandlingForm):
+class ChangeTemplateForm(TemplateForm):
+ class Meta:
+ name = _('Edit Template')
+ help_text = _('From here you can select a new template to re-launch '
+ 'a stack.')
+ stack_id = forms.CharField(label=_('Stack ID'),
+ widget=forms.widgets.HiddenInput,
+ required=True)
+ stack_name = forms.CharField(label=_('Stack Name'),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}
+ ))
+
+
+class CreateStackForm(forms.SelfHandlingForm):
param_prefix = '__param_'
@@ -193,11 +214,13 @@ class StackCreateForm(forms.SelfHandlingForm):
def __init__(self, *args, **kwargs):
parameters = kwargs.pop('parameters')
- super(StackCreateForm, self).__init__(*args, **kwargs)
+ # special case: load template data from API, not passed in params
+ if(kwargs.get('validate_me')):
+ parameters = kwargs.pop('validate_me')
+ super(CreateStackForm, self).__init__(*args, **kwargs)
self._build_parameter_fields(parameters)
def _build_parameter_fields(self, template_validate):
-
self.fields['password'] = forms.CharField(
label=_('Password for user "%s"') % self.request.user.username,
help_text=_('This is required for operations to be performed '
@@ -267,3 +290,49 @@ class StackCreateForm(forms.SelfHandlingForm):
except Exception as e:
msg = exception_to_validation_msg(e)
exceptions.handle(request, msg or _('Stack creation failed.'))
+
+
+class EditStackForm(CreateStackForm):
+
+ class Meta:
+ name = _('Update Stack Parameters')
+
+ stack_id = forms.CharField(label=_('Stack ID'),
+ widget=forms.widgets.HiddenInput,
+ required=True)
+ stack_name = forms.CharField(label=_('Stack Name'),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}
+ ))
+
+ @sensitive_variables('password')
+ def handle(self, request, data):
+ prefix_length = len(self.param_prefix)
+ params_list = [(k[prefix_length:], v) for (k, v) in data.iteritems()
+ if k.startswith(self.param_prefix)]
+
+ stack_id = data.get('stack_id')
+ fields = {
+ 'stack_name': data.get('stack_name'),
+ 'timeout_mins': data.get('timeout_mins'),
+ 'disable_rollback': not(data.get('enable_rollback')),
+ 'parameters': dict(params_list),
+ 'password': data.get('password')
+ }
+
+ # if the user went directly to this form, resubmit the existing
+ # template data. otherwise, submit what they had from the first form
+ if data.get('template_data'):
+ fields['template'] = data.get('template_data')
+ elif data.get('template_url'):
+ fields['template_url'] = data.get('template_url')
+ elif data.get('parameters'):
+ fields['template'] = data.get('parameters')
+
+ try:
+ api.heat.stack_update(self.request, stack_id=stack_id, **fields)
+ messages.success(request, _("Stack update started."))
+ return True
+ except Exception as e:
+ msg = exception_to_validation_msg(e)
+ exceptions.handle(request, msg or _('Stack update failed.'))
diff --git a/openstack_dashboard/dashboards/project/stacks/tables.py b/openstack_dashboard/dashboards/project/stacks/tables.py
index 475b5118..7848d523 100644
--- a/openstack_dashboard/dashboards/project/stacks/tables.py
+++ b/openstack_dashboard/dashboards/project/stacks/tables.py
@@ -12,9 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
+from django.core import urlresolvers
from django.http import Http404 # noqa
from django.template.defaultfilters import timesince # noqa
from django.template.defaultfilters import title # noqa
+from django.utils.http import urlencode # noqa
from django.utils.translation import ugettext_lazy as _
from horizon import messages
@@ -34,6 +36,16 @@ class LaunchStack(tables.LinkAction):
classes = ("btn-create", "ajax-modal")
+class ChangeStackTemplate(tables.LinkAction):
+ name = "edit"
+ verbose_name = _("Change Stack Template")
+ url = "horizon:project:stacks:change_template"
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, stack):
+ return urlresolvers.reverse(self.url, args=[stack.id])
+
+
class DeleteStack(tables.BatchAction):
name = "delete"
action_present = _("Delete")
@@ -100,7 +112,8 @@ class StacksTable(tables.DataTable):
status_columns = ["status", ]
row_class = StacksUpdateRow
table_actions = (LaunchStack, DeleteStack,)
- row_actions = (DeleteStack, )
+ row_actions = (DeleteStack,
+ ChangeStackTemplate)
class EventsTable(tables.DataTable):
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html
new file mode 100644
index 00000000..cd22962e
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_change_template.html
@@ -0,0 +1,28 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}select_template{% endblock %}
+{% block form_action %}{% url 'horizon:project:stacks:change_template' stack.id%}{% endblock %}
+{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
+
+{% block modal-header %}{% trans "Select Template" %}{% endblock %}
+{% block modal_id %}select_template_modal{% endblock %}
+
+{% block modal-body %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+
+
{% trans "Description" %}:
+
{% trans "Use one of the available template source options to specify the template to be used in creating this stack." %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
+
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html
new file mode 100644
index 00000000..7405932e
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_update.html
@@ -0,0 +1,26 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}update_stack{% endblock %}
+{% block form_action %}{% url 'horizon:project:stacks:edit_stack' stack.id %}{% endblock %}
+
+{% block modal-header %}{% trans "Update Stack Parameters" %}{% endblock %}
+{% block modal_id %}update_stack_modal{% endblock %}
+
+{% block modal-body %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+
+
{% trans "Description" %}:
+
{% trans "Update a stack with the provided values. Please note that any encrypted parameters, such as passwords, will be reset to default if you don't change them here." %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html
new file mode 100644
index 00000000..0765d840
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/change_template.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Change Template" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Change Template") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'project/stacks/_change_template.html' %}
+{% endblock %}
+
diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html
new file mode 100644
index 00000000..2b21f044
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Stack Parameters" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Update Stack") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'project/stacks/_update.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/stacks/tests.py b/openstack_dashboard/dashboards/project/stacks/tests.py
index bac0e273..5c0711d8 100644
--- a/openstack_dashboard/dashboards/project/stacks/tests.py
+++ b/openstack_dashboard/dashboards/project/stacks/tests.py
@@ -151,7 +151,76 @@ class StackTests(test.TestCase):
"__param_DBPassword": "admin",
"__param_DBRootPassword": "admin",
"__param_DBName": "wordpress",
- 'method': forms.StackCreateForm.__name__}
+ 'method': forms.CreateStackForm.__name__}
+ res = self.client.post(url, form_data)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.heat: ('stack_update', 'stack_get',
+ 'template_get', 'template_validate')})
+ def test_edit_stack_template(self):
+ template = self.stack_templates.first()
+ stack = self.stacks.first()
+
+ # GET to template form
+ api.heat.stack_get(IsA(http.HttpRequest),
+ stack.id).AndReturn(stack)
+ # POST template form, validation
+ api.heat.template_validate(IsA(http.HttpRequest),
+ template=template.data) \
+ .AndReturn(json.loads(template.validate))
+
+ # GET to edit form
+ api.heat.stack_get(IsA(http.HttpRequest),
+ stack.id).AndReturn(stack)
+ api.heat.template_get(IsA(http.HttpRequest),
+ stack.id) \
+ .AndReturn(json.loads(template.validate))
+
+ # POST to edit form
+ api.heat.stack_get(IsA(http.HttpRequest),
+ stack.id).AndReturn(stack)
+
+ fields = {
+ 'stack_name': stack.stack_name,
+ 'disable_rollback': True,
+ 'timeout_mins': 61,
+ 'password': 'password',
+ 'template': IsA(unicode),
+ 'parameters': IsA(dict)
+ }
+ api.heat.stack_update(IsA(http.HttpRequest),
+ stack_id=stack.id,
+ **fields)
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:project:stacks:change_template',
+ args=[stack.id])
+ res = self.client.get(url)
+ self.assertTemplateUsed(res, 'project/stacks/change_template.html')
+
+ form_data = {'template_source': 'raw',
+ 'template_data': template.data,
+ 'method': forms.ChangeTemplateForm.__name__}
+ res = self.client.post(url, form_data)
+
+ url = reverse('horizon:project:stacks:edit_stack',
+ args=[stack.id, ])
+ form_data = {'template_source': 'raw',
+ 'template_data': template.data,
+ 'password': 'password',
+ 'parameters': template.validate,
+ 'stack_name': stack.stack_name,
+ 'stack_id': stack.id,
+ "timeout_mins": 61,
+ "disable_rollback": True,
+ "__param_DBUsername": "admin",
+ "__param_LinuxDistribution": "F17",
+ "__param_InstanceType": "m1.small",
+ "__param_KeyName": "test",
+ "__param_DBPassword": "admin",
+ "__param_DBRootPassword": "admin",
+ "__param_DBName": "wordpress",
+ 'method': forms.EditStackForm.__name__}
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, INDEX_URL)
@@ -177,7 +246,7 @@ class StackTests(test.TestCase):
"__param_DBPassword": "admin",
"__param_DBRootPassword": "admin",
"__param_DBName": "wordpress",
- 'method': forms.StackCreateForm.__name__}
+ 'method': forms.CreateStackForm.__name__}
res = self.client.post(url, form_data)
error = ('Name must start with a letter and may only contain letters, '
diff --git a/openstack_dashboard/dashboards/project/stacks/urls.py b/openstack_dashboard/dashboards/project/stacks/urls.py
index 1ec8ea73..96edd570 100644
--- a/openstack_dashboard/dashboards/project/stacks/urls.py
+++ b/openstack_dashboard/dashboards/project/stacks/urls.py
@@ -26,6 +26,10 @@ urlpatterns = patterns(
url(r'^launch$', views.CreateStackView.as_view(), name='launch'),
url(r'^stack/(?P[^/]+)/$',
views.DetailView.as_view(), name='detail'),
+ url(r'^(?P[^/]+)/change_template$',
+ views.ChangeTemplateView.as_view(), name='change_template'),
+ url(r'^(?P[^/]+)/edit_stack$',
+ views.EditStackView.as_view(), name='edit_stack'),
url(r'^stack/(?P[^/]+)/(?P[^/]+)/$',
views.ResourceView.as_view(), name='resource'),
url(r'^get_d3_data/(?P[^/]+)/$',
diff --git a/openstack_dashboard/dashboards/project/stacks/views.py b/openstack_dashboard/dashboards/project/stacks/views.py
index 737c4ac5..09649ce9 100644
--- a/openstack_dashboard/dashboards/project/stacks/views.py
+++ b/openstack_dashboard/dashboards/project/stacks/views.py
@@ -67,8 +67,41 @@ class SelectTemplateView(forms.ModalFormView):
return kwargs
+class ChangeTemplateView(forms.ModalFormView):
+ form_class = project_forms.ChangeTemplateForm
+ template_name = 'project/stacks/change_template.html'
+ success_url = reverse_lazy('horizon:project:stacks:edit_stack')
+
+ def get_context_data(self, **kwargs):
+ context = super(ChangeTemplateView, self).get_context_data(**kwargs)
+ context['stack'] = self.get_object()
+ return context
+
+ @memoized.memoized_method
+ def get_object(self):
+ stack_id = self.kwargs['stack_id']
+ try:
+ self._stack = api.heat.stack_get(self.request, stack_id)
+ except Exception:
+ msg = _("Unable to retrieve stack.")
+ redirect = reverse('horizon:project:stacks:index')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._stack
+
+ def get_initial(self):
+ stack = self.get_object()
+ return {'stack_id': stack.id,
+ 'stack_name': stack.stack_name
+ }
+
+ def get_form_kwargs(self):
+ kwargs = super(ChangeTemplateView, self).get_form_kwargs()
+ kwargs['next_view'] = EditStackView
+ return kwargs
+
+
class CreateStackView(forms.ModalFormView):
- form_class = project_forms.StackCreateForm
+ form_class = project_forms.CreateStackForm
template_name = 'project/stacks/create.html'
success_url = reverse_lazy('horizon:project:stacks:index')
@@ -92,6 +125,42 @@ class CreateStackView(forms.ModalFormView):
return kwargs
+# edit stack parameters, coming from template selector
+class EditStackView(CreateStackView):
+ form_class = project_forms.EditStackForm
+ template_name = 'project/stacks/update.html'
+ success_url = reverse_lazy('horizon:project:stacks:index')
+
+ def get_initial(self):
+ initial = super(EditStackView, self).get_initial()
+
+ initial['stack'] = self.get_object()['stack']
+ if initial['stack']:
+ initial['stack_id'] = initial['stack'].id
+ initial['stack_name'] = initial['stack'].stack_name
+
+ return initial
+
+ def get_context_data(self, **kwargs):
+ context = super(EditStackView, self).get_context_data(**kwargs)
+ context['stack'] = self.get_object()['stack']
+ return context
+
+ @memoized.memoized_method
+ def get_object(self):
+ stack_id = self.kwargs['stack_id']
+ try:
+ stack = {}
+ stack['stack'] = api.heat.stack_get(self.request, stack_id)
+ stack['template'] = api.heat.template_get(self.request, stack_id)
+ self._stack = stack
+ except Exception:
+ msg = _("Unable to retrieve stack.")
+ redirect = reverse('horizon:project:stacks:index')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._stack
+
+
class DetailView(tabs.TabView):
tab_group_class = project_tabs.StackDetailTabs
template_name = 'project/stacks/detail.html'
diff --git a/openstack_dashboard/test/api_tests/heat_tests.py b/openstack_dashboard/test/api_tests/heat_tests.py
index 95ff3b76..e1ae6fcd 100644
--- a/openstack_dashboard/test/api_tests/heat_tests.py
+++ b/openstack_dashboard/test/api_tests/heat_tests.py
@@ -24,6 +24,35 @@ class HeatApiTests(test.APITestCase):
heatclient.stacks = self.mox.CreateMockAnything()
heatclient.stacks.list().AndReturn(iter(api_stacks))
self.mox.ReplayAll()
-
stacks = api.heat.stacks_list(self.request)
self.assertItemsEqual(stacks, api_stacks)
+
+ def test_template_get(self):
+ api_stacks = self.stacks.list()
+ stack_id = api_stacks[0].id
+ mock_data_template = self.stack_templates.list()[0]
+
+ heatclient = self.stub_heatclient()
+ heatclient.stacks = self.mox.CreateMockAnything()
+ heatclient.stacks.template(stack_id).AndReturn(mock_data_template)
+ self.mox.ReplayAll()
+
+ template = api.heat.template_get(self.request, stack_id)
+ self.assertEqual(template.data, mock_data_template.data)
+
+ def test_stack_update(self):
+ api_stacks = self.stacks.list()
+ stack = api_stacks[0]
+ stack_id = stack.id
+
+ heatclient = self.stub_heatclient()
+ heatclient.stacks = self.mox.CreateMockAnything()
+ form_data = {'timeout_mins': 600}
+ heatclient.stacks.update(stack_id, **form_data).AndReturn(stack)
+ self.mox.ReplayAll()
+
+ returned_stack = api.heat.stack_update(self.request,
+ stack_id,
+ **form_data)
+ from heatclient.v1 import stacks
+ self.assertIsInstance(returned_stack, stacks.Stack)