diff --git a/heat/common/environment_util.py b/heat/common/environment_util.py index 5ee4f49849..b19a4606d4 100644 --- a/heat/common/environment_util.py +++ b/heat/common/environment_util.py @@ -10,6 +10,9 @@ # 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 collections + +from heat.common import environment_format ALLOWED_PARAM_MERGE_STRATEGIES = (OVERWRITE, MERGE, DEEP_MERGE) = ( 'overwrite', 'merge', 'deep_merge') @@ -27,3 +30,38 @@ def get_param_merge_strategy(merge_strategies, param_key): return merge_strategy return env_default + + +def deep_update(old, new): + '''Merge nested dictionaries.''' + for k, v in new.items(): + if isinstance(v, collections.Mapping): + r = deep_update(old.get(k, {}), v) + old[k] = r + else: + old[k] = new[k] + return old + + +def merge_environments(environment_files, files, params): + """Merges environment files into the stack input parameters. + + If a list of environment files have been specified, this call will + pull the contents of each from the files dict, parse them as + environments, and merge them into the stack input params. This + behavior is the same as earlier versions of the Heat client that + performed this params population client-side. + + :param environment_files: ordered names of the environment files + found in the files dict + :type environment_files: list or None + :param files: mapping of stack filenames to contents + :type files: dict + :param params: parameters describing the stack + :type dict: + """ + if environment_files: + for filename in environment_files: + raw_env = files[filename] + parsed_env = environment_format.parse(raw_env) + deep_update(params, parsed_env) diff --git a/heat/engine/service.py b/heat/engine/service.py index 49788d4da5..ebe1193d9c 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -33,6 +33,7 @@ import webob from heat.common import context from heat.common import environment_format as env_fmt +from heat.common import environment_util as env_util from heat.common import exception from heat.common.i18n import _ from heat.common.i18n import _LE @@ -69,8 +70,6 @@ from heat.objects import watch_data from heat.objects import watch_rule from heat.rpc import api as rpc_api from heat.rpc import worker_api as rpc_worker_api -from heatclient.common import environment_format -from heatclient.common import template_utils cfg.CONF.import_opt('engine_life_check_timeout', 'heat.common.config') cfg.CONF.import_opt('max_resources_per_stack', 'heat.common.config') @@ -680,7 +679,7 @@ class EngineService(service.Service): tmpl = templatem.Template.load(cnxt, template_id) env = tmpl.env else: - self._merge_environments(environment_files, files, params) + env_util.merge_environments(environment_files, files, params) env = environment.Environment(params) tmpl = templatem.Template(template, files=files, env=env) self._validate_new_stack(cnxt, stack_name, tmpl) @@ -701,30 +700,6 @@ class EngineService(service.Service): env.registry.log_resource_info(prefix=stack_name) return stack - @staticmethod - def _merge_environments(environment_files, files, params): - """Merges environment files into the stack input parameters. - - If a list of environment files have been specified, this call will - pull the contents of each from the files dict, parse them as - environments, and merge them into the stack input params. This - behavior is the same as earlier versions of the Heat client that - performed this params population client-side. - - :param environment_files: ordered names of the environment files - found in the files dict - :type environment_files: list or None - :param files: mapping of stack filenames to contents - :type files: dict - :param params: parameters describing the stack - :type dict: - """ - if environment_files: - for filename in environment_files: - raw_env = files[filename] - parsed_env = environment_format.parse(raw_env) - template_utils.deep_update(params, parsed_env) - @context.request_context def preview_stack(self, cnxt, stack_name, template, params, files, args, environment_files=None): @@ -969,7 +944,7 @@ class EngineService(service.Service): :param template_id: the ID of a pre-stored template in the DB """ # Handle server-side environment file resolution - self._merge_environments(environment_files, files, params) + env_util.merge_environments(environment_files, files, params) # Get the database representation of the existing stack db_stack = self._get_stack(cnxt, stack_identity) @@ -1026,7 +1001,7 @@ class EngineService(service.Service): heat-api process if using a template-url. """ # Handle server-side environment file resolution - self._merge_environments(environment_files, files, params) + env_util.merge_environments(environment_files, files, params) # Get the database representation of the existing stack db_stack = self._get_stack(cnxt, stack_identity) @@ -1206,7 +1181,7 @@ class EngineService(service.Service): service_check_defer = True - self._merge_environments(environment_files, files, params) + env_util.merge_environments(environment_files, files, params) env = environment.Environment(params) tmpl = templatem.Template(template, files=files, env=env) try: diff --git a/heat/tests/engine/service/test_service_engine.py b/heat/tests/engine/service/test_service_engine.py index 0aa2d41b3b..8fb3226c1d 100644 --- a/heat/tests/engine/service/test_service_engine.py +++ b/heat/tests/engine/service/test_service_engine.py @@ -411,50 +411,3 @@ class ServiceEngineTest(common.HeatTestCase): self.eng.start() self.assertEqual(cfg.CONF.executor_thread_pool_size, cfg.CONF.database.max_overflow) - - def test_merge_environments(self): - # Setup - params = {'parameters': { - 'p0': 'CORRECT', - 'p1': 'INCORRECT', - 'p2': 'INCORRECT'} - } - env_1 = ''' - {'parameters' : { - 'p1': 'CORRECT', - 'p2': 'INCORRECT-ENV-1', - }}''' - env_2 = ''' - {'parameters': { - 'p2': 'CORRECT' - }}''' - - files = {'env_1': env_1, 'env_2': env_2} - environment_files = ['env_1', 'env_2'] - - # Test - self.eng._merge_environments(environment_files, files, params) - - # Verify - expected = {'parameters': { - 'p0': 'CORRECT', - 'p1': 'CORRECT', - 'p2': 'CORRECT', - }} - self.assertEqual(expected, params) - - def test_merge_environments_no_env_files(self): - params = {'parameters': {'p0': 'CORRECT'}} - env_1 = ''' - {'parameters' : { - 'p0': 'INCORRECT', - }}''' - - files = {'env_1': env_1} - - # Test - Should ignore env_1 in files - self.eng._merge_environments(None, files, params) - - # Verify - expected = {'parameters': {'p0': 'CORRECT'}} - self.assertEqual(expected, params) diff --git a/heat/tests/engine/service/test_stack_create.py b/heat/tests/engine/service/test_stack_create.py index 761b9aea4f..0db72f67ee 100644 --- a/heat/tests/engine/service/test_stack_create.py +++ b/heat/tests/engine/service/test_stack_create.py @@ -16,6 +16,7 @@ from oslo_messaging.rpc import dispatcher from oslo_service import threadgroup import six +from heat.common import environment_util as env_util from heat.common import exception from heat.engine.clients.os import glance from heat.engine.clients.os import nova @@ -54,7 +55,7 @@ class StackCreateTest(common.HeatTestCase): mock_env = self.patchobject(environment, 'Environment', return_value=stk.env) mock_stack = self.patchobject(stack, 'Stack', return_value=stk) - mock_merge = self.patchobject(self.man, '_merge_environments') + mock_merge = self.patchobject(env_util, 'merge_environments') result = self.man.create_stack(self.ctx, stack_name, template, params, None, {}, environment_files=environment_files) diff --git a/heat/tests/engine/service/test_stack_update.py b/heat/tests/engine/service/test_stack_update.py index 4b1b70cae4..49d736788b 100644 --- a/heat/tests/engine/service/test_stack_update.py +++ b/heat/tests/engine/service/test_stack_update.py @@ -18,6 +18,7 @@ from oslo_config import cfg from oslo_messaging.rpc import dispatcher import six +from heat.common import environment_util as env_util from heat.common import exception from heat.common import messaging from heat.common import service_utils @@ -121,7 +122,7 @@ class ServiceStackUpdateTest(common.HeatTestCase): self.patchobject(eventlet.queue, 'LightQueue', return_value=mock.Mock()) - mock_merge = self.patchobject(self.man, '_merge_environments') + mock_merge = self.patchobject(env_util, 'merge_environments') # Test environment_files = ['env_1'] @@ -991,7 +992,7 @@ resources: mock_env = self.patchobject(environment, 'Environment', return_value=stk.env) mock_validate = self.patchobject(stk, 'validate', return_value=None) - mock_merge = self.patchobject(self.man, '_merge_environments') + mock_merge = self.patchobject(env_util, 'merge_environments') # Patch _resolve_all_attributes or it tries to call novaclient self.patchobject(resource.Resource, '_resolve_all_attributes', diff --git a/heat/tests/test_common_env_util.py b/heat/tests/test_common_env_util.py index bfcb8665ea..b8975462d5 100644 --- a/heat/tests/test_common_env_util.py +++ b/heat/tests/test_common_env_util.py @@ -48,3 +48,53 @@ class TestEnvironmentUtil(common.HeatTestCase): param_strategy = env_util.get_param_merge_strategy(merge_strategies, 'param1') self.assertEqual(env_util.OVERWRITE, param_strategy) + + +class TestMergeEnvironments(common.HeatTestCase): + + def test_merge_environments(self): + # Setup + params = {'parameters': { + 'p0': 'CORRECT', + 'p1': 'INCORRECT', + 'p2': 'INCORRECT'} + } + env_1 = ''' + {'parameters' : { + 'p1': 'CORRECT', + 'p2': 'INCORRECT-ENV-1', + }}''' + env_2 = ''' + {'parameters': { + 'p2': 'CORRECT' + }}''' + + files = {'env_1': env_1, 'env_2': env_2} + environment_files = ['env_1', 'env_2'] + + # Test + env_util.merge_environments(environment_files, files, params) + + # Verify + expected = {'parameters': { + 'p0': 'CORRECT', + 'p1': 'CORRECT', + 'p2': 'CORRECT', + }} + self.assertEqual(expected, params) + + def test_merge_environments_no_env_files(self): + params = {'parameters': {'p0': 'CORRECT'}} + env_1 = ''' + {'parameters' : { + 'p0': 'INCORRECT', + }}''' + + files = {'env_1': env_1} + + # Test - Should ignore env_1 in files + env_util.merge_environments(None, files, params) + + # Verify + expected = {'parameters': {'p0': 'CORRECT'}} + self.assertEqual(expected, params) diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 60a1bb0abe..d70dfa2f3b 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -21,6 +21,7 @@ from oslo_serialization import jsonutils as json import six from heat.common import context +from heat.common import environment_util as env_util from heat.common import exception from heat.common import identifier from heat.common import template_format @@ -1165,7 +1166,7 @@ class StackServiceTest(common.HeatTestCase): self._preview_stack) self.assertEqual(exception.StackValidationFailed, ex.exc_info[0]) - @mock.patch.object(service.EngineService, '_merge_environments') + @mock.patch.object(env_util, 'merge_environments') def test_preview_environment_files(self, mock_merge): # Setup environment_files = ['env_1']