diff --git a/.travis.yml b/.travis.yml
index 47c64a35..b37fddb9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,13 +10,16 @@ python:
- "2.7"
env:
-- MODE=mitogen MITOGEN_TEST_DISTRO=debian
-- MODE=mitogen MITOGEN_TEST_DISTRO=centos
-- MODE=debops_common ANSIBLE_VERSION=2.4.3.0
-- MODE=debops_common ANSIBLE_VERSION=2.5.1
-- MODE=ansible ANSIBLE_VERSION=2.4.3.0 MITOGEN_TEST_DISTRO=debian
-- MODE=ansible ANSIBLE_VERSION=2.5.1 MITOGEN_TEST_DISTRO=centos
-- MODE=ansible ANSIBLE_VERSION=2.5.1 MITOGEN_TEST_DISTRO=debian
+- MODE=mitogen DISTRO=debian
+- MODE=mitogen DISTRO=centos
+- MODE=debops_common VER=2.4.3.0
+- MODE=debops_common VER=2.5.1
+# Ansible tests.
+- MODE=ansible VER=2.4.3.0 DISTRO=debian
+- MODE=ansible VER=2.5.1 DISTRO=centos
+- MODE=ansible VER=2.5.1 DISTRO=debian
+# Sanity check our tests against vanilla Ansible, they should still pass.
+- MODE=ansible VER=2.5.1 DISTRO=debian STRATEGY=linear
install:
- pip install -r dev_requirements.txt
diff --git a/.travis/ansible_tests.sh b/.travis/ansible_tests.sh
index 26da7cfa..b5162ae0 100755
--- a/.travis/ansible_tests.sh
+++ b/.travis/ansible_tests.sh
@@ -3,8 +3,9 @@
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}"
TMPDIR="/tmp/ansible-tests-$$"
-ANSIBLE_VERSION="${ANSIBLE_VERSION:-2.4.3.0}"
-MITOGEN_TEST_DISTRO="${MITOGEN_TEST_DISTRO:-debian}"
+ANSIBLE_VERSION="${VER:-2.4.3.0}"
+export ANSIBLE_STRATEGY="${STRATEGY:-mitogen_linear}"
+DISTRO="${DISTRO:-debian}"
export PYTHONPATH="${PYTHONPATH}:${TRAVIS_BUILD_DIR}"
@@ -30,7 +31,7 @@ docker run \
--detach \
--publish 0.0.0.0:2201:22/tcp \
--name=target \
- mitogen/${MITOGEN_TEST_DISTRO}-test
+ mitogen/${DISTRO}-test
echo travis_fold:end:docker_setup
@@ -57,15 +58,8 @@ make -C ${TRAVIS_BUILD_DIR}/tests/ansible
echo travis_fold:end:job_setup
-echo travis_fold:start:mitogen_linear
-/usr/bin/time ./mitogen_ansible_playbook.sh \
- all.yml \
- -i "${TMPDIR}/hosts"
-echo travis_fold:end:mitogen_linear
-
-
-echo travis_fold:start:vanilla_ansible
+echo travis_fold:start:ansible
/usr/bin/time ./run_ansible_playbook.sh \
all.yml \
-i "${TMPDIR}/hosts"
-echo travis_fold:end:vanilla_ansible
+echo travis_fold:end:ansible
diff --git a/.travis/debops_common_tests.sh b/.travis/debops_common_tests.sh
index eff7c901..bdfeb146 100755
--- a/.travis/debops_common_tests.sh
+++ b/.travis/debops_common_tests.sh
@@ -4,8 +4,8 @@
TMPDIR="/tmp/debops-$$"
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}"
TARGET_COUNT="${TARGET_COUNT:-2}"
-ANSIBLE_VERSION="${ANSIBLE_VERSION:-2.4.3.0}"
-MITOGEN_TEST_DISTRO=debian # Naturally DebOps only supports Debian.
+ANSIBLE_VERSION="${VER:-2.4.3.0}"
+DISTRO=debian # Naturally DebOps only supports Debian.
export PYTHONPATH="${PYTHONPATH}:${TRAVIS_BUILD_DIR}"
@@ -60,7 +60,7 @@ do
--detach \
--publish 0.0.0.0:$port:22/tcp \
--name=target$i \
- mitogen/${MITOGEN_TEST_DISTRO}-test
+ mitogen/${DISTRO}-test
echo \
target$i \
diff --git a/.travis/mitogen_tests.sh b/.travis/mitogen_tests.sh
index a070602a..01e24963 100755
--- a/.travis/mitogen_tests.sh
+++ b/.travis/mitogen_tests.sh
@@ -1,5 +1,5 @@
#!/bin/bash -ex
# Run the Mitogen tests.
-MITOGEN_TEST_DISTRO="${MITOGEN_TEST_DISTRO:-debian}"
+MITOGEN_TEST_DISTRO="${DISTRO:-debian}"
MITOGEN_LOG_LEVEL=debug PYTHONPATH=. ${TRAVIS_BUILD_DIR}/run_tests
diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py
index 93ad0df6..2b031a9f 100644
--- a/ansible_mitogen/connection.py
+++ b/ansible_mitogen/connection.py
@@ -58,6 +58,10 @@ def _connect_local(spec):
}
}
+def wrap_or_none(klass, value):
+ if value is not None:
+ return klass(value)
+
def _connect_ssh(spec):
if C.HOST_KEY_CHECKING:
@@ -71,7 +75,7 @@ def _connect_ssh(spec):
'check_host_keys': check_host_keys,
'hostname': spec['remote_addr'],
'username': spec['remote_user'],
- 'password': spec['password'],
+ 'password': wrap_or_none(mitogen.core.Secret, spec['password']),
'port': spec['port'],
'python_path': spec['python_path'],
'identity_file': spec['private_key_file'],
@@ -142,7 +146,7 @@ def _connect_su(spec):
'enable_lru': True,
'kwargs': {
'username': spec['become_user'],
- 'password': spec['become_pass'],
+ 'password': wrap_or_none(mitogen.core.Secret, spec['become_pass']),
'python_path': spec['python_path'],
'su_path': spec['become_exe'],
'connect_timeout': spec['timeout'],
@@ -156,7 +160,7 @@ def _connect_sudo(spec):
'enable_lru': True,
'kwargs': {
'username': spec['become_user'],
- 'password': spec['become_pass'],
+ 'password': wrap_or_none(mitogen.core.Secret, spec['become_pass']),
'python_path': spec['python_path'],
'sudo_path': spec['become_exe'],
'connect_timeout': spec['timeout'],
@@ -171,7 +175,7 @@ def _connect_mitogen_su(spec):
'method': 'su',
'kwargs': {
'username': spec['remote_user'],
- 'password': spec['password'],
+ 'password': wrap_or_none(mitogen.core.Secret, spec['password']),
'python_path': spec['python_path'],
'su_path': spec['become_exe'],
'connect_timeout': spec['timeout'],
@@ -185,7 +189,7 @@ def _connect_mitogen_sudo(spec):
'method': 'sudo',
'kwargs': {
'username': spec['remote_user'],
- 'password': spec['password'],
+ 'password': wrap_or_none(mitogen.core.Secret, spec['password']),
'python_path': spec['python_path'],
'sudo_path': spec['become_exe'],
'connect_timeout': spec['timeout'],
@@ -581,7 +585,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
"""
self.call(ansible_mitogen.target.write_path,
mitogen.utils.cast(out_path),
- mitogen.utils.cast(data),
+ mitogen.core.Blob(data),
mode=mode,
utimes=utimes)
diff --git a/ansible_mitogen/module_finder.py b/ansible_mitogen/module_finder.py
new file mode 100644
index 00000000..1b2e1850
--- /dev/null
+++ b/ansible_mitogen/module_finder.py
@@ -0,0 +1,137 @@
+
+import imp
+import os
+import sys
+import mitogen.master
+
+
+class Name(object):
+ def __str__(self):
+ return self.identifier
+
+ def __repr__(self):
+ return 'Name(%r)' % (self.identifier,)
+
+ def __init__(self, identifier):
+ self.identifier = identifier
+
+ def head(self):
+ head, _, tail = self.identifier.partition('.')
+ return head
+
+ def tail(self):
+ head, _, tail = self.identifier.partition('.')
+ return tail
+
+ def pop_n(self, level):
+ name = self.identifier
+ for _ in xrange(level):
+ if '.' not in name:
+ return None
+ name, _, _ = self.identifier.rpartition('.')
+ return Name(name)
+
+ def append(self, part):
+ return Name('%s.%s' % (self.identifier, part))
+
+
+class Module(object):
+ def __init__(self, name, path, kind=imp.PY_SOURCE, parent=None):
+ self.name = Name(name)
+ self.path = path
+ if kind == imp.PKG_DIRECTORY:
+ self.path = os.path.join(self.path, '__init__.py')
+ self.kind = kind
+ self.parent = parent
+
+ def fullname(self):
+ bits = [str(self.name)]
+ while self.parent:
+ bits.append(str(self.parent.name))
+ self = self.parent
+ return '.'.join(reversed(bits))
+
+ def __repr__(self):
+ return 'Module(%r, path=%r, parent=%r)' % (
+ self.name,
+ self.path,
+ self.parent,
+ )
+
+ def dirname(self):
+ return os.path.dirname(self.path)
+
+ def code(self):
+ fp = open(self.path)
+ try:
+ return compile(fp.read(), str(self.name), 'exec')
+ finally:
+ fp.close()
+
+
+def find(name, path=(), parent=None):
+ """
+ (Name, search path) -> Module instance or None.
+ """
+ try:
+ tup = imp.find_module(name.head(), list(path))
+ except ImportError:
+ return parent
+
+ fp, path, (suffix, mode, kind) = tup
+ if fp:
+ fp.close()
+
+ module = Module(name.head(), path, kind, parent)
+ if name.tail():
+ return find_relative(module, Name(name.tail()), path)
+ return module
+
+
+def find_relative(parent, name, path=()):
+ path = [parent.dirname()] + list(path)
+ return find(name, path, parent=parent)
+
+
+def path_pop(s, n):
+ return os.pathsep.join(s.split(os.pathsep)[-n:])
+
+
+def scan(module, path):
+ scanner = mitogen.master.scan_code_imports(module.code())
+ for level, modname_s, fromlist in scanner:
+ modname = Name(modname_s)
+ if level == -1:
+ imported = find_relative(module, modname, path)
+ elif level:
+ subpath = [path_pop(module.dirname(), level)] + list(path)
+ imported = find(modname.pop_n(level), subpath)
+ else:
+ imported = find(modname.pop_n(level), path)
+
+ if imported and mitogen.master.is_stdlib_path(imported.path):
+ continue
+
+ if imported and fromlist:
+ have = False
+ for fromname_s in fromlist:
+ fromname = modname.append(fromname_s)
+ f_imported = find_relative(imported, fromname, path)
+ if f_imported and f_imported.fullname() == fromname.identifier:
+ have = True
+ yield fromname, f_imported, None
+ if have:
+ continue
+
+ if imported:
+ yield modname, imported
+
+
+module = Module(name='ansible_module_apt', path='/Users/dmw/src/mitogen/.venv/lib/python2.7/site-packages/ansible/modules/packaging/os/apt.py')
+path = tuple(sys.path)
+path = ('/Users/dmw/src/ansible/lib',) + path
+
+
+from pprint import pprint
+for name, imported in scan(module, sys.path):
+ print '%s: %s' % (name, imported and (str(name) == imported.fullname()))
diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py
index 30c0ca7b..bd2b0cc7 100644
--- a/ansible_mitogen/runner.py
+++ b/ansible_mitogen/runner.py
@@ -37,6 +37,7 @@ how to build arguments for it, preseed related data, etc.
from __future__ import absolute_import
import cStringIO
+import ctypes
import json
import logging
import os
@@ -57,6 +58,17 @@ except ImportError:
import ansible.module_utils.basic
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
+# For tasks that modify /etc/resolv.conf, non-Debian derivative glibcs cache
+# resolv.conf at startup and never implicitly reload it. Cope with that via an
+# explicit call to res_init() on each task invocation. BSD-alikes export it
+# directly, Linux #defines it as "__res_init".
+libc = ctypes.CDLL(None)
+libc__res_init = None
+for symbol in 'res_init', '__res_init':
+ try:
+ libc__res_init = getattr(libc, symbol)
+ except AttributeError:
+ pass
LOG = logging.getLogger(__name__)
@@ -397,6 +409,8 @@ class NewStyleRunner(ScriptRunner):
# module, but this has never been a bug report. Instead act like an
# interpreter that had its script piped on stdin.
self._argv = TemporaryArgv([''])
+ if libc__res_init:
+ libc__res_init()
def revert(self):
self._argv.revert()
diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py
index 96661c4c..38bca7da 100644
--- a/ansible_mitogen/services.py
+++ b/ansible_mitogen/services.py
@@ -255,7 +255,7 @@ class ContextService(mitogen.service.Service):
except AttributeError:
raise Error('unsupported method: %(transport)s' % spec)
- context = method(via=via, **spec['kwargs'])
+ context = method(via=via, unidirectional=True, **spec['kwargs'])
if via and spec.get('enable_lru'):
self._update_lru(context, spec, via)
else:
@@ -489,8 +489,11 @@ class FileService(mitogen.service.Service):
# odd-sized messages waste one tiny write() per message on the trailer.
# Therefore subtract 10 bytes pickle overhead + 24 bytes header.
IO_SIZE = mitogen.core.CHUNK_SIZE - (mitogen.core.Stream.HEADER_LEN + (
- len(mitogen.core.Message.pickled(' ' * mitogen.core.CHUNK_SIZE).data) -
- mitogen.core.CHUNK_SIZE
+ len(
+ mitogen.core.Message.pickled(
+ mitogen.core.Blob(' ' * mitogen.core.CHUNK_SIZE)
+ ).data
+ ) - mitogen.core.CHUNK_SIZE
))
def _schedule_pending_unlocked(self, state):
@@ -507,7 +510,7 @@ class FileService(mitogen.service.Service):
s = fp.read(self.IO_SIZE)
if s:
state.unacked += len(s)
- sender.send(s)
+ sender.send(mitogen.core.Blob(s))
else:
# File is done. Cause the target's receive loop to exit by
# closing the sender, close the file, and remove the job entry.
diff --git a/docs/ansible.rst b/docs/ansible.rst
index e08fca9b..74f95679 100644
--- a/docs/ansible.rst
+++ b/docs/ansible.rst
@@ -43,11 +43,31 @@ it can only ensure the module executes as quickly as possible.
of magnitude** compared to SSH pipelining, with around 5x fewer frames
traversing the network in a typical run.
-* **No writes to the target's filesystem occur**, unless explicitly triggered
- by a playbook step. In all typical configurations, Ansible repeatedly
- rewrites and extracts ZIP files to multiple temporary directories on the
- target. Since no temporary files are used, security issues relating to those
- files in cross-account scenarios are entirely avoided.
+* **Fewer writes to the target filesystem occur**. In typical configurations,
+ Ansible repeatedly rewrites and extracts ZIP files to multiple temporary
+ directories on the target. Security issues relating to temporarily files in
+ cross-account scenarios are entirely avoided.
+
+
+Installation
+------------
+
+1. Thoroughly review the documented behavioural differences.
+2. Verify Ansible 2.3/2.4/2.5 and Python 2.7 are listed in ``ansible --version``
+ output.
+3. Download and extract https://github.com/dw/mitogen/archive/master.zip
+4. Modify ``ansible.cfg``:
+
+ .. code-block:: dosini
+
+ [defaults]
+ strategy_plugins = /path/to/mitogen-master/ansible_mitogen/plugins/strategy
+ strategy = mitogen_linear
+
+ The ``strategy`` key is optional. If omitted, the
+ ``ANSIBLE_STRATEGY=mitogen_linear`` environment variable can be set on a
+ per-run basis. Like ``mitogen_linear``, the ``mitogen_free`` strategy exists
+ to mimic the ``free`` strategy.
Demo
@@ -86,27 +106,6 @@ Testimonials
can't quite believe it."
-Installation
-------------
-
-1. Thoroughly review the documented behavioural differences.
-2. Verify Ansible 2.3/2.4/2.5 and Python 2.7 are listed in ``ansible --version``
- output.
-3. Download and extract https://github.com/dw/mitogen/archive/master.zip
-4. Modify ``ansible.cfg``:
-
- .. code-block:: dosini
-
- [defaults]
- strategy_plugins = /path/to/mitogen-master/ansible_mitogen/plugins/strategy
- strategy = mitogen_linear
-
- The ``strategy`` key is optional. If omitted, the
- ``ANSIBLE_STRATEGY=mitogen_linear`` environment variable can be set on a
- per-run basis. Like ``mitogen_linear``, the ``mitogen_free`` strategy exists
- to mimic the ``free`` strategy.
-
-
Noteworthy Differences
----------------------
diff --git a/docs/contributors.rst b/docs/contributors.rst
new file mode 100644
index 00000000..39d545ff
--- /dev/null
+++ b/docs/contributors.rst
@@ -0,0 +1,122 @@
+
+Contributors
+============
+
+Mitogen is production ready exclusively thanks to the careful testing, gracious
+sponsorship and outstanding future-thinking of its early adopters.
+
+.. raw:: html
+
+
+
+
+
+
+
+
+
+ Founded in 1976, CGI is one of the world’s largest IT and business
+ consulting services firms, helping clients achieve their goals,
+ including becoming customer-centric digital organizations.
+