Working on complex argument support.

This commit is contained in:
Michael DeHaan 2013-02-17 15:01:49 -05:00
parent 5a91873983
commit 1ecf4a6943
18 changed files with 85 additions and 27 deletions

View file

@ -18,6 +18,7 @@
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
REPLACER_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
REPLACER_LANG = "<<INCLUDE_ANSIBLE_MODULE_LANG>>"
REPLACER_COMPLEX = "<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
MODULE_COMMON = """
@ -25,6 +26,7 @@ MODULE_COMMON = """
MODULE_ARGS = <<INCLUDE_ANSIBLE_MODULE_ARGS>>
MODULE_LANG = <<INCLUDE_ANSIBLE_MODULE_LANG>>
MODULE_COMPLEX_ARGS = <<INCLUDE_ANSIBLE_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 '''

View file

@ -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:

View file

@ -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)

View file

@ -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)

View file

@ -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'))

View file

@ -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:

View file

@ -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

View file

@ -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!'

View file

@ -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.

View file

@ -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:

View file

@ -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.

View file

@ -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)

View file

@ -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'

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>