diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 98e3fd6847..13b32cddb0 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -140,17 +140,25 @@ class Resource(object): assert issubclass(ResourceClass, Resource) - if not ResourceClass.is_service_available(stack.context): - ex = exception.ResourceTypeUnavailable( - service_name=ResourceClass.default_client_name, - resource_type=definition.resource_type + if not stack.service_check_defer: + ResourceClass._validate_service_availability( + stack.context, + definition.resource_type ) + + return super(Resource, cls).__new__(ResourceClass) + + @classmethod + def _validate_service_availability(cls, context, resource_type): + if not cls.is_service_available(context): + ex = exception.ResourceTypeUnavailable( + service_name=cls.default_client_name, + resource_type=resource_type + ) LOG.info(six.text_type(ex)) raise ex - return super(Resource, cls).__new__(ResourceClass) - def _init_attributes(self): """The method that defines attribute initialization for a resource. @@ -1138,6 +1146,12 @@ class Resource(object): in an overridden validate() such as accessing properties may not work. """ + if self.stack.service_check_defer: + self._validate_service_availability( + self.stack.context, + self.t.resource_type + ) + function.validate(self.t) self.validate_deletion_policy(self.t.deletion_policy()) self.t.update_policy(self.update_policy_schema, diff --git a/heat/engine/service.py b/heat/engine/service.py index b0e3d62190..1dd3085dce 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -292,7 +292,7 @@ class EngineService(service.Service): by the RPC caller. """ - RPC_API_VERSION = '1.23' + RPC_API_VERSION = '1.24' def __init__(self, host, topic): super(EngineService, self).__init__() @@ -995,7 +995,8 @@ class EngineService(service.Service): @context.request_context def validate_template(self, cnxt, template, params=None, files=None, - environment_files=None, show_nested=False): + environment_files=None, show_nested=False, + ignorable_errors=None): """Check the validity of a template. Checks, so far as we can, that a template is valid, and returns @@ -1010,12 +1011,25 @@ class EngineService(service.Service): names included in the files dict :type environment_files: list or None :param show_nested: if True, any nested templates will be checked + :param ignorable_errors: List of error_code to be ignored as part of + validation """ LOG.info(_LI('validate_template')) if template is None: msg = _("No Template provided.") return webob.exc.HTTPBadRequest(explanation=msg) + service_check_defer = False + if ignorable_errors: + invalid_codes = (set(ignorable_errors) - + set(exception.ERROR_CODE_MAP.keys())) + if invalid_codes: + msg = (_("Invalid codes in ignore_errors : %s") % + list(invalid_codes)) + return webob.exc.HTTPBadRequest(explanation=msg) + + service_check_defer = True + env = environment.Environment(params) tmpl = templatem.Template(template, files=files, env=env) try: @@ -1024,10 +1038,12 @@ class EngineService(service.Service): return {'Error': six.text_type(ex)} stack_name = 'dummy' - stack = parser.Stack(cnxt, stack_name, tmpl, strict_validate=False) - stack.resource_validate = False + stack = parser.Stack(cnxt, stack_name, tmpl, + strict_validate=False, + resource_validate=False, + service_check_defer=service_check_defer) try: - stack.validate() + stack.validate(ignorable_errors=ignorable_errors) except exception.StackValidationFailed as ex: return {'Error': six.text_type(ex)} diff --git a/heat/engine/stack.py b/heat/engine/stack.py index caecf889ae..ac78008f94 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -120,7 +120,8 @@ class Stack(collections.Mapping): use_stored_context=False, username=None, nested_depth=0, strict_validate=True, convergence=False, current_traversal=None, tags=None, prev_raw_template_id=None, - current_deps=None, cache_data=None, resource_validate=True): + current_deps=None, cache_data=None, resource_validate=True, + service_check_defer=False): """Initialise the Stack. @@ -193,6 +194,12 @@ class Stack(collections.Mapping): # commonly done in plugin validate() methods self.resource_validate = resource_validate + # service_check_defer can be used to defer the validation of service + # availability for a given resource, which helps to create the resource + # dependency tree completely when respective service is not available, + # especially during template_validate + self.service_check_defer = service_check_defer + if use_stored_context: self.context = self.stored_context() self.context.roles = self.context.clients.client( @@ -671,7 +678,7 @@ class Stack(collections.Mapping): return handler and handler(resource_name) @profiler.trace('Stack.validate', hide_args=False) - def validate(self): + def validate(self, ignorable_errors=None): """Validates the stack.""" # TODO(sdake) Should return line number of invalid reference @@ -706,7 +713,10 @@ class Stack(collections.Mapping): result = res.validate_template() except exception.HeatException as ex: LOG.debug('%s', ex) - raise + if ignorable_errors and ex.error_code in ignorable_errors: + result = None + else: + raise ex except AssertionError: raise except Exception as ex: diff --git a/heat/tests/engine/service/test_service_engine.py b/heat/tests/engine/service/test_service_engine.py index 1c1f41de3f..908a1cb929 100644 --- a/heat/tests/engine/service/test_service_engine.py +++ b/heat/tests/engine/service/test_service_engine.py @@ -40,7 +40,7 @@ class ServiceEngineTest(common.HeatTestCase): def test_make_sure_rpc_version(self): self.assertEqual( - '1.23', + '1.24', service.EngineService.RPC_API_VERSION, ('RPC version is changed, please update this test to new version ' 'and make sure additional test cases are added for RPC APIs ' diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 79512823ad..5c763055e5 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -3128,6 +3128,7 @@ class ResourceAvailabilityTest(common.HeatTestCase): resource_type='UnavailableResourceType') mock_stack = mock.MagicMock() + mock_stack.service_check_defer = False ex = self.assertRaises( exception.ResourceTypeUnavailable, diff --git a/heat/tests/test_validate.py b/heat/tests/test_validate.py index 14d04ab41e..9c8f735a54 100644 --- a/heat/tests/test_validate.py +++ b/heat/tests/test_validate.py @@ -14,6 +14,7 @@ import mock from oslo_messaging.rpc import dispatcher import six +import webob from heat.common import exception from heat.common.i18n import _ @@ -1668,3 +1669,38 @@ class ValidateTest(common.HeatTestCase): t, {}) self.assertEqual(exception.ResourceTypeUnavailable, ex.exc_info[0]) + + def test_validate_with_ignorable_errors(self): + t = template_format.parse( + """ + heat_template_version: 2015-10-15 + resources: + my_instance: + type: AWS::EC2::Instance + """) + engine = service.EngineService('a', 't') + self.mock_is_service_available.return_value = False + + res = dict(engine.validate_template( + self.ctx, + t, + {}, + ignorable_errors=[exception.ResourceTypeUnavailable.error_code])) + expected = {'Description': 'No description', 'Parameters': {}} + self.assertEqual(expected, res) + + def test_validate_with_ignorable_errors_invalid_error_code(self): + engine = service.EngineService('a', 't') + + invalide_error_code = '123456' + invalid_codes = ['99001', invalide_error_code] + res = engine.validate_template( + self.ctx, + mock.MagicMock(), + {}, + ignorable_errors=invalid_codes) + + msg = _("Invalid codes in ignore_errors : %s") % [invalide_error_code] + ex = webob.exc.HTTPBadRequest(explanation=msg) + self.assertIsInstance(res, webob.exc.HTTPBadRequest) + self.assertEqual(ex.explanation, res.explanation)