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
This commit is contained in:
johndavidge 2014-09-02 03:52:43 -07:00 committed by John Davidge
parent e0e46374de
commit c2594b3d0e
3 changed files with 146 additions and 22 deletions

View File

@ -1,3 +1,3 @@
{% load i18n %}
<p>{% blocktrans %}You can customize your instance after it has launched using the options available here.{% endblocktrans %}</p>
<p>{% blocktrans %}The "Customization Script" field is analogous to "User Data" in other systems.{% endblocktrans %}</p>
<p>{% blocktrans %}"Customization Script" is analogous to "User Data" in other systems.{% endblocktrans %}</p>

View File

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

View File

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