From cab3912b69c8aad2fdf9c66f73e8a43413ea12a7 Mon Sep 17 00:00:00 2001 From: Tatiana Ovchinnikova Date: Fri, 20 Feb 2015 07:34:37 +0300 Subject: [PATCH] Add "Preview Stack" action to Stacks table This patch set adds "Preview Stack" button to Stacks table to provide user with a possibility to preview stack without creating it, as it is already implemented in CLI. Partially implements blueprint: heat-ui-improvement Change-Id: Idf92deb57f8213a403f102db467828087d91e79a --- openstack_dashboard/api/heat.py | 4 ++ openstack_dashboard/conf/heat_policy.json | 1 + .../dashboards/project/stacks/forms.py | 43 ++++++++++++++ .../dashboards/project/stacks/tables.py | 9 +++ .../stacks/templates/stacks/_preview.html | 6 ++ .../templates/stacks/_preview_details.html | 59 +++++++++++++++++++ .../templates/stacks/_preview_template.html | 7 +++ .../stacks/templates/stacks/preview.html | 7 +++ .../templates/stacks/preview_details.html | 7 +++ .../templates/stacks/preview_template.html | 7 +++ .../dashboards/project/stacks/tests.py | 48 +++++++++++++++ .../dashboards/project/stacks/urls.py | 5 ++ .../dashboards/project/stacks/views.py | 44 ++++++++++++++ 13 files changed, 247 insertions(+) create mode 100644 openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html create mode 100644 openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html create mode 100644 openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html create mode 100644 openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html create mode 100644 openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html create mode 100644 openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html diff --git a/openstack_dashboard/api/heat.py b/openstack_dashboard/api/heat.py index 8a7f2dacbb..a6bf6c7693 100644 --- a/openstack_dashboard/api/heat.py +++ b/openstack_dashboard/api/heat.py @@ -102,6 +102,10 @@ def stack_create(request, password=None, **kwargs): return heatclient(request, password).stacks.create(**kwargs) +def stack_preview(request, password=None, **kwargs): + return heatclient(request, password).stacks.preview(**kwargs) + + def stack_update(request, stack_id, password=None, **kwargs): return heatclient(request, password).stacks.update(stack_id, **kwargs) diff --git a/openstack_dashboard/conf/heat_policy.json b/openstack_dashboard/conf/heat_policy.json index d0037a9335..2e34982122 100644 --- a/openstack_dashboard/conf/heat_policy.json +++ b/openstack_dashboard/conf/heat_policy.json @@ -4,6 +4,7 @@ "cloudformation:ListStacks": "rule:deny_stack_user", "cloudformation:CreateStack": "rule:deny_stack_user", + "cloudformation:PreviewStack": "rule:deny_stack_user", "cloudformation:DescribeStacks": "rule:deny_stack_user", "cloudformation:DeleteStack": "rule:deny_stack_user", "cloudformation:UpdateStack": "rule:deny_stack_user", diff --git a/openstack_dashboard/dashboards/project/stacks/forms.py b/openstack_dashboard/dashboards/project/stacks/forms.py index 9ecfcff50c..b84f22ba1b 100644 --- a/openstack_dashboard/dashboards/project/stacks/forms.py +++ b/openstack_dashboard/dashboards/project/stacks/forms.py @@ -235,6 +235,12 @@ class ChangeTemplateForm(TemplateForm): 'readonly'})) +class PreviewTemplateForm(TemplateForm): + class Meta(object): + name = _('Preview Template') + help_text = _('Select a new template to preview a stack.') + + class CreateStackForm(forms.SelfHandlingForm): param_prefix = '__param_' @@ -429,3 +435,40 @@ class EditStackForm(CreateStackForm): return True except Exception: exceptions.handle(request) + + +class PreviewStackForm(CreateStackForm): + + class Meta(object): + name = _('Preview Stack Parameters') + + def __init__(self, *args, **kwargs): + self.next_view = kwargs.pop('next_view') + super(CreateStackForm, self).__init__(*args, **kwargs) + + def handle(self, request, data): + prefix_length = len(self.param_prefix) + params_list = [(k[prefix_length:], v) for (k, v) in six.iteritems(data) + if k.startswith(self.param_prefix)] + fields = { + 'stack_name': data.get('stack_name'), + 'timeout_mins': data.get('timeout_mins'), + 'disable_rollback': not(data.get('enable_rollback')), + 'parameters': dict(params_list), + } + + if data.get('template_data'): + fields['template'] = data.get('template_data') + else: + fields['template_url'] = data.get('template_url') + + if data.get('environment_data'): + fields['environment'] = data.get('environment_data') + + try: + stack_preview = api.heat.stack_preview(self.request, **fields) + request.method = 'GET' + return self.next_view.as_view()(request, + stack_preview=stack_preview) + except Exception: + exceptions.handle(request) diff --git a/openstack_dashboard/dashboards/project/stacks/tables.py b/openstack_dashboard/dashboards/project/stacks/tables.py index 9abbb20616..e204a8d98b 100644 --- a/openstack_dashboard/dashboards/project/stacks/tables.py +++ b/openstack_dashboard/dashboards/project/stacks/tables.py @@ -36,6 +36,14 @@ class LaunchStack(tables.LinkAction): policy_rules = (("orchestration", "cloudformation:CreateStack"),) +class PreviewStack(tables.LinkAction): + name = "preview" + verbose_name = _("Preview Stack") + url = "horizon:project:stacks:preview_template" + classes = ("ajax-modal",) + policy_rules = (("orchestration", "cloudformation:PreviewStack"),) + + class CheckStack(tables.BatchAction): name = "check" verbose_name = _("Check Stack") @@ -277,6 +285,7 @@ class StacksTable(tables.DataTable): status_columns = ["status", ] row_class = StacksUpdateRow table_actions = (LaunchStack, + PreviewStack, CheckStack, SuspendStack, ResumeStack, diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html new file mode 100644 index 0000000000..2402478753 --- /dev/null +++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Preview a new stack with the provided values." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html new file mode 100644 index 0000000000..c4163e9609 --- /dev/null +++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_details.html @@ -0,0 +1,59 @@ +{% extends "horizon/common/_modal.html" %} +{% load i18n %} +{% load url from future %} + +{% block modal-header %}{% trans "Stack Preview" %}{% endblock %} + +{% block modal-body %} +
+
+
+ {% for key, value in stack_preview.items %} + {% if key != 'parameters' and key != 'resources' and key != 'links' %} +
{{ key }}
+
{{ value }}
+ {% endif %} + {% endfor %} +
+ + {% if stack_preview.parameters %} +
{% trans "Parameters" %}
+
+
+ {% for key, value in stack_preview.parameters.items %} +
{{ key }}
+
{{ value }}
+ {% endfor %} +
+ {% endif %} + + {% if stack_preview.links %} +
{% trans "Links" %}
+
+ {% for link in stack_preview.links %} +
+
{{ link.rel }}
+
{{ link.href }}
+
+ {% endfor %} + {% endif %} + + {% if stack_preview.resources %} +
{% trans "Resources" %}
+ {% for resource in stack_preview.resources %} +
+
+ {% for key, value in resource.items %} +
{{ key }}
+
{{ value }}
+ {% endfor %} +
+ {% endfor %} + {% endif %} +
+
+{% endblock %} + +{% block modal-footer %} + {% trans "Close" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html new file mode 100644 index 0000000000..76f14b0c7f --- /dev/null +++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/_preview_template.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Use one of the available template source options to specify the template to be used in previewing this stack." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html new file mode 100644 index 0000000000..ded74a40c2 --- /dev/null +++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Preview Stack" %}{% endblock %} + +{% block main %} + {% include 'project/stacks/_preview.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html new file mode 100644 index 0000000000..8d86df5405 --- /dev/null +++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_details.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Preview Stack Details" %}{% endblock %} + +{% block main %} + {% include 'project/stacks/_preview_details.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html new file mode 100644 index 0000000000..e20931cf7f --- /dev/null +++ b/openstack_dashboard/dashboards/project/stacks/templates/stacks/preview_template.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Preview Template" %}{% endblock %} + +{% block main %} + {% include 'project/stacks/_preview_template.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/stacks/tests.py b/openstack_dashboard/dashboards/project/stacks/tests.py index e6b95e967e..f92bb834b5 100644 --- a/openstack_dashboard/dashboards/project/stacks/tests.py +++ b/openstack_dashboard/dashboards/project/stacks/tests.py @@ -740,6 +740,54 @@ class StackTests(test.TestCase): def test_resume_stack(self): self._test_stack_action('resume') + @test.create_stubs({api.heat: ('stack_preview', 'template_validate')}) + def test_preview_stack(self): + template = self.stack_templates.first() + stack = self.stacks.first() + + api.heat.template_validate(IsA(http.HttpRequest), + template=template.data) \ + .AndReturn(json.loads(template.validate)) + + api.heat.stack_preview(IsA(http.HttpRequest), + stack_name=stack.stack_name, + timeout_mins=60, + disable_rollback=True, + template=template.data, + parameters=IsA(dict)).AndReturn(stack) + + self.mox.ReplayAll() + + url = reverse('horizon:project:stacks:preview_template') + res = self.client.get(url) + self.assertTemplateUsed(res, 'project/stacks/preview_template.html') + + form_data = {'template_source': 'raw', + 'template_data': template.data, + 'method': forms.PreviewTemplateForm.__name__} + res = self.client.post(url, form_data) + self.assertTemplateUsed(res, 'project/stacks/preview.html') + + url = reverse('horizon:project:stacks:preview') + form_data = {'template_source': 'raw', + 'template_data': template.data, + 'parameters': template.validate, + 'stack_name': stack.stack_name, + "timeout_mins": 60, + "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.PreviewStackForm.__name__} + res = self.client.post(url, form_data) + self.assertTemplateUsed(res, 'project/stacks/preview_details.html') + self.assertEqual(res.context['stack_preview']['stack_name'], + stack.stack_name) + class TemplateFormTests(test.TestCase): diff --git a/openstack_dashboard/dashboards/project/stacks/urls.py b/openstack_dashboard/dashboards/project/stacks/urls.py index 57d54ab7ee..55e8c6858a 100644 --- a/openstack_dashboard/dashboards/project/stacks/urls.py +++ b/openstack_dashboard/dashboards/project/stacks/urls.py @@ -22,6 +22,11 @@ urlpatterns = patterns( views.SelectTemplateView.as_view(), name='select_template'), url(r'^launch$', views.CreateStackView.as_view(), name='launch'), + url(r'^preview_template$', + views.PreviewTemplateView.as_view(), name='preview_template'), + url(r'^preview$', views.PreviewStackView.as_view(), name='preview'), + url(r'^preview_details$', + views.PreviewStackDetailsView.as_view(), name='preview_details'), url(r'^stack/(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), url(r'^(?P[^/]+)/change_template$', diff --git a/openstack_dashboard/dashboards/project/stacks/views.py b/openstack_dashboard/dashboards/project/stacks/views.py index 2262dae546..69b4de2588 100644 --- a/openstack_dashboard/dashboards/project/stacks/views.py +++ b/openstack_dashboard/dashboards/project/stacks/views.py @@ -27,6 +27,7 @@ from horizon import forms from horizon import tables from horizon import tabs from horizon.utils import memoized +from horizon import views from openstack_dashboard import api from openstack_dashboard.dashboards.project.stacks \ import api as project_api @@ -130,6 +131,22 @@ class ChangeTemplateView(forms.ModalFormView): return kwargs +class PreviewTemplateView(forms.ModalFormView): + template_name = 'project/stacks/preview_template.html' + modal_header = _("Preview Template") + form_id = "preview_template" + form_class = project_forms.PreviewTemplateForm + submit_label = _("Next") + submit_url = reverse_lazy('horizon:project:stacks:preview_template') + success_url = reverse_lazy('horizon:project:stacks:preview') + page_title = _("Preview Template") + + def get_form_kwargs(self): + kwargs = super(PreviewTemplateView, self).get_form_kwargs() + kwargs['next_view'] = PreviewStackView + return kwargs + + class CreateStackView(forms.ModalFormView): form_class = project_forms.CreateStackForm template_name = 'project/stacks/create.html' @@ -198,6 +215,33 @@ class EditStackView(CreateStackView): return self._stack +class PreviewStackView(CreateStackView): + template_name = 'project/stacks/preview.html' + modal_header = _("Preview Stack") + form_id = "preview_stack" + form_class = project_forms.PreviewStackForm + submit_label = _("Preview") + submit_url = reverse_lazy('horizon:project:stacks:preview') + success_url = reverse_lazy('horizon:project:stacks:index') + page_title = _("Preview Stack") + + def get_form_kwargs(self): + kwargs = super(CreateStackView, self).get_form_kwargs() + kwargs['next_view'] = PreviewStackDetailsView + return kwargs + + +class PreviewStackDetailsView(forms.ModalFormMixin, views.HorizonTemplateView): + template_name = 'project/stacks/preview_details.html' + page_title = _("Preview Stack Details") + + def get_context_data(self, **kwargs): + context = super( + PreviewStackDetailsView, self).get_context_data(**kwargs) + context['stack_preview'] = self.kwargs['stack_preview'].to_dict() + return context + + class DetailView(tabs.TabView): tab_group_class = project_tabs.StackDetailTabs template_name = 'project/stacks/detail.html'