diff --git a/README.md b/README.md index 158607fd..5ef2447f 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,9 @@ Please see the documentation. ![](https://i.imgur.com/eBM6LhJ.gif) + +[![Total alerts](https://img.shields.io/lgtm/alerts/g/dw/mitogen.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/dw/mitogen/alerts/) + +[![Build Status](https://travis-ci.org/dw/mitogen.svg?branch=master)](https://travis-ci.org/dw/mitogen) + +[![Pipelines Status](https://dev.azure.com/dw-mitogen/Mitogen/_apis/build/status/dw.mitogen?branchName=master)](https://dev.azure.com/dw-mitogen/Mitogen/_build/latest?definitionId=1?branchName=master) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 5775bbed..a8bb74c7 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -29,6 +29,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import errno import logging import os import pprint @@ -1007,6 +1008,11 @@ class Connection(ansible.plugins.connection.ConnectionBase): #: slightly more overhead, so just randomly subtract 4KiB. SMALL_FILE_LIMIT = mitogen.core.CHUNK_SIZE - 4096 + def _throw_io_error(self, e, path): + if e.args[0] == errno.ENOENT: + s = 'file or module does not exist: ' + path + raise ansible.errors.AnsibleFileNotFound(s) + def put_file(self, in_path, out_path): """ Implement put_file() by streamily transferring the file via @@ -1017,7 +1023,12 @@ class Connection(ansible.plugins.connection.ConnectionBase): :param str out_path: Remote filesystem path to write. """ - st = os.stat(in_path) + try: + st = os.stat(in_path) + except OSError as e: + self._throw_io_error(e, in_path) + raise + if not stat.S_ISREG(st.st_mode): raise IOError('%r is not a regular file.' % (in_path,)) @@ -1025,17 +1036,22 @@ class Connection(ansible.plugins.connection.ConnectionBase): # rather than introducing an extra RTT for the child to request it from # FileService. if st.st_size <= self.SMALL_FILE_LIMIT: - fp = open(in_path, 'rb') try: - s = fp.read(self.SMALL_FILE_LIMIT + 1) - finally: - fp.close() + fp = open(in_path, 'rb') + try: + s = fp.read(self.SMALL_FILE_LIMIT + 1) + finally: + fp.close() + except OSError: + self._throw_io_error(e, in_path) + raise # Ensure did not grow during read. if len(s) == st.st_size: return self.put_data(out_path, s, mode=st.st_mode, utimes=(st.st_atime, st.st_mtime)) + self._connect() self.parent.call_service( service_name='mitogen.service.FileService', method_name='register', diff --git a/ansible_mitogen/logging.py b/ansible_mitogen/logging.py index 37e309e2..97832938 100644 --- a/ansible_mitogen/logging.py +++ b/ansible_mitogen/logging.py @@ -29,7 +29,6 @@ from __future__ import absolute_import import logging import os -import sys import mitogen.core import mitogen.utils diff --git a/ansible_mitogen/mixins.py b/ansible_mitogen/mixins.py index 21c07e01..e91e592e 100644 --- a/ansible_mitogen/mixins.py +++ b/ansible_mitogen/mixins.py @@ -30,7 +30,6 @@ from __future__ import absolute_import import logging import os import pwd -import shutil import traceback try: diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py index a58f39b7..03e7ecdf 100644 --- a/ansible_mitogen/planner.py +++ b/ansible_mitogen/planner.py @@ -205,15 +205,13 @@ class ScriptPlanner(BinaryPlanner): involved here, the vanilla implementation uses it and that use is exploited in common playbooks. """ + key = u'ansible_%s_interpreter' % os.path.basename(path).strip() 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]) - ) + return mitogen.utils.cast(self._inv.templar.template(template)) def _get_interpreter(self): path, arg = ansible_mitogen.parsing.parse_hashbang( diff --git a/ansible_mitogen/process.py b/ansible_mitogen/process.py index 6e18a863..219d78a5 100644 --- a/ansible_mitogen/process.py +++ b/ansible_mitogen/process.py @@ -154,13 +154,16 @@ class MuxProcess(object): _instance = None @classmethod - def start(cls): + def start(cls, _init_logging=True): """ Arrange for the subprocess to be started, if it is not already running. The parent process picks a UNIX socket path the child will use prior to fork, creates a socketpair used essentially as a semaphore, then blocks waiting for the child to indicate the UNIX socket is ready for use. + + :param bool _init_logging: + For testing, if :data:`False`, don't initialize logging. """ if cls.worker_sock is not None: return @@ -180,7 +183,8 @@ class MuxProcess(object): cls.original_env = dict(os.environ) cls.child_pid = os.fork() - ansible_mitogen.logging.setup() + if _init_logging: + ansible_mitogen.logging.setup() if cls.child_pid: cls.child_sock.close() cls.child_sock = None diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py index 9089025d..5826b2c5 100644 --- a/ansible_mitogen/runner.py +++ b/ansible_mitogen/runner.py @@ -40,7 +40,6 @@ from __future__ import unicode_literals import atexit import ctypes -import errno import imp import json import logging diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index 3cdf85f9..dd17f177 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -75,7 +75,6 @@ def wrap_action_loader__get(name, *args, **kwargs): """ klass = action_loader__get(name, class_only=True) if klass: - wrapped_name = 'MitogenActionModule_' + name bases = (ansible_mitogen.mixins.ActionModuleMixin, klass) adorned_klass = type(str(name), bases, {}) if kwargs.get('class_only'): diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ef62611..c4ef4b41 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -141,7 +141,8 @@ Enhancements disconnected targets. This ensures a task will gracefully fail rather than hang, for example on network failure or EC2 instance maintenance. -* `#369 `_: :meth:`Connection.reset` +* `#369 `_, + `#407 `_: :meth:`Connection.reset` is implemented, allowing `meta: reset_connection `_ to shut down the remote interpreter as documented, and improving support for the @@ -391,6 +392,7 @@ bug reports, testing, features and fixes in this release contributed by `Brian Candler `_, `Duane Zamrok `_, `Eric Chang `_, +`Gerben Meijer `_, `Guy Knights `_, `Jesse London `_, `Jiří Vávra `_, diff --git a/examples/mitogen-fuse.py b/examples/mitogen-fuse.py index 7421a0e2..d0cd9a3a 100644 --- a/examples/mitogen-fuse.py +++ b/examples/mitogen-fuse.py @@ -20,7 +20,6 @@ import mitogen.master import mitogen.utils import __main__ -import posix import os diff --git a/examples/service/client.py b/examples/service/client.py index e2d78fc0..fc2d8427 100644 --- a/examples/service/client.py +++ b/examples/service/client.py @@ -1,6 +1,4 @@ -import socket - import mitogen.master import mitogen.unix import mitogen.service diff --git a/examples/service/server.py b/examples/service/server.py index 2f488d20..1f8c1475 100644 --- a/examples/service/server.py +++ b/examples/service/server.py @@ -3,8 +3,6 @@ # hopefully lose those hard-coded magic numbers somehow), but meanwhile this is # a taster of how it looks today. -import time - import mitogen import mitogen.service import mitogen.unix diff --git a/mitogen/core.py b/mitogen/core.py index 825c580c..c0a93514 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -261,7 +261,6 @@ class CallError(Error): else: e = fmt fmt = '%s.%s: %s' % (type(e).__module__, type(e).__name__, e) - args = () tb = sys.exc_info()[2] if tb: fmt += '\n' @@ -920,6 +919,10 @@ class Channel(Sender, Receiver): Sender.__init__(self, context, dst_handle) Receiver.__init__(self, router, handle) + def close(self): + Receiver.close(self) + Sender.close(self) + def __repr__(self): return 'Channel(%s, %s)' % ( Sender.__repr__(self), diff --git a/mitogen/debug.py b/mitogen/debug.py index 19cf1a89..b1372e3e 100644 --- a/mitogen/debug.py +++ b/mitogen/debug.py @@ -99,10 +99,6 @@ def get_router_info(): } -def get_router_info(router): - pass - - def get_stream_info(router_id): router = get_routers().get(router_id) return { diff --git a/mitogen/doas.py b/mitogen/doas.py index 3a6f881d..0f6a106c 100644 --- a/mitogen/doas.py +++ b/mitogen/doas.py @@ -27,7 +27,6 @@ # POSSIBILITY OF SUCH DAMAGE. import logging -import os import mitogen.core import mitogen.parent diff --git a/mitogen/master.py b/mitogen/master.py index 1b6aaa61..85753cbc 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -1025,8 +1025,6 @@ class IdAllocator(object): id_, last_id = self.allocate_block() requestee = self.router.context_by_id(msg.src_id) - allocated = self.router.context_by_id(id_, msg.src_id) - LOG.debug('%r: allocating [%r..%r) to %r', self, id_, last_id, requestee) msg.reply((id_, last_id)) diff --git a/mitogen/parent.py b/mitogen/parent.py index fbb488e5..fad00b49 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -70,7 +70,7 @@ else: try: SC_OPEN_MAX = os.sysconf('SC_OPEN_MAX') -except: +except ValueError: SC_OPEN_MAX = 1024 OPENPTY_MSG = ( @@ -1554,6 +1554,9 @@ class Context(mitogen.core.Context): super(Context, self).__init__(*args, **kwargs) self.default_call_chain = self.call_chain_class(self) + def __ne__(self, other): + return not (self == other) + def __eq__(self, other): return (isinstance(other, mitogen.core.Context) and (other.context_id == self.context_id) and @@ -2079,15 +2082,6 @@ class ModuleForwarder(object): callback = lambda: self._on_cache_callback(msg, fullname) self.importer._request_module(fullname, callback) - def _send_one_module(self, msg, tup): - self.router._async_route( - mitogen.core.Message.pickled( - tup, - dst_id=msg.src_id, - handle=mitogen.core.LOAD_MODULE, - ) - ) - def _on_cache_callback(self, msg, fullname): LOG.debug('%r._on_get_module(): sending %r', self, fullname) stream = self.router.stream_by_id(msg.src_id) diff --git a/mitogen/ssh.py b/mitogen/ssh.py index 2bee15c8..571e80d8 100644 --- a/mitogen/ssh.py +++ b/mitogen/ssh.py @@ -32,7 +32,6 @@ Functionality to allow establishing new slave contexts over an SSH connection. import logging import re -import time try: from shlex import quote as shlex_quote diff --git a/mitogen/su.py b/mitogen/su.py index 9faee2d4..5f52e08b 100644 --- a/mitogen/su.py +++ b/mitogen/su.py @@ -27,7 +27,6 @@ # POSSIBILITY OF SUCH DAMAGE. import logging -import os import mitogen.core import mitogen.parent diff --git a/mitogen/sudo.py b/mitogen/sudo.py index cf8a44d9..b97118c9 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -28,8 +28,6 @@ import logging import optparse -import os -import time import mitogen.core import mitogen.parent diff --git a/tests/ansible/tests/connection_test.py b/tests/ansible/tests/connection_test.py index 33b60695..aaf4bf42 100644 --- a/tests/ansible/tests/connection_test.py +++ b/tests/ansible/tests/connection_test.py @@ -1,19 +1,33 @@ from __future__ import absolute_import +import os import os.path import subprocess import tempfile +import time + import unittest2 import mock +import ansible.errors +import ansible.playbook.play_context +import mitogen.core import ansible_mitogen.connection +import ansible_mitogen.plugins.connection.mitogen_local +import ansible_mitogen.process import testlib LOGGER_NAME = ansible_mitogen.target.LOG.name +# TODO: fixtureize +import mitogen.utils +mitogen.utils.log_to_file() +ansible_mitogen.process.MuxProcess.start(_init_logging=False) + + class OptionalIntTest(unittest2.TestCase): func = staticmethod(ansible_mitogen.connection.optional_int) @@ -34,5 +48,84 @@ class OptionalIntTest(unittest2.TestCase): self.assertEquals(None, self.func({1:2})) +class ConnectionMixin(object): + klass = ansible_mitogen.plugins.connection.mitogen_local.Connection + + def make_connection(self): + play_context = ansible.playbook.play_context.PlayContext() + return self.klass(play_context, new_stdin=False) + + def wait_for_completion(self): + # put_data() is asynchronous, must wait for operation to happen. Do + # that by making RPC for some junk that must run on the thread once op + # completes. + self.conn.get_chain().call(os.getpid) + + def setUp(self): + super(ConnectionMixin, self).setUp() + self.conn = self.make_connection() + + def tearDown(self): + self.conn.close() + super(ConnectionMixin, self).tearDown() + + +class PutDataTest(ConnectionMixin, unittest2.TestCase): + def test_out_path(self): + path = tempfile.mktemp(prefix='mitotest') + contents = mitogen.core.b('contents') + + self.conn.put_data(path, contents) + self.wait_for_completion() + self.assertEquals(contents, open(path, 'rb').read()) + os.unlink(path) + + def test_mode(self): + path = tempfile.mktemp(prefix='mitotest') + contents = mitogen.core.b('contents') + + self.conn.put_data(path, contents, mode=int('0123', 8)) + self.wait_for_completion() + st = os.stat(path) + self.assertEquals(int('0123', 8), st.st_mode & int('0777', 8)) + os.unlink(path) + + +class PutFileTest(ConnectionMixin, unittest2.TestCase): + @classmethod + def setUpClass(cls): + super(PutFileTest, cls).setUpClass() + cls.big_path = tempfile.mktemp(prefix='mitotestbig') + open(cls.big_path, 'w').write('x'*1048576) + + @classmethod + def tearDownClass(cls): + os.unlink(cls.big_path) + super(PutFileTest, cls).tearDownClass() + + def test_out_path_tiny(self): + path = tempfile.mktemp(prefix='mitotest') + self.conn.put_file(in_path=__file__, out_path=path) + self.wait_for_completion() + self.assertEquals(open(path, 'rb').read(), + open(__file__, 'rb').read()) + + os.unlink(path) + + def test_out_path_big(self): + path = tempfile.mktemp(prefix='mitotest') + self.conn.put_file(in_path=self.big_path, out_path=path) + self.wait_for_completion() + self.assertEquals(open(path, 'rb').read(), + open(self.big_path, 'rb').read()) + #self._compare_times_modes(path, __file__) + os.unlink(path) + + def test_big_in_path_not_found(self): + path = tempfile.mktemp(prefix='mitotest') + self.assertRaises(ansible.errors.AnsibleFileNotFound, + lambda: self.conn.put_file(in_path='/nonexistent', out_path=path)) + + if __name__ == '__main__': unittest2.main()