diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py index 32767398f0f..f640306fdc3 100644 --- a/lib/ansible/module_common.py +++ b/lib/ansible/module_common.py @@ -18,6 +18,7 @@ REPLACER = "#<>" REPLACER_ARGS = "<>" REPLACER_LANG = "<>" +REPLACER_COMPLEX = "<>" MODULE_COMMON = """ @@ -25,6 +26,7 @@ MODULE_COMMON = """ MODULE_ARGS = <> MODULE_LANG = <> +MODULE_COMPLEX_ARGS = <> BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1] BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0] @@ -559,7 +561,9 @@ class AnsibleModule(object): except: self.fail_json(msg="this module requires key=value arguments") params[k] = v - return (params, args) + params2 = json.loads(MODULE_COMPLEX_ARGS) + params2.update(params) + return (params2, args) def _log_invocation(self): ''' log that ansible ran the module ''' diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index e860146eedc..b28ca698f31 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -273,7 +273,7 @@ class PlayBook(object): conditional=task.only_if, callbacks=self.runner_callbacks, sudo=task.sudo, sudo_user=task.sudo_user, transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True, - check=self.check, diff=self.diff, environment=task.environment + check=self.check, diff=self.diff, environment=task.environment, complex_args=task.args ) if task.async_seconds == 0: diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index c17f5a9446e..9318b56f092 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -27,7 +27,7 @@ class Task(object): 'play', 'notified_by', 'tags', 'register', 'delegate_to', 'first_available_file', 'ignore_errors', 'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass', - 'items_lookup_plugin', 'items_lookup_terms', 'environment' + 'items_lookup_plugin', 'items_lookup_terms', 'environment', 'args' ] # to prevent typos and such @@ -35,7 +35,7 @@ class Task(object): 'name', 'action', 'only_if', 'async', 'poll', 'notify', 'first_available_file', 'include', 'tags', 'register', 'ignore_errors', 'delegate_to', 'local_action', 'transport', 'sudo', 'sudo_user', - 'sudo_pass', 'when', 'connection', 'environment' + 'sudo_pass', 'when', 'connection', 'environment', 'args' ] def __init__(self, play, ds, module_vars=None, additional_conditions=None): @@ -82,6 +82,10 @@ class Task(object): self.sudo = utils.boolean(ds.get('sudo', play.sudo)) self.environment = ds.get('environment', {}) + # rather than simple key=value args on the options line, these represent structured data and the values + # can be hashes and lists, not just scalars + self.args = ds.get('args', {}) + if self.sudo: self.sudo_user = ds.get('sudo_user', play.sudo_user) self.sudo_pass = ds.get('sudo_pass', play.playbook.sudo_pass) diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index 7e7b8e9269a..2d1723e5551 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -29,6 +29,7 @@ import socket import base64 import sys import shlex +import pipes import ansible.constants as C import ansible.inventory @@ -120,9 +121,13 @@ class Runner(object): subset=None, # subset pattern check=False, # don't make any changes, just try to probe for potential changes diff=False, # whether to show diffs for template files that change - environment=None # environment variables (as dict) to use inside the command + environment=None, # environment variables (as dict) to use inside the command + complex_args=None # structured data in addition to module_args, must be a dict ): + if not complex_args: + complex_args = {} + # storage & defaults self.check = check self.diff = diff @@ -151,6 +156,7 @@ class Runner(object): self.sudo_pass = sudo_pass self.is_playbook = is_playbook self.environment = environment + self.complex_args = complex_args # misc housekeeping if subset and self.inventory._subset is None: @@ -168,6 +174,27 @@ class Runner(object): # ensure we are using unique tmp paths random.seed() + + # ***************************************************** + + def _complex_args_hack(self, complex_args, module_args): + """ + ansible-playbook both allows specifying key=value string arguments and complex arguments + however not all modules use our python common module system and cannot + access these. An example might be a Bash module. This hack allows users to still pass "args" + as a hash of simple scalars to those arguments and is short term. We could technically + just feed JSON to the module, but that makes it hard on Bash consumers. The way this is implemented + it does mean values in 'args' have LOWER priority than those on the key=value line, allowing + args to provide yet another way to have pluggable defaults. + """ + if complex_args is None: + return module_args + if type(complex_args) != dict: + raise errors.AnsibleError("complex arguments are not a dictionary: %s" % complex_args) + for (k,v) in complex_args.iteritems(): + if isinstance(v, basestring): + module_args = "%s=%s %s" % (k, pipes.quote(v), module_args) + return module_args # ***************************************************** @@ -212,7 +239,7 @@ class Runner(object): # ***************************************************** def _execute_module(self, conn, tmp, module_name, args, - async_jid=None, async_module=None, async_limit=None, inject=None, persist_files=False): + async_jid=None, async_module=None, async_limit=None, inject=None, persist_files=False, complex_args=None): ''' runs a module that has already been transferred ''' @@ -222,7 +249,7 @@ class Runner(object): if 'port' not in args: args += " port=%s" % C.ZEROMQ_PORT - (remote_module_path, is_new_style, shebang) = self._copy_module(conn, tmp, module_name, args, inject) + (remote_module_path, is_new_style, shebang) = self._copy_module(conn, tmp, module_name, args, inject, complex_args) environment_string = self._compute_environment_string(inject) @@ -364,6 +391,7 @@ class Runner(object): def _executor_internal_inner(self, host, module_name, module_args, inject, port, is_chained=False): ''' decides how to invoke a module ''' + # allow module args to work as a dictionary # though it is usually a string new_args = "" @@ -374,6 +402,7 @@ class Runner(object): module_name = utils.template(self.basedir, module_name, inject) module_args = utils.template(self.basedir, module_args, inject) + if module_name in utils.plugins.action_loader: if self.background != 0: @@ -448,8 +477,8 @@ class Runner(object): # all modules get a tempdir, action plugins get one unless they have NEEDS_TMPPATH set to False if getattr(handler, 'NEEDS_TMPPATH', True): tmp = self._make_tmp_path(conn) - - result = handler.run(conn, tmp, module_name, module_args, inject) + + result = handler.run(conn, tmp, module_name, module_args, inject, self.complex_args) conn.close() @@ -558,9 +587,11 @@ class Runner(object): # ***************************************************** - def _copy_module(self, conn, tmp, module_name, module_args, inject): + def _copy_module(self, conn, tmp, module_name, module_args, inject, complex_args=None): ''' transfer a module over SFTP, does not run it ''' + # FIXME if complex args is none, set to {} + if module_name.startswith("/"): raise errors.AnsibleFileNotFound("%s is not a module" % module_name) @@ -578,11 +609,17 @@ class Runner(object): module_data = f.read() if module_common.REPLACER in module_data: is_new_style=True - module_data = module_data.replace(module_common.REPLACER, module_common.MODULE_COMMON) + + complex_args_json = utils.jsonify(complex_args) encoded_args = "\"\"\"%s\"\"\"" % module_args.replace("\"","\\\"") - module_data = module_data.replace(module_common.REPLACER_ARGS, encoded_args) encoded_lang = "\"\"\"%s\"\"\"" % C.DEFAULT_MODULE_LANG + encoded_complex = "\"\"\"%s\"\"\"" % complex_args_json + + module_data = module_data.replace(module_common.REPLACER, module_common.MODULE_COMMON) + module_data = module_data.replace(module_common.REPLACER_ARGS, encoded_args) module_data = module_data.replace(module_common.REPLACER_LANG, encoded_lang) + module_data = module_data.replace(module_common.REPLACER_COMPLEX, encoded_complex) + if is_new_style: facility = C.DEFAULT_SYSLOG_FACILITY if 'ansible_syslog_facility' in inject: @@ -684,7 +721,9 @@ class Runner(object): # run once per hostgroup, rather than pausing once per each # host. p = utils.plugins.action_loader.get(self.module_name, self) + if p and getattr(p, 'BYPASS_HOST_LOOP', None): + # Expose the current hostgroup to the bypassing plugins self.host_set = hosts # We aren't iterating over all the hosts in this @@ -697,6 +736,7 @@ class Runner(object): results = [ ReturnData(host=h, result=result_data, comm_ok=True) \ for h in hosts ] del self.host_set + elif self.forks > 1: try: results = self._parallel_exec(hosts) diff --git a/lib/ansible/runner/action_plugins/add_host.py b/lib/ansible/runner/action_plugins/add_host.py index 60f953483e2..2589c7f54cf 100644 --- a/lib/ansible/runner/action_plugins/add_host.py +++ b/lib/ansible/runner/action_plugins/add_host.py @@ -34,7 +34,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs) if self.runner.check: return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module')) diff --git a/lib/ansible/runner/action_plugins/async.py b/lib/ansible/runner/action_plugins/async.py index ab56404f652..72266d3a1fe 100644 --- a/lib/ansible/runner/action_plugins/async.py +++ b/lib/ansible/runner/action_plugins/async.py @@ -22,7 +22,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' transfer the given module name, plus the async module, then run it ''' if self.runner.check: diff --git a/lib/ansible/runner/action_plugins/copy.py b/lib/ansible/runner/action_plugins/copy.py index 4eacd42e2de..51682c6188d 100644 --- a/lib/ansible/runner/action_plugins/copy.py +++ b/lib/ansible/runner/action_plugins/copy.py @@ -26,7 +26,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' handler for file transfer operations ''' # load up options diff --git a/lib/ansible/runner/action_plugins/debug.py b/lib/ansible/runner/action_plugins/debug.py index e25d9c27397..2b1c50e385a 100644 --- a/lib/ansible/runner/action_plugins/debug.py +++ b/lib/ansible/runner/action_plugins/debug.py @@ -28,7 +28,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): args = utils.parse_kv(module_args) if not 'msg' in args: args['msg'] = 'Hello world!' diff --git a/lib/ansible/runner/action_plugins/fail.py b/lib/ansible/runner/action_plugins/fail.py index df06b3225e1..ce8cbdd8d3a 100644 --- a/lib/ansible/runner/action_plugins/fail.py +++ b/lib/ansible/runner/action_plugins/fail.py @@ -28,7 +28,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): # note: the fail module does not need to pay attention to check mode # it always runs. diff --git a/lib/ansible/runner/action_plugins/fetch.py b/lib/ansible/runner/action_plugins/fetch.py index 2c0a3b0a4bf..96c77067241 100644 --- a/lib/ansible/runner/action_plugins/fetch.py +++ b/lib/ansible/runner/action_plugins/fetch.py @@ -33,7 +33,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' handler for fetch operations ''' if self.runner.check: diff --git a/lib/ansible/runner/action_plugins/group_by.py b/lib/ansible/runner/action_plugins/group_by.py index aee6b647305..a4f9df67c21 100644 --- a/lib/ansible/runner/action_plugins/group_by.py +++ b/lib/ansible/runner/action_plugins/group_by.py @@ -32,7 +32,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): # the group_by module does not need to pay attention to check mode. # it always runs. diff --git a/lib/ansible/runner/action_plugins/normal.py b/lib/ansible/runner/action_plugins/normal.py index e077e29513c..51d7bb0ffe0 100644 --- a/lib/ansible/runner/action_plugins/normal.py +++ b/lib/ansible/runner/action_plugins/normal.py @@ -33,9 +33,12 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' transfer & execute a module that is not 'copy' or 'template' ''' + complex_args = utils.template(self.runner.basedir, complex_args, inject) + module_args = self.runner._complex_args_hack(complex_args, module_args) + if self.runner.check: if module_name in [ 'shell', 'command' ]: return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for %s' % module_name)) @@ -49,6 +52,6 @@ class ActionModule(object): module_args += " #USE_SHELL" vv("REMOTE_MODULE %s %s" % (module_name, module_args), host=conn.host) - return self.runner._execute_module(conn, tmp, module_name, module_args, inject=inject) + return self.runner._execute_module(conn, tmp, module_name, module_args, inject=inject, complex_args=complex_args) diff --git a/lib/ansible/runner/action_plugins/pause.py b/lib/ansible/runner/action_plugins/pause.py index d6ea53f4a3e..3a7708ce761 100644 --- a/lib/ansible/runner/action_plugins/pause.py +++ b/lib/ansible/runner/action_plugins/pause.py @@ -46,7 +46,7 @@ class ActionModule(object): 'delta': None, } - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' run the pause action module ''' # note: this module does not need to pay attention to the 'check' diff --git a/lib/ansible/runner/action_plugins/raw.py b/lib/ansible/runner/action_plugins/raw.py index c05a2344ed8..94b73c50ea7 100644 --- a/lib/ansible/runner/action_plugins/raw.py +++ b/lib/ansible/runner/action_plugins/raw.py @@ -28,7 +28,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): if self.runner.check: # in --check mode, always skip this module execution diff --git a/lib/ansible/runner/action_plugins/script.py b/lib/ansible/runner/action_plugins/script.py index c6de1a54a0d..3f6b968fe9e 100644 --- a/lib/ansible/runner/action_plugins/script.py +++ b/lib/ansible/runner/action_plugins/script.py @@ -28,7 +28,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' handler for file transfer operations ''' if self.runner.check: diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py index 0569d2099c7..3cd6e602338 100644 --- a/lib/ansible/runner/action_plugins/template.py +++ b/lib/ansible/runner/action_plugins/template.py @@ -27,7 +27,7 @@ class ActionModule(object): def __init__(self, runner): self.runner = runner - def run(self, conn, tmp, module_name, module_args, inject): + def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' handler for template operations ''' # note: since this module just calls the copy module, the --check mode support diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index 50d9f15c5f7..f419b65541c 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -118,6 +118,8 @@ def exit(msg, rc=1): def jsonify(result, format=False): ''' format JSON output (uncompressed or uncompressed) ''' + if result is None: + return {} result2 = result.copy() if format: return json.dumps(result2, sort_keys=True, indent=4) diff --git a/library/ping b/library/ping index e52da3c9e94..4bb99de0454 100644 --- a/library/ping +++ b/library/ping @@ -36,10 +36,15 @@ author: Michael DeHaan def main(): module = AnsibleModule( - argument_spec = dict(), + argument_spec = dict( + data=dict(required=False, default=None), + ), supports_check_mode = True ) - module.exit_json(ping='pong') + result = dict(ping='pong') + if module.params['data']: + result['ping'] = module.params['data'] + module.exit_json(**result) # this is magic, see lib/ansible/module_common.py #<>