|
|
|
@ -17,6 +17,10 @@
|
|
|
|
|
|
|
|
|
|
import exceptions
|
|
|
|
|
|
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
|
from ansible.plugins import module_finder
|
|
|
|
|
from ansible.parsing.splitter import parse_kv
|
|
|
|
|
|
|
|
|
|
class ModuleArgsParser(object):
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
@ -24,25 +28,25 @@ class ModuleArgsParser(object):
|
|
|
|
|
|
|
|
|
|
# legacy form (for a shell command)
|
|
|
|
|
- action: shell echo hi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# common shorthand for local actions vs delegate_to
|
|
|
|
|
- local_action: shell echo hi
|
|
|
|
|
|
|
|
|
|
# most commonly:
|
|
|
|
|
- copy: src=a dest=b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# legacy form
|
|
|
|
|
- action: copy src=a dest=b
|
|
|
|
|
|
|
|
|
|
# complex args form, for passing structured data
|
|
|
|
|
- copy:
|
|
|
|
|
- copy:
|
|
|
|
|
src: a
|
|
|
|
|
dest: b
|
|
|
|
|
|
|
|
|
|
# gross, but technically legal
|
|
|
|
|
- action:
|
|
|
|
|
module: copy
|
|
|
|
|
args:
|
|
|
|
|
args:
|
|
|
|
|
src: a
|
|
|
|
|
dest: b
|
|
|
|
|
|
|
|
|
@ -52,9 +56,257 @@ class ModuleArgsParser(object):
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
pass
|
|
|
|
|
self._ds = None
|
|
|
|
|
|
|
|
|
|
def _get_delegate_to(self):
|
|
|
|
|
'''
|
|
|
|
|
Returns the value of the delegate_to key from the task datastructure,
|
|
|
|
|
or None if the value was not directly specified
|
|
|
|
|
'''
|
|
|
|
|
return self._ds.get('delegate_to')
|
|
|
|
|
|
|
|
|
|
def _get_old_style_action(self):
|
|
|
|
|
'''
|
|
|
|
|
Searches the datastructure for 'action:' or 'local_action:' keywords.
|
|
|
|
|
When local_action is found, the delegate_to value is set to the localhost
|
|
|
|
|
IP, otherwise delegate_to is left as None.
|
|
|
|
|
|
|
|
|
|
Inputs:
|
|
|
|
|
- None
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
- None (if neither keyword is found), or a dictionary containing:
|
|
|
|
|
action:
|
|
|
|
|
the module name to be executed
|
|
|
|
|
args:
|
|
|
|
|
a dictionary containing the arguments to the module
|
|
|
|
|
delegate_to:
|
|
|
|
|
None or 'localhost'
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
# determine if this is an 'action' or 'local_action'
|
|
|
|
|
if 'action' in self._ds:
|
|
|
|
|
action_data = self._ds.get('action', '')
|
|
|
|
|
delegate_to = None
|
|
|
|
|
elif 'local_action' in self._ds:
|
|
|
|
|
action_data = self._ds.get('local_action', '')
|
|
|
|
|
delegate_to = 'localhost'
|
|
|
|
|
else:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# now we get the arguments for the module, which may be a
|
|
|
|
|
# string of key=value pairs, a dictionary of values, or a
|
|
|
|
|
# dictionary with a special 'args:' value in it
|
|
|
|
|
if isinstance(action_data, dict):
|
|
|
|
|
action = self._get_specified_module(action_data)
|
|
|
|
|
args = dict()
|
|
|
|
|
if 'args' in action_data:
|
|
|
|
|
args = self._get_args_from_ds(action, action_data)
|
|
|
|
|
del action_data['args']
|
|
|
|
|
other_args = action_data.copy()
|
|
|
|
|
# remove things we don't want in the args
|
|
|
|
|
if 'module' in other_args:
|
|
|
|
|
del other_args['module']
|
|
|
|
|
args.update(other_args)
|
|
|
|
|
elif isinstance(action_data, basestring):
|
|
|
|
|
action_data = action_data.strip()
|
|
|
|
|
if not action_data:
|
|
|
|
|
# TODO: change to an AnsibleParsingError so that the
|
|
|
|
|
# filename/line number can be reported in the error
|
|
|
|
|
raise AnsibleError("when using 'action:' or 'local_action:', the module name must be specified")
|
|
|
|
|
else:
|
|
|
|
|
# split up the string based on spaces, where the first
|
|
|
|
|
# item specified must be a valid module name
|
|
|
|
|
parts = action_data.split(' ', 1)
|
|
|
|
|
action = parts[0]
|
|
|
|
|
if action not in module_finder:
|
|
|
|
|
# TODO: change to an AnsibleParsingError so that the
|
|
|
|
|
# filename/line number can be reported in the error
|
|
|
|
|
raise AnsibleError("the module '%s' was not found in the list of loaded modules")
|
|
|
|
|
if len(parts) > 1:
|
|
|
|
|
args = self._get_args_from_action(action, ' '.join(parts[1:]))
|
|
|
|
|
else:
|
|
|
|
|
args = {}
|
|
|
|
|
else:
|
|
|
|
|
# TODO: change to an AnsibleParsingError so that the
|
|
|
|
|
# filename/line number can be reported in the error
|
|
|
|
|
raise AnsibleError('module args must be specified as a dictionary or string')
|
|
|
|
|
|
|
|
|
|
return dict(action=action, args=args, delegate_to=delegate_to)
|
|
|
|
|
|
|
|
|
|
def _get_new_style_action(self):
|
|
|
|
|
'''
|
|
|
|
|
Searches the datastructure for 'module_name:', where the module_name is a
|
|
|
|
|
valid module loaded by the module_finder plugin.
|
|
|
|
|
|
|
|
|
|
Inputs:
|
|
|
|
|
- None
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
- None (if no valid module is found), or a dictionary containing:
|
|
|
|
|
action:
|
|
|
|
|
the module name to be executed
|
|
|
|
|
args:
|
|
|
|
|
a dictionary containing the arguments to the module
|
|
|
|
|
delegate_to:
|
|
|
|
|
None
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
# for all keys in the datastructure, check to see if the value
|
|
|
|
|
# corresponds to a module found by the module_finder plugin
|
|
|
|
|
action = None
|
|
|
|
|
for item in self._ds:
|
|
|
|
|
if item in module_finder:
|
|
|
|
|
action = item
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
# none of the keys matched a known module name
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# now we get the arguments for the module, which may be a
|
|
|
|
|
# string of key=value pairs, a dictionary of values, or a
|
|
|
|
|
# dictionary with a special 'args:' value in it
|
|
|
|
|
action_data = self._ds.get(action, '')
|
|
|
|
|
if isinstance(action_data, dict):
|
|
|
|
|
args = dict()
|
|
|
|
|
if 'args' in action_data:
|
|
|
|
|
args = self._get_args_from_ds(action, action_data)
|
|
|
|
|
del action_data['args']
|
|
|
|
|
other_args = action_data.copy()
|
|
|
|
|
# remove things we don't want in the args
|
|
|
|
|
if 'module' in other_args:
|
|
|
|
|
del other_args['module']
|
|
|
|
|
args.update(other_args)
|
|
|
|
|
else:
|
|
|
|
|
args = self._get_args_from_action(action, action_data.strip())
|
|
|
|
|
|
|
|
|
|
return dict(action=action, args=args, delegate_to=None)
|
|
|
|
|
|
|
|
|
|
def _get_args_from_ds(self, action, action_data):
|
|
|
|
|
'''
|
|
|
|
|
Gets the module arguments from the 'args' value of the
|
|
|
|
|
action_data, when action_data is a dict. The value of
|
|
|
|
|
'args' can be either a string or a dictionary itself, so
|
|
|
|
|
we use parse_kv() to split up the key=value pairs when
|
|
|
|
|
a string is found.
|
|
|
|
|
|
|
|
|
|
Inputs:
|
|
|
|
|
- action_data:
|
|
|
|
|
a dictionary of values, which may or may not contain a
|
|
|
|
|
key named 'args'
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
- a dictionary of values, representing the arguments to the
|
|
|
|
|
module action specified
|
|
|
|
|
'''
|
|
|
|
|
args = action_data.get('args', {}).copy()
|
|
|
|
|
if isinstance(args, basestring):
|
|
|
|
|
if action in ('command', 'shell'):
|
|
|
|
|
args = parse_kv(args, check_raw=True)
|
|
|
|
|
else:
|
|
|
|
|
args = parse_kv(args)
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
def _get_args_from_action(self, action, action_data):
|
|
|
|
|
'''
|
|
|
|
|
Gets the module arguments from the action data when it is
|
|
|
|
|
specified as a string of key=value pairs. Special handling
|
|
|
|
|
is used for the command/shell modules, which allow free-
|
|
|
|
|
form syntax for the options.
|
|
|
|
|
|
|
|
|
|
Inputs:
|
|
|
|
|
- action:
|
|
|
|
|
the module to be executed
|
|
|
|
|
- action_data:
|
|
|
|
|
a string of key=value pairs (and possibly free-form arguments)
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
- A dictionary of values, representing the arguments to the
|
|
|
|
|
module action specified OR a string of key=value pairs (when
|
|
|
|
|
the module action is command or shell)
|
|
|
|
|
'''
|
|
|
|
|
tokens = action_data.split()
|
|
|
|
|
if len(tokens) == 0:
|
|
|
|
|
return {}
|
|
|
|
|
else:
|
|
|
|
|
joined = " ".join(tokens)
|
|
|
|
|
if action in ('command', 'shell'):
|
|
|
|
|
return parse_kv(joined, check_raw=True)
|
|
|
|
|
else:
|
|
|
|
|
return parse_kv(joined)
|
|
|
|
|
|
|
|
|
|
def _get_specified_module(self, action_data):
|
|
|
|
|
'''
|
|
|
|
|
gets the module if specified directly in the arguments, ie:
|
|
|
|
|
- action:
|
|
|
|
|
module: foo
|
|
|
|
|
|
|
|
|
|
Inputs:
|
|
|
|
|
- action_data:
|
|
|
|
|
a dictionary of values, which may or may not contain the
|
|
|
|
|
key 'module'
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
- a string representing the module specified in the data, or
|
|
|
|
|
None if that key was not found
|
|
|
|
|
'''
|
|
|
|
|
return action_data.get('module')
|
|
|
|
|
|
|
|
|
|
def parse(self, ds):
|
|
|
|
|
'''
|
|
|
|
|
Given a task in one of the supported forms, parses and returns
|
|
|
|
|
returns the action, arguments, and delegate_to values for the
|
|
|
|
|
task.
|
|
|
|
|
|
|
|
|
|
Inputs:
|
|
|
|
|
- ds:
|
|
|
|
|
a dictionary datastructure representing the task as parsed
|
|
|
|
|
from a YAML file
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
- A tuple containing 3 values:
|
|
|
|
|
action:
|
|
|
|
|
the action (module name) to be executed
|
|
|
|
|
args:
|
|
|
|
|
the args for the action
|
|
|
|
|
delegate_to:
|
|
|
|
|
the delegate_to option (which may be None, if no delegate_to
|
|
|
|
|
option was specified and this is not a local_action)
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
assert type(ds) == dict
|
|
|
|
|
|
|
|
|
|
self._ds = ds
|
|
|
|
|
|
|
|
|
|
# first we try to get the module action/args based on the
|
|
|
|
|
# new-style format, where the module name is the key
|
|
|
|
|
result = self._get_new_style_action()
|
|
|
|
|
if result is None:
|
|
|
|
|
# failing that, we resort to checking for the old-style syntax,
|
|
|
|
|
# where 'action' or 'local_action' is the key
|
|
|
|
|
result = self._get_old_style_action()
|
|
|
|
|
if result is None:
|
|
|
|
|
# TODO: change to an AnsibleParsingError so that the
|
|
|
|
|
# filename/line number can be reported in the error
|
|
|
|
|
raise AnsibleError('no action specified for this task')
|
|
|
|
|
|
|
|
|
|
# if the action is set to 'shell', we switch that to 'command' and
|
|
|
|
|
# set the special parameter '_uses_shell' to true in the args dict
|
|
|
|
|
if result['action'] == 'shell':
|
|
|
|
|
result['action'] = 'command'
|
|
|
|
|
result['args']['_uses_shell'] = True
|
|
|
|
|
|
|
|
|
|
def parse(self, thing1, thing2):
|
|
|
|
|
raise exceptions.NotImplementedError
|
|
|
|
|
# finally, we check to see if a delegate_to value was specified
|
|
|
|
|
# in the task datastructure (and raise an error for local_action,
|
|
|
|
|
# which essentially means we're delegating to localhost)
|
|
|
|
|
specified_delegate_to = self._get_delegate_to()
|
|
|
|
|
if specified_delegate_to is not None:
|
|
|
|
|
if result['delegate_to'] is not None:
|
|
|
|
|
# TODO: change to an AnsibleParsingError so that the
|
|
|
|
|
# filename/line number can be reported in the error
|
|
|
|
|
raise AnsibleError('delegate_to cannot be used with local_action')
|
|
|
|
|
else:
|
|
|
|
|
result['delegate_to'] = specified_delegate_to
|
|
|
|
|
|
|
|
|
|
return (result['action'], result['args'], result['delegate_to'])
|
|
|
|
|
|
|
|
|
|