summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2011-12-08 14:51:29 +0000
committerGerrit Code Review <review@openstack.org>2011-12-08 14:51:29 +0000
commit4bdee9eb9627093c3f4d42431ed997a30af6a56c (patch)
tree31b6e01e51edae0c2459dd4cb98f1afa5b2270ff
parent35782f24fa50b5b96072f9b81128f352098d50af (diff)
parent3c87de7c12c30d380e12b19dc0473d1e3bcfd233 (diff)
Merge "A more secure root-wrapper alternative"
-rwxr-xr-xbin/nova-rootwrap72
-rwxr-xr-xnova/rootwrap/__init__.py16
-rwxr-xr-xnova/rootwrap/compute.py159
-rwxr-xr-xnova/rootwrap/filters.py80
-rwxr-xr-xnova/rootwrap/network.py83
-rwxr-xr-xnova/rootwrap/volume.py48
-rwxr-xr-xnova/rootwrap/wrapper.py59
-rw-r--r--nova/tests/test_nova_rootwrap.py65
-rw-r--r--setup.py1
9 files changed, 583 insertions, 0 deletions
diff --git a/bin/nova-rootwrap b/bin/nova-rootwrap
new file mode 100755
index 000000000..80bb55ca8
--- /dev/null
+++ b/bin/nova-rootwrap
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# 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.
+
+"""Root wrapper for Nova
+
+ Uses modules in nova.rootwrap containing filters for commands
+ that nova is allowed to run as another user.
+
+ To switch to using this, you should:
+ * Set "--root_helper=sudo nova-rootwrap" in nova.conf
+ * Allow nova to run nova-rootwrap as root in nova_sudoers:
+ nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap
+ (all other commands can be removed from this file)
+
+ To make allowed commands node-specific, your packaging should only
+ install nova/rootwrap/{compute,network,volume}.py respectively on
+ compute, network and volume nodes (i.e. nova-api nodes should not
+ have any of those files installed).
+"""
+
+import os
+import subprocess
+import sys
+
+
+RC_UNAUTHORIZED = 99
+RC_NOCOMMAND = 98
+
+if __name__ == '__main__':
+ # Split arguments, require at least a command
+ execname = sys.argv.pop(0)
+ if len(sys.argv) == 0:
+ print "%s: %s" % (execname, "No command specified")
+ sys.exit(RC_NOCOMMAND)
+
+ userargs = sys.argv[:]
+
+ # 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))
+ if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
+ sys.path.insert(0, possible_topdir)
+
+ from nova.rootwrap import wrapper
+
+ # Execute command if it matches any of the loaded filters
+ filters = wrapper.load_filters()
+ filtermatch = wrapper.match_filter(filters, userargs)
+ if filtermatch:
+ obj = subprocess.Popen(filtermatch.get_command(userargs),
+ stdin=sys.stdin,
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+ sys.exit(obj.returncode)
+
+ print "Unauthorized command: %s" % ' '.join(userargs)
+ sys.exit(RC_UNAUTHORIZED)
diff --git a/nova/rootwrap/__init__.py b/nova/rootwrap/__init__.py
new file mode 100755
index 000000000..d6bd39db6
--- /dev/null
+++ b/nova/rootwrap/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/nova/rootwrap/compute.py b/nova/rootwrap/compute.py
new file mode 100755
index 000000000..cd8521d08
--- /dev/null
+++ b/nova/rootwrap/compute.py
@@ -0,0 +1,159 @@
+# 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 nova.rootwrap.filters import CommandFilter, DnsmasqFilter
+
+filters = [
+ # nova/virt/disk.py: 'kpartx', '-a', device
+ # nova/virt/disk.py: 'kpartx', '-d', device
+ CommandFilter("/sbin/kpartx", "root"),
+
+ # nova/virt/disk.py: 'tune2fs', '-c', 0, '-i', 0, mapped_device
+ # nova/virt/xenapi/vm_utils.py: "tune2fs", "-O ^has_journal", part_path
+ # nova/virt/xenapi/vm_utils.py: "tune2fs", "-j", partition_path
+ CommandFilter("/sbin/tune2fs", "root"),
+
+ # nova/virt/disk.py: 'mount', mapped_device, tmpdir
+ # nova/virt/disk.py: 'mount', device, container_dir
+ # nova/virt/disk.py: 'mount'
+ # nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
+ CommandFilter("/bin/mount", "root"),
+
+ # nova/virt/disk.py: 'umount', mapped_device
+ # nova/virt/disk.py: 'umount', container_dir
+ # nova/virt/xenapi/vm_utils.py: 'umount', dev_path
+ CommandFilter("/bin/umount", "root"),
+
+ # nova/virt/disk.py: 'qemu-nbd', '-c', device, image
+ # nova/virt/disk.py: 'qemu-nbd', '-d', device
+ CommandFilter("/usr/bin/qemu-nbd", "root"),
+
+ # nova/virt/disk.py: 'losetup', '--find', '--show', image
+ # nova/virt/disk.py: 'losetup', '--detach', device
+ CommandFilter("/sbin/losetup", "root"),
+
+ # nova/virt/disk.py: 'tee', metadata_path
+ # nova/virt/disk.py: 'tee', '-a', keyfile
+ # nova/virt/disk.py: 'tee', netfile
+ CommandFilter("/usr/bin/tee", "root"),
+
+ # nova/virt/disk.py: 'mkdir', '-p', sshdir
+ # nova/virt/disk.py: 'mkdir', '-p', netdir
+ CommandFilter("/bin/mkdir", "root"),
+
+ # nova/virt/disk.py: 'chown', 'root', sshdir
+ # nova/virt/disk.py: 'chown', 'root:root', netdir
+ # nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
+ # nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
+ # nova/virt/libvirt/connection.py: 'chown', 'root', basepath('disk')
+ # nova/virt/xenapi/vm_utils.py: 'chown', os.getuid(), dev_path
+ CommandFilter("/bin/chown", "root"),
+
+ # nova/virt/disk.py: 'chmod', '700', sshdir
+ # nova/virt/disk.py: 'chmod', 755, netdir
+ CommandFilter("/bin/chmod", "root"),
+
+ # nova/virt/libvirt/vif.py: 'ip', 'tuntap', 'add', dev, 'mode', 'tap'
+ # nova/virt/libvirt/vif.py: 'ip', 'link', 'set', dev, 'up'
+ # nova/virt/libvirt/vif.py: 'ip', 'link', 'delete', dev
+ # nova/network/linux_net.py: 'ip', 'addr', 'add', str(floating_ip)+'/32'i..
+ # nova/network/linux_net.py: 'ip', 'addr', 'del', str(floating_ip)+'/32'..
+ # nova/network/linux_net.py: 'ip', 'addr', 'add', '169.254.169.254/32',..
+ # nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', dev, 'scope',..
+ # nova/network/linux_net.py: 'ip', 'addr', 'del/add', ip_params, dev)
+ # nova/network/linux_net.py: 'ip', 'addr', 'del', params, fields[-1]
+ # nova/network/linux_net.py: 'ip', 'addr', 'add', params, bridge
+ # nova/network/linux_net.py: 'ip', '-f', 'inet6', 'addr', 'change', ..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', 'dev', dev, 'promisc',..
+ # nova/network/linux_net.py: 'ip', 'link', 'add', 'link', bridge_if ...
+ # nova/network/linux_net.py: 'ip', 'link', 'set', interface, "address",..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', interface, 'up'
+ # nova/network/linux_net.py: 'ip', 'link', 'set', bridge, 'up'
+ # nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', interface, ..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', dev, "address", ..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', dev, 'up'
+ CommandFilter("/sbin/ip", "root"),
+
+ # nova/virt/libvirt/vif.py: 'tunctl', '-b', '-t', dev
+ CommandFilter("/usr/sbin/tunctl", "root"),
+
+ # nova/virt/libvirt/vif.py: 'ovs-vsctl', ...
+ # nova/virt/libvirt/vif.py: 'ovs-vsctl', 'del-port', ...
+ # nova/network/linux_net.py: 'ovs-vsctl', ....
+ CommandFilter("/usr/bin/ovs-vsctl", "root"),
+
+ # nova/virt/libvirt/connection.py: 'dd', "if=%s" % virsh_output, ...
+ CommandFilter("/bin/dd", "root"),
+
+ # nova/virt/xenapi/volume_utils.py: 'iscsiadm', '-m', ...
+ CommandFilter("/sbin/iscsiadm", "root"),
+
+ # nova/virt/xenapi/vm_utils.py: "parted", "--script", ...
+ # nova/virt/xenapi/vm_utils.py: 'parted', '--script', dev_path, ..*.
+ CommandFilter("/sbin/parted", "root"),
+
+ # nova/virt/xenapi/vm_utils.py: fdisk %(dev_path)s
+ CommandFilter("/sbin/fdisk", "root"),
+
+ # nova/virt/xenapi/vm_utils.py: "e2fsck", "-f", "-p", partition_path
+ CommandFilter("/sbin/e2fsck", "root"),
+
+ # nova/virt/xenapi/vm_utils.py: "resize2fs", partition_path
+ CommandFilter("/sbin/resize2fs", "root"),
+
+ # nova/network/linux_net.py: 'ip[6]tables-save' % (cmd,), '-t', ...
+ CommandFilter("/sbin/iptables-save", "root"),
+ CommandFilter("/sbin/ip6tables-save", "root"),
+
+ # nova/network/linux_net.py: 'ip[6]tables-restore' % (cmd,)
+ CommandFilter("/sbin/iptables-restore", "root"),
+ CommandFilter("/sbin/ip6tables-restore", "root"),
+
+ # nova/network/linux_net.py: 'arping', '-U', floating_ip, '-A', '-I', ...
+ # nova/network/linux_net.py: 'arping', '-U', network_ref['dhcp_server'],..
+ CommandFilter("/usr/bin/arping", "root"),
+
+ # nova/network/linux_net.py: 'route', '-n'
+ # nova/network/linux_net.py: 'route', 'del', 'default', 'gw'
+ # nova/network/linux_net.py: 'route', 'add', 'default', 'gw'
+ # nova/network/linux_net.py: 'route', '-n'
+ # nova/network/linux_net.py: 'route', 'del', 'default', 'gw', old_gw, ..
+ # nova/network/linux_net.py: 'route', 'add', 'default', 'gw', old_gateway
+ CommandFilter("/sbin/route", "root"),
+
+ # nova/network/linux_net.py: 'dhcp_release', dev, address, mac_address
+ CommandFilter("/usr/bin/dhcp_release", "root"),
+
+ # nova/network/linux_net.py: 'kill', '-9', pid
+ # nova/network/linux_net.py: 'kill', '-HUP', pid
+ # nova/network/linux_net.py: 'kill', pid
+ CommandFilter("/bin/kill", "root"),
+
+ # nova/network/linux_net.py: dnsmasq call
+ DnsmasqFilter("/usr/sbin/dnsmasq", "root"),
+
+ # nova/network/linux_net.py: 'radvd', '-C', '%s' % _ra_file(dev, 'conf'),..
+ CommandFilter("/usr/sbin/radvd", "root"),
+
+ # nova/network/linux_net.py: 'brctl', 'addbr', bridge
+ # nova/network/linux_net.py: 'brctl', 'setfd', bridge, 0
+ # nova/network/linux_net.py: 'brctl', 'stp', bridge, 'off'
+ # nova/network/linux_net.py: 'brctl', 'addif', bridge, interface
+ CommandFilter("/sbin/brctl", "root"),
+ CommandFilter("/usr/sbin/brctl", "root"),
+ ]
diff --git a/nova/rootwrap/filters.py b/nova/rootwrap/filters.py
new file mode 100755
index 000000000..2932c5e1a
--- /dev/null
+++ b/nova/rootwrap/filters.py
@@ -0,0 +1,80 @@
+# 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.
+
+
+import os
+import re
+
+
+class CommandFilter(object):
+ """Command filter only checking that the 1st argument matches exec_path"""
+
+ def __init__(self, exec_path, run_as, *args):
+ self.exec_path = exec_path
+ self.run_as = run_as
+ self.args = args
+
+ def match(self, userargs):
+ """Only check that the first argument (command) matches exec_path"""
+ if (os.path.basename(self.exec_path) == userargs[0]):
+ return True
+ return False
+
+ def get_command(self, userargs):
+ """Returns command to execute (with sudo -u if run_as != root)."""
+ if (self.run_as != 'root'):
+ # Used to run commands at lesser privileges
+ return ['sudo', '-u', self.run_as, self.exec_path] + userargs[1:]
+ return [self.exec_path] + userargs[1:]
+
+
+class RegExpFilter(CommandFilter):
+ """Command filter doing regexp matching for every argument"""
+
+ def match(self, userargs):
+ # Early skip if command or number of args don't match
+ if (len(self.args) != len(userargs)):
+ # DENY: argument numbers don't match
+ return False
+ # Compare each arg (anchoring pattern explicitly at end of string)
+ for (pattern, arg) in zip(self.args, userargs):
+ try:
+ if not re.match(pattern + '$', arg):
+ break
+ except re.error:
+ # DENY: Badly-formed filter
+ return False
+ else:
+ # ALLOW: All arguments matched
+ return True
+
+ # DENY: Some arguments did not match
+ return False
+
+
+class DnsmasqFilter(CommandFilter):
+ """Specific filter for the dnsmasq call (which includes env)"""
+
+ def match(self, userargs):
+ if (userargs[0].startswith("FLAGFILE=") and
+ userargs[1].startswith("NETWORK_ID=") and
+ userargs[2] == "dnsmasq"):
+ return True
+ return False
+
+ def get_command(self, userargs):
+ return userargs[0:2] + [self.exec_path] + userargs[3:]
diff --git a/nova/rootwrap/network.py b/nova/rootwrap/network.py
new file mode 100755
index 000000000..a930176c6
--- /dev/null
+++ b/nova/rootwrap/network.py
@@ -0,0 +1,83 @@
+# 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 nova.rootwrap.filters import CommandFilter, DnsmasqFilter
+
+filters = [
+ # nova/network/linux_net.py: 'ip', 'addr', 'add', str(floating_ip)+'/32'i..
+ # nova/network/linux_net.py: 'ip', 'addr', 'del', str(floating_ip)+'/32'..
+ # nova/network/linux_net.py: 'ip', 'addr', 'add', '169.254.169.254/32',..
+ # nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', dev, 'scope',..
+ # nova/network/linux_net.py: 'ip', 'addr', 'del/add', ip_params, dev)
+ # nova/network/linux_net.py: 'ip', 'addr', 'del', params, fields[-1]
+ # nova/network/linux_net.py: 'ip', 'addr', 'add', params, bridge
+ # nova/network/linux_net.py: 'ip', '-f', 'inet6', 'addr', 'change', ..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', 'dev', dev, 'promisc',..
+ # nova/network/linux_net.py: 'ip', 'link', 'add', 'link', bridge_if ...
+ # nova/network/linux_net.py: 'ip', 'link', 'set', interface, "address",..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', interface, 'up'
+ # nova/network/linux_net.py: 'ip', 'link', 'set', bridge, 'up'
+ # nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', interface, ..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', dev, "address", ..
+ # nova/network/linux_net.py: 'ip', 'link', 'set', dev, 'up'
+ CommandFilter("/sbin/ip", "root"),
+
+ # nova/network/linux_net.py: 'ip[6]tables-save' % (cmd,), '-t', ...
+ CommandFilter("/sbin/iptables-save", "root"),
+ CommandFilter("/sbin/ip6tables-save", "root"),
+
+ # nova/network/linux_net.py: 'ip[6]tables-restore' % (cmd,)
+ CommandFilter("/sbin/iptables-restore", "root"),
+ CommandFilter("/sbin/ip6tables-restore", "root"),
+
+ # nova/network/linux_net.py: 'arping', '-U', floating_ip, '-A', '-I', ...
+ # nova/network/linux_net.py: 'arping', '-U', network_ref['dhcp_server'],..
+ CommandFilter("/usr/bin/arping", "root"),
+
+ # nova/network/linux_net.py: 'route', '-n'
+ # nova/network/linux_net.py: 'route', 'del', 'default', 'gw'
+ # nova/network/linux_net.py: 'route', 'add', 'default', 'gw'
+ # nova/network/linux_net.py: 'route', '-n'
+ # nova/network/linux_net.py: 'route', 'del', 'default', 'gw', old_gw, ..
+ # nova/network/linux_net.py: 'route', 'add', 'default', 'gw', old_gateway
+ CommandFilter("/sbin/route", "root"),
+
+ # nova/network/linux_net.py: 'dhcp_release', dev, address, mac_address
+ CommandFilter("/usr/bin/dhcp_release", "root"),
+
+ # nova/network/linux_net.py: 'kill', '-9', pid
+ # nova/network/linux_net.py: 'kill', '-HUP', pid
+ # nova/network/linux_net.py: 'kill', pid
+ CommandFilter("/bin/kill", "root"),
+
+ # nova/network/linux_net.py: dnsmasq call
+ DnsmasqFilter("/usr/sbin/dnsmasq", "root"),
+
+ # nova/network/linux_net.py: 'radvd', '-C', '%s' % _ra_file(dev, 'conf'),..
+ CommandFilter("/usr/sbin/radvd", "root"),
+
+ # nova/network/linux_net.py: 'brctl', 'addbr', bridge
+ # nova/network/linux_net.py: 'brctl', 'setfd', bridge, 0
+ # nova/network/linux_net.py: 'brctl', 'stp', bridge, 'off'
+ # nova/network/linux_net.py: 'brctl', 'addif', bridge, interface
+ CommandFilter("/sbin/brctl", "root"),
+ CommandFilter("/usr/sbin/brctl", "root"),
+
+ # nova/network/linux_net.py: 'ovs-vsctl', ....
+ CommandFilter("/usr/bin/ovs-vsctl", "root"),
+ ]
diff --git a/nova/rootwrap/volume.py b/nova/rootwrap/volume.py
new file mode 100755
index 000000000..4ddce7f25
--- /dev/null
+++ b/nova/rootwrap/volume.py
@@ -0,0 +1,48 @@
+# 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 nova.rootwrap.filters import CommandFilter
+
+filters = [
+ # nova/volume/iscsi.py: iscsi_helper '--op' ...
+ CommandFilter("/usr/sbin/ietadm", "root"),
+ CommandFilter("/usr/sbin/tgtadm", "root"),
+
+ # nova/volume/driver.py: 'vgs', '--noheadings', '-o', 'name'
+ CommandFilter("/sbin/vgs", "root"),
+
+ # nova/volume/driver.py: 'lvcreate', '-L', sizestr, '-n', volume_name,..
+ # nova/volume/driver.py: 'lvcreate', '-L', ...
+ CommandFilter("/sbin/lvcreate", "root"),
+
+ # nova/volume/driver.py: 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,...
+ CommandFilter("/bin/dd", "root"),
+
+ # nova/volume/driver.py: 'lvremove', '-f', "%s/%s" % ...
+ CommandFilter("/sbin/lvremove", "root"),
+
+ # nova/volume/driver.py: 'lvdisplay', '--noheading', '-C', '-o', 'Attr',..
+ CommandFilter("/sbin/lvdisplay", "root"),
+
+ # nova/volume/driver.py: 'iscsiadm', '-m', 'discovery', '-t',...
+ # nova/volume/driver.py: 'iscsiadm', '-m', 'node', '-T', ...
+ CommandFilter("/sbin/iscsiadm", "root"),
+
+ # nova/volume/driver.py:'/var/lib/zadara/bin/zadara_sncfg', *
+ # sudoers does not allow zadara_sncfg yet
+ ]
diff --git a/nova/rootwrap/wrapper.py b/nova/rootwrap/wrapper.py
new file mode 100755
index 000000000..2f24c0787
--- /dev/null
+++ b/nova/rootwrap/wrapper.py
@@ -0,0 +1,59 @@
+# 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.
+
+
+import os
+import sys
+
+
+FILTERS_MODULES = ['nova.rootwrap.compute',
+ 'nova.rootwrap.network',
+ 'nova.rootwrap.volume',
+ ]
+
+
+def load_filters():
+ """Load filters from modules present in nova.rootwrap."""
+ filters = []
+ for modulename in FILTERS_MODULES:
+ try:
+ __import__(modulename)
+ module = sys.modules[modulename]
+ filters = filters + module.filters
+ except ImportError:
+ # It's OK to have missing filters, since filter modules are
+ # shipped with specific nodes rather than with python-nova
+ pass
+ return filters
+
+
+def match_filter(filters, userargs):
+ """
+ Checks user command and arguments through command filters and
+ returns the first matching filter, or None is none matched.
+ """
+
+ for f in filters:
+ if f.match(userargs):
+ # Skip if executable is absent
+ if not os.access(f.exec_path, os.X_OK):
+ continue
+ # Otherwise return matching filter for execution
+ return f
+
+ # No filter matched
+ return None
diff --git a/nova/tests/test_nova_rootwrap.py b/nova/tests/test_nova_rootwrap.py
new file mode 100644
index 000000000..f73f9857d
--- /dev/null
+++ b/nova/tests/test_nova_rootwrap.py
@@ -0,0 +1,65 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC
+#
+# 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 nova.rootwrap.filters import CommandFilter, RegExpFilter, DnsmasqFilter
+from nova.rootwrap.wrapper import match_filter
+from nova import test
+
+
+class RootwrapTestCase(test.TestCase):
+
+ def setUp(self):
+ super(RootwrapTestCase, self).setUp()
+ self.filters = [
+ RegExpFilter("/bin/ls", "root", 'ls', '/[a-z]+'),
+ CommandFilter("/usr/bin/foo_bar_not_exist", "root"),
+ RegExpFilter("/bin/cat", "root", 'cat', '/[a-z]+'),
+ CommandFilter("/nonexistant/cat", "root"),
+ CommandFilter("/bin/cat", "root") # Keep this one last
+ ]
+
+ def tearDown(self):
+ super(RootwrapTestCase, self).tearDown()
+
+ def test_RegExpFilter_match(self):
+ usercmd = ["ls", "/root"]
+ filtermatch = match_filter(self.filters, usercmd)
+ self.assertFalse(filtermatch is None)
+ self.assertEqual(filtermatch.get_command(usercmd),
+ ["/bin/ls", "/root"])
+
+ def test_RegExpFilter_reject(self):
+ usercmd = ["ls", "root"]
+ filtermatch = match_filter(self.filters, usercmd)
+ self.assertTrue(filtermatch is None)
+
+ def test_missing_command(self):
+ usercmd = ["foo_bar_not_exist"]
+ filtermatch = match_filter(self.filters, usercmd)
+ self.assertTrue(filtermatch is None)
+
+ def test_dnsmasq_filter(self):
+ usercmd = ['FLAGFILE=A', 'NETWORK_ID="foo bar"', 'dnsmasq', 'foo']
+ f = DnsmasqFilter("/usr/bin/dnsmasq", "root")
+ self.assertTrue(f.match(usercmd))
+ self.assertEqual(f.get_command(usercmd),
+ ['FLAGFILE=A', 'NETWORK_ID="foo bar"', '/usr/bin/dnsmasq', 'foo'])
+
+ def test_skips(self):
+ # Check that all filters are skipped and that the last matches
+ usercmd = ["cat", "/"]
+ filtermatch = match_filter(self.filters, usercmd)
+ self.assertTrue(filtermatch is self.filters[-1])
diff --git a/setup.py b/setup.py
index 859c20ebc..f44acfcbf 100644
--- a/setup.py
+++ b/setup.py
@@ -104,6 +104,7 @@ setup(name='nova',
'bin/nova-manage',
'bin/nova-network',
'bin/nova-objectstore',
+ 'bin/nova-rootwrap',
'bin/nova-scheduler',
'bin/nova-spoolsentry',
'bin/stack',