diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index aefd38e12..3460a8607 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -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) diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py new file mode 100644 index 000000000..be2752dd5 --- /dev/null +++ b/openstack/orchestration/v1/template.py @@ -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 diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index eb43a4959..8d2ffc890 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -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)) diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py new file mode 100644 index 000000000..c92b0ae79 --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -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)