Heat Stack update view/form

2 Part view for updating Heat Templates. The first page allows you to
select a new template for your stack. The second allows you to set new
template parameters for your stack. Like the launch stack workflow, this
is not a horizon workflow, but two separate forms.

Implements: blueprint heat-stack-update

Change-Id: I2854e9e4bb578be5187ef962808b93f11ac6b1f1
This commit is contained in:
Jordan OMara 2014-01-14 10:25:26 -05:00
parent 9f8a5eb349
commit 015aff2630
12 changed files with 360 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "Use one of the available template source options to specify the template to be used in creating this stack." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Next" %}" />
<a href="{% url 'horizon:project:stacks:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% 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." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update" %}" />
<a href="{% url 'horizon:project:stacks:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -26,6 +26,10 @@ urlpatterns = patterns(
url(r'^launch$', views.CreateStackView.as_view(), name='launch'),
url(r'^stack/(?P<stack_id>[^/]+)/$',
views.DetailView.as_view(), name='detail'),
url(r'^(?P<stack_id>[^/]+)/change_template$',
views.ChangeTemplateView.as_view(), name='change_template'),
url(r'^(?P<stack_id>[^/]+)/edit_stack$',
views.EditStackView.as_view(), name='edit_stack'),
url(r'^stack/(?P<stack_id>[^/]+)/(?P<resource_name>[^/]+)/$',
views.ResourceView.as_view(), name='resource'),
url(r'^get_d3_data/(?P<stack_id>[^/]+)/$',

View File

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

View File

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

View File

@ -326,6 +326,17 @@ def data(TEST):
"05b4f39f-ea96-4d91-910c-e758c078a089",
"rel": "self"
}],
"parameters": {
'DBUsername': '******',
'InstanceType': 'm1.small',
'AWS::StackId':
'arn:openstack:heat::2ce287:stacks/teststack/88553ec',
'DBRootPassword': '******',
'AWS::StackName': 'teststack',
'DBPassword': '******',
'AWS::Region': 'ap-southeast-1',
'DBName': u'wordpress'
},
"stack_status_reason": "Stack successfully created",
"stack_name": "stack-test",
"creation_time": "2013-04-22T00:11:39Z",