"""Root wrapper for Cinder
- Uses modules in cinder.rootwrap containing filters for commands
- that cinder is allowed to run as another user.
+ Filters which commands cinder is allowed to run as another user.
- To switch to using this, you should:
- * Set "--root_helper=sudo cinder-rootwrap" in cinder.conf
- * Allow cinder to run cinder-rootwrap as root in cinder_sudoers:
- cinder ALL = (root) NOPASSWD: /usr/bin/cinder-rootwrap
- (all other commands can be removed from this file)
+ To use this, you should set the following in cinder.conf:
+ root_helper=sudo cinder-rootwrap /etc/cinder/rootwrap.conf
+
+ You also need to let the cinder user run cinder-rootwrap as root in sudoers:
+ cinder ALL = (root) NOPASSWD: /usr/bin/cinder-rootwrap
+ /etc/cinder/rootwrap.conf *
To make allowed commands node-specific, your packaging should only
- install cinder/rootwrap/{compute,network,volume}.py respectively on
- compute, network and volume nodes (i.e. cinder-api nodes should not
+ install volume.filters on volume nodes (i.e. cinder-api nodes should not
have any of those files installed).
"""
+import ConfigParser
import os
import subprocess
import sys
RC_UNAUTHORIZED = 99
RC_NOCOMMAND = 98
+RC_BADCONFIG = 97
if __name__ == '__main__':
# Split arguments, require at least a command
execname = sys.argv.pop(0)
- if len(sys.argv) == 0:
+ if len(sys.argv) < 2:
print "%s: %s" % (execname, "No command specified")
sys.exit(RC_NOCOMMAND)
+ 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(",")
+ 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))
from cinder.rootwrap import wrapper
# Execute command if it matches any of the loaded filters
- filters = wrapper.load_filters()
+ filters = wrapper.load_filters(filters_path)
filtermatch = wrapper.match_filter(filters, userargs)
if filtermatch:
obj = subprocess.Popen(filtermatch.get_command(userargs),
class KillFilter(CommandFilter):
"""Specific filter for the kill calls.
- 1st argument is a list of accepted signals (emptystring means no signal)
- 2nd argument is a list of accepted affected executables.
+ 1st argument is the user to run /bin/kill under
+ 2nd argument is the location of the affected executable
+ Subsequent arguments list the accepted signals (if any)
This filter relies on /proc to accurately determine affected
executable, so it will only work on procfs-capable systems (not OSX).
"""
+ def __init__(self, *args):
+ super(KillFilter, self).__init__("/bin/kill", *args)
+
def match(self, userargs):
if userargs[0] != "kill":
return False
args = list(userargs)
if len(args) == 3:
+ # A specific signal is requested
signal = args.pop(1)
- if signal not in self.args[0]:
+ if signal not in self.args[1:]:
# Requested signal not in accepted list
return False
else:
if len(args) != 2:
# Incorrect number of arguments
return False
- if '' not in self.args[0]:
- # No signal, but list doesn't include empty string
+ if len(self.args) > 1:
+ # No signal requested, but filter requires specific signal
return False
try:
command = os.readlink("/proc/%d/exe" % int(args[1]))
# the end if an executable is updated or deleted
if command.endswith(" (deleted)"):
command = command[:command.rindex(" ")]
- if command not in self.args[1]:
- # Affected executable not in accepted list
+ if command != self.args[0]:
+ # Affected executable does not match
return False
except (ValueError, OSError):
# Incorrect PID
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2011 OpenStack, LLC.
-# 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
-#
-# 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.
-
-
-from cinder.rootwrap import filters
-
-filterlist = [
- # cinder/volume/iscsi.py: iscsi_helper '--op' ...
- filters.CommandFilter("/usr/sbin/ietadm", "root"),
- filters.CommandFilter("/usr/sbin/tgtadm", "root"),
-
- # cinder/volume/driver.py: 'vgs', '--noheadings', '-o', 'name'
- filters.CommandFilter("/sbin/vgs", "root"),
-
- # cinder/volume/driver.py: 'lvcreate', '-L', sizestr, '-n', volume_name,..
- # cinder/volume/driver.py: 'lvcreate', '-L', ...
- filters.CommandFilter("/sbin/lvcreate", "root"),
-
- # cinder/volume/driver.py: 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,...
- filters.CommandFilter("/bin/dd", "root"),
-
- # cinder/volume/driver.py: 'lvremove', '-f', "%s/%s" % ...
- filters.CommandFilter("/sbin/lvremove", "root"),
-
- # cinder/volume/driver.py: 'lvdisplay','--noheading','-C','-o','Attr',..
- filters.CommandFilter("/sbin/lvdisplay", "root"),
-
- # cinder/volume/driver.py: 'iscsiadm', '-m', 'discovery', '-t',...
- # cinder/volume/driver.py: 'iscsiadm', '-m', 'node', '-T', ...
- filters.CommandFilter("/sbin/iscsiadm", "root"),
- ]
# under the License.
+import ConfigParser
import os
-import sys
+import string
+from cinder.rootwrap import filters
-FILTERS_MODULES = ['cinder.rootwrap.volume']
+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 cinder-rootwrap has a log file)
+ return None
+ filterclass = getattr(filters, class_name)
+ return filterclass(*args)
-def load_filters():
- """Load filters from modules present in cinder.rootwrap."""
- filters = []
- for modulename in FILTERS_MODULES:
- try:
- __import__(modulename)
- module = sys.modules[modulename]
- filters = filters + module.filterlist
- except ImportError:
- # It's OK to have missing filters, since filter modules are
- # shipped with specific nodes rather than with python-cinder
- pass
- return filters
+
+def load_filters(filters_path):
+ """Load filters from a list of directories"""
+ filterlist = []
+ for filterdir in filters_path:
+ if not os.path.isdir(filterdir):
+ continue
+ for filterfile in os.listdir(filterdir):
+ filterconfig = ConfigParser.RawConfigParser()
+ filterconfig.read(os.path.join(filterdir, filterfile))
+ for (name, value) in filterconfig.items("Filters"):
+ filterdefinition = [string.strip(s) for s in value.split(',')]
+ newfilter = build_filter(*filterdefinition)
+ if newfilter is None:
+ continue
+ filterlist.append(newfilter)
+ return filterlist
def match_filter(filters, userargs):
"Test requires /proc filesystem (procfs)")
def test_KillFilter(self):
p = subprocess.Popen(["/bin/sleep", "5"])
- f = filters.KillFilter("/bin/kill", "root",
- ["-ALRM"],
- ["/bin/sleep"])
- usercmd = ['kill', '-9', p.pid]
+ f = filters.KillFilter("root", "/bin/sleep", "-9", "-HUP")
+ f2 = filters.KillFilter("root", "/usr/bin/sleep", "-9", "-HUP")
+ usercmd = ['kill', '-ALRM', p.pid]
# Incorrect signal should fail
- self.assertFalse(f.match(usercmd))
+ self.assertFalse(f.match(usercmd) or f2.match(usercmd))
usercmd = ['kill', p.pid]
# Providing no signal should fail
- self.assertFalse(f.match(usercmd))
+ self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+ # Providing matching signal should be allowed
+ usercmd = ['kill', '-9', p.pid]
+ self.assertTrue(f.match(usercmd) or f2.match(usercmd))
- f = filters.KillFilter("/bin/kill", "root",
- ["-9", ""],
- ["/bin/sleep"])
- usercmd = ['kill', '-9', os.getpid()]
+ f = filters.KillFilter("root", "/bin/sleep")
+ f2 = filters.KillFilter("root", "/usr/bin/sleep")
+ usercmd = ['kill', os.getpid()]
# Our own PID does not match /bin/sleep, so it should fail
- self.assertFalse(f.match(usercmd))
- usercmd = ['kill', '-9', 999999]
+ self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+ usercmd = ['kill', 999999]
# Nonexistant PID should fail
- self.assertFalse(f.match(usercmd))
+ self.assertFalse(f.match(usercmd) or f2.match(usercmd))
usercmd = ['kill', p.pid]
# Providing no signal should work
- self.assertTrue(f.match(usercmd))
- usercmd = ['kill', '-9', p.pid]
- # Providing -9 signal should work
- self.assertTrue(f.match(usercmd))
+ self.assertTrue(f.match(usercmd) or f2.match(usercmd))
def test_KillFilter_no_raise(self):
"""Makes sure ValueError from bug 926412 is gone"""
- f = filters.KillFilter("/bin/kill", "root", [""])
+ f = filters.KillFilter("root", "")
# Providing anything other than kill should be False
usercmd = ['notkill', 999999]
self.assertFalse(f.match(usercmd))
def fake_readlink(blah):
return '/bin/commandddddd (deleted)'
- f = filters.KillFilter("/bin/kill", "root",
- [""],
- ["/bin/commandddddd"])
+ f = filters.KillFilter("root", "/bin/commandddddd")
usercmd = ['kill', 1234]
# Providing no signal should work
self.stubs.Set(os, 'readlink', fake_readlink)
--- /dev/null
+# Configuration for cinder-rootwrap
+# This file should be owned by (and only-writeable by) the root user
+
+[DEFAULT]
+# List of directories to load filter definitions from (separated by ',').
+# These directories MUST all be only writeable by root !
+filters_path=/etc/cinder/rootwrap.d,/usr/share/cinder/rootwrap
--- /dev/null
+# nova-rootwrap command filters for volume nodes
+# This file should be owned by (and only-writeable by) the root user
+
+[Filters]
+# nova/volume/iscsi.py: iscsi_helper '--op' ...
+ietadm: CommandFilter, /usr/sbin/ietadm, root
+tgtadm: CommandFilter, /usr/sbin/tgtadm, root
+
+# nova/volume/driver.py: 'vgs', '--noheadings', '-o', 'name'
+vgs: CommandFilter, /sbin/vgs, root
+
+# nova/volume/driver.py: 'lvcreate', '-L', sizestr, '-n', volume_name,..
+# nova/volume/driver.py: 'lvcreate', '-L', ...
+lvcreate: CommandFilter, /sbin/lvcreate, root
+
+# nova/volume/driver.py: 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,...
+dd: CommandFilter, /bin/dd, root
+
+# nova/volume/driver.py: 'lvremove', '-f', %s/%s % ...
+lvremove: CommandFilter, /sbin/lvremove, root
+
+# nova/volume/driver.py: 'lvdisplay', '--noheading', '-C', '-o', 'Attr',..
+lvdisplay: CommandFilter, /sbin/lvdisplay, root
+
+# nova/volume/driver.py: 'iscsiadm', '-m', 'discovery', '-t',...
+# nova/volume/driver.py: 'iscsiadm', '-m', 'node', '-T', ...
+iscsiadm: CommandFilter, /sbin/iscsiadm, root