diff --git a/docsite/rst/playbooks_variables.rst b/docsite/rst/playbooks_variables.rst index 82d28c71eef..1e61cecbb01 100644 --- a/docsite/rst/playbooks_variables.rst +++ b/docsite/rst/playbooks_variables.rst @@ -180,6 +180,27 @@ Jinja2 provides a useful 'default' filter, that is often a better approach to fa In the above example, if the variable 'some_variable' is not defined, the value used will be 5, rather than an error being raised. + +.. _omitting_undefined_variables: + +Omitting Undefined Variables and Parameters +------------------------------------------- + +As of Ansible 1.8, it is possible to use the default filter to omit variables and module parameters using the special +`omit` variable:: + + - name: touch files with an optional mode + file: dest={{item.path}} state=touch mode={{item.mode|default(omit)}} + with_items: + - path: /tmp/foo + - path: /tmp/bar + - path: /tmp/baz + mode: "0444" + +For the first two files in the list, the default mode will be determined by the umask of the system as the `mode=` +parameter will not be sent to the file module while the final file will receive the `mode=0444` option. + + .. _list_filters: List Filters diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index a63bf426533..b7bfc7bd3c0 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -46,12 +46,17 @@ import connection from return_data import ReturnData from ansible.callbacks import DefaultRunnerCallbacks, vv from ansible.module_common import ModuleReplacer -from ansible.module_utils.splitter import split_args +from ansible.module_utils.splitter import split_args, unquote from ansible.cache import FactCache from ansible.utils import update_hash module_replacer = ModuleReplacer(strip_comments=False) +try: + from hashlib import md5 as _md5 +except ImportError: + from md5 import md5 as _md5 + HAS_ATFORK=True try: from Crypto.Random import atfork @@ -202,6 +207,7 @@ class Runner(object): self.su_user_var = su_user self.su_user = None self.su_pass = su_pass + self.omit_token = '__omit_place_holder__%s' % _md5(os.urandom(64)).hexdigest() self.vault_pass = vault_pass self.no_log = no_log self.run_once = run_once @@ -623,6 +629,7 @@ class Runner(object): inject['defaults'] = self.default_vars inject['environment'] = self.environment inject['playbook_dir'] = os.path.abspath(self.basedir) + inject['omit'] = self.omit_token # template this one is available, callbacks use this delegate_to = self.module_vars.get('delegate_to') @@ -740,14 +747,6 @@ class Runner(object): if self.su_user_var is not None: self.su_user = template.template(self.basedir, self.su_user_var, inject) - # allow module args to work as a dictionary - # though it is usually a string - new_args = "" - if type(module_args) == dict: - for (k,v) in module_args.iteritems(): - new_args = new_args + "%s='%s' " % (k,v) - module_args = new_args - # module_name may be dynamic (but cannot contain {{ ansible_ssh_user }}) module_name = template.template(self.basedir, module_name, inject) @@ -872,6 +871,11 @@ class Runner(object): if self._early_needs_tmp_path(module_name, handler): tmp = self._make_tmp_path(conn) + # allow module args to work as a dictionary + # though it is usually a string + if isinstance(module_args, dict): + module_args = utils.serialize_args(module_args) + # render module_args and complex_args templates try: # When templating module_args, we need to be careful to ensure @@ -892,6 +896,24 @@ class Runner(object): except jinja2.exceptions.UndefinedError, e: raise errors.AnsibleUndefinedVariable("One or more undefined variables: %s" % str(e)) + # filter omitted arguments out from complex_args + if complex_args: + complex_args = dict(filter(lambda x: x[1] != self.omit_token, complex_args.iteritems())) + + # Filter omitted arguments out from module_args. + # We do this with split_args instead of parse_kv to ensure + # that things are not unquoted/requoted incorrectly + args = split_args(module_args) + final_args = [] + for arg in args: + if '=' in arg: + k,v = arg.split('=', 1) + if unquote(v) != self.omit_token: + final_args.append(arg) + else: + # not a k=v param, append it + final_args.append(arg) + module_args = ' '.join(final_args) result = handler.run(conn, tmp, module_name, module_args, inject, complex_args) # Code for do until feature diff --git a/lib/ansible/runner/filter_plugins/core.py b/lib/ansible/runner/filter_plugins/core.py index 7ca0f937420..61b80bce2c5 100644 --- a/lib/ansible/runner/filter_plugins/core.py +++ b/lib/ansible/runner/filter_plugins/core.py @@ -31,6 +31,7 @@ from distutils.version import LooseVersion, StrictVersion from random import SystemRandom from jinja2.filters import environmentfilter + def to_nice_yaml(*a, **kw): '''Make verbose, human readable yaml''' return yaml.safe_dump(*a, indent=4, allow_unicode=True, default_flow_style=False, **kw) @@ -234,6 +235,7 @@ def rand(environment, end, start=None, step=None): else: raise errors.AnsibleFilterError('random can only be used on sequences and integers') + class FilterModule(object): ''' Ansible core jinja2 filters ''' @@ -306,4 +308,3 @@ class FilterModule(object): # random numbers 'random': rand, } - diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index 131a3e315df..aa0a1b8ee71 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -61,6 +61,7 @@ LOOKUP_REGEX = re.compile(r'lookup\s*\(') PRINT_CODE_REGEX = re.compile(r'(?:{[{%]|[%}]})') CODE_REGEX = re.compile(r'(?:{%|%})') + try: import json except ImportError: @@ -110,6 +111,7 @@ try: except ImportError: pass + ############################################################### # Abstractions around keyczar ############################################################### @@ -543,6 +545,18 @@ def parse_json(raw_data, from_remote=False, from_inventory=False): return results +def serialize_args(args): + ''' + Flattens a dictionary args to a k=v string + ''' + module_args = "" + for (k,v) in args.iteritems(): + if isinstance(v, basestring): + module_args = "%s=%s %s" % (k, pipes.quote(v), module_args) + elif isinstance(v, bool): + module_args = "%s=%s %s" % (k, str(v), module_args) + return module_args.strip() + def merge_module_args(current_args, new_args): ''' merges either a dictionary or string of k=v pairs with another string of k=v pairs, @@ -557,14 +571,7 @@ def merge_module_args(current_args, new_args): elif isinstance(new_args, basestring): new_args_kv = parse_kv(new_args) final_args.update(new_args_kv) - # then we re-assemble into a string - module_args = "" - for (k,v) in final_args.iteritems(): - if isinstance(v, basestring): - module_args = "%s=%s %s" % (k, pipes.quote(v), module_args) - elif isinstance(v, bool): - module_args = "%s=%s %s" % (k, str(v), module_args) - return module_args.strip() + return serialize_args(final_args) def parse_yaml(data, path_hint=None): ''' convert a yaml string to a data structure. Also supports JSON, ssssssh!!!''' diff --git a/test/integration/roles/test_good_parsing/tasks/main.yml b/test/integration/roles/test_good_parsing/tasks/main.yml index 6461d8e7728..27475ce0f53 100644 --- a/test/integration/roles/test_good_parsing/tasks/main.yml +++ b/test/integration/roles/test_good_parsing/tasks/main.yml @@ -172,3 +172,33 @@ assert: that: - nested_include_var is undefined + +- name: test omit in complex args + set_fact: + foo: bar + spam: "{{ omit }}" + should_not_omit: "prefix{{ omit }}" + +- assert: + that: + - foo == 'bar' + - spam is undefined + - should_not_omit is defined + +- name: test omit in module args + set_fact: > + yo=whatsup + eggs="{{ omit }}" + default_omitted="{{ not_exists|default(omit) }}" + should_not_omit_1="prefix{{ omit }}" + should_not_omit_2="{{ omit }}suffix" + should_not_omit_3="__omit_place_holder__afb6b9bc3d20bfeaa00a1b23a5930f89" + +- assert: + that: + - yo == 'whatsup' + - eggs is undefined + - default_omitted is undefined + - should_not_omit_1 is defined + - should_not_omit_2 is defined + - should_not_omit_3 == "__omit_place_holder__afb6b9bc3d20bfeaa00a1b23a5930f89"