Merge "Add handle get_file when launch stack from horizon"

This commit is contained in:
Jenkins 2015-12-20 23:53:51 +00:00 committed by Gerrit Code Review
commit fe7e812001
6 changed files with 212 additions and 62 deletions

View File

@ -170,6 +170,26 @@ class AlreadyExists(HorizonException):
return self.msg % self.attrs
@six.python_2_unicode_compatible
class GetFileError(HorizonException):
"""Exception to be raised when the value of get_file did not start with
https:// or http://
"""
def __init__(self, name, resource_type):
self.attrs = {"name": name, "resource": resource_type}
self.msg = _('The value of %(resource)s is %(name)s inside the '
'template. When launching a stack from this interface,'
' the value must start with "http://" or "https://"')
def __repr__(self):
return '<%s name=%r resource_type=%r>' % (self.__class__.__name__,
self.attrs['name'],
self.attrs['resource_type'])
def __str__(self):
return self.msg % self.attrs
class ConfigurationError(HorizonException):
"""Exception to be raised when invalid settings have been provided."""
pass
@ -203,6 +223,7 @@ class HandledException(HorizonException):
UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized'])
UNAUTHORIZED += (NotAuthorized,)
NOT_FOUND = tuple(HORIZON_CONFIG['exceptions']['not_found'])
NOT_FOUND += (GetFileError,)
RECOVERABLE = (AlreadyExists, Conflict, NotAvailable, ServiceCatalogException)
RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable'])

View File

@ -10,9 +10,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from heatclient import client as heat_client
import contextlib
import six
from six.moves.urllib import request
from django.conf import settings
from oslo_serialization import jsonutils
from heatclient import client as heat_client
from heatclient.common import template_format
from heatclient.common import template_utils
from heatclient.common import utils as heat_utils
from horizon import exceptions
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized # noqa
from openstack_dashboard.api import base
@ -82,6 +91,57 @@ def stacks_list(request, marker=None, sort_dir='desc', sort_key='created_at',
return (stacks, has_more_data, has_prev_data)
def _ignore_if(key, value):
if key != 'get_file' and key != 'type':
return True
if not isinstance(value, six.string_types):
return True
if (key == 'type' and
not value.endswith(('.yaml', '.template'))):
return True
return False
def get_template_files(template_data=None, template_url=None):
if template_data:
tpl = template_data
elif template_url:
with contextlib.closing(request.urlopen(template_url)) as u:
tpl = u.read()
else:
return {}, None
if not tpl:
return {}, None
if isinstance(tpl, six.binary_type):
tpl = tpl.decode('utf-8')
template = template_format.parse(tpl)
files = {}
_get_file_contents(template, files)
return files, template
def _get_file_contents(from_data, files):
if not isinstance(from_data, (dict, list)):
return
if isinstance(from_data, dict):
recurse_data = six.itervalues(from_data)
for key, value in six.iteritems(from_data):
if _ignore_if(key, value):
continue
if not value.startswith(('http://', 'https://')):
raise exceptions.GetFileError(value, 'get_file')
if value not in files:
file_content = heat_utils.read_url_content(value)
if template_utils.is_template(file_content):
template = get_template_files(template_url=value)[1]
file_content = jsonutils.dumps(template)
files[value] = file_content
else:
recurse_data = from_data
for value in recurse_data:
_get_file_contents(value, files)
def stack_delete(request, stack_id):
return heatclient(request).stacks.delete(stack_id)

View File

@ -139,17 +139,18 @@ class TemplateForm(forms.SelfHandlingForm):
# 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']
if cleaned['environment_data']:
kwargs['environment'] = cleaned['environment_data']
try:
files, tpl =\
api.heat.get_template_files(cleaned.get('template_data'),
cleaned.get('template_url'))
kwargs['files'] = files
kwargs['template'] = tpl
validated = api.heat.template_validate(self.request, **kwargs)
cleaned['template_validate'] = validated
cleaned['template_validate']['files'] = files
cleaned['template_validate']['template'] = tpl
except Exception as e:
raise forms.ValidationError(six.text_type(e))
@ -209,9 +210,7 @@ class TemplateForm(forms.SelfHandlingForm):
def create_kwargs(self, data):
kwargs = {'parameters': data['template_validate'],
'environment_data': data['environment_data'],
'template_data': data['template_data'],
'template_url': data['template_url']}
'environment_data': data['environment_data']}
if data.get('stack_id'):
kwargs['stack_id'] = data['stack_id']
return kwargs
@ -250,12 +249,6 @@ class CreateStackForm(forms.SelfHandlingForm):
class Meta(object):
name = _('Create Stack')
template_data = forms.CharField(
widget=forms.widgets.HiddenInput,
required=False)
template_url = forms.CharField(
widget=forms.widgets.HiddenInput,
required=False)
environment_data = forms.CharField(
widget=forms.widgets.HiddenInput,
required=False)
@ -375,15 +368,12 @@ class CreateStackForm(forms.SelfHandlingForm):
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'files': json.loads(data.get('parameters')).get('files'),
'template': json.loads(data.get('parameters')).get('template')
}
if data.get('password'):
fields['password'] = data.get('password')
if data.get('template_data'):
fields['template'] = data.get('template_data')
else:
fields['template_url'] = data.get('template_url')
if data.get('environment_data'):
fields['environment'] = data.get('environment_data')
@ -430,19 +420,12 @@ class EditStackForm(CreateStackForm):
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'files': json.loads(data.get('parameters')).get('files'),
'template': json.loads(data.get('parameters')).get('template')
}
if data.get('password'):
fields['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."))
@ -469,13 +452,10 @@ class PreviewStackForm(CreateStackForm):
'timeout_mins': data.get('timeout_mins'),
'disable_rollback': not(data.get('enable_rollback')),
'parameters': dict(params_list),
'files': json.loads(data.get('parameters')).get('files'),
'template': json.loads(data.get('parameters')).get('template')
}
if data.get('template_data'):
fields['template'] = data.get('template_data')
else:
fields['template_url'] = data.get('template_url')
if data.get('environment_data'):
fields['environment'] = data.get('environment_data')

View File

@ -24,6 +24,7 @@ from django.utils import html
from mox3.mox import IsA # noqa
import six
from heatclient.common import template_format as hc_format
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
@ -231,16 +232,18 @@ class StackTests(test.TestCase):
stack = self.stacks.first()
api.heat.template_validate(IsA(http.HttpRequest),
template=template.data) \
files={},
template=hc_format.parse(template.data)) \
.AndReturn(json.loads(template.validate))
api.heat.stack_create(IsA(http.HttpRequest),
stack_name=stack.stack_name,
timeout_mins=60,
disable_rollback=True,
template=template.data,
template=None,
parameters=IsA(dict),
password='password')
password='password',
files=None)
api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
self.tenant.id) \
.AndReturn(self.networks.list())
@ -288,7 +291,8 @@ class StackTests(test.TestCase):
stack = self.stacks.first()
api.heat.template_validate(IsA(http.HttpRequest),
template=template.data,
files={},
template=hc_format.parse(template.data),
environment=environment.data) \
.AndReturn(json.loads(template.validate))
@ -296,10 +300,11 @@ class StackTests(test.TestCase):
stack_name=stack.stack_name,
timeout_mins=60,
disable_rollback=True,
template=template.data,
template=None,
environment=environment.data,
parameters=IsA(dict),
password='password')
password='password',
files=None)
api.neutron.network_list_for_tenant(IsA(http.HttpRequest),
self.tenant.id) \
.AndReturn(self.networks.list())
@ -372,7 +377,8 @@ class StackTests(test.TestCase):
}
}
api.heat.template_validate(IsA(http.HttpRequest),
template=template['data']) \
files={},
template=hc_format.parse(template['data'])) \
.AndReturn(template['validate'])
self.mox.ReplayAll()
@ -449,7 +455,8 @@ class StackTests(test.TestCase):
}
}
api.heat.template_validate(IsA(http.HttpRequest),
template=template['data']) \
files={},
template=hc_format.parse(template['data'])) \
.AndReturn(template['validate'])
self.mox.ReplayAll()
@ -523,20 +530,22 @@ class StackTests(test.TestCase):
stack = self.stacks.first()
api.heat.template_validate(IsA(http.HttpRequest),
template=template['data']) \
files={},
template=hc_format.parse(template['data'])) \
.AndReturn(template['validate'])
api.heat.stack_create(IsA(http.HttpRequest),
stack_name=stack.stack_name,
timeout_mins=60,
disable_rollback=True,
template=template['data'],
template=hc_format.parse(template['data']),
parameters={'param1': 'some string',
'param2': 42,
'param3': '{"key": "value"}',
'param4': 'a,b,c',
'param5': True},
password='password')
password='password',
files={})
self.mox.ReplayAll()
@ -613,7 +622,8 @@ class StackTests(test.TestCase):
stack.id).AndReturn(stack)
# POST template form, validation
api.heat.template_validate(IsA(http.HttpRequest),
template=template.data) \
files={},
template=hc_format.parse(template.data)) \
.AndReturn(json.loads(template.validate))
# GET to edit form
@ -632,8 +642,9 @@ class StackTests(test.TestCase):
'disable_rollback': True,
'timeout_mins': 61,
'password': 'password',
'template': IsA(six.text_type),
'parameters': IsA(dict)
'template': None,
'parameters': IsA(dict),
'files': None
}
api.heat.stack_update(IsA(http.HttpRequest),
stack_id=stack.id,
@ -756,15 +767,17 @@ class StackTests(test.TestCase):
stack = self.stacks.first()
api.heat.template_validate(IsA(http.HttpRequest),
template=template.data) \
files={},
template=hc_format.parse(template.data)) \
.AndReturn(json.loads(template.validate))
api.heat.stack_preview(IsA(http.HttpRequest),
stack_name=stack.stack_name,
timeout_mins=60,
disable_rollback=True,
template=template.data,
parameters=IsA(dict)).AndReturn(stack)
template=None,
parameters=IsA(dict),
files=None).AndReturn(stack)
self.mox.ReplayAll()

View File

@ -179,19 +179,12 @@ class CreateStackView(forms.ModalFormView):
def get_initial(self):
initial = {}
self.load_kwargs(initial)
if 'environment_data' in self.kwargs:
initial['environment_data'] = self.kwargs['environment_data']
if 'parameters' in self.kwargs:
initial['parameters'] = json.dumps(self.kwargs['parameters'])
return initial
def load_kwargs(self, initial):
# load the "passed through" data from template form
for prefix in ('template', 'environment'):
for suffix in ('_data', '_url'):
key = prefix + suffix
if key in self.kwargs:
initial[key] = self.kwargs[key]
def get_form_kwargs(self):
kwargs = super(CreateStackView, self).get_form_kwargs()
if 'parameters' in self.kwargs:

View File

@ -9,10 +9,12 @@
# 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 six
from django.conf import settings
from django.test.utils import override_settings # noqa
from horizon import exceptions
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
@ -211,3 +213,84 @@ class HeatApiTests(test.APITestCase):
**form_data)
from heatclient.v1 import stacks
self.assertIsInstance(returned_stack, stacks.Stack)
def test_get_template_files_with_template_data(self):
tmpl = '''
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
'''
expected_files = {}
files = api.heat.get_template_files(template_data=tmpl)[0]
self.assertEqual(files, expected_files)
def test_get_template_files(self):
tmpl = '''
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
user_data_format: RAW
user_data:
get_file: http://test.example/example
'''
expected_files = {u'http://test.example/example': b'echo "test"'}
url = 'http://test.example/example'
data = b'echo "test"'
self.mox.StubOutWithMock(six.moves.urllib.request, 'urlopen')
six.moves.urllib.request.urlopen(url).AndReturn(
six.BytesIO(data))
self.mox.ReplayAll()
files = api.heat.get_template_files(template_data=tmpl)[0]
self.assertEqual(files, expected_files)
def test_get_template_files_with_template_url(self):
url = 'https://test.example/example.yaml'
data = b'''
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
user_data_format: RAW
user_data:
get_file: http://test.example/example
'''
url2 = 'http://test.example/example'
data2 = b'echo "test"'
expected_files = {'http://test.example/example': b'echo "test"'}
self.mox.StubOutWithMock(six.moves.urllib.request, 'urlopen')
six.moves.urllib.request.urlopen(url).AndReturn(
six.BytesIO(data))
six.moves.urllib.request.urlopen(url2).AndReturn(
six.BytesIO(data2))
self.mox.ReplayAll()
files = api.heat.get_template_files(template_url=url)[0]
self.assertEqual(files, expected_files)
def test_get_template_files_invalid(self):
tmpl = '''
heat_template_version: 2013-05-23
resources:
server1:
type: OS::Nova::Server
properties:
flavor: m1.medium
image: cirros
user_data_format: RAW
user_data:
get_file: file:///example
'''
try:
api.heat.get_template_files(template_data=tmpl)[0]
except exceptions.GetFileError:
self.assertRaises(exceptions.GetFileError)