Merge "Allow finer grained log levels"

This commit is contained in:
Zuul 2021-06-22 17:22:35 +00:00 committed by Gerrit Code Review
commit fa47d53dcb
4 changed files with 79 additions and 19 deletions

View File

@ -193,7 +193,8 @@ class PrivsepLogHandler(pylogging.Handler):
class _ClientChannel(comm.ClientChannel): class _ClientChannel(comm.ClientChannel):
"""Our protocol, layered on the basic primitives in comm.ClientChannel""" """Our protocol, layered on the basic primitives in comm.ClientChannel"""
def __init__(self, sock): def __init__(self, sock, context):
self.log = logging.getLogger(context.conf.logger_name)
super(_ClientChannel, self).__init__(sock) super(_ClientChannel, self).__init__(sock)
self.exchange_ping() self.exchange_ping()
@ -203,11 +204,12 @@ class _ClientChannel(comm.ClientChannel):
reply = self.send_recv((Message.PING.value,)) reply = self.send_recv((Message.PING.value,))
success = reply[0] == Message.PONG success = reply[0] == Message.PONG
except Exception as e: except Exception as e:
LOG.exception('Error while sending initial PING to privsep: %s', e) self.log.exception('Error while sending initial PING to privsep: '
'%s', e)
success = False success = False
if not success: if not success:
msg = _('Privsep daemon failed to start') msg = _('Privsep daemon failed to start')
LOG.critical(msg) self.log.critical(msg)
raise FailedToDropPrivileges(msg) raise FailedToDropPrivileges(msg)
def remote_call(self, name, args, kwargs): def remote_call(self, name, args, kwargs):
@ -231,11 +233,11 @@ class _ClientChannel(comm.ClientChannel):
message = {encodeutils.safe_decode(k): v message = {encodeutils.safe_decode(k): v
for k, v in msg[1].items()} for k, v in msg[1].items()}
record = pylogging.makeLogRecord(message) record = pylogging.makeLogRecord(message)
if LOG.isEnabledFor(record.levelno): if self.log.isEnabledFor(record.levelno):
LOG.logger.handle(record) self.log.logger.handle(record)
else: else:
LOG.warning('Ignoring unexpected OOB message from privileged ' self.log.warning('Ignoring unexpected OOB message from privileged '
'process: %r', msg) 'process: %r', msg)
def fdopen(fd, *args, **kwargs): def fdopen(fd, *args, **kwargs):
@ -330,7 +332,7 @@ class ForkingClientChannel(_ClientChannel):
# parent # parent
sock_b.close() sock_b.close()
super(ForkingClientChannel, self).__init__(sock_a) super(ForkingClientChannel, self).__init__(sock_a, context)
class RootwrapClientChannel(_ClientChannel): class RootwrapClientChannel(_ClientChannel):
@ -380,7 +382,7 @@ class RootwrapClientChannel(_ClientChannel):
raise raise
os.rmdir(tmpdir) os.rmdir(tmpdir)
super(RootwrapClientChannel, self).__init__(sock) super(RootwrapClientChannel, self).__init__(sock, context)
class Daemon(object): class Daemon(object):

View File

@ -65,6 +65,10 @@ OPTS = [
'recreate the current configuration. ' 'recreate the current configuration. '
'This command must accept suitable --privsep_context ' 'This command must accept suitable --privsep_context '
'and --privsep_sock_path arguments.')), 'and --privsep_sock_path arguments.')),
cfg.StrOpt('logger_name',
help=_('Logger name to use for this privsep context. By '
'default all contexts log with oslo_privsep.daemon.'),
default='oslo_privsep.daemon'),
] ]
_ENTRYPOINT_ATTR = 'privsep_entrypoint' _ENTRYPOINT_ATTR = 'privsep_entrypoint'
@ -124,7 +128,7 @@ def init(root_helper=None):
class PrivContext(object): class PrivContext(object):
def __init__(self, prefix, cfg_section='privsep', pypath=None, def __init__(self, prefix, cfg_section='privsep', pypath=None,
capabilities=None): capabilities=None, logger_name='oslo_privsep.daemon'):
# Note that capabilities=[] means retaining no capabilities # Note that capabilities=[] means retaining no capabilities
# and leaves even uid=0 with no powers except being able to # and leaves even uid=0 with no powers except being able to
@ -150,6 +154,8 @@ class PrivContext(object):
cfg.CONF.register_opts(OPTS, group=cfg_section) cfg.CONF.register_opts(OPTS, group=cfg_section)
cfg.CONF.set_default('capabilities', group=cfg_section, cfg.CONF.set_default('capabilities', group=cfg_section,
default=capabilities) default=capabilities)
cfg.CONF.set_default('logger_name', group=cfg_section,
default=logger_name)
@property @property
def conf(self): def conf(self):

View File

@ -45,6 +45,20 @@ class TestException(Exception):
pass pass
def get_fake_context(conf_attrs=None, **context_attrs):
conf_attrs = conf_attrs or {}
context = mock.NonCallableMock()
context.conf.user = 42
context.conf.group = 84
context.conf.thread_pool_size = 10
context.conf.capabilities = [
capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN]
context.conf.logger_name = 'oslo_privsep.daemon'
vars(context).update(context_attrs)
vars(context.conf).update(conf_attrs)
return context
@testctx.context.entrypoint @testctx.context.entrypoint
def logme(level, msg, exc_info=False): def logme(level, msg, exc_info=False):
# We want to make sure we log everything from the priv side for # We want to make sure we log everything from the priv side for
@ -152,12 +166,7 @@ class DaemonTest(base.BaseTestCase):
def test_drop_privs(self, mock_dropcaps, mock_keepcaps, def test_drop_privs(self, mock_dropcaps, mock_keepcaps,
mock_setgroups, mock_setgid, mock_setuid): mock_setgroups, mock_setgid, mock_setuid):
channel = mock.NonCallableMock() channel = mock.NonCallableMock()
context = mock.NonCallableMock() context = get_fake_context()
context.conf.user = 42
context.conf.group = 84
context.conf.thread_pool_size = 10
context.conf.capabilities = [
capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN]
d = daemon.Daemon(channel, context) d = daemon.Daemon(channel, context)
d._drop_privs() d._drop_privs()
@ -200,23 +209,51 @@ class ClientChannelTestCase(base.BaseTestCase):
def setUp(self): def setUp(self):
super(ClientChannelTestCase, self).setUp() super(ClientChannelTestCase, self).setUp()
context = get_fake_context()
with mock.patch.object(comm.ClientChannel, '__init__'), \ with mock.patch.object(comm.ClientChannel, '__init__'), \
mock.patch.object(daemon._ClientChannel, 'exchange_ping'): mock.patch.object(daemon._ClientChannel, 'exchange_ping'):
self.client_channel = daemon._ClientChannel(mock.ANY) self.client_channel = daemon._ClientChannel(mock.ANY, context)
def test_out_of_band_log_message(self): @mock.patch.object(daemon.LOG.logger, 'handle')
def test_out_of_band_log_message(self, handle_mock):
message = [daemon.Message.LOG, self.DICT] message = [daemon.Message.LOG, self.DICT]
self.assertEqual(self.client_channel.log, daemon.LOG)
with mock.patch.object(pylogging, 'makeLogRecord') as mock_make_log, \ with mock.patch.object(pylogging, 'makeLogRecord') as mock_make_log, \
mock.patch.object(daemon.LOG, 'isEnabledFor', mock.patch.object(daemon.LOG, 'isEnabledFor',
return_value=False): return_value=True) as mock_enabled:
self.client_channel.out_of_band(message) self.client_channel.out_of_band(message)
mock_make_log.assert_called_once_with(self.EXPECTED) mock_make_log.assert_called_once_with(self.EXPECTED)
handle_mock.assert_called_once_with(mock_make_log.return_value)
mock_enabled.assert_called_once_with(
mock_make_log.return_value.levelno)
def test_out_of_band_not_log_message(self): def test_out_of_band_not_log_message(self):
with mock.patch.object(daemon.LOG, 'warning') as mock_warning: with mock.patch.object(daemon.LOG, 'warning') as mock_warning:
self.client_channel.out_of_band([daemon.Message.PING]) self.client_channel.out_of_band([daemon.Message.PING])
mock_warning.assert_called_once() mock_warning.assert_called_once()
@mock.patch.object(daemon.logging, 'getLogger')
@mock.patch.object(pylogging, 'makeLogRecord')
def test_out_of_band_log_message_context_logger(self, make_log_mock,
get_logger_mock):
logger_name = 'os_brick.privileged'
context = get_fake_context(conf_attrs={'logger_name': logger_name})
with mock.patch.object(comm.ClientChannel, '__init__'), \
mock.patch.object(daemon._ClientChannel, 'exchange_ping'):
channel = daemon._ClientChannel(mock.ANY, context)
get_logger_mock.assert_called_once_with(logger_name)
self.assertEqual(get_logger_mock.return_value, channel.log)
message = [daemon.Message.LOG, self.DICT]
channel.out_of_band(message)
make_log_mock.assert_called_once_with(self.EXPECTED)
channel.log.isEnabledFor.assert_called_once_with(
make_log_mock.return_value.levelno)
channel.log.logger.handle.assert_called_once_with(
make_log_mock.return_value)
class UnMonkeyPatch(base.BaseTestCase): class UnMonkeyPatch(base.BaseTestCase):

View File

@ -0,0 +1,15 @@
---
features:
- |
``PrivContext`` accepts a new string parameter called ``logger_name`` to
define the logger we want to use for the daemon logs of this context.
By default all contexts use ``oslo_privsep.daemon``, but in some cases we
may need finer grained log levels, for example nova running in debug mode
could log its own privsep calls on INFO level regardless, but leave all
libraries' privsep calls, such as os-brick's, to be logged in the normal
DEBUG level.
See `bug 1922052`_.
.. _`bug 1922052`: https://bugs.launchpad.net/nova/+bug/1922052