summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils/azure_rm_common_ext.py
blob: ab3c31c7960ed4b46295c8ff582ef533e55a52c9 (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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# Copyright (c) 2019 Zim Kalinowski, (@zikalino)
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from ansible.module_utils.azure_rm_common import AzureRMModuleBase
import re
from ansible.module_utils.common.dict_transformations import _camel_to_snake, _snake_to_camel
from ansible.module_utils.six import string_types


class AzureRMModuleBaseExt(AzureRMModuleBase):

    def inflate_parameters(self, spec, body, level):
        if isinstance(body, list):
            for item in body:
                self.inflate_parameters(spec, item, level)
            return
        for name in spec.keys():
            # first check if option was passed
            param = body.get(name)
            if param is None:
                if spec[name].get('purgeIfNone', False):
                    body.pop(name, None)
                continue
            # check if pattern needs to be used
            pattern = spec[name].get('pattern', None)
            if pattern:
                if pattern == 'camelize':
                    param = _snake_to_camel(param, True)
                elif isinstance(pattern, list):
                    normalized = None
                    for p in pattern:
                        normalized = self.normalize_resource_id(param, p)
                        body[name] = normalized
                        if normalized is not None:
                            break
                else:
                    param = self.normalize_resource_id(param, pattern)
                    body[name] = param
            disposition = spec[name].get('disposition', '*')
            if level == 0 and not disposition.startswith('/'):
                continue
            if disposition == '/':
                disposition = '/*'
            parts = disposition.split('/')
            if parts[0] == '':
                # should fail if level is > 0?
                parts.pop(0)
            target_dict = body
            elem = body.pop(name)
            while len(parts) > 1:
                target_dict = target_dict.setdefault(parts.pop(0), {})
            targetName = parts[0] if parts[0] != '*' else name
            target_dict[targetName] = elem
            if spec[name].get('options'):
                self.inflate_parameters(spec[name].get('options'), target_dict[targetName], level + 1)

    def normalize_resource_id(self, value, pattern):
        '''
        Return a proper resource id string..

        :param resource_id: It could be a resource name, resource id or dict containing parts from the pattern.
        :param pattern: pattern of resource is, just like in Azure Swagger
        '''
        value_dict = {}
        if isinstance(value, string_types):
            value_parts = value.split('/')
            if len(value_parts) == 1:
                value_dict['name'] = value
            else:
                pattern_parts = pattern.split('/')
                if len(value_parts) != len(pattern_parts):
                    return None
                for i in range(len(value_parts)):
                    if pattern_parts[i].startswith('{'):
                        value_dict[pattern_parts[i][1:-1]] = value_parts[i]
                    elif value_parts[i].lower() != pattern_parts[i].lower():
                        return None
        elif isinstance(value, dict):
            value_dict = value
        else:
            return None
        if not value_dict.get('subscription_id'):
            value_dict['subscription_id'] = self.subscription_id
        if not value_dict.get('resource_group'):
            value_dict['resource_group'] = self.resource_group

        # check if any extra values passed
        for k in value_dict:
            if not ('{' + k + '}') in pattern:
                return None
        # format url
        return pattern.format(**value_dict)

    def idempotency_check(self, old_params, new_params):
        '''
        Return True if something changed. Function will use fields from module_arg_spec to perform dependency checks.
        :param old_params: old parameters dictionary, body from Get request.
        :param new_params: new parameters dictionary, unpacked module parameters.
        '''
        modifiers = {}
        result = {}
        self.create_compare_modifiers(self.module.argument_spec, '', modifiers)
        self.results['modifiers'] = modifiers
        return self.default_compare(modifiers, new_params, old_params, '', self.results)

    def create_compare_modifiers(self, arg_spec, path, result):
        for k in arg_spec.keys():
            o = arg_spec[k]
            updatable = o.get('updatable', True)
            comparison = o.get('comparison', 'default')
            disposition = o.get('disposition', '*')
            if disposition == '/':
                disposition = '/*'
            p = (path +
                 ('/' if len(path) > 0 else '') +
                 disposition.replace('*', k) +
                 ('/*' if o['type'] == 'list' else ''))
            if comparison != 'default' or not updatable:
                result[p] = {'updatable': updatable, 'comparison': comparison}
            if o.get('options'):
                self.create_compare_modifiers(o.get('options'), p, result)

    def default_compare(self, modifiers, new, old, path, result):
        '''
            Default dictionary comparison.
            This function will work well with most of the Azure resources.
            It correctly handles "location" comparison.

            Value handling:
                - if "new" value is None, it will be taken from "old" dictionary if "incremental_update"
                  is enabled.
            List handling:
                - if list contains "name" field it will be sorted by "name" before comparison is done.
                - if module has "incremental_update" set, items missing in the new list will be copied
                  from the old list

            Warnings:
                If field is marked as non-updatable, appropriate warning will be printed out and
                "new" structure will be updated to old value.

            :modifiers: Optional dictionary of modifiers, where key is the path and value is dict of modifiers
            :param new: New version
            :param old: Old version

            Returns True if no difference between structures has been detected.
            Returns False if difference was detected.
        '''
        if new is None:
            return True
        elif isinstance(new, dict):
            comparison_result = True
            if not isinstance(old, dict):
                result['compare'].append('changed [' + path + '] old dict is null')
                comparison_result = False
            else:
                for k in set(new.keys()) | set(old.keys()):
                    new_item = new.get(k, None)
                    old_item = old.get(k, None)
                    if new_item is None:
                        if isinstance(old_item, dict):
                            new[k] = old_item
                            result['compare'].append('new item was empty, using old [' + path + '][ ' + k + ' ]')
                    elif not self.default_compare(modifiers, new_item, old_item, path + '/' + k, result):
                        comparison_result = False
            return comparison_result
        elif isinstance(new, list):
            comparison_result = True
            if not isinstance(old, list) or len(new) != len(old):
                result['compare'].append('changed [' + path + '] length is different or old value is null')
                comparison_result = False
            else:
                if isinstance(old[0], dict):
                    key = None
                    if 'id' in old[0] and 'id' in new[0]:
                        key = 'id'
                    elif 'name' in old[0] and 'name' in new[0]:
                        key = 'name'
                    else:
                        key = next(iter(old[0]))
                        new = sorted(new, key=lambda x: x.get(key, None))
                        old = sorted(old, key=lambda x: x.get(key, None))
                else:
                    new = sorted(new)
                    old = sorted(old)
                for i in range(len(new)):
                    if not self.default_compare(modifiers, new[i], old[i], path + '/*', result):
                        comparison_result = False
            return comparison_result
        else:
            updatable = modifiers.get(path, {}).get('updatable', True)
            comparison = modifiers.get(path, {}).get('comparison', 'default')
            if comparison == 'ignore':
                return True
            elif comparison == 'default' or comparison == 'sensitive':
                if isinstance(old, string_types) and isinstance(new, string_types):
                    new = new.lower()
                    old = old.lower()
            elif comparison == 'location':
                if isinstance(old, string_types) and isinstance(new, string_types):
                    new = new.replace(' ', '').lower()
                    old = old.replace(' ', '').lower()
            if str(new) != str(old):
                result['compare'].append('changed [' + path + '] ' + str(new) + ' != ' + str(old) + ' - ' + str(comparison))
                if updatable:
                    return False
                else:
                    self.module.warn("property '" + path + "' cannot be updated (" + str(old) + "->" + str(new) + ")")
                    return True
            else:
                return True