Merge remote-tracking branch 'origin/master' into stable v0.2.2

pull/862/head v0.2.2
David Wilson 8 years ago
commit b1c7afa948

@ -0,0 +1,15 @@
Feel free to write an issue in your preferred format, however if in doubt, use
the following checklist as a guide for what to include.
* Have you tried the latest master version from Git?
* Mention your host and target OS and versions
* Mention your host and target Python versions
* If reporting a performance issue, mention the number of targets and a rough
description of your workload (lots of copies, lots of tiny file edits, etc.)
* If reporting a crash or hang in Ansible, please rerun with -vvvv and include
the last 200 lines of output, along with a full copy of any traceback or
error text in the log. Beware "-vvvv" may include secret data! Edit as
necessary before posting.
* If reporting any kind of problem with Ansible, please include the Ansible
version along with output of "ansible-config dump --only-changed".

@ -47,10 +47,10 @@ matrix:
env: MODE=debops_common VER=2.4.3.0
# 2.5.5; 2.7 -> 2.7
- python: "2.7"
env: MODE=debops_common VER=2.5.5
env: MODE=debops_common VER=2.6.1
# 2.5.5; 3.6 -> 2.7
- python: "3.6"
env: MODE=debops_common VER=2.5.5
env: MODE=debops_common VER=2.6.1
# ansible_mitogen tests.
# 2.4.3.0; Debian; 2.7 -> 2.7
@ -59,22 +59,60 @@ matrix:
# 2.5.5; Debian; 2.7 -> 2.7
- python: "2.7"
env: MODE=ansible VER=2.5.5 DISTRO=debian
# 2.5.5; CentOS; 2.7 -> 2.7
# 2.6.0; Debian; 2.7 -> 2.7
- python: "2.7"
env: MODE=ansible VER=2.5.5 DISTRO=centos7
# 2.5.5; CentOS; 2.7 -> 2.6
env: MODE=ansible VER=2.6.0 DISTRO=debian
# 2.6.1; Debian; 2.7 -> 2.7
- python: "2.7"
env: MODE=ansible VER=2.5.5 DISTRO=centos6
# 2.5.5; CentOS; 2.6 -> 2.7
env: MODE=ansible VER=2.6.1 DISTRO=debian
# Centos 7 Python2
# Latest
- python: "2.6"
env: MODE=ansible VER=2.6.1 DISTRO=centos7
# Backward Compatiability
- python: "2.7"
env: MODE=ansible VER=2.5.5 DISTRO=centos7
- python: "2.7"
env: MODE=ansible VER=2.6.0 DISTRO=centos7
- python: "2.7"
env: MODE=ansible VER=2.6.1 DISTRO=centos7
# Centos 7 Python3
- python: "3.6"
env: MODE=ansible VER=2.5.5 DISTRO=centos7
# 2.5.5; CentOS; 2.6 -> 2.6
- python: "3.6"
env: MODE=ansible VER=2.6.0 DISTRO=centos7
- python: "3.6"
env: MODE=ansible VER=2.6.1 DISTRO=centos7
# Centos 6 Python2
# Latest
- python: "2.6"
env: MODE=ansible VER=2.6.1 DISTRO=centos6
# Backward Compatiability
- python: "2.6"
env: MODE=ansible VER=2.5.5 DISTRO=centos6
# 2.5.5; Debian; 3.6 -> 2.7
- python: "2.6"
env: MODE=ansible VER=2.6.0 DISTRO=centos6
- python: "2.7"
env: MODE=ansible VER=2.6.1 DISTRO=centos6
# Centos 6 Python3
- python: "3.6"
env: MODE=ansible VER=2.5.5 DISTRO=centos6
- python: "3.6"
env: MODE=ansible VER=2.6.0 DISTRO=centos6
- python: "3.6"
env: MODE=ansible VER=2.6.1 DISTRO=centos6
# Sanity check our tests against vanilla Ansible, they should pass.
- python: "2.7"
env: MODE=ansible VER=2.5.5 DISTRO=debian STRATEGY=linear
- python: "2.7"
env: MODE=ansible VER=2.6.0 DISTRO=debian STRATEGY=linear
- python: "2.7"
env: MODE=ansible VER=2.6.1 DISTRO=debian STRATEGY=linear

@ -3,7 +3,7 @@
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}"
TMPDIR="/tmp/ansible-tests-$$"
ANSIBLE_VERSION="${VER:-2.5.5}"
ANSIBLE_VERSION="${VER:-2.6.1}"
export ANSIBLE_STRATEGY="${STRATEGY:-mitogen_linear}"
DISTRO="${DISTRO:-debian}"

@ -4,7 +4,7 @@
TMPDIR="/tmp/debops-$$"
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-`pwd`}"
TARGET_COUNT="${TARGET_COUNT:-2}"
ANSIBLE_VERSION="${VER:-2.5.5}"
ANSIBLE_VERSION="${VER:-2.6.1}"
DISTRO=debian # Naturally DebOps only supports Debian.
export PYTHONPATH="${PYTHONPATH}:${TRAVIS_BUILD_DIR}"
@ -81,10 +81,10 @@ echo travis_fold:end:job_setup
echo travis_fold:start:first_run
/usr/bin/time debops common
/usr/bin/time debops common "$@"
echo travis_fold:end:first_run
echo travis_fold:start:second_run
/usr/bin/time debops common
/usr/bin/time debops common "$@"
echo travis_fold:end:second_run

@ -1,4 +1,4 @@
# Mitogen
<!-- [![Build Status](https://travis-ci.org/dw/mitogen.png?branch=master)](https://travis-ci.org/dw/mitogen}) -->
<a href="https://mitogen.readthedocs.io/">Please see the documentation</a>.

@ -44,9 +44,10 @@ import ansible.utils.shlex
import mitogen.unix
import mitogen.utils
import ansible_mitogen.target
import ansible_mitogen.parsing
import ansible_mitogen.process
import ansible_mitogen.services
import ansible_mitogen.target
LOG = logging.getLogger(__name__)
@ -173,6 +174,20 @@ def _connect_sudo(spec):
}
def _connect_doas(spec):
return {
'method': 'doas',
'enable_lru': True,
'kwargs': {
'username': spec['become_user'],
'password': wrap_or_none(mitogen.core.Secret, spec['become_pass']),
'python_path': spec['python_path'],
'doas_path': spec['become_exe'],
'connect_timeout': spec['timeout'],
}
}
def _connect_mitogen_su(spec):
# su as a first-class proxied connection, not a become method.
return {
@ -202,6 +217,20 @@ def _connect_mitogen_sudo(spec):
}
def _connect_mitogen_doas(spec):
# doas as a first-class proxied connection, not a become method.
return {
'method': 'doas',
'kwargs': {
'username': spec['remote_user'],
'password': wrap_or_none(mitogen.core.Secret, spec['password']),
'python_path': spec['python_path'],
'doas_path': spec['become_exe'],
'connect_timeout': spec['timeout'],
}
}
CONNECTION_METHOD = {
'docker': _connect_docker,
'jail': _connect_jail,
@ -213,11 +242,24 @@ CONNECTION_METHOD = {
'ssh': _connect_ssh,
'su': _connect_su,
'sudo': _connect_sudo,
'doas': _connect_doas,
'mitogen_su': _connect_mitogen_su,
'mitogen_sudo': _connect_mitogen_sudo,
'mitogen_doas': _connect_mitogen_doas,
}
def parse_python_path(s):
"""
Given the string set for ansible_python_interpeter, parse it using shell
syntax and return an appropriate argument vector.
"""
if not s:
return None
return ansible.utils.shlex.shlex_split(s)
def config_from_play_context(transport, inventory_name, connection):
"""
Return a dict representing all important connection configuration, allowing
@ -235,7 +277,7 @@ def config_from_play_context(transport, inventory_name, connection):
'become_pass': connection._play_context.become_pass,
'password': connection._play_context.password,
'port': connection._play_context.port,
'python_path': connection.python_path,
'python_path': parse_python_path(connection.python_path),
'private_key_file': connection._play_context.private_key_file,
'ssh_executable': connection._play_context.ssh_executable,
'timeout': connection._play_context.timeout,
@ -284,7 +326,7 @@ def config_from_hostvars(transport, inventory_name, connection,
'password': (hostvars.get('ansible_ssh_pass') or
hostvars.get('ansible_password')),
'port': hostvars.get('ansible_port'),
'python_path': hostvars.get('ansible_python_interpreter'),
'python_path': parse_python_path(hostvars.get('ansible_python_interpreter')),
'private_key_file': (hostvars.get('ansible_ssh_private_key_file') or
hostvars.get('ansible_private_key_file')),
'mitogen_via': hostvars.get('mitogen_via'),
@ -302,20 +344,24 @@ class Connection(ansible.plugins.connection.ConnectionBase):
#: mitogen.master.Router for this worker.
router = None
#: mitogen.master.Context representing the parent Context, which is
#: mitogen.parent.Context representing the parent Context, which is
#: presently always the connection multiplexer process.
parent = None
#: mitogen.master.Context connected to the target user account on the
#: target machine (i.e. via sudo).
#: mitogen.parent.Context for the target account on the target, possibly
#: reached via become.
context = None
#: mitogen.master.Context connected to the fork parent process in the
#: mitogen.parent.Context for the login account on the target. This is
#: always the login account, even when become=True.
login_context = None
#: mitogen.parent.Context connected to the fork parent process in the
#: target user account.
fork_context = None
#: Only sudo and su are supported for now.
become_methods = ['sudo', 'su']
#: Only sudo, su, and doas are supported for now.
become_methods = ['sudo', 'su', 'doas']
#: Set to 'ansible_python_interpreter' by on_action_run().
python_path = None
@ -450,7 +496,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
"""
Establish a connection to the master process's UNIX listener socket,
constructing a mitogen.master.Router to communicate with the master,
and a mitogen.master.Context to represent it.
and a mitogen.parent.Context to represent it.
Depending on the original transport we should emulate, trigger one of
the _connect_*() service calls defined above to cause the master
@ -487,6 +533,11 @@ class Connection(ansible.plugins.connection.ConnectionBase):
raise ansible.errors.AnsibleConnectionFailure(dct['msg'])
self.context = dct['context']
if self._play_context.become:
self.login_context = dct['via']
else:
self.login_context = self.context
self.fork_context = dct['init_child_result']['fork_context']
self.home_dir = dct['init_child_result']['home_dir']
@ -504,6 +555,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
)
self.context = None
self.fork_context = None
self.login_context = None
if self.broker and not new_task:
self.broker.shutdown()
self.broker.join()
@ -514,11 +567,18 @@ class Connection(ansible.plugins.connection.ConnectionBase):
"""
Start a function call to the target.
:param bool use_login_context:
If present and :data:`True`, send the call to the login account
context rather than the optional become user context.
:returns:
mitogen.core.Receiver that receives the function call result.
"""
self._connect()
return self.context.call_async(func, *args, **kwargs)
if kwargs.pop('use_login_context', None):
call_context = self.login_context
else:
call_context = self.context
return call_context.call_async(func, *args, **kwargs)
def call(self, func, *args, **kwargs):
"""
@ -533,8 +593,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
try:
return self.call_async(func, *args, **kwargs).get().unpickle()
finally:
LOG.debug('Call took %d ms: %s%r', 1000 * (time.time() - t0),
func.__name__, args)
LOG.debug('Call took %d ms: %r', 1000 * (time.time() - t0),
mitogen.parent.CallSpec(func, args, kwargs))
def create_fork_child(self):
"""

@ -34,15 +34,20 @@ import sys
import mitogen.core
import mitogen.utils
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class Handler(logging.Handler):
"""
Use Mitogen's log format, but send the result to a Display method.
"""
def __init__(self, display, normal_method):
def __init__(self, normal_method):
logging.Handler.__init__(self)
self.formatter = mitogen.utils.log_get_formatter()
self.display = display
self.normal_method = normal_method
#: Set of target loggers that produce warnings and errors that spam the
@ -62,41 +67,34 @@ class Handler(logging.Handler):
s = '[pid %d] %s' % (os.getpid(), self.format(record))
if record.levelno >= logging.ERROR:
self.display.error(s, wrap_text=False)
display.error(s, wrap_text=False)
elif record.levelno >= logging.WARNING:
self.display.warning(s, formatted=True)
display.warning(s, formatted=True)
else:
self.normal_method(s)
def find_display():
"""
Find the CLI tool's display variable somewhere up the stack. Why god why,
right? Because it's the the simplest way to get access to the verbosity
configured on the command line.
"""
f = sys._getframe()
while f:
if 'display' in f.f_locals:
return f.f_locals['display']
f = f.f_back
def setup():
"""
Install a handler for Mitogen's logger to redirect it into the Ansible
display framework, and prevent propagation to the root logger.
"""
display = find_display()
logging.getLogger('ansible_mitogen').handlers = [Handler(display, display.vvv)]
mitogen.core.LOG.handlers = [Handler(display, display.vvv)]
mitogen.core.IOLOG.handlers = [Handler(display, display.vvvv)]
logging.getLogger('ansible_mitogen').handlers = [Handler(display.vvv)]
mitogen.core.LOG.handlers = [Handler(display.vvv)]
mitogen.core.IOLOG.handlers = [Handler(display.vvvv)]
mitogen.core.IOLOG.propagate = False
if display.verbosity > 2:
logging.getLogger('ansible_mitogen').setLevel(logging.DEBUG)
mitogen.core.LOG.setLevel(logging.DEBUG)
logging.getLogger('ansible_mitogen').setLevel(logging.DEBUG)
else:
# Mitogen copies the active log level into new children, allowing them
# to filter tiny messages before they hit the network, and therefore
# before they wake the IO loop. Explicitly setting INFO saves ~4%
# running against just the local machine.
mitogen.core.LOG.setLevel(logging.ERROR)
logging.getLogger('ansible_mitogen').setLevel(logging.ERROR)
if display.verbosity > 3:
mitogen.core.IOLOG.setLevel(logging.DEBUG)
logging.getLogger('ansible_mitogen').setLevel(logging.DEBUG)

@ -188,21 +188,23 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
except AttributeError:
s = ansible.constants.DEFAULT_REMOTE_TMP # <=2.4.x
return self._remote_expand_user(s)
return self._remote_expand_user(s, sudoable=False)
def _make_tmp_path(self, remote_user=None):
"""
Replace the base implementation's use of shell to implement mkdtemp()
with an actual call to mkdtemp().
with an actual call to mkdtemp(). Like vanilla, the directory is always
created in the login account context.
"""
LOG.debug('_make_tmp_path(remote_user=%r)', remote_user)
# _make_tmp_path() is basically a global stashed away as Shell.tmpdir.
# The copy action plugin violates layering and grabs this attribute
# directly.
self._connection._shell.tmpdir = self.call(
self._connection._shell.tmpdir = self._connection.call(
ansible_mitogen.target.make_temp_directory,
base_dir=self._get_remote_tmp(),
use_login_context=True,
)
LOG.debug('Temporary directory: %r', self._connection._shell.tmpdir)
self._cleanup_remote_tmp = True
@ -280,20 +282,26 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
"""
Replace the base implementation's attempt to emulate
os.path.expanduser() with an actual call to os.path.expanduser().
:param bool sudoable:
If :data:`True`, indicate unqualified tilde ("~" with no username)
should be evaluated in the context of the login account, not any
become_user.
"""
LOG.debug('_remote_expand_user(%r, sudoable=%r)', path, sudoable)
if not path.startswith('~'):
# /home/foo -> /home/foo
return path
if path == '~':
# ~ -> /home/dmw
return self._connection.homedir
if path.startswith('~/'):
# ~/.ansible -> /home/dmw/.ansible
return os.path.join(self._connection.homedir, path[2:])
if path.startswith('~'):
# ~root/.ansible -> /root/.ansible
return self.call(os.path.expanduser, mitogen.utils.cast(path))
if sudoable or not self._play_context.become:
if path == '~':
# ~ -> /home/dmw
return self._connection.homedir
if path.startswith('~/'):
# ~/.ansible -> /home/dmw/.ansible
return os.path.join(self._connection.homedir, path[2:])
# ~root/.ansible -> /root/.ansible
return self.call(os.path.expanduser, mitogen.utils.cast(path),
use_login_context=not sudoable)
def get_task_timeout_secs(self):
"""

@ -0,0 +1,84 @@
# 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
from __future__ import unicode_literals
import mitogen.core
def parse_script_interpreter(source):
"""
Parse the script interpreter portion of a UNIX hashbang using the rules
Linux uses.
:param str source: String like "/usr/bin/env python".
:returns:
Tuple of `(interpreter, arg)`, where `intepreter` is the script
interpreter and `arg` is its sole argument if present, otherwise
:py:data:`None`.
"""
# Find terminating newline. Assume last byte of binprm_buf if absent.
nl = source.find(b'\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[0:nl].strip().split(None, 1)
if len(bits) == 1:
return mitogen.core.to_text(bits[0]), None
return mitogen.core.to_text(bits[0]), mitogen.core.to_text(bits[1])
def parse_hashbang(source):
"""
Parse a UNIX "hashbang line" using the syntax supported by Linux.
:param str source: String like "#!/usr/bin/env python".
: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(b'#!'):
return None, None
return parse_script_interpreter(source[2:])

@ -48,6 +48,7 @@ import ansible.module_utils
import mitogen.core
import ansible_mitogen.loaders
import ansible_mitogen.parsing
import ansible_mitogen.target
@ -56,34 +57,6 @@ NO_METHOD_MSG = 'Mitogen: no invocation method found for: '
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(b'#!'):
return None, None
# Find terminating newline. Assume last byte of binprm_buf if absent.
nl = source.find(b'\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 mitogen.core.to_text(bits[0]), None
return mitogen.core.to_text(bits[0]), mitogen.core.to_text(bits[1])
class Invocation(object):
"""
Collect up a module's execution environment then use it to invoke
@ -214,27 +187,51 @@ class ScriptPlanner(BinaryPlanner):
Common functionality for script module planners -- handle interpreter
detection and rewrite.
"""
def _rewrite_interpreter(self, path):
"""
Given the original interpreter binary extracted from the script's
interpreter line, look up the associated `ansible_*_interpreter`
variable, render it and return it.
:param str path:
Absolute UNIX path to original interpreter.
:returns:
Shell fragment prefix used to execute the script via "/bin/sh -c".
While `ansible_*_interpreter` documentation suggests shell isn't
involved here, the vanilla implementation uses it and that use is
exploited in common playbooks.
"""
try:
key = u'ansible_%s_interpreter' % os.path.basename(path).strip()
template = self._inv.task_vars[key]
except KeyError:
return path
return mitogen.utils.cast(
self._inv.templar.template(self._inv.task_vars[key])
)
def _get_interpreter(self):
interpreter, arg = parse_script_interpreter(
path, arg = ansible_mitogen.parsing.parse_hashbang(
self._inv.module_source
)
if interpreter is None:
if path is None:
raise ansible.errors.AnsibleError(NO_INTERPRETER_MSG % (
self._inv.module_name,
))
key = u'ansible_%s_interpreter' % os.path.basename(interpreter).strip()
try:
template = self._inv.task_vars[key].strip()
return self._inv.templar.template(template), arg
except KeyError:
return interpreter, arg
fragment = self._rewrite_interpreter(path)
if arg:
fragment += ' ' + arg
return fragment, path.startswith('python')
def get_kwargs(self, **kwargs):
interpreter, arg = self._get_interpreter()
interpreter_fragment, is_python = self._get_interpreter()
return super(ScriptPlanner, self).get_kwargs(
interpreter_arg=arg,
interpreter=interpreter,
interpreter_fragment=interpreter_fragment,
is_python=is_python,
**kwargs
)

@ -0,0 +1,43 @@
# 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
try:
import ansible_mitogen.connection
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.connection
class Connection(ansible_mitogen.connection.Connection):
transport = 'mitogen_doas'

@ -288,9 +288,13 @@ class TemporaryEnvironment(object):
os.environ[key] = str(value)
def revert(self):
if self.env:
os.environ.clear()
os.environ.update(self.original)
"""
Revert changes made by the module to the process environment. This must
always run, as some modules (e.g. git.py) set variables like GIT_SSH
that must be cleared out between runs.
"""
os.environ.clear()
os.environ.update(self.original)
class TemporaryArgv(object):
@ -377,11 +381,10 @@ class ProgramRunner(Runner):
)
def _get_program_args(self):
return [
self.args['_ansible_shell_executable'],
'-c',
self.program_fp.name
]
"""
Return any arguments to pass to the program.
"""
return []
def revert(self):
"""
@ -391,14 +394,30 @@ class ProgramRunner(Runner):
self.program_fp.close()
super(ProgramRunner, self).revert()
def _get_argv(self):
"""
Return the final argument vector used to execute the program.
"""
return [
self.args['_ansible_shell_executable'],
'-c',
self._get_shell_fragment(),
]
def _get_shell_fragment(self):
return "%s %s" % (
shlex_quote(self.program_fp.name),
' '.join(map(shlex_quote, self._get_program_args())),
)
def _run(self):
try:
rc, stdout, stderr = ansible_mitogen.target.exec_args(
args=self._get_program_args(),
args=self._get_argv(),
emulate_tty=self.emulate_tty,
)
except Exception as e:
LOG.exception('While running %s', self._get_program_args())
LOG.exception('While running %s', self._get_argv())
return {
'rc': 1,
'stdout': '',
@ -438,11 +457,7 @@ class ArgsFileRunner(Runner):
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),
]
return [self.args_fp.name]
def revert(self):
"""
@ -457,10 +472,10 @@ class BinaryRunner(ArgsFileRunner, ProgramRunner):
class ScriptRunner(ProgramRunner):
def __init__(self, interpreter, interpreter_arg, **kwargs):
def __init__(self, interpreter_fragment, is_python, **kwargs):
super(ScriptRunner, self).__init__(**kwargs)
self.interpreter = interpreter
self.interpreter_arg = interpreter_arg
self.interpreter_fragment = interpreter_fragment
self.is_python = is_python
b_ENCODING_STRING = b'# -*- coding: utf-8 -*-'
@ -469,21 +484,34 @@ class ScriptRunner(ProgramRunner):
super(ScriptRunner, self)._get_program()
)
def _get_argv(self):
return [
self.args['_ansible_shell_executable'],
'-c',
self._get_shell_fragment(),
]
def _get_shell_fragment(self):
"""
Scripts are eligible for having their hashbang line rewritten, and to
be executed via /bin/sh using the ansible_*_interpreter value used as a
shell fragment prefixing to the invocation.
"""
return "%s %s %s" % (
self.interpreter_fragment,
shlex_quote(self.program_fp.name),
' '.join(map(shlex_quote, self._get_program_args())),
)
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 = b'#!' + utf8(self.interpreter)
if self.interpreter_arg:
shebang += b' ' + utf8(self.interpreter_arg)
new = [shebang]
if os.path.basename(self.interpreter).startswith('python'):
# While Ansible rewrites the #! using ansible_*_interpreter, it is
# never actually used to execute the script, instead it is a shell
# fragment consumed by shell/__init__.py::build_module_command().
new = [b'#!' + utf8(self.interpreter_fragment)]
if self.is_python:
new.append(self.b_ENCODING_STRING)
_, _, rest = s.partition(b'\n')

@ -267,6 +267,7 @@ class ContextService(mitogen.service.Service):
{
'context': mitogen.core.Context or None,
'via': mitogen.core.Context or None,
'init_child_result': {
'fork_context': mitogen.core.Context,
'home_dir': str or None,
@ -297,7 +298,8 @@ class ContextService(mitogen.service.Service):
lambda: self._on_stream_disconnect(stream))
self._send_module_forwards(context)
init_child_result = context.call(ansible_mitogen.target.init_child)
init_child_result = context.call(ansible_mitogen.target.init_child,
log_level=LOG.getEffectiveLevel())
if os.environ.get('MITOGEN_DUMP_THREAD_STACKS'):
from mitogen import debug
@ -307,6 +309,7 @@ class ContextService(mitogen.service.Service):
self._refs_by_context[context] = 0
return {
'context': context,
'via': via,
'init_child_result': init_child_result,
'msg': None,
}
@ -357,7 +360,7 @@ class ContextService(mitogen.service.Service):
Subsequent elements are proxied via the previous.
:returns dict:
* context: mitogen.master.Context or None.
* context: mitogen.parent.Context or None.
* init_child_result: Result of :func:`init_child`.
* msg: StreamError exception text or None.
* method_name: string failing method name.

@ -54,7 +54,7 @@ def wrap_action_loader__get(name, *args, **kwargs):
return adorned_klass(*args, **kwargs)
def wrap_connection_loader__get(name, play_context, new_stdin, **kwargs):
def wrap_connection_loader__get(name, *args, **kwargs):
"""
While the strategy is active, rewrite connection_loader.get() calls for
some transports into requests for a compatible Mitogen transport.
@ -62,7 +62,7 @@ def wrap_connection_loader__get(name, play_context, new_stdin, **kwargs):
if name in ('docker', 'jail', 'local', 'lxc',
'lxd', 'machinectl', 'setns', 'ssh'):
name = 'mitogen_' + name
return connection_loader__get(name, play_context, new_stdin, **kwargs)
return connection_loader__get(name, *args, **kwargs)
class StrategyMixin(object):

@ -202,7 +202,7 @@ def reset_temp_dir(econtext):
@mitogen.core.takes_econtext
def init_child(econtext):
def init_child(econtext, log_level):
"""
Called by ContextService immediately after connection; arranges for the
(presently) spotless Python interpreter to be forked, where the newly
@ -213,6 +213,9 @@ def init_child(econtext):
polluting the global interpreter state in a way that effects explicitly
isolated modules.
:param int log_level:
Logging package level active in the master.
:returns:
Dict like::
@ -230,6 +233,12 @@ def init_child(econtext):
_fork_parent = econtext.router.fork()
reset_temp_dir(econtext)
# Copying the master's log level causes log messages to be filtered before
# they reach LogForwarder, thus reducing an influx of tiny messges waking
# the connection multiplexer process in the master.
LOG.setLevel(log_level)
logging.getLogger('ansible_mitogen').setLevel(log_level)
return {
'fork_context': _fork_parent,
'home_dir': mitogen.core.to_text(os.path.expanduser('~')),
@ -407,6 +416,9 @@ def make_temp_directory(base_dir):
:returns:
Newly created temporary directory.
"""
# issue #301: remote_tmp may contain $vars.
base_dir = os.path.expandvars(base_dir)
if not os.path.exists(base_dir):
os.makedirs(base_dir, mode=int('0700', 8))
return tempfile.mkdtemp(

@ -1,10 +1,10 @@
-r docs/docs-requirements.txt
ansible==2.5.5
ansible==2.6.1
coverage==4.5.1
Django==1.6.11 # Last version supporting 2.6.
mock==2.0.0
pytz==2012d # Last 2.6-compat version.
paramiko==2.3.1 # Last 2.6-compat version.
paramiko==2.3.2 # Last 2.6-compat version.
pytest-catchlog==1.2.2
pytest==3.1.2
PyYAML==3.11; python_version < '2.7'

@ -57,11 +57,9 @@ write files.
Installation
------------
1. Thoroughly review the :ref:`noteworthy_differences` and :ref:`changelog`.
2. Verify Ansible 2.3-2.5 and Python 2.6, 2.7 or 3.6 are listed in ``ansible
--version`` output.
3. Download and extract |mitogen_url| from PyPI.
4. Modify ``ansible.cfg``:
1. Thoroughly review :ref:`noteworthy_differences` and :ref:`changelog`.
2. Download and extract |mitogen_url|.
3. Modify ``ansible.cfg``:
.. parsed-literal::
@ -74,12 +72,16 @@ Installation
per-run basis. Like ``mitogen_linear``, the ``mitogen_free`` strategy exists
to mimic the ``free`` strategy.
5. If targets have a restrictive ``sudoers`` file, add a rule like:
4. If targets have a restrictive ``sudoers`` file, add a rule like:
::
::
deploy = (ALL) NOPASSWD:/usr/bin/python -c*
5. Subscribe to the `mitogen-announce mailing list
<https://www.freelists.org/list/mitogen-announce>`_ to stay updated with new
releases and important bug fixes.
Demo
~~~~
@ -123,23 +125,27 @@ Testimonials
Noteworthy Differences
----------------------
* Ansible 2.3-2.5 are supported along with Python 2.6, 2.7 or 3.6. Verify your
installation is running one of these versions by checking ``ansible
--version`` output.
* The Ansible ``raw`` action executes as a regular Mitogen connection,
precluding its use for installing Python on a target. This will be addressed
soon.
* The ``su`` and ``sudo`` become methods are available. File bugs to register
interest in more.
* The ``doas``, ``su`` and ``sudo`` become methods are available. File bugs to
register interest in more.
* The `docker <https://docs.ansible.com/ansible/2.5/plugins/connection/docker.html>`_,
`jail <https://docs.ansible.com/ansible/2.5/plugins/connection/jail.html>`_,
`local <https://docs.ansible.com/ansible/2.5/plugins/connection/local.html>`_,
`lxc <https://docs.ansible.com/ansible/2.5/plugins/connection/lxc.html>`_,
`lxd <https://docs.ansible.com/ansible/2.5/plugins/connection/lxd.html>`_,
and `ssh <https://docs.ansible.com/ansible/2.5/plugins/connection/ssh.html>`_
* The `docker <https://docs.ansible.com/ansible/2.6/plugins/connection/docker.html>`_,
`jail <https://docs.ansible.com/ansible/2.6/plugins/connection/jail.html>`_,
`local <https://docs.ansible.com/ansible/2.6/plugins/connection/local.html>`_,
`lxc <https://docs.ansible.com/ansible/2.6/plugins/connection/lxc.html>`_,
`lxd <https://docs.ansible.com/ansible/2.6/plugins/connection/lxd.html>`_,
and `ssh <https://docs.ansible.com/ansible/2.6/plugins/connection/ssh.html>`_
built-in connection types are supported, along with Mitogen-specific
:ref:`machinectl <machinectl>`, :ref:`mitogen_su <su>`, :ref:`mitogen_sudo
<sudo>`, and :ref:`setns <setns>` types. File bugs to register interest in
others.
:ref:`machinectl <machinectl>`, :ref:`mitogen_doas <doas>`,
:ref:`mitogen_su <su>`, :ref:`mitogen_sudo <sudo>`, and :ref:`setns <setns>`
types. File bugs to register interest in others.
* Local commands execute in a reuseable interpreter created identically to
interpreters on targets. Presently one interpreter per ``become_user``
@ -158,6 +164,13 @@ Noteworthy Differences
may be established in parallel by default, this can be modified by setting
the ``MITOGEN_POOL_SIZE`` environment variable.
* The ``ansible_python_interpreter`` variable is parsed using a restrictive
:mod:`shell-like <shlex>` syntax, permitting values such as ``/usr/bin/env
FOO=bar python``, which occur in practice. Ansible `documents this
<https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#ansible-python-interpreter>`_
as an absolute path, however the implementation passes it unquoted through
the shell, permitting arbitrary code to be injected.
* Performance does not scale linearly with target count. This will improve over
time.
@ -477,40 +490,48 @@ establishment of additional reuseable interpreters as necessary to match the
configuration of each task.
.. _method-docker:
.. _doas:
Docker
~~~~~~
Doas
~~~~
Like `docker
<https://docs.ansible.com/ansible/2.5/plugins/connection/docker.html>`_ except
connection delegation is supported.
``doas`` can be used as a connection method that supports connection delegation, or
as a become method.
* ``ansible_host``: Name of Docker container (default: inventory hostname).
* ``ansible_user``: Name of user within the container to execute as.
When used as a become method:
* ``ansible_python_interpreter``
* ``ansible_become_exe``: path to ``doas`` binary.
* ``ansible_become_user`` (default: ``root``)
* ``ansible_become_pass`` (default: assume passwordless)
* ansible.cfg: ``timeout``
When used as the ``mitogen_doas`` connection method:
.. _machinectl:
* The inventory hostname has no special meaning.
* ``ansible_user``: username to use.
* ``ansible_password``: password to use.
* ``ansible_python_interpreter``
Machinectl
~~~~~~~~~~
Like the `machinectl third party plugin
<https://github.com/BaxterStockman/ansible-connection-machinectl>`_ except
connection delegation is supported. This is a light wrapper around the
:ref:`setns <setns>` method.
.. _method-docker:
Docker
~~~~~~
Like `docker
<https://docs.ansible.com/ansible/2.6/plugins/connection/docker.html>`_ except
connection delegation is supported.
* ``ansible_host``: Name of Docker container (default: inventory hostname).
* ``ansible_user``: Name of user within the container to execute as.
* ``mitogen_machinectl_path``: path to ``machinectl`` command if not available
as ``/bin/machinectl``.
FreeBSD Jail
~~~~~~~~~~~~
Like `jail
<https://docs.ansible.com/ansible/2.5/plugins/connection/jail.html>`_ except
<https://docs.ansible.com/ansible/2.6/plugins/connection/jail.html>`_ except
connection delegation is supported.
* ``ansible_host``: Name of jail (default: inventory hostname).
@ -521,19 +542,36 @@ Local
~~~~~
Like `local
<https://docs.ansible.com/ansible/2.5/plugins/connection/local.html>`_ except
<https://docs.ansible.com/ansible/2.6/plugins/connection/local.html>`_ except
connection delegation is supported.
* ``ansible_python_interpreter``
Process Model
^^^^^^^^^^^^^
Ansible usually executes local connection commands as a transient subprocess of
the forked worker executing a task. With the extension, the local connection
exists as a persistent subprocess of the connection multiplexer.
This means that global state mutations made to the top-level Ansible process
that are normally visible to newly forked subprocesses, such as vars plug-ins
that modify the environment, will not be reflected when executing local
commands without additional effort.
During execution the extension presently mimics the working directory and
process environment inheritence of regular Ansible, however it is possible some
additional differences exist that may break existing playbooks.
.. _method-lxc:
LXC
~~~
Like `lxc <https://docs.ansible.com/ansible/2.5/plugins/connection/lxc.html>`_
and `lxd <https://docs.ansible.com/ansible/2.5/plugins/connection/lxd.html>`_
Like `lxc <https://docs.ansible.com/ansible/2.6/plugins/connection/lxc.html>`_
and `lxd <https://docs.ansible.com/ansible/2.6/plugins/connection/lxd.html>`_
except connection delegation is supported, and ``lxc-attach`` is always used
rather than the LXC Python bindings, as is usual with ``lxc``.
@ -543,6 +581,22 @@ The ``lxc-attach`` command must be available on the host machine.
* ``ansible_host``: Name of LXC container (default: inventory hostname).
.. _machinectl:
Machinectl
~~~~~~~~~~
Like the `machinectl third party plugin
<https://github.com/BaxterStockman/ansible-connection-machinectl>`_ except
connection delegation is supported. This is a light wrapper around the
:ref:`setns <setns>` method.
* ``ansible_host``: Name of Docker container (default: inventory hostname).
* ``ansible_user``: Name of user within the container to execute as.
* ``mitogen_machinectl_path``: path to ``machinectl`` command if not available
as ``/bin/machinectl``.
.. _setns:
Setns
@ -622,7 +676,7 @@ When used as the ``mitogen_sudo`` connection method:
SSH
~~~
Like `ssh <https://docs.ansible.com/ansible/2.5/plugins/connection/ssh.html>`_
Like `ssh <https://docs.ansible.com/ansible/2.6/plugins/connection/ssh.html>`_
except connection delegation is supported.
* ``ansible_ssh_timeout``
@ -648,7 +702,7 @@ controller with ``-vvvv`` or higher.
Although use of standard IO and the logging package on the target is forwarded
to the controller, it is not possible to receive IO activity logs, as the
processs of receiving those logs would would itself generate IO activity. To
process of receiving those logs would would itself generate IO activity. To
receive a complete trace of every process on every machine, file-based logging
is necessary. File-based logging can be enabled by setting
``MITOGEN_ROUTER_DEBUG=1`` in your environment.
@ -657,7 +711,8 @@ When file-based logging is enabled, one file per context will be created on the
local machine and every target machine, as ``/tmp/mitogen.<pid>.log``.
If you are experiencing a hang, ``MITOGEN_DUMP_THREAD_STACKS=1`` causes every
process to dump every thread stack into the logging framework every 5 seconds.
process on every machine to dump every thread stack into the logging framework
every 5 seconds.
Getting Help

@ -483,9 +483,13 @@ Router Class
determine its installation prefix. This is required to support
virtualenv.
:param str python_path:
Path to the Python interpreter to use for bootstrap. Defaults to
:data:`sys.executable`. For SSH, defaults to ``python``.
:param str|list python_path:
String or list path to the Python interpreter to use for bootstrap.
Defaults to :data:`sys.executable` for local connections, and
``python`` for remote connections.
It is possible to pass a list to invoke Python wrapped using
another tool, such as ``["/usr/bin/env", "python"]``.
:param bool debug:
If :data:`True`, arrange for debug logging (:py:meth:`enable_debug`) to
@ -523,6 +527,31 @@ Router Class
# Use the SSH connection to create a sudo connection.
remote_root = router.sudo(username='root', via=remote_machine)
.. method:: dos (username=None, password=None, su_path=None, password_prompt=None, incorrect_prompts=None, \**kwargs)
Construct a context on the local machine over a ``su`` invocation. The
``su`` process is started in a newly allocated pseudo-terminal, and
supports typing interactive passwords.
Accepts all parameters accepted by :py:meth:`local`, in addition to:
:param str username:
Username to use, defaults to ``root``.
:param str password:
The account password to use if requested.
:param str su_path:
Filename or complete path to the ``su`` binary. ``PATH`` will be
searched if given as a filename. Defaults to ``su``.
:param bytes password_prompt:
A string that indicates ``doas`` is requesting a password. Defaults
to ``Password:``.
:param list incorrect_prompts:
List of bytestrings indicating the password is incorrect. Defaults
to `(b"doas: authentication failed")`.
:raises mitogen.su.PasswordError:
A password was requested but none was provided, the supplied
password was incorrect, or the target account did not exist.
.. method:: docker (container=None, image=None, docker_path=None, \**kwargs)
Construct a context on the local machine within an existing or
@ -616,12 +645,9 @@ Router Class
:param str su_path:
Filename or complete path to the ``su`` binary. ``PATH`` will be
searched if given as a filename. Defaults to ``su``.
:param str password_prompt:
The string to wait to that signals ``su`` is requesting a password.
Defaults to ``Password:``.
:param str password_prompt:
The string that signal a request for the password. Defaults to
``Password:``.
:param bytes password_prompt:
The string that indicates ``su`` is requesting a password. Defaults
to ``Password:``.
:param str incorrect_prompts:
Strings that signal the password is incorrect. Defaults to `("su:
sorry", "su: authentication failure")`.

@ -15,6 +15,111 @@ Release Notes
</style>
.. comment
v0.2.3 (2018-07-??)
-------------------
* `#315 <https://github.com/dw/mitogen/pull/315>`_: Mitogen for Ansible is
supported under Ansible 2.6. Contributed by `Dan Quackenbush
<https://github.com/danquack>`_.
v0.2.2 (2018-07-26)
-------------------
Mitogen for Ansible
~~~~~~~~~~~~~~~~~~~
* `#291 <https://github.com/dw/mitogen/issues/291>`_: ``ansible_*_interpreter``
variables are parsed using a restrictive shell-like syntax, supporting a
common idiom where ``ansible_python_interpreter`` is set to ``/usr/bin/env
python``.
* `#299 <https://github.com/dw/mitogen/issues/299>`_: fix the ``network_cli``
connection type when the Mitogen strategy is active. Mitogen cannot help
network device connections, however it should still be possible to use device
connections while Mitogen is active.
* `#301 <https://github.com/dw/mitogen/pull/301>`_: variables like ``$HOME`` in
the ``remote_tmp`` setting are evaluated correctly.
* `#303 <https://github.com/dw/mitogen/pull/303>`_: the :ref:`doas` become method
is supported. Contributed by `Mike Walker
<https://github.com/napkindrawing>`_.
* `#309 <https://github.com/dw/mitogen/issues/309>`_: fix a regression to
process environment cleanup, caused by the change in v0.2.1 to run local
tasks with the correct environment.
* `#317 <https://github.com/dw/mitogen/issues/317>`_: respect the verbosity
setting when writing to Ansible's ``log_path``, if it is enabled. Child log
filtering was also incorrect, causing the master to needlessly wake many
times. This nets a 3.5% runtime improvement running against the local
machine.
Core Library
~~~~~~~~~~~~
* `#291 <https://github.com/dw/mitogen/issues/291>`_: the ``python_path``
parameter may specify an argument vector prefix rather than a string program
path.
* `#300 <https://github.com/dw/mitogen/issues/300>`_: the broker could crash on
OS X during shutdown due to scheduled `kqueue
<https://www.freebsd.org/cgi/man.cgi?query=kevent>`_ filter changes for
descriptors that were closed before the IO loop resumes. As a temporary
workaround, kqueue's bulk change feature is not used.
* `#303 <https://github.com/dw/mitogen/pull/303>`_: the :ref:`doas` become method
is now supported. Contributed by `Mike Walker
<https://github.com/napkindrawing>`_.
* `#307 <https://github.com/dw/mitogen/issues/307>`_: SSH login banner output
containing the word 'password' is no longer confused for a password prompt.
* `#319 <https://github.com/dw/mitogen/issues/319>`_: SSH connections would
fail immediately on Windows Subsystem for Linux, due to use of `TCSAFLUSH`
with :func:`termios.tcsetattr`. The flag is omitted if WSL is detected.
* `#320 <https://github.com/dw/mitogen/issues/320>`_: The OS X poller
could spuriously wake up due to ignoring an error bit set on events returned
by the kernel, manifesting as a failure to read from an unrelated descriptor.
* Standard IO forwarding accidentally configured the replacement ``stdout`` and
``stderr`` write descriptors as non-blocking, causing subprocesses that
generate more output than kernel buffer space existed to throw errors. The
write ends are now configured as blocking.
* When :func:`mitogen.core.enable_profiling` is active, :mod:`mitogen.service`
threads are profiled just like other threads.
* Debug logs containing command lines are printed with the minimal quoting and
escaping required.
Thanks!
~~~~~~~
Mitogen would not be possible without the support of users. A huge thanks for
the bug reports and pull requests in this release contributed by
`Alex Russu <https://github.com/alexrussu>`_,
`Andy Freeland <https://github.com/rouge8>`_,
`Ayaz Ahmed Khan <https://github.com/ayaz>`_,
`Colin McCarthy <https://github.com/colin-mccarthy>`_,
`Dan Quackenbush <https://github.com/danquack>`_,
`Duane Zamrok <https://github.com/dewthefifth>`_,
`falbanese <https://github.com/falbanese>`_,
`Gonzalo Servat <https://github.com/gservat>`_,
`Guy Knights <https://github.com/knightsg>`_,
`Josh Smift <https://github.com/jbscare>`_,
`Mark Janssen <https://github.com/sigio>`_,
`Mike Walker <https://github.com/napkindrawing>`_,
`Tawana Musewe <https://github.com/tbtmuse>`_, and
`Zach Swanson <https://github.com/zswanson>`_.
v0.2.1 (2018-07-10)
-------------------
@ -91,6 +196,12 @@ Mitogen for Ansible
for Message(..., 102, ...), my ID is ...* may be visible. These are due to a
minor race while initializing logging and can be ignored.
* When running with ``-vvv``, log messages will be printed to the console
*after* the Ansible run completes, as connection multiplexer shutdown only
begins after Ansible exits. This is due to a lack of suitable shutdown hook
in Ansible, and is fairly harmless, albeit cosmetically annoying. A future
release may include a solution.
* Performance does not scale linearly with target count. This requires
significant additional work, as major bottlenecks exist in the surrounding
Ansible code. Performance-related bug reports for any scenario remain

@ -101,6 +101,7 @@ sponsorship and outstanding future-thinking of its early adopters.
<ul>
<li>Alex Willmer</li>
<li><a href="https://underwhelm.net/">Dan Dorman</a> &mdash; - <em>When I truly understand my enemy … then in that very moment I also love him.</em></li>
<li>Daniel Foerster</li>
<li><a href="https://www.deps.co/">Deps</a> &mdash; <em>Private Maven Repository Hosting for Java, Scala, Groovy, Clojure</em></li>
<li><a href="https://www.edport.co.uk/">Edward Wilson</a> &mdash; <em>To efficiency and beyond! I wish Mitogen and all who sail in her the best of luck.</em></li>
<li><a href="https://www.epartment.nl/">Epartment</a></li>

@ -163,7 +163,7 @@ Logging Records
Messages received from a child context via :class:`mitogen.master.LogForwarder`
receive extra attributes:
* `mitogen_context`: :class:`mitogen.master.Context` referring to the message
* `mitogen_context`: :class:`mitogen.parent.Context` referring to the message
source.
* `mitogen_name`: original logger name in the source context.
* `mitogen_msg`: original message in the source context.

@ -125,6 +125,8 @@ class Operations(fuse.Operations): # fuse.LoggingMixIn,
self.host = host
self.root = path
self.ready = threading.Event()
if not hasattr(self, 'encoding'):
self.encoding = 'utf-8'
def init(self, path):
self.broker = mitogen.master.Broker(install_watcher=False)

@ -33,7 +33,7 @@ be expected. On the slave, it is built dynamically during startup.
#: Library version as a tuple.
__version__ = (0, 2, 1)
__version__ = (0, 2, 2)
#: This is :data:`False` in slave contexts. Previously it was used to prevent

@ -607,6 +607,7 @@ class Importer(object):
self._present = {'mitogen': [
'compat',
'debug',
'doas',
'docker',
'fakessh',
'fork',
@ -814,10 +815,21 @@ class Importer(object):
def get_filename(self, fullname):
if fullname in self._cache:
path = self._cache[fullname][2]
if path is None:
# If find_loader() returns self but a subsequent master RPC
# reveals the module can't be loaded, and so load_module()
# throws ImportError, on Python 3.x it is still possible for
# the loader to be called to fetch metadata.
raise ImportError('master cannot serve %r' % (fullname,))
return u'master:' + self._cache[fullname][2]
def get_source(self, fullname):
if fullname in self._cache:
compressed = self._cache[fullname][3]
if compressed is None:
raise ImportError('master cannot serve %r' % (fullname,))
source = zlib.decompress(self._cache[fullname][3])
if PY3:
return to_text(source)
@ -850,7 +862,7 @@ class LogHandler(logging.Handler):
class Side(object):
_fork_refs = weakref.WeakValueDictionary()
def __init__(self, stream, fd, cloexec=True, keep_alive=True):
def __init__(self, stream, fd, cloexec=True, keep_alive=True, blocking=False):
self.stream = stream
self.fd = fd
self.closed = False
@ -858,7 +870,8 @@ class Side(object):
self._fork_refs[id(self)] = self
if cloexec:
set_cloexec(fd)
set_nonblock(fd)
if not blocking:
set_nonblock(fd)
def __repr__(self):
return '<Side of %r fd %s>' % (self.stream, self.fd)
@ -1519,7 +1532,7 @@ class IoLogger(BasicStream):
set_cloexec(self._wsock.fileno())
self.receive_side = Side(self, self._rsock.fileno())
self.transmit_side = Side(self, dest_fd, cloexec=False)
self.transmit_side = Side(self, dest_fd, cloexec=False, blocking=True)
self._broker.start_receive(self)
def __repr__(self):

@ -0,0 +1,118 @@
# 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 logging
import os
import mitogen.core
import mitogen.parent
from mitogen.core import b
LOG = logging.getLogger(__name__)
class PasswordError(mitogen.core.StreamError):
pass
class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False
#: Once connected, points to the corresponding TtyLogStream, allowing it to
#: be disconnected at the same time this stream is being torn down.
tty_stream = None
username = 'root'
password = None
doas_path = 'doas'
password_prompt = b('Password:')
incorrect_prompts = (
b('doas: authentication failed'),
)
def construct(self, username=None, password=None, doas_path=None,
password_prompt=None, incorrect_prompts=None, **kwargs):
super(Stream, self).construct(**kwargs)
if username is not None:
self.username = username
if password is not None:
self.password = password
if doas_path is not None:
self.doas_path = doas_path
if password_prompt is not None:
self.password_prompt = password_prompt.lower()
if incorrect_prompts is not None:
self.incorrect_prompts = map(str.lower, incorrect_prompts)
def connect(self):
super(Stream, self).connect()
self.name = u'doas.' + mitogen.core.to_text(self.username)
def on_disconnect(self, broker):
self.tty_stream.on_disconnect(broker)
super(Stream, self).on_disconnect(broker)
def get_boot_command(self):
bits = [self.doas_path, '-u', self.username, '--']
bits = bits + super(Stream, self).get_boot_command()
LOG.debug('doas command line: %r', bits)
return bits
password_incorrect_msg = 'doas password is incorrect'
password_required_msg = 'doas password is required'
def _connect_bootstrap(self, extra_fd):
self.tty_stream = mitogen.parent.TtyLogStream(extra_fd, self)
password_sent = False
it = mitogen.parent.iter_read(
fds=[self.receive_side.fd, extra_fd],
deadline=self.connect_deadline,
)
for buf in it:
LOG.debug('%r: received %r', self, buf)
if buf.endswith(self.EC0_MARKER):
self._ec0_received()
return
if any(s in buf.lower() for s in self.incorrect_prompts):
if password_sent:
raise PasswordError(self.password_incorrect_msg)
elif self.password_prompt in buf.lower():
if self.password is None:
raise PasswordError(self.password_required_msg)
if password_sent:
raise PasswordError(self.password_incorrect_msg)
LOG.debug('sending password')
self.tty_stream.transmit_side.write(
mitogen.core.to_text(self.password + '\n').encode('utf-8')
)
password_sent = True
raise mitogen.core.StreamError('bootstrap failed')

@ -69,6 +69,8 @@ from mitogen.core import LOG
from mitogen.core import IOLOG
IS_WSL = 'Microsoft' in os.uname()[2]
if mitogen.core.PY3:
xrange = range
@ -102,36 +104,34 @@ def is_immediate_child(msg, stream):
def flags(names):
"""Return the result of ORing a set of (space separated) :py:mod:`termios`
module constants together."""
return sum(getattr(termios, name) for name in names.split())
return sum(getattr(termios, name, 0)
for name in names.split())
def cfmakeraw(tflags):
"""Given a list returned by :py:func:`termios.tcgetattr`, return a list
that has been modified in the same manner as the `cfmakeraw()` C library
function."""
modified in a manner similar to the `cfmakeraw()` C library function, but
additionally disabling local echo."""
# BSD: https://github.com/freebsd/freebsd/blob/master/lib/libc/gen/termios.c#L162
# Linux: https://github.com/lattera/glibc/blob/master/termios/cfmakeraw.c#L20
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = tflags
iflag &= ~flags('IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON')
oflag &= ~flags('OPOST IXOFF')
lflag &= ~flags('ECHO ECHOE ECHONL ICANON ISIG IEXTEN')
iflag &= ~flags('IMAXBEL IXOFF INPCK BRKINT PARMRK ISTRIP INLCR ICRNL IXON IGNPAR')
iflag &= ~flags('IGNBRK BRKINT PARMRK')
oflag &= ~flags('OPOST')
lflag &= ~flags('ECHO ECHOE ECHOK ECHONL ICANON ISIG IEXTEN NOFLSH TOSTOP PENDIN')
cflag &= ~flags('CSIZE PARENB')
cflag |= flags('CS8')
# TODO: one or more of the above bit twiddles sets or omits a necessary
# flag. Forcing these fields to zero, as shown below, gets us what we want
# on Linux/OS X, but it is possibly broken on some other OS.
iflag = 0
oflag = 0
lflag = 0
cflag |= flags('CS8 CREAD')
return [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
def disable_echo(fd):
old = termios.tcgetattr(fd)
new = cfmakeraw(old)
flags = (
termios.TCSAFLUSH |
getattr(termios, 'TCSASOFT', 0)
)
flags = getattr(termios, 'TCSASOFT', 0)
if not IS_WSL:
# issue #319: Windows Subsystem for Linux as of July 2018 throws EINVAL
# if TCSAFLUSH is specified.
flags |= termios.TCSAFLUSH
termios.tcsetattr(fd, flags, new)
@ -457,10 +457,16 @@ class Argv(object):
def __init__(self, argv):
self.argv = argv
must_escape = frozenset('\\$"`!')
must_escape_or_space = must_escape | frozenset(' ')
def escape(self, x):
if not self.must_escape_or_space.intersection(x):
return x
s = '"'
for c in x:
if c in '\\$"`':
if c in self.must_escape:
s += '\\'
s += c
s += '"'
@ -525,7 +531,15 @@ class KqueuePoller(mitogen.core.Poller):
def _control(self, fd, filters, flags):
mitogen.core._vv and IOLOG.debug(
'%r._control(%r, %r, %r)', self, fd, filters, flags)
self._changelist.append(select.kevent(fd, filters, flags))
# TODO: at shutdown it is currently possible for KQ_EV_ADD/KQ_EV_DEL
# pairs to be pending after the associated file descriptor has already
# been closed. Fixing this requires maintaining extra state, or perhaps
# making fd closure the poller's responsibility. In the meantime,
# simply apply changes immediately.
# self._changelist.append(select.kevent(fd, filters, flags))
changelist = [select.kevent(fd, filters, flags)]
events, _ = mitogen.core.io_op(self._kqueue.control, changelist, 0, 0)
assert not events
def start_receive(self, fd, data=None):
mitogen.core._vv and IOLOG.debug('%r.start_receive(%r, %r)',
@ -560,7 +574,10 @@ class KqueuePoller(mitogen.core.Poller):
changelist, 32, timeout)
for event in events:
fd = event.ident
if event.filter == select.KQ_FILTER_READ and fd in self._rfds:
if event.flags & select.KQ_EV_ERROR:
LOG.debug('ignoring stale event for fd %r: errno=%d: %s',
fd, event.data, errno.errorcode.get(event.data))
elif event.filter == select.KQ_FILTER_READ and fd in self._rfds:
# Events can still be read for an already-discarded fd.
mitogen.core._vv and IOLOG.debug('%r: POLLIN: %r', self, fd)
yield self._rfds[fd]
@ -865,6 +882,19 @@ class Stream(mitogen.core.Stream):
fp.close()
os.write(1,'MITO001\n'.encode())
def get_python_argv(self):
"""
Return the initial argument vector elements necessary to invoke Python,
by returning a 1-element list containing :attr:`python_path` if it is a
string, or simply returning it if it is already a list.
This allows emulation of existing tools where the Python invocation may
be set to e.g. `['/usr/bin/env', 'python']`.
"""
if isinstance(self.python_path, list):
return self.python_path
return [self.python_path]
def get_boot_command(self):
source = inspect.getsource(self._first_stage)
source = textwrap.dedent('\n'.join(source.strip().split('\n')[2:]))
@ -880,8 +910,8 @@ class Stream(mitogen.core.Stream):
# codecs.decode() requires a bytes object. Since we must be compatible
# with 2.4 (no bytes literal), an extra .encode() either returns the
# same str (2.x) or an equivalent bytes (3.x).
return [
self.python_path, '-c',
return self.get_python_argv() + [
'-c',
'import codecs,os,sys;_=codecs.decode;'
'exec(_(_("%s".encode(),"base64"),"zip"))' % (encoded.decode(),)
]
@ -1240,6 +1270,9 @@ class Router(mitogen.core.Router):
self._context_by_id[context.context_id] = context
return context
def doas(self, **kwargs):
return self.connect(u'doas', **kwargs)
def docker(self, **kwargs):
return self.connect(u'docker', **kwargs)

@ -444,9 +444,11 @@ class Pool(object):
self.add(service)
self._threads = []
for x in range(size):
name = 'mitogen.service.Pool.%x.worker-%d' % (id(self), x,)
thread = threading.Thread(
name='mitogen.service.Pool.%x.worker-%d' % (id(self), x,),
target=self._worker_main,
name=name,
target=mitogen.core._profile_hook,
args=(name, self._worker_main),
)
thread.start()
self._threads.append(thread)

@ -61,7 +61,18 @@ def filter_debug(stream, it):
This contains the mess of dealing with both line-oriented input, and partial
lines such as the password prompt.
Yields `(line, partial)` tuples, where `line` is the line, `partial` is
:data:`True` if no terminating newline character was present and no more
data exists in the read buffer. Consuming code can use this to unreliably
detect the presence of an interactive prompt.
"""
# The `partial` test is unreliable, but is only problematic when verbosity
# is enabled: it's possible for a combination of SSH banner, password
# prompt, verbose output, timing and OS buffering specifics to create a
# situation where an otherwise newline-terminated line appears to not be
# terminated, due to a partial read(). If something is broken when
# ssh_debug_level>0, this is the first place to look.
state = 'start_of_line'
buf = b('')
for chunk in it:
@ -86,7 +97,7 @@ def filter_debug(stream, it):
state = 'start_of_line'
elif state == 'in_plain':
line, nl, buf = buf.partition(b('\n'))
yield line + nl
yield line + nl, not (nl or buf)
if nl:
state = 'start_of_line'
@ -161,6 +172,11 @@ class Stream(mitogen.parent.Stream):
bits = [self.ssh_path]
if self.ssh_debug_level:
bits += ['-' + ('v' * min(3, self.ssh_debug_level))]
else:
# issue #307: suppress any login banner, as it may contain the
# password prompt, and there is no robust way to tell the
# difference.
bits += ['-o', 'LogLevel ERROR']
if self.username:
bits += ['-l', self.username]
if self.port is not None:
@ -232,7 +248,7 @@ class Stream(mitogen.parent.Stream):
deadline=self.connect_deadline
)
for buf in filter_debug(self, it):
for buf, partial in filter_debug(self, it):
LOG.debug('%r: received %r', self, buf)
if buf.endswith(self.EC0_MARKER):
self._router.broker.start_receive(self.tty_stream)
@ -250,7 +266,7 @@ class Stream(mitogen.parent.Stream):
raise PasswordError(self.password_incorrect_msg)
else:
raise PasswordError(self.auth_incorrect_msg)
elif PASSWORD_PROMPT in buf.lower():
elif partial and PASSWORD_PROMPT in buf.lower():
if self.password is None:
raise PasswordError(self.password_required_msg)
LOG.debug('%r: sending password', self)

@ -17,8 +17,9 @@ timeout = 10
# On Travis, paramiko check fails due to host key checking enabled.
host_key_checking = False
# Required by integration/runner__remote_tmp.yml
remote_tmp = ~/.ansible/mitogen-tests/
# "mitogen-tests" required by integration/runner/remote_tmp.yml
# "$HOME" required by integration/action/make_tmp_path.yml
remote_tmp = $HOME/.ansible/mitogen-tests/
[ssh_connection]
ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s

@ -1,4 +1,5 @@
- import_playbook: remote_file_exists.yml
- import_playbook: remote_expand_user.yml
- import_playbook: low_level_execute_command.yml
- import_playbook: make_tmp_path.yml
- import_playbook: transfer_data.yml

@ -2,16 +2,54 @@
- name: integration/action/make_tmp_path.yml
hosts: test-targets
any_errors_fatal: true
gather_facts: true
tasks:
- name: "Find out root's homedir."
# Runs first because it blats regular Ansible facts with junk, so
# non-become run fixes that up.
setup: gather_subset=min
become: true
register: root_facts
- name: "Find regular homedir"
setup: gather_subset=min
register: user_facts
#
# non-become
#
- action_passthrough:
method: _make_tmp_path
register: out
- assert:
# This string must match ansible.cfg::remote_tmp
that: out.result.startswith("{{user_facts.ansible_facts.ansible_user_dir}}/.ansible/mitogen-tests/")
- 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
#
# become. make_tmp_path() must evaluate HOME in the context of the SSH
# user, not the become user.
#
- action_passthrough:
method: _make_tmp_path
register: out
become: true
- assert:
# This string must match ansible.cfg::remote_tmp
that: out.result.startswith("{{ansible_user_dir}}/.ansible/mitogen-tests/")
that: out.result.startswith("{{user_facts.ansible_facts.ansible_user_dir}}/.ansible/mitogen-tests/")
- stat:
path: "{{out.result}}"

@ -0,0 +1,104 @@
# related to issue 301. Essentially ensure remote_expand_user does not support
# $HOME expansion.
- name: integration/action/remote_expand_user.yml
hosts: test-targets
any_errors_fatal: true
tasks:
- name: "Find out root's homedir."
# Runs first because it blats regular Ansible facts with junk, so
# non-become run fixes that up.
setup: gather_subset=min
become: true
register: root_facts
- name: "Find regular homedir"
setup: gather_subset=min
register: user_facts
# ------------------------
- name: "Expand ~/foo"
action_passthrough:
method: _remote_expand_user
kwargs:
path: '~/foo'
sudoable: false
register: out
- assert:
that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
- name: "Expand ~/foo with become active. ~ is become_user's home."
action_passthrough:
method: _remote_expand_user
kwargs:
path: '~/foo'
sudoable: false
register: out
become: true
- assert:
that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
- name: "Expand ~user/foo"
action_passthrough:
method: _remote_expand_user
kwargs:
path: '~{{ansible_user_id}}/foo'
sudoable: false
register: out
- assert:
that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
- name: "Expanding $HOME/foo has no effect."
action_passthrough:
method: _remote_expand_user
kwargs:
path: '$HOME/foo'
sudoable: false
register: out
- assert:
that: out.result == '$HOME/foo'
# ------------------------
- name: "sudoable; Expand ~/foo"
action_passthrough:
method: _remote_expand_user
kwargs:
path: '~/foo'
sudoable: true
register: out
- assert:
that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
- name: "sudoable; Expand ~/foo with become active. ~ is become_user's home."
action_passthrough:
method: _remote_expand_user
kwargs:
path: '~/foo'
sudoable: true
register: out
become: true
- assert:
that: out.result == '{{root_facts.ansible_facts.ansible_user_dir}}/foo'
- name: "sudoable; Expand ~user/foo"
action_passthrough:
method: _remote_expand_user
kwargs:
path: '~{{ansible_user_id}}/foo'
sudoable: true
register: out
- assert:
that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
- name: "sudoable; Expanding $HOME/foo has no effect."
action_passthrough:
method: _remote_expand_user
kwargs:
path: '$HOME/foo'
sudoable: true
register: out
- assert:
that: out.result == '$HOME/foo'

@ -1,6 +1,7 @@
- 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_bash_hashbang_argument.yml
- import_playbook: custom_binary_producing_json.yml
- import_playbook: custom_binary_producing_junk.yml
- import_playbook: custom_binary_single_null.yml
@ -11,4 +12,5 @@
- import_playbook: custom_python_new_style_module.yml
- import_playbook: custom_python_want_json_module.yml
- import_playbook: custom_script_interpreter.yml
- import_playbook: environment_isolation.yml
- import_playbook: forking_behaviour.yml

@ -0,0 +1,19 @@
# https://github.com/dw/mitogen/issues/291
- name: integration/runner/custom_bash_hashbang_argument.yml
hosts: test-targets
any_errors_fatal: true
tasks:
- custom_bash_old_style_module:
foo: true
with_sequence: start=1 end={{end|default(1)}}
register: out
vars:
ansible_bash_interpreter: "/usr/bin/env RUN_VIA_ENV=yes bash"
- assert:
that: |
(not out.changed) and
(not out.results[0].changed) and
out.results[0].msg == 'Here is my input' and
out.results[0].run_via_env == "yes"

@ -0,0 +1,50 @@
# issue #309: ensure process environment is restored after a module runs.
- name: integration/runner/environment_isolation.yml
hosts: test-targets
any_errors_fatal: true
gather_facts: true
tasks:
# ---
# Verify custom env setting is cleared out.
# ---
# Verify sane state first.
- custom_python_detect_environment:
register: out
- assert:
that: not out.env.evil_key is defined
- shell: echo 'hi'
environment:
evil_key: evil
# Verify environment was cleaned up.
- custom_python_detect_environment:
register: out
- assert:
that: not out.env.evil_key is defined
# ---
# Verify non-explicit module env mutations are cleared out.
# ---
# Verify sane state first.
- custom_python_detect_environment:
register: out
- assert:
that: not out.env.evil_key is defined
- custom_python_modify_environ:
key: evil_key
val: evil
# Verify environment was cleaned up.
- custom_python_detect_environment:
register: out
- assert:
that: not out.env.evil_key is defined

@ -16,5 +16,6 @@ echo "{"
echo " \"changed\": false,"
echo " \"msg\": \"Here is my input\","
echo " \"filename\": \"$INPUT\","
echo " \"run_via_env\": \"$RUN_VIA_ENV\","
echo " \"input\": [\"$(cat $INPUT | tr \" \' )\"]"
echo "}"

@ -0,0 +1,22 @@
#!/usr/bin/python
# I am an Ansible new-style Python module. I modify the process environment and
# don't clean up after myself.
from ansible.module_utils.basic import AnsibleModule
import os
import pwd
import socket
import sys
def main():
module = AnsibleModule(argument_spec={
'key': {'type': str},
'val': {'type': str}
})
os.environ[module.params['key']] = module.params['val']
module.exit_json(msg='Muahahaha!')
if __name__ == '__main__':
main()

@ -45,10 +45,12 @@ RUN yum clean all && \
DOCKERFILE = r"""
COPY data/001-mitogen.sudo /etc/sudoers.d/001-mitogen
COPY data/docker/ssh_login_banner.txt /etc/ssh/banner.txt
RUN \
chsh -s /bin/bash && \
mkdir -p /var/run/sshd && \
echo i-am-mitogen-test-docker-image > /etc/sentinel && \
echo "Banner /etc/ssh/banner.txt" >> /etc/ssh/sshd_config && \
groupadd mitogen__sudo_nopw && \
useradd -s /bin/bash -m mitogen__has_sudo -G SUDO_GROUP && \
useradd -s /bin/bash -m mitogen__has_sudo_pubkey -G SUDO_GROUP && \

@ -0,0 +1,21 @@
This banner tests Mitogen's ability to differentiate the word 'password'
appearing in a login banner, and 'password' appearing in a password prompt.
This system is for the use of authorized users only. Individuals using this
computer system without authority or in excess of their authority are subject
to having all of their activities on this system monitored and recorded by
system personnel.
In the course of monitoring this system with regard to any unauthorized or
improper use or in the course of system maintenance the system personnel may
have insights into regular business activity.
Anyone using this system expressly consents to such monitoring and is advised
that if such monitoring reveals possible evidence of improper activity, system
personnel may provide the evidence of such monitoring to internal Compliance
and Security Officers who will - in the case of criminal offences - relay such
incidents to law enforcement officials.
**************************************************************
NOTE: This system is connected to DOMAIN.COM,
please use your password.

@ -0,0 +1,12 @@
#!/bin/bash
# This script exists to test the behavior of Stream.python_path being set to a
# list. It sets an environmnt variable that we can detect, then executes any
# arguments passed to it.
export EXECUTED_VIA_ENV_WRAPPER=1
if [ "${1:0:1}" == "-" ]; then
exec "$PYTHON" "$@"
else
export ENV_WRAPPER_FIRST_ARG="$1"
shift
exec "$@"
fi

@ -1,5 +1,6 @@
import os
import sys
import unittest2
@ -11,6 +12,14 @@ import testlib
import plain_old_module
def get_sys_executable():
return sys.executable
def get_os_environ():
return dict(os.environ)
class LocalTest(testlib.RouterMixin, unittest2.TestCase):
stream_class = mitogen.ssh.Stream
@ -20,5 +29,35 @@ class LocalTest(testlib.RouterMixin, unittest2.TestCase):
self.assertEquals('local.%d' % (pid,), context.name)
class PythonPathTest(testlib.RouterMixin, unittest2.TestCase):
stream_class = mitogen.ssh.Stream
def test_inherited(self):
context = self.router.local()
self.assertEquals(sys.executable, context.call(get_sys_executable))
def test_string(self):
os.environ['PYTHON'] = sys.executable
context = self.router.local(
python_path=testlib.data_path('env_wrapper.sh'),
)
self.assertEquals(sys.executable, context.call(get_sys_executable))
env = context.call(get_os_environ)
self.assertEquals('1', env['EXECUTED_VIA_ENV_WRAPPER'])
def test_list(self):
context = self.router.local(
python_path=[
testlib.data_path('env_wrapper.sh'),
"magic_first_arg",
sys.executable
]
)
self.assertEquals(sys.executable, context.call(get_sys_executable))
env = context.call(get_os_environ)
self.assertEquals('magic_first_arg', env['ENV_WRAPPER_FIRST_ARG'])
self.assertEquals('1', env['EXECUTED_VIA_ENV_WRAPPER'])
if __name__ == '__main__':
unittest2.main()

@ -105,5 +105,24 @@ class SshTest(testlib.DockerMixin, unittest2.TestCase):
)
class BannerTest(testlib.DockerMixin, unittest2.TestCase):
# Verify the ability to disambiguate random spam appearing in the SSHd's
# login banner from a legitimate password prompt.
stream_class = mitogen.ssh.Stream
def test_verbose_enabled(self):
context = self.docker_ssh(
username='mitogen__has_sudo',
password='has_sudo_password',
ssh_debug_level=3,
)
name = 'ssh.%s:%s' % (
self.dockerized_ssh.get_host(),
self.dockerized_ssh.port,
)
self.assertEquals(name, context.name)
if __name__ == '__main__':
unittest2.main()

Loading…
Cancel
Save