Merge "Use param_schema and merge strategy for merging"

This commit is contained in:
Jenkins 2016-08-29 07:54:27 +00:00 committed by Gerrit Code Review
commit c1dec1b720
3 changed files with 231 additions and 68 deletions

View File

@ -12,9 +12,12 @@
# under the License.
import collections
from oslo_serialization import jsonutils
import six
from heat.common import environment_format as env_fmt
from heat.common import exception
from heat.common.i18n import _
ALLOWED_PARAM_MERGE_STRATEGIES = (OVERWRITE, MERGE, DEEP_MERGE) = (
'overwrite', 'merge', 'deep_merge')
@ -62,14 +65,69 @@ def merge_map(old, new, deep_merge=False):
not isinstance(v, six.string_types)):
old_v = old.get(k)
old[k] = merge_list(old_v, v) if old_v else v
elif isinstance(v, six.string_types):
old[k] = ''.join([old.get(k, ''), v])
else:
old[k] = v
return old
def parse_param(p_val, p_schema):
try:
if p_schema.type == p_schema.MAP:
if not isinstance(p_val, six.string_types):
p_val = jsonutils.dumps(p_val)
if p_val:
return jsonutils.loads(p_val)
elif not isinstance(p_val, collections.Sequence):
raise ValueError()
except (ValueError, TypeError) as err:
msg = _("Invalid parameter in environment %(s).") % six.text_type(err)
raise ValueError(msg)
return p_val
def merge_parameters(old, new, param_schemata,
merge_strategies):
def param_merge(p_key, p_value, p_schema, deep_merge=False):
p_type = p_schema.type
p_value = parse_param(p_value, p_schema)
if p_type == p_schema.MAP:
old[p_key] = merge_map(old.get(p_key, {}), p_value, deep_merge)
elif p_type == p_schema.LIST:
old[p_key] = merge_list(old.get(p_key), p_value)
elif p_type == p_schema.STRING:
old[p_key] = ''.join([old.get(p_key, ''), p_value])
elif p_type == p_schema.NUMBER:
old[p_key] = old.get(p_key, 0) + p_value
else:
raise exception.InvalidMergeStrategyForParam(strategy=MERGE,
param=p_key)
if not old:
return new
for key, value in new.items():
# if key not in param_schemata ignore it
if key in param_schemata and value:
param_merge_strategy = get_param_merge_strategy(merge_strategies,
key)
if param_merge_strategy == DEEP_MERGE:
param_merge(key, value,
param_schemata[key],
deep_merge=True)
elif param_merge_strategy == MERGE:
param_merge(key, value, param_schemata[key])
else:
old[key] = value
return old
def merge_environments(environment_files, files,
params, param_schemata=None):
params, param_schemata):
"""Merges environment files into the stack input parameters.
If a list of environment files have been specified, this call will
@ -85,6 +143,8 @@ def merge_environments(environment_files, files,
:type files: dict
:param params: parameters describing the stack
:type dict:
:param param_schemata: parameter schema dict
:type param_schemata: dict
"""
if not environment_files:
return
@ -92,8 +152,17 @@ def merge_environments(environment_files, files,
for filename in environment_files:
raw_env = files[filename]
parsed_env = env_fmt.parse(raw_env)
merge_strategies = parsed_env.pop(
env_fmt.PARAMETER_MERGE_STRATEGIES, {})
for section_key, section_value in parsed_env.items():
if section_value:
params[section_key] = merge_map(params[section_key],
section_value,
deep_merge=True)
if section_key in (env_fmt.PARAMETERS,
env_fmt.PARAMETER_DEFAULTS):
params[section_key] = merge_parameters(params[section_key],
section_value,
param_schemata,
merge_strategies)
else:
params[section_key] = merge_map(params[section_key],
section_value)

View File

@ -152,6 +152,11 @@ class ImmutableParameterModified(HeatException):
super(ImmutableParameterModified, self).__init__(**kwargs)
class InvalidMergeStrategyForParam(HeatException):
msg_fmt = _("Invalid merge strategy %(strategy)s for "
"parameter %(param)s.")
class InvalidTemplateAttribute(HeatException):
msg_fmt = _("The Referenced Attribute (%(resource)s %(key)s)"
" is incorrect.")

View File

@ -11,8 +11,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
from heat.common import environment_util as env_util
from heat.tests import common
from heat.tests import utils
class TestEnvironmentUtil(common.HeatTestCase):
@ -51,89 +54,175 @@ class TestEnvironmentUtil(common.HeatTestCase):
class TestMergeEnvironments(common.HeatTestCase):
def test_merge_environments(self):
def setUp(self):
super(TestMergeEnvironments, self).setUp()
self.ctx = utils.dummy_context(tenant_id='stack_service_test_tenant')
# Setup
params = {'parameters': {
'p0': 'CORRECT',
'p1': 'INCORRECT',
'p2': 'INCORRECT'}
self.params = {'parameters': {
'str_value1': "test1",
'str_value2': "test2",
'del_lst_value1': '',
'del_lst_value2': '',
'lst_value1': [],
'lst_value2': [],
'json_value1': {},
'json_value2': {}},
'resource_registry': {}
}
env_1 = '''
{'parameters' : {
'p1': 'CORRECT',
'p2': 'INCORRECT-ENV-1',
}}'''
env_2 = '''
{'parameters': {
'p2': 'CORRECT'
}}'''
files = {'env_1': env_1, 'env_2': env_2}
self.env_1 = {'parameters': {
'str_value1': "string1",
'str_value2': "string2",
'del_lst_value1': '1,2',
'del_lst_value2': '3,4',
'lst_value1': [1, 2],
'lst_value2': [3, 4],
'json_value1': {"1": ["str1", "str2"]},
'json_value2': {"2": ["test1", "test2"]}},
'resource_registry': {
'test::R1': "OS::Heat::RandomString",
'test::R2': "BROKEN"}
}
self.env_2 = {'parameters': {
'str_value1': "string3",
'str_value2': "string4",
'del_lst_value1': '5,6',
'del_lst_value2': '7,8',
'lst_value1': [5, 6],
'lst_value2': [7, 8],
'json_value1': {"3": ["str3", "str4"]},
'json_value2': {"4": ["test3", "test4"]}},
'resource_registry': {
'test::R2': "OS::Heat::None"}
}
class mock_schema(object):
types = (MAP, LIST, STRING, NUMBER) = (
'json', 'comma_delimited_list', 'string', 'number')
def __init__(self, d):
self.__dict__ = d
self.param_schemata = {
'str_value1': mock_schema({
'type': "string"}),
'str_value2': mock_schema({
'type': "string"}),
'del_lst_value1': mock_schema({
'type': "comma_delimited_list"}),
'del_lst_value2': mock_schema({
'type': "comma_delimited_list"}),
'lst_value1': mock_schema({
'type': "comma_delimited_list"}),
'lst_value2': mock_schema({
'type': "comma_delimited_list"}),
'json_value1': mock_schema({
'type': "json"}),
'json_value2': mock_schema({
'type': "json"})
}
def test_merge_envs_with_param_default_merge_strategy(self):
files = {'env_1': json.dumps(self.env_1),
'env_2': json.dumps(self.env_2)}
environment_files = ['env_1', 'env_2']
# Test
env_util.merge_environments(environment_files, files, params)
env_util.merge_environments(environment_files, files, self.params,
self.param_schemata)
# Verify
expected = {'parameters': {
'p0': 'CORRECT',
'p1': 'CORRECT',
'p2': 'CORRECT',
}}
self.assertEqual(expected, params)
'json_value1': {u'3': [u'str3', u'str4']},
'json_value2': {u'4': [u'test3', u'test4']},
'del_lst_value1': '5,6',
'del_lst_value2': '7,8',
'lst_value1': [5, 6],
'lst_value2': [7, 8],
'str_value1': u'string3',
'str_value2': u'string4'},
'resource_registry': {
'test::R1': "OS::Heat::RandomString",
'test::R2': "OS::Heat::None"}}
self.assertEqual(expected, self.params)
def test_merge_environments_deep_merge(self):
# Setup
params = {'parameters': {
'p0': 'CORRECT',
'p1': ['CORRECT1'],
'p2': {'A': ['CORRECT1', 'CORRECT2']},
'p3': 'CORRECT1,CORRECT2'}
}
env_1 = '''
{'parameters' : {
'p1': ['CORRECT2', 'CORRECT3'],
'p2': {'B': ['CORRECT3', 'CORRECT4'],
'C': [CORRECT5]},
'p3': 'CORRECT3,CORRECT4'
}}'''
env_2 = '''
{'parameters': {
'p2': {'C': ['CORRECT6']},
'p3': 'CORRECT5,CORRECT6'
}}'''
files = {'env_1': env_1, 'env_2': env_2}
def test_merge_envs_with_specified_default(self):
merge_strategies = {'default': 'deep_merge'}
self.env_2['parameter_merge_strategies'] = merge_strategies
files = {'env_1': json.dumps(self.env_1),
'env_2': json.dumps(self.env_2)}
environment_files = ['env_1', 'env_2']
# Test
env_util.merge_environments(environment_files, files, params)
env_util.merge_environments(environment_files, files, self.params,
self.param_schemata)
# Verify
# Does not work for comma_delimited_list.
expected = {'parameters': {
'p0': 'CORRECT',
'p1': ['CORRECT1', 'CORRECT2', 'CORRECT3'],
'p2': {'A': ['CORRECT1', 'CORRECT2'],
'B': ['CORRECT3', 'CORRECT4'],
'C': ['CORRECT5', 'CORRECT6']},
'p3': 'CORRECT5,CORRECT6'
}}
self.assertEqual(expected, params)
'json_value1': {u'3': [u'str3', u'str4'],
u'1': [u'str1', u'str2']}, # added
'json_value2': {u'4': [u'test3', u'test4'],
u'2': [u'test1', u'test2']},
'del_lst_value1': '1,2,5,6',
'del_lst_value2': '3,4,7,8',
'lst_value1': [1, 2, 5, 6], # added
'lst_value2': [3, 4, 7, 8],
'str_value1': u'string1string3',
'str_value2': u'string2string4'},
'resource_registry': {
'test::R1': "OS::Heat::RandomString",
'test::R2': "OS::Heat::None"}}
self.assertEqual(expected, self.params)
def test_merge_envs_with_param_specific_merge_strategy(self):
merge_strategies = {
'default': "overwrite",
'lst_value1': "merge",
'json_value1': "deep_merge"}
self.env_2['parameter_merge_strategies'] = merge_strategies
files = {'env_1': json.dumps(self.env_1),
'env_2': json.dumps(self.env_2)}
environment_files = ['env_1', 'env_2']
# Test
env_util.merge_environments(environment_files, files, self.params,
self.param_schemata)
# Verify
expected = {'parameters': {
'json_value1': {u'3': [u'str3', u'str4'],
u'1': [u'str1', u'str2']}, # added
'json_value2': {u'4': [u'test3', u'test4']},
'del_lst_value1': '5,6',
'del_lst_value2': '7,8',
'lst_value1': [1, 2, 5, 6], # added
'lst_value2': [7, 8],
'str_value1': u'string3',
'str_value2': u'string4'},
'resource_registry': {
'test::R1': 'OS::Heat::RandomString',
'test::R2': 'OS::Heat::None'}}
self.assertEqual(expected, self.params)
def test_merge_environments_no_env_files(self):
params = {'parameters': {'p0': 'CORRECT'}}
env_1 = '''
{'parameters' : {
'p0': 'INCORRECT',
}}'''
files = {'env_1': env_1}
files = {'env_1': json.dumps(self.env_1)}
# Test - Should ignore env_1 in files
env_util.merge_environments(None, files, params)
env_util.merge_environments(None, files, self.params,
self.param_schemata)
# Verify
expected = {'parameters': {'p0': 'CORRECT'}}
self.assertEqual(expected, params)
expected = {'parameters': {
'str_value1': "test1",
'str_value2': "test2",
'del_lst_value1': '',
'del_lst_value2': '',
'lst_value1': [],
'lst_value2': [],
'json_value1': {},
'json_value2': {}},
'resource_registry': {}}
self.assertEqual(expected, self.params)