From c2594b3d0ead60a7afe79f374056c3a0c66de12d Mon Sep 17 00:00:00 2001 From: johndavidge Date: Tue, 2 Sep 2014 03:52:43 -0700 Subject: [PATCH] Instance customization script can now be uploaded as file Added the ability for a user to upload a customization script file from their local machine instead of copy/pasting it into a text field if they prefer. Tests are included to cover this new functionality. Change-Id: Icc0e750346822a26ea853d4cc3d790d9d9f289d5 Closes-Bug: 1298483 --- .../instances/_launch_customize_help.html | 2 +- .../dashboards/project/instances/tests.py | 83 ++++++++++++++++--- .../instances/workflows/create_instance.py | 83 ++++++++++++++++--- 3 files changed, 146 insertions(+), 22 deletions(-) diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html index ba4e1fe231..a1a83b8cd5 100644 --- a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html @@ -1,3 +1,3 @@ {% load i18n %}

{% blocktrans %}You can customize your instance after it has launched using the options available here.{% endblocktrans %}

-

{% blocktrans %}The "Customization Script" field is analogous to "User Data" in other systems.{% endblocktrans %}

+

{% blocktrans %}"Customization Script" is analogous to "User Data" in other systems.{% endblocktrans %}

diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 33d89d3296..46a522078b 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -17,6 +17,7 @@ # under the License. import json +import sys import uuid from django.conf import settings @@ -30,6 +31,7 @@ from mox import IgnoreArg # noqa from mox import IsA # noqa from horizon import exceptions +from horizon import forms from horizon.workflows import views from openstack_dashboard import api from openstack_dashboard.api import cinder @@ -1583,7 +1585,8 @@ class InstanceTests(helpers.TestCase): 'image_id': image.id, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -1707,7 +1710,8 @@ class InstanceTests(helpers.TestCase): 'source_id': volume_choice, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -1834,7 +1838,8 @@ class InstanceTests(helpers.TestCase): # 'image_id': '', 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -1933,7 +1938,8 @@ class InstanceTests(helpers.TestCase): 'image_id': '', 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -2040,7 +2046,7 @@ class InstanceTests(helpers.TestCase): server = self.servers.first() sec_group = self.security_groups.first() avail_zone = self.availability_zones.first() - customization_script = 'userData' + customization_script = 'user data' nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] quota_usages = self.quota_usages.first() @@ -2116,7 +2122,8 @@ class InstanceTests(helpers.TestCase): 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -2218,7 +2225,8 @@ class InstanceTests(helpers.TestCase): 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -2316,7 +2324,8 @@ class InstanceTests(helpers.TestCase): 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -2431,7 +2440,8 @@ class InstanceTests(helpers.TestCase): 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -2558,7 +2568,8 @@ class InstanceTests(helpers.TestCase): 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, - 'customization_script': customization_script, + 'script_source': 'raw', + 'script_data': customization_script, 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, @@ -3243,6 +3254,58 @@ class InstanceTests(helpers.TestCase): self.assertRedirectsNoFollow(res, next_page_url) self.assertMessageCount(success=1) + class SimpleFile(object): + def __init__(self, name, data, size): + self.name = name + self.data = data + self._size = size + + def read(self): + return self.data + + def test_clean_file_upload_form_oversize_data(self): + t = workflows.create_instance.CustomizeAction(self.request, {}) + upload_str = 'user data' + files = {'script_upload': + self.SimpleFile('script_name', + upload_str, + (16 * 1024) + 1)} + + self.assertRaises( + forms.ValidationError, + t.clean_uploaded_files, + 'script', + files) + + def test_clean_file_upload_form_invalid_data(self): + t = workflows.create_instance.CustomizeAction(self.request, {}) + upload_str = '\x81' + files = {'script_upload': + self.SimpleFile('script_name', + upload_str, + sys.getsizeof(upload_str))} + + self.assertRaises( + forms.ValidationError, + t.clean_uploaded_files, + 'script', + files) + + def test_clean_file_upload_form_valid_data(self): + t = workflows.create_instance.CustomizeAction(self.request, {}) + precleaned = 'user data' + upload_str = 'user data' + files = {'script_upload': + self.SimpleFile('script_name', + upload_str, + sys.getsizeof(upload_str))} + + cleaned = t.clean_uploaded_files('script', files) + + self.assertEqual( + cleaned, + precleaned) + class InstanceAjaxTests(helpers.TestCase): @helpers.create_stubs({api.nova: ("server_get", diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index ea62a6d421..f20d32f597 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -555,24 +555,84 @@ class SetAccessControls(workflows.Step): class CustomizeAction(workflows.Action): - customization_script = forms.CharField(widget=forms.Textarea, - label=_("Customization Script"), - required=False, - help_text=_("A script or set of " - "commands to be " - "executed after the " - "instance has been " - "built (max 16kb).")) - class Meta: name = _("Post-Creation") help_text_template = ("project/instances/" "_launch_customize_help.html") + source_choices = [('raw', _('Direct Input')), + ('file', _('File'))] + + attributes = {'class': 'switchable', 'data-slug': 'scriptsource'} + script_source = forms.ChoiceField(label=_('Customization Script Source'), + choices=source_choices, + widget=forms.Select(attrs=attributes)) + + script_help = _("A script or set of commands to be executed after the " + "instance has been built (max 16kb).") + + script_upload = forms.FileField( + label=_('Script File'), + help_text=script_help, + widget=forms.FileInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'scriptsource', + 'data-scriptsource-file': _('Script File')}), + required=False) + + script_data = forms.CharField( + label=_('Script Data'), + help_text=script_help, + widget=forms.widgets.Textarea(attrs={ + 'class': 'switched', + 'data-switch-on': 'scriptsource', + 'data-scriptsource-raw': _('Script Data')}), + required=False) + + def __init__(self, *args): + super(CustomizeAction, self).__init__(*args) + + def clean(self): + cleaned = super(CustomizeAction, self).clean() + + files = self.request.FILES + script = self.clean_uploaded_files('script', files) + + if script is not None: + cleaned['script_data'] = script + + return cleaned + + def clean_uploaded_files(self, prefix, files): + upload_str = prefix + "_upload" + + has_upload = upload_str in files + if has_upload: + upload_file = files[upload_str] + log_script_name = upload_file.name + LOG.info('got upload %s' % log_script_name) + + if upload_file._size > 16 * 1024: # 16kb + msg = _('File exceeds maximum size (16kb)') + raise forms.ValidationError(msg) + else: + script = upload_file.read() + if script != "": + try: + normalize_newlines(script) + 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) + return script + else: + return None + class PostCreationStep(workflows.Step): action_class = CustomizeAction - contributes = ("customization_script",) + contributes = ("script_data",) class SetNetworkAction(workflows.Action): @@ -698,6 +758,7 @@ class LaunchInstance(workflows.Workflow): success_message = _('Launched %(count)s named "%(name)s".') failure_message = _('Unable to launch %(count)s named "%(name)s".') success_url = "horizon:project:instances:index" + multipart = True default_steps = (SelectProjectUser, SetInstanceDetails, SetAccessControls, @@ -716,7 +777,7 @@ class LaunchInstance(workflows.Workflow): @sensitive_variables('context') def handle(self, request, context): - custom_script = context.get('customization_script', '') + custom_script = context.get('script_data', '') dev_mapping_1 = None dev_mapping_2 = None