Skip to content

Commit

Permalink
Move files out of the namespace package
Browse files Browse the repository at this point in the history
Move the public API out of oslo.rootwrap to oslo_rootwrap. Retain
the ability to import from the old namespace package for backwards
compatibility for this release cycle.

bp/drop-namespace-packages

Change-Id: Ifed1a99e5ea6d999760731867c4294707698d41c
  • Loading branch information
dhellmann committed Dec 18, 2014
1 parent 44aa91f commit bdb739e
Show file tree
Hide file tree
Showing 27 changed files with 2,213 additions and 1,136 deletions.
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ The Oslo Rootwrap allows fine filtering of shell commands to run as `root`
from OpenStack services.

Rootwrap should be used as a separate Python process calling the
oslo.rootwrap.cmd:main function. You can set up a specific console_script
calling into oslo.rootwrap.cmd:main, called for example `nova-rootwrap`.
``oslo_rootwrap.cmd:main`` function. You can set up a specific console_script
calling into ``oslo_rootwrap.cmd:main``, called for example `nova-rootwrap`.
To keep things simple, this document will consider that your console_script
is called `/usr/bin/nova-rootwrap`.

Expand Down Expand Up @@ -318,13 +318,13 @@ Daemon mode
Since 1.3.0 version ``oslo.rootwrap`` supports "daemon mode". In this mode
rootwrap would start, read config file and wait for commands to be run with
root priviledges. All communications with the daemon should go through
``Client`` class that resides in ``oslo.rootwrap.client`` module.
``Client`` class that resides in ``oslo_rootwrap.client`` module.

Its constructor expects one argument - a list that can be passed to ``Popen``
to create rootwrap daemon process. For ``root_helper`` above it will be
``["sudo", "nova-rootwrap-daemon", "/etc/neutron/rootwrap.conf"]``,
for example. Note that it uses a separate script that points to
``oslo.rootwrap.cmd:daemon`` endpoint (instead of ``:main``).
``oslo_rootwrap.cmd:daemon`` endpoint (instead of ``:main``).

The class provides one method ``execute`` with following arguments:

Expand Down
6 changes: 3 additions & 3 deletions benchmark/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import sys
import timeit

from oslo.rootwrap import client
from oslo_rootwrap import client

config_path = "rootwrap.conf"
num_iterations = 100
Expand All @@ -44,12 +44,12 @@ def run_sudo(cmd):
def run_rootwrap(cmd):
return run_plain([
"sudo", sys.executable, "-c",
"from oslo.rootwrap import cmd; cmd.main()", config_path] + cmd)
"from oslo_rootwrap import cmd; cmd.main()", config_path] + cmd)


run_daemon = client.Client([
"sudo", sys.executable, "-c",
"from oslo.rootwrap import cmd; cmd.daemon()", config_path]).execute
"from oslo_rootwrap import cmd; cmd.daemon()", config_path]).execute


def run_one(runner, cmd):
Expand Down
26 changes: 26 additions & 0 deletions oslo/rootwrap/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import warnings


def deprecated():
new_name = __name__.replace('.', '_')
warnings.warn(
('The oslo namespace package is deprecated. Please use %s instead.' %
new_name),
DeprecationWarning,
stacklevel=3,
)


deprecated()
133 changes: 1 addition & 132 deletions oslo/rootwrap/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Copyright (c) 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
Expand All @@ -13,132 +10,4 @@
# License for the specific language governing permissions and limitations
# under the License.

import logging
from multiprocessing import managers
from multiprocessing import util as mp_util
import os
import subprocess
import threading
import weakref

try:
import eventlet.patcher
except ImportError:
patched_socket = False
else:
# In tests patching happens later, so we'll rely on environment variable
patched_socket = (eventlet.patcher.is_monkey_patched('socket') or
os.environ.get('TEST_EVENTLET', False))

from oslo.rootwrap import daemon
from oslo.rootwrap import jsonrpc

if patched_socket:
# We have to use slow version of recvall with eventlet because of a bug in
# GreenSocket.recv_into:
# https://bitbucket.org/eventlet/eventlet/pull-request/41
# This check happens here instead of jsonrpc to avoid importing eventlet
# from daemon code that is run with root priviledges.
jsonrpc.JsonConnection.recvall = jsonrpc.JsonConnection._recvall_slow

try:
finalize = weakref.finalize
except AttributeError:
def finalize(obj, func, *args, **kwargs):
return mp_util.Finalize(obj, func, args=args, kwargs=kwargs,
exitpriority=0)

ClientManager = daemon.get_manager_class()
LOG = logging.getLogger(__name__)


class Client(object):
def __init__(self, rootwrap_daemon_cmd):
self._start_command = rootwrap_daemon_cmd
self._initialized = False
self._mutex = threading.Lock()
self._manager = None
self._proxy = None
self._process = None
self._finalize = None

def _initialize(self):
if self._process is not None and self._process.poll() is not None:
LOG.warning("Leaving behind already spawned process with pid %d, "
"root should kill it if it's still there (I can't)",
self._process.pid)

process_obj = subprocess.Popen(self._start_command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
LOG.info("Spawned new rootwrap daemon process with pid=%d",
process_obj.pid)

self._process = process_obj
socket_path = process_obj.stdout.readline()[:-1]
# For Python 3 we need to convert bytes to str here
if not isinstance(socket_path, str):
socket_path = socket_path.decode('utf-8')
authkey = process_obj.stdout.read(32)
if process_obj.poll() is not None:
stderr = process_obj.stderr.read()
# NOTE(yorik-sar): don't expose stdout here
raise Exception("Failed to spawn rootwrap process.\nstderr:\n%s" %
(stderr,))
self._manager = ClientManager(socket_path, authkey)
self._manager.connect()
self._proxy = self._manager.rootwrap()
self._finalize = finalize(self, self._shutdown, self._process,
self._manager)
self._initialized = True

@staticmethod
def _shutdown(process, manager, JsonClient=jsonrpc.JsonClient):
# Storing JsonClient in arguments because globals are set to None
# before executing atexit routines in Python 2.x
if process.poll() is None:
LOG.info('Stopping rootwrap daemon process with pid=%s',
process.pid)
try:
manager.rootwrap().shutdown()
except (EOFError, IOError):
pass # assume it is dead already
# We might want to wait for process to exit or kill it, but we
# can't provide sane timeout on 2.x and we most likely don't have
# permisions to do so
# Invalidate manager's state so that proxy won't try to do decref
manager._state.value = managers.State.SHUTDOWN

def _ensure_initialized(self):
with self._mutex:
if not self._initialized:
self._initialize()

def _restart(self, proxy):
with self._mutex:
assert self._initialized
# Verify if someone has already restarted this.
if self._proxy is proxy:
self._finalize()
self._manager = None
self._proxy = None
self._initialized = False
self._initialize()
return self._proxy

def execute(self, cmd, env=None, stdin=None):
self._ensure_initialized()
proxy = self._proxy
retry = False
try:
res = proxy.run_one_command(cmd, env, stdin)
except (EOFError, IOError):
retry = True
# res can be None if we received final None sent by dying server thread
# instead of response to our request. Process is most likely to be dead
# at this point.
if retry or res is None:
proxy = self._restart(proxy)
res = proxy.run_one_command(cmd, env, stdin)
return res
from oslo_rootwrap.client import * # noqa
113 changes: 1 addition & 112 deletions oslo/rootwrap/cmd.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Copyright (c) 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
Expand All @@ -13,112 +10,4 @@
# License for the specific language governing permissions and limitations
# under the License.

"""Root wrapper for OpenStack services
Filters which commands a service is allowed to run as another user.
To use this with oslo, you should set the following in
oslo.conf:
rootwrap_config=/etc/oslo/rootwrap.conf
You also need to let the oslo user run oslo-rootwrap
as root in sudoers:
oslo ALL = (root) NOPASSWD: /usr/bin/oslo-rootwrap
/etc/oslo/rootwrap.conf *
Service packaging should deploy .filters files only on nodes where
they are needed, to avoid allowing more than is necessary.
"""

from __future__ import print_function

import logging
import sys

from six import moves

from oslo.rootwrap import daemon as daemon_mod
from oslo.rootwrap import wrapper

RC_UNAUTHORIZED = 99
RC_NOCOMMAND = 98
RC_BADCONFIG = 97
RC_NOEXECFOUND = 96
SIGNAL_BASE = 128


def _exit_error(execname, message, errorcode, log=True):
print("%s: %s" % (execname, message), file=sys.stderr)
if log:
logging.error(message)
sys.exit(errorcode)


def daemon():
return main(run_daemon=True)


def main(run_daemon=False):
# Split arguments, require at least a command
execname = sys.argv.pop(0)
if run_daemon:
if len(sys.argv) != 1:
_exit_error(execname, "Extra arguments to daemon", RC_NOCOMMAND,
log=False)
else:
if len(sys.argv) < 2:
_exit_error(execname, "No command specified", RC_NOCOMMAND,
log=False)

configfile = sys.argv.pop(0)

# Load configuration
try:
rawconfig = moves.configparser.RawConfigParser()
rawconfig.read(configfile)
config = wrapper.RootwrapConfig(rawconfig)
except ValueError as exc:
msg = "Incorrect value in %s: %s" % (configfile, exc.message)
_exit_error(execname, msg, RC_BADCONFIG, log=False)
except moves.configparser.Error:
_exit_error(execname, "Incorrect configuration file: %s" % configfile,
RC_BADCONFIG, log=False)

if config.use_syslog:
wrapper.setup_syslog(execname,
config.syslog_log_facility,
config.syslog_log_level)

filters = wrapper.load_filters(config.filters_path)

if run_daemon:
daemon_mod.daemon_start(config, filters)
else:
run_one_command(execname, config, filters, sys.argv)


def run_one_command(execname, config, filters, userargs):
# Execute command if it matches any of the loaded filters
try:
obj = wrapper.start_subprocess(
filters, userargs,
exec_dirs=config.exec_dirs,
log=config.use_syslog,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr)
returncode = obj.wait()
# Fix returncode of Popen
if returncode < 0:
returncode = SIGNAL_BASE - returncode
sys.exit(returncode)

except wrapper.FilterMatchNotExecutable as exc:
msg = ("Executable not found: %s (filter match = %s)"
% (exc.match.exec_path, exc.match.name))
_exit_error(execname, msg, RC_NOEXECFOUND, log=config.use_syslog)

except wrapper.NoFilterMatched:
msg = ("Unauthorized command: %s (no filter matched)"
% ' '.join(userargs))
_exit_error(execname, msg, RC_UNAUTHORIZED, log=config.use_syslog)
from oslo_rootwrap.cmd import * # noqa
Loading

0 comments on commit bdb739e

Please sign in to comment.