From 9d070541d92234a398169c6652abff3d80359fc4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 2 Oct 2018 21:06:00 +0100 Subject: [PATCH] ansible: try to create tempdir if missing. Closes #358. --- ansible_mitogen/target.py | 72 +++++++++++++++++---------- tests/ansible/tests/helpers_test.py | 20 -------- tests/ansible/tests/target_test.py | 77 +++++++++++++++++++++++++++++ tests/testlib.py | 30 ++++++++++- 4 files changed, 150 insertions(+), 49 deletions(-) delete mode 100644 tests/ansible/tests/helpers_test.py create mode 100644 tests/ansible/tests/target_test.py diff --git a/ansible_mitogen/target.py b/ansible_mitogen/target.py index 026ddda7..ff6ed083 100644 --- a/ansible_mitogen/target.py +++ b/ansible_mitogen/target.py @@ -214,6 +214,50 @@ def _on_broker_shutdown(): prune_tree(temp_dir) +def is_good_temp_dir(path): + """ + 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`. + """ + 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 + + try: + tmp = tempfile.NamedTemporaryFile( + prefix='ansible_mitogen_is_good_temp_dir', + dir=path, + ) + except (OSError, IOError) as e: + LOG.debug('temp dir %r unusable: %s', path, e) + return False + + 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) + return False + + 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) + return False + finally: + tmp.close() + + return True + + def find_good_temp_dir(candidate_temp_dirs): """ Given a list of candidate temp directories extracted from ``ansible.cfg``, @@ -230,35 +274,9 @@ def find_good_temp_dir(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 - + if is_good_temp_dir(path): 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), diff --git a/tests/ansible/tests/helpers_test.py b/tests/ansible/tests/helpers_test.py deleted file mode 100644 index 95973b1f..00000000 --- a/tests/ansible/tests/helpers_test.py +++ /dev/null @@ -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() diff --git a/tests/ansible/tests/target_test.py b/tests/ansible/tests/target_test.py new file mode 100644 index 00000000..e3d59433 --- /dev/null +++ b/tests/ansible/tests/target_test.py @@ -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() diff --git a/tests/testlib.py b/tests/testlib.py index d812609c..63d96233 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -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):