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 %}
|
{% load i18n %}
|
||||||
<p>{% blocktrans %}You can customize your instance after it has launched using the options available here.{% endblocktrans %}</p>
|
<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.
|
# under the License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -30,6 +31,7 @@ from mox import IgnoreArg # noqa
|
|||||||
from mox import IsA # noqa
|
from mox import IsA # noqa
|
||||||
|
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
from horizon.workflows import views
|
from horizon.workflows import views
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
from openstack_dashboard.api import cinder
|
from openstack_dashboard.api import cinder
|
||||||
@ -1583,7 +1585,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'image_id': image.id,
|
'image_id': image.id,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -1707,7 +1710,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'source_id': volume_choice,
|
'source_id': volume_choice,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -1834,7 +1838,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
# 'image_id': '',
|
# 'image_id': '',
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -1933,7 +1938,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'image_id': '',
|
'image_id': '',
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -2040,7 +2046,7 @@ class InstanceTests(helpers.TestCase):
|
|||||||
server = self.servers.first()
|
server = self.servers.first()
|
||||||
sec_group = self.security_groups.first()
|
sec_group = self.security_groups.first()
|
||||||
avail_zone = self.availability_zones.first()
|
avail_zone = self.availability_zones.first()
|
||||||
customization_script = 'userData'
|
customization_script = 'user data'
|
||||||
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
|
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
|
||||||
quota_usages = self.quota_usages.first()
|
quota_usages = self.quota_usages.first()
|
||||||
|
|
||||||
@ -2116,7 +2122,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'availability_zone': avail_zone.zoneName,
|
'availability_zone': avail_zone.zoneName,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -2218,7 +2225,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'availability_zone': avail_zone.zoneName,
|
'availability_zone': avail_zone.zoneName,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -2316,7 +2324,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'availability_zone': avail_zone.zoneName,
|
'availability_zone': avail_zone.zoneName,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -2431,7 +2440,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'availability_zone': avail_zone.zoneName,
|
'availability_zone': avail_zone.zoneName,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -2558,7 +2568,8 @@ class InstanceTests(helpers.TestCase):
|
|||||||
'availability_zone': avail_zone.zoneName,
|
'availability_zone': avail_zone.zoneName,
|
||||||
'keypair': keypair.name,
|
'keypair': keypair.name,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
'customization_script': customization_script,
|
'script_source': 'raw',
|
||||||
|
'script_data': customization_script,
|
||||||
'project_id': self.tenants.first().id,
|
'project_id': self.tenants.first().id,
|
||||||
'user_id': self.user.id,
|
'user_id': self.user.id,
|
||||||
'groups': sec_group.name,
|
'groups': sec_group.name,
|
||||||
@ -3243,6 +3254,58 @@ class InstanceTests(helpers.TestCase):
|
|||||||
self.assertRedirectsNoFollow(res, next_page_url)
|
self.assertRedirectsNoFollow(res, next_page_url)
|
||||||
self.assertMessageCount(success=1)
|
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):
|
class InstanceAjaxTests(helpers.TestCase):
|
||||||
@helpers.create_stubs({api.nova: ("server_get",
|
@helpers.create_stubs({api.nova: ("server_get",
|
||||||
|
@ -555,24 +555,84 @@ class SetAccessControls(workflows.Step):
|
|||||||
|
|
||||||
|
|
||||||
class CustomizeAction(workflows.Action):
|
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:
|
class Meta:
|
||||||
name = _("Post-Creation")
|
name = _("Post-Creation")
|
||||||
help_text_template = ("project/instances/"
|
help_text_template = ("project/instances/"
|
||||||
"_launch_customize_help.html")
|
"_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):
|
class PostCreationStep(workflows.Step):
|
||||||
action_class = CustomizeAction
|
action_class = CustomizeAction
|
||||||
contributes = ("customization_script",)
|
contributes = ("script_data",)
|
||||||
|
|
||||||
|
|
||||||
class SetNetworkAction(workflows.Action):
|
class SetNetworkAction(workflows.Action):
|
||||||
@ -698,6 +758,7 @@ class LaunchInstance(workflows.Workflow):
|
|||||||
success_message = _('Launched %(count)s named "%(name)s".')
|
success_message = _('Launched %(count)s named "%(name)s".')
|
||||||
failure_message = _('Unable to launch %(count)s named "%(name)s".')
|
failure_message = _('Unable to launch %(count)s named "%(name)s".')
|
||||||
success_url = "horizon:project:instances:index"
|
success_url = "horizon:project:instances:index"
|
||||||
|
multipart = True
|
||||||
default_steps = (SelectProjectUser,
|
default_steps = (SelectProjectUser,
|
||||||
SetInstanceDetails,
|
SetInstanceDetails,
|
||||||
SetAccessControls,
|
SetAccessControls,
|
||||||
@ -716,7 +777,7 @@ class LaunchInstance(workflows.Workflow):
|
|||||||
|
|
||||||
@sensitive_variables('context')
|
@sensitive_variables('context')
|
||||||
def handle(self, request, context):
|
def handle(self, request, context):
|
||||||
custom_script = context.get('customization_script', '')
|
custom_script = context.get('script_data', '')
|
||||||
|
|
||||||
dev_mapping_1 = None
|
dev_mapping_1 = None
|
||||||
dev_mapping_2 = None
|
dev_mapping_2 = None
|
||||||
|
Loading…
Reference in New Issue
Block a user