diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index eee0ac0e..e4d2d786 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -112,6 +112,26 @@ def _connect_lxc(spec): } +def _connect_machinectl(spec): + return _connect_setns(dict(spec, mitogen_kind='machinectl')) + + +def _connect_setns(spec): + print 'ULTRAFLEEN', spec['remote_addr'], spec['remote_user'] + return { + 'method': 'setns', + 'kwargs': { + 'container': spec['remote_addr'], + 'username': spec['remote_user'], + 'python_path': spec['python_path'], + 'kind': spec['mitogen_kind'], + 'docker_path': spec['mitogen_docker_path'], + 'lxc_info_path': spec['mitogen_lxc_info_path'], + 'machinectl_path': spec['mitogen_machinectl_path'], + } + } + + def _connect_sudo(spec): return { 'method': 'sudo', @@ -132,6 +152,8 @@ CONNECTION_METHOD = { 'local': _connect_local, 'lxc': _connect_lxc, 'lxd': _connect_lxc, + 'machinectl': _connect_machinectl, + 'setns': _connect_setns, 'ssh': _connect_ssh, 'sudo': _connect_sudo, } @@ -178,6 +200,10 @@ def config_from_play_context(transport, inventory_name, connection): for term in shlex.split(s or '') ], 'mitogen_via': connection.mitogen_via, + 'mitogen_kind': connection.mitogen_kind, + 'mitogen_docker_path': connection.mitogen_docker_path, + 'mitogen_lxc_info_path': connection.mitogen_lxc_info_path, + 'mitogen_machinectl_path': connection.mitogen_machinectl_path, } @@ -202,6 +228,10 @@ def config_from_hostvars(transport, inventory_name, connection, 'private_key_file': (hostvars.get('ansible_ssh_private_key_file') or hostvars.get('ansible_private_key_file')), 'mitogen_via': hostvars.get('mitogen_via'), + 'mitogen_kind': hostvars.get('mitogen_kind'), + 'mitogen_docker_path': hostvars.get('mitogen_docker_path'), + 'mitogen_lxc_info_path': hostvars.get('mitogen_lxc_info_path'), + 'mitogen_machinectl_path': hostvars.get('mitogen_machinctl_path'), }) @@ -232,6 +262,18 @@ class Connection(ansible.plugins.connection.ConnectionBase): #: Set to 'mitogen_via' by on_action_run(). mitogen_via = None + #: Set to 'mitogen_kind' by on_action_run(). + mitogen_kind = None + + #: Set to 'mitogen_docker_path' by on_action_run(). + mitogen_docker_path = None + + #: Set to 'mitogen_lxc_info_path' by on_action_run(). + mitogen_lxc_info_path = None + + #: Set to 'mitogen_lxc_info_path' by on_action_run(). + mitogen_machinectl_path = None + #: Set to 'inventory_hostname' by on_action_run(). inventory_hostname = None @@ -267,6 +309,10 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.python_path = task_vars.get('ansible_python_interpreter', '/usr/bin/python') self.mitogen_via = task_vars.get('mitogen_via') + self.mitogen_kind = task_vars.get('mitogen_kind') + self.mitogen_docker_path = task_vars.get('mitogen_docker_path') + self.mitogen_lxc_info_path = task_vars.get('mitogen_lxc_info_path') + self.mitogen_machinectl_path = task_vars.get('mitogen_machinectl_path') self.inventory_hostname = task_vars['inventory_hostname'] self.host_vars = task_vars['hostvars'] self.close(new_task=True) diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index 44d08566..ce528c7f 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -63,12 +63,11 @@ def wrap_action_loader__get(name, *args, **kwargs): def wrap_connection_loader__get(name, play_context, new_stdin, **kwargs): """ - While the mitogen strategy is active, rewrite connection_loader.get() calls - for the 'ssh' and 'local' transports into corresponding requests for the - 'mitogen' connection type, passing the original transport name into it as - an argument, so that it can emulate the original type. + While the strategy is active, rewrite connection_loader.get() calls for + some transports into requests for a compatible Mitogen transport. """ - if name in ('ssh', 'local', 'docker', 'lxc', 'lxd', 'jail'): + if name in ('docker', 'jail', 'local', 'lxc', + 'lxd', 'machinectl', 'setns', 'ssh'): name = 'mitogen_' + name return connection_loader__get(name, play_context, new_stdin, **kwargs) diff --git a/ansible_mitogen/target.py b/ansible_mitogen/target.py index 99bb5ec4..425ed4bf 100644 --- a/ansible_mitogen/target.py +++ b/ansible_mitogen/target.py @@ -159,7 +159,7 @@ def transfer_file(context, in_path, out_path, sync=False, set_owner=False): prefix='.ansible_mitogen_transfer-', dir=os.path.dirname(out_path)) fp = os.fdopen(fd, 'wb', mitogen.core.CHUNK_SIZE) - LOG.debug('transfer_file(%r) tempory file: %s', out_path, tmp_path) + LOG.debug('transfer_file(%r) temporary file: %s', out_path, tmp_path) try: try: diff --git a/docs/ansible.rst b/docs/ansible.rst index 71607d7a..a87382e0 100644 --- a/docs/ansible.rst +++ b/docs/ansible.rst @@ -455,6 +455,20 @@ connection delegation is supported. * ``ansible_user``: Name of user within the container to execute as. +Machinectl +~~~~~~~~~~ + +Behaves like `machinectl +`_ except +connection delegation is supported. This is a lightweight wrapper around the +``setns`` method below. + +* ``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``. + + Sudo ~~~~ @@ -478,9 +492,10 @@ supported. Utility programs must still be installed to discover the PID of the container's root process. -* ``mitogen_container_kind``: one of ``docker``, ``lxc`` or ``machinectl``. +* ``mitogen_kind``: one of ``docker``, ``lxc`` or ``machinectl``. * ``ansible_host``: Name of container as it is known to the corresponding tool (default: inventory hostname). +* ``ansible_user``: Name of user within the container to execute as. * ``mitogen_docker_path``: path to Docker if not available on the system path. * ``mitogen_lxc_info_path``: path to ``lxc-info`` command if not available as ``/usr/bin/lxc-info``. @@ -537,6 +552,7 @@ rather than the LXC Python bindings, as is usual with the ``lxc`` method. The ``lxc-attach`` command must be available on the host machine. +* ``ansible_python_interpreter`` * ``ansible_host``: Name of LXC container (default: inventory hostname). diff --git a/mitogen/parent.py b/mitogen/parent.py index cbeea1bb..eae9ee6c 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -484,7 +484,10 @@ def _proxy_connect(name, method_name, kwargs, econtext): return { 'id': None, 'name': None, - 'msg': str(sys.exc_info()[1]), + 'msg': 'error occurred on host %s: %s' % ( + socket.gethostname(), + sys.exc_info()[1], + ), } return { diff --git a/mitogen/setns.py b/mitogen/setns.py index 4b87ef34..2311951e 100644 --- a/mitogen/setns.py +++ b/mitogen/setns.py @@ -27,9 +27,12 @@ # POSSIBILITY OF SUCH DAMAGE. import ctypes +import grp import logging import os +import pwd import subprocess +import sys import mitogen.core import mitogen.parent @@ -53,11 +56,16 @@ def setns(kind, fd): def _run_command(args): - proc = subprocess.Popen( - args=args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) + argv = mitogen.parent.Argv(args) + try: + proc = subprocess.Popen( + args=args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + except OSError: + e = sys.exc_info()[1] + raise Error('could not execute %s: %s', argv, e) output, _ = proc.communicate() if not proc.returncode: @@ -97,6 +105,7 @@ def get_machinectl_pid(path, name): class Stream(mitogen.parent.Stream): container = None + username = None kind = None docker_path = 'docker' lxc_info_path = 'lxc-info' @@ -108,7 +117,7 @@ class Stream(mitogen.parent.Stream): 'machinectl': ('machinectl_path', get_machinectl_pid), } - def construct(self, container, kind, docker_path=None, + def construct(self, container, kind, username=None, docker_path=None, lxc_info_path=None, machinectl_path=None, **kwargs): super(Stream, self).construct(**kwargs) if kind not in self.GET_LEADER_BY_KIND: @@ -116,6 +125,8 @@ class Stream(mitogen.parent.Stream): self.container = container self.kind = kind + if username: + self.username = username if docker_path: self.docker_path = docker_path if lxc_info_path: @@ -140,12 +151,43 @@ class Stream(mitogen.parent.Stream): except Exception, e: raise Error(str(e)) - os.chroot('/proc/%s/root' % (self.leader_pid,)) + os.chdir('/proc/%s/root' % (self.leader_pid,)) + os.chroot('.') os.chdir('/') for fp in ns_fps: setns(fp.name, fp.fileno()) fp.close() + for sym in 'endpwent', 'endgrent', 'endspent', 'endsgent': + try: + getattr(LIBC, sym)() + except AttributeError: + pass + + if self.username: + try: + os.setgroups([grent.gr_gid + for grent in grp.getgrall() + if self.username in grent.gr_mem]) + pwent = pwd.getpwnam(self.username) + os.setreuid(pwent.pw_uid, pwent.pw_uid) + # shadow-4.4/libmisc/setupenv.c. Not done: MAIL, PATH + os.environ.update({ + 'HOME': pwent.pw_dir, + 'SHELL': pwent.pw_shell or '/bin/sh', + 'LOGNAME': self.username, + 'USER': self.username, + }) + if ((os.path.exists(pwent.pw_dir) and + os.access(pwent.pw_dir, os.X_OK))): + os.chdir(pwent.pw_dir) + except Exception: + e = sys.exc_info()[1] + raise Error(self.username_msg, self.username, self.container, + type(e).__name__, e) + + username_msg = 'while transitioning to user %r in container %r: %s: %s' + def get_boot_command(self): # With setns(CLONE_NEWPID), new children of the caller receive a new # PID namespace, however the caller's namespace won't change. That diff --git a/tests/parent_test.py b/tests/parent_test.py index 888ed186..4cbdad38 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -68,7 +68,7 @@ class StreamErrorTest(testlib.RouterMixin, testlib.TestCase): connect_timeout=3, ) ) - self.assertEquals(e.args[0], "EOF on stream; last 300 bytes received: ''") + self.assertTrue("EOF on stream; last 300 bytes received: ''" in e.args[0]) def test_direct_enoent(self): e = self.assertRaises(mitogen.core.StreamError, @@ -89,8 +89,8 @@ class StreamErrorTest(testlib.RouterMixin, testlib.TestCase): connect_timeout=3, ) ) - prefix = 'Child start failed: [Errno 2] No such file or directory.' - self.assertTrue(e.args[0].startswith(prefix)) + s = 'Child start failed: [Errno 2] No such file or directory.' + self.assertTrue(s in e.args[0]) class ContextTest(testlib.RouterMixin, unittest2.TestCase):