Skip to content

Commit

Permalink
Working on complex argument support.
Browse files Browse the repository at this point in the history
  • Loading branch information
mpdehaan committed Feb 17, 2013
1 parent 5a91873 commit 1ecf4a6
Show file tree
Hide file tree
Showing 18 changed files with 85 additions and 27 deletions.
6 changes: 5 additions & 1 deletion lib/ansible/module_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
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 = """
# == BEGIN DYNAMICALLY INSERTED CODE ==
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]
Expand Down Expand Up @@ -559,7 +561,9 @@ def _load_params(self):
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 '''
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/playbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def _run_task_internal(self, task):
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:
Expand Down
8 changes: 6 additions & 2 deletions lib/ansible/playbook/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ 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
VALID_KEYS = [
'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):
Expand Down Expand Up @@ -82,6 +82,10 @@ def __init__(self, play, ds, module_vars=None, additional_conditions=None):
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)
Expand Down
56 changes: 48 additions & 8 deletions lib/ansible/runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import base64
import sys
import shlex
import pipes

import ansible.constants as C
import ansible.inventory
Expand Down Expand Up @@ -120,9 +121,13 @@ def __init__(self,
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
Expand Down Expand Up @@ -151,6 +156,7 @@ def __init__(self,
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:
Expand All @@ -168,6 +174,27 @@ def __init__(self,

# 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

# *****************************************************

Expand Down Expand Up @@ -212,7 +239,7 @@ def _compute_environment_string(self, inject=None):
# *****************************************************

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

Expand All @@ -222,7 +249,7 @@ def _execute_module(self, conn, tmp, module_name, args,
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)

Expand Down Expand Up @@ -364,6 +391,7 @@ def _executor_internal(self, host):
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 = ""
Expand All @@ -374,6 +402,7 @@ def _executor_internal_inner(self, host, module_name, module_args, inject, port,

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:
Expand Down Expand Up @@ -448,8 +477,8 @@ def _executor_internal_inner(self, host, module_name, module_args, inject, port,
# 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()

Expand Down Expand Up @@ -558,9 +587,11 @@ def _make_tmp_path(self, conn):

# *****************************************************

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)

Expand All @@ -578,11 +609,17 @@ def _copy_module(self, conn, tmp, module_name, module_args, inject):
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:
Expand Down Expand Up @@ -684,7 +721,9 @@ def run(self):
# 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
Expand All @@ -697,6 +736,7 @@ def run(self):
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)
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/add_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/async.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!'
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/fail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/group_by.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions lib/ansible/runner/action_plugins/normal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -49,6 +52,6 @@ def run(self, conn, tmp, module_name, module_args, inject):
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)


2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/pause.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(self, runner):
'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'
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/runner/action_plugins/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/ansible/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions library/ping
Original file line number Diff line number Diff line change
Expand Up @@ -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>>
Expand Down

0 comments on commit 1ecf4a6

Please sign in to comment.