Merge remote-tracking branch 'origin/dmw'

- issue #410 sudo additions
issue260
David Wilson 6 years ago
commit 731b4ac841

@ -717,6 +717,10 @@ Router Class
:param bool preserve_env: :param bool preserve_env:
If :data:`True`, request ``sudo`` to preserve the environment of If :data:`True`, request ``sudo`` to preserve the environment of
the parent process. 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: :param list sudo_args:
Arguments in the style of :data:`sys.argv` that would normally Arguments in the style of :data:`sys.argv` that would normally
be passed to ``sudo``. The arguments are parsed in-process to set be passed to ``sudo``. The arguments are parsed in-process to set

@ -32,7 +32,7 @@ PidfulStreamHandler Class
Side Class Side Class
---------- ==========
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core
@ -105,7 +105,7 @@ Side Class
Stream Classes Stream Classes
-------------- ==============
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core
@ -196,7 +196,7 @@ Stream Classes
Other Stream Subclasses Other Stream Subclasses
----------------------- =======================
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core
@ -208,7 +208,7 @@ Other Stream Subclasses
Poller Class Poller Class
------------ ============
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core
.. autoclass:: Poller .. autoclass:: Poller
@ -221,7 +221,7 @@ Poller Class
Importer Class Importer Class
-------------- ==============
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core
.. autoclass:: Importer .. autoclass:: Importer
@ -229,15 +229,23 @@ Importer Class
Responder Class Responder Class
--------------- ===============
.. currentmodule:: mitogen.master .. currentmodule:: mitogen.master
.. autoclass:: ModuleResponder .. autoclass:: ModuleResponder
:members: :members:
RouteMonitor Class
==================
.. currentmodule:: mitogen.parent
.. autoclass:: RouteMonitor
:members:
Forwarder Class Forwarder Class
--------------- ===============
.. currentmodule:: mitogen.parent .. currentmodule:: mitogen.parent
.. autoclass:: ModuleForwarder .. autoclass:: ModuleForwarder
@ -245,7 +253,7 @@ Forwarder Class
ExternalContext Class ExternalContext Class
--------------------- =====================
.. currentmodule:: mitogen.core .. currentmodule:: mitogen.core

@ -1431,6 +1431,29 @@ class Context(mitogen.core.Context):
class RouteMonitor(object): 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): def __init__(self, router, parent=None):
self.router = router self.router = router
self.parent = parent self.parent = parent
@ -1451,6 +1474,18 @@ class RouteMonitor(object):
self._routes_by_stream = {} self._routes_by_stream = {}
def _send_one(self, stream, handle, target_id, name): 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) data = str(target_id)
if name: if name:
data = '%s:%s' % (target_id, mitogen.core.b(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): def _propagate_up(self, handle, target_id, name=None):
if not self.parent: """
# self.parent is None in the master. In a non-master context, propagate an update towards the master.
return
:param int handle:
stream = self.router.stream_by_id(self.parent.context_id) :data:`mitogen.core.ADD_ROUTE` or :data:`mitogen.core.DEL_ROUTE`
self._send_one(stream, handle, target_id, name) :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 For DEL_ROUTE, we additionally want to broadcast the message to any
stream that has ever communicated with the disconnecting ID, so stream that has ever communicated with the disconnecting ID, so
core.py's :meth:`mitogen.core.Router._on_del_route` can turn the core.py's :meth:`mitogen.core.Router._on_del_route` can turn the
message into a disconnect event. 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): for stream in itervalues(self.router._stream_by_id):
if target_id in stream.egress_ids: if target_id in stream.egress_ids:
@ -1488,7 +1537,7 @@ class RouteMonitor(object):
if/when that child disconnects. if/when that child disconnects.
""" """
self._routes_by_stream[stream] = set([stream.remote_id]) 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) stream.name)
mitogen.core.listen( mitogen.core.listen(
obj=stream, obj=stream,
@ -1513,14 +1562,19 @@ class RouteMonitor(object):
LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes) LOG.debug('%r is gone; propagating DEL_ROUTE for %r', stream, routes)
for target_id in routes: for target_id in routes:
self.router.del_route(target_id) self.router.del_route(target_id)
self._propagate(mitogen.core.DEL_ROUTE, target_id) self._propagate_up(mitogen.core.DEL_ROUTE, target_id)
self._child_propagate(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) context = self.router.context_by_id(target_id, create=False)
if context: if context:
mitogen.core.fire(context, 'disconnect') mitogen.core.fire(context, 'disconnect')
def _on_add_route(self, msg): 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: if msg.is_dead:
return return
@ -1539,9 +1593,15 @@ class RouteMonitor(object):
LOG.debug('Adding route to %d via %r', target_id, stream) LOG.debug('Adding route to %d via %r', target_id, stream)
self._routes_by_stream[stream].add(target_id) self._routes_by_stream[stream].add(target_id)
self.router.add_route(target_id, stream) 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): 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: if msg.is_dead:
return return
@ -1565,8 +1625,8 @@ class RouteMonitor(object):
self.router.del_route(target_id) self.router.del_route(target_id)
if stream.remote_id != mitogen.parent_id: if stream.remote_id != mitogen.parent_id:
self._propagate(mitogen.core.DEL_ROUTE, target_id) self._propagate_up(mitogen.core.DEL_ROUTE, target_id)
self._child_propagate(mitogen.core.DEL_ROUTE, target_id) self._propagate_down(mitogen.core.DEL_ROUTE, target_id)
class Router(mitogen.core.Router): class Router(mitogen.core.Router):

@ -55,7 +55,10 @@ SUDO_OPTIONS = [
#(False, 'bool', '--list', '-l') #(False, 'bool', '--list', '-l')
#(False, 'bool', '--preserve-groups', '-P') #(False, 'bool', '--preserve-groups', '-P')
#(False, 'str', '--prompt', '-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 # These options are supplied by default by Ansible, but are ignored, as
# sudo always runs under a TTY with Mitogen. # sudo always runs under a TTY with Mitogen.
@ -63,9 +66,8 @@ SUDO_OPTIONS = [
(True, 'bool', '--non-interactive', '-n'), (True, 'bool', '--non-interactive', '-n'),
#(False, 'str', '--shell', '-s') #(False, 'str', '--shell', '-s')
#(False, 'str', '--type', '-t')
#(False, 'str', '--other-user', '-U') #(False, 'str', '--other-user', '-U')
#(False, 'str', '--user', '-u') (False, 'str', '--user', '-u'),
#(False, 'bool', '--version', '-V') #(False, 'bool', '--version', '-V')
#(False, 'bool', '--validate', '-v') #(False, 'bool', '--validate', '-v')
] ]
@ -103,6 +105,13 @@ class PasswordError(mitogen.core.StreamError):
pass pass
def option(default, *args):
for arg in args:
if arg is not None:
return arg
return default
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False child_is_immediate_subprocess = False
@ -118,24 +127,24 @@ class Stream(mitogen.parent.Stream):
set_home = False set_home = False
login = False login = False
selinux_role = None
selinux_type = None
def construct(self, username=None, sudo_path=None, password=None, def construct(self, username=None, sudo_path=None, password=None,
preserve_env=None, set_home=None, sudo_args=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) super(Stream, self).construct(**kwargs)
opts = parse_sudo_flags(sudo_args or []) opts = parse_sudo_flags(sudo_args or [])
if username is not None: self.username = option(self.username, username, opts.user)
self.username = username self.sudo_path = option(self.sudo_path, sudo_path)
if sudo_path is not None: self.password = password or None
self.sudo_path = sudo_path self.preserve_env = option(self.preserve_env,
if password is not None: preserve_env, opts.preserve_env)
self.password = password self.set_home = option(self.set_home, set_home, opts.set_home)
if (preserve_env or opts.preserve_env) is not None: self.login = option(self.login, login, opts.login)
self.preserve_env = preserve_env or opts.preserve_env self.selinux_role = option(self.selinux_role, selinux_role, opts.role)
if (set_home or opts.set_home) is not None: self.selinux_type = option(self.selinux_type, selinux_type, opts.type)
self.set_home = set_home or opts.set_home
if (login or opts.login) is not None:
self.login = True
def connect(self): def connect(self):
super(Stream, self).connect() super(Stream, self).connect()
@ -156,8 +165,12 @@ class Stream(mitogen.parent.Stream):
bits += ['-H'] bits += ['-H']
if self.login: if self.login:
bits += ['-i'] 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) LOG.debug('sudo command line: %r', bits)
return bits return bits

@ -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()
Loading…
Cancel
Save