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