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 = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
REPLACER_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>" REPLACER_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
REPLACER_LANG = "<<INCLUDE_ANSIBLE_MODULE_LANG>>" REPLACER_LANG = "<<INCLUDE_ANSIBLE_MODULE_LANG>>"
REPLACER_COMPLEX = "<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
MODULE_COMMON = """ MODULE_COMMON = """
@ -25,6 +26,7 @@ MODULE_COMMON = """
MODULE_ARGS = <<INCLUDE_ANSIBLE_MODULE_ARGS>> MODULE_ARGS = <<INCLUDE_ANSIBLE_MODULE_ARGS>>
MODULE_LANG = <<INCLUDE_ANSIBLE_MODULE_LANG>> MODULE_LANG = <<INCLUDE_ANSIBLE_MODULE_LANG>>
MODULE_COMPLEX_ARGS = <<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>
BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1] BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1]
BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0] BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0]
@ -559,7 +561,9 @@ class AnsibleModule(object):
except: except:
self.fail_json(msg="this module requires key=value arguments") self.fail_json(msg="this module requires key=value arguments")
params[k] = v params[k] = v
return (params, args) params2 = json.loads(MODULE_COMPLEX_ARGS)
params2.update(params)
return (params2, args)
def _log_invocation(self): def _log_invocation(self):
''' log that ansible ran the module ''' ''' log that ansible ran the module '''

View file

@ -273,7 +273,7 @@ class PlayBook(object):
conditional=task.only_if, callbacks=self.runner_callbacks, conditional=task.only_if, callbacks=self.runner_callbacks,
sudo=task.sudo, sudo_user=task.sudo_user, sudo=task.sudo, sudo_user=task.sudo_user,
transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True, 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: if task.async_seconds == 0:

View file

@ -27,7 +27,7 @@ class Task(object):
'play', 'notified_by', 'tags', 'register', 'play', 'notified_by', 'tags', 'register',
'delegate_to', 'first_available_file', 'ignore_errors', 'delegate_to', 'first_available_file', 'ignore_errors',
'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass', '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 # to prevent typos and such
@ -35,7 +35,7 @@ class Task(object):
'name', 'action', 'only_if', 'async', 'poll', 'notify', 'name', 'action', 'only_if', 'async', 'poll', 'notify',
'first_available_file', 'include', 'tags', 'register', 'ignore_errors', 'first_available_file', 'include', 'tags', 'register', 'ignore_errors',
'delegate_to', 'local_action', 'transport', 'sudo', 'sudo_user', '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): 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.sudo = utils.boolean(ds.get('sudo', play.sudo))
self.environment = ds.get('environment', {}) 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: if self.sudo:
self.sudo_user = ds.get('sudo_user', play.sudo_user) self.sudo_user = ds.get('sudo_user', play.sudo_user)
self.sudo_pass = ds.get('sudo_pass', play.playbook.sudo_pass) self.sudo_pass = ds.get('sudo_pass', play.playbook.sudo_pass)

View file

@ -29,6 +29,7 @@ import socket
import base64 import base64
import sys import sys
import shlex import shlex
import pipes
import ansible.constants as C import ansible.constants as C
import ansible.inventory import ansible.inventory
@ -120,9 +121,13 @@ class Runner(object):
subset=None, # subset pattern subset=None, # subset pattern
check=False, # don't make any changes, just try to probe for potential changes 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 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 # storage & defaults
self.check = check self.check = check
self.diff = diff self.diff = diff
@ -151,6 +156,7 @@ class Runner(object):
self.sudo_pass = sudo_pass self.sudo_pass = sudo_pass
self.is_playbook = is_playbook self.is_playbook = is_playbook
self.environment = environment self.environment = environment
self.complex_args = complex_args
# misc housekeeping # misc housekeeping
if subset and self.inventory._subset is None: if subset and self.inventory._subset is None:
@ -171,6 +177,27 @@ class Runner(object):
# ***************************************************** # *****************************************************
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
# *****************************************************
def _transfer_str(self, conn, tmp, name, data): def _transfer_str(self, conn, tmp, name, data):
''' transfer string to remote file ''' ''' transfer string to remote file '''
@ -212,7 +239,7 @@ class Runner(object):
# ***************************************************** # *****************************************************
def _execute_module(self, conn, tmp, module_name, args, 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 ''' ''' runs a module that has already been transferred '''
@ -222,7 +249,7 @@ class Runner(object):
if 'port' not in args: if 'port' not in args:
args += " port=%s" % C.ZEROMQ_PORT 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) 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): def _executor_internal_inner(self, host, module_name, module_args, inject, port, is_chained=False):
''' decides how to invoke a module ''' ''' decides how to invoke a module '''
# allow module args to work as a dictionary # allow module args to work as a dictionary
# though it is usually a string # though it is usually a string
new_args = "" new_args = ""
@ -375,6 +403,7 @@ class Runner(object):
module_name = utils.template(self.basedir, module_name, inject) module_name = utils.template(self.basedir, module_name, inject)
module_args = utils.template(self.basedir, module_args, inject) module_args = utils.template(self.basedir, module_args, inject)
if module_name in utils.plugins.action_loader: if module_name in utils.plugins.action_loader:
if self.background != 0: if self.background != 0:
raise errors.AnsibleError("async mode is not supported with the %s module" % module_name) raise errors.AnsibleError("async mode is not supported with the %s module" % module_name)
@ -449,7 +478,7 @@ class Runner(object):
if getattr(handler, 'NEEDS_TMPPATH', True): if getattr(handler, 'NEEDS_TMPPATH', True):
tmp = self._make_tmp_path(conn) 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() 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 ''' ''' transfer a module over SFTP, does not run it '''
# FIXME if complex args is none, set to {}
if module_name.startswith("/"): if module_name.startswith("/"):
raise errors.AnsibleFileNotFound("%s is not a module" % module_name) raise errors.AnsibleFileNotFound("%s is not a module" % module_name)
@ -578,11 +609,17 @@ class Runner(object):
module_data = f.read() module_data = f.read()
if module_common.REPLACER in module_data: if module_common.REPLACER in module_data:
is_new_style=True 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("\"","\\\"") 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_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_LANG, encoded_lang)
module_data = module_data.replace(module_common.REPLACER_COMPLEX, encoded_complex)
if is_new_style: if is_new_style:
facility = C.DEFAULT_SYSLOG_FACILITY facility = C.DEFAULT_SYSLOG_FACILITY
if 'ansible_syslog_facility' in inject: if 'ansible_syslog_facility' in inject:
@ -684,7 +721,9 @@ class Runner(object):
# run once per hostgroup, rather than pausing once per each # run once per hostgroup, rather than pausing once per each
# host. # host.
p = utils.plugins.action_loader.get(self.module_name, self) p = utils.plugins.action_loader.get(self.module_name, self)
if p and getattr(p, 'BYPASS_HOST_LOOP', None): if p and getattr(p, 'BYPASS_HOST_LOOP', None):
# Expose the current hostgroup to the bypassing plugins # Expose the current hostgroup to the bypassing plugins
self.host_set = hosts self.host_set = hosts
# We aren't iterating over all the hosts in this # 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) \ results = [ ReturnData(host=h, result=result_data, comm_ok=True) \
for h in hosts ] for h in hosts ]
del self.host_set del self.host_set
elif self.forks > 1: elif self.forks > 1:
try: try:
results = self._parallel_exec(hosts) results = self._parallel_exec(hosts)

View file

@ -34,7 +34,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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: if self.runner.check:
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module')) 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): def __init__(self, runner):
self.runner = 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 ''' ''' transfer the given module name, plus the async module, then run it '''
if self.runner.check: if self.runner.check:

View file

@ -26,7 +26,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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 ''' ''' handler for file transfer operations '''
# load up options # load up options

View file

@ -28,7 +28,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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) args = utils.parse_kv(module_args)
if not 'msg' in args: if not 'msg' in args:
args['msg'] = 'Hello world!' args['msg'] = 'Hello world!'

View file

@ -28,7 +28,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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 # note: the fail module does not need to pay attention to check mode
# it always runs. # it always runs.

View file

@ -33,7 +33,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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 ''' ''' handler for fetch operations '''
if self.runner.check: if self.runner.check:

View file

@ -32,7 +32,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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. # the group_by module does not need to pay attention to check mode.
# it always runs. # it always runs.

View file

@ -33,9 +33,12 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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' ''' ''' 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 self.runner.check:
if module_name in [ 'shell', 'command' ]: 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)) 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" module_args += " #USE_SHELL"
vv("REMOTE_MODULE %s %s" % (module_name, module_args), host=conn.host) 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, '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 ''' ''' run the pause action module '''
# note: this module does not need to pay attention to the 'check' # 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): def __init__(self, runner):
self.runner = 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: if self.runner.check:
# in --check mode, always skip this module execution # in --check mode, always skip this module execution

View file

@ -28,7 +28,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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 ''' ''' handler for file transfer operations '''
if self.runner.check: if self.runner.check:

View file

@ -27,7 +27,7 @@ class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
self.runner = 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 ''' ''' handler for template operations '''
# note: since this module just calls the copy module, the --check mode support # 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): def jsonify(result, format=False):
''' format JSON output (uncompressed or uncompressed) ''' ''' format JSON output (uncompressed or uncompressed) '''
if result is None:
return {}
result2 = result.copy() result2 = result.copy()
if format: if format:
return json.dumps(result2, sort_keys=True, indent=4) return json.dumps(result2, sort_keys=True, indent=4)

View file

@ -36,10 +36,15 @@ author: Michael DeHaan
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict(), argument_spec = dict(
data=dict(required=False, default=None),
),
supports_check_mode = True 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 # this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>> #<<INCLUDE_ANSIBLE_MODULE_COMMON>>