diff options
author | Thierry Carrez <thierry@openstack.org> | 2012-12-05 16:23:44 +0100 |
---|---|---|
committer | Thierry Carrez <thierry@openstack.org> | 2012-12-13 10:09:46 +0100 |
commit | a5b12b675ced2bc7e942cb107a8e181dbc5f6f45 (patch) | |
tree | 74dca69f5cb6600106111f346b630182df5d0ab4 | |
parent | 3fa86bc504b4e9ff716836f09201f2fac2c81bf4 (diff) | |
download | nova-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-x | bin/nova-rootwrap | 67 | ||||
-rw-r--r-- | etc/nova/rootwrap.conf | 14 | ||||
-rw-r--r-- | nova/rootwrap/filters.py | 1 | ||||
-rw-r--r-- | nova/rootwrap/wrapper.py | 59 | ||||
-rw-r--r-- | nova/tests/test_nova_rootwrap.py | 47 |
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) |