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:
@@ -10,10 +10,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from openstack import exceptions
|
||||||
from openstack.orchestration.v1 import resource as _resource
|
from openstack.orchestration.v1 import resource as _resource
|
||||||
from openstack.orchestration.v1 import software_config as _sc
|
from openstack.orchestration.v1 import software_config as _sc
|
||||||
from openstack.orchestration.v1 import software_deployment as _sd
|
from openstack.orchestration.v1 import software_deployment as _sd
|
||||||
from openstack.orchestration.v1 import stack as _stack
|
from openstack.orchestration.v1 import stack as _stack
|
||||||
|
from openstack.orchestration.v1 import template as _template
|
||||||
from openstack import proxy2
|
from openstack import proxy2
|
||||||
|
|
||||||
|
|
||||||
@@ -249,3 +251,33 @@ class Proxy(proxy2.BaseProxy):
|
|||||||
"""
|
"""
|
||||||
return self._update(_sd.SoftwareDeployment, software_deployment,
|
return self._update(_sd.SoftwareDeployment, software_deployment,
|
||||||
**attrs)
|
**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)
|
||||||
|
52
openstack/orchestration/v1/template.py
Normal file
52
openstack/orchestration/v1/template.py
Normal 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
|
@@ -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_config as sc
|
||||||
from openstack.orchestration.v1 import software_deployment as sd
|
from openstack.orchestration.v1 import software_deployment as sd
|
||||||
from openstack.orchestration.v1 import stack
|
from openstack.orchestration.v1 import stack
|
||||||
|
from openstack.orchestration.v1 import template
|
||||||
from openstack.tests.unit import test_proxy_base2
|
from openstack.tests.unit import test_proxy_base2
|
||||||
|
|
||||||
|
|
||||||
@@ -131,3 +132,24 @@ class TestOrchestrationProxy(test_proxy_base2.TestProxyBase):
|
|||||||
sd.SoftwareDeployment, True)
|
sd.SoftwareDeployment, True)
|
||||||
self.verify_delete(self.proxy.delete_software_deployment,
|
self.verify_delete(self.proxy.delete_software_deployment,
|
||||||
sd.SoftwareDeployment, False)
|
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))
|
||||||
|
103
openstack/tests/unit/orchestration/v1/test_template.py
Normal file
103
openstack/tests/unit/orchestration/v1/test_template.py
Normal 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)
|
Reference in New Issue
Block a user