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:
parent
e0e46374de
commit
c2594b3d0e
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user