issue #321: take remote_tmp and system_tmpdirs into account.

Can't simply ignore these settings as some users may have weird noexec
filesystems.
pull/350/head
David Wilson 6 years ago
parent a2686b1a2c
commit f24f02ba06

@ -37,10 +37,12 @@ try:
from ansible.plugins.loader import connection_loader from ansible.plugins.loader import connection_loader
from ansible.plugins.loader import module_loader from ansible.plugins.loader import module_loader
from ansible.plugins.loader import module_utils_loader from ansible.plugins.loader import module_utils_loader
from ansible.plugins.loader import shell_loader
from ansible.plugins.loader import strategy_loader from ansible.plugins.loader import strategy_loader
except ImportError: # Ansible <2.4 except ImportError: # Ansible <2.4
from ansible.plugins import action_loader from ansible.plugins import action_loader
from ansible.plugins import connection_loader from ansible.plugins import connection_loader
from ansible.plugins import module_loader from ansible.plugins import module_loader
from ansible.plugins import module_utils_loader from ansible.plugins import module_utils_loader
from ansible.plugins import shell_loader
from ansible.plugins import strategy_loader from ansible.plugins import strategy_loader

@ -315,7 +315,8 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# module_common and ensures no second temporary directory or atexit # module_common and ensures no second temporary directory or atexit
# handler is installed. # handler is installed.
self._connection._connect() self._connection._connect()
module_args['_ansible_tmpdir'] = self._connection.get_temp_dir() if not module_args.get('_ansible_tmpdir', object()):
module_args['_ansible_tmpdir'] = self._connection.get_temp_dir()
return ansible_mitogen.planner.invoke( return ansible_mitogen.planner.invoke(
ansible_mitogen.planner.Invocation( ansible_mitogen.planner.Invocation(

@ -46,8 +46,12 @@ import os.path
import sys import sys
import threading import threading
import ansible.constants
import mitogen import mitogen
import mitogen.service import mitogen.service
import mitogen.utils
import ansible_mitogen.loaders
import ansible_mitogen.module_finder import ansible_mitogen.module_finder
import ansible_mitogen.target import ansible_mitogen.target
@ -69,6 +73,19 @@ else:
) )
def _get_candidate_temp_dirs():
# Force load of plugin to ensure ConfigManager has definitions loaded.
ansible_mitogen.loaders.shell_loader.get('sh')
options = ansible.constants.config.get_plugin_options('shell', 'sh')
# Pre 2.5 this came from ansible.constants.
remote_tmp = (options.get('remote_tmp') or
ansible.constants.DEFAULT_REMOTE_TMP)
dirs = list(options.get('system_tmpdirs', ('/var/tmp', '/tmp')))
dirs.insert(0, remote_tmp)
return mitogen.utils.cast(dirs)
class Error(Exception): class Error(Exception):
pass pass
@ -252,6 +269,18 @@ class ContextService(mitogen.service.Service):
for fullname in self.ALWAYS_PRELOAD: for fullname in self.ALWAYS_PRELOAD:
self.router.responder.forward_module(context, fullname) self.router.responder.forward_module(context, fullname)
_candidate_temp_dirs = None
def _get_candidate_temp_dirs(self):
"""
Return a list of locations to try to create the single temporary
directory used by the run. This simply caches the (expensive) plugin
load of :func:`_get_candidate_temp_dirs`.
"""
if self._candidate_temp_dirs is None:
self._candidate_temp_dirs = _get_candidate_temp_dirs()
return self._candidate_temp_dirs
def _connect(self, key, spec, via=None): def _connect(self, key, spec, via=None):
""" """
Actual connect implementation. Arranges for the Mitogen connection to Actual connect implementation. Arranges for the Mitogen connection to
@ -298,8 +327,11 @@ class ContextService(mitogen.service.Service):
lambda: self._on_stream_disconnect(stream)) lambda: self._on_stream_disconnect(stream))
self._send_module_forwards(context) self._send_module_forwards(context)
init_child_result = context.call(ansible_mitogen.target.init_child, init_child_result = context.call(
log_level=LOG.getEffectiveLevel()) ansible_mitogen.target.init_child,
log_level=LOG.getEffectiveLevel(),
candidate_temp_dirs=self._get_candidate_temp_dirs(),
)
if os.environ.get('MITOGEN_DUMP_THREAD_STACKS'): if os.environ.get('MITOGEN_DUMP_THREAD_STACKS'):
from mitogen import debug from mitogen import debug

@ -70,26 +70,29 @@ import ansible_mitogen.runner
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
MAKE_TEMP_FAILED_MSG = ( MAKE_TEMP_FAILED_MSG = (
"Unable to create a temporary directory for the persistent interpreter.\n" "Unable to find a useable temporary directory. This likely means no\n"
"This likely means no system-supplied TMP directory can be written to.\n" "system-supplied TMP directory can be written to, or all directories\n"
"were mounted on 'noexec' filesystems.\n"
"\n" "\n"
"The following paths were tried:\n" "The following paths were tried:\n"
" %(namelist)s\n" " %(namelist)s\n"
"\n" "\n"
"The original exception was:\n" "Please check '-vvv' output for a log of individual path errors."
"\n"
"%(exception)s"
) )
#: Set by init_child() to the single temporary directory that will exist for
#: the duration of the process.
temp_dir = None
#: Initialized to an econtext.parent.Context pointing at a pristine fork of #: Initialized to an econtext.parent.Context pointing at a pristine fork of
#: the target Python interpreter before it executes any code or imports. #: the target Python interpreter before it executes any code or imports.
_fork_parent = None _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 reset_temp_dir() to the single temporary directory that will exist
#: for the duration of the process.
temp_dir = None
def get_small_file(context, path): def get_small_file(context, path):
""" """
@ -203,6 +206,53 @@ def _on_broker_shutdown():
prune_tree(temp_dir) prune_tree(temp_dir)
def find_good_temp_dir():
"""
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.
"""
paths = [os.path.expandvars(os.path.expanduser(p))
for p in _candidate_temp_dirs]
paths.extend(tempfile._candidate_tempdir_list())
for path in paths:
try:
tmp = tempfile.NamedTemporaryFile(
prefix='ansible_mitogen_find_good_temp_dir',
dir=path,
)
except (OSError, IOError) as e:
LOG.debug('temp dir %r unusable: %s', path, e)
continue
try:
try:
os.chmod(tmp.name, int('0700', 8))
except OSError as e:
LOG.debug('temp dir %r unusable: %s: chmod failed: %s',
path, e)
continue
try:
# access(.., X_OK) is sufficient to detect noexec.
if not os.access(tmp.name, os.X_OK):
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
finally:
tmp.close()
raise IOError(MAKE_TEMP_FAILED_MSG % {
'paths': '\n '.join(paths),
})
@mitogen.core.takes_econtext @mitogen.core.takes_econtext
def reset_temp_dir(econtext): def reset_temp_dir(econtext):
""" """
@ -218,13 +268,8 @@ def reset_temp_dir(econtext):
global temp_dir global temp_dir
# https://github.com/dw/mitogen/issues/239 # https://github.com/dw/mitogen/issues/239
try: basedir = find_good_temp_dir()
temp_dir = tempfile.mkdtemp(prefix='ansible_mitogen_') temp_dir = tempfile.mkdtemp(prefix='ansible_mitogen_', dir=basedir)
except IOError:
raise IOError(MAKE_TEMP_FAILED_MSG % {
'namelist': '\n '.join(tempfile._candidate_tempdir_list()),
'exception': traceback.format_exc()
})
# This must be reinstalled in forked children too, since the Broker # This must be reinstalled in forked children too, since the Broker
# instance from the parent process does not carry over to the new child. # instance from the parent process does not carry over to the new child.
@ -232,7 +277,7 @@ def reset_temp_dir(econtext):
@mitogen.core.takes_econtext @mitogen.core.takes_econtext
def init_child(econtext, log_level): def init_child(econtext, log_level, candidate_temp_dirs):
""" """
Called by ContextService immediately after connection; arranges for the Called by ContextService immediately after connection; arranges for the
(presently) spotless Python interpreter to be forked, where the newly (presently) spotless Python interpreter to be forked, where the newly
@ -245,6 +290,9 @@ def init_child(econtext, log_level):
:param int log_level: :param int log_level:
Logging package level active in the master. Logging package level active in the master.
:param list[str] candidate_temp_dirs:
List of $variable-expanded and tilde-expanded directory names to add to
candidate list of temporary directories.
:returns: :returns:
Dict like:: Dict like::
@ -258,6 +306,9 @@ def init_child(econtext, log_level):
the controller will use to start forked jobs, and `home_dir` is the the controller will use to start forked jobs, and `home_dir` is the
home directory for the active user account. home directory for the active user account.
""" """
global _candidate_temp_dirs
_candidate_temp_dirs = candidate_temp_dirs
global _fork_parent global _fork_parent
mitogen.parent.upgrade_router(econtext) mitogen.parent.upgrade_router(econtext)
_fork_parent = econtext.router.fork() _fork_parent = econtext.router.fork()

@ -443,23 +443,30 @@ In summary, for each task Ansible may create one or more of:
* ``$TMPDIR/ansible_<modname>_payload_.../`` owned by the become user, * ``$TMPDIR/ansible_<modname>_payload_.../`` owned by the become user,
* ``$TMPDIR/ansible-module-tmp-.../`` owned by the become user. * ``$TMPDIR/ansible-module-tmp-.../`` owned by the become user.
The extension must create a temporary directory to maintain compatibility with A directory must be created in order to maintain compatibility with Ansible,
Ansible, since many modules introspect :data:`sys.argv` in order to find a as many modules introspect :data:`sys.argv` in order to find a directory where
directory where they may write temporary files, however for simplicity only they may write files, however for only one such directory exists for the
one such directory exists for the lifetime of each interpreter, stored in a lifetime of each interpreter, its location is consistent for each target
system-supplied temporary directory, and always privately owned by the target account, and it is always privately owned by that account.
user account.
The following candidate paths are tried until one is found that is writeable
The ``remote_tmp`` path is unused, since Ansible does not make exclusive use of and appears live on a filesystem that does not have ``noexec`` enabled:
it, existing semantics are untenable, environments exist with read-only home
directories where the default ``remote_tmp`` path (``~/.ansible/tmp``) cannot 1. The ``$variable`` and tilde-expanded ``remote_tmp`` setting from
be used, and new-style modules always depended on the existence of a ``ansible.cfg``.
system-supplied directory anyway, so no requirement is introduced by simply 2. The ``$variable`` and tilde-expanded ``system_tmpdirs`` setting from
ignoring ``remote_tmp``. ``ansible.cfg``.
3. The ``TMPDIR`` environment variable.
4. The ``TEMP`` environment variable.
5. The ``TMP`` environment variable.
6. ``/tmp``
7. ``/var/tmp``
8. ``/usr/tmp``
9. The current working directory.
As the directory is created once at startup, and its content is managed by code As the directory is created once at startup, and its content is managed by code
running remotely, no additional network roundtrips are required to create and running remotely, no additional network roundtrips are required to create and
destroy it for each task requiring temporary file storage. destroy it for each task requiring temporary storage.
.. _ansible_process_env: .. _ansible_process_env:

@ -3,6 +3,7 @@
# interpreter I run within. # interpreter I run within.
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import get_module_path
from ansible.module_utils import six from ansible.module_utils import six
import os import os
@ -30,6 +31,7 @@ def main():
hostname=socket.gethostname(), hostname=socket.gethostname(),
username=pwd.getpwuid(os.getuid()).pw_name, username=pwd.getpwuid(os.getuid()).pw_name,
module_tmpdir=getattr(module, 'tmpdir', None), module_tmpdir=getattr(module, 'tmpdir', None),
module_path=get_module_path(),
) )
if __name__ == '__main__': if __name__ == '__main__':

Loading…
Cancel
Save