Working on complex argument support.
This commit is contained in:
parent
5a91873983
commit
1ecf4a6943
18 changed files with 85 additions and 27 deletions
|
@ -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 '''
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
@ -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):
|
||||
''' transfer string to remote file '''
|
||||
|
||||
|
@ -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 = ""
|
||||
|
@ -375,6 +403,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:
|
||||
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):
|
||||
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)
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!'
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>>
|
||||
|
|
Loading…
Reference in a new issue