summaryrefslogtreecommitdiff
path: root/heat/common/environment_util.py
blob: a07015f0846a7da6a3ca8a028b8e30ca7db8942b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#
#    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 collections

from oslo_serialization import jsonutils

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')


def get_param_merge_strategy(merge_strategies, param_key,
                             available_strategies=None):
    if not available_strategies:
        available_strategies = {}

    if merge_strategies is None:
        return OVERWRITE

    env_default = merge_strategies.get('default', OVERWRITE)
    merge_strategy = merge_strategies.get(
        param_key, available_strategies.get(
            param_key, env_default))

    if merge_strategy in ALLOWED_PARAM_MERGE_STRATEGIES:
        return merge_strategy

    return env_default


def merge_list(old, new):
    """merges lists and comma delimited lists."""
    if not old:
        return new

    if isinstance(new, list):
        old.extend(new)
        return old
    else:
        return ','.join([old, new])


def merge_map(old, new, deep_merge=False):
    """Merge nested dictionaries."""
    if not old:
        return new

    for k, v in new.items():
        if v is not None:
            if not deep_merge:
                old[k] = v
            elif isinstance(v, collections.abc.Mapping):
                old_v = old.get(k)
                old[k] = merge_map(old_v, v, deep_merge) if old_v else v
            elif (isinstance(v, collections.abc.Sequence) and
                    not isinstance(v, str)):
                old_v = old.get(k)
                old[k] = merge_list(old_v, v) if old_v else v
            elif isinstance(v, str):
                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, str):
                p_val = jsonutils.dumps(p_val)
            if p_val:
                return jsonutils.loads(p_val)
        elif not isinstance(p_val, collections.abc.Sequence):
            raise ValueError()
    except (ValueError, TypeError) as err:
        msg = _("Invalid parameter in environment %s.") % str(err)
        raise ValueError(msg)
    return p_val


def merge_parameters(old, new, param_schemata, strategies_in_file,
                     available_strategies, env_file):

    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)

    for key, value in new.items():
        # if key not in param_schemata ignore it
        if key in param_schemata and value is not None:
            param_merge_strategy = get_param_merge_strategy(
                strategies_in_file, key, available_strategies)
            if key not in available_strategies:
                available_strategies[key] = param_merge_strategy

            elif param_merge_strategy != available_strategies[key]:
                raise exception.ConflictingMergeStrategyForParam(
                    strategy=param_merge_strategy,
                    param=key, env_file=env_file)

    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 is not None:
            param_merge_strategy = available_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):
    """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  params: dict
    :param param_schemata: parameter schema dict
    :type  param_schemata: dict
    """
    if not environment_files:
        return

    available_strategies = {}

    for filename in environment_files:
        raw_env = files[filename]
        parsed_env = env_fmt.parse(raw_env)
        strategies_in_file = parsed_env.pop(
            env_fmt.PARAMETER_MERGE_STRATEGIES, {})

        for section_key, section_value in parsed_env.items():
            if section_value:
                if section_key in (env_fmt.PARAMETERS,
                                   env_fmt.PARAMETER_DEFAULTS):
                    params[section_key] = merge_parameters(
                        params[section_key], section_value,
                        param_schemata, strategies_in_file,
                        available_strategies, filename)
                else:
                    params[section_key] = merge_map(params[section_key],
                                                    section_value)