Merge branch 'master' into eye-of-the-token-its-the-thrill-of-the-light
commit
dc3f5730a2
@ -1,7 +1,13 @@
|
||||
.coverage
|
||||
.tox
|
||||
.venv
|
||||
**/.DS_Store
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.pyo
|
||||
MANIFEST
|
||||
build/
|
||||
dist/
|
||||
docs/_build
|
||||
htmlcov/
|
||||
*.egg-info
|
||||
__pycache__/
|
||||
|
@ -0,0 +1,45 @@
|
||||
#!/bin/bash -ex
|
||||
# Run tests/ansible/integration/all.yml under Ansible and Ansible-Mitogen
|
||||
|
||||
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}"
|
||||
TMPDIR="/tmp/ansible-tests-$$"
|
||||
ANSIBLE_VERSION="${ANSIBLE_VERSION:-2.4.3.0}"
|
||||
|
||||
function on_exit()
|
||||
{
|
||||
rm -rf "$TMPDIR"
|
||||
docker kill target || true
|
||||
}
|
||||
|
||||
trap on_exit EXIT
|
||||
mkdir "$TMPDIR"
|
||||
|
||||
|
||||
echo travis_fold:start:docker_setup
|
||||
docker run --rm --detach --name=target d2mw/mitogen-test /bin/sleep 86400
|
||||
echo travis_fold:end:docker_setup
|
||||
|
||||
|
||||
echo travis_fold:start:job_setup
|
||||
pip install -U ansible=="${ANSIBLE_VERSION}"
|
||||
cd ${TRAVIS_BUILD_DIR}/tests/ansible
|
||||
|
||||
cat >> ${TMPDIR}/hosts <<-EOF
|
||||
localhost
|
||||
target ansible_connection=docker ansible_python_interpreter=/usr/bin/python2.7
|
||||
EOF
|
||||
echo travis_fold:end:job_setup
|
||||
|
||||
|
||||
echo travis_fold:start:mitogen_linear
|
||||
ANSIBLE_STRATEGY=mitogen_linear /usr/bin/time ansible-playbook \
|
||||
integration/all.yml \
|
||||
-i "${TMPDIR}/hosts"
|
||||
echo travis_fold:end:mitogen_linear
|
||||
|
||||
|
||||
echo travis_fold:start:vanilla_ansible
|
||||
/usr/bin/time ansible-playbook \
|
||||
integration/all.yml \
|
||||
-i "${TMPDIR}/hosts"
|
||||
echo travis_fold:end:vanilla_ansible
|
@ -0,0 +1,77 @@
|
||||
#!/bin/bash -ex
|
||||
# Run some invocations of DebOps.
|
||||
|
||||
TMPDIR="/tmp/debops-$$"
|
||||
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}"
|
||||
TARGET_COUNT="${TARGET_COUNT:-4}"
|
||||
|
||||
|
||||
function on_exit()
|
||||
{
|
||||
echo travis_fold:start:cleanup
|
||||
[ "$KEEP" ] || {
|
||||
rm -rf "$TMPDIR" || true
|
||||
for i in $(seq $TARGET_COUNT)
|
||||
do
|
||||
docker kill target$i || true
|
||||
done
|
||||
}
|
||||
echo travis_fold:end:cleanup
|
||||
}
|
||||
|
||||
trap on_exit EXIT
|
||||
mkdir "$TMPDIR"
|
||||
|
||||
|
||||
echo travis_fold:start:job_setup
|
||||
pip install -qqqU debops==0.7.2 ansible==2.4.3.0
|
||||
debops-init "$TMPDIR/project"
|
||||
cd "$TMPDIR/project"
|
||||
|
||||
cat > .debops.cfg <<-EOF
|
||||
[ansible defaults]
|
||||
strategy_plugins = ${TRAVIS_BUILD_DIR}/ansible_mitogen/plugins/strategy
|
||||
strategy = mitogen_linear
|
||||
EOF
|
||||
|
||||
cat > ansible/inventory/group_vars/debops_all_hosts.yml <<-EOF
|
||||
ansible_python_interpreter: /usr/bin/python2.7
|
||||
|
||||
ansible_user: has-sudo-pubkey
|
||||
ansible_become_pass: y
|
||||
ansible_ssh_private_key_file: ${TRAVIS_BUILD_DIR}/tests/data/docker/has-sudo-pubkey.key
|
||||
|
||||
# Speed up slow DH generation.
|
||||
dhparam__bits: ["128", "64"]
|
||||
EOF
|
||||
|
||||
DOCKER_HOSTNAME="$(python ${TRAVIS_BUILD_DIR}/tests/show_docker_hostname.py)"
|
||||
|
||||
for i in $(seq $TARGET_COUNT)
|
||||
do
|
||||
port=$((2200 + $i))
|
||||
docker run \
|
||||
--rm \
|
||||
--detach \
|
||||
--publish 0.0.0.0:$port:22/tcp \
|
||||
--name=target$i \
|
||||
d2mw/mitogen-test
|
||||
|
||||
echo \
|
||||
target$i \
|
||||
ansible_host=$DOCKER_HOSTNAME \
|
||||
ansible_port=$port \
|
||||
>> ansible/inventory/hosts
|
||||
done
|
||||
|
||||
echo travis_fold:end:job_setup
|
||||
|
||||
|
||||
echo travis_fold:start:first_run
|
||||
/usr/bin/time debops common
|
||||
echo travis_fold:end:first_run
|
||||
|
||||
|
||||
echo travis_fold:start:second_run
|
||||
/usr/bin/time debops common
|
||||
echo travis_fold:end:second_run
|
@ -0,0 +1,4 @@
|
||||
#!/bin/bash -ex
|
||||
# Run the Mitogen tests.
|
||||
|
||||
MITOGEN_LOG_LEVEL=debug PYTHONPATH=. ${TRAVIS_BUILD_DIR}/run_tests
|
@ -1,320 +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.
|
||||
|
||||
import json
|
||||
import operator
|
||||
import os
|
||||
import pwd
|
||||
import random
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
import mitogen.core
|
||||
|
||||
# Prevent accidental import of an Ansible module from hanging on stdin read.
|
||||
import ansible.module_utils.basic
|
||||
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
|
||||
|
||||
#: Mapping of job_id<->result dict
|
||||
_result_by_job_id = {}
|
||||
|
||||
#: Mapping of job_id<->threading.Thread
|
||||
_thread_by_job_id = {}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def monkey_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)
|
||||
|
||||
|
||||
def monkey_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)
|
||||
|
||||
|
||||
def module_fixups(mod):
|
||||
"""
|
||||
Apply fixups for known problems with mainline Ansible modules.
|
||||
"""
|
||||
if mod.__name__ == 'ansible.modules.packaging.os.yum_repository':
|
||||
# https://github.com/dw/mitogen/issues/154
|
||||
mod.YumRepo.repofile = mod.configparser.RawConfigParser()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def run_module(module, raw_params=None, args=None, env=None):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if args is None:
|
||||
args = {}
|
||||
if raw_params is not None:
|
||||
args['_raw_params'] = raw_params
|
||||
|
||||
ansible.module_utils.basic.AnsibleModule.exit_json = monkey_exit_json
|
||||
ansible.module_utils.basic.AnsibleModule.fail_json = monkey_fail_json
|
||||
ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps({
|
||||
'ANSIBLE_MODULE_ARGS': args
|
||||
})
|
||||
|
||||
temp_env = TemporaryEnvironment(env)
|
||||
try:
|
||||
try:
|
||||
mod = __import__(module, {}, {}, [''])
|
||||
module_fixups(mod)
|
||||
# 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:
|
||||
result = json.dumps(e.dct)
|
||||
finally:
|
||||
temp_env.revert()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _async_main(job_id, module, raw_params, args, env):
|
||||
"""
|
||||
Implementation for the thread that implements asynchronous module
|
||||
execution.
|
||||
"""
|
||||
try:
|
||||
rc = run_module(module, raw_params, args, env)
|
||||
except Exception, e:
|
||||
rc = mitogen.core.CallError(e)
|
||||
|
||||
_result_by_job_id[job_id] = rc
|
||||
|
||||
|
||||
def run_module_async(module, raw_params=None, args=None):
|
||||
"""
|
||||
Arrange for an Ansible module to be executed in a thread of the current
|
||||
process, with results available via :py:func:`get_async_result`.
|
||||
"""
|
||||
job_id = '%08x' % random.randint(0, 2**32-1)
|
||||
_result_by_job_id[job_id] = None
|
||||
_thread_by_job_id[job_id] = threading.Thread(
|
||||
target=_async_main,
|
||||
kwargs={
|
||||
'job_id': job_id,
|
||||
'module': module,
|
||||
'raw_params': raw_params,
|
||||
'args': args,
|
||||
}
|
||||
)
|
||||
_thread_by_job_id[job_id].start()
|
||||
return json.dumps({
|
||||
'ansible_job_id': job_id,
|
||||
'changed': True
|
||||
})
|
||||
|
||||
|
||||
def get_async_result(job_id):
|
||||
"""
|
||||
Poll for the result of an asynchronous task.
|
||||
|
||||
:param str job_id:
|
||||
Job ID to poll for.
|
||||
:returns:
|
||||
``None`` if job is still running, JSON-encoded result dictionary if
|
||||
execution completed normally, or :py:class:`mitogen.core.CallError` if
|
||||
an exception was thrown.
|
||||
"""
|
||||
if not _thread_by_job_id[job_id].isAlive():
|
||||
return _result_by_job_id[job_id]
|
||||
|
||||
|
||||
def get_user_shell():
|
||||
"""
|
||||
For commands executed directly via an SSH command-line, SSH looks up the
|
||||
user's shell via getpwuid() and only defaults to /bin/sh if that field is
|
||||
missing or empty.
|
||||
"""
|
||||
try:
|
||||
pw_shell = pwd.getpwuid(os.geteuid()).pw_shell
|
||||
except KeyError:
|
||||
pw_shell = None
|
||||
|
||||
return pw_shell or '/bin/sh'
|
||||
|
||||
|
||||
def exec_command(cmd, in_data='', chdir=None, shell=None):
|
||||
"""
|
||||
Run a command in a subprocess, emulating the argument handling behaviour of
|
||||
SSH.
|
||||
|
||||
:param bytes cmd:
|
||||
String command line, passed to user's shell.
|
||||
:param bytes in_data:
|
||||
Optional standard input for the command.
|
||||
:return:
|
||||
(return code, stdout bytes, stderr bytes)
|
||||
"""
|
||||
assert isinstance(cmd, basestring)
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args=[get_user_shell(), '-c', cmd],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
cwd=chdir,
|
||||
)
|
||||
stdout, stderr = proc.communicate(in_data)
|
||||
return proc.returncode, stdout, stderr
|
||||
|
||||
|
||||
def read_path(path):
|
||||
"""
|
||||
Fetch the contents of a filesystem `path` as bytes.
|
||||
"""
|
||||
return open(path, 'rb').read()
|
||||
|
||||
|
||||
def write_path(path, s):
|
||||
"""
|
||||
Writes bytes `s` to a filesystem `path`.
|
||||
"""
|
||||
open(path, 'wb').write(s)
|
||||
|
||||
|
||||
CHMOD_CLAUSE_PAT = re.compile(r'([uoga]*)([+\-=])([ugo]|[rwx]*)')
|
||||
CHMOD_MASKS = {
|
||||
'u': stat.S_IRWXU,
|
||||
'g': stat.S_IRWXG,
|
||||
'o': stat.S_IRWXO,
|
||||
'a': (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO),
|
||||
}
|
||||
CHMOD_BITS = {
|
||||
'u': {'r': stat.S_IRUSR, 'w': stat.S_IWUSR, 'x': stat.S_IXUSR},
|
||||
'g': {'r': stat.S_IRGRP, 'w': stat.S_IWGRP, 'x': stat.S_IXGRP},
|
||||
'o': {'r': stat.S_IROTH, 'w': stat.S_IWOTH, 'x': stat.S_IXOTH},
|
||||
'a': {
|
||||
'r': (stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH),
|
||||
'w': (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH),
|
||||
'x': (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def apply_mode_spec(spec, mode):
|
||||
"""
|
||||
Given a symbolic file mode change specification in the style of chmod(1)
|
||||
`spec`, apply changes in the specification to the numeric file mode `mode`.
|
||||
"""
|
||||
for clause in spec.split(','):
|
||||
match = CHMOD_CLAUSE_PAT.match(clause)
|
||||
who, op, perms = match.groups()
|
||||
for ch in who or 'a':
|
||||
mask = CHMOD_MASKS[ch]
|
||||
bits = CHMOD_BITS[ch]
|
||||
cur_perm_bits = mode & mask
|
||||
new_perm_bits = reduce(operator.or_, (bits[p] for p in perms), 0)
|
||||
mode &= ~mask
|
||||
if op == '=':
|
||||
mode |= new_perm_bits
|
||||
elif op == '+':
|
||||
mode |= new_perm_bits | cur_perm_bits
|
||||
else:
|
||||
mode |= cur_perm_bits & ~new_perm_bits
|
||||
return mode
|
||||
|
||||
|
||||
def set_file_mode(path, spec):
|
||||
"""
|
||||
Update the permissions of a file using the same syntax as chmod(1).
|
||||
"""
|
||||
mode = os.stat(path).st_mode
|
||||
|
||||
if spec.isdigit():
|
||||
new_mode = int(spec, 8)
|
||||
else:
|
||||
new_mode = apply_mode_spec(spec, mode)
|
||||
|
||||
os.chmod(path, new_mode)
|
@ -0,0 +1,349 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
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.target
|
||||
import ansible_mitogen.services
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
NO_METHOD_MSG = 'Mitogen: no invocation method found for: '
|
||||
CRASHED_MSG = 'Mitogen: internal error: '
|
||||
NO_INTERPRETER_MSG = 'module (%s) is missing interpreter line'
|
||||
|
||||
|
||||
def parse_script_interpreter(source):
|
||||
"""
|
||||
Extract the script interpreter and its sole argument from the module
|
||||
source code.
|
||||
|
||||
:returns:
|
||||
Tuple of `(interpreter, arg)`, where `intepreter` is the script
|
||||
interpreter and `arg` is its sole argument if present, otherwise
|
||||
:py:data:`None`.
|
||||
"""
|
||||
# Linux requires first 2 bytes with no whitespace, pretty sure it's the
|
||||
# same everywhere. See binfmt_script.c.
|
||||
if not source.startswith('#!'):
|
||||
return None, None
|
||||
|
||||
# Find terminating newline. Assume last byte of binprm_buf if absent.
|
||||
nl = source.find('\n', 0, 128)
|
||||
if nl == -1:
|
||||
nl = min(128, len(source))
|
||||
|
||||
# Split once on the first run of whitespace. If no whitespace exists,
|
||||
# bits just contains the interpreter filename.
|
||||
bits = source[2:nl].strip().split(None, 1)
|
||||
if len(bits) == 1:
|
||||
return bits[0], None
|
||||
return bits[0], bits[1]
|
||||
|
||||
|
||||
class Invocation(object):
|
||||
"""
|
||||
Collect up a module's execution environment then use it to invoke
|
||||
target.run_module() or helpers.run_module_async() in the target context.
|
||||
"""
|
||||
def __init__(self, action, connection, module_name, module_args,
|
||||
remote_tmp, task_vars, templar, env, wrap_async):
|
||||
#: ActionBase instance invoking the module. Required to access some
|
||||
#: output postprocessing methods that don't belong in ActionBase at
|
||||
#: all.
|
||||
self.action = action
|
||||
#: Ansible connection to use to contact the target. Must be an
|
||||
#: ansible_mitogen connection.
|
||||
self.connection = connection
|
||||
#: Name of the module ('command', 'shell', etc.) to execute.
|
||||
self.module_name = module_name
|
||||
#: Final module arguments.
|
||||
self.module_args = module_args
|
||||
#: Value of 'remote_tmp' parameter, to allow target to create temporary
|
||||
#: files in correct location.
|
||||
self.remote_tmp = remote_tmp
|
||||
#: Task variables, needed to extract ansible_*_interpreter.
|
||||
self.task_vars = task_vars
|
||||
#: Templar, needed to extract ansible_*_interpreter.
|
||||
self.templar = templar
|
||||
#: Final module environment.
|
||||
self.env = env
|
||||
#: Boolean, if :py:data:`True`, launch the module asynchronously.
|
||||
self.wrap_async = wrap_async
|
||||
|
||||
#: Initially ``None``, but set by :func:`invoke`. The path on the
|
||||
#: master to the module's implementation file.
|
||||
self.module_path = None
|
||||
#: Initially ``None``, but set by :func:`invoke`. The raw source or
|
||||
#: binary contents of the module.
|
||||
self.module_source = None
|
||||
|
||||
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, invocation):
|
||||
"""
|
||||
Return true if the supplied `invocation` matches the module type
|
||||
implemented by this planner.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def plan(self, invocation):
|
||||
"""
|
||||
If :meth:`detect` returned :data:`True`, plan for the module's
|
||||
execution, including granting access to or delivering any files to it
|
||||
that are known to be absent, and finally return a dict::
|
||||
|
||||
{
|
||||
# Name of the class from runners.py that implements the
|
||||
# target-side execution of this module type.
|
||||
"runner_name": "...",
|
||||
|
||||
# Remaining keys are passed to the constructor of the class
|
||||
# named by `runner_name`.
|
||||
}
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class BinaryPlanner(Planner):
|
||||
"""
|
||||
Binary modules take their arguments and will return data to Ansible in the
|
||||
same way as want JSON modules.
|
||||
"""
|
||||
runner_name = 'BinaryRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
return module_common._is_binary(invocation.module_source)
|
||||
|
||||
def plan(self, invocation):
|
||||
invocation.connection._connect()
|
||||
mitogen.service.call(
|
||||
invocation.connection.parent,
|
||||
ansible_mitogen.services.FileService.handle,
|
||||
('register', invocation.module_path)
|
||||
)
|
||||
return {
|
||||
'runner_name': self.runner_name,
|
||||
'module': invocation.module_name,
|
||||
'service_context': invocation.connection.parent,
|
||||
'path': invocation.module_path,
|
||||
'args': invocation.module_args,
|
||||
'env': invocation.env,
|
||||
'remote_tmp': invocation.remote_tmp,
|
||||
}
|
||||
|
||||
|
||||
class ScriptPlanner(BinaryPlanner):
|
||||
"""
|
||||
Common functionality for script module planners -- handle interpreter
|
||||
detection and rewrite.
|
||||
"""
|
||||
def _rewrite_interpreter(self, invocation, interpreter):
|
||||
key = u'ansible_%s_interpreter' % os.path.basename(interpreter).strip()
|
||||
try:
|
||||
template = invocation.task_vars[key].strip()
|
||||
return invocation.templar.template(template)
|
||||
except KeyError:
|
||||
return interpreter
|
||||
|
||||
def plan(self, invocation):
|
||||
kwargs = super(ScriptPlanner, self).plan(invocation)
|
||||
interpreter, arg = parse_script_interpreter(invocation.module_source)
|
||||
if interpreter is None:
|
||||
raise ansible.errors.AnsibleError(NO_INTERPRETER_MSG % (
|
||||
invocation.module_name,
|
||||
))
|
||||
|
||||
return dict(kwargs,
|
||||
interpreter_arg=arg,
|
||||
interpreter=self._rewrite_interpreter(
|
||||
interpreter=interpreter,
|
||||
invocation=invocation
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ReplacerPlanner(BinaryPlanner):
|
||||
"""
|
||||
The Module Replacer framework is the original framework implementing
|
||||
new-style modules. It is essentially a preprocessor (like the C
|
||||
Preprocessor for those familiar with that programming language). It does
|
||||
straight substitutions of specific substring patterns in the module file.
|
||||
There are two types of substitutions.
|
||||
|
||||
* Replacements that only happen in the module file. These are public
|
||||
replacement strings that modules can utilize to get helpful boilerplate
|
||||
or access to arguments.
|
||||
|
||||
"from ansible.module_utils.MOD_LIB_NAME import *" is replaced with the
|
||||
contents of the ansible/module_utils/MOD_LIB_NAME.py. These should only
|
||||
be used with new-style Python modules.
|
||||
|
||||
"#<<INCLUDE_ANSIBLE_MODULE_COMMON>>" is equivalent to
|
||||
"from ansible.module_utils.basic import *" and should also only apply to
|
||||
new-style Python modules.
|
||||
|
||||
"# POWERSHELL_COMMON" substitutes the contents of
|
||||
"ansible/module_utils/powershell.ps1". It should only be used with
|
||||
new-style Powershell modules.
|
||||
"""
|
||||
runner_name = 'ReplacerRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
return module_common.REPLACER in invocation.module_source
|
||||
|
||||
|
||||
class JsonArgsPlanner(ScriptPlanner):
|
||||
"""
|
||||
Script that has its interpreter directive and the task arguments
|
||||
substituted into its source as a JSON string.
|
||||
"""
|
||||
runner_name = 'JsonArgsRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
return module_common.REPLACER_JSONARGS in invocation.module_source
|
||||
|
||||
|
||||
class WantJsonPlanner(ScriptPlanner):
|
||||
"""
|
||||
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
|
||||
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.
|
||||
"""
|
||||
runner_name = 'WantJsonRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
return 'WANT_JSON' in invocation.module_source
|
||||
|
||||
|
||||
class NewStylePlanner(ScriptPlanner):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
runner_name = 'NewStyleRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
return 'from ansible.module_utils.' in invocation.module_source
|
||||
|
||||
|
||||
class ReplacerPlanner(NewStylePlanner):
|
||||
runner_name = 'ReplacerRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
return module_common.REPLACER in invocation.module_source
|
||||
|
||||
|
||||
class OldStylePlanner(ScriptPlanner):
|
||||
runner_name = 'OldStyleRunner'
|
||||
|
||||
def detect(self, invocation):
|
||||
# Everything else.
|
||||
return True
|
||||
|
||||
|
||||
_planners = [
|
||||
BinaryPlanner,
|
||||
# ReplacerPlanner,
|
||||
NewStylePlanner,
|
||||
JsonArgsPlanner,
|
||||
WantJsonPlanner,
|
||||
OldStylePlanner,
|
||||
]
|
||||
|
||||
|
||||
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.target.run_module_async
|
||||
else:
|
||||
helper = ansible_mitogen.target.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)
|
@ -0,0 +1,60 @@
|
||||
# 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.
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
#
|
||||
# This is not the real Strategy implementation module, it simply exists as a
|
||||
# proxy to the real module, which is loaded using Python's regular import
|
||||
# mechanism, to prevent Ansible's PluginLoader from making up a fake name that
|
||||
# results in ansible_mitogen plugin modules being loaded twice: once by
|
||||
# PluginLoader with a name like "ansible.plugins.strategy.mitogen", which is
|
||||
# stuffed into sys.modules even though attempting to import it will trigger an
|
||||
# ImportError, and once under its canonical name, "ansible_mitogen.strategy".
|
||||
#
|
||||
# Therefore we have a proxy module that imports it under the real name, and
|
||||
# sets up the duff PluginLoader-imported module to just contain objects from
|
||||
# the real module, so duplicate types don't exist in memory, and things like
|
||||
# debuggers and isinstance() work predictably.
|
||||
#
|
||||
|
||||
try:
|
||||
import ansible_mitogen
|
||||
except ImportError:
|
||||
base_dir = os.path.dirname(__file__)
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
|
||||
del base_dir
|
||||
|
||||
import ansible_mitogen.strategy
|
||||
import ansible.plugins.strategy.free
|
||||
|
||||
|
||||
class StrategyModule(ansible_mitogen.strategy.StrategyMixin,
|
||||
ansible.plugins.strategy.free.StrategyModule):
|
||||
pass
|
@ -0,0 +1,60 @@
|
||||
# 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.
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
#
|
||||
# This is not the real Strategy implementation module, it simply exists as a
|
||||
# proxy to the real module, which is loaded using Python's regular import
|
||||
# mechanism, to prevent Ansible's PluginLoader from making up a fake name that
|
||||
# results in ansible_mitogen plugin modules being loaded twice: once by
|
||||
# PluginLoader with a name like "ansible.plugins.strategy.mitogen", which is
|
||||
# stuffed into sys.modules even though attempting to import it will trigger an
|
||||
# ImportError, and once under its canonical name, "ansible_mitogen.strategy".
|
||||
#
|
||||
# Therefore we have a proxy module that imports it under the real name, and
|
||||
# sets up the duff PluginLoader-imported module to just contain objects from
|
||||
# the real module, so duplicate types don't exist in memory, and things like
|
||||
# debuggers and isinstance() work predictably.
|
||||
#
|
||||
|
||||
try:
|
||||
import ansible_mitogen
|
||||
except ImportError:
|
||||
base_dir = os.path.dirname(__file__)
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
|
||||
del base_dir
|
||||
|
||||
import ansible_mitogen.strategy
|
||||
import ansible.plugins.strategy.linear
|
||||
|
||||
|
||||
class StrategyModule(ansible_mitogen.strategy.StrategyMixin,
|
||||
ansible.plugins.strategy.linear.StrategyModule):
|
||||
pass
|
@ -0,0 +1,418 @@
|
||||
# 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 target.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 shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
import ansible_mitogen.target # 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__)
|
||||
|
||||
|
||||
def reopen_readonly(fp):
|
||||
"""
|
||||
Replace the file descriptor belonging to the file object `fp` with one
|
||||
open on the same file (`fp.name`), but opened with :py:data:`os.O_RDONLY`.
|
||||
This enables temporary files to be executed on Linux, which usually theows
|
||||
``ETXTBUSY`` if any writeable handle exists pointing to a file passed to
|
||||
`execve()`.
|
||||
"""
|
||||
fd = os.open(fp.name, os.O_RDONLY)
|
||||
os.dup2(fd, fp.fileno())
|
||||
os.close(fd)
|
||||
|
||||
|
||||
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, remote_tmp, 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.remote_tmp = os.path.expanduser(remote_tmp)
|
||||
self.raw_params = raw_params
|
||||
self.args = args
|
||||
self.env = env
|
||||
self._temp_dir = None
|
||||
|
||||
def get_temp_dir(self):
|
||||
if not self._temp_dir:
|
||||
self._temp_dir = ansible_mitogen.target.make_temp_directory(
|
||||
self.remote_tmp,
|
||||
)
|
||||
return self._temp_dir
|
||||
|
||||
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()
|
||||
if self._temp_dir:
|
||||
shutil.rmtree(self._temp_dir)
|
||||
|
||||
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 = open(
|
||||
os.path.join(self.get_temp_dir(), self.module),
|
||||
'wb'
|
||||
)
|
||||
self.program_fp.write(self._get_program())
|
||||
self.program_fp.flush()
|
||||
os.chmod(self.program_fp.name, int('0700', 8))
|
||||
reopen_readonly(self.program_fp)
|
||||
|
||||
def _get_program(self):
|
||||
"""
|
||||
Fetch the module binary from the master if necessary.
|
||||
"""
|
||||
return ansible_mitogen.target.get_file(
|
||||
context=self.service_context,
|
||||
path=self.path,
|
||||
)
|
||||
|
||||
def _get_program_args(self):
|
||||
return [
|
||||
self.args['_ansible_shell_executable'],
|
||||
'-c',
|
||||
self.program_fp.name
|
||||
]
|
||||
|
||||
def revert(self):
|
||||
"""
|
||||
Delete the temporary program file.
|
||||
"""
|
||||
self.program_fp.close()
|
||||
super(ProgramRunner, self).revert()
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
rc, stdout, stderr = ansible_mitogen.target.exec_args(
|
||||
args=self._get_program_args(),
|
||||
emulate_tty=True,
|
||||
)
|
||||
except Exception, e:
|
||||
LOG.exception('While running %s', self._get_program_args())
|
||||
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',
|
||||
dir=self.get_temp_dir(),
|
||||
)
|
||||
self.args_fp.write(self._get_args_contents())
|
||||
self.args_fp.flush()
|
||||
reopen_readonly(self.program_fp)
|
||||
|
||||
def _get_args_contents(self):
|
||||
"""
|
||||
Return the module arguments formatted as JSON.
|
||||
"""
|
||||
return json.dumps(self.args)
|
||||
|
||||
def _get_program_args(self):
|
||||
return [
|
||||
self.args['_ansible_shell_executable'],
|
||||
'-c',
|
||||
"%s %s" % (self.program_fp.name, self.args_fp.name),
|
||||
]
|
||||
|
||||
def revert(self):
|
||||
"""
|
||||
Delete the temporary argument file.
|
||||
"""
|
||||
self.args_fp.close()
|
||||
super(ArgsFileRunner, self).revert()
|
||||
|
||||
|
||||
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.target.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 @@
|
||||
build
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- bin_bash_module:
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: "Run hostname"
|
||||
command: hostname
|
||||
with_sequence: start=1 end=100
|
@ -0,0 +1,14 @@
|
||||
#/bin/sh
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
UNIT2="$(which unit2)"
|
||||
|
||||
coverage erase
|
||||
coverage run "${UNIT2}" discover \
|
||||
--start-directory "tests" \
|
||||
--pattern '*_test.py' \
|
||||
"$@"
|
||||
coverage html
|
||||
echo coverage report is at "file://$(pwd)/htmlcov/index.html"
|
@ -1,3 +1,10 @@
|
||||
[coverage:run]
|
||||
branch = true
|
||||
source =
|
||||
mitogen
|
||||
omit =
|
||||
mitogen/compat/*
|
||||
|
||||
[flake8]
|
||||
ignore = E402,E128,W503
|
||||
exclude = mitogen/compat
|
||||
|
@ -1,63 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
timeout()
|
||||
{
|
||||
python -c '
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
deadline = time.time() + float(sys.argv[1])
|
||||
proc = subprocess.Popen(sys.argv[2:])
|
||||
while time.time() < deadline and proc.poll() is None:
|
||||
time.sleep(1.0)
|
||||
|
||||
if proc.poll() is not None:
|
||||
sys.exit(proc.returncode)
|
||||
proc.terminate()
|
||||
print
|
||||
print >> sys.stderr, "Timeout! Command was:", sys.argv[2:]
|
||||
print
|
||||
sys.exit(1)
|
||||
' "$@"
|
||||
}
|
||||
|
||||
trap 'sigint' INT
|
||||
sigint()
|
||||
{
|
||||
echo "SIGINT received, stopping.."
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_test()
|
||||
{
|
||||
echo "Running $1.."
|
||||
timeout 10 python $1 || fail=$?
|
||||
}
|
||||
|
||||
run_test tests/ansible_helpers_test.py
|
||||
run_test tests/call_function_test.py
|
||||
run_test tests/channel_test.py
|
||||
run_test tests/fakessh_test.py
|
||||
run_test tests/first_stage_test.py
|
||||
run_test tests/fork_test.py
|
||||
run_test tests/id_allocation_test.py
|
||||
run_test tests/importer_test.py
|
||||
run_test tests/latch_test.py
|
||||
run_test tests/local_test.py
|
||||
run_test tests/master_test.py
|
||||
run_test tests/minimize_source_test.py
|
||||
run_test tests/module_finder_test.py
|
||||
run_test tests/nested_test.py
|
||||
run_test tests/parent_test.py
|
||||
run_test tests/responder_test.py
|
||||
run_test tests/router_test.py
|
||||
run_test tests/select_test.py
|
||||
run_test tests/ssh_test.py
|
||||
run_test tests/testlib.py
|
||||
run_test tests/utils_test.py
|
||||
|
||||
if [ "$fail" ]; then
|
||||
echo "AT LEAST ONE TEST FAILED" >&2
|
||||
exit 1
|
||||
fi
|
@ -0,0 +1,2 @@
|
||||
lib/modules/custom_binary_producing_junk
|
||||
lib/modules/custom_binary_producing_json
|
@ -0,0 +1,4 @@
|
||||
|
||||
all: \
|
||||
lib/modules/custom_binary_producing_junk \
|
||||
lib/modules/custom_binary_producing_json
|
@ -0,0 +1,22 @@
|
||||
|
||||
# ``tests/ansible`` Directory
|
||||
|
||||
This is an an organically growing collection of integration and regression
|
||||
tests used for development and end-user bug reports.
|
||||
|
||||
It will be tidied up over time, meanwhile, the playbooks here are a useful
|
||||
demonstrator for what does and doesn't work.
|
||||
|
||||
|
||||
## ``run_ansible_playbook.sh``
|
||||
|
||||
This is necessary to set some environment variables used by future tests, as
|
||||
there appears to be no better way to inject them into the top-level process
|
||||
environment before the Mitogen connection process forks.
|
||||
|
||||
|
||||
## Running Everything
|
||||
|
||||
```
|
||||
ANSIBLE_STRATEGY=mitogen_linear ./run_ansible_playbook.sh all.yml
|
||||
```
|
@ -0,0 +1,3 @@
|
||||
- import_playbook: regression/all.yml
|
||||
- import_playbook: integration/all.yml
|
||||
|
@ -1,11 +1,15 @@
|
||||
[defaults]
|
||||
inventory = hosts
|
||||
gathering = explicit
|
||||
strategy_plugins = ../../ansible_mitogen/plugins/strategy
|
||||
strategy = mitogen
|
||||
library = modules
|
||||
action_plugins = lib/action
|
||||
library = lib/modules
|
||||
retry_files_enabled = False
|
||||
forks = 50
|
||||
|
||||
# Required by integration/runner__remote_tmp.yml
|
||||
remote_tmp = ~/.ansible/mitogen-tests/
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s
|
||||
pipelining = True
|
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import difflib
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
suffixes = [
|
||||
'-m custom_bash_old_style_module',
|
||||
'-m custom_bash_want_json_module',
|
||||
'-m custom_binary_producing_json',
|
||||
'-m custom_binary_producing_junk',
|
||||
'-m custom_binary_single_null',
|
||||
'-m custom_python_json_args_module',
|
||||
'-m custom_python_new_style_module',
|
||||
'-m custom_python_want_json_module',
|
||||
'-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,4 @@
|
||||
- import_playbook: remote_file_exists.yml
|
||||
- import_playbook: low_level_execute_command.yml
|
||||
- import_playbook: make_tmp_path.yml
|
||||
- import_playbook: transfer_data.yml
|
@ -0,0 +1,25 @@
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/action/make_tmp_path.yml
|
||||
assert:
|
||||
that: true
|
||||
|
||||
- action_passthrough:
|
||||
method: _make_tmp_path
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: out.result.startswith(ansible_remote_tmp|expanduser)
|
||||
|
||||
- stat:
|
||||
path: "{{out.result}}"
|
||||
register: st
|
||||
|
||||
- assert:
|
||||
that: st.stat.exists and st.stat.isdir and st.stat.mode == "0700"
|
||||
|
||||
- file:
|
||||
path: "{{out.result}}"
|
||||
state: absent
|
@ -0,0 +1,38 @@
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/action/remote_file_exists.yml
|
||||
assert:
|
||||
that: true
|
||||
|
||||
- file:
|
||||
path: /tmp/does-not-exist
|
||||
state: absent
|
||||
|
||||
- action_passthrough:
|
||||
method: _remote_file_exists
|
||||
args: ['/tmp/does-not-exist']
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: out.result == False
|
||||
|
||||
# ---
|
||||
|
||||
- copy:
|
||||
dest: /tmp/does-exist
|
||||
content: "I think, therefore I am"
|
||||
|
||||
- action_passthrough:
|
||||
method: _remote_file_exists
|
||||
args: ['/tmp/does-exist']
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: out.result == True
|
||||
|
||||
- file:
|
||||
path: /tmp/does-exist
|
||||
state: absent
|
||||
|
@ -0,0 +1,47 @@
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/action/transfer_data.yml
|
||||
file:
|
||||
path: /tmp/transfer-data
|
||||
state: absent
|
||||
|
||||
# Ensure it JSON-encodes dicts.
|
||||
- action_passthrough:
|
||||
method: _transfer_data
|
||||
kwargs:
|
||||
remote_path: /tmp/transfer-data
|
||||
data: {
|
||||
"I am JSON": true
|
||||
}
|
||||
|
||||
- slurp:
|
||||
src: /tmp/transfer-data
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
out.content.decode('base64') == '{"I am JSON": true}'
|
||||
|
||||
|
||||
# Ensure it handles strings.
|
||||
- action_passthrough:
|
||||
method: _transfer_data
|
||||
kwargs:
|
||||
remote_path: /tmp/transfer-data
|
||||
data: "I am text."
|
||||
|
||||
- slurp:
|
||||
src: /tmp/transfer-data
|
||||
register: out
|
||||
|
||||
- debug: msg={{out}}
|
||||
|
||||
- assert:
|
||||
that:
|
||||
out.content.decode('base64') == 'I am text.'
|
||||
|
||||
- file:
|
||||
path: /tmp/transfer-data
|
||||
state: absent
|
@ -0,0 +1,9 @@
|
||||
|
||||
#
|
||||
# This playbook imports all tests that are known to work at present.
|
||||
#
|
||||
|
||||
- import_playbook: action/all.yml
|
||||
- import_playbook: connection_loader/all.yml
|
||||
- import_playbook: runner/all.yml
|
||||
- import_playbook: playbook_semantics/all.yml
|
@ -1,9 +1,6 @@
|
||||
---
|
||||
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
|
||||
- name: simulate long running op (3 sec), wait for up to 5 sec, poll every 1 sec
|
||||
command: /bin/sleep 2
|
||||
async: 4
|
@ -0,0 +1,3 @@
|
||||
- import_playbook: local_blemished.yml
|
||||
- import_playbook: paramiko_unblemished.yml
|
||||
- import_playbook: ssh_blemished.yml
|
@ -0,0 +1,14 @@
|
||||
# Ensure 'local' connections are grabbed.
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/connection_loader__local_blemished.yml
|
||||
determine_strategy:
|
||||
|
||||
- custom_python_detect_environment:
|
||||
connection: local
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: out.mitogen_loaded or not is_mitogen
|
@ -0,0 +1,12 @@
|
||||
# Ensure paramiko connections aren't grabbed.
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/connection_loader__paramiko_unblemished.yml
|
||||
custom_python_detect_environment:
|
||||
connection: paramiko
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: not out.mitogen_loaded
|
@ -0,0 +1,14 @@
|
||||
# Ensure 'ssh' connections are grabbed.
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/connection_loader__ssh_blemished.yml
|
||||
determine_strategy:
|
||||
|
||||
- custom_python_detect_environment:
|
||||
connection: ssh
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: out.mitogen_loaded or not is_mitogen
|
@ -0,0 +1,3 @@
|
||||
- import_playbook: become_flags.yml
|
||||
- import_playbook: delegate_to.yml
|
||||
- import_playbook: environment.yml
|
@ -0,0 +1,32 @@
|
||||
#
|
||||
# Test sudo_flags respects -E.
|
||||
#
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/playbook_semantics/become_flags.yml
|
||||
assert:
|
||||
that: true
|
||||
|
||||
- name: "without -E"
|
||||
become: true
|
||||
shell: "echo $I_WAS_PRESERVED"
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: "out.stdout == ''"
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
become_flags: -E
|
||||
tasks:
|
||||
- name: "with -E"
|
||||
become: true
|
||||
shell: "echo $I_WAS_PRESERVED"
|
||||
register: out2
|
||||
environment:
|
||||
I_WAS_PRESERVED: 2
|
||||
|
||||
- assert:
|
||||
that: "out2.stdout == '2'"
|
@ -1,9 +1,6 @@
|
||||
---
|
||||
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
|
||||
#
|
||||
# delegate_to, no sudo
|
||||
#
|
@ -0,0 +1,12 @@
|
||||
- import_playbook: builtin_command_module.yml
|
||||
- import_playbook: custom_bash_old_style_module.yml
|
||||
- import_playbook: custom_bash_want_json_module.yml
|
||||
- import_playbook: custom_binary_producing_json.yml
|
||||
- import_playbook: custom_binary_producing_junk.yml
|
||||
- import_playbook: custom_binary_single_null.yml
|
||||
- import_playbook: custom_perl_json_args_module.yml
|
||||
- import_playbook: custom_perl_want_json_module.yml
|
||||
- import_playbook: custom_python_json_args_module.yml
|
||||
- import_playbook: custom_python_new_style_module.yml
|
||||
- import_playbook: custom_python_want_json_module.yml
|
||||
- import_playbook: remote_tmp.yml
|
@ -0,0 +1,17 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
gather_facts: true
|
||||
tasks:
|
||||
- name: integration/runner__builtin_command_module.yml
|
||||
command: hostname
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
out.changed and
|
||||
out.results[0].changed and
|
||||
out.results[0].cmd == ['hostname'] and
|
||||
out.results[0].item == '1' and
|
||||
out.results[0].rc == 0 and
|
||||
(out.results[0].stdout == ansible_nodename)
|
@ -0,0 +1,14 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_bash_old_style_module.yml
|
||||
custom_bash_old_style_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].msg == 'Here is my input'
|
@ -0,0 +1,14 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_bash_want_json_module.yml
|
||||
custom_bash_want_json_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].msg == 'Here is my input'
|
@ -0,0 +1,14 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_binary_producing_json.yml
|
||||
custom_binary_producing_json:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
out.changed and
|
||||
out.results[0].changed and
|
||||
out.results[0].msg == 'Hello, world.'
|
@ -0,0 +1,19 @@
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: integration/runner__custom_binary_producing_junk.yml
|
||||
custom_binary_producing_junk:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
ignore_errors: true
|
||||
register: out
|
||||
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- assert:
|
||||
that: |
|
||||
out.failed and
|
||||
out.results[0].failed and
|
||||
out.results[0].msg == 'MODULE FAILURE' and
|
||||
out.results[0].rc == 0
|
@ -0,0 +1,24 @@
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: integration/runner__custom_binary_single_null.yml
|
||||
custom_binary_single_null:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
ignore_errors: true
|
||||
register: out
|
||||
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- assert:
|
||||
that: |
|
||||
out.failed and
|
||||
out.results[0].failed and
|
||||
out.results[0].msg == 'MODULE FAILURE' and
|
||||
out.results[0].module_stdout.startswith('/bin/sh: ') and
|
||||
out.results[0].module_stdout.endswith('/custom_binary_single_null: cannot execute binary file\r\n')
|
||||
|
||||
|
||||
# Can't test this: Mitogen returns 126, 2.5.x returns 126, 2.4.x discarded the
|
||||
# return value and always returned 0.
|
||||
# out.results[0].rc == 126
|
@ -0,0 +1,15 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_perl_json_args_module.yml
|
||||
custom_perl_json_args_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].input[0].foo and
|
||||
out.results[0].message == 'I am a perl script! Here is my input.'
|
@ -0,0 +1,15 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_perl_want_json_module.yml
|
||||
custom_perl_want_json_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].input[0].foo and
|
||||
out.results[0].message == 'I am a want JSON perl script! Here is my input.'
|
@ -0,0 +1,15 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_python_json_args_module.yml
|
||||
custom_python_json_args_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].input[0].foo and
|
||||
out.results[0].msg == 'Here is my input'
|
@ -0,0 +1,15 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_python_new_style_module.yml
|
||||
custom_python_new_style_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].input[0].ANSIBLE_MODULE_ARGS.foo and
|
||||
out.results[0].msg == 'Here is my input'
|
@ -0,0 +1,15 @@
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: integration/runner__custom_python_want_json_module.yml
|
||||
custom_python_want_json_module:
|
||||
foo: true
|
||||
with_sequence: start=1 end={{end|default(1)}}
|
||||
register: out
|
||||
|
||||
- assert:
|
||||
that: |
|
||||
(not out.changed) and
|
||||
(not out.results[0].changed) and
|
||||
out.results[0].input[0].foo and
|
||||
out.results[0].msg == 'Here is my input'
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
# The ansible.cfg remote_tmp setting should be copied to the target and used
|
||||
# when generating temporary paths created by the runner.py code executing
|
||||
# remotely.
|
||||
#
|
||||
- hosts: all
|
||||
any_errors_fatal: true
|
||||
gather_facts: true
|
||||
tasks:
|
||||
- name: integration/runner__remote_tmp.yml
|
||||
bash_return_paths:
|
||||
register: output
|
||||
|
||||
- assert:
|
||||
that: output.argv0.startswith('%s/.ansible/mitogen-tests/' % ansible_user_dir)
|
||||
|
||||
- assert:
|
||||
that: output.argv1.startswith('%s/.ansible/mitogen-tests/' % ansible_user_dir)
|
@ -0,0 +1,28 @@
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from ansible.plugins.strategy import StrategyBase
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
try:
|
||||
method = getattr(self, self._task.args['method'])
|
||||
args = tuple(self._task.args.get('args', ()))
|
||||
kwargs = self._task.args.get('kwargs', {})
|
||||
|
||||
return {
|
||||
'changed': False,
|
||||
'failed': False,
|
||||
'result': method(*args, **kwargs)
|
||||
}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return {
|
||||
'changed': False,
|
||||
'failed': True,
|
||||
'msg': str(e),
|
||||
'result': e,
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
|
||||
import sys
|
||||
|
||||
from ansible.plugins.strategy import StrategyBase
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
def _get_strategy_name(self):
|
||||
frame = sys._getframe()
|
||||
while frame:
|
||||
st = frame.f_locals.get('self')
|
||||
if isinstance(st, StrategyBase):
|
||||
return '%s.%s' % (type(st).__module__, type(st).__name__)
|
||||
frame = frame.f_back
|
||||
return ''
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
return {
|
||||
'changed': False,
|
||||
'ansible_facts': {
|
||||
'strategy': self._get_strategy_name(),
|
||||
'is_mitogen': 'ansible_mitogen' in self._get_strategy_name(),
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# I am an Ansible WANT_JSON module that returns the paths to its argv[0] and
|
||||
# args file.
|
||||
|
||||
INPUT="$1"
|
||||
|
||||
[ ! -r "$INPUT" ] && {
|
||||
echo "Usage: $0 <input_file.json>" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "{"
|
||||
echo " \"changed\": false,"
|
||||
echo " \"msg\": \"Here is my input\","
|
||||
echo " \"input\": [$(< $INPUT)],"
|
||||
echo " \"argv0\": \"$0\","
|
||||
echo " \"argv1\": \"$1\""
|
||||
echo "}"
|
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# I am an Ansible old-style module.
|
||||
|
||||
INPUT=$1
|
||||
|
||||
[ ! -r "$INPUT" ] && {
|
||||
echo "Usage: $0 <input_file>" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "{"
|
||||
echo " \"changed\": false,"
|
||||
echo " \"msg\": \"Here is my input\","
|
||||
echo " \"filename\": \"$INPUT\","
|
||||
echo " \"input\": [\"$(cat $INPUT | tr \" \' )\"]"
|
||||
echo "}"
|
@ -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,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;
|
||||
}
|
Binary file not shown.
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
use utf8;
|
||||
|
||||
use JSON;
|
||||
|
||||
my $json_args = <<'END_MESSAGE';
|
||||
<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>
|
||||
END_MESSAGE
|
||||
|
||||
print encode_json({
|
||||
message => "I am a perl script! Here is my input.",
|
||||
input => [decode_json($json_args)]
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
use utf8;
|
||||
|
||||
my $WANT_JSON = 1;
|
||||
|
||||
use JSON;
|
||||
|
||||
my $json;
|
||||
{
|
||||
local $/; #Enable 'slurp' mode
|
||||
open my $fh, "<", $ARGV[0];
|
||||
$json_args = <$fh>;
|
||||
close $fh;
|
||||
}
|
||||
|
||||
print encode_json({
|
||||
message => "I am a want JSON perl script! Here is my input.",
|
||||
input => [decode_json($json_args)]
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/python
|
||||
# I am an Ansible new-style Python module. I return details about the Python
|
||||
# interpreter I run within.
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec={})
|
||||
module.exit_json(
|
||||
sys_executable=sys.executable,
|
||||
mitogen_loaded='mitogen.core' in sys.modules,
|
||||
hostname=socket.gethostname(),
|
||||
username=pwd.getpwuid(os.getuid()).pw_name,
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -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,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 "}"
|
@ -0,0 +1,11 @@
|
||||
- import_playbook: issue_109.yml
|
||||
- import_playbook: issue_113.yml
|
||||
- import_playbook: issue_118.yml
|
||||
- import_playbook: issue_122.yml
|
||||
- import_playbook: issue_131.yml
|
||||
- import_playbook: issue_140.yml
|
||||
- import_playbook: issue_152.yml
|
||||
- import_playbook: issue_152b.yml
|
||||
- import_playbook: issue_154.yml
|
||||
- import_playbook: issue_174.yml
|
||||
- import_playbook: issue_177.yml
|
@ -1,8 +1,5 @@
|
||||
---
|
||||
|
||||
# Reproduction for issue #109.
|
||||
|
||||
- hosts: all
|
||||
roles:
|
||||
- issue_109
|
||||
gather_facts: no
|
@ -1,7 +1,4 @@
|
||||
---
|
||||
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
|
||||
- name: Get auth token
|
@ -1,5 +1,3 @@
|
||||
---
|
||||
|
||||
# issue #118 repro: chmod +x not happening during script upload
|
||||
#
|
||||
- name: saytrue
|
@ -1,4 +1,3 @@
|
||||
|
||||
- hosts: all
|
||||
tasks:
|
||||
- script: scripts/print_env.sh
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue