Skip to content

Commit

Permalink
Instance customization script can now be uploaded as file
Browse files Browse the repository at this point in the history
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
  • Loading branch information
johndavidge committed Sep 23, 2014
1 parent e0e4637 commit c2594b3
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -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>
83 changes: 73 additions & 10 deletions openstack_dashboard/dashboards/project/instances/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# under the License.

import json
import sys
import uuid

from django.conf import settings
Expand All @@ -30,6 +31,7 @@
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
Expand Down Expand Up @@ -1583,7 +1585,8 @@ def test_launch_instance_post(self,
'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,
Expand Down Expand Up @@ -1707,7 +1710,8 @@ def test_launch_instance_post_boot_from_volume(self,
'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,
Expand Down Expand Up @@ -1834,7 +1838,8 @@ def test_launch_instance_post_no_images_available_boot_from_volume(
# '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,
Expand Down Expand Up @@ -1933,7 +1938,8 @@ def test_launch_instance_post_no_images_available(self,
'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,
Expand Down Expand Up @@ -2040,7 +2046,7 @@ def test_launch_form_keystone_exception(self,
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()

Expand Down Expand Up @@ -2116,7 +2122,8 @@ def test_launch_form_keystone_exception(self,
'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,
Expand Down Expand Up @@ -2218,7 +2225,8 @@ def test_launch_form_instance_count_error(self,
'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,
Expand Down Expand Up @@ -2316,7 +2324,8 @@ def _test_launch_form_count_error(self, resource,
'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,
Expand Down Expand Up @@ -2431,7 +2440,8 @@ def _test_launch_form_instance_requirement_error(self, image, flavor,
'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,
Expand Down Expand Up @@ -2558,7 +2568,8 @@ def _test_launch_form_instance_volume_size(self, image, volume_size, msg,
'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,
Expand Down Expand Up @@ -3243,6 +3254,58 @@ def test_terminate_instance_with_pagination(self):
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,24 +555,84 @@ def contribute(self, data, context):


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):
Expand Down Expand Up @@ -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,
Expand All @@ -716,7 +777,7 @@ def format_status_message(self, message):

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

0 comments on commit c2594b3

Please sign in to comment.