Merge branch 'master' into eye-of-the-token-its-the-thrill-of-the-light
commit
dc3f5730a2
@ -1,7 +1,13 @@
|
|||||||
|
.coverage
|
||||||
|
.tox
|
||||||
.venv
|
.venv
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
*.pyc
|
||||||
|
*.pyd
|
||||||
|
*.pyo
|
||||||
MANIFEST
|
MANIFEST
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
docs/_build
|
htmlcov/
|
||||||
*.egg-info
|
*.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]
|
[flake8]
|
||||||
ignore = E402,E128,W503
|
ignore = E402,E128,W503
|
||||||
exclude = mitogen/compat
|
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]
|
[defaults]
|
||||||
inventory = hosts
|
inventory = hosts
|
||||||
|
gathering = explicit
|
||||||
strategy_plugins = ../../ansible_mitogen/plugins/strategy
|
strategy_plugins = ../../ansible_mitogen/plugins/strategy
|
||||||
strategy = mitogen
|
action_plugins = lib/action
|
||||||
library = modules
|
library = lib/modules
|
||||||
retry_files_enabled = False
|
retry_files_enabled = False
|
||||||
forks = 50
|
forks = 50
|
||||||
|
|
||||||
|
# Required by integration/runner__remote_tmp.yml
|
||||||
|
remote_tmp = ~/.ansible/mitogen-tests/
|
||||||
|
|
||||||
[ssh_connection]
|
[ssh_connection]
|
||||||
ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s
|
ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s
|
||||||
pipelining = True
|
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
|
- hosts: all
|
||||||
gather_facts: false
|
any_errors_fatal: true
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- name: simulate long running op (3 sec), wait for up to 5 sec, poll every 1 sec
|
- name: simulate long running op (3 sec), wait for up to 5 sec, poll every 1 sec
|
||||||
command: /bin/sleep 2
|
command: /bin/sleep 2
|
||||||
async: 4
|
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
|
- hosts: all
|
||||||
gather_facts: false
|
any_errors_fatal: true
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
#
|
#
|
||||||
# delegate_to, no sudo
|
# 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.
|
# Reproduction for issue #109.
|
||||||
|
|
||||||
- hosts: all
|
- hosts: all
|
||||||
roles:
|
roles:
|
||||||
- issue_109
|
- issue_109
|
||||||
gather_facts: no
|
|
@ -1,7 +1,4 @@
|
|||||||
---
|
|
||||||
|
|
||||||
- hosts: all
|
- hosts: all
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- name: Get auth token
|
- name: Get auth token
|
@ -1,5 +1,3 @@
|
|||||||
---
|
|
||||||
|
|
||||||
# issue #118 repro: chmod +x not happening during script upload
|
# issue #118 repro: chmod +x not happening during script upload
|
||||||
#
|
#
|
||||||
- name: saytrue
|
- name: saytrue
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
- hosts: all
|
- hosts: all
|
||||||
tasks:
|
tasks:
|
||||||
- script: scripts/print_env.sh
|
- 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