|
|
|
@ -27,32 +27,69 @@
|
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
This exists to detect every case defined in [0] and prepare arguments necessary
|
|
|
|
|
for the executor implementation running within the target, including preloading
|
|
|
|
|
any requisite files/Python modules known to be missing.
|
|
|
|
|
Classes to detect each case from [0] and prepare arguments necessary for the
|
|
|
|
|
corresponding Runner class within the target, including preloading requisite
|
|
|
|
|
files/modules known missing.
|
|
|
|
|
|
|
|
|
|
[0] "Ansible Module Architecture", developing_program_flow_modules.html
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
from ansible.executor import module_common
|
|
|
|
|
import ansible.errors
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from ansible.plugins.loader import module_loader
|
|
|
|
|
except ImportError: # Ansible <2.4
|
|
|
|
|
from ansible.plugins import module_loader
|
|
|
|
|
|
|
|
|
|
import mitogen
|
|
|
|
|
import mitogen.service
|
|
|
|
|
import ansible_mitogen.helpers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Invocation(object):
|
|
|
|
|
"""
|
|
|
|
|
Collect up a module's execution environment then use it to invoke
|
|
|
|
|
helpers.run_module() or helpers.run_module_async() in the target context.
|
|
|
|
|
"""
|
|
|
|
|
def __init__(self, action, connection, module_name, module_args,
|
|
|
|
|
task_vars, tmp, env, wrap_async):
|
|
|
|
|
#: Instance of the ActionBase subclass invoking the module. Required to
|
|
|
|
|
#: access some output postprocessing methods that don't belong in
|
|
|
|
|
#: ActionBase at all.
|
|
|
|
|
self.action = action
|
|
|
|
|
self.connection = connection
|
|
|
|
|
self.module_name = module_name
|
|
|
|
|
self.module_args = module_args
|
|
|
|
|
self.module_path = None
|
|
|
|
|
self.module_source = None
|
|
|
|
|
self.task_vars = task_vars
|
|
|
|
|
self.tmp = tmp
|
|
|
|
|
self.env = env
|
|
|
|
|
self.wrap_async = wrap_async
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return 'Invocation(module_name=%s)' % (self.module_name,)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Planner(object):
|
|
|
|
|
"""
|
|
|
|
|
A Planner receives a module name and the contents of its implementation
|
|
|
|
|
file, indicates whether or not it understands how to run the module, and
|
|
|
|
|
exports a method to run the module.
|
|
|
|
|
"""
|
|
|
|
|
def detect(self, name, source):
|
|
|
|
|
assert 0
|
|
|
|
|
def detect(self, invocation):
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
def run(self, connection, name, source, args, env):
|
|
|
|
|
assert 0
|
|
|
|
|
def plan(self, invocation):
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class JsonArgsPlanner(Planner):
|
|
|
|
@ -60,10 +97,10 @@ class JsonArgsPlanner(Planner):
|
|
|
|
|
Script that has its interpreter directive and the task arguments
|
|
|
|
|
substituted into its source as a JSON string.
|
|
|
|
|
"""
|
|
|
|
|
def detect(self, name, source):
|
|
|
|
|
return module_common.REPLACER_JSONARGS in source
|
|
|
|
|
def detect(self, invocation):
|
|
|
|
|
return module_common.REPLACER_JSONARGS in invocation.module_source
|
|
|
|
|
|
|
|
|
|
def run(self, name, source, args, env):
|
|
|
|
|
def plan(self, invocation):
|
|
|
|
|
path = None # TODO
|
|
|
|
|
mitogen.service.call(501, ('register', path))
|
|
|
|
|
return {
|
|
|
|
@ -79,17 +116,17 @@ class WantJsonPlanner(Planner):
|
|
|
|
|
If a module has the string WANT_JSON in it anywhere, Ansible treats it as a
|
|
|
|
|
non-native module that accepts a filename as its only command line
|
|
|
|
|
parameter. The filename is for a temporary file containing a JSON string
|
|
|
|
|
containing the module’s parameters. The module needs to open the file, read
|
|
|
|
|
containing the module's parameters. The module needs to open the file, read
|
|
|
|
|
and parse the parameters, operate on the data, and print its return data as
|
|
|
|
|
a JSON encoded dictionary to stdout before exiting.
|
|
|
|
|
|
|
|
|
|
These types of modules are self-contained entities. As of Ansible 2.1,
|
|
|
|
|
Ansible only modifies them to change a shebang line if present.
|
|
|
|
|
"""
|
|
|
|
|
def detect(self, name, source):
|
|
|
|
|
return 'WANT_JSON' in source
|
|
|
|
|
def detect(self, invocation):
|
|
|
|
|
return 'WANT_JSON' in invocation.module_source
|
|
|
|
|
|
|
|
|
|
def run(self, name, source, args, env):
|
|
|
|
|
def plan(self, name, source, args, env):
|
|
|
|
|
return {
|
|
|
|
|
'func': 'run_want_json_module',
|
|
|
|
|
'binary': source,
|
|
|
|
@ -122,10 +159,10 @@ class ReplacerPlanner(Planner):
|
|
|
|
|
"ansible/module_utils/powershell.ps1". It should only be used with
|
|
|
|
|
new-style Powershell modules.
|
|
|
|
|
"""
|
|
|
|
|
def detect(self, name, source):
|
|
|
|
|
return module_common.REPLACER in source
|
|
|
|
|
def detect(self, invocation):
|
|
|
|
|
return module_common.REPLACER in invocation.module_source
|
|
|
|
|
|
|
|
|
|
def run(self, name, source, args, env):
|
|
|
|
|
def plan(self, name, source, args, env):
|
|
|
|
|
return {
|
|
|
|
|
'func': 'run_replacer_module',
|
|
|
|
|
'binary': source,
|
|
|
|
@ -139,37 +176,49 @@ class BinaryPlanner(Planner):
|
|
|
|
|
Binary modules take their arguments and will return data to Ansible in the
|
|
|
|
|
same way as want JSON modules.
|
|
|
|
|
"""
|
|
|
|
|
helper = staticmethod(ansible_mitogen.helpers.run_binary)
|
|
|
|
|
|
|
|
|
|
def detect(self, name, source):
|
|
|
|
|
return module_common._is_binary(source)
|
|
|
|
|
def detect(self, invocation):
|
|
|
|
|
return module_common._is_binary(invocation.module_source)
|
|
|
|
|
|
|
|
|
|
def run(self, name, source, args, env):
|
|
|
|
|
def plan(self, name, source, args, env):
|
|
|
|
|
return {
|
|
|
|
|
'func': 'run_binary_module',
|
|
|
|
|
'runner_name': 'BinaryRunner',
|
|
|
|
|
'binary': source,
|
|
|
|
|
'args': args,
|
|
|
|
|
'env': env,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PythonPlanner(Planner):
|
|
|
|
|
class NativePlanner(Planner):
|
|
|
|
|
"""
|
|
|
|
|
The Ansiballz framework differs from module replacer in that it uses real
|
|
|
|
|
Python imports of things in ansible/module_utils instead of merely
|
|
|
|
|
preprocessing the module.
|
|
|
|
|
"""
|
|
|
|
|
helper = staticmethod(ansible_mitogen.helpers.run_module)
|
|
|
|
|
|
|
|
|
|
def detect(self, name, source):
|
|
|
|
|
def detect(self, invocation):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def run(self, name, source, args, env):
|
|
|
|
|
def get_command_module_name(self, module_name):
|
|
|
|
|
"""
|
|
|
|
|
Given the name of an Ansible command module, return its canonical
|
|
|
|
|
module path within the ansible.
|
|
|
|
|
|
|
|
|
|
:param module_name:
|
|
|
|
|
"shell"
|
|
|
|
|
:return:
|
|
|
|
|
"ansible.modules.commands.shell"
|
|
|
|
|
"""
|
|
|
|
|
path = module_loader.find_plugin(module_name, '')
|
|
|
|
|
relpath = os.path.relpath(path, os.path.dirname(ansible.__file__))
|
|
|
|
|
root, _ = os.path.splitext(relpath)
|
|
|
|
|
return 'ansible.' + root.replace('/', '.')
|
|
|
|
|
|
|
|
|
|
def plan(self, invocation):
|
|
|
|
|
return {
|
|
|
|
|
'func': 'run_python_module',
|
|
|
|
|
'module': name,
|
|
|
|
|
'args': args,
|
|
|
|
|
'env': env
|
|
|
|
|
'runner_name': 'NativeRunner',
|
|
|
|
|
'module': invocation.module_name,
|
|
|
|
|
'mod_name': self.get_command_module_name(invocation.module_name),
|
|
|
|
|
'args': invocation.module_args,
|
|
|
|
|
'env': invocation.env,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -178,9 +227,46 @@ _planners = [
|
|
|
|
|
# WantJsonPlanner,
|
|
|
|
|
# ReplacerPlanner,
|
|
|
|
|
BinaryPlanner,
|
|
|
|
|
PythonPlanner,
|
|
|
|
|
NativePlanner,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def plan():
|
|
|
|
|
pass
|
|
|
|
|
NO_METHOD_MSG = 'Mitogen: no invocation method found for: '
|
|
|
|
|
CRASHED_MSG = 'Mitogen: internal error: '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_module_data(name):
|
|
|
|
|
path = module_loader.find_plugin(name, '')
|
|
|
|
|
with open(path, 'rb') as fp:
|
|
|
|
|
source = fp.read()
|
|
|
|
|
return path, source
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def invoke(invocation):
|
|
|
|
|
"""
|
|
|
|
|
Find a suitable Planner that knows how to run `invocation`.
|
|
|
|
|
"""
|
|
|
|
|
(invocation.module_path,
|
|
|
|
|
invocation.module_source) = get_module_data(invocation.module_name)
|
|
|
|
|
|
|
|
|
|
for klass in _planners:
|
|
|
|
|
planner = klass()
|
|
|
|
|
if planner.detect(invocation):
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise ansible.errors.AnsibleError(NO_METHOD_MSG + repr(invocation))
|
|
|
|
|
|
|
|
|
|
kwargs = planner.plan(invocation)
|
|
|
|
|
if invocation.wrap_async:
|
|
|
|
|
helper = ansible_mitogen.helpers.run_module_async
|
|
|
|
|
else:
|
|
|
|
|
helper = ansible_mitogen.helpers.run_module
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
js = invocation.connection.call(helper, kwargs)
|
|
|
|
|
except mitogen.core.CallError as e:
|
|
|
|
|
LOG.exception('invocation crashed: %r', invocation)
|
|
|
|
|
summary = str(e).splitlines()[0]
|
|
|
|
|
raise ansible.errors.AnsibleInternalError(CRASHED_MSG + summary)
|
|
|
|
|
|
|
|
|
|
return invocation.action._postprocess_response(js)
|
|
|
|
|