diff --git a/docs/api.rst b/docs/api.rst index 52d5dcec..ea20ada7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -717,6 +717,10 @@ Router Class :param bool preserve_env: If :data:`True`, request ``sudo`` to preserve the environment of the parent process. + :param str selinux_type: + If not :data:`None`, the SELinux security context to use. + :param str selinux_role: + If not :data:`None`, the SELinux role to use. :param list sudo_args: Arguments in the style of :data:`sys.argv` that would normally be passed to ``sudo``. The arguments are parsed in-process to set diff --git a/docs/internals.rst b/docs/internals.rst index 03f12e1e..9c533952 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -32,7 +32,7 @@ PidfulStreamHandler Class Side Class ----------- +========== .. currentmodule:: mitogen.core @@ -105,7 +105,7 @@ Side Class Stream Classes --------------- +============== .. currentmodule:: mitogen.core @@ -196,7 +196,7 @@ Stream Classes Other Stream Subclasses ------------------------ +======================= .. currentmodule:: mitogen.core @@ -208,7 +208,7 @@ Other Stream Subclasses Poller Class ------------- +============ .. currentmodule:: mitogen.core .. autoclass:: Poller @@ -221,7 +221,7 @@ Poller Class Importer Class --------------- +============== .. currentmodule:: mitogen.core .. autoclass:: Importer @@ -229,15 +229,23 @@ Importer Class Responder Class ---------------- +=============== .. currentmodule:: mitogen.master .. autoclass:: ModuleResponder :members: +RouteMonitor Class +================== + +.. currentmodule:: mitogen.parent +.. autoclass:: RouteMonitor + :members: + + Forwarder Class ---------------- +=============== .. currentmodule:: mitogen.parent .. autoclass:: ModuleForwarder @@ -245,7 +253,7 @@ Forwarder Class ExternalContext Class ---------------------- +===================== .. currentmodule:: mitogen.core diff --git a/mitogen/parent.py b/mitogen/parent.py index 4549d877..78a62380 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -1431,6 +1431,29 @@ class Context(mitogen.core.Context): class RouteMonitor(object): + """ + Generate and respond to :data:`mitogen.core.ADD_ROUTE` and + :data:`mitogen.core.DEL_ROUTE` messages sent to the local context by + maintaining a table of available routes, and propagating messages towards + parents and siblings as appropriate. + + :class:`RouteMonitor` is responsible for generating routing messages for + directly attached children. It learns of new children via + :meth:`notice_stream` called by :class:`Router`, and subscribes to their + ``disconnect`` event to learn when they disappear. + + In children, constructing this class overwrites the stub + :data:`mitogen.core.DEL_ROUTE` handler installed by + :class:`mitogen.core.ExternalContext`, which is expected behaviour when a + child is beging upgraded in preparation to become a parent of children of + its own. + + :param Router router: + Router to install handlers on. + :param Context parent: + :data:`None` in the master process, or reference to the parent context + we should propagate route updates towards. + """ def __init__(self, router, parent=None): self.router = router self.parent = parent @@ -1451,6 +1474,18 @@ class RouteMonitor(object): self._routes_by_stream = {} def _send_one(self, stream, handle, target_id, name): + """ + Compose and send an update message on a stream. + + :param mitogen.core.Stream stream: + Stream to send it on. + :param int handle: + :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE` + :param int target_id: + ID of the connecting or disconnecting context. + :param str name: + Context name or :data:`None`. + """ data = str(target_id) if name: data = '%s:%s' % (target_id, mitogen.core.b(name)) @@ -1462,20 +1497,34 @@ class RouteMonitor(object): ) ) - def _propagate(self, handle, target_id, name=None): - if not self.parent: - # self.parent is None in the master. - return - - stream = self.router.stream_by_id(self.parent.context_id) - self._send_one(stream, handle, target_id, name) + def _propagate_up(self, handle, target_id, name=None): + """ + In a non-master context, propagate an update towards the master. + + :param int handle: + :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE` + :param int target_id: + ID of the connecting or disconnecting context. + :param str name: + For :data:`mitogen.core.ADD_ROUTE`, the name of the new context + assigned by its parent. This is used by parents to assign the + :attr:`mitogen.core.Context.name` attribute. + """ + if self.parent: + stream = self.router.stream_by_id(self.parent.context_id) + self._send_one(stream, handle, target_id, name) - def _child_propagate(self, handle, target_id): + def _propagate_down(self, handle, target_id): """ For DEL_ROUTE, we additionally want to broadcast the message to any stream that has ever communicated with the disconnecting ID, so core.py's :meth:`mitogen.core.Router._on_del_route` can turn the message into a disconnect event. + + :param int handle: + :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE` + :param int target_id: + ID of the connecting or disconnecting context. """ for stream in itervalues(self.router._stream_by_id): if target_id in stream.egress_ids: @@ -1488,7 +1537,7 @@ class RouteMonitor(object): if/when that child disconnects. """ self._routes_by_stream[stream] = set([stream.remote_id]) - self._propagate(mitogen.core.ADD_ROUTE, stream.remote_id, + self._propagate_up(mitogen.core.ADD_ROUTE, stream.remote_id, stream.name) mitogen.core.listen( obj=stream, @@ -1513,14 +1562,19 @@ class RouteMonitor(object): LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes) for target_id in routes: self.router.del_route(target_id) - self._propagate(mitogen.core.DEL_ROUTE, target_id) - self._child_propagate(mitogen.core.DEL_ROUTE, target_id) + self._propagate_up(mitogen.core.DEL_ROUTE, target_id) + self._propagate_down(mitogen.core.DEL_ROUTE, target_id) context = self.router.context_by_id(target_id, create=False) if context: mitogen.core.fire(context, 'disconnect') def _on_add_route(self, msg): + """ + Respond to :data:`mitogen.core.ADD_ROUTE` by validating the source of + the message, updating the local table, and propagating the message + upwards. + """ if msg.is_dead: return @@ -1539,9 +1593,15 @@ class RouteMonitor(object): LOG.debug('Adding route to %d via %r', target_id, stream) self._routes_by_stream[stream].add(target_id) self.router.add_route(target_id, stream) - self._propagate(mitogen.core.ADD_ROUTE, target_id, target_name) + self._propagate_up(mitogen.core.ADD_ROUTE, target_id, target_name) def _on_del_route(self, msg): + """ + Respond to :data:`mitogen.core.DEL_ROUTE` by validating the source of + the message, updating the local table, propagating the message + upwards, and downwards towards any stream that every had a message + forwarded from it towards the disconnecting context. + """ if msg.is_dead: return @@ -1565,8 +1625,8 @@ class RouteMonitor(object): self.router.del_route(target_id) if stream.remote_id != mitogen.parent_id: - self._propagate(mitogen.core.DEL_ROUTE, target_id) - self._child_propagate(mitogen.core.DEL_ROUTE, target_id) + self._propagate_up(mitogen.core.DEL_ROUTE, target_id) + self._propagate_down(mitogen.core.DEL_ROUTE, target_id) class Router(mitogen.core.Router): diff --git a/mitogen/sudo.py b/mitogen/sudo.py index c410dac9..84b81ddc 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -55,7 +55,10 @@ SUDO_OPTIONS = [ #(False, 'bool', '--list', '-l') #(False, 'bool', '--preserve-groups', '-P') #(False, 'str', '--prompt', '-p') - #(False, 'str', '--role', '-r') + + # SELinux options. Passed through as-is. + (False, 'str', '--role', '-r'), + (False, 'str', '--type', '-t'), # These options are supplied by default by Ansible, but are ignored, as # sudo always runs under a TTY with Mitogen. @@ -63,9 +66,8 @@ SUDO_OPTIONS = [ (True, 'bool', '--non-interactive', '-n'), #(False, 'str', '--shell', '-s') - #(False, 'str', '--type', '-t') #(False, 'str', '--other-user', '-U') - #(False, 'str', '--user', '-u') + (False, 'str', '--user', '-u'), #(False, 'bool', '--version', '-V') #(False, 'bool', '--validate', '-v') ] @@ -103,6 +105,13 @@ class PasswordError(mitogen.core.StreamError): pass +def option(default, *args): + for arg in args: + if arg is not None: + return arg + return default + + class Stream(mitogen.parent.Stream): create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) child_is_immediate_subprocess = False @@ -118,24 +127,24 @@ class Stream(mitogen.parent.Stream): set_home = False login = False + selinux_role = None + selinux_type = None + def construct(self, username=None, sudo_path=None, password=None, preserve_env=None, set_home=None, sudo_args=None, - login=None, **kwargs): + login=None, selinux_role=None, selinux_type=None, **kwargs): super(Stream, self).construct(**kwargs) opts = parse_sudo_flags(sudo_args or []) - if username is not None: - self.username = username - if sudo_path is not None: - self.sudo_path = sudo_path - if password is not None: - self.password = password - if (preserve_env or opts.preserve_env) is not None: - self.preserve_env = preserve_env or opts.preserve_env - if (set_home or opts.set_home) is not None: - self.set_home = set_home or opts.set_home - if (login or opts.login) is not None: - self.login = True + self.username = option(self.username, username, opts.user) + self.sudo_path = option(self.sudo_path, sudo_path) + self.password = password or None + self.preserve_env = option(self.preserve_env, + preserve_env, opts.preserve_env) + self.set_home = option(self.set_home, set_home, opts.set_home) + self.login = option(self.login, login, opts.login) + self.selinux_role = option(self.selinux_role, selinux_role, opts.role) + self.selinux_type = option(self.selinux_type, selinux_type, opts.type) def connect(self): super(Stream, self).connect() @@ -156,8 +165,12 @@ class Stream(mitogen.parent.Stream): bits += ['-H'] if self.login: bits += ['-i'] + if self.selinux_role: + bits += ['-r', self.selinux_role] + if self.selinux_type: + bits += ['-t', self.selinux_type] - bits = bits + super(Stream, self).get_boot_command() + bits = bits + ['--'] + super(Stream, self).get_boot_command() LOG.debug('sudo command line: %r', bits) return bits diff --git a/tests/sudo_test.py b/tests/sudo_test.py new file mode 100644 index 00000000..87a13cf9 --- /dev/null +++ b/tests/sudo_test.py @@ -0,0 +1,60 @@ + +import os + +import mitogen +import mitogen.lxd +import mitogen.parent + +import unittest2 + +import testlib + + +class ConstructorTest(testlib.RouterMixin, testlib.TestCase): + sudo_path = testlib.data_path('stubs/stub-sudo.py') + + def run_sudo(self, **kwargs): + context = self.router.sudo( + sudo_path=self.sudo_path, + **kwargs + ) + argv = eval(context.call(os.getenv, 'ORIGINAL_ARGV')) + return context, argv + + + def test_basic(self): + context, argv = self.run_sudo() + self.assertEquals(argv[:4], [ + self.sudo_path, + '-u', 'root', + '--' + ]) + + def test_selinux_type_role(self): + context, argv = self.run_sudo( + selinux_type='setype', + selinux_role='serole', + ) + self.assertEquals(argv[:8], [ + self.sudo_path, + '-u', 'root', + '-r', 'serole', + '-t', 'setype', + '--' + ]) + + def test_reparse_args(self): + context, argv = self.run_sudo( + sudo_args=['--type', 'setype', '--role', 'serole', '--user', 'user'] + ) + self.assertEquals(argv[:8], [ + self.sudo_path, + '-u', 'user', + '-r', 'serole', + '-t', 'setype', + '--' + ]) + + +if __name__ == '__main__': + unittest2.main()