1# Copyright (c) 2018 Cisco and/or its affiliates.
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17#
18import re
19
20from ansible.module_utils._text import to_text
21from ansible.module_utils.common.collections import is_string
22from ansible.module_utils.six import iteritems
23
24INVALID_IDENTIFIER_SYMBOLS = r'[^a-zA-Z0-9_]'
25
26IDENTITY_PROPERTIES = ['id', 'version', 'ruleId']
27NON_COMPARABLE_PROPERTIES = IDENTITY_PROPERTIES + ['isSystemDefined', 'links']
28
29
30class HTTPMethod:
31    GET = 'get'
32    POST = 'post'
33    PUT = 'put'
34    DELETE = 'delete'
35
36
37class ResponseParams:
38    SUCCESS = 'success'
39    STATUS_CODE = 'status_code'
40    RESPONSE = 'response'
41
42
43class FtdConfigurationError(Exception):
44    def __init__(self, msg, obj=None):
45        super(FtdConfigurationError, self).__init__(msg)
46        self.msg = msg
47        self.obj = obj
48
49
50class FtdServerError(Exception):
51    def __init__(self, response, code):
52        super(FtdServerError, self).__init__(response)
53        self.response = response
54        self.code = code
55
56
57class FtdUnexpectedResponse(Exception):
58    """The exception to be raised in case of unexpected responses from 3d parties."""
59    pass
60
61
62def construct_ansible_facts(response, params):
63    facts = dict()
64    if response:
65        response_body = response['items'] if 'items' in response else response
66        if params.get('register_as'):
67            facts[params['register_as']] = response_body
68        elif type(response_body) is dict and response_body.get('name') and response_body.get('type'):
69            object_name = re.sub(INVALID_IDENTIFIER_SYMBOLS, '_', response_body['name'].lower())
70            fact_name = '%s_%s' % (response_body['type'], object_name)
71            facts[fact_name] = response_body
72    return facts
73
74
75def copy_identity_properties(source_obj, dest_obj):
76    for property_name in IDENTITY_PROPERTIES:
77        if property_name in source_obj:
78            dest_obj[property_name] = source_obj[property_name]
79    return dest_obj
80
81
82def is_object_ref(d):
83    """
84    Checks if a dictionary is a reference object. The dictionary is considered to be a
85    reference object when it contains non-empty 'id' and 'type' fields.
86
87    :type d: dict
88    :return: True if passed dictionary is a reference object, otherwise False
89    """
90    has_id = 'id' in d.keys() and d['id']
91    has_type = 'type' in d.keys() and d['type']
92    return has_id and has_type
93
94
95def equal_object_refs(d1, d2):
96    """
97    Checks whether two references point to the same object.
98
99    :type d1: dict
100    :type d2: dict
101    :return: True if passed references point to the same object, otherwise False
102    """
103    have_equal_ids = d1['id'] == d2['id']
104    have_equal_types = d1['type'] == d2['type']
105    return have_equal_ids and have_equal_types
106
107
108def equal_lists(l1, l2):
109    """
110    Checks whether two lists are equal. The order of elements in the arrays is important.
111
112    :type l1: list
113    :type l2: list
114    :return: True if passed lists, their elements and order of elements are equal. Otherwise, returns False.
115    """
116    if len(l1) != len(l2):
117        return False
118
119    for v1, v2 in zip(l1, l2):
120        if not equal_values(v1, v2):
121            return False
122
123    return True
124
125
126def equal_dicts(d1, d2, compare_by_reference=True):
127    """
128    Checks whether two dictionaries are equal. If `compare_by_reference` is set to True, dictionaries referencing
129    objects are compared using `equal_object_refs` method. Otherwise, every key and value is checked.
130
131    :type d1: dict
132    :type d2: dict
133    :param compare_by_reference: if True, dictionaries referencing objects are compared using `equal_object_refs` method
134    :return: True if passed dicts are equal. Otherwise, returns False.
135    """
136    if compare_by_reference and is_object_ref(d1) and is_object_ref(d2):
137        return equal_object_refs(d1, d2)
138
139    if len(d1) != len(d2):
140        return False
141
142    for key, v1 in d1.items():
143        if key not in d2:
144            return False
145
146        v2 = d2[key]
147        if not equal_values(v1, v2):
148            return False
149
150    return True
151
152
153def equal_values(v1, v2):
154    """
155    Checks whether types and content of two values are the same. In case of complex objects, the method might be
156    called recursively.
157
158    :param v1: first value
159    :param v2: second value
160    :return: True if types and content of passed values are equal. Otherwise, returns False.
161    :rtype: bool
162    """
163
164    # string-like values might have same text but different types, so checking them separately
165    if is_string(v1) and is_string(v2):
166        return to_text(v1) == to_text(v2)
167
168    if type(v1) != type(v2):
169        return False
170    value_type = type(v1)
171
172    if value_type == list:
173        return equal_lists(v1, v2)
174    elif value_type == dict:
175        return equal_dicts(v1, v2)
176    else:
177        return v1 == v2
178
179
180def equal_objects(d1, d2):
181    """
182    Checks whether two objects are equal. Ignores special object properties (e.g. 'id', 'version') and
183    properties with None and empty values. In case properties contains a reference to the other object,
184    only object identities (ids and types) are checked. Also, if an array field contains multiple references
185    to the same object, duplicates are ignored when comparing objects.
186
187    :type d1: dict
188    :type d2: dict
189    :return: True if passed objects and their properties are equal. Otherwise, returns False.
190    """
191
192    def prepare_data_for_comparison(d):
193        d = dict((k, d[k]) for k in d.keys() if k not in NON_COMPARABLE_PROPERTIES and d[k])
194        d = delete_ref_duplicates(d)
195        return d
196
197    d1 = prepare_data_for_comparison(d1)
198    d2 = prepare_data_for_comparison(d2)
199    return equal_dicts(d1, d2, compare_by_reference=False)
200
201
202def delete_ref_duplicates(d):
203    """
204    Removes reference duplicates from array fields: if an array contains multiple items and some of
205    them refer to the same object, only unique references are preserved (duplicates are removed).
206
207    :param d: dict with data
208    :type d: dict
209    :return: dict without reference duplicates
210    """
211
212    def delete_ref_duplicates_from_list(refs):
213        if all(type(i) == dict and is_object_ref(i) for i in refs):
214            unique_refs = set()
215            unique_list = list()
216            for i in refs:
217                key = (i['id'], i['type'])
218                if key not in unique_refs:
219                    unique_refs.add(key)
220                    unique_list.append(i)
221
222            return list(unique_list)
223
224        else:
225            return refs
226
227    if not d:
228        return d
229
230    modified_d = {}
231    for k, v in iteritems(d):
232        if type(v) == list:
233            modified_d[k] = delete_ref_duplicates_from_list(v)
234        elif type(v) == dict:
235            modified_d[k] = delete_ref_duplicates(v)
236        else:
237            modified_d[k] = v
238    return modified_d
239