From 11eebb8aa3fbf02bb3cd48e0f8975d33b6765c9b Mon Sep 17 00:00:00 2001 From: Jordan OMara Date: Thu, 30 Jan 2014 16:43:00 -0500 Subject: [PATCH] Additional optional Environment data to heat template selection Change-Id: I4e0d32e7e032d5c0ec93595c14134e780fa21d66 Implements: blueprint heat-environment-file --- .../dashboards/project/stacks/forms.py | 193 ++++++++++++++---- .../dashboards/project/stacks/tests.py | 127 ++++++++++++ .../dashboards/project/stacks/views.py | 13 +- .../test/test_data/heat_data.py | 14 ++ 4 files changed, 299 insertions(+), 48 deletions(-) diff --git a/openstack_dashboard/dashboards/project/stacks/forms.py b/openstack_dashboard/dashboards/project/stacks/forms.py index 4fa0ebacb7..dc3bac2e5f 100644 --- a/openstack_dashboard/dashboards/project/stacks/forms.py +++ b/openstack_dashboard/dashboards/project/stacks/forms.py @@ -53,6 +53,23 @@ def exception_to_validation_msg(e): return match.group(1) +def create_upload_form_attributes(prefix, input_type, name): + """Creates attribute dicts for the switchable upload form + + :type prefix: str + :param prefix: prefix (environment, template) of field + :type input_type: str + :param input_type: field type (file, raw, url) + :type name: str + :param name: translated text label to display to user + :rtype: dict + :return: an attribute set to pass to form build + """ + attributes = {'class': 'switched', 'data-switch-on': prefix + 'source'} + attributes['data-' + prefix + 'source-' + input_type] = name + return attributes + + class TemplateForm(forms.SelfHandlingForm): class Meta: @@ -60,34 +77,79 @@ class TemplateForm(forms.SelfHandlingForm): help_text = _('From here you can select a template to launch ' 'a stack.') + choices = [('url', _('URL')), + ('file', _('File')), + ('raw', _('Direct Input'))] + attributes = {'class': 'switchable', 'data-slug': 'templatesource'} template_source = forms.ChoiceField(label=_('Template Source'), - choices=[('url', _('URL')), - ('file', _('File')), - ('raw', _('Direct Input'))], - widget=forms.Select(attrs={ - 'class': 'switchable', - 'data-slug': 'source'})) + choices=choices, + widget=forms.Select(attrs=attributes)) + + attributes = create_upload_form_attributes( + 'template', + 'file', + _('Template File')) template_upload = forms.FileField( label=_('Template File'), help_text=_('A local template to upload.'), - widget=forms.FileInput(attrs={'class': 'switched', - 'data-switch-on': 'source', - 'data-source-file': _('Template File')}), + widget=forms.FileInput(attrs=attributes), required=False) + + attributes = create_upload_form_attributes( + 'template', + 'url', + _('Template URL')) template_url = forms.URLField( label=_('Template URL'), help_text=_('An external (HTTP) URL to load the template from.'), - widget=forms.TextInput(attrs={'class': 'switched', - 'data-switch-on': 'source', - 'data-source-url': _('Template URL')}), + widget=forms.TextInput(attrs=attributes), required=False) + + attributes = create_upload_form_attributes( + 'template', + 'raw', + _('Template Data')) template_data = forms.CharField( label=_('Template Data'), help_text=_('The raw contents of the template.'), - widget=forms.widgets.Textarea(attrs={ - 'class': 'switched', - 'data-switch-on': 'source', - 'data-source-raw': _('Template Data')}), + widget=forms.widgets.Textarea(attrs=attributes), + required=False) + + attributes = {'data-slug': 'envsource', 'class': 'switchable'} + environment_source = forms.ChoiceField( + label=_('Environment Source'), + choices=choices, + widget=forms.Select(attrs=attributes), + required=False) + + attributes = create_upload_form_attributes( + 'env', + 'file', + _('Environment File')) + environment_upload = forms.FileField( + label=_('Environment File'), + help_text=_('A local environment to upload.'), + widget=forms.FileInput(attrs=attributes), + required=False) + + attributes = create_upload_form_attributes( + 'env', + 'url', + _('Environment URL')) + environment_url = forms.URLField( + label=_('Environment URL'), + help_text=_('An external (HTTP) URL to load the environment from.'), + widget=forms.TextInput(attrs=attributes), + required=False) + + attributes = create_upload_form_attributes( + 'env', + 'raw', + _('Environment Data')) + environment_data = forms.CharField( + label=_('Environment Data'), + help_text=_('The raw contents of the environment file.'), + widget=forms.widgets.Textarea(attrs=attributes), required=False) def __init__(self, *args, **kwargs): @@ -96,35 +158,13 @@ class TemplateForm(forms.SelfHandlingForm): def clean(self): cleaned = super(TemplateForm, self).clean() - template_url = cleaned.get('template_url') - template_data = cleaned.get('template_data') + files = self.request.FILES - has_upload = 'template_upload' in files - - # Uploaded file handler - if has_upload and not template_url: - log_template_name = self.request.FILES['template_upload'].name - LOG.info('got upload %s' % log_template_name) - - tpl = self.request.FILES['template_upload'].read() - if tpl.startswith('{'): - try: - json.loads(tpl) - except Exception as e: - msg = _('There was a problem parsing the template: %s') % e - raise forms.ValidationError(msg) - cleaned['template_data'] = tpl - - # URL handler - elif template_url and (has_upload or template_data): - msg = _('Please specify a template using only one source method.') - raise forms.ValidationError(msg) - - # Check for raw template input - elif not template_url and not template_data: - msg = _('You must specify a template via one of the ' - 'available sources.') - raise forms.ValidationError(msg) + self.clean_uploaded_files('template', _('template'), cleaned, files) + self.clean_uploaded_files('environment', + _('environment'), + cleaned, + files) # Validate the template and get back the params. kwargs = {} @@ -145,8 +185,62 @@ class TemplateForm(forms.SelfHandlingForm): return cleaned + def clean_uploaded_files(self, prefix, field_label, cleaned, files): + """Cleans Template & Environment data from form upload. + + Does some of the crunchy bits for processing uploads vs raw + data depending on what the user specified. Identical process + for environment data & template data. + + :type prefix: str + :param prefix: prefix (environment, template) of field + :type field_label: str + :param field_label: translated prefix str for messages + :type input_type: dict + :param prefix: existing cleaned fields from form + :rtype: dict + :return: cleaned dict including environment & template data + """ + + upload_str = prefix + "_upload" + data_str = prefix + "_data" + url = cleaned.get(prefix + '_url') + data = cleaned.get(prefix + '_data') + + has_upload = upload_str in files + # Uploaded file handler + if has_upload and not url: + log_template_name = files[upload_str].name + LOG.info('got upload %s' % log_template_name) + + tpl = files[upload_str].read() + if tpl.startswith('{'): + try: + json.loads(tpl) + except Exception as e: + msg = _('There was a problem parsing the' + ' %(prefix)s: %(error)s') + msg = msg % {'prefix': prefix, 'error': e} + raise forms.ValidationError(msg) + cleaned[data_str] = tpl + + # URL handler + elif url and (has_upload or data): + msg = _('Please specify a %s using only one source method.') + msg = msg % field_label + raise forms.ValidationError(msg) + + elif prefix == 'template': + # Check for raw template input - blank environment allowed + if not url and not data: + msg = _('You must specify a template via one of the ' + 'available sources.') + raise forms.ValidationError(msg) + def create_kwargs(self, data): kwargs = {'parameters': data['template_validate'], + 'environment_data': data['environment_data'], + 'environment_url': data['environment_url'], 'template_data': data['template_data'], 'template_url': data['template_url']} if data.get('stack_id'): @@ -190,6 +284,12 @@ class CreateStackForm(forms.SelfHandlingForm): template_url = forms.CharField( widget=forms.widgets.HiddenInput, required=False) + environment_data = forms.CharField( + widget=forms.widgets.HiddenInput, + required=False) + environment_url = forms.CharField( + widget=forms.widgets.HiddenInput, + required=False) parameters = forms.CharField( widget=forms.widgets.HiddenInput, required=True) @@ -283,6 +383,11 @@ class CreateStackForm(forms.SelfHandlingForm): else: fields['template_url'] = data.get('template_url') + if data.get('environment_data'): + fields['environment'] = data.get('environment_data') + elif data.get('environment_url'): + fields['environment_url'] = data.get('environment_url') + try: api.heat.stack_create(self.request, **fields) messages.success(request, _("Stack creation started.")) diff --git a/openstack_dashboard/dashboards/project/stacks/tests.py b/openstack_dashboard/dashboards/project/stacks/tests.py index 5c0711d8ce..4c364c8597 100644 --- a/openstack_dashboard/dashboards/project/stacks/tests.py +++ b/openstack_dashboard/dashboards/project/stacks/tests.py @@ -14,6 +14,7 @@ import json +from django.core import exceptions from django.core.urlresolvers import reverse from django import http @@ -155,6 +156,60 @@ class StackTests(test.TestCase): res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) + @test.create_stubs({api.heat: ('stack_create', 'template_validate')}) + def test_launch_stackwith_environment(self): + template = self.stack_templates.first() + environment = self.stack_environments.first() + stack = self.stacks.first() + + api.heat.template_validate(IsA(http.HttpRequest), + template=template.data) \ + .AndReturn(json.loads(template.validate)) + + api.heat.stack_create(IsA(http.HttpRequest), + stack_name=stack.stack_name, + timeout_mins=60, + disable_rollback=True, + template=template.data, + environment=environment.data, + parameters=IsA(dict), + password='password') + + self.mox.ReplayAll() + + url = reverse('horizon:project:stacks:select_template') + res = self.client.get(url) + self.assertTemplateUsed(res, 'project/stacks/select_template.html') + + form_data = {'template_source': 'raw', + 'template_data': template.data, + 'environment_source': 'raw', + 'environment_data': environment.data, + 'method': forms.TemplateForm.__name__} + res = self.client.post(url, form_data) + self.assertTemplateUsed(res, 'project/stacks/create.html') + + url = reverse('horizon:project:stacks:launch') + form_data = {'template_source': 'raw', + 'template_data': template.data, + 'environment_source': 'raw', + 'environment_data': environment.data, + 'password': 'password', + '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.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): @@ -258,6 +313,14 @@ class StackTests(test.TestCase): class TemplateFormTests(test.TestCase): + class SimpleFile(object): + def __init__(self, name, data): + self.name = name + self.data = data + + def read(self): + return self.data + def test_exception_to_validation(self): json_error = """{ "code": 400, @@ -298,3 +361,67 @@ malformed or otherwise incorrect. msg = forms.exception_to_validation_msg(json_error) self.assertIsNone(msg) + + def test_create_upload_form_attributes(self): + attrs = forms.create_upload_form_attributes( + 'env', 'url', 'Environment') + self.assertEqual(attrs['data-envsource-url'], 'Environment') + + def test_clean_file_upload_form_url(self): + kwargs = {'next_view': 'Launch Stack'} + t = forms.TemplateForm({}, **kwargs) + precleaned = { + 'template_url': 'http://templateurl.com', + } + t.clean_uploaded_files('template', 'template', precleaned, {}) + + self.assertEqual(precleaned['template_url'], 'http://templateurl.com') + + def test_clean_file_upload_form_multiple(self): + kwargs = {'next_view': 'Launch Stack'} + t = forms.TemplateForm({}, **kwargs) + precleaned = { + 'template_url': 'http://templateurl.com', + 'template_data': 'http://templateurl.com', + } + self.assertRaises( + exceptions.ValidationError, + t.clean_uploaded_files, + 'template', + 'template', + precleaned, + {}) + + def test_clean_file_upload_form_invalid_json(self): + kwargs = {'next_view': 'Launch Stack'} + t = forms.TemplateForm({}, **kwargs) + precleaned = { + 'template_data': 'http://templateurl.com', + } + json_str = '{notvalidjson::::::json/////json' + files = {'template_upload': + self.SimpleFile('template_name', json_str)} + + self.assertRaises( + exceptions.ValidationError, + t.clean_uploaded_files, + 'template', + 'template', + precleaned, + files) + + def test_clean_file_upload_form_valid_data(self): + kwargs = {'next_view': 'Launch Stack'} + t = forms.TemplateForm({}, **kwargs) + precleaned = { + 'template_data': 'http://templateurl.com', + } + + json_str = '{"isvalid":"json"}' + files = {'template_upload': + self.SimpleFile('template_name', json_str)} + + t.clean_uploaded_files('template', 'template', precleaned, files) + self.assertEqual( + json_str, + precleaned['template_data']) diff --git a/openstack_dashboard/dashboards/project/stacks/views.py b/openstack_dashboard/dashboards/project/stacks/views.py index 09649ce93c..31096a20fc 100644 --- a/openstack_dashboard/dashboards/project/stacks/views.py +++ b/openstack_dashboard/dashboards/project/stacks/views.py @@ -107,14 +107,19 @@ class CreateStackView(forms.ModalFormView): def get_initial(self): initial = {} - if 'template_data' in self.kwargs: - initial['template_data'] = self.kwargs['template_data'] - if 'template_url' in self.kwargs: - initial['template_url'] = self.kwargs['template_url'] + self.load_kwargs(initial) if 'parameters' in self.kwargs: initial['parameters'] = json.dumps(self.kwargs['parameters']) return initial + def load_kwargs(self, initial): + # load the "passed through" data from template form + for prefix in ('template', 'environment'): + for suffix in ('_data', '_url'): + key = prefix + suffix + if key in self.kwargs: + initial[key] = self.kwargs[key] + def get_form_kwargs(self): kwargs = super(CreateStackView, self).get_form_kwargs() if 'parameters' in self.kwargs: diff --git a/openstack_dashboard/test/test_data/heat_data.py b/openstack_dashboard/test/test_data/heat_data.py index 70dc5b4559..e0854b1fd2 100644 --- a/openstack_dashboard/test/test_data/heat_data.py +++ b/openstack_dashboard/test/test_data/heat_data.py @@ -305,6 +305,18 @@ VALIDATE = """ } """ +ENVIRONMENT = """ +parameters: + InstanceType: m1.xsmall + db_password: verybadpass + KeyName: heat_key +""" + + +class Environment(object): + def __init__(self, data): + self.data = data + class Template(object): def __init__(self, data, validate): @@ -315,6 +327,7 @@ class Template(object): def data(TEST): TEST.stacks = utils.TestDataContainer() TEST.stack_templates = utils.TestDataContainer() + TEST.stack_environments = utils.TestDataContainer() # Stacks stack1 = { @@ -348,3 +361,4 @@ def data(TEST): TEST.stacks.add(stack) TEST.stack_templates.add(Template(TEMPLATE, VALIDATE)) + TEST.stack_environments.add(Environment(ENVIRONMENT))