Merge branch 'dmw'

- static binaries for runner tests
- temp files take 5
- kubectl updates
- fix tests/ansible/tests/ via run_tests
- extra locking for ContextService
- cap child proceses to 512 fds to fix RedHat stupidity.
- split find_good_temp_dir/is_good_temp_dir
- install instructions updates
- handle null sys.executable
- define explicit localhost for tests, needed when running under Travis
- block import if __main__ lacks an execution guard
issue72
David Wilson 6 years ago
commit ad81a64ee0

@ -24,9 +24,10 @@ with ci_lib.Fold('docker_setup'):
--rm
--detach
--publish 0.0.0.0:%s:22/tcp
--hostname=target-%s
--name=target-%s
mitogen/%s-test
""", BASE_PORT + i, distro, distro,)
""", BASE_PORT + i, distro, distro, distro)
with ci_lib.Fold('job_setup'):
@ -37,7 +38,7 @@ with ci_lib.Fold('job_setup'):
run("pip install -q ansible==%s", ci_lib.ANSIBLE_VERSION)
run("mkdir %s", HOSTS_DIR)
run("ln -s %s/common-hosts %s", TESTS_DIR, HOSTS_DIR)
run("ln -s %s/hosts/common-hosts %s", TESTS_DIR, HOSTS_DIR)
with open(os.path.join(HOSTS_DIR, 'target'), 'w') as fp:
fp.write('[test-targets]\n')
@ -54,7 +55,7 @@ with ci_lib.Fold('job_setup'):
))
# Build the binaries.
run("make -C %s", TESTS_DIR)
# run("make -C %s", TESTS_DIR)
if not ci_lib.exists_in_path('sshpass'):
run("sudo apt-get update")
run("sudo apt-get install -y sshpass")

@ -10,6 +10,8 @@ import shlex
import shutil
import tempfile
import os
os.system('curl -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata/v1/instance/machine-type')
#
# check_output() monkeypatch cutpasted from testlib.py

@ -31,6 +31,7 @@ from __future__ import unicode_literals
import logging
import os
import random
import stat
import time
@ -132,11 +133,10 @@ def _connect_kubectl(spec):
return {
'method': 'kubectl',
'kwargs': {
'username': spec['remote_user'],
'pod': spec['remote_addr'],
#'container': spec['container'],
'python_path': spec['python_path'],
'connect_timeout': spec['ansible_ssh_timeout'] or spec['timeout'],
'kubectl_args': spec['extra_args'],
}
}
@ -392,6 +392,8 @@ def config_from_play_context(transport, inventory_name, connection):
connection.get_task_var('mitogen_machinectl_path'),
'mitogen_ssh_debug_level':
connection.get_task_var('mitogen_ssh_debug_level'),
'extra_args':
connection.get_extra_args(),
}
@ -474,26 +476,19 @@ class Connection(ansible.plugins.connection.ConnectionBase):
#: Only sudo, su, and doas are supported for now.
become_methods = ['sudo', 'su', 'doas']
#: Dict containing init_child() return vaue as recorded at startup by
#: Dict containing init_child() return value as recorded at startup by
#: ContextService. Contains:
#:
#: fork_context: Context connected to the fork parent : process in the
#: target account.
#: home_dir: Target context's home directory.
#: temp_dir: A writeable temporary directory managed by the
#: target, automatically destroyed at shutdown.
#: good_temp_dir: A writeable directory where new temporary directories
#: can be created.
init_child_result = None
#: A private temporary directory destroyed during :meth:`close`, or
#: automatically during shutdown if :meth:`close` failed or was never
#: called.
temp_dir = None
#: A :class:`mitogen.parent.CallChain` to use for calls made to the target
#: account, to ensure subsequent calls fail if pipelined directory creation
#: or file transfer fails. This eliminates roundtrips when a call is likely
#: to succeed, and ensures subsequent actions will fail with the original
#: exception if the pipelined call failed.
#: A :class:`mitogen.parent.CallChain` for calls made to the target
#: account, to ensure subsequent calls fail with the original exception if
#: pipelined directory creation or file transfer fails.
chain = None
#
@ -695,14 +690,24 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.init_child_result = dct['init_child_result']
def _init_temp_dir(self):
"""
"""
self.temp_dir = os.path.join(
self.init_child_result['temp_dir'],
'worker-%d-%x' % (os.getpid(), id(self))
def get_good_temp_dir(self):
self._connect()
return self.init_child_result['good_temp_dir']
def _generate_tmp_path(self):
return os.path.join(
self.get_good_temp_dir(),
'ansible_mitogen_action_%016x' % (
random.getrandbits(8*8),
)
)
self.get_chain().call_no_reply(os.mkdir, self.temp_dir)
def _make_tmp_path(self):
assert getattr(self._shell, 'tmpdir', None) is None
self._shell.tmpdir = self._generate_tmp_path()
LOG.debug('Temporary directory: %r', self._shell.tmpdir)
self.get_chain().call_no_reply(os.mkdir, self._shell.tmpdir)
return self._shell.tmpdir
def _connect(self):
"""
@ -721,7 +726,6 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self._connect_broker()
stack = self._build_stack()
self._connect_stack(stack)
self._init_temp_dir()
def close(self, new_task=False):
"""
@ -729,18 +733,22 @@ class Connection(ansible.plugins.connection.ConnectionBase):
gracefully shut down, and wait for shutdown to complete. Safe to call
multiple times.
"""
if getattr(self._shell, 'tmpdir', None) is not None:
# Avoid CallChain to ensure exception is logged on failure.
self.context.call_no_reply(
ansible_mitogen.target.prune_tree,
self._shell.tmpdir,
)
self._shell.tmpdir = None
if self.context:
self.chain.reset()
# No pipelining to ensure exception is logged on failure.
self.context.call_no_reply(ansible_mitogen.target.prune_tree,
self.temp_dir)
self.parent.call_service(
service_name='ansible_mitogen.services.ContextService',
method_name='put',
context=self.context
)
self.temp_dir = None
self.context = None
self.login_context = None
self.init_child_result = None
@ -783,6 +791,14 @@ class Connection(ansible.plugins.connection.ConnectionBase):
ansible_mitogen.target.create_fork_child
)
def get_extra_args(self):
"""
Overridden by connections/mitogen_kubectl.py to a list of additional
arguments for the command.
"""
# TODO: maybe use this for SSH too.
return []
def get_default_cwd(self):
"""
Overridden by connections/mitogen_local.py to emulate behaviour of CWD

@ -180,12 +180,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
connection.
"""
LOG.debug('_make_tmp_path(remote_user=%r)', remote_user)
self._connection._connect()
# _make_tmp_path() is basically a global stashed away as Shell.tmpdir.
self._connection._shell.tmpdir = self._connection.temp_dir
LOG.debug('Temporary directory: %r', self._connection._shell.tmpdir)
self._cleanup_remote_tmp = True
return self._connection._shell.tmpdir
return self._connection._make_tmp_path()
def _remove_tmp_path(self, tmp_path):
"""
@ -193,6 +188,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
with nothing, as the persistent interpreter automatically cleans up
after itself without introducing roundtrips.
"""
# The actual removal is pipelined by Connection.close().
LOG.debug('_remove_tmp_path(%r)', tmp_path)
self._connection._shell.tmpdir = None
@ -293,6 +289,25 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
except AttributeError:
return getattr(self._task, 'async')
def _temp_file_gibberish(self, module_args, wrap_async):
# Ansible>2.5 module_utils reuses the action's temporary directory if
# one exists. Older versions error if this key is present.
if ansible.__version__ > '2.5':
if wrap_async:
# Sharing is not possible with async tasks, as in that case,
# the directory must outlive the action plug-in.
module_args['_ansible_tmpdir'] = None
else:
module_args['_ansible_tmpdir'] = self._connection._shell.tmpdir
# If _ansible_tmpdir is unset, Ansible>2.6 module_utils will use
# _ansible_remote_tmp as the location to create the module's temporary
# directory. Older versions error if this key is present.
if ansible.__version__ > '2.6':
module_args['_ansible_remote_tmp'] = (
self._connection.get_good_temp_dir()
)
def _execute_module(self, module_name=None, module_args=None, tmp=None,
task_vars=None, persist_files=False,
delete_remote_tmp=True, wrap_async=False):
@ -311,16 +326,9 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
self._update_module_args(module_name, module_args, task_vars)
env = {}
self._compute_environment_string(env)
self._temp_file_gibberish(module_args, wrap_async)
# Always set _ansible_tmpdir regardless of whether _make_remote_tmp()
# has ever been called. This short-circuits all the .tmpdir logic in
# module_common and ensures no second temporary directory or atexit
# handler is installed.
self._connection._connect()
if ansible.__version__ > '2.5':
module_args['_ansible_tmpdir'] = self._connection.temp_dir
return ansible_mitogen.planner.invoke(
ansible_mitogen.planner.Invocation(
action=self,

@ -149,7 +149,8 @@ class Planner(object):
"""
new = dict((mitogen.core.UnicodeType(k), kwargs[k])
for k in kwargs)
new.setdefault('temp_dir', self._inv.connection.temp_dir)
new.setdefault('good_temp_dir',
self._inv.connection.get_good_temp_dir())
new.setdefault('cwd', self._inv.connection.get_default_cwd())
new.setdefault('extra_env', self._inv.connection.get_default_env())
new.setdefault('emulate_tty', True)

@ -31,6 +31,9 @@ from __future__ import absolute_import
import os.path
import sys
import ansible.plugins.connection.kubectl
from ansible.module_utils.six import iteritems
try:
import ansible_mitogen
except ImportError:
@ -43,3 +46,11 @@ import ansible_mitogen.connection
class Connection(ansible_mitogen.connection.Connection):
transport = 'kubectl'
def get_extra_args(self):
parameters = []
for key, option in iteritems(ansible.plugins.connection.kubectl.CONNECTION_OPTIONS):
if self.get_task_var('ansible_' + key) is not None:
parameters += [ option, self.get_task_var('ansible_' + key) ]
return parameters

@ -230,6 +230,11 @@ class Runner(object):
This is passed as a string rather than a dict in order to mimic the
implicit bytes/str conversion behaviour of a 2.x controller running
against a 3.x target.
:param str good_temp_dir:
The writeable temporary directory for this user account reported by
:func:`ansible_mitogen.target.init_child` passed via the controller.
This is specified explicitly to remain compatible with Ansible<2.5, and
for forked tasks where init_child never runs.
:param dict env:
Additional environment variables to set during the run. Keys with
:data:`None` are unset if present.
@ -242,7 +247,7 @@ class Runner(object):
When :data:`True`, indicate the runner should detach the context from
its parent after setup has completed successfully.
"""
def __init__(self, module, service_context, json_args, temp_dir,
def __init__(self, module, service_context, json_args, good_temp_dir,
extra_env=None, cwd=None, env=None, econtext=None,
detach=False):
self.module = module
@ -250,10 +255,32 @@ class Runner(object):
self.econtext = econtext
self.detach = detach
self.args = json.loads(json_args)
self.temp_dir = temp_dir
self.good_temp_dir = good_temp_dir
self.extra_env = extra_env
self.env = env
self.cwd = cwd
#: If not :data:`None`, :meth:`get_temp_dir` had to create a temporary
#: directory for this run, because we're in an asynchronous task, or
#: because the originating action did not create a directory.
self._temp_dir = None
def get_temp_dir(self):
path = self.args.get('_ansible_tmpdir')
if path is not None:
return path
if self._temp_dir is None:
self._temp_dir = tempfile.mkdtemp(
prefix='ansible_mitogen_runner_',
dir=self.good_temp_dir,
)
return self._temp_dir
def revert_temp_dir(self):
if self._temp_dir is not None:
ansible_mitogen.target.prune_tree(self._temp_dir)
self._temp_dir = None
def setup(self):
"""
@ -291,6 +318,7 @@ class Runner(object):
implementation simply restores the original environment.
"""
self._env.revert()
self.revert_temp_dir()
def _run(self):
"""
@ -466,7 +494,7 @@ class ProgramRunner(Runner):
fetched via :meth:`_get_program`.
"""
filename = self._get_program_filename()
path = os.path.join(self.temp_dir, filename)
path = os.path.join(self.get_temp_dir(), filename)
self.program_fp = open(path, 'wb')
self.program_fp.write(self._get_program())
self.program_fp.flush()
@ -546,7 +574,7 @@ class ArgsFileRunner(Runner):
self.args_fp = tempfile.NamedTemporaryFile(
prefix='ansible_mitogen',
suffix='-args',
dir=self.temp_dir,
dir=self.get_temp_dir(),
)
self.args_fp.write(utf8(self._get_args_contents()))
self.args_fp.flush()
@ -661,7 +689,7 @@ class NewStyleRunner(ScriptRunner):
def setup(self):
super(NewStyleRunner, self).setup()
self._stdio = NewStyleStdio(self.args, self.temp_dir)
self._stdio = NewStyleStdio(self.args, self.get_temp_dir())
# It is possible that not supplying the script filename will break some
# module, but this has never been a bug report. Instead act like an
# interpreter that had its script piped on stdin.
@ -739,7 +767,7 @@ class NewStyleRunner(ScriptRunner):
# don't want to pointlessly write the module to disk when it never
# actually needs to exist. So just pass the filename as it would exist.
mod.__file__ = os.path.join(
self.temp_dir,
self.get_temp_dir(),
'ansible_module_' + os.path.basename(self.path),
)

@ -139,11 +139,15 @@ class ContextService(mitogen.service.Service):
count reaches zero.
"""
LOG.debug('%r.put(%r)', self, context)
self._lock.acquire()
try:
if self._refs_by_context.get(context, 0) == 0:
LOG.warning('%r.put(%r): refcount was 0. shutdown_all called?',
self, context)
return
self._refs_by_context[context] -= 1
finally:
self._lock.release()
def key_from_kwargs(self, **kwargs):
"""
@ -183,18 +187,15 @@ class ContextService(mitogen.service.Service):
self._lock.release()
return count
def _shutdown(self, context, lru=None, new_context=None):
def _shutdown_unlocked(self, context, lru=None, new_context=None):
"""
Arrange for `context` to be shut down, and optionally add `new_context`
to the LRU list while holding the lock.
"""
LOG.info('%r._shutdown(): shutting down %r', self, context)
LOG.info('%r._shutdown_unlocked(): shutting down %r', self, context)
context.shutdown()
key = self._key_by_context[context]
self._lock.acquire()
try:
del self._response_by_key[key]
del self._refs_by_context[context]
del self._key_by_context[context]
@ -202,10 +203,8 @@ class ContextService(mitogen.service.Service):
lru.remove(context)
if new_context:
lru.append(new_context)
finally:
self._lock.release()
def _update_lru(self, new_context, spec, via):
def _update_lru_unlocked(self, new_context, spec, via):
"""
Update the LRU ("MRU"?) list associated with the connection described
by `kwargs`, destroying the most recently created context if the list
@ -224,16 +223,27 @@ class ContextService(mitogen.service.Service):
'but they are all marked as in-use.', via)
return
self._shutdown(context, lru=lru, new_context=new_context)
self._shutdown_unlocked(context, lru=lru, new_context=new_context)
def _update_lru(self, new_context, spec, via):
self._lock.acquire()
try:
self._update_lru_unlocked(new_context, spec, via)
finally:
self._lock.release()
@mitogen.service.expose(mitogen.service.AllowParents())
def shutdown_all(self):
"""
For testing use, arrange for all connections to be shut down.
"""
self._lock.acquire()
try:
for context in list(self._key_by_context):
self._shutdown(context)
self._shutdown_unlocked(context)
self._lru_by_via = {}
finally:
self._lock.release()
def _on_stream_disconnect(self, stream):
"""

@ -43,6 +43,7 @@ import operator
import os
import pwd
import re
import resource
import signal
import stat
import subprocess
@ -85,13 +86,20 @@ MAKE_TEMP_FAILED_MSG = (
#: the target Python interpreter before it executes any code or imports.
_fork_parent = None
#: Set by init_child() to a list of candidate $variable-expanded and
#: tilde-expanded directory paths that may be usable as a temporary directory.
_candidate_temp_dirs = None
#: Set by :func:`init_child` to the name of a writeable and executable
#: temporary directory accessible by the active user account.
good_temp_dir = None
#: Set by reset_temp_dir() to the single temporary directory that will exist
#: for the duration of the process.
temp_dir = None
# issue #362: subprocess.Popen(close_fds=True) aka. AnsibleModule.run_command()
# loops the entire SC_OPEN_MAX space. CentOS>5 ships with 1,048,576 FDs by
# default, resulting in huge (>500ms) runtime waste running many commands.
# Therefore if we are a child, cap the range to something reasonable.
rlimit = resource.getrlimit(resource.RLIMIT_NOFILE)
if (rlimit[0] > 512 or rlimit[1] > 512) and not mitogen.is_master:
resource.setrlimit(resource.RLIMIT_NOFILE, (512, 512))
subprocess.MAXFD = 512 # Python <3.x
del rlimit
def get_small_file(context, path):
@ -206,26 +214,28 @@ def _on_broker_shutdown():
prune_tree(temp_dir)
def find_good_temp_dir():
def is_good_temp_dir(path):
"""
Given a list of candidate temp directories extracted from ``ansible.cfg``
and stored in _candidate_temp_dirs, combine it with the Python-builtin list
of candidate directories used by :mod:`tempfile`, then iteratively try each
in turn until one is found that is both writeable and executable.
Return :data:`True` if `path` can be used as a temporary directory, logging
any failures that may cause it to be unsuitable. If the directory doesn't
exist, we attempt to create it using :func:`os.makedirs`.
"""
paths = [os.path.expandvars(os.path.expanduser(p))
for p in _candidate_temp_dirs]
paths.extend(tempfile._candidate_tempdir_list())
if not os.path.exists(path):
try:
os.makedirs(path, mode=int('0700', 8))
except OSError as e:
LOG.debug('temp dir %r unusable: did not exist and attempting '
'to create it failed: %s', path, e)
return False
for path in paths:
try:
tmp = tempfile.NamedTemporaryFile(
prefix='ansible_mitogen_find_good_temp_dir',
prefix='ansible_mitogen_is_good_temp_dir',
dir=path,
)
except (OSError, IOError) as e:
LOG.debug('temp dir %r unusable: %s', path, e)
continue
return False
try:
try:
@ -233,7 +243,7 @@ def find_good_temp_dir():
except OSError as e:
LOG.debug('temp dir %r unusable: %s: chmod failed: %s',
path, e)
continue
return False
try:
# access(.., X_OK) is sufficient to detect noexec.
@ -241,39 +251,36 @@ def find_good_temp_dir():
raise OSError('filesystem appears to be mounted noexec')
except OSError as e:
LOG.debug('temp dir %r unusable: %s: %s', path, e)
continue
LOG.debug('Selected temp directory: %r (from %r)', path, paths)
return path
return False
finally:
tmp.close()
raise IOError(MAKE_TEMP_FAILED_MSG % {
'paths': '\n '.join(paths),
})
return True
@mitogen.core.takes_econtext
def reset_temp_dir(econtext):
def find_good_temp_dir(candidate_temp_dirs):
"""
Create one temporary directory to be reused by all runner.py invocations
for the lifetime of the process. The temporary directory is changed for
each forked job, and emptied as necessary by runner.py::_cleanup_temp()
after each module invocation.
Given a list of candidate temp directories extracted from ``ansible.cfg``,
combine it with the Python-builtin list of candidate directories used by
:mod:`tempfile`, then iteratively try each until one is found that is both
writeable and executable.
The result is that a context need only create and delete one directory
during startup and shutdown, and no further filesystem writes need occur
assuming no modules execute that create temporary files.
:param list candidate_temp_dirs:
List of candidate $variable-expanded and tilde-expanded directory paths
that may be usable as a temporary directory.
"""
global temp_dir
# https://github.com/dw/mitogen/issues/239
paths = [os.path.expandvars(os.path.expanduser(p))
for p in candidate_temp_dirs]
paths.extend(tempfile._candidate_tempdir_list())
basedir = find_good_temp_dir()
temp_dir = tempfile.mkdtemp(prefix='ansible_mitogen_', dir=basedir)
for path in paths:
if is_good_temp_dir(path):
LOG.debug('Selected temp directory: %r (from %r)', path, paths)
return path
# This must be reinstalled in forked children too, since the Broker
# instance from the parent process does not carry over to the new child.
mitogen.core.listen(econtext.broker, 'shutdown', _on_broker_shutdown)
raise IOError(MAKE_TEMP_FAILED_MSG % {
'paths': '\n '.join(paths),
})
@mitogen.core.takes_econtext
@ -306,24 +313,23 @@ def init_child(econtext, log_level, candidate_temp_dirs):
the controller will use to start forked jobs, and `home_dir` is the
home directory for the active user account.
"""
global _candidate_temp_dirs
_candidate_temp_dirs = candidate_temp_dirs
global _fork_parent
mitogen.parent.upgrade_router(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)
global _fork_parent
mitogen.parent.upgrade_router(econtext)
_fork_parent = econtext.router.fork()
global good_temp_dir
good_temp_dir = find_good_temp_dir(candidate_temp_dirs)
return {
'fork_context': _fork_parent,
'home_dir': mitogen.core.to_text(os.path.expanduser('~')),
'temp_dir': temp_dir,
'good_temp_dir': good_temp_dir,
}
@ -336,7 +342,6 @@ def create_fork_child(econtext):
"""
mitogen.parent.upgrade_router(econtext)
context = econtext.router.fork()
context.call(reset_temp_dir)
LOG.debug('create_fork_child() -> %r', context)
return context

@ -78,9 +78,27 @@ Installation
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.
5.
.. raw:: html
<form action="https://www.freelists.org/cgi-bin/subscription.cgi" method="post">
Releases occur frequently and often include important fixes. Subscribe
to the <a
href="https://www.freelists.org/list/mitogen-announce">mitogen-announce
mailing list</a> be notified of new releases.
<p>
<input type="email" placeholder="E-mail Address" name="email" style="font-size: 105%;">
<input type=hidden name="list" value="mitogen-announce">
<!-- <input type=hidden name="url_or_message" value="https://mitogen.readthedocs.io/en/stable/ansible.html#installation">-->
<input type="hidden" name="action" value="subscribe">
<button type="submit" style="font-size: 105%;">
Subscribe
</button>
</p>
</form>
Demo
@ -137,6 +155,7 @@ Noteworthy Differences
* 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>`_,
`kubectl <https://docs.ansible.com/ansible/2.6/plugins/connection/kubectl.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>`_,
@ -407,6 +426,9 @@ specific variables with a particular linefeed style.
Temporary Files
~~~~~~~~~~~~~~~
Temporary file handling in Ansible is incredibly tricky business, and the exact
behaviour varies across major releases.
Ansible creates a variety of temporary files and directories depending on its
operating mode.
@ -444,11 +466,20 @@ In summary, for each task Ansible may create one or more of:
* ``$TMPDIR/ansible_<modname>_payload_.../`` owned by the become user,
* ``$TMPDIR/ansible-module-tmp-.../`` owned by the become user.
A directory must exist to maintain compatibility with Ansible, as many modules
introspect :data:`sys.argv` to find a directory where they may write files,
however only one directory exists for the lifetime of each interpreter, its
location is consistent for each target account, and it is always privately
owned by that account.
Mitogen for Ansible
^^^^^^^^^^^^^^^^^^^
Temporary h
Temporary directory handling is fiddly and varies across major Ansible
releases.
Temporary directories must exist to maintain compatibility with Ansible, as
many modules introspect :data:`sys.argv` to find a directory where they may
write files, however only one directory exists for the lifetime of each
interpreter, its location is consistent for each target account, and it is
always privately owned by that account.
The paths below are tried until one is found that is writeable and lives on a
filesystem with ``noexec`` disabled:
@ -651,6 +682,8 @@ connection delegation is supported.
* ``ansible_user``: Name of user within the container to execute as.
.. _method-jail:
FreeBSD Jail
~~~~~~~~~~~~
@ -662,6 +695,19 @@ connection delegation is supported.
* ``ansible_user``: Name of user within the jail to execute as.
.. _method-kubectl:
Kubernetes Pod
~~~~~~~~~~~~~~
Like `kubectl
<https://docs.ansible.com/ansible/2.6/plugins/connection/kubectl.html>`_ except
connection delegation is supported.
* ``ansible_host``: Name of pod (default: inventory hostname).
* ``ansible_user``: Name of user to authenticate to API as.
Local
~~~~~

@ -589,6 +589,21 @@ Router Class
Filename or complete path to the ``jexec`` binary. ``PATH`` will be
searched if given as a filename. Defaults to ``/usr/sbin/jexec``.
.. method:: kubectl (pod, kubectl_path=None, kubectl_args=None, \**kwargs)
Construct a context in a container via the Kubernetes ``kubectl``
program.
Accepts all parameters accepted by :meth:`local`, in addition to:
:param str pod:
Kubernetes pod to connect to.
:param str kubectl_path:
Filename or complete path to the ``kubectl`` binary. ``PATH`` will
be searched if given as a filename. Defaults to ``kubectl``.
:param list kubectl_args:
Additional arguments to pass to the ``kubectl`` command.
.. method:: lxc (container, lxc_attach_path=None, \**kwargs)
Construct a context on the local machine within an LXC classic
@ -709,7 +724,7 @@ Router Class
:class:`mitogen.core.StreamError` to be raised, and that
attributes of the stream match the actual behaviour of ``sudo``.
.. method:: ssh (hostname, username=None, ssh_path=None, port=None, check_host_keys='enforce', password=None, identity_file=None, identities_only=True, compression=True, \**kwargs)
.. method:: ssh (hostname, username=None, ssh_path=None, ssh_args=None, port=None, check_host_keys='enforce', password=None, identity_file=None, identities_only=True, compression=True, \**kwargs)
Construct a remote context over an OpenSSH ``ssh`` invocation.
@ -727,6 +742,8 @@ Router Class
the username to use.
:param str ssh_path:
Absolute or relative path to ``ssh``. Defaults to ``ssh``.
:param list ssh_args:
Additional arguments to pass to the SSH command.
:param int port:
Port number to connect to; default is unspecified, which causes SSH
to pick the port number.

@ -41,6 +41,10 @@ Enhancements
`uri <http://docs.ansible.com/ansible/latest/modules/uri_module.html>`_). See
:ref:`ansible_tempfiles` for a complete description.
* `#376 <https://github.com/dw/mitogen/pull/376>`_,
`#377 <https://github.com/dw/mitogen/pull/377>`_: the ``kubectl`` connection
type is now supported. Contributed by Yannig Perré.
* `084c0ac0 <https://github.com/dw/mitogen/commit/084c0ac0>`_: avoid a
roundtrip in
`copy <http://docs.ansible.com/ansible/latest/modules/copy_module.html>`_ and
@ -71,11 +75,11 @@ Enhancements
* The `faulthandler <https://faulthandler.readthedocs.io/>`_ module is
automatically activated if it is installed, simplifying debugging of hangs.
See :ref:`diagnosing-hangs` for more information.
See :ref:`diagnosing-hangs` for details.
* The ``MITOGEN_DUMP_THREAD_STACKS`` environment variable's value now indicates
the number of seconds between stack dumps. See :ref:`diagnosing-hangs` for
more information.
details.
Fixes
@ -125,6 +129,11 @@ Fixes
This meant built-in modules overridden via a custom ``module_utils`` search
path may not have had any effect.
* `#362 <https://github.com/dw/mitogen/issues/362>`_: to work around a slow
algorithm in the :mod:`subprocess` module, the maximum number of open files
in processes running on the target is capped to 512, reducing the work
required to start a subprocess by >2000x in default CentOS configurations.
* A missing check caused an exception traceback to appear when using the
``ansible`` command-line tool with a missing or misspelled module name.
@ -165,6 +174,21 @@ Core Library
* `#345 <https://github.com/dw/mitogen/issues/345>`_: the SSH connection method
allows optionally disabling ``IdentitiesOnly yes``.
* `#356 <https://github.com/dw/mitogen/issues/356>`_: if the master Python
process does not have :data:`sys.executable` set, the default Python
interpreter used for new children on the local machine defaults to
``"/usr/bin/python"``.
* `#366 <https://github.com/dw/mitogen/issues/366>`_,
`#380 <https://github.com/dw/mitogen/issues/380>`_: attempts by children to
import :mod:`__main__` where the main program module lacks an execution guard
are refused, and an error is logged. This prevents a common and highly
confusing error when prototyping new scripts.
* `#371 <https://github.com/dw/mitogen/pull/371>`_: the LXC connection method
uses a more compatible method to establish an non-interactive session.
Contributed by Brian Candler.
* `af2ded66 <https://github.com/dw/mitogen/commit/af2ded66>`_: add
:func:`mitogen.fork.on_fork` to allow non-Mitogen managed process forks to
clean up Mitogen resources in the child.
@ -186,9 +210,11 @@ the bug reports in this release contributed by
`Alex Russu <https://github.com/alexrussu>`_,
`atoom <https://github.com/atoom>`_,
`Berend De Schouwer <https://github.com/berenddeschouwer>`_,
`Brian Candler <https://github.com/candlerb>`_,
`Dan Quackenbush <https://github.com/danquack>`_,
`dsgnr <https://github.com/dsgnr>`_,
`Jesse London <https://github.com/jesteria>`_,
`John McGrath <https://github.com/jmcgrath207>`_,
`Jonathan Rosser <https://github.com/jrosser>`_,
`Josh Smift <https://github.com/jbscare>`_,
`Luca Nunzi <https://github.com/0xlc>`_,
@ -197,9 +223,11 @@ the bug reports in this release contributed by
`Pierre-Henry Muller <https://github.com/pierrehenrymuller>`_,
`Pierre-Louis Bonicoli <https://github.com/jesteria>`_,
`Prateek Jain <https://github.com/prateekj201>`_,
`RedheatWei <https://github.com/RedheatWei>`_,
`Rick Box <https://github.com/boxrick>`_,
`Tawana Musewe <https://github.com/tbtmuse>`_, and
`Timo Beckers <https://github.com/ti-mo>`_.
`Tawana Musewe <https://github.com/tbtmuse>`_,
`Timo Beckers <https://github.com/ti-mo>`_, and
`Yannig Perré <https://github.com/yannig>`_.
v0.2.2 (2018-07-26)

@ -436,7 +436,7 @@ def run(dest, router, args, deadline=None, econtext=None):
ssh_path = os.path.join(tmp_path, 'ssh')
fp = open(ssh_path, 'w')
try:
fp.write('#!%s\n' % (sys.executable,))
fp.write('#!%s\n' % (mitogen.parent.get_sys_executable(),))
fp.write(inspect.getsource(mitogen.core))
fp.write('\n')
fp.write('ExternalContext(%r).main()\n' % (
@ -449,7 +449,7 @@ def run(dest, router, args, deadline=None, econtext=None):
env = os.environ.copy()
env.update({
'PATH': '%s:%s' % (tmp_path, env.get('PATH', '')),
'ARGV0': sys.executable,
'ARGV0': mitogen.parent.get_sys_executable(),
'SSH_PATH': ssh_path,
})

@ -35,44 +35,31 @@ import mitogen.parent
LOG = logging.getLogger(__name__)
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = True
pod = None
container = None
username = None
kubectl_path = 'kubectl'
kubectl_args = None
# TODO: better way of capturing errors such as "No such container."
create_child_args = {
'merge_stdio': True
}
def construct(self, pod = None, container=None,
kubectl_path=None, username=None,
**kwargs):
assert pod
def construct(self, pod, kubectl_path=None, kubectl_args=None, **kwargs):
super(Stream, self).construct(**kwargs)
if pod:
assert pod
self.pod = pod
if container:
self.container = container
if kubectl_path:
self.kubectl_path = kubectl_path
if username:
self.username = username
self.kubectl_args = kubectl_args or []
def connect(self):
super(Stream, self).connect()
self.name = u'kubectl.' + (self.pod) + str(self.container)
self.name = u'kubectl.%s%s' % (self.pod, self.kubectl_args)
def get_boot_command(self):
args = ['exec', '-it', self.pod]
if self.username:
args += ['--username=' + self.username]
if self.container:
args += ['--container=' + self.container]
bits = [self.kubectl_path]
return bits + args + [ "--" ] + super(Stream, self).get_boot_command()
bits = [self.kubectl_path] + self.kubectl_args + ['exec', '-it', self.pod]
return bits + ["--"] + super(Stream, self).get_boot_command()

@ -553,6 +553,14 @@ class ModuleResponder(object):
return 'ModuleResponder(%r)' % (self._router,)
MAIN_RE = re.compile(b(r'^if\s+__name__\s*==\s*.__main__.\s*:'), re.M)
main_guard_msg = (
"A child context attempted to import __main__, however the main "
"module present in the master process lacks an execution guard. "
"Update %r to prevent unintended execution, using a guard like:\n"
"\n"
" if __name__ == '__main__':\n"
" # your code here.\n"
)
def whitelist_prefix(self, fullname):
if self.whitelist == ['']:
@ -562,15 +570,20 @@ class ModuleResponder(object):
def blacklist_prefix(self, fullname):
self.blacklist.append(fullname)
def neutralize_main(self, src):
def neutralize_main(self, path, src):
"""Given the source for the __main__ module, try to find where it
begins conditional execution based on a "if __name__ == '__main__'"
guard, and remove any code after that point."""
match = self.MAIN_RE.search(src)
if match:
return src[:match.start()]
if b('mitogen.main(') in src:
return src
LOG.error(self.main_guard_msg, path)
raise ImportError('refused')
def _make_negative_response(self, fullname):
return (fullname, None, None, None, ())
@ -596,7 +609,7 @@ class ModuleResponder(object):
pkg_present = None
if fullname == '__main__':
source = self.neutralize_main(source)
source = self.neutralize_main(path, source)
compressed = mitogen.core.Blob(zlib.compress(source, 9))
related = [
to_text(name)

@ -85,11 +85,35 @@ OPENPTY_MSG = (
"to avoid PTY use."
)
SYS_EXECUTABLE_MSG = (
"The Python sys.executable variable is unset, indicating Python was "
"unable to determine its original program name. Unless explicitly "
"configured otherwise, child contexts will be started using "
"'/usr/bin/python'"
)
_sys_executable_warning_logged = False
def get_log_level():
return (LOG.level or logging.getLogger().level or logging.INFO)
def get_sys_executable():
"""
Return :data:`sys.executable` if it is set, otherwise return
``"/usr/bin/python"`` and log a warning.
"""
if sys.executable:
return sys.executable
global _sys_executable_warning_logged
if not _sys_executable_warning_logged:
LOG.warn(SYS_EXECUTABLE_MSG)
_sys_executable_warning_logged = True
return '/usr/bin/python'
def get_core_source():
"""
In non-masters, simply fetch the cached mitogen.core source code via the
@ -841,7 +865,7 @@ class Stream(mitogen.core.Stream):
Base for streams capable of starting new slaves.
"""
#: The path to the remote Python interpreter.
python_path = sys.executable
python_path = get_sys_executable()
#: Maximum time to wait for a connection attempt.
connect_timeout = 30.0

@ -6,15 +6,32 @@ echo '-------------------'
echo
set -o errexit
set -o nounset
set -o pipefail
UNIT2="$(which unit2)"
coverage erase
coverage run "${UNIT2}" discover \
# First run overwites coverage output.
[ "$SKIP_MITOGEN" ] || {
coverage run "${UNIT2}" discover \
--start-directory "tests" \
--pattern '*_test.py' \
"$@"
}
# Second run appends. This is since 'discover' treats subdirs as packages and
# the 'ansible' subdir shadows the real Ansible package when it contains
# __init__.py, so hack around it by just running again with 'ansible' as the
# start directory. Alternative seems to be renaming tests/ansible/ and making a
# mess of Git history.
[ "$SKIP_ANSIBLE" ] || {
export PYTHONPATH=`pwd`/tests:$PYTHONPATH
coverage run -a "${UNIT2}" discover \
--start-directory "tests/ansible" \
--pattern '*_test.py' \
"$@"
}
coverage html
echo coverage report is at "file://$(pwd)/htmlcov/index.html"

@ -1,13 +1,15 @@
TARGETS+=lib/modules/custom_binary_producing_junk
TARGETS+=lib/modules/custom_binary_producing_json
SYSTEM=$(shell uname -s)
TARGETS+=lib/modules/custom_binary_producing_junk_$(SYSTEM)
TARGETS+=lib/modules/custom_binary_producing_json_$(SYSTEM)
all: clean $(TARGETS)
lib/modules/custom_binary_producing_junk: lib/modules.src/custom_binary_producing_junk.c
lib/modules/custom_binary_producing_junk_$(SYSTEM): lib/modules.src/custom_binary_producing_junk.c
$(CC) -o $@ $<
lib/modules/custom_binary_producing_json: lib/modules.src/custom_binary_producing_json.c
lib/modules/custom_binary_producing_json_$(SYSTEM): lib/modules.src/custom_binary_producing_json.c
$(CC) -o $@ $<
clean:

@ -6,7 +6,6 @@ import re
import subprocess
import tempfile
LOG = logging.getLogger(__name__)
suffixes = [
@ -42,9 +41,10 @@ def run(s):
return fp.read()
logging.basicConfig(level=logging.DEBUG)
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
for suffix in suffixes:
for suffix in suffixes:
ansible = run('ansible localhost %s' % (suffix,))
mitogen = run('ANSIBLE_STRATEGY=mitogen ansible localhost %s' % (suffix,))

@ -1,5 +1,11 @@
# vim: syntax=dosini
# This must be defined explicitly, otherwise _create_implicit_localhost()
# generates its own copy, which includes an ansible_python_interpreter that
# varies according to host machine.
localhost
[connection-delegation-test]
cd-bastion
cd-rack11 mitogen_via=ssh-user@cd-bastion

@ -23,7 +23,6 @@
register: raw
# Can't test stdout because TTY inserts \r in Ansible version.
- debug: msg={{raw}}
- name: Verify raw module output.
assert:
that:

@ -28,18 +28,18 @@
method: _make_tmp_path
register: tmp_path2
- name: "Find parent temp path"
- name: "Find good temp path"
set_fact:
parent_temp_path: "{{tmp_path.result|dirname}}"
good_temp_path: "{{tmp_path.result|dirname}}"
- name: "Find parent temp path (new task)"
- name: "Find good temp path (new task)"
set_fact:
parent_temp_path2: "{{tmp_path2.result|dirname}}"
good_temp_path2: "{{tmp_path2.result|dirname}}"
- name: "Verify common base path for both tasks"
assert:
that:
- parent_temp_path == parent_temp_path2
- good_temp_path == good_temp_path2
- name: "Verify different subdir for both tasks"
assert:
@ -67,15 +67,15 @@
- not stat2.stat.exists
#
# Verify parent directory persistence.
# Verify good directory persistence.
#
- name: Stat parent temp path (new task)
- name: Stat good temp path (new task)
stat:
path: "{{parent_temp_path}}"
path: "{{good_temp_path}}"
register: stat
- name: "Verify parent temp path is persistent"
- name: "Verify good temp path is persistent"
assert:
that:
- stat.stat.exists
@ -102,36 +102,6 @@
that:
- not out.stat.exists
#
#
#
- name: "Verify temp path changes across connection reset"
mitogen_shutdown_all:
- name: "Verify temp path changes across connection reset"
action_passthrough:
method: _make_tmp_path
register: tmp_path2
- name: "Verify temp path changes across connection reset"
set_fact:
parent_temp_path2: "{{tmp_path2.result|dirname}}"
- name: "Verify temp path changes across connection reset"
assert:
that:
- parent_temp_path != parent_temp_path2
- name: "Verify old path disappears across connection reset"
stat: path={{parent_temp_path}}
register: junk_stat
- name: "Verify old path disappears across connection reset"
assert:
that:
- not junk_stat.stat.exists
#
# root
#
@ -175,12 +145,12 @@
when: ansible_version.full < '2.5'
assert:
that:
- out.module_path.startswith(parent_temp_path2)
- out.module_path.startswith(good_temp_path2)
- out.module_tmpdir == None
- name: "Verify modules get the same tmpdir as the action plugin (>2.5)"
when: ansible_version.full > '2.5'
assert:
that:
- out.module_path.startswith(parent_temp_path2)
- out.module_tmpdir.startswith(parent_temp_path2)
- out.module_path.startswith(good_temp_path2)
- out.module_tmpdir.startswith(good_temp_path2)

@ -3,7 +3,22 @@
- name: integration/action/synchronize.yml
hosts: test-targets
any_errors_fatal: true
vars:
ansible_user: mitogen__has_sudo_pubkey
ansible_ssh_private_key_file: /tmp/synchronize-action-key
tasks:
# must copy git file to set proper file mode.
- copy:
dest: /tmp/synchronize-action-key
src: ../../../data/docker/mitogen__has_sudo_pubkey.key
mode: u=rw,go=
connection: local
- file:
path: /tmp/sync-test
state: absent
connection: local
- file:
path: /tmp/sync-test
state: directory
@ -14,12 +29,17 @@
content: "item!"
connection: local
- file:
path: /tmp/sync-test.out
state: absent
- synchronize:
dest: /tmp/sync-test
src: /tmp/sync-test
private_key: /tmp/synchronize-action-key
dest: /tmp/sync-test.out
src: /tmp/sync-test/
- slurp:
src: /tmp/sync-test/item
src: /tmp/sync-test.out/item
register: out
- set_fact: outout="{{out.content|b64decode}}"

@ -37,8 +37,6 @@
src: /tmp/transfer-data
register: out
- debug: msg={{out}}
- assert:
that:
out.content|b64decode == 'I am text.'

@ -5,10 +5,21 @@
any_errors_fatal: true
tasks:
- custom_binary_producing_json:
- block:
- custom_binary_producing_json_Darwin:
async: 100
poll: 0
register: job
register: job_darwin
- set_fact: job={{job_darwin}}
when: ansible_system == "Darwin"
- block:
- custom_binary_producing_json_Linux:
async: 100
poll: 0
register: job_linux
- set_fact: job={{job_linux}}
when: ansible_system == "Linux"
- assert:
that: |
@ -30,9 +41,9 @@
src: "{{ansible_user_dir}}/.ansible_async/{{job.ansible_job_id}}"
register: result
- debug: msg={{async_out}}
vars:
async_out: "{{result.content|b64decode|from_json}}"
#- debug: msg={{async_out}}
#vars:
#async_out: "{{result.content|b64decode|from_json}}"
- assert:
that:

@ -5,10 +5,21 @@
any_errors_fatal: true
tasks:
- custom_binary_producing_junk:
- block:
- custom_binary_producing_junk_Darwin:
async: 100
poll: 0
register: job
register: job_darwin
- set_fact: job={{job_darwin}}
when: ansible_system == "Darwin"
- block:
- custom_binary_producing_junk_Linux:
async: 100
poll: 0
register: job_linux
- set_fact: job={{job_linux}}
when: ansible_system == "Linux"
- shell: sleep 1
@ -16,9 +27,9 @@
src: "{{ansible_user_dir}}/.ansible_async/{{job.ansible_job_id}}"
register: result
- debug: msg={{async_out}}
vars:
async_out: "{{result.content|b64decode|from_json}}"
#- debug: msg={{async_out}}
#vars:
#async_out: "{{result.content|b64decode|from_json}}"
- assert:
that:

@ -16,9 +16,9 @@
src: "{{ansible_user_dir}}/.ansible_async/{{job.ansible_job_id}}"
register: result
- debug: msg={{async_out}}
vars:
async_out: "{{result.content|b64decode|from_json}}"
#- debug: msg={{async_out}}
#vars:
#async_out: "{{result.content|b64decode|from_json}}"
- assert:
that:

@ -11,7 +11,6 @@
vars:
ansible_become_flags: --derps
- debug: msg={{out}}
- name: Verify raw module output.
assert:
that:

@ -13,11 +13,10 @@
register: original
connection: local
- stat: path=/tmp/{{file_name}}
- stat: path=/tmp/{{file_name}}.out
register: copied
- assert:
that:
- original.stat.checksum == copied.stat.checksum
#- original.stat.atime == copied.stat.atime
- original.stat.mtime == copied.stat.mtime
- original.stat.mtime|int == copied.stat.mtime|int

@ -10,7 +10,6 @@
- mitogen_get_stack:
register: out
- debug: msg={{out}}
- assert:
that: |
out.result == [

@ -331,9 +331,7 @@
out.result == [
{
'kwargs': {
'python_path': [
hostvars['cd-normal'].local_env.sys_executable
],
'python_path': None
},
'method': 'local',
},

@ -9,7 +9,6 @@
ansible_become_pass: has_sudo_pubkey_password
tasks:
- debug: msg={{hostvars}}
- mitogen_test_gethostbyname:
name: www.google.com
register: out

@ -9,7 +9,6 @@
- custom_python_external_module:
register: out
- debug: msg={{out}}
- assert:
that:
- out.external1_path == "ansible/integration/module_utils/module_utils/external1.py"

@ -3,7 +3,6 @@
- uses_external3:
register: out
- debug: msg={{out}}
- assert:
that:
- out.external3_path == "integration/module_utils/roles/modrole/module_utils/external3.py"

@ -3,7 +3,6 @@
- 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"

@ -9,7 +9,5 @@
SOME_ENV: 123
register: result
- debug: msg={{result}}
- assert:
that: "result.stdout == '123'"

@ -1,11 +1,23 @@
- name: integration/runner/custom_binary_producing_json.yml
hosts: test-targets
any_errors_fatal: true
gather_facts: true
tasks:
- custom_binary_producing_json:
- block:
- custom_binary_producing_json_Darwin:
foo: true
with_sequence: start=1 end={{end|default(1)}}
register: out
register: out_darwin
- set_fact: out={{out_darwin}}
when: ansible_system == "Darwin"
- block:
- custom_binary_producing_json_Linux:
foo: true
with_sequence: start=1 end={{end|default(1)}}
register: out_linux
- set_fact: out={{out_linux}}
when: ansible_system == "Linux"
- assert:
that: |

@ -1,17 +1,29 @@
- name: integration/runner/custom_binary_producing_junk.yml
hosts: test-targets
gather_facts: true
tasks:
- custom_binary_producing_junk:
- block:
- custom_binary_producing_junk_Darwin:
foo: true
with_sequence: start=1 end={{end|default(1)}}
ignore_errors: true
register: out
register: out_darwin
- set_fact: out={{out_darwin}}
when: ansible_system == "Darwin"
- block:
- custom_binary_producing_junk_Linux:
foo: true
with_sequence: start=1 end={{end|default(1)}}
ignore_errors: true
register: out_linux
- set_fact: out={{out_linux}}
when: ansible_system == "Linux"
- hosts: test-targets
any_errors_fatal: true
tasks:
- debug: msg={{out}}
- assert:
that: |
out.failed and

@ -0,0 +1,2 @@
# Integration tests that require a real target available.

@ -0,0 +1,2 @@
- import_playbook: kubectl.yml

@ -1,8 +1,11 @@
---
- name: "Create pod"
tags: always
tags: create
hosts: localhost
vars:
pod_count: 10
loop_count: 5
gather_facts: no
tasks:
- name: Create a test pod
@ -19,7 +22,10 @@
- name: python2
image: python:2
args: [ "sleep", "100000" ]
loop: "{{ range(10)|list }}"
- name: python3
image: python:3
args: [ "sleep", "100000" ]
loop: "{{ range(pod_count|int)|list }}"
- name: "Wait pod to be running"
debug: { msg: "pod is running" }
@ -30,7 +36,7 @@
delay: 2
vars:
pod_def: "{{lookup('k8s', kind='Pod', namespace='default', resource_name='test-pod-' ~ item)}}"
loop: "{{ range(10)|list }}"
loop: "{{ range(pod_count|int)|list }}"
- name: "Add pod to pods group"
add_host:
@ -39,45 +45,95 @@
ansible_connection: "kubectl"
changed_when: no
tags: "always"
loop: "{{ range(10)|list }}"
loop: "{{ range(pod_count|int)|list }}"
- name: "Test kubectl connection (default strategy)"
tags: default
hosts: pods
strategy: "linear"
vars:
pod_count: 10
loop_count: 5
gather_facts: no
tasks:
- name: "Simple shell with linear"
shell: ls /tmp
loop: [ 1, 2, 3, 4, 5 ]
loop: "{{ range(loop_count|int)|list }}"
- name: "Simple file with linear"
file:
path: "/etc"
state: directory
loop: [ 1, 2, 3, 4, 5 ]
loop: "{{ range(loop_count|int)|list }}"
- block:
- name: "Check python version on python3 container"
command: python --version
vars:
ansible_kubectl_container: python3
register: _
- assert: { that: "'Python 3' in _.stdout" }
- debug: var=_.stdout,_.stderr
run_once: yes
- name: "Check python version on default container"
command: python --version
register: _
- assert: { that: "'Python 2' in _.stderr" }
- debug: var=_.stdout,_.stderr
run_once: yes
- name: "Test kubectl connection (mitogen strategy)"
tags: mitogen
hosts: pods
strategy: "mitogen_linear"
vars:
pod_count: 10
loop_count: 5
gather_facts: no
tasks:
- name: "Simple shell with mitogen"
shell: ls /tmp
loop: [ 1, 2, 3, 4, 5 ]
loop: "{{ range(loop_count|int)|list }}"
- name: "Simple file with mitogen"
file:
path: "/etc"
state: directory
loop: [ 1, 2, 3, 4, 5 ]
loop: "{{ range(loop_count|int)|list }}"
- block:
- name: "Check python version on python3 container"
command: python --version
vars:
ansible_kubectl_container: python3
register: _
- assert: { that: "'Python 3' in _.stdout" }
- debug: var=_.stdout,_.stderr
run_once: yes
- name: "Check python version on default container"
command: python --version
register: _
- assert: { that: "'Python 2' in _.stderr" }
- debug: var=_.stdout,_.stderr
run_once: yes
tags: check
- name: "Destroy pod"
tags: cleanup
hosts: localhost
hosts: pods
gather_facts: no
vars:
ansible_connection: "local"
tasks:
- name: Destroy pod
k8s:
@ -86,6 +142,5 @@
apiVersion: v1
kind: Pod
metadata:
name: test-pod-{{item}}
name: "{{inventory_hostname}}"
namespace: default
loop: "{{ range(10)|list }}"

@ -1,20 +0,0 @@
import unittest2
import ansible_mitogen.helpers
import testlib
class ApplyModeSpecTest(unittest2.TestCase):
func = staticmethod(ansible_mitogen.helpers.apply_mode_spec)
def test_simple(self):
spec = 'u+rwx,go=x'
self.assertEquals(0711, self.func(spec, 0))
spec = 'g-rw'
self.assertEquals(0717, self.func(spec, 0777))
if __name__ == '__main__':
unittest2.main()

@ -0,0 +1,77 @@
from __future__ import absolute_import
import os.path
import subprocess
import tempfile
import unittest2
import mock
import ansible_mitogen.target
import testlib
LOGGER_NAME = ansible_mitogen.target.LOG.name
class NamedTemporaryDirectory(object):
def __enter__(self):
self.path = tempfile.mkdtemp()
return self.path
def __exit__(self, _1, _2, _3):
subprocess.check_call(['rm', '-rf', self.path])
class ApplyModeSpecTest(unittest2.TestCase):
func = staticmethod(ansible_mitogen.target.apply_mode_spec)
def test_simple(self):
spec = 'u+rwx,go=x'
self.assertEquals(0711, self.func(spec, 0))
spec = 'g-rw'
self.assertEquals(0717, self.func(spec, 0777))
class IsGoodTempDirTest(unittest2.TestCase):
func = staticmethod(ansible_mitogen.target.is_good_temp_dir)
def test_creates(self):
with NamedTemporaryDirectory() as temp_path:
bleh = os.path.join(temp_path, 'bleh')
self.assertFalse(os.path.exists(bleh))
self.assertTrue(self.func(bleh))
self.assertTrue(os.path.exists(bleh))
def test_file_exists(self):
with NamedTemporaryDirectory() as temp_path:
bleh = os.path.join(temp_path, 'bleh')
with open(bleh, 'w') as fp:
fp.write('derp')
self.assertTrue(os.path.isfile(bleh))
self.assertFalse(self.func(bleh))
self.assertEquals(open(bleh).read(), 'derp')
def test_unwriteable(self):
with NamedTemporaryDirectory() as temp_path:
os.chmod(temp_path, 0)
self.assertFalse(self.func(temp_path))
os.chmod(temp_path, int('0700', 8))
@mock.patch('os.chmod')
def test_weird_filesystem(self, os_chmod):
os_chmod.side_effect = OSError('nope')
with NamedTemporaryDirectory() as temp_path:
self.assertFalse(self.func(temp_path))
@mock.patch('os.access')
def test_noexec(self, os_access):
os_access.return_value = False
with NamedTemporaryDirectory() as temp_path:
self.assertFalse(self.func(temp_path))
if __name__ == '__main__':
unittest2.main()

@ -7,6 +7,7 @@
sudo_group:
MacOSX: admin
Debian: sudo
Ubuntu: sudo
CentOS: wheel
- import_playbook: _container_setup.yml

@ -1,5 +1,6 @@
import mock
import textwrap
import subprocess
import sys
@ -12,6 +13,60 @@ import plain_old_module
import simple_pkg.a
class NeutralizeMainTest(testlib.RouterMixin, unittest2.TestCase):
klass = mitogen.master.ModuleResponder
def call(self, *args, **kwargs):
return self.klass(self.router).neutralize_main(*args, **kwargs)
def test_missing_exec_guard(self):
path = testlib.data_path('main_with_no_exec_guard.py')
args = [sys.executable, path]
proc = subprocess.Popen(args, stderr=subprocess.PIPE)
_, stderr = proc.communicate()
self.assertEquals(1, proc.returncode)
expect = self.klass.main_guard_msg % (path,)
self.assertTrue(expect in stderr.decode())
HAS_MITOGEN_MAIN = mitogen.core.b(
textwrap.dedent("""
herp derp
def myprog():
pass
@mitogen.main(maybe_some_option=True)
def main(router):
pass
""")
)
def test_mitogen_main(self):
untouched = self.call("derp.py", self.HAS_MITOGEN_MAIN)
self.assertEquals(untouched, self.HAS_MITOGEN_MAIN)
HAS_EXEC_GUARD = mitogen.core.b(
textwrap.dedent("""
herp derp
def myprog():
pass
def main():
pass
if __name__ == '__main__':
main()
""")
)
def test_exec_guard(self):
touched = self.call("derp.py", self.HAS_EXEC_GUARD)
bits = touched.decode().split()
self.assertEquals(bits[-3:], ['def', 'main():', 'pass'])
class GoodModulesTest(testlib.RouterMixin, unittest2.TestCase):
def test_plain_old_module(self):
# The simplest case: a top-level module with no interesting imports or

@ -158,22 +158,48 @@ def sync_with_broker(broker, timeout=10.0):
sem.get(timeout=10.0)
class CaptureStreamHandler(logging.StreamHandler):
def __init__(self, *args, **kwargs):
super(CaptureStreamHandler, self).__init__(*args, **kwargs)
self.msgs = []
def emit(self, msg):
self.msgs.append(msg)
return super(CaptureStreamHandler, self).emit(msg)
class LogCapturer(object):
def __init__(self, name=None):
self.sio = StringIO()
self.logger = logging.getLogger(name)
self.handler = logging.StreamHandler(self.sio)
self.handler = CaptureStreamHandler(self.sio)
self.old_propagate = self.logger.propagate
self.old_handlers = self.logger.handlers
self.old_level = self.logger.level
def start(self):
self.logger.handlers = [self.handler]
self.logger.propagate = False
self.logger.level = logging.DEBUG
def raw(self):
return self.sio.getvalue()
def msgs(self):
return self.handler.msgs
def __enter__(self):
self.start()
return self
def __exit__(self, _1, _2, _3):
self.stop()
def stop(self):
self.logger.level = self.old_level
self.logger.handlers = self.old_handlers
self.logger.propagate = self.old_propagate
return self.sio.getvalue()
return self.raw()
class TestCase(unittest2.TestCase):

Loading…
Cancel
Save