horizon/openstack_dashboard/dashboards/project/stacks/forms.py

339 lines
13 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import logging
import re
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables # noqa
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
LOG = logging.getLogger(__name__)
def exception_to_validation_msg(e):
"""Extracts a validation message to display to the user."""
try:
error = json.loads(str(e))
# NOTE(jianingy): if no message exists, we just return 'None'
# and let the caller to deciede what to show
return error['error'].get('message', None)
except Exception:
# NOTE(jianingy): fallback to legacy message parsing approach
# either if error message isn't a json nor the json isn't in
# valid format.
validation_patterns = [
"Remote error: \w* {'Error': '(.*?)'}",
'Remote error: \w* (.*?) \[',
'400 Bad Request\n\nThe server could not comply with the request '
'since it is either malformed or otherwise incorrect.\n\n (.*)',
'(ParserError: .*)'
]
for pattern in validation_patterns:
match = re.search(pattern, str(e))
if match:
return match.group(1)
class TemplateForm(forms.SelfHandlingForm):
class Meta:
name = _('Select Template')
help_text = _('From here you can select a template to launch '
'a stack.')
template_source = forms.ChoiceField(label=_('Template Source'),
choices=[('url', _('URL')),
('file', _('File')),
('raw', _('Direct Input'))],
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'source'}))
template_upload = forms.FileField(
label=_('Template File'),
help_text=_('A local template to upload.'),
widget=forms.FileInput(attrs={'class': 'switched',
'data-switch-on': 'source',
'data-source-file': _('Template File')}),
required=False)
template_url = forms.URLField(
label=_('Template URL'),
help_text=_('An external (HTTP) URL to load the template from.'),
widget=forms.TextInput(attrs={'class': 'switched',
'data-switch-on': 'source',
'data-source-url': _('Template URL')}),
required=False)
template_data = forms.CharField(
label=_('Template Data'),
help_text=_('The raw contents of the template.'),
widget=forms.widgets.Textarea(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-raw': _('Template Data')}),
required=False)
def __init__(self, *args, **kwargs):
self.next_view = kwargs.pop('next_view')
super(TemplateForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned = super(TemplateForm, self).clean()
template_url = cleaned.get('template_url')
template_data = cleaned.get('template_data')
files = self.request.FILES
has_upload = 'template_upload' in files
# Uploaded file handler
if has_upload and not template_url:
log_template_name = self.request.FILES['template_upload'].name
LOG.info('got upload %s' % log_template_name)
tpl = self.request.FILES['template_upload'].read()
if tpl.startswith('{'):
try:
json.loads(tpl)
except Exception as e:
msg = _('There was a problem parsing the template: %s') % e
raise forms.ValidationError(msg)
cleaned['template_data'] = tpl
# URL handler
elif template_url and (has_upload or template_data):
msg = _('Please specify a template using only one source method.')
raise forms.ValidationError(msg)
# Check for raw template input
elif not template_url and not template_data:
msg = _('You must specify a template via one of the '
'available sources.')
raise forms.ValidationError(msg)
# Validate the template and get back the params.
kwargs = {}
if cleaned['template_data']:
kwargs['template'] = cleaned['template_data']
else:
kwargs['template_url'] = cleaned['template_url']
try:
validated = api.heat.template_validate(self.request, **kwargs)
cleaned['template_validate'] = validated
except Exception as e:
msg = exception_to_validation_msg(e)
if not msg:
msg = _('An unknown problem occurred validating the template.')
LOG.exception(msg)
raise forms.ValidationError(msg)
return cleaned
def create_kwargs(self, data):
kwargs = {'parameters': data['template_validate'],
'template_data': data['template_data'],
'template_url': data['template_url']}
if data.get('stack_id'):
kwargs['stack_id'] = data['stack_id']
return kwargs
def handle(self, request, data):
kwargs = self.create_kwargs(data)
# NOTE (gabriel): This is a bit of a hack, essentially rewriting this
# request so that we can chain it as an input to the next view...
# but hey, it totally works.
request.method = 'GET'
return self.next_view.as_view()(request, **kwargs)
class ChangeTemplateForm(TemplateForm):
class Meta:
name = _('Edit Template')
help_text = _('From here you can select a new template to re-launch '
'a stack.')
stack_id = forms.CharField(label=_('Stack ID'),
widget=forms.widgets.HiddenInput,
required=True)
stack_name = forms.CharField(label=_('Stack Name'),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
))
class CreateStackForm(forms.SelfHandlingForm):
param_prefix = '__param_'
class Meta:
name = _('Create Stack')
template_data = forms.CharField(
widget=forms.widgets.HiddenInput,
required=False)
template_url = forms.CharField(
widget=forms.widgets.HiddenInput,
required=False)
parameters = forms.CharField(
widget=forms.widgets.HiddenInput,
required=True)
stack_name = forms.RegexField(
max_length='255',
label=_('Stack Name'),
help_text=_('Name of the stack to create.'),
regex=r"^[a-zA-Z][a-zA-Z0-9_.-]*$",
error_messages={'invalid': _('Name must start with a letter and may '
'only contain letters, numbers, underscores, '
'periods and hyphens.')},
required=True)
timeout_mins = forms.IntegerField(
initial=60,
label=_('Creation Timeout (minutes)'),
help_text=_('Stack creation timeout in minutes.'),
required=True)
enable_rollback = forms.BooleanField(
label=_('Rollback On Failure'),
help_text=_('Enable rollback on create/update failure.'),
required=False)
def __init__(self, *args, **kwargs):
parameters = kwargs.pop('parameters')
# special case: load template data from API, not passed in params
if(kwargs.get('validate_me')):
parameters = kwargs.pop('validate_me')
super(CreateStackForm, self).__init__(*args, **kwargs)
self._build_parameter_fields(parameters)
def _build_parameter_fields(self, template_validate):
self.fields['password'] = forms.CharField(
label=_('Password for user "%s"') % self.request.user.username,
help_text=_('This is required for operations to be performed '
'throughout the lifecycle of the stack'),
required=True,
widget=forms.PasswordInput())
self.help_text = template_validate['Description']
params = template_validate.get('Parameters', {})
for param_key, param in params.items():
field_key = self.param_prefix + param_key
field_args = {
'initial': param.get('Default', None),
'label': param_key,
'help_text': param.get('Description', ''),
'required': param.get('Default', None) is None
}
param_type = param.get('Type', None)
if 'AllowedValues' in param:
choices = map(lambda x: (x, x), param['AllowedValues'])
field_args['choices'] = choices
field = forms.ChoiceField(**field_args)
elif param_type in ('CommaDelimitedList', 'String'):
if 'MinLength' in param:
field_args['min_length'] = int(param['MinLength'])
field_args['required'] = param.get('MinLength', 0) > 0
if 'MaxLength' in param:
field_args['max_length'] = int(param['MaxLength'])
field = forms.CharField(**field_args)
elif param_type == 'Number':
if 'MinValue' in param:
field_args['min_value'] = int(param['MinValue'])
if 'MaxValue' in param:
field_args['max_value'] = int(param['MaxValue'])
field = forms.IntegerField(**field_args)
self.fields[field_key] = field
@sensitive_variables('password')
def handle(self, request, data):
prefix_length = len(self.param_prefix)
params_list = [(k[prefix_length:], v) for (k, v) in data.iteritems()
if k.startswith(self.param_prefix)]
fields = {
'stack_name': data.get('stack_name'),
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'password': data.get('password')
}
if data.get('template_data'):
fields['template'] = data.get('template_data')
else:
fields['template_url'] = data.get('template_url')
try:
api.heat.stack_create(self.request, **fields)
messages.success(request, _("Stack creation started."))
return True
except Exception as e:
msg = exception_to_validation_msg(e)
exceptions.handle(request, msg or _('Stack creation failed.'))
class EditStackForm(CreateStackForm):
class Meta:
name = _('Update Stack Parameters')
stack_id = forms.CharField(label=_('Stack ID'),
widget=forms.widgets.HiddenInput,
required=True)
stack_name = forms.CharField(label=_('Stack Name'),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
))
@sensitive_variables('password')
def handle(self, request, data):
prefix_length = len(self.param_prefix)
params_list = [(k[prefix_length:], v) for (k, v) in data.iteritems()
if k.startswith(self.param_prefix)]
stack_id = data.get('stack_id')
fields = {
'stack_name': data.get('stack_name'),
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'password': data.get('password')
}
# if the user went directly to this form, resubmit the existing
# template data. otherwise, submit what they had from the first form
if data.get('template_data'):
fields['template'] = data.get('template_data')
elif data.get('template_url'):
fields['template_url'] = data.get('template_url')
elif data.get('parameters'):
fields['template'] = data.get('parameters')
try:
api.heat.stack_update(self.request, stack_id=stack_id, **fields)
messages.success(request, _("Stack update started."))
return True
except Exception as e:
msg = exception_to_validation_msg(e)
exceptions.handle(request, msg or _('Stack update failed.'))