Merge branch 'issue106'
commit
c65911e811
@ -1,287 +0,0 @@
|
|||||||
# Copyright 2017, David Wilson
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without
|
|
||||||
# modification, are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# 1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer.
|
|
||||||
#
|
|
||||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
||||||
# may be used to endorse or promote products derived from this software without
|
|
||||||
# specific prior written permission.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
||||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import ansible_mitogen.helpers
|
|
||||||
|
|
||||||
try:
|
|
||||||
from shlex import quote as shlex_quote
|
|
||||||
except ImportError:
|
|
||||||
from pipes import quote as shlex_quote
|
|
||||||
|
|
||||||
# Prevent accidental import of an Ansible module from hanging on stdin read.
|
|
||||||
import ansible.module_utils.basic
|
|
||||||
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
|
|
||||||
|
|
||||||
|
|
||||||
class Exit(Exception):
|
|
||||||
"""
|
|
||||||
Raised when a module exits with success.
|
|
||||||
"""
|
|
||||||
def __init__(self, dct):
|
|
||||||
self.dct = dct
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleError(Exception):
|
|
||||||
"""
|
|
||||||
Raised when a module voluntarily indicates failure via .fail_json().
|
|
||||||
"""
|
|
||||||
def __init__(self, msg, dct):
|
|
||||||
Exception.__init__(self, msg)
|
|
||||||
self.dct = dct
|
|
||||||
|
|
||||||
|
|
||||||
class TemporaryEnvironment(object):
|
|
||||||
def __init__(self, env=None):
|
|
||||||
self.original = os.environ.copy()
|
|
||||||
self.env = env or {}
|
|
||||||
os.environ.update((k, str(v)) for k, v in self.env.iteritems())
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
os.environ.clear()
|
|
||||||
os.environ.update(self.original)
|
|
||||||
|
|
||||||
|
|
||||||
class MethodOverrides(object):
|
|
||||||
@staticmethod
|
|
||||||
def exit_json(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Replace AnsibleModule.exit_json() with something that doesn't try to
|
|
||||||
kill the process or JSON-encode the result dictionary. Instead, cause
|
|
||||||
Exit to be raised, with a `dct` attribute containing the successful
|
|
||||||
result dictionary.
|
|
||||||
"""
|
|
||||||
self.add_path_info(kwargs)
|
|
||||||
kwargs.setdefault('changed', False)
|
|
||||||
kwargs.setdefault('invocation', {
|
|
||||||
'module_args': self.params
|
|
||||||
})
|
|
||||||
kwargs = ansible.module_utils.basic.remove_values(
|
|
||||||
kwargs,
|
|
||||||
self.no_log_values,
|
|
||||||
)
|
|
||||||
self.do_cleanup_files()
|
|
||||||
raise Exit(kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def fail_json(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Replace AnsibleModule.fail_json() with something that raises
|
|
||||||
ModuleError, which includes a `dct` attribute.
|
|
||||||
"""
|
|
||||||
self.add_path_info(kwargs)
|
|
||||||
kwargs.setdefault('failed', True)
|
|
||||||
kwargs.setdefault('invocation', {
|
|
||||||
'module_args': self.params
|
|
||||||
})
|
|
||||||
kwargs = ansible.module_utils.basic.remove_values(
|
|
||||||
kwargs,
|
|
||||||
self.no_log_values,
|
|
||||||
)
|
|
||||||
self.do_cleanup_files()
|
|
||||||
raise ModuleError(kwargs.get('msg'), kwargs)
|
|
||||||
|
|
||||||
klass = ansible.module_utils.basic.AnsibleModule
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._original_exit_json = self.klass.exit_json
|
|
||||||
self._original_fail_json = self.klass.fail_json
|
|
||||||
self.klass.exit_json = self.exit_json
|
|
||||||
self.klass.fail_json = self.fail_json
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
self.klass.exit_json = self._original_exit_json
|
|
||||||
self.klass.fail_json = self._original_fail_json
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleArguments(object):
|
|
||||||
"""
|
|
||||||
Patch the ansible.module_utils.basic global arguments variable on
|
|
||||||
construction, and revert the changes on call to :meth:`revert`.
|
|
||||||
"""
|
|
||||||
def __init__(self, args):
|
|
||||||
self.original = ansible.module_utils.basic._ANSIBLE_ARGS
|
|
||||||
ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps({
|
|
||||||
'ANSIBLE_MODULE_ARGS': args
|
|
||||||
})
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
ansible.module_utils.basic._ANSIBLE_ARGS = self._original_args
|
|
||||||
|
|
||||||
|
|
||||||
class Runner(object):
|
|
||||||
def __init__(self, module, raw_params=None, args=None, env=None,
|
|
||||||
runner_params=None):
|
|
||||||
if args is None:
|
|
||||||
args = {}
|
|
||||||
if raw_params is not None:
|
|
||||||
args['_raw_params'] = raw_params
|
|
||||||
if runner_params is None:
|
|
||||||
runner_params = {}
|
|
||||||
|
|
||||||
self.module = module
|
|
||||||
self.raw_params = raw_params
|
|
||||||
self.args = args
|
|
||||||
self.env = env
|
|
||||||
self.runner_params = runner_params
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
self._env = TemporaryEnvironment(self.env)
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
self._env.revert()
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Set up the process environment in preparation for running an Ansible
|
|
||||||
module. This monkey-patches the Ansible libraries in various places to
|
|
||||||
prevent it from trying to kill the process on completion, and to
|
|
||||||
prevent it from reading sys.stdin.
|
|
||||||
"""
|
|
||||||
self.setup()
|
|
||||||
try:
|
|
||||||
return self._run()
|
|
||||||
finally:
|
|
||||||
self.revert()
|
|
||||||
|
|
||||||
|
|
||||||
class PythonRunner(object):
|
|
||||||
"""
|
|
||||||
Execute a new-style Ansible module, where Module Replacer-related tricks
|
|
||||||
aren't required.
|
|
||||||
"""
|
|
||||||
def setup(self):
|
|
||||||
super(PythonRunner, self).setup()
|
|
||||||
self._overrides = MethodOverrides()
|
|
||||||
self._args = ModuleArguments(self.args)
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
super(PythonRunner, self).revert()
|
|
||||||
self._args.revert()
|
|
||||||
self._overrides.revert()
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
try:
|
|
||||||
mod = __import__(self.module, {}, {}, [''])
|
|
||||||
# Ansible modules begin execution on import. Thus the above
|
|
||||||
# __import__ will cause either Exit or ModuleError to be raised. If
|
|
||||||
# we reach the line below, the module did not execute and must
|
|
||||||
# already have been imported for a previous invocation, so we need
|
|
||||||
# to invoke main explicitly.
|
|
||||||
mod.main()
|
|
||||||
except (Exit, ModuleError), e:
|
|
||||||
return json.dumps(e.dct)
|
|
||||||
|
|
||||||
assert False, "Module returned no result."
|
|
||||||
|
|
||||||
|
|
||||||
class BinaryRunner(object):
|
|
||||||
def setup(self):
|
|
||||||
super(BinaryRunner, self).setup()
|
|
||||||
self._setup_binary()
|
|
||||||
self._setup_args()
|
|
||||||
|
|
||||||
def _get_binary(self):
|
|
||||||
"""
|
|
||||||
Fetch the module binary from the master if necessary.
|
|
||||||
"""
|
|
||||||
return ansible_mitogen.helpers.get_file(
|
|
||||||
path=self.runner_params['path'],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_args(self):
|
|
||||||
"""
|
|
||||||
Return the module arguments formatted as JSON.
|
|
||||||
"""
|
|
||||||
return json.dumps(self.args)
|
|
||||||
|
|
||||||
def _setup_program(self):
|
|
||||||
"""
|
|
||||||
Create a temporary file containing the program code. The code is
|
|
||||||
fetched via :meth:`_get_binary`.
|
|
||||||
"""
|
|
||||||
self.bin_fp = tempfile.NamedTemporaryFile(
|
|
||||||
prefix='ansible_mitogen',
|
|
||||||
suffix='-binary',
|
|
||||||
)
|
|
||||||
self.bin_fp.write(self._get_binary())
|
|
||||||
self.bin_fp.flush()
|
|
||||||
os.chmod(self.fp.name, int('0700', 8))
|
|
||||||
|
|
||||||
def _setup_args(self):
|
|
||||||
"""
|
|
||||||
Create a temporary file containing the module's arguments. The
|
|
||||||
arguments are formatted via :meth:`_get_args`.
|
|
||||||
"""
|
|
||||||
self.args_fp = tempfile.NamedTemporaryFile(
|
|
||||||
prefix='ansible_mitogen',
|
|
||||||
suffix='-args',
|
|
||||||
)
|
|
||||||
self.args_fp.write(self._get_args())
|
|
||||||
self.args_fp.flush()
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
"""
|
|
||||||
Delete the temporary binary and argument files.
|
|
||||||
"""
|
|
||||||
self.args_fp.close()
|
|
||||||
self.bin_fp.close()
|
|
||||||
super(BinaryRunner, self).revert()
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
rc, stdout, stderr = ansible_mitogen.helpers.exec_args(
|
|
||||||
args=[self.bin_fp.name, self.args_fp.name],
|
|
||||||
)
|
|
||||||
# ...
|
|
||||||
assert 0
|
|
||||||
|
|
||||||
|
|
||||||
class WantJsonRunner(BinaryRunner):
|
|
||||||
def _get_binary(self):
|
|
||||||
s = super(WantJsonRunner, self)._get_binary()
|
|
||||||
# fix up shebang.
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
class OldStyleRunner(BinaryRunner):
|
|
||||||
def _get_args(self):
|
|
||||||
"""
|
|
||||||
Mimic the argument formatting behaviour of
|
|
||||||
ActionBase._execute_module().
|
|
||||||
"""
|
|
||||||
return ' '.join(
|
|
||||||
'%s=%s' % (key, shlex_quote(str(self.args[key])))
|
|
||||||
for key in self.args
|
|
||||||
)
|
|
||||||
@ -0,0 +1,380 @@
|
|||||||
|
# Copyright 2017, David Wilson
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# 3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
# may be used to endorse or promote products derived from this software without
|
||||||
|
# specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
These classes implement execution for each style of Ansible module. They are
|
||||||
|
instantiated in the target context by way of helpers.py::run_module().
|
||||||
|
|
||||||
|
Each class in here has a corresponding Planner class in planners.py that knows
|
||||||
|
how to build arguments for it, preseed related data, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import cStringIO
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import types
|
||||||
|
|
||||||
|
import ansible_mitogen.helpers # TODO: circular import
|
||||||
|
|
||||||
|
try:
|
||||||
|
from shlex import quote as shlex_quote
|
||||||
|
except ImportError:
|
||||||
|
from pipes import quote as shlex_quote
|
||||||
|
|
||||||
|
# Prevent accidental import of an Ansible module from hanging on stdin read.
|
||||||
|
import ansible.module_utils.basic
|
||||||
|
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Runner(object):
|
||||||
|
"""
|
||||||
|
Ansible module runner. After instantiation (with kwargs supplied by the
|
||||||
|
corresponding Planner), `.run()` is invoked, upon which `setup()`,
|
||||||
|
`_run()`, and `revert()` are invoked, with the return value of `_run()`
|
||||||
|
returned by `run()`.
|
||||||
|
|
||||||
|
Subclasses may override `_run`()` and extend `setup()` and `revert()`.
|
||||||
|
"""
|
||||||
|
def __init__(self, module, raw_params=None, args=None, env=None):
|
||||||
|
if args is None:
|
||||||
|
args = {}
|
||||||
|
if raw_params is not None:
|
||||||
|
args['_raw_params'] = raw_params
|
||||||
|
|
||||||
|
self.module = module
|
||||||
|
self.raw_params = raw_params
|
||||||
|
self.args = args
|
||||||
|
self.env = env
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""
|
||||||
|
Prepare the current process for running a module. The base
|
||||||
|
implementation simply prepares the environment.
|
||||||
|
"""
|
||||||
|
self._env = TemporaryEnvironment(self.env)
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
"""
|
||||||
|
Revert any changes made to the process after running a module. The base
|
||||||
|
implementation simply restores the original environment.
|
||||||
|
"""
|
||||||
|
self._env.revert()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
"""
|
||||||
|
The _run() method is expected to return a dictionary in the form of
|
||||||
|
ActionBase._low_level_execute_command() output, i.e. having::
|
||||||
|
|
||||||
|
{
|
||||||
|
"rc": int,
|
||||||
|
"stdout": "stdout data",
|
||||||
|
"stderr": "stderr data"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Set up the process environment in preparation for running an Ansible
|
||||||
|
module. This monkey-patches the Ansible libraries in various places to
|
||||||
|
prevent it from trying to kill the process on completion, and to
|
||||||
|
prevent it from reading sys.stdin.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
Module result dictionary.
|
||||||
|
"""
|
||||||
|
self.setup()
|
||||||
|
try:
|
||||||
|
return self._run()
|
||||||
|
finally:
|
||||||
|
self.revert()
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryEnvironment(object):
|
||||||
|
def __init__(self, env=None):
|
||||||
|
self.original = os.environ.copy()
|
||||||
|
self.env = env or {}
|
||||||
|
os.environ.update((k, str(v)) for k, v in self.env.iteritems())
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
os.environ.clear()
|
||||||
|
os.environ.update(self.original)
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryArgv(object):
|
||||||
|
def __init__(self, argv):
|
||||||
|
self.original = sys.argv[:]
|
||||||
|
sys.argv[:] = argv
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
sys.argv[:] = self.original
|
||||||
|
|
||||||
|
|
||||||
|
class NewStyleStdio(object):
|
||||||
|
"""
|
||||||
|
Patch ansible.module_utils.basic argument globals.
|
||||||
|
"""
|
||||||
|
def __init__(self, args):
|
||||||
|
self.original_stdout = sys.stdout
|
||||||
|
self.original_stderr = sys.stderr
|
||||||
|
self.original_stdin = sys.stdin
|
||||||
|
sys.stdout = cStringIO.StringIO()
|
||||||
|
sys.stderr = cStringIO.StringIO()
|
||||||
|
ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps({
|
||||||
|
'ANSIBLE_MODULE_ARGS': args
|
||||||
|
})
|
||||||
|
sys.stdin = cStringIO.StringIO(
|
||||||
|
ansible.module_utils.basic._ANSIBLE_ARGS
|
||||||
|
)
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
sys.stdout = self.original_stdout
|
||||||
|
sys.stderr = self.original_stderr
|
||||||
|
sys.stdin = self.original_stdin
|
||||||
|
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
|
||||||
|
|
||||||
|
|
||||||
|
class ProgramRunner(Runner):
|
||||||
|
def __init__(self, path, service_context, **kwargs):
|
||||||
|
super(ProgramRunner, self).__init__(**kwargs)
|
||||||
|
self.path = path
|
||||||
|
self.service_context = service_context
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(ProgramRunner, self).setup()
|
||||||
|
self._setup_program()
|
||||||
|
|
||||||
|
def _setup_program(self):
|
||||||
|
"""
|
||||||
|
Create a temporary file containing the program code. The code is
|
||||||
|
fetched via :meth:`_get_program`.
|
||||||
|
"""
|
||||||
|
self.program_fp = tempfile.NamedTemporaryFile(
|
||||||
|
prefix='ansible_mitogen',
|
||||||
|
suffix='-binary',
|
||||||
|
)
|
||||||
|
self.program_fp.write(self._get_program())
|
||||||
|
self.program_fp.flush()
|
||||||
|
os.chmod(self.program_fp.name, int('0700', 8))
|
||||||
|
|
||||||
|
def _get_program(self):
|
||||||
|
"""
|
||||||
|
Fetch the module binary from the master if necessary.
|
||||||
|
"""
|
||||||
|
return ansible_mitogen.helpers.get_file(
|
||||||
|
context=self.service_context,
|
||||||
|
path=self.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_program_args(self):
|
||||||
|
return [self.program_fp.name]
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
"""
|
||||||
|
Delete the temporary program file.
|
||||||
|
"""
|
||||||
|
super(ProgramRunner, self).revert()
|
||||||
|
self.program_fp.close()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
try:
|
||||||
|
rc, stdout, stderr = ansible_mitogen.helpers.exec_args(
|
||||||
|
args=self._get_program_args(),
|
||||||
|
)
|
||||||
|
except Exception, e:
|
||||||
|
return {
|
||||||
|
'rc': 1,
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': '%s: %s' % (type(e), e),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'rc': rc,
|
||||||
|
'stdout': stdout,
|
||||||
|
'stderr': stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ArgsFileRunner(Runner):
|
||||||
|
def setup(self):
|
||||||
|
super(ArgsFileRunner, self).setup()
|
||||||
|
self._setup_args()
|
||||||
|
|
||||||
|
def _setup_args(self):
|
||||||
|
"""
|
||||||
|
Create a temporary file containing the module's arguments. The
|
||||||
|
arguments are formatted via :meth:`_get_args`.
|
||||||
|
"""
|
||||||
|
self.args_fp = tempfile.NamedTemporaryFile(
|
||||||
|
prefix='ansible_mitogen',
|
||||||
|
suffix='-args',
|
||||||
|
)
|
||||||
|
self.args_fp.write(self._get_args_contents())
|
||||||
|
self.args_fp.flush()
|
||||||
|
|
||||||
|
def _get_args_contents(self):
|
||||||
|
"""
|
||||||
|
Return the module arguments formatted as JSON.
|
||||||
|
"""
|
||||||
|
return json.dumps(self.args)
|
||||||
|
|
||||||
|
def _get_program_args(self):
|
||||||
|
return [self.program_fp.name, self.args_fp.name]
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
"""
|
||||||
|
Delete the temporary argument file.
|
||||||
|
"""
|
||||||
|
super(ArgsFileRunner, self).revert()
|
||||||
|
self.args_fp.close()
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryRunner(ArgsFileRunner, ProgramRunner):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptRunner(ProgramRunner):
|
||||||
|
def __init__(self, interpreter, interpreter_arg, **kwargs):
|
||||||
|
super(ScriptRunner, self).__init__(**kwargs)
|
||||||
|
self.interpreter = interpreter
|
||||||
|
self.interpreter_arg = interpreter_arg
|
||||||
|
|
||||||
|
b_ENCODING_STRING = b'# -*- coding: utf-8 -*-'
|
||||||
|
|
||||||
|
def _get_program(self):
|
||||||
|
return self._rewrite_source(
|
||||||
|
super(ScriptRunner, self)._get_program()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rewrite_source(self, s):
|
||||||
|
"""
|
||||||
|
Mutate the source according to the per-task parameters.
|
||||||
|
"""
|
||||||
|
# Couldn't find shebang, so let shell run it, because shell assumes
|
||||||
|
# executables like this are just shell scripts.
|
||||||
|
if not self.interpreter:
|
||||||
|
return s
|
||||||
|
|
||||||
|
shebang = '#!' + self.interpreter
|
||||||
|
if self.interpreter_arg:
|
||||||
|
shebang += ' ' + self.interpreter_arg
|
||||||
|
|
||||||
|
new = [shebang]
|
||||||
|
if os.path.basename(self.interpreter).startswith('python'):
|
||||||
|
new.append(self.b_ENCODING_STRING)
|
||||||
|
|
||||||
|
_, _, rest = s.partition('\n')
|
||||||
|
new.append(rest)
|
||||||
|
return '\n'.join(new)
|
||||||
|
|
||||||
|
|
||||||
|
class NewStyleRunner(ScriptRunner):
|
||||||
|
"""
|
||||||
|
Execute a new-style Ansible module, where Module Replacer-related tricks
|
||||||
|
aren't required.
|
||||||
|
"""
|
||||||
|
#: path => new-style module bytecode.
|
||||||
|
_code_by_path = {}
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(NewStyleRunner, self).setup()
|
||||||
|
self._stdio = NewStyleStdio(self.args)
|
||||||
|
self._argv = TemporaryArgv([self.path])
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
self._argv.revert()
|
||||||
|
self._stdio.revert()
|
||||||
|
super(NewStyleRunner, self).revert()
|
||||||
|
|
||||||
|
def _get_code(self):
|
||||||
|
try:
|
||||||
|
return self._code_by_path[self.path]
|
||||||
|
except KeyError:
|
||||||
|
return self._code_by_path.setdefault(self.path, compile(
|
||||||
|
source=ansible_mitogen.helpers.get_file(
|
||||||
|
context=self.service_context,
|
||||||
|
path=self.path,
|
||||||
|
),
|
||||||
|
filename=self.path,
|
||||||
|
mode='exec',
|
||||||
|
dont_inherit=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
code = self._get_code()
|
||||||
|
mod = types.ModuleType('__main__')
|
||||||
|
d = vars(mod)
|
||||||
|
e = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
exec code in d, d
|
||||||
|
except SystemExit, e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'rc': e[0] if e else 2,
|
||||||
|
'stdout': sys.stdout.getvalue(),
|
||||||
|
'stderr': sys.stderr.getvalue(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class JsonArgsRunner(ScriptRunner):
|
||||||
|
JSON_ARGS = '<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>'
|
||||||
|
|
||||||
|
def _get_args_contents(self):
|
||||||
|
return json.dumps(self.args)
|
||||||
|
|
||||||
|
def _rewrite_source(self, s):
|
||||||
|
return (
|
||||||
|
super(JsonArgsRunner, self)._rewrite_source(s)
|
||||||
|
.replace(self.JSON_ARGS, self._get_args_contents())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WantJsonRunner(ArgsFileRunner, ScriptRunner):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OldStyleRunner(ArgsFileRunner, ScriptRunner):
|
||||||
|
def _get_args_contents(self):
|
||||||
|
"""
|
||||||
|
Mimic the argument formatting behaviour of
|
||||||
|
ActionBase._execute_module().
|
||||||
|
"""
|
||||||
|
return ' '.join(
|
||||||
|
'%s=%s' % (key, shlex_quote(str(self.args[key])))
|
||||||
|
for key in self.args
|
||||||
|
) + ' ' # Bug-for-bug :(
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
all: \
|
||||||
|
modules/binary_producing_junk \
|
||||||
|
modules/binary_producing_json
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exec >/tmp/derp
|
||||||
|
echo "$1"
|
||||||
|
cat "$1"
|
||||||
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "binary_producing_json: oh noes\n");
|
||||||
|
printf("{"
|
||||||
|
"\"changed\": true, "
|
||||||
|
"\"failed\": false, "
|
||||||
|
"\"msg\": \"Hello, world.\""
|
||||||
|
"}\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "binary_producing_junk: oh noes\n");
|
||||||
|
printf("Hello, world.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# I am an Ansible Python JSONARGS module. I should receive an encoding string.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
json_arguments = """<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>"""
|
||||||
|
|
||||||
|
print "{"
|
||||||
|
print " \"changed\": false,"
|
||||||
|
print " \"msg\": \"Here is my input\","
|
||||||
|
print " \"input\": [%s]" % (json_arguments,)
|
||||||
|
print "}"
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# I am an Ansible old-style module.
|
||||||
|
|
||||||
|
INPUT=$1
|
||||||
|
|
||||||
|
[ ! -r "$INPUT" ] && {
|
||||||
|
echo "Usage: $0 <input.json>" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "{"
|
||||||
|
echo " \"changed\": false,"
|
||||||
|
echo " \"msg\": \"Here is my input\","
|
||||||
|
echo " \"filname\": \"$INPUT\","
|
||||||
|
echo " \"input\": [\"$(cat $INPUT | tr \" \' )\"]"
|
||||||
|
echo "}"
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# I am an Ansible new-style Python module. I should receive an encoding string.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# This is the magic marker Ansible looks for:
|
||||||
|
# from ansible.module_utils.
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
sys.stderr.write('Usage: %s <input.json>\n' % (sys.argv[0],))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Also must slurp in our own source code, to verify the encoding string was
|
||||||
|
# added.
|
||||||
|
with open(sys.argv[0]) as fp:
|
||||||
|
me = fp.read()
|
||||||
|
|
||||||
|
input_json = sys.stdin.read()
|
||||||
|
|
||||||
|
print "{"
|
||||||
|
print " \"changed\": false,"
|
||||||
|
print " \"msg\": \"Here is my input\","
|
||||||
|
print " \"source\": [%s]," % (json.dumps(me),)
|
||||||
|
print " \"input\": [%s]" % (input_json,)
|
||||||
|
print "}"
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# I am an Ansible Python WANT_JSON module. I should receive an encoding string.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
WANT_JSON = 1
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
sys.stderr.write('Usage: %s <input.json>\n' % (sys.argv[0],))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
usage()
|
||||||
|
|
||||||
|
# Also must slurp in our own source code, to verify the encoding string was
|
||||||
|
# added.
|
||||||
|
with open(sys.argv[0]) as fp:
|
||||||
|
me = fp.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(sys.argv[1]) as fp:
|
||||||
|
input_json = fp.read()
|
||||||
|
except IOError:
|
||||||
|
usage()
|
||||||
|
|
||||||
|
print "{"
|
||||||
|
print " \"changed\": false,"
|
||||||
|
print " \"msg\": \"Here is my input\","
|
||||||
|
print " \"source\": [%s]," % (json.dumps(me),)
|
||||||
|
print " \"input\": [%s]" % (input_json,)
|
||||||
|
print "}"
|
||||||
Binary file not shown.
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# I am an Ansible WANT_JSON module.
|
||||||
|
|
||||||
|
WANT_JSON=1
|
||||||
|
INPUT=$1
|
||||||
|
|
||||||
|
[ ! -r "$INPUT" ] && {
|
||||||
|
echo "Usage: $0 <input.json>" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "{"
|
||||||
|
echo " \"changed\": false,"
|
||||||
|
echo " \"msg\": \"Here is my input\","
|
||||||
|
echo " \"input\": [$(< $INPUT)]"
|
||||||
|
echo "}"
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
suffixes = [
|
||||||
|
'-m bin_bash_module',
|
||||||
|
'-m binary_producing_json',
|
||||||
|
'-m binary_producing_junk',
|
||||||
|
'-m old_style_module',
|
||||||
|
'-m python_new_style_module',
|
||||||
|
'-m python_want_json_module',
|
||||||
|
'-m single_null_binary',
|
||||||
|
'-m want_json_module',
|
||||||
|
'-m json_args_python',
|
||||||
|
'-m setup',
|
||||||
|
]
|
||||||
|
|
||||||
|
fixups = [
|
||||||
|
('Shared connection to localhost closed\\.(\r\n)?', ''), # TODO
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fixup(s):
|
||||||
|
for regex, to in fixups:
|
||||||
|
s = re.sub(regex, to, s, re.DOTALL|re.M)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def run(s):
|
||||||
|
LOG.debug('running: %r', s)
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
# https://www.systutorials.com/docs/linux/man/1-ansible-playbook/#lbAG
|
||||||
|
returncode = subprocess.call(s, stdout=fp, stderr=fp, shell=True)
|
||||||
|
fp.write('\nReturn code: %s\n' % (returncode,))
|
||||||
|
fp.seek(0)
|
||||||
|
return fp.read()
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
for suffix in suffixes:
|
||||||
|
ansible = run('ansible localhost %s' % (suffix,))
|
||||||
|
mitogen = run('ANSIBLE_STRATEGY=mitogen ansible localhost %s' % (suffix,))
|
||||||
|
|
||||||
|
diff = list(difflib.unified_diff(
|
||||||
|
a=fixup(ansible).splitlines(),
|
||||||
|
b=fixup(mitogen).splitlines(),
|
||||||
|
fromfile='ansible-output.txt',
|
||||||
|
tofile='mitogen-output.txt',
|
||||||
|
))
|
||||||
|
if diff:
|
||||||
|
print '++ differ! suffix: %r' % (suffix,)
|
||||||
|
for line in diff:
|
||||||
|
print line
|
||||||
|
print
|
||||||
|
print
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
|
||||||
|
import errno
|
||||||
|
import select
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import unittest2
|
||||||
|
|
||||||
|
import testlib
|
||||||
|
import mitogen.core
|
||||||
|
|
||||||
|
|
||||||
|
class RestartTest(object):
|
||||||
|
func = staticmethod(mitogen.core.io_op)
|
||||||
|
exception_class = None
|
||||||
|
|
||||||
|
def test_eintr_restarts(self):
|
||||||
|
m = mock.Mock()
|
||||||
|
m.side_effect = [
|
||||||
|
self.exception_class(errno.EINTR),
|
||||||
|
self.exception_class(errno.EINTR),
|
||||||
|
self.exception_class(errno.EINTR),
|
||||||
|
'yay',
|
||||||
|
]
|
||||||
|
rc, disconnected = self.func(m, 'input')
|
||||||
|
self.assertEquals(rc, 'yay')
|
||||||
|
self.assertFalse(disconnected)
|
||||||
|
self.assertEquals(4, m.call_count)
|
||||||
|
self.assertEquals(m.mock_calls, [
|
||||||
|
mock.call('input'),
|
||||||
|
mock.call('input'),
|
||||||
|
mock.call('input'),
|
||||||
|
mock.call('input'),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class SelectRestartTest(RestartTest, testlib.TestCase):
|
||||||
|
exception_class = select.error
|
||||||
|
|
||||||
|
|
||||||
|
class OsErrorRestartTest(RestartTest, testlib.TestCase):
|
||||||
|
exception_class = OSError
|
||||||
|
|
||||||
|
|
||||||
|
class DisconnectTest(object):
|
||||||
|
func = staticmethod(mitogen.core.io_op)
|
||||||
|
errno = None
|
||||||
|
exception_class = None
|
||||||
|
|
||||||
|
def test_disconnection(self):
|
||||||
|
m = mock.Mock()
|
||||||
|
m.side_effect = self.exception_class(self.errno)
|
||||||
|
rc, disconnected = self.func(m, 'input')
|
||||||
|
self.assertEquals(rc, None)
|
||||||
|
self.assertTrue(disconnected)
|
||||||
|
self.assertEquals(1, m.call_count)
|
||||||
|
self.assertEquals(m.mock_calls, [
|
||||||
|
mock.call('input'),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class SelectDisconnectEioTest(DisconnectTest, testlib.TestCase):
|
||||||
|
errno = errno.EIO
|
||||||
|
exception_class = select.error
|
||||||
|
|
||||||
|
|
||||||
|
class SelectDisconnectEconnresetTest(DisconnectTest, testlib.TestCase):
|
||||||
|
errno = errno.ECONNRESET
|
||||||
|
exception_class = select.error
|
||||||
|
|
||||||
|
|
||||||
|
class SelectDisconnectEpipeTest(DisconnectTest, testlib.TestCase):
|
||||||
|
errno = errno.EPIPE
|
||||||
|
exception_class = select.error
|
||||||
|
|
||||||
|
|
||||||
|
class OsErrorDisconnectEioTest(DisconnectTest, testlib.TestCase):
|
||||||
|
errno = errno.EIO
|
||||||
|
exception_class = OSError
|
||||||
|
|
||||||
|
|
||||||
|
class OsErrorDisconnectEconnresetTest(DisconnectTest, testlib.TestCase):
|
||||||
|
errno = errno.ECONNRESET
|
||||||
|
exception_class = OSError
|
||||||
|
|
||||||
|
|
||||||
|
class OsErrorDisconnectEpipeTest(DisconnectTest, testlib.TestCase):
|
||||||
|
errno = errno.EPIPE
|
||||||
|
exception_class = OSError
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionTest(object):
|
||||||
|
func = staticmethod(mitogen.core.io_op)
|
||||||
|
errno = None
|
||||||
|
exception_class = None
|
||||||
|
|
||||||
|
def test_exception(self):
|
||||||
|
m = mock.Mock()
|
||||||
|
m.side_effect = self.exception_class(self.errno)
|
||||||
|
e = self.assertRaises(self.exception_class,
|
||||||
|
lambda: self.func(m, 'input'))
|
||||||
|
self.assertEquals(e, m.side_effect)
|
||||||
|
self.assertEquals(1, m.call_count)
|
||||||
|
self.assertEquals(m.mock_calls, [
|
||||||
|
mock.call('input'),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class SelectExceptionTest(ExceptionTest, testlib.TestCase):
|
||||||
|
errno = errno.EBADF
|
||||||
|
exception_class = select.error
|
||||||
|
|
||||||
|
|
||||||
|
class OsErrorExceptionTest(ExceptionTest, testlib.TestCase):
|
||||||
|
errno = errno.EBADF
|
||||||
|
exception_class = OSError
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest2.main()
|
||||||
Loading…
Reference in New Issue