diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 6751afe54a4..6e62fef711b 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -177,6 +177,7 @@ DEFAULT_CONNECTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'connection_plugins', ' DEFAULT_LOOKUP_PLUGIN_PATH = get_config(p, DEFAULTS, 'lookup_plugins', 'ANSIBLE_LOOKUP_PLUGINS', '~/.ansible/plugins/lookup_plugins:/usr/share/ansible_plugins/lookup_plugins') DEFAULT_VARS_PLUGIN_PATH = get_config(p, DEFAULTS, 'vars_plugins', 'ANSIBLE_VARS_PLUGINS', '~/.ansible/plugins/vars_plugins:/usr/share/ansible_plugins/vars_plugins') DEFAULT_FILTER_PLUGIN_PATH = get_config(p, DEFAULTS, 'filter_plugins', 'ANSIBLE_FILTER_PLUGINS', '~/.ansible/plugins/filter_plugins:/usr/share/ansible_plugins/filter_plugins') +DEFAULT_TEST_PLUGIN_PATH = get_config(p, DEFAULTS, 'test_plugins', 'ANSIBLE_TEST_PLUGINS', '~/.ansible/plugins/test_plugins:/usr/share/ansible_plugins/test_plugins') DEFAULT_STDOUT_CALLBACK = get_config(p, DEFAULTS, 'stdout_callback', 'ANSIBLE_STDOUT_CALLBACK', 'default') CACHE_PLUGIN = get_config(p, DEFAULTS, 'fact_caching', 'ANSIBLE_CACHE_PLUGIN', 'memory') diff --git a/lib/ansible/plugins/__init__.py b/lib/ansible/plugins/__init__.py index 13a3da71776..f8273fbcdad 100644 --- a/lib/ansible/plugins/__init__.py +++ b/lib/ansible/plugins/__init__.py @@ -383,6 +383,13 @@ filter_loader = PluginLoader( 'filter_plugins', ) +test_loader = PluginLoader( + 'TestModule', + 'ansible.plugins.test', + C.DEFAULT_TEST_PLUGIN_PATH, + 'test_plugins' +) + fragment_loader = PluginLoader( 'ModuleDocFragment', 'ansible.utils.module_docs_fragments', diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py index b8b506e508a..fa8d0e5cbbd 100644 --- a/lib/ansible/plugins/filter/core.py +++ b/lib/ansible/plugins/filter/core.py @@ -85,55 +85,6 @@ def to_nice_json(a, *args, **kw): return to_json(a, *args, **kw) return json.dumps(a, indent=4, sort_keys=True, *args, **kw) -def failed(*a, **kw): - ''' Test if task result yields failed ''' - item = a[0] - if type(item) != dict: - raise errors.AnsibleFilterError("|failed expects a dictionary") - rc = item.get('rc',0) - failed = item.get('failed',False) - if rc != 0 or failed: - return True - else: - return False - -def success(*a, **kw): - ''' Test if task result yields success ''' - return not failed(*a, **kw) - -def changed(*a, **kw): - ''' Test if task result yields changed ''' - item = a[0] - if type(item) != dict: - raise errors.AnsibleFilterError("|changed expects a dictionary") - if not 'changed' in item: - changed = False - if ('results' in item # some modules return a 'results' key - and type(item['results']) == list - and type(item['results'][0]) == dict): - for result in item['results']: - changed = changed or result.get('changed', False) - else: - changed = item.get('changed', False) - return changed - -def skipped(*a, **kw): - ''' Test if task result yields skipped ''' - item = a[0] - if type(item) != dict: - raise errors.AnsibleFilterError("|skipped expects a dictionary") - skipped = item.get('skipped', False) - return skipped - -def mandatory(a): - ''' Make a variable mandatory ''' - try: - a - except NameError: - raise errors.AnsibleFilterError('Mandatory variable not defined.') - else: - return a - def bool(a): ''' return a bool for the arg ''' if a is None or type(a) == bool: @@ -153,27 +104,6 @@ def fileglob(pathname): ''' return list of matched files for glob ''' return glob.glob(pathname) -def regex(value='', pattern='', ignorecase=False, match_type='search'): - ''' Expose `re` as a boolean filter using the `search` method by default. - This is likely only useful for `search` and `match` which already - have their own filters. - ''' - if ignorecase: - flags = re.I - else: - flags = 0 - _re = re.compile(pattern, flags=flags) - _bool = __builtins__.get('bool') - return _bool(getattr(_re, match_type, 'search')(value)) - -def match(value, pattern='', ignorecase=False): - ''' Perform a `re.match` returning a boolean ''' - return regex(value, pattern, ignorecase, 'match') - -def search(value, pattern='', ignorecase=False): - ''' Perform a `re.search` returning a boolean ''' - return regex(value, pattern, ignorecase, 'search') - def regex_replace(value='', pattern='', replacement='', ignorecase=False): ''' Perform a `re.sub` returning a string ''' @@ -322,19 +252,6 @@ class FilterModule(object): 'relpath': partial(unicode_wrap, os.path.relpath), 'splitext': partial(unicode_wrap, os.path.splitext), - # failure testing - 'failed' : failed, - 'success' : success, - - # changed testing - 'changed' : changed, - - # skip testing - 'skipped' : skipped, - - # variable existence - 'mandatory': mandatory, - # value as boolean 'bool': bool, @@ -356,9 +273,6 @@ class FilterModule(object): 'fileglob': fileglob, # regex - 'match': match, - 'search': search, - 'regex': regex, 'regex_replace': regex_replace, 'regex_escape': regex_escape, diff --git a/lib/ansible/plugins/test/__init__.py b/lib/ansible/plugins/test/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/plugins/test/core.py b/lib/ansible/plugins/test/core.py new file mode 100644 index 00000000000..cc8c702d754 --- /dev/null +++ b/lib/ansible/plugins/test/core.py @@ -0,0 +1,113 @@ +# (c) 2012, Jeroen Hoekx +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import re +from ansible import errors + +def failed(*a, **kw): + ''' Test if task result yields failed ''' + item = a[0] + if type(item) != dict: + raise errors.AnsibleFilterError("|failed expects a dictionary") + rc = item.get('rc',0) + failed = item.get('failed',False) + if rc != 0 or failed: + return True + else: + return False + +def success(*a, **kw): + ''' Test if task result yields success ''' + return not failed(*a, **kw) + +def changed(*a, **kw): + ''' Test if task result yields changed ''' + item = a[0] + if type(item) != dict: + raise errors.AnsibleFilterError("|changed expects a dictionary") + if not 'changed' in item: + changed = False + if ('results' in item # some modules return a 'results' key + and type(item['results']) == list + and type(item['results'][0]) == dict): + for result in item['results']: + changed = changed or result.get('changed', False) + else: + changed = item.get('changed', False) + return changed + +def skipped(*a, **kw): + ''' Test if task result yields skipped ''' + item = a[0] + if type(item) != dict: + raise errors.AnsibleFilterError("|skipped expects a dictionary") + skipped = item.get('skipped', False) + return skipped + +def mandatory(a): + ''' Make a variable mandatory ''' + try: + a + except NameError: + raise errors.AnsibleFilterError('Mandatory variable not defined.') + else: + return a + +def regex(value='', pattern='', ignorecase=False, match_type='search'): + ''' Expose `re` as a boolean filter using the `search` method by default. + This is likely only useful for `search` and `match` which already + have their own filters. + ''' + if ignorecase: + flags = re.I + else: + flags = 0 + _re = re.compile(pattern, flags=flags) + _bool = __builtins__.get('bool') + return _bool(getattr(_re, match_type, 'search')(value)) + +def match(value, pattern='', ignorecase=False): + ''' Perform a `re.match` returning a boolean ''' + return regex(value, pattern, ignorecase, 'match') + +def search(value, pattern='', ignorecase=False): + ''' Perform a `re.search` returning a boolean ''' + return regex(value, pattern, ignorecase, 'search') + +class TestModule(object): + ''' Ansible core jinja2 tests ''' + + def tests(self): + return { + # failure testing + 'failed' : failed, + 'success' : success, + + # changed testing + 'changed' : changed, + + # skip testing + 'skipped' : skipped, + + # variable existence + 'mandatory': mandatory, + + # regex + 'match': match, + 'search': search, + 'regex': regex, + } diff --git a/lib/ansible/plugins/test/math.py b/lib/ansible/plugins/test/math.py new file mode 100644 index 00000000000..3ac871c4357 --- /dev/null +++ b/lib/ansible/plugins/test/math.py @@ -0,0 +1,36 @@ +# (c) 2014, Brian Coca +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import + +import math +from ansible import errors + +def isnotanumber(x): + try: + return math.isnan(x) + except TypeError: + return False + +class TestModule(object): + ''' Ansible math jinja2 tests ''' + + def tests(self): + return { + # general math + 'isnan': isnotanumber, + } diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index 6158611a308..6bcade22154 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -30,7 +30,7 @@ from jinja2.runtime import StrictUndefined from ansible import constants as C from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleUndefinedVariable -from ansible.plugins import _basedirs, filter_loader, lookup_loader +from ansible.plugins import _basedirs, filter_loader, lookup_loader, test_loader from ansible.template.safe_eval import safe_eval from ansible.template.template import AnsibleJ2Template from ansible.template.vars import AnsibleJ2Vars @@ -57,6 +57,7 @@ class Templar: def __init__(self, loader, shared_loader_obj=None, variables=dict()): self._loader = loader self._filters = None + self._tests = None self._available_variables = variables if loader: @@ -118,11 +119,28 @@ class Templar: self._filters = dict() for fp in plugins: self._filters.update(fp.filters()) + self._filters.update(self._get_tests()) return self._filters.copy() + def _get_tests(self): + ''' + Returns tests plugins, after loading and caching them if need be + ''' + + if self._tests is not None: + return self._tests.copy() + + plugins = [x for x in test_loader.all()] + + self._tests = dict() + for fp in plugins: + self._tests.update(fp.tests()) + + return self._tests.copy() + def _get_extensions(self): - ''' + ''' Return jinja2 extensions to load. If some extensions are set via jinja_extensions in ansible.cfg, we try @@ -277,6 +295,7 @@ class Templar: #FIXME: add tests myenv.filters.update(self._get_filters()) + myenv.tests.update(self._get_tests()) try: t = myenv.from_string(data) diff --git a/lib/ansible/template/safe_eval.py b/lib/ansible/template/safe_eval.py index 26899495044..5e2d1e1fe38 100644 --- a/lib/ansible/template/safe_eval.py +++ b/lib/ansible/template/safe_eval.py @@ -23,7 +23,7 @@ import sys from six.moves import builtins from ansible import constants as C -from ansible.plugins import filter_loader +from ansible.plugins import filter_loader, test_loader def safe_eval(expr, locals={}, include_exceptions=False): ''' @@ -77,7 +77,11 @@ def safe_eval(expr, locals={}, include_exceptions=False): for filter in filter_loader.all(): filter_list.extend(filter.filters().keys()) - CALL_WHITELIST = C.DEFAULT_CALLABLE_WHITELIST + filter_list + test_list = [] + for test in test_loader.all(): + test_list.extend(test.tests().keys()) + + CALL_WHITELIST = C.DEFAULT_CALLABLE_WHITELIST + filter_list + test_list class CleansingNodeVisitor(ast.NodeVisitor): def generic_visit(self, node, inside_call=False):