Allow the backdoor to serve from a local unix domain socket

Local files can be made accessible to certain users vs random
ports which can be accessible to anyone on a machine so allow
using unix files as a way to start the eventlet backdoor (so that
user and group permissions common on unix are not lost).

To use this new type of files `socat` is needed (or other way
to interact with telnet over a unix domain socket).

For example (with the path at /tmp/my_special_socket):

  socat - UNIX-CONNECT:/tmp/my_special_socket

Depends-On: Ia2385879e09991102f8f305ec41dbb651a4374de

Change-Id: I7f25913168ebe5854f360db3d6310b72a56b2b4d
This commit is contained in:
Joshua Harlow
2016-02-02 16:17:10 -08:00
committed by Joshua Harlow
parent de959dc78f
commit db1fc249e6
3 changed files with 109 additions and 19 deletions

View File

@@ -24,7 +24,14 @@ help_for_backdoor_port = (
"chosen port is displayed in the service's log file.") "chosen port is displayed in the service's log file.")
eventlet_backdoor_opts = [ eventlet_backdoor_opts = [
cfg.StrOpt('backdoor_port', cfg.StrOpt('backdoor_port',
help="Enable eventlet backdoor. %s" % help_for_backdoor_port) help="Enable eventlet backdoor. %s" % help_for_backdoor_port),
cfg.StrOpt('backdoor_socket',
help="Enable eventlet backdoor, using the provided path"
" as a unix socket that can receive connections. This"
" option is mutually exclusive with 'backdoor_port' in"
" that only one should be be provided. If both are"
" provided then the existence of this option overrides"
" the usage of that option.")
] ]
periodic_opts = [ periodic_opts = [

View File

@@ -133,6 +133,27 @@ def _listen(host, start_port, end_port, listen_func):
try_port += 1 try_port += 1
def _try_open_unix_domain_socket(socket_path):
try:
return eventlet.listen(socket_path, socket.AF_UNIX)
except socket.error as e:
if e.errno != errno.EADDRINUSE:
# NOTE(harlowja): Some other non-address in use error
# occurred, since we aren't handling those, re-raise
# and give up...
raise
else:
# Attempt to remove the file before opening it again.
try:
os.unlink(socket_path)
except OSError as e:
if e.errno != errno.ENOENT:
# NOTE(harlowja): File existed, but we couldn't
# delete it, give up...
raise
return eventlet.listen(socket_path, socket.AF_UNIX)
def _initialize_if_enabled(conf): def _initialize_if_enabled(conf):
conf.register_opts(_options.eventlet_backdoor_opts) conf.register_opts(_options.eventlet_backdoor_opts)
backdoor_locals = { backdoor_locals = {
@@ -143,10 +164,18 @@ def _initialize_if_enabled(conf):
'pnt': _print_nativethreads, 'pnt': _print_nativethreads,
} }
if conf.backdoor_port is None: if conf.backdoor_port is None and conf.backdoor_socket is None:
return None return None
start_port, end_port = _parse_port_range(str(conf.backdoor_port)) if conf.backdoor_socket is None:
start_port, end_port = _parse_port_range(str(conf.backdoor_port))
sock = _listen('localhost', start_port, end_port, eventlet.listen)
# In the case of backdoor port being zero, a port number is assigned by
# listen(). In any case, pull the port number out here.
where_running = sock.getsockname()[1]
else:
sock = _try_open_unix_domain_socket(conf.backdoor_socket)
where_running = conf.backdoor_socket
# NOTE(johannes): The standard sys.displayhook will print the value of # NOTE(johannes): The standard sys.displayhook will print the value of
# the last expression and set it to __builtin__._, which overwrites # the last expression and set it to __builtin__._, which overwrites
@@ -158,27 +187,23 @@ def _initialize_if_enabled(conf):
pprint.pprint(val) pprint.pprint(val)
sys.displayhook = displayhook sys.displayhook = displayhook
sock = _listen('localhost', start_port, end_port, eventlet.listen)
# In the case of backdoor port being zero, a port number is assigned by
# listen(). In any case, pull the port number out here.
port = sock.getsockname()[1]
LOG.info( LOG.info(
_LI('Eventlet backdoor listening on %(port)s for process %(pid)d'), _LI('Eventlet backdoor listening on %(where_running)s for'
{'port': port, 'pid': os.getpid()} ' process %(pid)d'),
{'where_running': where_running, 'pid': os.getpid()}
) )
thread = eventlet.spawn(eventlet.backdoor.backdoor_server, sock, thread = eventlet.spawn(eventlet.backdoor.backdoor_server, sock,
locals=backdoor_locals) locals=backdoor_locals)
return (port, thread) return (where_running, thread)
def initialize_if_enabled(conf): def initialize_if_enabled(conf):
port_thread = _initialize_if_enabled(conf) where_running_thread = _initialize_if_enabled(conf)
if not port_thread: if not where_running_thread:
return None return None
else: else:
port, _thread = port_thread where_running, _thread = where_running_thread
return port return where_running
def _main(): def _main():
@@ -193,11 +218,11 @@ def _main():
conf.register_cli_opts(_options.eventlet_backdoor_opts) conf.register_cli_opts(_options.eventlet_backdoor_opts)
conf(sys.argv[1:]) conf(sys.argv[1:])
port_thread = _initialize_if_enabled(conf) where_running_thread = _initialize_if_enabled(conf)
if not port_thread: if not where_running_thread:
raise RuntimeError("Did not create backdoor at requested port") raise RuntimeError("Did not create backdoor at requested location")
else: else:
_port, thread = port_thread _where_running, thread = where_running_thread
thread.wait() thread.wait()

View File

@@ -18,6 +18,7 @@
Unit Tests for eventlet backdoor Unit Tests for eventlet backdoor
""" """
import errno import errno
import os
import socket import socket
import eventlet import eventlet
@@ -27,6 +28,63 @@ from oslo_service import eventlet_backdoor
from oslo_service.tests import base from oslo_service.tests import base
class BackdoorSocketPathTest(base.ServiceBaseTestCase):
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path(self, listen_mock, spawn_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
listen_mock.side_effect = mock.MagicMock()
path = eventlet_backdoor.initialize_if_enabled(self.conf)
self.assertEqual(path, "/tmp/my_special_socket")
@mock.patch.object(os, 'unlink')
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_already_exists(self, listen_mock,
spawn_mock, unlink_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
sock = mock.MagicMock()
listen_mock.side_effect = [socket.error(errno.EADDRINUSE, ''), sock]
path = eventlet_backdoor.initialize_if_enabled(self.conf)
self.assertEqual(path, "/tmp/my_special_socket")
unlink_mock.assert_called_with("/tmp/my_special_socket")
@mock.patch.object(os, 'unlink')
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_already_exists_and_gone(self, listen_mock,
spawn_mock, unlink_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
sock = mock.MagicMock()
listen_mock.side_effect = [socket.error(errno.EADDRINUSE, ''), sock]
unlink_mock.side_effect = OSError(errno.ENOENT, '')
path = eventlet_backdoor.initialize_if_enabled(self.conf)
self.assertEqual(path, "/tmp/my_special_socket")
unlink_mock.assert_called_with("/tmp/my_special_socket")
@mock.patch.object(os, 'unlink')
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_already_exists_and_not_gone(self, listen_mock,
spawn_mock,
unlink_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
listen_mock.side_effect = socket.error(errno.EADDRINUSE, '')
unlink_mock.side_effect = OSError(errno.EPERM, '')
self.assertRaises(OSError, eventlet_backdoor.initialize_if_enabled,
self.conf)
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_no_perms(self, listen_mock, spawn_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
listen_mock.side_effect = socket.error(errno.EPERM, '')
self.assertRaises(socket.error,
eventlet_backdoor.initialize_if_enabled,
self.conf)
class BackdoorPortTest(base.ServiceBaseTestCase): class BackdoorPortTest(base.ServiceBaseTestCase):
@mock.patch.object(eventlet, 'spawn') @mock.patch.object(eventlet, 'spawn')