1# Copyright 2014, Brian Coca <bcoca@ansible.com>
2# Copyright 2017, Ken Celenza <ken@networktocode.com>
3# Copyright 2017, Jason Edelman <jason@networktocode.com>
4# Copyright 2017, Ansible Project
5#
6# This file is part of Ansible
7#
8# Ansible is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# Ansible is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
20
21# Make coding more python3-ish
22from __future__ import (absolute_import, division, print_function)
23__metaclass__ = type
24
25
26import itertools
27import math
28
29from jinja2.filters import environmentfilter
30
31from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError
32from ansible.module_utils.common.text import formatters
33from ansible.module_utils.six import binary_type, text_type
34from ansible.module_utils.six.moves import zip, zip_longest
35from ansible.module_utils.common._collections_compat import Hashable, Mapping, Iterable
36from ansible.module_utils._text import to_native, to_text
37from ansible.utils.display import Display
38
39try:
40    from jinja2.filters import do_unique
41    HAS_UNIQUE = True
42except ImportError:
43    HAS_UNIQUE = False
44
45try:
46    from jinja2.filters import do_max, do_min
47    HAS_MIN_MAX = True
48except ImportError:
49    HAS_MIN_MAX = False
50
51display = Display()
52
53
54@environmentfilter
55def unique(environment, a, case_sensitive=False, attribute=None):
56
57    def _do_fail(e):
58        if case_sensitive or attribute:
59            raise AnsibleFilterError("Jinja2's unique filter failed and we cannot fall back to Ansible's version "
60                                     "as it does not support the parameters supplied", orig_exc=e)
61
62    error = e = None
63    try:
64        if HAS_UNIQUE:
65            c = list(do_unique(environment, a, case_sensitive=case_sensitive, attribute=attribute))
66    except TypeError as e:
67        error = e
68        _do_fail(e)
69    except Exception as e:
70        error = e
71        _do_fail(e)
72        display.warning('Falling back to Ansible unique filter as Jinja2 one failed: %s' % to_text(e))
73
74    if not HAS_UNIQUE or error:
75
76        # handle Jinja2 specific attributes when using Ansible's version
77        if case_sensitive or attribute:
78            raise AnsibleFilterError("Ansible's unique filter does not support case_sensitive nor attribute parameters, "
79                                     "you need a newer version of Jinja2 that provides their version of the filter.")
80
81        c = []
82        for x in a:
83            if x not in c:
84                c.append(x)
85
86    return c
87
88
89@environmentfilter
90def intersect(environment, a, b):
91    if isinstance(a, Hashable) and isinstance(b, Hashable):
92        c = set(a) & set(b)
93    else:
94        c = unique(environment, [x for x in a if x in b])
95    return c
96
97
98@environmentfilter
99def difference(environment, a, b):
100    if isinstance(a, Hashable) and isinstance(b, Hashable):
101        c = set(a) - set(b)
102    else:
103        c = unique(environment, [x for x in a if x not in b])
104    return c
105
106
107@environmentfilter
108def symmetric_difference(environment, a, b):
109    if isinstance(a, Hashable) and isinstance(b, Hashable):
110        c = set(a) ^ set(b)
111    else:
112        isect = intersect(environment, a, b)
113        c = [x for x in union(environment, a, b) if x not in isect]
114    return c
115
116
117@environmentfilter
118def union(environment, a, b):
119    if isinstance(a, Hashable) and isinstance(b, Hashable):
120        c = set(a) | set(b)
121    else:
122        c = unique(environment, a + b)
123    return c
124
125
126@environmentfilter
127def min(environment, a, **kwargs):
128    if HAS_MIN_MAX:
129        return do_min(environment, a, **kwargs)
130    else:
131        if kwargs:
132            raise AnsibleFilterError("Ansible's min filter does not support any keyword arguments. "
133                                     "You need Jinja2 2.10 or later that provides their version of the filter.")
134        _min = __builtins__.get('min')
135        return _min(a)
136
137
138@environmentfilter
139def max(environment, a, **kwargs):
140    if HAS_MIN_MAX:
141        return do_max(environment, a, **kwargs)
142    else:
143        if kwargs:
144            raise AnsibleFilterError("Ansible's max filter does not support any keyword arguments. "
145                                     "You need Jinja2 2.10 or later that provides their version of the filter.")
146        _max = __builtins__.get('max')
147        return _max(a)
148
149
150def logarithm(x, base=math.e):
151    try:
152        if base == 10:
153            return math.log10(x)
154        else:
155            return math.log(x, base)
156    except TypeError as e:
157        raise AnsibleFilterTypeError('log() can only be used on numbers: %s' % to_native(e))
158
159
160def power(x, y):
161    try:
162        return math.pow(x, y)
163    except TypeError as e:
164        raise AnsibleFilterTypeError('pow() can only be used on numbers: %s' % to_native(e))
165
166
167def inversepower(x, base=2):
168    try:
169        if base == 2:
170            return math.sqrt(x)
171        else:
172            return math.pow(x, 1.0 / float(base))
173    except (ValueError, TypeError) as e:
174        raise AnsibleFilterTypeError('root() can only be used on numbers: %s' % to_native(e))
175
176
177def human_readable(size, isbits=False, unit=None):
178    ''' Return a human readable string '''
179    try:
180        return formatters.bytes_to_human(size, isbits, unit)
181    except TypeError as e:
182        raise AnsibleFilterTypeError("human_readable() failed on bad input: %s" % to_native(e))
183    except Exception:
184        raise AnsibleFilterError("human_readable() can't interpret following string: %s" % size)
185
186
187def human_to_bytes(size, default_unit=None, isbits=False):
188    ''' Return bytes count from a human readable string '''
189    try:
190        return formatters.human_to_bytes(size, default_unit, isbits)
191    except TypeError as e:
192        raise AnsibleFilterTypeError("human_to_bytes() failed on bad input: %s" % to_native(e))
193    except Exception:
194        raise AnsibleFilterError("human_to_bytes() can't interpret following string: %s" % size)
195
196
197def rekey_on_member(data, key, duplicates='error'):
198    """
199    Rekey a dict of dicts on another member
200
201    May also create a dict from a list of dicts.
202
203    duplicates can be one of ``error`` or ``overwrite`` to specify whether to error out if the key
204    value would be duplicated or to overwrite previous entries if that's the case.
205    """
206    if duplicates not in ('error', 'overwrite'):
207        raise AnsibleFilterError("duplicates parameter to rekey_on_member has unknown value: {0}".format(duplicates))
208
209    new_obj = {}
210
211    if isinstance(data, Mapping):
212        iterate_over = data.values()
213    elif isinstance(data, Iterable) and not isinstance(data, (text_type, binary_type)):
214        iterate_over = data
215    else:
216        raise AnsibleFilterTypeError("Type is not a valid list, set, or dict")
217
218    for item in iterate_over:
219        if not isinstance(item, Mapping):
220            raise AnsibleFilterTypeError("List item is not a valid dict")
221
222        try:
223            key_elem = item[key]
224        except KeyError:
225            raise AnsibleFilterError("Key {0} was not found".format(key))
226        except TypeError as e:
227            raise AnsibleFilterTypeError(to_native(e))
228        except Exception as e:
229            raise AnsibleFilterError(to_native(e))
230
231        # Note: if new_obj[key_elem] exists it will always be a non-empty dict (it will at
232        # minimum contain {key: key_elem}
233        if new_obj.get(key_elem, None):
234            if duplicates == 'error':
235                raise AnsibleFilterError("Key {0} is not unique, cannot correctly turn into dict".format(key_elem))
236            elif duplicates == 'overwrite':
237                new_obj[key_elem] = item
238        else:
239            new_obj[key_elem] = item
240
241    return new_obj
242
243
244class FilterModule(object):
245    ''' Ansible math jinja2 filters '''
246
247    def filters(self):
248        filters = {
249            # general math
250            'min': min,
251            'max': max,
252
253            # exponents and logarithms
254            'log': logarithm,
255            'pow': power,
256            'root': inversepower,
257
258            # set theory
259            'unique': unique,
260            'intersect': intersect,
261            'difference': difference,
262            'symmetric_difference': symmetric_difference,
263            'union': union,
264
265            # combinatorial
266            'product': itertools.product,
267            'permutations': itertools.permutations,
268            'combinations': itertools.combinations,
269
270            # computer theory
271            'human_readable': human_readable,
272            'human_to_bytes': human_to_bytes,
273            'rekey_on_member': rekey_on_member,
274
275            # zip
276            'zip': zip,
277            'zip_longest': zip_longest,
278
279        }
280
281        return filters
282