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. +

+ +

+
+ For global career opportunities, please visit http://www.cgi.com/en/careers/working-at-cgi. +

+ +

+ To directly + apply to a UK team currently using Mitogen, contact us + regarding Open + Source Developer/DevOps opportunities. +

+
+
+ +.. raw:: html + + + + + + + + + + +
+ + + Seantis GmbH
+ www.seantis.ch +
+ + + Secure Third-Party Remote Access for Highly Regulated Industries
+ www.securelink.com +
+ + +.. raw:: html + +

Private Sponsors

+ + + + +

Defenders of Time

+ + + + +.. raw:: html + +

Productivity Lovers

+ + diff --git a/docs/getting_started.rst b/docs/getting_started.rst index aec86bd7..ddaccf87 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -320,6 +320,8 @@ remote procedure calls: User-defined types may not be used, except for: +* :py:class:`mitogen.core.Blob` +* :py:class:`mitogen.core.Secret` * :py:class:`mitogen.core.CallError` * :py:class:`mitogen.core.Context` * :py:class:`mitogen.core.Sender` diff --git a/docs/images/sponsors/a.html b/docs/images/sponsors/a.html new file mode 100644 index 00000000..dda8d08a --- /dev/null +++ b/docs/images/sponsors/a.html @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/sponsors/cgi.svg b/docs/images/sponsors/cgi.svg new file mode 100644 index 00000000..c165f687 --- /dev/null +++ b/docs/images/sponsors/cgi.svg @@ -0,0 +1 @@ +CGI_Logo_color_v2 diff --git a/docs/images/sponsors/seantis.svg b/docs/images/sponsors/seantis.svg new file mode 100644 index 00000000..506b9b74 --- /dev/null +++ b/docs/images/sponsors/seantis.svg @@ -0,0 +1 @@ +logo \ No newline at end of file diff --git a/docs/images/sponsors/securelink.svg b/docs/images/sponsors/securelink.svg new file mode 100644 index 00000000..52267a6f --- /dev/null +++ b/docs/images/sponsors/securelink.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mitogen/core.py b/mitogen/core.py index f47b8380..c8fab715 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -81,8 +81,12 @@ IS_DEAD = 999 PY3 = sys.version_info > (3,) if PY3: b = lambda s: s.encode('latin-1') + BytesType = bytes + UnicodeType = unicode else: b = str + BytesType = str + UnicodeType = unicode CHUNK_SIZE = 131072 _tls = threading.local() @@ -109,6 +113,25 @@ class LatchError(Error): pass +class Blob(BytesType): + def __repr__(self): + return '[blob: %d bytes]' % len(self) + + def __reduce__(self): + return (Blob, (BytesType(self),)) + + +class Secret(UnicodeType): + def __repr__(self): + return '[secret]' + + def __str__(self): + return UnicodeType(self) + + def __reduce__(self): + return (Secret, (UnicodeType(self),)) + + class CallError(Error): def __init__(self, fmt=None, *args): if not isinstance(fmt, Exception): @@ -310,7 +333,10 @@ class Message(object): return self._unpickle_sender elif func == '_unpickle_context': return self._unpickle_context - + elif func == 'Blob': + return Blob + elif func == 'Secret': + return Secret raise StreamError('cannot unpickle %r/%r', module, func) @property @@ -797,6 +823,11 @@ class Stream(BasicStream): #: :py:attr:`Message.auth_id` of every message received on this stream. auth_id = None + #: If not :data:`False`, indicates the stream has :attr:`auth_id` set and + #: its value is the same as :data:`mitogen.context_id` or appears in + #: :data:`mitogen.parent_ids`. + is_privileged = False + def __init__(self, router, remote_id, **kwargs): self._router = router self.remote_id = remote_id @@ -1239,6 +1270,7 @@ class IoLogger(BasicStream): class Router(object): context_class = Context max_message_size = 128 * 1048576 + unidirectional = False def __init__(self, broker): self.broker = broker @@ -1343,47 +1375,57 @@ class Router(object): except Exception: LOG.exception('%r._invoke(%r): %r crashed', self, msg, fn) - def _async_route(self, msg, stream=None): - _vv and IOLOG.debug('%r._async_route(%r, %r)', self, msg, stream) + def _async_route(self, msg, in_stream=None): + _vv and IOLOG.debug('%r._async_route(%r, %r)', self, msg, in_stream) if len(msg.data) > self.max_message_size: LOG.error('message too large (max %d bytes): %r', self.max_message_size, msg) return # Perform source verification. - if stream: + if in_stream: parent = self._stream_by_id.get(mitogen.parent_id) expect = self._stream_by_id.get(msg.auth_id, parent) - if stream != expect: + if in_stream != expect: LOG.error('%r: bad auth_id: got %r via %r, not %r: %r', - self, msg.auth_id, stream, expect, msg) + self, msg.auth_id, in_stream, expect, msg) return if msg.src_id != msg.auth_id: expect = self._stream_by_id.get(msg.src_id, parent) - if stream != expect: + if in_stream != expect: LOG.error('%r: bad src_id: got %r via %r, not %r: %r', - self, msg.src_id, stream, expect, msg) + self, msg.src_id, in_stream, expect, msg) return - if stream.auth_id is not None: - msg.auth_id = stream.auth_id + if in_stream.auth_id is not None: + msg.auth_id = in_stream.auth_id if msg.dst_id == mitogen.context_id: - return self._invoke(msg, stream) + return self._invoke(msg, in_stream) - stream = self._stream_by_id.get(msg.dst_id) - if stream is None: - stream = self._stream_by_id.get(mitogen.parent_id) + out_stream = self._stream_by_id.get(msg.dst_id) + if out_stream is None: + out_stream = self._stream_by_id.get(mitogen.parent_id) - if stream is None: + dead = False + if out_stream is None: LOG.error('%r: no route for %r, my ID is %r', self, msg, mitogen.context_id) + dead = True + + if in_stream and self.unidirectional and not dead and \ + not (in_stream.is_privileged or out_stream.is_privileged): + LOG.error('routing mode prevents forward of %r from %r -> %r', + msg, in_stream, out_stream) + dead = True + + if dead: if msg.reply_to and not msg.is_dead: msg.reply(Message.dead(), router=self) return - stream._send(msg) + out_stream._send(msg) def route(self, msg): self.broker.defer(self._async_route, msg) @@ -1551,14 +1593,15 @@ class ExternalContext(object): LOG.error('Stream had %d bytes after 2000ms', pending) self.broker.defer(stream.on_disconnect, self.broker) - def _setup_master(self, max_message_size, profiling, parent_id, - context_id, in_fd, out_fd): + def _setup_master(self, max_message_size, profiling, unidirectional, + parent_id, context_id, in_fd, out_fd): Router.max_message_size = max_message_size self.profiling = profiling if profiling: enable_profiling() self.broker = Broker() self.router = Router(self.broker) + self.router.undirectional = unidirectional self.router.add_handler( fn=self._on_shutdown_msg, handle=SHUTDOWN, @@ -1694,11 +1737,11 @@ class ExternalContext(object): self.dispatch_stopped = True def main(self, parent_ids, context_id, debug, profiling, log_level, - max_message_size, version, in_fd=100, out_fd=1, core_src_fd=101, - setup_stdio=True, setup_package=True, importer=None, - whitelist=(), blacklist=()): - self._setup_master(max_message_size, profiling, parent_ids[0], - context_id, in_fd, out_fd) + unidirectional, max_message_size, version, in_fd=100, out_fd=1, + core_src_fd=101, setup_stdio=True, setup_package=True, + importer=None, whitelist=(), blacklist=()): + self._setup_master(max_message_size, profiling, unidirectional, + parent_ids[0], context_id, in_fd, out_fd) try: try: self._setup_logging(debug, log_level) diff --git a/mitogen/fakessh.py b/mitogen/fakessh.py index 0e737c21..3ee91015 100644 --- a/mitogen/fakessh.py +++ b/mitogen/fakessh.py @@ -349,6 +349,7 @@ def run(dest, router, args, deadline=None, econtext=None): 'out_fd': sock2.fileno(), 'parent_ids': parent_ids, 'profiling': getattr(router, 'profiling', False), + 'unidirectional': getattr(router, 'unidirectional', False), 'setup_stdio': False, 'version': mitogen.__version__, },)) diff --git a/mitogen/fork.py b/mitogen/fork.py index 70737fc8..4a5627dc 100644 --- a/mitogen/fork.py +++ b/mitogen/fork.py @@ -90,10 +90,11 @@ class Stream(mitogen.parent.Stream): on_fork = None def construct(self, old_router, max_message_size, on_fork=None, - debug=False, profiling=False): + debug=False, profiling=False, unidirectional=False): # fork method only supports a tiny subset of options. super(Stream, self).construct(max_message_size=max_message_size, - debug=debug, profiling=profiling) + debug=debug, profiling=profiling, + unidirectional=False) self.on_fork = on_fork responder = getattr(old_router, 'responder', None) diff --git a/mitogen/master.py b/mitogen/master.py index 95202e35..22117a50 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -319,9 +319,34 @@ class LogForwarder(object): return 'LogForwarder(%r)' % (self._router,) -class ModuleFinder(object): - _STDLIB_PATHS = _stdlib_paths() +_STDLIB_PATHS = _stdlib_paths() + + +def is_stdlib_path(path): + return any( + os.path.commonprefix((libpath, path)) == libpath + and 'site-packages' not in path + and 'dist-packages' not in path + for libpath in _STDLIB_PATHS + ) + + +def is_stdlib_name(modname): + """Return ``True`` if `modname` appears to come from the standard + library.""" + if imp.is_builtin(modname) != 0: + return True + + module = sys.modules.get(modname) + if module is None: + return False + + # six installs crap with no __file__ + modpath = os.path.abspath(getattr(module, '__file__', '')) + return is_stdlib_path(modpath) + +class ModuleFinder(object): def __init__(self): #: Import machinery is expensive, keep :py:meth`:get_module_source` #: results around. @@ -333,27 +358,6 @@ class ModuleFinder(object): def __repr__(self): return 'ModuleFinder()' - def is_stdlib_name(self, modname): - """Return ``True`` if `modname` appears to come from the standard - library.""" - if imp.is_builtin(modname) != 0: - return True - - module = sys.modules.get(modname) - if module is None: - return False - - # six installs crap with no __file__ - modpath = os.path.abspath(getattr(module, '__file__', '')) - if 'site-packages' in modpath: - return False - - for dirname in self._STDLIB_PATHS: - if os.path.commonprefix((dirname, modpath)) == dirname: - return True - - return False - def _looks_like_script(self, path): """ Return :data:`True` if the (possibly extensionless) file at `path` @@ -515,7 +519,7 @@ class ModuleFinder(object): name for name in maybe_names if sys.modules.get(name) is not None - and not self.is_stdlib_name(name) + and not is_stdlib_name(name) and 'six.moves' not in name # TODO: crap ) )) @@ -674,7 +678,6 @@ class Broker(mitogen.core.Broker): class Router(mitogen.parent.Router): broker_class = Broker - debug = False profiling = False def __init__(self, broker=None, max_message_size=None): diff --git a/mitogen/parent.py b/mitogen/parent.py index feac28a8..5786b96a 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -563,7 +563,7 @@ class Stream(mitogen.core.Stream): def construct(self, max_message_size, remote_name=None, python_path=None, debug=False, connect_timeout=None, profiling=False, - old_router=None, **kwargs): + unidirectional=False, old_router=None, **kwargs): """Get the named context running on the local machine, creating it if it does not exist.""" super(Stream, self).construct(**kwargs) @@ -585,6 +585,7 @@ class Stream(mitogen.core.Stream): self.remote_name = remote_name self.debug = debug self.profiling = profiling + self.unidirectional = unidirectional self.max_message_size = max_message_size self.connect_deadline = time.time() + self.connect_timeout @@ -709,6 +710,7 @@ class Stream(mitogen.core.Stream): 'context_id': self.remote_id, 'debug': self.debug, 'profiling': self.profiling, + 'unidirectional': self.unidirectional, 'log_level': get_log_level(), 'whitelist': self._router.get_module_whitelist(), 'blacklist': self._router.get_module_blacklist(), @@ -1021,6 +1023,7 @@ class Router(mitogen.core.Router): klass = stream_by_method_name(method_name) kwargs.setdefault('debug', self.debug) kwargs.setdefault('profiling', self.profiling) + kwargs.setdefault('unidirectional', self.unidirectional) via = kwargs.pop('via', None) if via is not None: diff --git a/mitogen/unix.py b/mitogen/unix.py index 376ddf65..8eda7692 100644 --- a/mitogen/unix.py +++ b/mitogen/unix.py @@ -89,6 +89,7 @@ class Listener(mitogen.core.BasicStream): stream.accept(sock.fileno(), sock.fileno()) stream.name = 'unix_client.%d' % (pid,) stream.auth_id = mitogen.context_id + stream.is_privileged = True self._router.register(context, stream) sock.send(struct.pack('>LLL', context_id, mitogen.context_id, os.getpid())) diff --git a/mitogen/utils.py b/mitogen/utils.py index 480940ab..df876ea0 100644 --- a/mitogen/utils.py +++ b/mitogen/utils.py @@ -101,19 +101,25 @@ def with_router(func): return wrapper +PASSTHROUGH = ( + int, float, bool, + type(None), + mitogen.core.Context, + mitogen.core.CallError, + mitogen.core.Blob, + mitogen.core.Secret, +) + def cast(obj): if isinstance(obj, dict): return {cast(k): cast(v) for k, v in obj.iteritems()} if isinstance(obj, (list, tuple)): return [cast(v) for v in obj] - if obj is None or isinstance(obj, (int, float)): + if isinstance(obj, PASSTHROUGH): return obj if isinstance(obj, unicode): return unicode(obj) if isinstance(obj, str): return str(obj) - if isinstance(obj, (mitogen.core.Context, - mitogen.core.CallError)): - return obj raise TypeError("Cannot serialize: %r: %r" % (type(obj), obj)) diff --git a/tests/ansible/ansible.cfg b/tests/ansible/ansible.cfg index 9a2887a4..8c9f66e0 100644 --- a/tests/ansible/ansible.cfg +++ b/tests/ansible/ansible.cfg @@ -5,6 +5,7 @@ strategy_plugins = ../../ansible_mitogen/plugins/strategy action_plugins = lib/action callback_plugins = lib/callback library = lib/modules +# module_utils = lib/module_utils retry_files_enabled = False forks = 50 diff --git a/tests/ansible/integration/all.yml b/tests/ansible/integration/all.yml index 54841a67..3451f464 100644 --- a/tests/ansible/integration/all.yml +++ b/tests/ansible/integration/all.yml @@ -3,12 +3,14 @@ # This playbook imports all tests that are known to work at present. # -#- import_playbook: action/all.yml -#- import_playbook: async/all.yml -#- import_playbook: become/all.yml -#- import_playbook: connection_loader/all.yml -#- import_playbook: context_service/all.yml -#- import_playbook: playbook_semantics/all.yml -#- import_playbook: remote_tmp/all.yml -#- import_playbook: runner/all.yml +- import_playbook: action/all.yml +- import_playbook: async/all.yml +- import_playbook: become/all.yml +- import_playbook: connection_loader/all.yml +- import_playbook: context_service/all.yml +#- import_playbook: module_utils/all.yml +- import_playbook: playbook_semantics/all.yml +- import_playbook: remote_tmp/all.yml +- import_playbook: runner/all.yml - import_playbook: ssh/all.yml +- import_playbook: glibc_caches/all.yml diff --git a/tests/ansible/integration/glibc_caches/all.yml b/tests/ansible/integration/glibc_caches/all.yml new file mode 100644 index 00000000..7d524540 --- /dev/null +++ b/tests/ansible/integration/glibc_caches/all.yml @@ -0,0 +1,2 @@ + +- import_playbook: resolv_conf.yml diff --git a/tests/ansible/integration/glibc_caches/resolv_conf.yml b/tests/ansible/integration/glibc_caches/resolv_conf.yml new file mode 100644 index 00000000..d1a466e9 --- /dev/null +++ b/tests/ansible/integration/glibc_caches/resolv_conf.yml @@ -0,0 +1,38 @@ + +# This cannot run against localhost, it damages /etc + +- name: integration/glibc_caches/resolv_conf.yml + gather_facts: true + become: true + hosts: test-targets + vars: + ansible_become_pass: has_sudo_pubkey_password + tasks: + + - debug: msg={{hostvars}} + - mitogen_test_gethostbyname: + name: www.google.com + register: out + when: ansible_virtualization_type == "docker" + + - shell: cp /etc/resolv.conf /tmp/resolv.conf + when: ansible_virtualization_type == "docker" + + - shell: echo > /etc/resolv.conf + when: ansible_virtualization_type == "docker" + + - mitogen_test_gethostbyname: + name: www.google.com + register: out + ignore_errors: true + when: ansible_virtualization_type == "docker" + + - shell: cat /tmp/resolv.conf > /etc/resolv.conf + when: ansible_virtualization_type == "docker" + + - assert: + that: + - out.failed + - '"Name or service not known" in out.msg or + "Temporary failure in name resolution" in out.msg' + when: ansible_virtualization_type == "docker" diff --git a/tests/ansible/integration/module_utils/adjacent_to_playbook.yml b/tests/ansible/integration/module_utils/adjacent_to_playbook.yml new file mode 100644 index 00000000..34cf1c5d --- /dev/null +++ b/tests/ansible/integration/module_utils/adjacent_to_playbook.yml @@ -0,0 +1,17 @@ +# external2 is loaded from config path. +# external1 is loaded from integration/module_utils/module_utils/.. + +- name: integration/module_utils/adjacent_to_playbook.yml + hosts: test-targets + any_errors_fatal: true + tasks: + + - custom_python_external_module: + register: out + + - debug: msg={{out}} + - assert: + that: + - out.external1_path == "ansible/integration/module_utils/module_utils/external1.py" + - out.external2_path == "ansible/lib/module_utils/external2.py" + diff --git a/tests/ansible/integration/module_utils/adjacent_to_role.yml b/tests/ansible/integration/module_utils/adjacent_to_role.yml new file mode 100644 index 00000000..3fd3b1e6 --- /dev/null +++ b/tests/ansible/integration/module_utils/adjacent_to_role.yml @@ -0,0 +1,8 @@ +# external2 is loaded from config path. +# external1 is loaded from integration/module_utils/roles/modrole/module_utils/.. + +- name: integration/module_utils/adjacent_to_playbook.yml + hosts: test-targets + any_errors_fatal: true + roles: + - modrole diff --git a/tests/ansible/integration/module_utils/all.yml b/tests/ansible/integration/module_utils/all.yml new file mode 100644 index 00000000..920b5d1c --- /dev/null +++ b/tests/ansible/integration/module_utils/all.yml @@ -0,0 +1,6 @@ + +- import_playbook: from_config_path.yml +- import_playbook: from_config_path_pkg.yml +- import_playbook: adjacent_to_playbook.yml +- import_playbook: adjacent_to_role.yml +- import_playbook: overrides_builtin.yml diff --git a/tests/ansible/integration/module_utils/from_config_path.yml b/tests/ansible/integration/module_utils/from_config_path.yml new file mode 100644 index 00000000..e469fe32 --- /dev/null +++ b/tests/ansible/integration/module_utils/from_config_path.yml @@ -0,0 +1,15 @@ +# external1 and external2 are loaded from config path. + +- name: integration/module_utils/from_config_path.yml + hosts: test-targets + any_errors_fatal: true + tasks: + + - custom_python_external_module: + register: out + + - assert: + that: + - out.external1_path == "ansible/lib/module_utils/external1.py" + - out.external2_path == "ansible/lib/module_utils/external2.py" + diff --git a/tests/ansible/integration/module_utils/from_config_path_pkg.yml b/tests/ansible/integration/module_utils/from_config_path_pkg.yml new file mode 100644 index 00000000..5db3d124 --- /dev/null +++ b/tests/ansible/integration/module_utils/from_config_path_pkg.yml @@ -0,0 +1,14 @@ +# external1 and external2 are loaded from config path. + +- name: integration/module_utils/from_config_path.yml + hosts: test-targets + any_errors_fatal: true + tasks: + + - custom_python_external_pkg: + register: out + + - assert: + that: + - out.extmod_path == "ansible/lib/module_utils/externalpkg/extmod.py" + diff --git a/tests/ansible/integration/module_utils/module_utils/external1.py b/tests/ansible/integration/module_utils/module_utils/external1.py new file mode 100644 index 00000000..9faa8484 --- /dev/null +++ b/tests/ansible/integration/module_utils/module_utils/external1.py @@ -0,0 +1,11 @@ +# I am ansible.module_utils.external1 for any module that does not have an +# adjacent module_utils directory overriding the name, since I appear in the +# 'module_utils' path in ansible.cfg. + +from ansible.module_utils import external2 + +def path(): + return "ansible/integration/module_utils/module_utils/external1.py" + +def path2(): + return external2.path() diff --git a/tests/ansible/integration/module_utils/overrides_builtin.yml b/tests/ansible/integration/module_utils/overrides_builtin.yml new file mode 100644 index 00000000..635876f1 --- /dev/null +++ b/tests/ansible/integration/module_utils/overrides_builtin.yml @@ -0,0 +1,6 @@ + +- name: integration/module_utils/overrides_builtin.yml + hosts: test-targets + any_errors_fatal: true + roles: + - overrides_modrole diff --git a/tests/ansible/integration/module_utils/roles/modrole/library/uses_external3.py b/tests/ansible/integration/module_utils/roles/modrole/library/uses_external3.py new file mode 100644 index 00000000..78f2d71d --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/modrole/library/uses_external3.py @@ -0,0 +1,12 @@ +#!/usr/bin/python + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils import external3 + +def main(): + module = AnsibleModule(argument_spec={}) + module.exit_json(external2_path=external3.path2(), + external3_path=external3.path()) + +if __name__ == '__main__': + main() diff --git a/tests/ansible/integration/module_utils/roles/modrole/module_utils/external1.py b/tests/ansible/integration/module_utils/roles/modrole/module_utils/external1.py new file mode 100644 index 00000000..e4cd1847 --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/modrole/module_utils/external1.py @@ -0,0 +1,12 @@ +# I am ansible.module_utils.external1 for any module that does not have an +# adjacent module_utils directory overriding the name, since I appear in the +# 'module_utils' path in ansible.cfg. + +from ansible.module_utils import external2 + +def path(): + return "ansible/integration/module_utils/roles/modroel/module_utils/external1.py" + +def path2(): + return external2.path() + diff --git a/tests/ansible/integration/module_utils/roles/modrole/module_utils/external2.py b/tests/ansible/integration/module_utils/roles/modrole/module_utils/external2.py new file mode 100644 index 00000000..a00278a0 --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/modrole/module_utils/external2.py @@ -0,0 +1,3 @@ + +def path(): + return "integration/module_utils/roles/modrole/module_utils/external2.py" diff --git a/tests/ansible/integration/module_utils/roles/modrole/module_utils/external3.py b/tests/ansible/integration/module_utils/roles/modrole/module_utils/external3.py new file mode 100644 index 00000000..82a0ba9a --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/modrole/module_utils/external3.py @@ -0,0 +1,12 @@ +# I am ansible.module_utils.external1 for any module that does not have an +# adjacent module_utils directory overriding the name, since I appear in the +# 'module_utils' path in ansible.cfg. + +from ansible.module_utils import external2 + +def path(): + return "integration/module_utils/roles/modrole/module_utils/external3.py" + +def path2(): + return external2.path() + diff --git a/tests/ansible/integration/module_utils/roles/modrole/tasks/main.yml b/tests/ansible/integration/module_utils/roles/modrole/tasks/main.yml new file mode 100644 index 00000000..857abae5 --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/modrole/tasks/main.yml @@ -0,0 +1,10 @@ +--- + +- uses_external3: + register: out + +- debug: msg={{out}} +- assert: + that: + - out.external3_path == "integration/module_utils/roles/modrole/module_utils/external3.py" + - out.external2_path == "integration/module_utils/roles/modrole/module_utils/external2.py" diff --git a/tests/ansible/integration/module_utils/roles/overrides_modrole/library/uses_custom_known_hosts.py b/tests/ansible/integration/module_utils/roles/overrides_modrole/library/uses_custom_known_hosts.py new file mode 100644 index 00000000..1bf5e9a7 --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/overrides_modrole/library/uses_custom_known_hosts.py @@ -0,0 +1,12 @@ +#!/usr/bin/python + +import json +from ansible.module_utils.basic import path + +def main(): + print json.dumps({ + 'path': path() + }) + +if __name__ == '__main__': + main() diff --git a/tests/ansible/integration/module_utils/roles/overrides_modrole/module_utils/known_hosts.py b/tests/ansible/integration/module_utils/roles/overrides_modrole/module_utils/known_hosts.py new file mode 100644 index 00000000..8cf2e976 --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/overrides_modrole/module_utils/known_hosts.py @@ -0,0 +1,4 @@ +# Override basic.py with our own thing. + +def path(): + return 'ansible/integration/module_utils/roles/override_modrole/module_utils/basic.py' diff --git a/tests/ansible/integration/module_utils/roles/overrides_modrole/tasks/main.yml b/tests/ansible/integration/module_utils/roles/overrides_modrole/tasks/main.yml new file mode 100644 index 00000000..24717693 --- /dev/null +++ b/tests/ansible/integration/module_utils/roles/overrides_modrole/tasks/main.yml @@ -0,0 +1,9 @@ +--- + +- uses_custom_known_hosts: + register: out + +- debug: msg={{out}} +- assert: + that: + - out.path == "ansible/integration/module_utils/roles/override_modrole/module_utils/known_hosts.py" diff --git a/tests/ansible/integration/remote_tmp/readonly_homedir.yml b/tests/ansible/integration/remote_tmp/readonly_homedir.yml index 62435189..ffad455a 100644 --- a/tests/ansible/integration/remote_tmp/readonly_homedir.yml +++ b/tests/ansible/integration/remote_tmp/readonly_homedir.yml @@ -17,4 +17,4 @@ - name: Verify system temp directory was used. assert: that: - - out.__file__.startswith("/tmp/ansible_mitogen_") + - out.__file__.startswith("/tmp/ansible_") diff --git a/tests/ansible/lib/module_utils/external1.py b/tests/ansible/lib/module_utils/external1.py new file mode 100644 index 00000000..429d579a --- /dev/null +++ b/tests/ansible/lib/module_utils/external1.py @@ -0,0 +1,11 @@ +# I am ansible.module_utils.external1 for any module that does not have an +# adjacent module_utils directory overriding the name, since I appear in the +# 'module_utils' path in ansible.cfg. + +from ansible.module_utils import external2 + +def path(): + return "ansible/lib/module_utils/external1.py" + +def path2(): + return external2.path() diff --git a/tests/ansible/lib/module_utils/external2.py b/tests/ansible/lib/module_utils/external2.py new file mode 100644 index 00000000..c815dcdb --- /dev/null +++ b/tests/ansible/lib/module_utils/external2.py @@ -0,0 +1,3 @@ + +def path(): + return "ansible/lib/module_utils/external2.py" diff --git a/tests/ansible/lib/module_utils/externalpkg/__init__.py b/tests/ansible/lib/module_utils/externalpkg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ansible/lib/module_utils/externalpkg/extmod.py b/tests/ansible/lib/module_utils/externalpkg/extmod.py new file mode 100644 index 00000000..619be5dd --- /dev/null +++ b/tests/ansible/lib/module_utils/externalpkg/extmod.py @@ -0,0 +1,3 @@ + +def path(): + return 'ansible/lib/module_utils/externalpkg/extmod.py' diff --git a/tests/ansible/lib/modules/custom_python_external_module.py b/tests/ansible/lib/modules/custom_python_external_module.py new file mode 100644 index 00000000..507e53dd --- /dev/null +++ b/tests/ansible/lib/modules/custom_python_external_module.py @@ -0,0 +1,13 @@ +#!/usr/bin/python +# I expect the quote from modules2/module_utils/joker.py. + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils import external1 + +def main(): + module = AnsibleModule(argument_spec={}) + module.exit_json(external1_path=external1.path(), + external2_path=external1.path2()) + +if __name__ == '__main__': + main() diff --git a/tests/ansible/lib/modules/custom_python_external_pkg.py b/tests/ansible/lib/modules/custom_python_external_pkg.py new file mode 100644 index 00000000..95bd0c7b --- /dev/null +++ b/tests/ansible/lib/modules/custom_python_external_pkg.py @@ -0,0 +1,11 @@ +#!/usr/bin/python + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.externalpkg import extmod + +def main(): + module = AnsibleModule(argument_spec={}) + module.exit_json(extmod_path=extmod.path()) + +if __name__ == '__main__': + main() diff --git a/tests/ansible/lib/modules/mitogen_test_gethostbyname.py b/tests/ansible/lib/modules/mitogen_test_gethostbyname.py new file mode 100644 index 00000000..23dff9bd --- /dev/null +++ b/tests/ansible/lib/modules/mitogen_test_gethostbyname.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +# I am a module that indirectly depends on glibc cached /etc/resolv.conf state. + +import socket +from ansible.module_utils.basic import AnsibleModule + +def main(): + module = AnsibleModule(argument_spec={'name': {'type': 'str'}}) + try: + module.exit_json(addr=socket.gethostbyname(module.params['name'])) + except socket.error, e: + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + main() diff --git a/tests/module_finder_test.py b/tests/module_finder_test.py index 41ebd9ba..c4b65e11 100644 --- a/tests/module_finder_test.py +++ b/tests/module_finder_test.py @@ -22,33 +22,30 @@ class ReprTest(testlib.TestCase): class IsStdlibNameTest(testlib.TestCase): - klass = mitogen.master.ModuleFinder - - def call(self, fullname): - return self.klass().is_stdlib_name(fullname) + func = staticmethod(mitogen.master.is_stdlib_name) def test_builtin(self): import sys - self.assertTrue(self.call('sys')) + self.assertTrue(self.func('sys')) def test_stdlib_1(self): import logging - self.assertTrue(self.call('logging')) + self.assertTrue(self.func('logging')) def test_stdlib_2(self): # virtualenv only symlinks some paths to its local site-packages # directory. Ensure both halves of the search path return the correct # result. import email - self.assertTrue(self.call('email')) + self.assertTrue(self.func('email')) def test_mitogen_core(self): import mitogen.core - self.assertFalse(self.call('mitogen.core')) + self.assertFalse(self.func('mitogen.core')) def test_mitogen_fakessh(self): import mitogen.fakessh - self.assertFalse(self.call('mitogen.fakessh')) + self.assertFalse(self.func('mitogen.fakessh')) class GetModuleViaPkgutilTest(testlib.TestCase): diff --git a/tests/router_test.py b/tests/router_test.py index b32c5e4b..01f64d87 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -8,6 +8,7 @@ import unittest2 import testlib import mitogen.master +import mitogen.parent import mitogen.utils @@ -15,6 +16,12 @@ def ping(): return True +@mitogen.core.takes_router +def ping_context(other, router): + other = mitogen.parent.Context(router, other.context_id) + other.call(ping) + + @mitogen.core.takes_router def return_router_max_message_size(router): return router.max_message_size @@ -50,7 +57,7 @@ class SourceVerifyTest(testlib.RouterMixin, unittest2.TestCase): self.broker.defer(self.router._async_route, self.child2_msg, - stream=self.child1_stream) + in_stream=self.child1_stream) # Wait for IO loop to finish everything above. self.sync_with_broker() @@ -270,5 +277,39 @@ class NoRouteTest(testlib.RouterMixin, testlib.TestCase): self.assertEquals(e.args[0], mitogen.core.ChannelError.local_msg) +class UnidirectionalTest(testlib.RouterMixin, testlib.TestCase): + def test_siblings_cant_talk(self): + self.router.unidirectional = True + l1 = self.router.fork() + l2 = self.router.fork() + logs = testlib.LogCapturer() + logs.start() + e = self.assertRaises(mitogen.core.CallError, + lambda: l2.call(ping_context, l1)) + + msg = 'mitogen.core.ChannelError: Channel closed by remote end.' + self.assertTrue(msg in str(e)) + self.assertTrue('routing mode prevents forward of ' in logs.stop()) + + def test_auth_id_can_talk(self): + self.router.unidirectional = True + # One stream has auth_id stamped to that of the master, so it should be + # treated like a parent. + l1 = self.router.fork() + l1s = self.router.stream_by_id(l1.context_id) + l1s.auth_id = mitogen.context_id + l1s.is_privileged = True + + l2 = self.router.fork() + logs = testlib.LogCapturer() + logs.start() + e = self.assertRaises(mitogen.core.CallError, + lambda: l2.call(ping_context, l1)) + + msg = 'mitogen.core.CallError: Refused by policy.' + self.assertTrue(msg in str(e)) + self.assertTrue('policy refused message: ' in logs.stop()) + + if __name__ == '__main__': unittest2.main() diff --git a/tests/testlib.py b/tests/testlib.py index 0997752d..28316ba9 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -22,7 +22,8 @@ if mitogen.is_master: # TODO: shouldn't be necessary. DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') sys.path.append(DATA_DIR) -mitogen.utils.log_to_file() +if mitogen.is_master: + mitogen.utils.log_to_file() def data_path(suffix): diff --git a/tests/types_test.py b/tests/types_test.py new file mode 100644 index 00000000..44296656 --- /dev/null +++ b/tests/types_test.py @@ -0,0 +1,72 @@ + +import cStringIO + +import unittest2 + +import mitogen.core + + +class BlobTest(unittest2.TestCase): + klass = mitogen.core.Blob + + def make(self): + return self.klass('x' * 128) + + def test_repr(self): + blob = self.make() + self.assertEquals('[blob: 128 bytes]', repr(blob)) + + def test_decays_on_constructor(self): + blob = self.make() + self.assertEquals('x'*128, mitogen.core.BytesType(blob)) + + def test_decays_on_write(self): + blob = self.make() + io = cStringIO.StringIO() + io.write(blob) + self.assertEquals(128, io.tell()) + self.assertEquals('x'*128, io.getvalue()) + + def test_message_roundtrip(self): + blob = self.make() + msg = mitogen.core.Message.pickled(blob) + blob2 = msg.unpickle() + self.assertEquals(type(blob), type(blob2)) + self.assertEquals(repr(blob), repr(blob2)) + self.assertEquals(mitogen.core.BytesType(blob), + mitogen.core.BytesType(blob2)) + + +class SecretTest(unittest2.TestCase): + klass = mitogen.core.Secret + + def make(self): + return self.klass('password') + + def test_repr(self): + secret = self.make() + self.assertEquals('[secret]', repr(secret)) + + def test_decays_on_constructor(self): + secret = self.make() + self.assertEquals('password', mitogen.core.UnicodeType(secret)) + + def test_decays_on_write(self): + secret = self.make() + io = cStringIO.StringIO() + io.write(secret) + self.assertEquals(8, io.tell()) + self.assertEquals('password', io.getvalue()) + + def test_message_roundtrip(self): + secret = self.make() + msg = mitogen.core.Message.pickled(secret) + secret2 = msg.unpickle() + self.assertEquals(type(secret), type(secret2)) + self.assertEquals(repr(secret), repr(secret2)) + self.assertEquals(mitogen.core.BytesType(secret), + mitogen.core.BytesType(secret2)) + + +if __name__ == '__main__': + unittest2.main()