Add syslogging to nova-rootwrap

Add syslogging capabilities to nova-rootwrap, if you set parameter
use_syslog to True. You can specify a facility (syslog_log_facility)
and level (syslog_log_level) to use. Finalizes bp nova-rootwrap-options.

In doing so, it moves rootwrap config parsing to a nova.rootwrap.wrapper
object (and adds unit testing for it). It also improves log messages
content (including the name of the matching filter and the escalation
path used). Incidentally fixes bug 1084766.

Change-Id: Idb8cd9c9febd6263dafab4bc2bff817f00c53dc0
This commit is contained in:
Thierry Carrez 2012-12-05 16:23:44 +01:00
parent 3fa86bc504
commit a5b12b675c
5 changed files with 163 additions and 25 deletions

View File

@ -33,7 +33,9 @@
"""
import ConfigParser
import logging
import os
import pwd
import signal
import subprocess
import sys
@ -51,30 +53,22 @@ def _subprocess_setup():
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def _exit_error(execname, message, errorcode, log=True):
print "%s: %s" % (execname, message)
if log:
logging.error(message)
sys.exit(errorcode)
if __name__ == '__main__':
# Split arguments, require at least a command
execname = sys.argv.pop(0)
if len(sys.argv) < 2:
print "%s: %s" % (execname, "No command specified")
sys.exit(RC_NOCOMMAND)
_exit_error(execname, "No command specified", RC_NOCOMMAND, log=False)
configfile = sys.argv.pop(0)
userargs = sys.argv[:]
# Load configuration
config = ConfigParser.RawConfigParser()
config.read(configfile)
try:
filters_path = config.get("DEFAULT", "filters_path").split(",")
if config.has_option("DEFAULT", "exec_dirs"):
exec_dirs = config.get("DEFAULT", "exec_dirs").split(",")
else:
# Use system PATH if exec_dirs is not specified
exec_dirs = os.environ["PATH"].split(':')
except ConfigParser.Error:
print "%s: Incorrect configuration file: %s" % (execname, configfile)
sys.exit(RC_BADCONFIG)
# Add ../ to sys.path to allow running from branch
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(execname),
os.pardir, os.pardir))
@ -83,14 +77,37 @@ if __name__ == '__main__':
from nova.rootwrap import wrapper
# Load configuration
try:
rawconfig = 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 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)
# Execute command if it matches any of the loaded filters
filters = wrapper.load_filters(filters_path)
filters = wrapper.load_filters(config.filters_path)
try:
filtermatch = wrapper.match_filter(filters, userargs,
exec_dirs=exec_dirs)
exec_dirs=config.exec_dirs)
if filtermatch:
obj = subprocess.Popen(filtermatch.get_command(userargs,
exec_dirs=exec_dirs),
command = filtermatch.get_command(userargs,
exec_dirs=config.exec_dirs)
if config.use_syslog:
logging.info("(%s > %s) Executing %s (filter match = %s)" % (
os.getlogin(), pwd.getpwuid(os.getuid())[0],
command, filtermatch.name))
obj = subprocess.Popen(command,
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr,
@ -100,9 +117,11 @@ if __name__ == '__main__':
sys.exit(obj.returncode)
except wrapper.FilterMatchNotExecutable as exc:
print "Executable not found: %s" % exc.match.exec_path
sys.exit(RC_NOEXECFOUND)
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:
print "Unauthorized command: %s" % ' '.join(userargs)
sys.exit(RC_UNAUTHORIZED)
msg = ("Unauthorized command: %s (no filter matched)"
% ' '.join(userargs))
_exit_error(execname, msg, RC_UNAUTHORIZED, log=config.use_syslog)

View File

@ -11,3 +11,17 @@ filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap
# If not specified, defaults to system PATH environment variable.
# These directories MUST all be only writeable by root !
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin
# Enable logging to syslog
# Default value is False
use_syslog=False
# Which syslog facility to use.
# Valid values include auth, authpriv, syslog, user0, user1...
# Default value is 'syslog'
syslog_log_facility=syslog
# Which messages to log.
# INFO means log all usage
# ERROR means only log unsuccessful attempts
syslog_log_level=ERROR

View File

@ -23,6 +23,7 @@ class CommandFilter(object):
"""Command filter only checking that the 1st argument matches exec_path"""
def __init__(self, exec_path, run_as, *args):
self.name = ''
self.exec_path = exec_path
self.run_as = run_as
self.args = args

View File

@ -17,6 +17,8 @@
import ConfigParser
import logging
import logging.handlers
import os
import string
@ -37,10 +39,64 @@ class FilterMatchNotExecutable(Exception):
self.match = match
class RootwrapConfig(object):
def __init__(self, config):
# filters_path
self.filters_path = config.get("DEFAULT", "filters_path").split(",")
# exec_dirs
if config.has_option("DEFAULT", "exec_dirs"):
self.exec_dirs = config.get("DEFAULT", "exec_dirs").split(",")
else:
# Use system PATH if exec_dirs is not specified
self.exec_dirs = os.environ["PATH"].split(':')
# syslog_log_facility
if config.has_option("DEFAULT", "syslog_log_facility"):
v = config.get("DEFAULT", "syslog_log_facility")
facility_names = logging.handlers.SysLogHandler.facility_names
self.syslog_log_facility = getattr(logging.handlers.SysLogHandler,
v, None)
if self.syslog_log_facility is None and v in facility_names:
self.syslog_log_facility = facility_names.get(v)
if self.syslog_log_facility is None:
raise ValueError('Unexpected syslog_log_facility: %s' % v)
else:
default_facility = logging.handlers.SysLogHandler.LOG_SYSLOG
self.syslog_log_facility = default_facility
# syslog_log_level
if config.has_option("DEFAULT", "syslog_log_level"):
v = config.get("DEFAULT", "syslog_log_level")
self.syslog_log_level = logging.getLevelName(v.upper())
if (self.syslog_log_level == "Level %s" % v.upper()):
raise ValueError('Unexepected syslog_log_level: %s' % v)
else:
self.syslog_log_level = logging.ERROR
# use_syslog
if config.has_option("DEFAULT", "use_syslog"):
self.use_syslog = config.getboolean("DEFAULT", "use_syslog")
else:
self.use_syslog = False
def setup_syslog(execname, facility, level):
rootwrap_logger = logging.getLogger()
rootwrap_logger.setLevel(level)
handler = logging.handlers.SysLogHandler(address='/dev/log',
facility=facility)
handler.setFormatter(logging.Formatter(
os.path.basename(execname) + ': %(message)s'))
rootwrap_logger.addHandler(handler)
def build_filter(class_name, *args):
"""Returns a filter object of class class_name"""
if not hasattr(filters, class_name):
# TODO(ttx): Log the error (whenever nova-rootwrap has a log file)
logging.warning("Skipping unknown filter class (%s) specified "
"in filter definitions" % class_name)
return None
filterclass = getattr(filters, class_name)
return filterclass(*args)
@ -60,6 +116,7 @@ def load_filters(filters_path):
newfilter = build_filter(*filterdefinition)
if newfilter is None:
continue
newfilter.name = name
filterlist.append(newfilter)
return filterlist

View File

@ -14,6 +14,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import ConfigParser
import logging
import logging.handlers
import os
import subprocess
@ -149,3 +152,47 @@ class RootwrapTestCase(test.TestCase):
usercmd = ["cat", "/"]
filtermatch = wrapper.match_filter(self.filters, usercmd)
self.assertTrue(filtermatch is self.filters[-1])
def test_RootwrapConfig(self):
raw = ConfigParser.RawConfigParser()
# Empty config should raise ConfigParser.Error
self.assertRaises(ConfigParser.Error, wrapper.RootwrapConfig, raw)
# Check default values
raw.set('DEFAULT', 'filters_path', '/a,/b')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.filters_path, ['/a', '/b'])
self.assertEqual(config.exec_dirs, os.environ["PATH"].split(':'))
self.assertFalse(config.use_syslog)
self.assertEqual(config.syslog_log_facility,
logging.handlers.SysLogHandler.LOG_SYSLOG)
self.assertEqual(config.syslog_log_level, logging.ERROR)
# Check general values
raw.set('DEFAULT', 'exec_dirs', '/a,/x')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.exec_dirs, ['/a', '/x'])
raw.set('DEFAULT', 'use_syslog', 'oui')
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
raw.set('DEFAULT', 'use_syslog', 'true')
config = wrapper.RootwrapConfig(raw)
self.assertTrue(config.use_syslog)
raw.set('DEFAULT', 'syslog_log_facility', 'moo')
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
raw.set('DEFAULT', 'syslog_log_facility', 'local0')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.syslog_log_facility,
logging.handlers.SysLogHandler.LOG_LOCAL0)
raw.set('DEFAULT', 'syslog_log_facility', 'LOG_AUTH')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.syslog_log_facility,
logging.handlers.SysLogHandler.LOG_AUTH)
raw.set('DEFAULT', 'syslog_log_level', 'bar')
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
raw.set('DEFAULT', 'syslog_log_level', 'INFO')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.syslog_log_level, logging.INFO)