Add template validation support to orchestration

This patch adds the support to validate stack templates based on V1
API[1]. Due to the design of the API, it is not like any of a regular
resource you will operate in other services. We have to live with it.

[1]
http://developer.openstack.org/api-ref/orchestration/v1/index.html?expanded=#validate-template

Change-Id: I2277943d2413459d84a0041ef6eeb2506b2611a6
This commit is contained in:
tengqm 2016-08-05 05:19:55 -04:00
parent 49db6006dd
commit 71a0d1615d
4 changed files with 209 additions and 0 deletions

View File

@ -10,10 +10,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import exceptions
from openstack.orchestration.v1 import resource as _resource
from openstack.orchestration.v1 import software_config as _sc
from openstack.orchestration.v1 import software_deployment as _sd
from openstack.orchestration.v1 import stack as _stack
from openstack.orchestration.v1 import template as _template
from openstack import proxy2
@ -249,3 +251,33 @@ class Proxy(proxy2.BaseProxy):
"""
return self._update(_sd.SoftwareDeployment, software_deployment,
**attrs)
def validate_template(self, template, environment=None, template_url=None,
ignore_errors=None):
"""Validates a template.
:param template: The stack template on which the validation is
performed.
:param environment: A JSON environment for the stack, if provided.
:param template_url: A URI to the location containing the stack
template for validation. This parameter is only
required if the ``template`` parameter is None.
This parameter is ignored if ``template`` is
specified.
:param ignore_errors: A string containing comma separated error codes
to ignore. Currently the only valid error code
is '99001'.
:returns: The result of template validation.
:raises: :class:`~openstack.exceptions.InvalidRequest` if neither
`template` not `template_url` is provided.
:raises: :class:`~openstack.exceptions.HttpException` if the template
fails the validation.
"""
if template is None and template_url is None:
raise exceptions.InvalidRequest(
"'template_url' must be specified when template is None")
tmpl = _template.Template.new()
return tmpl.validate(self.session, template, environment=environment,
template_url=template_url,
ignore_errors=ignore_errors)

View File

@ -0,0 +1,52 @@
# 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.
from six.moves.urllib import parse
from openstack.orchestration import orchestration_service
from openstack import resource2 as resource
class Template(resource.Resource):
service = orchestration_service.OrchestrationService()
# capabilities
allow_create = False
allow_list = False
allow_retrieve = False
allow_delete = False
allow_update = False
# Properties
#: The description specified in the template
description = resource.Body('Description')
#: Key and value pairs that contain template parameters
parameters = resource.Body('Parameters', type=dict)
#: A list of parameter groups each contains a lsit of parameter names.
parameter_groups = resource.Body('ParameterGroups', type=list)
def validate(self, session, template, environment=None, template_url=None,
ignore_errors=None):
url = '/validate'
body = {'template': template}
if environment is not None:
body['environment'] = environment
if template_url is not None:
body['template_url'] = template_url
if ignore_errors:
qry = parse.urlencode({'ignore_errors': ignore_errors})
url = '?'.join([url, qry])
resp = session.post(url, endpoint_filter=self.service, json=body)
self._translate_response(resp)
return self

View File

@ -19,6 +19,7 @@ from openstack.orchestration.v1 import resource
from openstack.orchestration.v1 import software_config as sc
from openstack.orchestration.v1 import software_deployment as sd
from openstack.orchestration.v1 import stack
from openstack.orchestration.v1 import template
from openstack.tests.unit import test_proxy_base2
@ -131,3 +132,24 @@ class TestOrchestrationProxy(test_proxy_base2.TestProxyBase):
sd.SoftwareDeployment, True)
self.verify_delete(self.proxy.delete_software_deployment,
sd.SoftwareDeployment, False)
@mock.patch.object(template.Template, 'validate')
def test_validate_template(self, mock_validate):
tmpl = mock.Mock()
env = mock.Mock()
tmpl_url = 'A_URI'
ignore_errors = 'a_string'
res = self.proxy.validate_template(tmpl, env, tmpl_url, ignore_errors)
mock_validate.assert_called_once_with(
self.proxy.session, tmpl, environment=env, template_url=tmpl_url,
ignore_errors=ignore_errors)
self.assertEqual(mock_validate.return_value, res)
def test_validate_template_invalid_request(self):
err = self.assertRaises(exceptions.InvalidRequest,
self.proxy.validate_template,
None, template_url=None)
self.assertEqual("'template_url' must be specified when template is "
"None", six.text_type(err))

View File

@ -0,0 +1,103 @@
# 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 mock
import testtools
from openstack.orchestration.v1 import template
from openstack import resource2 as resource
FAKE = {
'Description': 'Blah blah',
'Parameters': {
'key_name': {
'type': 'string'
}
},
'ParameterGroups': [{
'label': 'Group 1',
'parameters': ['key_name']
}]
}
class TestTemplate(testtools.TestCase):
def test_basic(self):
sot = template.Template()
self.assertEqual('orchestration', sot.service.service_type)
self.assertFalse(sot.allow_create)
self.assertFalse(sot.allow_get)
self.assertFalse(sot.allow_update)
self.assertFalse(sot.allow_delete)
self.assertFalse(sot.allow_list)
def test_make_it(self):
sot = template.Template(**FAKE)
self.assertEqual(FAKE['Description'], sot.description)
self.assertEqual(FAKE['Parameters'], sot.parameters)
self.assertEqual(FAKE['ParameterGroups'], sot.parameter_groups)
@mock.patch.object(resource.Resource, '_translate_response')
def test_validate(self, mock_translate):
sess = mock.Mock()
sot = template.Template()
tmpl = mock.Mock()
body = {'template': tmpl}
sot.validate(sess, tmpl)
sess.post.assert_called_once_with(
'/validate', endpoint_filter=sot.service, json=body)
mock_translate.assert_called_once_with(sess.post.return_value)
@mock.patch.object(resource.Resource, '_translate_response')
def test_validate_with_env(self, mock_translate):
sess = mock.Mock()
sot = template.Template()
tmpl = mock.Mock()
env = mock.Mock()
body = {'template': tmpl, 'environment': env}
sot.validate(sess, tmpl, environment=env)
sess.post.assert_called_once_with(
'/validate', endpoint_filter=sot.service, json=body)
mock_translate.assert_called_once_with(sess.post.return_value)
@mock.patch.object(resource.Resource, '_translate_response')
def test_validate_with_template_url(self, mock_translate):
sess = mock.Mock()
sot = template.Template()
template_url = 'http://host1'
body = {'template': None, 'template_url': template_url}
sot.validate(sess, None, template_url=template_url)
sess.post.assert_called_once_with(
'/validate', endpoint_filter=sot.service, json=body)
mock_translate.assert_called_once_with(sess.post.return_value)
@mock.patch.object(resource.Resource, '_translate_response')
def test_validate_with_ignore_errors(self, mock_translate):
sess = mock.Mock()
sot = template.Template()
tmpl = mock.Mock()
body = {'template': tmpl}
sot.validate(sess, tmpl, ignore_errors='123,456')
sess.post.assert_called_once_with(
'/validate?ignore_errors=123%2C456',
endpoint_filter=sot.service, json=body)
mock_translate.assert_called_once_with(sess.post.return_value)