Add ruff
Change-Id: I58ca56045ff3dbf9f3bb0a96ec99188e6b54a797 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
@@ -12,18 +12,14 @@ repos:
|
||||
- id: debug-statements
|
||||
- id: check-yaml
|
||||
files: .*\.(yaml|yml)$
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.13
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: ['--fix', '--unsafe-fixes']
|
||||
- id: ruff-format
|
||||
- repo: https://opendev.org/openstack/hacking
|
||||
rev: 7.0.0
|
||||
rev: 8.0.0
|
||||
hooks:
|
||||
- id: hacking
|
||||
additional_dependencies: []
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.8.6
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ['-c', 'pyproject.toml']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.20.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
|
||||
@@ -129,8 +129,7 @@ else:
|
||||
|
||||
def set_keepcaps(enable):
|
||||
"""Set/unset thread's "keep capabilities" flag - see prctl(2)"""
|
||||
ret = _prctl(crt.PR_SET_KEEPCAPS,
|
||||
ffi.cast('unsigned long', bool(enable)))
|
||||
ret = _prctl(crt.PR_SET_KEEPCAPS, ffi.cast('unsigned long', bool(enable)))
|
||||
if ret != 0:
|
||||
errno = ffi.errno
|
||||
raise OSError(errno, os.strerror(errno))
|
||||
@@ -142,15 +141,16 @@ def drop_all_caps_except(effective, permitted, inheritable):
|
||||
prm = _caps_to_mask(permitted)
|
||||
inh = _caps_to_mask(inheritable)
|
||||
|
||||
header = ffi.new('cap_user_header_t',
|
||||
{'version': crt._LINUX_CAPABILITY_VERSION_2,
|
||||
'pid': 0})
|
||||
header = ffi.new(
|
||||
'cap_user_header_t',
|
||||
{'version': crt._LINUX_CAPABILITY_VERSION_2, 'pid': 0},
|
||||
)
|
||||
data = ffi.new('struct __user_cap_data_struct[2]')
|
||||
data[0].effective = eff & 0xffffffff
|
||||
data[0].effective = eff & 0xFFFFFFFF
|
||||
data[1].effective = eff >> 32
|
||||
data[0].permitted = prm & 0xffffffff
|
||||
data[0].permitted = prm & 0xFFFFFFFF
|
||||
data[1].permitted = prm >> 32
|
||||
data[0].inheritable = inh & 0xffffffff
|
||||
data[0].inheritable = inh & 0xFFFFFFFF
|
||||
data[1].inheritable = inh >> 32
|
||||
|
||||
ret = _capset(header, data)
|
||||
@@ -174,9 +174,10 @@ def _caps_to_mask(caps):
|
||||
|
||||
def get_caps():
|
||||
"""Return (effective, permitted, inheritable) as lists of caps"""
|
||||
header = ffi.new('cap_user_header_t',
|
||||
{'version': crt._LINUX_CAPABILITY_VERSION_2,
|
||||
'pid': 0})
|
||||
header = ffi.new(
|
||||
'cap_user_header_t',
|
||||
{'version': crt._LINUX_CAPABILITY_VERSION_2, 'pid': 0},
|
||||
)
|
||||
data = ffi.new('struct __user_cap_data_struct[2]')
|
||||
ret = _capget(header, data)
|
||||
if ret != 0:
|
||||
@@ -184,10 +185,7 @@ def get_caps():
|
||||
raise OSError(errno, os.strerror(errno))
|
||||
|
||||
return (
|
||||
_mask_to_caps(data[0].effective |
|
||||
(data[1].effective << 32)),
|
||||
_mask_to_caps(data[0].permitted |
|
||||
(data[1].permitted << 32)),
|
||||
_mask_to_caps(data[0].inheritable |
|
||||
(data[1].inheritable << 32)),
|
||||
_mask_to_caps(data[0].effective | (data[1].effective << 32)),
|
||||
_mask_to_caps(data[0].permitted | (data[1].permitted << 32)),
|
||||
_mask_to_caps(data[0].inheritable | (data[1].inheritable << 32)),
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ LOG = logging.getLogger(__name__)
|
||||
@enum.unique
|
||||
class Message(enum.IntEnum):
|
||||
"""Types of messages sent across the communication channel"""
|
||||
|
||||
PING = 1
|
||||
PONG = 2
|
||||
CALL = 3
|
||||
@@ -55,8 +56,9 @@ class Serializer:
|
||||
self.writesock = writesock
|
||||
|
||||
def send(self, msg):
|
||||
buf = msgpack.packb(msg, use_bin_type=True,
|
||||
unicode_errors='surrogateescape')
|
||||
buf = msgpack.packb(
|
||||
msg, use_bin_type=True, unicode_errors='surrogateescape'
|
||||
)
|
||||
self.writesock.sendall(buf)
|
||||
|
||||
def close(self):
|
||||
@@ -120,13 +122,17 @@ class Future:
|
||||
before = datetime.datetime.now()
|
||||
if not self.condvar.wait(timeout=self.timeout):
|
||||
now = datetime.datetime.now()
|
||||
LOG.warning('Timeout while executing a command, timeout: %s, '
|
||||
'time elapsed: %s', self.timeout,
|
||||
(now - before).total_seconds())
|
||||
return (Message.ERR.value,
|
||||
'{}.{}'.format(PrivsepTimeout.__module__,
|
||||
PrivsepTimeout.__name__),
|
||||
'')
|
||||
LOG.warning(
|
||||
'Timeout while executing a command, timeout: %s, '
|
||||
'time elapsed: %s',
|
||||
self.timeout,
|
||||
(now - before).total_seconds(),
|
||||
)
|
||||
return (
|
||||
Message.ERR.value,
|
||||
f'{PrivsepTimeout.__module__}.{PrivsepTimeout.__name__}',
|
||||
'',
|
||||
)
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
return self.data
|
||||
@@ -158,8 +164,10 @@ class ClientChannel:
|
||||
else:
|
||||
with self.lock:
|
||||
if msgid not in self.outstanding_msgs:
|
||||
LOG.warning("msgid should be in oustanding_msgs, it is"
|
||||
"possible that timeout is reached!")
|
||||
LOG.warning(
|
||||
"msgid should be in oustanding_msgs, it is"
|
||||
"possible that timeout is reached!"
|
||||
)
|
||||
continue
|
||||
self.outstanding_msgs[msgid].set_result(data)
|
||||
|
||||
@@ -169,7 +177,7 @@ class ClientChannel:
|
||||
# get an immediate similar error.
|
||||
LOG.debug('EOF on privsep read channel')
|
||||
|
||||
exc = IOError(_('Premature eof waiting for privileged process'))
|
||||
exc = OSError(_('Premature eof waiting for privileged process'))
|
||||
with self.lock:
|
||||
for mbox in self.outstanding_msgs.values():
|
||||
mbox.set_exception(exc)
|
||||
@@ -193,7 +201,7 @@ class ClientChannel:
|
||||
|
||||
reply = future.result()
|
||||
except Exception:
|
||||
LOG.warning(f"Unexpected error: {sys.exc_info()[0]}")
|
||||
LOG.warning("Unexpected error: %s", sys.exc_info()[0])
|
||||
raise
|
||||
finally:
|
||||
del self.outstanding_msgs[myid]
|
||||
|
||||
@@ -76,8 +76,16 @@ if platform.system() == 'Linux':
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EVENTLET_MODULES = ('os', 'select', 'socket', 'thread', 'time', 'MySQLdb',
|
||||
'builtins', 'subprocess')
|
||||
EVENTLET_MODULES = (
|
||||
'os',
|
||||
'select',
|
||||
'socket',
|
||||
'thread',
|
||||
'time',
|
||||
'MySQLdb',
|
||||
'builtins',
|
||||
'subprocess',
|
||||
)
|
||||
EVENTLET_LIBRARIES = []
|
||||
|
||||
|
||||
@@ -89,17 +97,18 @@ _MONKEY_PATCHED = False
|
||||
for module in EVENTLET_MODULES:
|
||||
if eventlet.patcher.is_monkey_patched(module):
|
||||
_MONKEY_PATCHED = True
|
||||
if hasattr(patcher, '_green_%s_modules' % module):
|
||||
method = getattr(patcher, '_green_%s_modules' % module)
|
||||
elif hasattr(patcher, '_green_%s' % module):
|
||||
method = getattr(patcher, '_green_%s' % module)
|
||||
if hasattr(patcher, f'_green_{module}_modules'):
|
||||
method = getattr(patcher, f'_green_{module}_modules')
|
||||
elif hasattr(patcher, f'_green_{module}'):
|
||||
method = getattr(patcher, f'_green_{module}')
|
||||
else:
|
||||
method = _null()
|
||||
EVENTLET_LIBRARIES.append((module, method))
|
||||
|
||||
if _MONKEY_PATCHED:
|
||||
debtcollector.deprecate(
|
||||
"Eventlet support is deprecated and will be removed")
|
||||
"Eventlet support is deprecated and will be removed"
|
||||
)
|
||||
|
||||
|
||||
@enum.unique
|
||||
@@ -201,8 +210,9 @@ class _ClientChannel(comm.ClientChannel):
|
||||
reply = self.send_recv((comm.Message.PING.value,))
|
||||
success = reply[0] == comm.Message.PONG
|
||||
except Exception as e:
|
||||
self.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
|
||||
if not success:
|
||||
msg = _('Privsep daemon failed to start')
|
||||
@@ -210,8 +220,9 @@ class _ClientChannel(comm.ClientChannel):
|
||||
raise FailedToDropPrivileges(msg)
|
||||
|
||||
def remote_call(self, name, args, kwargs, timeout):
|
||||
result = self.send_recv((comm.Message.CALL.value, name, args, kwargs),
|
||||
timeout)
|
||||
result = self.send_recv(
|
||||
(comm.Message.CALL.value, name, args, kwargs), timeout
|
||||
)
|
||||
if result[0] == comm.Message.RET:
|
||||
# (RET, return value)
|
||||
return result[1]
|
||||
@@ -234,14 +245,17 @@ class _ClientChannel(comm.ClientChannel):
|
||||
def out_of_band(self, msg):
|
||||
if msg[0] == comm.Message.LOG:
|
||||
# (LOG, LogRecord __dict__)
|
||||
message = {encodeutils.safe_decode(k): v
|
||||
for k, v in msg[1].items()}
|
||||
message = {
|
||||
encodeutils.safe_decode(k): v for k, v in msg[1].items()
|
||||
}
|
||||
record = pylogging.makeLogRecord(message)
|
||||
if self.log.isEnabledFor(record.levelno):
|
||||
self.log.logger.handle(record)
|
||||
else:
|
||||
self.log.warning('Ignoring unexpected OOB message from privileged '
|
||||
'process: %r', msg)
|
||||
self.log.warning(
|
||||
'Ignoring unexpected OOB message from privileged process: %r',
|
||||
msg,
|
||||
)
|
||||
|
||||
|
||||
def fdopen(fd, *args, **kwargs):
|
||||
@@ -266,10 +280,8 @@ def _fd_logger(level=logging.WARN):
|
||||
def logger(f):
|
||||
for line in f:
|
||||
LOG.log(level, 'privsep log: %s', line.rstrip())
|
||||
t = threading.Thread(
|
||||
name='fd_logger',
|
||||
target=logger, args=(read_end,)
|
||||
)
|
||||
|
||||
t = threading.Thread(name='fd_logger', target=logger, args=(read_end,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
@@ -326,8 +338,9 @@ class ForkingClientChannel(_ClientChannel):
|
||||
sock_a.close()
|
||||
|
||||
# Replace root logger early (to capture any errors during setup)
|
||||
replace_logging(PrivsepLogHandler(channel,
|
||||
processName=str(context)))
|
||||
replace_logging(
|
||||
PrivsepLogHandler(channel, processName=str(context))
|
||||
)
|
||||
|
||||
Daemon(channel, context=context).run()
|
||||
LOG.debug('privsep daemon exiting')
|
||||
@@ -365,10 +378,14 @@ class RootwrapClientChannel(_ClientChannel):
|
||||
|
||||
cmd = context.helper_command(sockpath)
|
||||
LOG.info('Running privsep helper: %s', cmd)
|
||||
proc = subprocess.Popen(cmd, shell=False, stderr=_fd_logger())
|
||||
proc = subprocess.Popen( # noqa: S603
|
||||
cmd, shell=False, stderr=_fd_logger()
|
||||
)
|
||||
if proc.wait() != 0:
|
||||
msg = ('privsep helper command exited non-zero (%s)' %
|
||||
proc.returncode)
|
||||
msg = (
|
||||
f'privsep helper command exited non-zero '
|
||||
f'({proc.returncode})'
|
||||
)
|
||||
LOG.critical(msg)
|
||||
raise FailedToDropPrivileges(msg)
|
||||
LOG.info('Spawned new privsep daemon via rootwrap')
|
||||
@@ -399,7 +416,8 @@ class Daemon:
|
||||
self.group = context.conf.group
|
||||
self.caps = set(context.conf.capabilities)
|
||||
self.thread_pool = futures.ThreadPoolExecutor(
|
||||
context.conf.thread_pool_size)
|
||||
context.conf.thread_pool_size
|
||||
)
|
||||
self.communication_error = None
|
||||
|
||||
def run(self):
|
||||
@@ -437,16 +455,17 @@ class Daemon:
|
||||
finally:
|
||||
capabilities.set_keepcaps(False)
|
||||
|
||||
LOG.info('privsep process running with uid/gid: %(uid)s/%(gid)s',
|
||||
{'uid': os.getuid(), 'gid': os.getgid()})
|
||||
LOG.info(
|
||||
'privsep process running with uid/gid: %(uid)s/%(gid)s',
|
||||
{'uid': os.getuid(), 'gid': os.getgid()},
|
||||
)
|
||||
|
||||
capabilities.drop_all_caps_except(self.caps, self.caps, [])
|
||||
|
||||
def fmt_caps(capset):
|
||||
if not capset:
|
||||
return 'none'
|
||||
fc = [capabilities.CAPS_BYVALUE.get(c, str(c))
|
||||
for c in capset]
|
||||
fc = [capabilities.CAPS_BYVALUE.get(c, str(c)) for c in capset]
|
||||
fc.sort()
|
||||
return '|'.join(fc)
|
||||
|
||||
@@ -458,7 +477,8 @@ class Daemon:
|
||||
'eff': fmt_caps(eff),
|
||||
'prm': fmt_caps(prm),
|
||||
'inh': fmt_caps(inh),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
def _process_cmd(self, msgid, cmd, *args):
|
||||
"""Executes the requested command in an execution thread.
|
||||
@@ -490,12 +510,18 @@ class Daemon:
|
||||
return (comm.Message.RET.value, ret)
|
||||
except Exception as e:
|
||||
LOG.debug(
|
||||
'privsep: Exception during request[%(msgid)s]: '
|
||||
'%(err)s', {'msgid': msgid, 'err': e}, exc_info=True)
|
||||
'privsep: Exception during request[%(msgid)s]: %(err)s',
|
||||
{'msgid': msgid, 'err': e},
|
||||
exc_info=True,
|
||||
)
|
||||
cls = e.__class__
|
||||
cls_name = f'{cls.__module__}.{cls.__name__}'
|
||||
return (comm.Message.ERR.value, cls_name, e.args,
|
||||
traceback.format_exc())
|
||||
return (
|
||||
comm.Message.ERR.value,
|
||||
cls_name,
|
||||
e.args,
|
||||
traceback.format_exc(),
|
||||
)
|
||||
|
||||
def _create_done_callback(self, msgid):
|
||||
"""Creates a future callback to receive command execution results.
|
||||
@@ -512,19 +538,27 @@ class Daemon:
|
||||
"""
|
||||
try:
|
||||
reply = result.result()
|
||||
LOG.debug('privsep: reply[%(msgid)s]: %(reply)s',
|
||||
{'msgid': msgid, 'reply': reply})
|
||||
LOG.debug(
|
||||
'privsep: reply[%(msgid)s]: %(reply)s',
|
||||
{'msgid': msgid, 'reply': reply},
|
||||
)
|
||||
channel.send((msgid, reply))
|
||||
except OSError:
|
||||
self.communication_error = sys.exc_info()
|
||||
except Exception as e:
|
||||
LOG.debug(
|
||||
'privsep: Exception during request[%(msgid)s]: '
|
||||
'%(err)s', {'msgid': msgid, 'err': e}, exc_info=True)
|
||||
'privsep: Exception during request[%(msgid)s]: %(err)s',
|
||||
{'msgid': msgid, 'err': e},
|
||||
exc_info=True,
|
||||
)
|
||||
cls = e.__class__
|
||||
cls_name = f'{cls.__module__}.{cls.__name__}'
|
||||
reply = (comm.Message.ERR.value, cls_name, e.args,
|
||||
traceback.format_exc())
|
||||
reply = (
|
||||
comm.Message.ERR.value,
|
||||
cls_name,
|
||||
e.args,
|
||||
traceback.format_exc(),
|
||||
)
|
||||
try:
|
||||
channel.send((msgid, reply))
|
||||
except OSError as exc:
|
||||
@@ -558,10 +592,12 @@ class Daemon:
|
||||
def helper_main():
|
||||
"""Start privileged process, serving requests over a Unix socket."""
|
||||
|
||||
cfg.CONF.register_cli_opts([
|
||||
cfg.StrOpt('privsep_context', required=True),
|
||||
cfg.StrOpt('privsep_sock_path', required=True),
|
||||
])
|
||||
cfg.CONF.register_cli_opts(
|
||||
[
|
||||
cfg.StrOpt('privsep_context', required=True),
|
||||
cfg.StrOpt('privsep_sock_path', required=True),
|
||||
]
|
||||
)
|
||||
|
||||
logging.register_options(cfg.CONF)
|
||||
|
||||
@@ -570,10 +606,13 @@ def helper_main():
|
||||
logging.setup(cfg.CONF, 'privsep', fix_eventlet=False)
|
||||
|
||||
context = importutils.import_class(cfg.CONF.privsep_context)
|
||||
from oslo_privsep import priv_context # Avoid circular import
|
||||
from oslo_privsep import priv_context # Avoid circular import
|
||||
|
||||
if not isinstance(context, priv_context.PrivContext):
|
||||
LOG.fatal('--privsep_context must be the (python) name of a '
|
||||
'PrivContext object')
|
||||
LOG.fatal(
|
||||
'--privsep_context must be the (python) name of a '
|
||||
'PrivContext object'
|
||||
)
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
sock.connect(cfg.CONF.privsep_sock_path)
|
||||
|
||||
@@ -36,14 +36,14 @@ test_context_with_timeout = priv_context.PrivContext(
|
||||
cfg_section='privsep',
|
||||
pypath=__name__ + '.test_context_with_timeout',
|
||||
capabilities=[],
|
||||
timeout=0.03
|
||||
timeout=0.03,
|
||||
)
|
||||
|
||||
|
||||
@test_context.entrypoint
|
||||
def sleep():
|
||||
# We don't want the daemon to be able to handle these calls too fast.
|
||||
time.sleep(.001)
|
||||
time.sleep(0.001)
|
||||
|
||||
|
||||
@test_context.entrypoint_with_timeout(0.03)
|
||||
@@ -65,7 +65,7 @@ def one():
|
||||
|
||||
@test_context.entrypoint
|
||||
def logs():
|
||||
logging.warning('foo')
|
||||
logging.warning('foo') # noqa: LOG015
|
||||
|
||||
|
||||
class TestDaemon(base.BaseTestCase):
|
||||
@@ -75,7 +75,8 @@ class TestDaemon(base.BaseTestCase):
|
||||
self.cfg_fixture = self.useFixture(config_fixture.Config())
|
||||
self.cfg_fixture.config(
|
||||
group='privsep',
|
||||
helper_command='sudo -E %s/bin/privsep-helper' % venv_path)
|
||||
helper_command=f'sudo -E {venv_path}/bin/privsep-helper',
|
||||
)
|
||||
priv_context.init()
|
||||
|
||||
def test_concurrency(self):
|
||||
|
||||
@@ -41,37 +41,55 @@ def CapNameOrInt(value):
|
||||
|
||||
|
||||
OPTS = [
|
||||
cfg.StrOpt('user',
|
||||
help=_('User that the privsep daemon should run as.')),
|
||||
cfg.StrOpt('group',
|
||||
help=_('Group that the privsep daemon should run as.')),
|
||||
cfg.Opt('capabilities',
|
||||
type=types.List(CapNameOrInt), default=[],
|
||||
help=_('List of Linux capabilities retained by the privsep '
|
||||
'daemon.')),
|
||||
cfg.IntOpt('thread_pool_size',
|
||||
min=1,
|
||||
help=_("The number of threads available for privsep to "
|
||||
"concurrently run processes. Defaults to the number of "
|
||||
"CPU cores in the system."),
|
||||
default=multiprocessing.cpu_count(),
|
||||
sample_default='multiprocessing.cpu_count()'),
|
||||
cfg.StrOpt('helper_command',
|
||||
help=_('Command to invoke to start the privsep daemon if '
|
||||
'not using the "fork" method. '
|
||||
'If not specified, a default is generated using '
|
||||
'"sudo privsep-helper" and arguments designed to '
|
||||
'recreate the current configuration. '
|
||||
'This command must accept suitable --privsep_context '
|
||||
'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'),
|
||||
cfg.BoolOpt('log_daemon_traceback',
|
||||
help=_('Print the exception traceback happened in the daemon '
|
||||
'in the client logger'),
|
||||
default=False),
|
||||
cfg.StrOpt('user', help=_('User that the privsep daemon should run as.')),
|
||||
cfg.StrOpt(
|
||||
'group', help=_('Group that the privsep daemon should run as.')
|
||||
),
|
||||
cfg.Opt(
|
||||
'capabilities',
|
||||
type=types.List(CapNameOrInt),
|
||||
default=[],
|
||||
help=_('List of Linux capabilities retained by the privsep daemon.'),
|
||||
),
|
||||
cfg.IntOpt(
|
||||
'thread_pool_size',
|
||||
min=1,
|
||||
help=_(
|
||||
"The number of threads available for privsep to "
|
||||
"concurrently run processes. Defaults to the number of "
|
||||
"CPU cores in the system."
|
||||
),
|
||||
default=multiprocessing.cpu_count(),
|
||||
sample_default='multiprocessing.cpu_count()',
|
||||
),
|
||||
cfg.StrOpt(
|
||||
'helper_command',
|
||||
help=_(
|
||||
'Command to invoke to start the privsep daemon if '
|
||||
'not using the "fork" method. '
|
||||
'If not specified, a default is generated using '
|
||||
'"sudo privsep-helper" and arguments designed to '
|
||||
'recreate the current configuration. '
|
||||
'This command must accept suitable --privsep_context '
|
||||
'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',
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
'log_daemon_traceback',
|
||||
help=_(
|
||||
'Print the exception traceback happened in the daemon '
|
||||
'in the client logger'
|
||||
),
|
||||
default=False,
|
||||
),
|
||||
]
|
||||
|
||||
_ENTRYPOINT_ATTR = 'privsep_entrypoint'
|
||||
@@ -95,13 +113,14 @@ def _list_opts():
|
||||
:returns: a list of (group_name, opts) tuples
|
||||
"""
|
||||
# This is the default group name, but that can be overridden by the caller
|
||||
group = cfg.OptGroup('privsep',
|
||||
title='oslo.privsep options',
|
||||
help='Configuration options for the oslo.privsep '
|
||||
'daemon. Note that this group name can be '
|
||||
'changed by the consuming service. Check the '
|
||||
'service\'s docs to see if this is the case.'
|
||||
)
|
||||
group = cfg.OptGroup(
|
||||
'privsep',
|
||||
title='oslo.privsep options',
|
||||
help='Configuration options for the oslo.privsep '
|
||||
'daemon. Note that this group name can be '
|
||||
'changed by the consuming service. Check the '
|
||||
'service\'s docs to see if this is the case.',
|
||||
)
|
||||
return [(group, copy.deepcopy(OPTS))]
|
||||
|
||||
|
||||
@@ -130,10 +149,15 @@ def init(root_helper=None):
|
||||
|
||||
|
||||
class PrivContext:
|
||||
def __init__(self, prefix, cfg_section='privsep', pypath=None,
|
||||
capabilities=None, logger_name='oslo_privsep.daemon',
|
||||
timeout=None):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix,
|
||||
cfg_section='privsep',
|
||||
pypath=None,
|
||||
capabilities=None,
|
||||
logger_name='oslo_privsep.daemon',
|
||||
timeout=None,
|
||||
):
|
||||
# Note that capabilities=[] means retaining no capabilities
|
||||
# and leaves even uid=0 with no powers except being able to
|
||||
# read/write to the filesystem as uid=0. This might be what
|
||||
@@ -153,10 +177,12 @@ class PrivContext:
|
||||
self.start_lock = threading.Lock()
|
||||
|
||||
cfg.CONF.register_opts(OPTS, group=cfg_section)
|
||||
cfg.CONF.set_default('capabilities', group=cfg_section,
|
||||
default=capabilities)
|
||||
cfg.CONF.set_default('logger_name', group=cfg_section,
|
||||
default=logger_name)
|
||||
cfg.CONF.set_default(
|
||||
'capabilities', group=cfg_section, default=capabilities
|
||||
)
|
||||
cfg.CONF.set_default(
|
||||
'logger_name', group=cfg_section, default=logger_name
|
||||
)
|
||||
self.timeout = timeout
|
||||
|
||||
@property
|
||||
@@ -167,7 +193,7 @@ class PrivContext:
|
||||
return cfg.CONF[self.cfg_section]
|
||||
|
||||
def __repr__(self):
|
||||
return 'PrivContext(cfg_section=%s)' % self.cfg_section
|
||||
return f'PrivContext(cfg_section={self.cfg_section})'
|
||||
|
||||
def helper_command(self, sockpath):
|
||||
# We need to be able to reconstruct the context object in the new
|
||||
@@ -180,11 +206,14 @@ class PrivContext:
|
||||
# These asserts here are just attempts to catch errors earlier.
|
||||
# TODO(gus): Consider replacing with setuptools entry_points.
|
||||
if self.pypath is None:
|
||||
raise AssertionError('helper_command requires priv_context '
|
||||
'pypath to be specified')
|
||||
raise AssertionError(
|
||||
'helper_command requires priv_context pypath to be specified'
|
||||
)
|
||||
if importutils.import_class(self.pypath) is not self:
|
||||
raise AssertionError('helper_command requires priv_context '
|
||||
'pypath for context object')
|
||||
raise AssertionError(
|
||||
'helper_command requires priv_context '
|
||||
'pypath for context object'
|
||||
)
|
||||
|
||||
# Note order is important here. Deployments will (hopefully)
|
||||
# have the exact arguments in sudoers/rootwrap configs and
|
||||
@@ -209,8 +238,8 @@ class PrivContext:
|
||||
pass
|
||||
|
||||
cmd.extend(
|
||||
['--privsep_context', self.pypath,
|
||||
'--privsep_sock_path', sockpath])
|
||||
['--privsep_context', self.pypath, '--privsep_sock_path', sockpath]
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -225,19 +254,21 @@ class PrivContext:
|
||||
"""This is intended to be used as a decorator with timeout."""
|
||||
|
||||
def wrap(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
f = self._entrypoint(func)
|
||||
return f(*args, _wrap_timeout=timeout, **kwargs)
|
||||
|
||||
setattr(inner, _ENTRYPOINT_ATTR, self)
|
||||
return inner
|
||||
|
||||
return wrap
|
||||
|
||||
def _entrypoint(self, func):
|
||||
if not func.__module__.startswith(self.prefix):
|
||||
raise AssertionError('%r entrypoints must be below "%s"' %
|
||||
(self, self.prefix))
|
||||
raise AssertionError(
|
||||
f'{self!r} entrypoints must be below "{self.prefix}"'
|
||||
)
|
||||
|
||||
# Right now, we only track a single context in
|
||||
# _ENTRYPOINT_ATTR. This could easily be expanded into a set,
|
||||
@@ -245,8 +276,9 @@ class PrivContext:
|
||||
# someone has a need to associate the same entrypoint with
|
||||
# multiple contexts.
|
||||
if getattr(func, _ENTRYPOINT_ATTR, None) is not None:
|
||||
raise AssertionError('%r is already associated with another '
|
||||
'PrivContext' % func)
|
||||
raise AssertionError(
|
||||
f'{func!r} is already associated with another PrivContext'
|
||||
)
|
||||
|
||||
f = functools.partial(self._wrap, func)
|
||||
setattr(f, _ENTRYPOINT_ATTR, self)
|
||||
@@ -264,8 +296,7 @@ class PrivContext:
|
||||
if self.channel is None:
|
||||
self.start()
|
||||
r_call_timeout = _wrap_timeout or self.timeout
|
||||
return self.channel.remote_call(name, args, kwargs,
|
||||
r_call_timeout)
|
||||
return self.channel.remote_call(name, args, kwargs, r_call_timeout)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@@ -280,7 +311,7 @@ class PrivContext:
|
||||
elif method is Method.FORK:
|
||||
channel = daemon.ForkingClientChannel(context=self)
|
||||
else:
|
||||
raise ValueError('Unknown method: %s' % method)
|
||||
raise ValueError(f'Unknown method: {method}')
|
||||
|
||||
self.channel = channel
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import fixtures
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import fixtures
|
||||
from oslo_config import fixture as cfg_fixture
|
||||
|
||||
from oslo_privsep import priv_context
|
||||
@@ -34,11 +33,11 @@ class UnprivilegedPrivsepFixture(fixtures.Fixture):
|
||||
super().setUp()
|
||||
|
||||
self.conf = self.useFixture(cfg_fixture.Config()).conf
|
||||
self.conf.set_override('capabilities', [],
|
||||
group=self.context.cfg_section)
|
||||
self.conf.set_override(
|
||||
'capabilities', [], group=self.context.cfg_section
|
||||
)
|
||||
for k in ('user', 'group'):
|
||||
self.conf.set_override(
|
||||
k, None, group=self.context.cfg_section)
|
||||
self.conf.set_override(k, None, group=self.context.cfg_section)
|
||||
for k, v in self.config_override.items():
|
||||
self.conf.set_override(k, v, group='privsep')
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ from oslo_privsep import capabilities
|
||||
|
||||
|
||||
class TestCapabilities(base.BaseTestCase):
|
||||
|
||||
@mock.patch('oslo_privsep.capabilities._prctl')
|
||||
def test_set_keepcaps_error(self, mock_prctl):
|
||||
mock_prctl.return_value = -1
|
||||
@@ -36,13 +35,15 @@ class TestCapabilities(base.BaseTestCase):
|
||||
self.assertEqual(1, mock_prctl.call_count)
|
||||
self.assertCountEqual(
|
||||
[8, 1], # [PR_SET_KEEPCAPS, true]
|
||||
[int(x) for x in mock_prctl.call_args[0]])
|
||||
[int(x) for x in mock_prctl.call_args[0]],
|
||||
)
|
||||
|
||||
@mock.patch('oslo_privsep.capabilities._capset')
|
||||
def test_drop_all_caps_except_error(self, mock_capset):
|
||||
mock_capset.return_value = -1
|
||||
self.assertRaises(
|
||||
OSError, capabilities.drop_all_caps_except, [0], [0], [0])
|
||||
OSError, capabilities.drop_all_caps_except, [0], [0], [0]
|
||||
)
|
||||
|
||||
@mock.patch('oslo_privsep.capabilities._capset')
|
||||
def test_drop_all_caps_except(self, mock_capset):
|
||||
@@ -50,12 +51,15 @@ class TestCapabilities(base.BaseTestCase):
|
||||
|
||||
# Somewhat arbitrary bit patterns to exercise _caps_to_mask
|
||||
capabilities.drop_all_caps_except(
|
||||
(17, 24, 49), (8, 10, 35, 56), (24, 31, 40))
|
||||
(17, 24, 49), (8, 10, 35, 56), (24, 31, 40)
|
||||
)
|
||||
|
||||
self.assertEqual(1, mock_capset.call_count)
|
||||
hdr, data = mock_capset.call_args[0]
|
||||
self.assertEqual(0x20071026, # _LINUX_CAPABILITY_VERSION_2
|
||||
hdr.version)
|
||||
self.assertEqual(
|
||||
0x20071026, # _LINUX_CAPABILITY_VERSION_2
|
||||
hdr.version,
|
||||
)
|
||||
self.assertEqual(0x01020000, data[0].effective)
|
||||
self.assertEqual(0x00020000, data[1].effective)
|
||||
self.assertEqual(0x00000500, data[0].permitted)
|
||||
@@ -79,10 +83,10 @@ class TestCapabilities(base.BaseTestCase):
|
||||
data[0].inheritable = 0x81000000
|
||||
data[1].inheritable = 0x00000100
|
||||
return 0
|
||||
|
||||
mock_capget.side_effect = impl
|
||||
|
||||
self.assertCountEqual(
|
||||
([17, 24, 49],
|
||||
[8, 10, 35, 56],
|
||||
[24, 31, 40]),
|
||||
capabilities.get_caps())
|
||||
([17, 24, 49], [8, 10, 35, 56], [24, 31, 40]),
|
||||
capabilities.get_caps(),
|
||||
)
|
||||
|
||||
@@ -51,7 +51,9 @@ def get_fake_context(conf_attrs=None, **context_attrs):
|
||||
context.conf.group = 84
|
||||
context.conf.thread_pool_size = 10
|
||||
context.conf.capabilities = [
|
||||
capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN]
|
||||
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)
|
||||
@@ -88,12 +90,12 @@ class LogRecorder(pylogging.Formatter):
|
||||
return super().format(record)
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class LogTest(testctx.TestContextTestCase):
|
||||
def test_priv_loglevel(self):
|
||||
logger = self.useFixture(fixtures.FakeLogger(
|
||||
level=logging.INFO))
|
||||
logger = self.useFixture(fixtures.FakeLogger(level=logging.INFO))
|
||||
|
||||
# These write to the log on the priv side
|
||||
logme(logging.DEBUG, 'test@DEBUG')
|
||||
@@ -109,15 +111,19 @@ class LogTest(testctx.TestContextTestCase):
|
||||
def test_record_data(self):
|
||||
logs = []
|
||||
|
||||
self.useFixture(fixtures.FakeLogger(
|
||||
level=logging.INFO, format='dummy',
|
||||
# fixtures.FakeLogger accepts only a formatter
|
||||
# class/function, not an instance :(
|
||||
formatter=functools.partial(LogRecorder, logs)))
|
||||
self.useFixture(
|
||||
fixtures.FakeLogger(
|
||||
level=logging.INFO,
|
||||
format='dummy',
|
||||
# fixtures.FakeLogger accepts only a formatter
|
||||
# class/function, not an instance :(
|
||||
formatter=functools.partial(LogRecorder, logs),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
logme(logging.WARN, 'test with exc', exc_info=True)
|
||||
except Exception:
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
time.sleep(0.1) # Hack to give logging thread a chance to run
|
||||
@@ -128,8 +134,9 @@ class LogTest(testctx.TestContextTestCase):
|
||||
self.assertIn('test with exc', record.getMessage())
|
||||
self.assertIsNone(record.exc_info)
|
||||
self.assertIn('TestException: with arg', record.exc_text)
|
||||
self.assertEqual('PrivContext(cfg_section=privsep)',
|
||||
record.processName)
|
||||
self.assertEqual(
|
||||
'PrivContext(cfg_section=privsep)', record.processName
|
||||
)
|
||||
self.assertIn('test_daemon.py', record.exc_text)
|
||||
self.assertEqual(logging.WARN, record.levelno)
|
||||
self.assertEqual('logme', record.funcName)
|
||||
@@ -137,11 +144,15 @@ class LogTest(testctx.TestContextTestCase):
|
||||
def test_format_record(self):
|
||||
logs = []
|
||||
|
||||
self.useFixture(fixtures.FakeLogger(
|
||||
level=logging.INFO, format='dummy',
|
||||
# fixtures.FakeLogger accepts only a formatter
|
||||
# class/function, not an instance :(
|
||||
formatter=functools.partial(LogRecorder, logs)))
|
||||
self.useFixture(
|
||||
fixtures.FakeLogger(
|
||||
level=logging.INFO,
|
||||
format='dummy',
|
||||
# fixtures.FakeLogger accepts only a formatter
|
||||
# class/function, not an instance :(
|
||||
formatter=functools.partial(LogRecorder, logs),
|
||||
)
|
||||
)
|
||||
|
||||
logme(logging.WARN, 'test with exc', exc_info=True)
|
||||
|
||||
@@ -152,13 +163,15 @@ class LogTest(testctx.TestContextTestCase):
|
||||
record = logs[0]
|
||||
# Verify the log record can be formatted by ContextFormatter
|
||||
fake_config = mock.Mock(
|
||||
logging_default_format_string="NOCTXT: %(message)s")
|
||||
logging_default_format_string="NOCTXT: %(message)s"
|
||||
)
|
||||
formatter = formatters.ContextFormatter(config=fake_config)
|
||||
formatter.format(record)
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class LogTestDaemonTraceback(testctx.TestContextTestCase):
|
||||
def setUp(self):
|
||||
self.config_override = {'log_daemon_traceback': True}
|
||||
@@ -166,13 +179,18 @@ class LogTestDaemonTraceback(testctx.TestContextTestCase):
|
||||
|
||||
def test_record_daemon_traceback(self):
|
||||
self.privsep_conf.set_override(
|
||||
'log_daemon_traceback', True, group='privsep')
|
||||
'log_daemon_traceback', True, group='privsep'
|
||||
)
|
||||
logs = []
|
||||
self.useFixture(fixtures.FakeLogger(
|
||||
level=logging.INFO, format='dummy',
|
||||
# fixtures.FakeLogger accepts only a formatter
|
||||
# class/function, not an instance :(
|
||||
formatter=functools.partial(LogRecorder, logs)))
|
||||
self.useFixture(
|
||||
fixtures.FakeLogger(
|
||||
level=logging.INFO,
|
||||
format='dummy',
|
||||
# fixtures.FakeLogger accepts only a formatter
|
||||
# class/function, not an instance :(
|
||||
formatter=functools.partial(LogRecorder, logs),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertRaises(RuntimeError, raise_runtimeerror)
|
||||
time.sleep(0.1) # Hack to give logging thread a chance to run
|
||||
@@ -185,17 +203,23 @@ class LogTestDaemonTraceback(testctx.TestContextTestCase):
|
||||
self.assertEqual(logging.WARN, record.levelno)
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class DaemonTest(base.BaseTestCase):
|
||||
|
||||
@mock.patch('os.setuid')
|
||||
@mock.patch('os.setgid')
|
||||
@mock.patch('os.setgroups')
|
||||
@mock.patch('oslo_privsep.capabilities.set_keepcaps')
|
||||
@mock.patch('oslo_privsep.capabilities.drop_all_caps_except')
|
||||
def test_drop_privs(self, mock_dropcaps, mock_keepcaps,
|
||||
mock_setgroups, mock_setgid, mock_setuid):
|
||||
def test_drop_privs(
|
||||
self,
|
||||
mock_dropcaps,
|
||||
mock_keepcaps,
|
||||
mock_setgroups,
|
||||
mock_setgid,
|
||||
mock_setuid,
|
||||
):
|
||||
channel = mock.NonCallableMock()
|
||||
context = get_fake_context()
|
||||
|
||||
@@ -211,30 +235,33 @@ class DaemonTest(base.BaseTestCase):
|
||||
mock_setgid.assert_called_once_with(84)
|
||||
mock_setgroups.assert_called_once_with([])
|
||||
|
||||
assert manager.mock_calls == expected_calls
|
||||
self.assertEqual(expected_calls, manager.mock_calls)
|
||||
|
||||
self.assertCountEqual(
|
||||
[mock.call(True), mock.call(False)],
|
||||
mock_keepcaps.mock_calls)
|
||||
[mock.call(True), mock.call(False)], mock_keepcaps.mock_calls
|
||||
)
|
||||
|
||||
mock_dropcaps.assert_called_once_with(
|
||||
{capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN},
|
||||
{capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN},
|
||||
[])
|
||||
[],
|
||||
)
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class WithContextTest(testctx.TestContextTestCase):
|
||||
|
||||
def test_unexported(self):
|
||||
self.assertRaisesRegex(
|
||||
NameError, 'undecorated not exported',
|
||||
testctx.context._wrap, undecorated)
|
||||
NameError,
|
||||
'undecorated not exported',
|
||||
testctx.context._wrap,
|
||||
undecorated,
|
||||
)
|
||||
|
||||
|
||||
class ClientChannelTestCase(base.BaseTestCase):
|
||||
|
||||
DICT = {
|
||||
'string_1': ('tuple_1', b'tuple_2'),
|
||||
b'byte_1': ['list_1', 'list_2'],
|
||||
@@ -248,22 +275,28 @@ class ClientChannelTestCase(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
context = get_fake_context()
|
||||
with mock.patch.object(comm.ClientChannel, '__init__'), \
|
||||
mock.patch.object(daemon._ClientChannel, 'exchange_ping'):
|
||||
with (
|
||||
mock.patch.object(comm.ClientChannel, '__init__'),
|
||||
mock.patch.object(daemon._ClientChannel, 'exchange_ping'),
|
||||
):
|
||||
self.client_channel = daemon._ClientChannel(mock.ANY, context)
|
||||
|
||||
@mock.patch.object(daemon.LOG.logger, 'handle')
|
||||
def test_out_of_band_log_message(self, handle_mock):
|
||||
message = [comm.Message.LOG, self.DICT]
|
||||
self.assertEqual(self.client_channel.log, daemon.LOG)
|
||||
with mock.patch.object(pylogging, 'makeLogRecord') as mock_make_log, \
|
||||
mock.patch.object(daemon.LOG, 'isEnabledFor',
|
||||
return_value=True) as mock_enabled:
|
||||
with (
|
||||
mock.patch.object(pylogging, 'makeLogRecord') as mock_make_log,
|
||||
mock.patch.object(
|
||||
daemon.LOG, 'isEnabledFor', return_value=True
|
||||
) as mock_enabled,
|
||||
):
|
||||
self.client_channel.out_of_band(message)
|
||||
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)
|
||||
mock_make_log.return_value.levelno
|
||||
)
|
||||
|
||||
def test_out_of_band_not_log_message(self):
|
||||
with mock.patch.object(daemon.LOG, 'warning') as mock_warning:
|
||||
@@ -272,12 +305,15 @@ class ClientChannelTestCase(base.BaseTestCase):
|
||||
|
||||
@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):
|
||||
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'):
|
||||
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)
|
||||
@@ -288,22 +324,29 @@ class ClientChannelTestCase(base.BaseTestCase):
|
||||
|
||||
make_log_mock.assert_called_once_with(self.EXPECTED)
|
||||
channel.log.isEnabledFor.assert_called_once_with(
|
||||
make_log_mock.return_value.levelno)
|
||||
make_log_mock.return_value.levelno
|
||||
)
|
||||
channel.log.logger.handle.assert_called_once_with(
|
||||
make_log_mock.return_value)
|
||||
make_log_mock.return_value
|
||||
)
|
||||
|
||||
|
||||
class UnMonkeyPatch(base.BaseTestCase):
|
||||
|
||||
def test_un_monkey_patch(self):
|
||||
self.assertFalse(any(
|
||||
eventlet.patcher.is_monkey_patched(eventlet_mod_name)
|
||||
for eventlet_mod_name in daemon.EVENTLET_MODULES))
|
||||
self.assertFalse(
|
||||
any(
|
||||
eventlet.patcher.is_monkey_patched(eventlet_mod_name)
|
||||
for eventlet_mod_name in daemon.EVENTLET_MODULES
|
||||
)
|
||||
)
|
||||
|
||||
eventlet.monkey_patch()
|
||||
self.assertTrue(any(
|
||||
eventlet.patcher.is_monkey_patched(eventlet_mod_name)
|
||||
for eventlet_mod_name in daemon.EVENTLET_MODULES))
|
||||
self.assertTrue(
|
||||
any(
|
||||
eventlet.patcher.is_monkey_patched(eventlet_mod_name)
|
||||
for eventlet_mod_name in daemon.EVENTLET_MODULES
|
||||
)
|
||||
)
|
||||
|
||||
daemon.un_monkey_patch()
|
||||
for eventlet_mod_name, func_modules in daemon.EVENTLET_LIBRARIES:
|
||||
@@ -314,7 +357,8 @@ class UnMonkeyPatch(base.BaseTestCase):
|
||||
orig_mod = eventlet.patcher.original(name)
|
||||
patched_mod = sys.modules.get(name)
|
||||
for attr_name in green_mod.__patched__:
|
||||
un_monkey_patched_attr = getattr(patched_mod, attr_name,
|
||||
None)
|
||||
un_monkey_patched_attr = getattr(
|
||||
patched_mod, attr_name, None
|
||||
)
|
||||
original_attr = getattr(orig_mod, attr_name, None)
|
||||
self.assertEqual(un_monkey_patched_attr, original_attr)
|
||||
|
||||
@@ -66,10 +66,10 @@ def fail(custom=False):
|
||||
raise RuntimeError("I can't let you do that Dave")
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class PrivContextTest(testctx.TestContextTestCase):
|
||||
|
||||
def test_set_client_mode(self):
|
||||
context = priv_context.PrivContext('test', capabilities=[])
|
||||
self.assertTrue(context.client_mode)
|
||||
@@ -82,9 +82,12 @@ class PrivContextTest(testctx.TestContextTestCase):
|
||||
_, temp_path = tempfile.mkstemp()
|
||||
cmd = testctx.context.helper_command(temp_path)
|
||||
expected = [
|
||||
'foo', '--bar',
|
||||
'--privsep_context', testctx.context.pypath,
|
||||
'--privsep_sock_path', temp_path,
|
||||
'foo',
|
||||
'--bar',
|
||||
'--privsep_context',
|
||||
testctx.context.pypath,
|
||||
'--privsep_sock_path',
|
||||
temp_path,
|
||||
]
|
||||
self.assertEqual(expected, cmd)
|
||||
|
||||
@@ -93,11 +96,15 @@ class PrivContextTest(testctx.TestContextTestCase):
|
||||
_, temp_path = tempfile.mkstemp()
|
||||
cmd = testctx.context.helper_command(temp_path)
|
||||
expected = [
|
||||
'sudo', 'privsep-helper',
|
||||
'--config-file', '/bar.conf',
|
||||
'sudo',
|
||||
'privsep-helper',
|
||||
'--config-file',
|
||||
'/bar.conf',
|
||||
# --config-dir arg should be skipped
|
||||
'--privsep_context', testctx.context.pypath,
|
||||
'--privsep_sock_path', temp_path,
|
||||
'--privsep_context',
|
||||
testctx.context.pypath,
|
||||
'--privsep_sock_path',
|
||||
temp_path,
|
||||
]
|
||||
self.assertEqual(expected, cmd)
|
||||
|
||||
@@ -107,21 +114,31 @@ class PrivContextTest(testctx.TestContextTestCase):
|
||||
_, temp_path = tempfile.mkstemp()
|
||||
cmd = testctx.context.helper_command(temp_path)
|
||||
expected = [
|
||||
'sudo', 'privsep-helper',
|
||||
'--config-file', '/bar.conf',
|
||||
'--config-file', '/baz.conf',
|
||||
'--config-dir', '/foo.d',
|
||||
'--privsep_context', testctx.context.pypath,
|
||||
'--privsep_sock_path', temp_path,
|
||||
'sudo',
|
||||
'privsep-helper',
|
||||
'--config-file',
|
||||
'/bar.conf',
|
||||
'--config-file',
|
||||
'/baz.conf',
|
||||
'--config-dir',
|
||||
'/foo.d',
|
||||
'--privsep_context',
|
||||
testctx.context.pypath,
|
||||
'--privsep_sock_path',
|
||||
temp_path,
|
||||
]
|
||||
self.assertEqual(expected, cmd)
|
||||
|
||||
def test_init_known_contexts(self):
|
||||
self.assertEqual(testctx.context.helper_command('/sock')[:2],
|
||||
['sudo', 'privsep-helper'])
|
||||
self.assertEqual(
|
||||
testctx.context.helper_command('/sock')[:2],
|
||||
['sudo', 'privsep-helper'],
|
||||
)
|
||||
priv_context.init(root_helper=['sudo', 'rootwrap'])
|
||||
self.assertEqual(testctx.context.helper_command('/sock')[:3],
|
||||
['sudo', 'rootwrap', 'privsep-helper'])
|
||||
self.assertEqual(
|
||||
testctx.context.helper_command('/sock')[:3],
|
||||
['sudo', 'rootwrap', 'privsep-helper'],
|
||||
)
|
||||
|
||||
def test_start_acquires_lock(self):
|
||||
context = priv_context.PrivContext('test', capabilities=[])
|
||||
@@ -134,8 +151,9 @@ class PrivContextTest(testctx.TestContextTestCase):
|
||||
self.assertTrue(context.start_lock.__enter__.called)
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class SeparationTest(testctx.TestContextTestCase):
|
||||
def test_getpid(self):
|
||||
# Verify that priv_getpid() was executed in another process.
|
||||
@@ -156,8 +174,9 @@ class SeparationTest(testctx.TestContextTestCase):
|
||||
self.assertNotMyPid(priv_getpid())
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class RootwrapTest(testctx.TestContextTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -167,15 +186,18 @@ class RootwrapTest(testctx.TestContextTestCase):
|
||||
# requiring it to be properly installed.
|
||||
cmd = [
|
||||
'env',
|
||||
'PYTHON_PATH=%s' % os.path.pathsep.join(sys.path),
|
||||
sys.executable, daemon.__file__,
|
||||
f'PYTHON_PATH={os.path.pathsep.join(sys.path)}',
|
||||
sys.executable,
|
||||
daemon.__file__,
|
||||
]
|
||||
if LOG.isEnabledFor(logging.DEBUG):
|
||||
cmd.append('--debug')
|
||||
|
||||
self.privsep_conf.set_override(
|
||||
'helper_command', ' '.join(map(shlex.quote, cmd)),
|
||||
group=testctx.context.cfg_section)
|
||||
'helper_command',
|
||||
' '.join(map(shlex.quote, cmd)),
|
||||
group=testctx.context.cfg_section,
|
||||
)
|
||||
|
||||
testctx.context.start(method=priv_context.Method.ROOTWRAP)
|
||||
|
||||
@@ -185,25 +207,24 @@ class RootwrapTest(testctx.TestContextTestCase):
|
||||
self.assertNotMyPid(priv_pid)
|
||||
|
||||
def test_long_call_with_timeout(self):
|
||||
self.assertRaises(
|
||||
comm.PrivsepTimeout,
|
||||
do_some_long
|
||||
)
|
||||
self.assertRaises(comm.PrivsepTimeout, do_some_long)
|
||||
|
||||
def test_long_call_within_timeout(self):
|
||||
res = do_some_long(0.001)
|
||||
self.assertEqual(42, res)
|
||||
|
||||
|
||||
@testtools.skipIf(platform.system() != 'Linux',
|
||||
'works only on Linux platform.')
|
||||
@testtools.skipIf(
|
||||
platform.system() != 'Linux', 'works only on Linux platform.'
|
||||
)
|
||||
class SerializationTest(testctx.TestContextTestCase):
|
||||
def test_basic_functionality(self):
|
||||
self.assertEqual(43, add1(42))
|
||||
|
||||
def test_raises_standard(self):
|
||||
self.assertRaisesRegex(
|
||||
RuntimeError, "I can't let you do that Dave", fail)
|
||||
RuntimeError, "I can't let you do that Dave", fail
|
||||
)
|
||||
|
||||
def test_raises_custom(self):
|
||||
exc = self.assertRaises(CustomError, fail, custom=True)
|
||||
|
||||
@@ -35,8 +35,7 @@ class TestContextTestCase(base.BaseTestCase):
|
||||
super().setUp()
|
||||
config_override = getattr(self, 'config_override', {})
|
||||
privsep_fixture = self.useFixture(
|
||||
fixture.UnprivilegedPrivsepFixture(
|
||||
context, config_override)
|
||||
fixture.UnprivilegedPrivsepFixture(context, config_override)
|
||||
)
|
||||
self.privsep_conf = privsep_fixture.conf
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ privsep-helper = "oslo_privsep.daemon:helper_main"
|
||||
[tool.setuptools]
|
||||
packages = ["oslo_privsep"]
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["tests"]
|
||||
skips = ['B404', 'B603']
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "preserve"
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E4", "E5", "E7", "E9", "F", "G", "LOG", "S", "UP"]
|
||||
|
||||
@@ -197,9 +197,13 @@ htmlhelp_basename = 'oslo.privsepReleaseNotesDoc'
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'oslo.privsepReleaseNotes.tex',
|
||||
'oslo.privsep Release Notes Documentation',
|
||||
'oslo.privsep Developers', 'manual'),
|
||||
(
|
||||
'index',
|
||||
'oslo.privsepReleaseNotes.tex',
|
||||
'oslo.privsep Release Notes Documentation',
|
||||
'oslo.privsep Developers',
|
||||
'manual',
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -228,9 +232,13 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'oslo.privsepReleaseNotes',
|
||||
'oslo.privsep Release Notes Documentation',
|
||||
['oslo.privsep Developers'], 1)
|
||||
(
|
||||
'index',
|
||||
'oslo.privsepReleaseNotes',
|
||||
'oslo.privsep Release Notes Documentation',
|
||||
['oslo.privsep Developers'],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@@ -243,11 +251,15 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'oslo.privsepReleaseNotes',
|
||||
'oslo.privsep Release Notes Documentation',
|
||||
'oslo.privsep Developers', 'oslo.privsepReleaseNotes',
|
||||
'OpenStack library for privilege separation.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
'index',
|
||||
'oslo.privsepReleaseNotes',
|
||||
'oslo.privsep Release Notes Documentation',
|
||||
'oslo.privsep Developers',
|
||||
'oslo.privsepReleaseNotes',
|
||||
'OpenStack library for privilege separation.',
|
||||
'Miscellaneous',
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
4
setup.py
4
setup.py
@@ -15,6 +15,4 @@
|
||||
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=2.0.0'],
|
||||
pbr=True)
|
||||
setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True)
|
||||
|
||||
65
tox.ini
65
tox.ini
@@ -9,26 +9,14 @@ deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
commands = stestr run --slowest {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
skip_install = true
|
||||
deps =
|
||||
pre-commit>=2.6.0 # MIT
|
||||
commands =
|
||||
pre-commit run -a
|
||||
[testenv:functional]
|
||||
setenv =
|
||||
OS_TEST_PATH=./oslo_privsep/functional
|
||||
OS_LOG_CAPTURE=1
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:docs]
|
||||
allowlist_externals =
|
||||
rm
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/doc/requirements.txt
|
||||
commands =
|
||||
rm -rf doc/build doc/source/reference/api
|
||||
sphinx-build -W --keep-going -b html doc/source doc/build/html
|
||||
|
||||
[testenv:cover]
|
||||
deps = {[testenv]deps}
|
||||
coverage
|
||||
@@ -42,21 +30,15 @@ commands =
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report --show-missing
|
||||
|
||||
[flake8]
|
||||
show-source = True
|
||||
# H904: Delay string interpolations at logging calls
|
||||
enable-extensions = H106,H203,H904
|
||||
# E123, E125 skipped as they are invalid PEP-8.
|
||||
# [H106] Don’t put vim configuration in source files
|
||||
# [H203] Use assertIs(Not)None to check for None
|
||||
# [W504] line break after binary operator
|
||||
ignore = E123,E125,W504
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
||||
|
||||
[hacking]
|
||||
import_exceptions =
|
||||
oslo_privsep._i18n
|
||||
[testenv:docs]
|
||||
allowlist_externals =
|
||||
rm
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/doc/requirements.txt
|
||||
commands =
|
||||
rm -rf doc/build doc/source/reference/api
|
||||
sphinx-build -W --keep-going -b html doc/source doc/build/html
|
||||
|
||||
[testenv:releasenotes]
|
||||
allowlist_externals =
|
||||
@@ -68,7 +50,20 @@ commands =
|
||||
rm -rf releasenotes/build
|
||||
sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[testenv:functional]
|
||||
setenv =
|
||||
OS_TEST_PATH=./oslo_privsep/functional
|
||||
OS_LOG_CAPTURE=1
|
||||
[testenv:pep8]
|
||||
skip_install = true
|
||||
deps =
|
||||
pre-commit>=2.6.0 # MIT
|
||||
commands =
|
||||
pre-commit run -a
|
||||
|
||||
[flake8]
|
||||
# We only enable the hacking (H) checks
|
||||
select = H
|
||||
show-source = True
|
||||
builtins = _
|
||||
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
||||
|
||||
[hacking]
|
||||
import_exceptions =
|
||||
oslo_privsep._i18n
|
||||
|
||||
Reference in New Issue
Block a user