summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThierry Carrez <thierry@openstack.org>2012-12-05 16:23:44 +0100
committerThierry Carrez <thierry@openstack.org>2012-12-13 10:09:46 +0100
commita5b12b675ced2bc7e942cb107a8e181dbc5f6f45 (patch)
tree74dca69f5cb6600106111f346b630182df5d0ab4
parent3fa86bc504b4e9ff716836f09201f2fac2c81bf4 (diff)
downloadnova-a5b12b675ced2bc7e942cb107a8e181dbc5f6f45.tar.gz
nova-a5b12b675ced2bc7e942cb107a8e181dbc5f6f45.tar.xz
nova-a5b12b675ced2bc7e942cb107a8e181dbc5f6f45.zip
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
-rwxr-xr-xbin/nova-rootwrap67
-rw-r--r--etc/nova/rootwrap.conf14
-rw-r--r--nova/rootwrap/filters.py1
-rw-r--r--nova/rootwrap/wrapper.py59
-rw-r--r--nova/tests/test_nova_rootwrap.py47
5 files changed, 163 insertions, 25 deletions
diff --git a/bin/nova-rootwrap b/bin/nova-rootwrap
index 3322bc815..c8e880d79 100755
--- a/bin/nova-rootwrap
+++ b/bin/nova-rootwrap
@@ -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)
diff --git a/etc/nova/rootwrap.conf b/etc/nova/rootwrap.conf
index 5d6034eb9..fb2997abd 100644
--- a/etc/nova/rootwrap.conf
+++ b/etc/nova/rootwrap.conf
@@ -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
diff --git a/nova/rootwrap/filters.py b/nova/rootwrap/filters.py
index a3e5f1c3c..632e8d5bc 100644
--- a/nova/rootwrap/filters.py
+++ b/nova/rootwrap/filters.py
@@ -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
diff --git a/nova/rootwrap/wrapper.py b/nova/rootwrap/wrapper.py
index 742f23b14..848538234 100644
--- a/nova/rootwrap/wrapper.py
+++ b/nova/rootwrap/wrapper.py
@@ -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
diff --git a/nova/tests/test_nova_rootwrap.py b/nova/tests/test_nova_rootwrap.py
index 1dfd57a72..df7b88f2c 100644
--- a/nova/tests/test_nova_rootwrap.py
+++ b/nova/tests/test_nova_rootwrap.py
@@ -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)