"""Root wrapper for Quantum
- Uses modules in quantum.rootwrap containing filters for commands
- that quantum agents are allowed to run as another user.
+ Filters which commands quantum is allowed to run as another user.
- To switch to using this, you should:
- * Set "--root_helper=sudo quantum-rootwrap" in the agents config file.
- * Allow quantum to run quantum-rootwrap as root in quantum_sudoers:
- quantum ALL = (root) NOPASSWD: /usr/bin/quantum-rootwrap
- (all other commands can be removed from this file)
+ To use this, you should set the following in quantum.conf and the
+ various .ini files for the agent plugins:
+ root_helper=sudo quantum-rootwrap /etc/quantum/rootwrap.conf
+ You also need to let the quantum user run quantum-rootwrap as root in
+ /etc/sudoers:
+ quantum ALL = (root) NOPASSWD: /usr/bin/quantum-rootwrap
+ /etc/quantum/rootwrap.conf *
+
+ Filter specs live in /etc/quantum/rootwrap.d/*.filters, or
+ other locations pointed to by /etc/quantum/rootwrap.conf.
To make allowed commands node-specific, your packaging should only
- install quantum/rootwrap/quantum-*-agent.py on compute nodes where
- agents that need root privileges are run.
+ install apropriate .filters for commands which are needed on each
+ node.
"""
+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:
+ # argv[0] required; path to conf file
+ 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(",")
+ filters = None
+ 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 quantum.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),
# Allow overlapping IP (Must have kernel build with CONFIG_NET_NS=y and
# iproute2 package that supports namespaces).
# use_namespaces = True
+
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
+root_helper = sudo
# Show debugging output in log (sets DEBUG log level output)
# debug = True
-# L3 requires that an inteface driver be set. Choose the one that best
+# L3 requires that an interface driver be set. Choose the one that best
# matches your plugin.
# OVS
admin_user = %SERVICE_USER%
admin_password = %SERVICE_PASSWORD%
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
+root_helper = sudo
[AGENT]
# Agent's polling interval in seconds
polling_interval = 2
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
-root_helper = sudo
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
+root_helper = "sudo"
# Use RPC messaging to interface between agent and plugin
# rpc = True
[AGENT]
# Agent's polling interval in seconds
polling_interval = 2
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
root_helper = sudo
[OFC]
[AGENT]
# Agent's polling interval in seconds
polling_interval = 2
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
root_helper = sudo
#-----------------------------------------------------------------------------
openflow_rest_api = 127.0.0.1:8080
[AGENT]
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
root_helper = sudo
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# dhcp-agent
+ip_exec_dnsmasq: DnsmasqFilter, /sbin/ip, root
+dnsmasq: DnsmasqFilter, /sbin/dnsmasq, root
+dnsmasq_usr: DnsmasqFilter, /usr/sbin/dnsmasq, root
+# dhcp-agent uses kill as well, that's handled by the generic KillFilter
+# it looks like these are the only signals needed, per
+# quantum/agent/linux/dhcp.py
+kill_dnsmasq: KillFilter, root, /sbin/dnsmasq, -9, -HUP
+kill_dnsmasq_usr: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
+
+# dhcp-agent uses cat
+cat: RegExpFilter, /bin/cat, root, cat, /proc/\d+/cmdline
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# quantum/agent/linux/iptables_manager.py
+# "iptables-save", ...
+iptables-save: CommandFilter, /sbin/iptables-save, root
+iptables-restore: CommandFilter, /sbin/iptables-restore, root
+ip6tables-save: CommandFilter, /sbin/ip6tables-save, root
+ip6tables-restore: CommandFilter, /sbin/ip6tables-restore, root
+
+# quantum/agent/linux/iptables_manager.py
+# "iptables", "-A", ...
+iptables: CommandFilter, /sbin/iptables, root
+ip6tables: CommandFilter, /sbin/ip6tables, root
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# l3_agent
+sysctl: CommandFilter, /sbin/sysctl, root
+
+# ip_lib
+ip: CommandFilter, /sbin/ip, root
+ip_usr: CommandFilter, /usr/sbin/ip, root
+
+# ovs_lib (if OVSInterfaceDriver is used)
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
+
+# iptables_manager
+iptables-save: CommandFilter, /sbin/iptables-save, root
+iptables-restore: CommandFilter, /sbin/iptables-restore, root
+ip6tables-save: CommandFilter, /sbin/ip6tables-save, root
+ip6tables-restore: CommandFilter, /sbin/ip6tables-restore, root
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# linuxbridge-agent
+# unclear whether both variants are necessary, but I'm transliterating
+# from the old mechanism
+brctl: CommandFilter, /sbin/brctl, root
+brctl_usr: CommandFilter, /usr/sbin/brctl, root
+ip: CommandFilter, /sbin/ip, root
+ip_usr: CommandFilter, /usr/sbin/ip, root
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# nec_quantum_agent
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# openvswitch-agent
+# unclear whether both variants are necessary, but I'm transliterating
+# from the old mechanism
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
+ovs-ofctl: CommandFilter, /bin/ovs-ofctl, root
+ovs-ofctl_usr: CommandFilter, /usr/bin/ovs-ofctl, root
+ovs-ofctl_sbin: CommandFilter, /sbin/ovs-ofctl, root
+ovs-ofctl_sbin_usr: CommandFilter, /usr/sbin/ovs-ofctl, root
+xe: CommandFilter, /sbin/xe, root
+xe_usr: CommandFilter, /usr/sbin/xe, root
--- /dev/null
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# ryu-agent
+# unclear whether both variants are necessary, but I'm transliterating
+# from the old mechanism
+
+# quantum/plugins/ryu/agent/ryu_quantum_agent.py:
+# "ovs-vsctl", "--timeout=2", ...
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
+
+# quantum/plugins/ryu/agent/ryu_quantum_agent.py:
+# "xe", "vif-param-get", ...
+xe: CommandFilter, /bin/xe, root
+xe_usr: CommandFilter, /usr/bin/xe, root
--- /dev/null
+[DEFAULT]
+# List of directories to load filter definitions from (separated by ',').
+# These directories MUST all be only writeable by root !
+filters_path=/etc/quantum/rootwrap.d,/usr/share/quantum/filters
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 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 quantum.rootwrap import filters
-
-filterlist = [
- # quantum/agent/linux/dhcp.py:
- # "dnsmasq", "--no-hosts", ...
- filters.CommandFilter("/usr/sbin/dnsmasq", "root"),
- filters.KillFilter("/bin/kill", "root", [''], ['/usr/sbin/dnsmasq']),
-]
class DnsmasqFilter(CommandFilter):
"""Specific filter for the dnsmasq call (which includes env)"""
+ def is_dnsmasq_cmd(self, argv):
+ if (argv[0] == "dnsmasq"):
+ return True
+ return False
+
+ def is_ip_netns_cmd(self, argv):
+ if ((argv[0] == "ip") and
+ (argv[1] == "netns") and
+ (argv[2] == "exec")):
+ return True
+ return False
+
def match(self, userargs):
- if ((userargs[0].startswith("FLAGFILE=") and
- userargs[1].startswith("NETWORK_ID=") and
- userargs[2] == "dnsmasq")):
+ """This matches the combination of the leading env
+ vars, plus either "dnsmasq" (for the case where we're
+ not using netns) or "ip" "netns" "exec" <foo> "dnsmasq"
+ (for the case where we are)"""
+ if ((userargs[0].startswith("QUANTUM_RELAY_SOCKET_PATH=") and
+ userargs[1].startswith("QUANTUM_NETWORK_ID=") and
+ (self.is_dnsmasq_cmd(userargs[2:]) or
+ (self.is_ip_netns_cmd(userargs[2:]) and
+ self.is_dnsmasq_cmd(userargs[6:]))))):
return True
return False
def get_environment(self, userargs):
env = os.environ.copy()
- env['FLAGFILE'] = userargs[0].split('=')[-1]
- env['NETWORK_ID'] = userargs[1].split('=')[-1]
+ env['QUANTUM_RELAY_SOCKET_PATH'] = userargs[0].split('=')[-1]
+ env['QUANTUM_NETWORK_ID'] = userargs[1].split('=')[-1]
return env
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:
+ # this means we're asking for a specific signal
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
- elif len(args) != 2:
- # Incorrect number of arguments
- return False
- elif '' not in self.args[0]:
- # No signal, but list doesn't include empty string
- return False
+ else:
+ if len(args) != 2:
+ # Incorrect number of arguments
+ return False
+ 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]))
- if command not in self.args[1]:
- # Affected executable not in accepted list
+ if command != self.args[0]:
+ # Affected executable doesn't match
return False
except (ValueError, OSError):
# Incorrect PID
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2012 Locaweb.
-# 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.
-#
-# @author: Juliano Martinez, Locaweb.
-
-from quantum.rootwrap import filters
-
-filterlist = [
- # quantum/agent/linux/iptables_manager.py
- # "iptables-save", ...
- filters.CommandFilter("/sbin/iptables-save", "root"),
- filters.CommandFilter("/sbin/iptables-restore", "root"),
- filters.CommandFilter("/sbin/ip6tables-save", "root"),
- filters.CommandFilter("/sbin/ip6tables-restore", "root"),
-
- # quantum/agent/linux/iptables_manager.py
- # "iptables", "-A", ...
- filters.CommandFilter("/sbin/iptables", "root"),
- filters.CommandFilter("/sbin/ip6tables", "root"),
-]
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 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 quantum.rootwrap import filters
-
-filterlist = [
- # quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py:
- # 'brctl', 'addbr', bridge_name
- # 'brctl', 'addif', bridge_name, interface
- # 'brctl', 'addif', bridge_name, tap_device_name
- # 'brctl', 'delbr', bridge_name
- # 'brctl', 'delif', bridge_name, interface_name
- # 'brctl', 'delif', current_bridge_name, ...
- # 'brctl', 'setfd', bridge_name, ...
- # 'brctl', 'stp', bridge_name, 'off'
- filters.CommandFilter("/usr/sbin/brctl", "root"),
- filters.CommandFilter("/sbin/brctl", "root"),
-
- # quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py:
- # 'ip', 'link', 'add', 'link', ...
- # 'ip', 'link', 'delete', interface
- # 'ip', 'link', 'set', bridge_name, 'down'
- # 'ip', 'link', 'set', bridge_name, 'up'
- # 'ip', 'link', 'set', interface, 'down'
- # 'ip', 'link', 'set', interface, 'up'
- # 'ip', 'link', 'show', 'dev', device
- # 'ip', 'tuntap'
- # 'ip', 'tuntap'
- filters.CommandFilter("/usr/sbin/ip", "root"),
- filters.CommandFilter("/sbin/ip", "root"),
-]
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 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 quantum.rootwrap import filters
-
-filterlist = [
- # quantum/plugins/openvswitch/agent/ovs_quantum_agent.py:
- # "ovs-vsctl", "--timeout=2", ...
- filters.CommandFilter("/usr/bin/ovs-vsctl", "root"),
- filters.CommandFilter("/bin/ovs-vsctl", "root"),
-
- # quantum/plugins/openvswitch/agent/ovs_quantum_agent.py:
- # "ovs-ofctl", cmd, self.br_name, args
- filters.CommandFilter("/usr/bin/ovs-ofctl", "root"),
- filters.CommandFilter("/bin/ovs-ofctl", "root"),
-
- # quantum/plugins/openvswitch/agent/ovs_quantum_agent.py:
- # "xe", "vif-param-get", ...
- filters.CommandFilter("/usr/bin/xe", "root"),
- filters.CommandFilter("/usr/sbin/xe", "root"),
-]
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 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 quantum.rootwrap import filters
-
-filterlist = [
- # quantum/plugins/ryu/agent/ryu_quantum_agent.py:
- # "ovs-vsctl", "--timeout=2", ...
- filters.CommandFilter("/usr/bin/ovs-vsctl", "root"),
- filters.CommandFilter("/bin/ovs-vsctl", "root"),
-
- # quantum/plugins/ryu/agent/ryu_quantum_agent.py:
- # "xe", "vif-param-get", ...
- filters.CommandFilter("/usr/bin/xe", "root"),
- filters.CommandFilter("/usr/sbin/xe", "root"),
-]
# under the License.
+import ConfigParser
import os
+import string
import sys
+# this import has the effect of defining global var "filters",
+# referenced by build_filter(), below. It gets set up by
+# quantum-rootwrap, when we load_filters().
+from quantum.rootwrap import filters
-FILTERS_MODULES = ['quantum.rootwrap.linuxbridge-agent',
- 'quantum.rootwrap.openvswitch-agent',
- 'quantum.rootwrap.ryu-agent',
- 'quantum.rootwrap.iptables-firewall-agent']
-
-
-def load_filters():
- """Load filters from modules present in quantum.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
- # may be shipped with specific nodes
- pass
- return filters
+
+def build_filter(class_name, *args):
+ """Returns a filter object of class class_name"""
+ if not hasattr(filters, class_name):
+ # TODO(jrd): Log the error (whenever quantum-rootwrap has a log file)
+ return None
+ filterclass = getattr(filters, class_name)
+ return filterclass(*args)
+
+
+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):
--- /dev/null
+# quantum-rootwrap command filters for the unit test
+
+# this file goes with quantum/tests/unit/_test_rootwrap_exec.py.
+# See the comments there about how to run that unit tests
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# a test filter for the RootwrapTest unit test
+bash: CommandFilter, /usr/bin/bash, root
--- /dev/null
+# Copyright 2012 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 unittest
+import mock
+from quantum.agent.linux import utils
+import os
+import logging
+
+
+LOG = logging.getLogger('quantum.tests.database_stubs')
+
+
+class RootwrapTestExec(unittest.TestCase):
+ """Simple unit test to test the basic rootwrap mechanism
+
+ Essentially hello-world. Just run a command as root and check that
+ it actually *did* run as root, and generated the right output.
+
+ NB that this is named _test_rootwrap so as not to get run by default
+ from scripts like tox. That's because it actually executes a sudo'ed
+ command, and that won't work in the automated test environment, at
+ least as it stands today. To run this, rename it to
+ test_rootwrap.py, or run it by hand.
+ """
+
+ def setUp(self):
+ self.cwd = os.getcwd() + "/../../.."
+ # stuff a stupid bash script into /tmp, so that the next
+ # method can execute it.
+ self.test_file = '/tmp/rootwrap-test.sh'
+ with open(self.test_file, 'w') as f:
+ f.write('#!/bin/bash\n')
+ f.write('ID=`id | sed \'s/uid=//\' | sed \'s/(.*//\' `\n')
+ f.write("echo $ID $1\
+\" Now is the time for all good men to come \
+to the aid of their party.\"\n")
+ # we need a temporary conf file, pointing into pwd for the filter
+ # specs. there's probably a better way to do this, but I couldn't
+ # figure it out. 08/15/12 -- jrd
+ self.conf_file = '/tmp/rootwrap.conf'
+ with open(self.conf_file, 'w') as f:
+ f.write("# temporary conf file for rootwrap-test, " +
+ "generated by test_rootwrap.py\n")
+ f.write("[DEFAULT]\n")
+ f.write("filters_path=" + self.cwd +
+ "/quantum/tests/etc/rootwrap.d/")
+ # now set the root helper to sudo our rootwrap script,
+ # with the new conf
+ self.root_helper = "sudo " + self.cwd + "/bin/quantum-rootwrap "
+ self.root_helper += self.conf_file
+
+ def runTest(self):
+ try:
+ result = utils.execute(["bash", self.test_file, 'arg'],
+ self.root_helper)
+ self.assertEqual(result,
+ "0 arg Now is the time for all good men to \
+come to the aid of their party.")
+ except Exception, ex:
+ LOG.exception("Losing in rootwrap test")
+
+ def tearDown(self):
+ os.remove(self.test_file)
+ os.remove(self.conf_file)
--- /dev/null
+# 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.
+
+import os
+import subprocess
+
+from quantum.rootwrap import filters
+from quantum.rootwrap import wrapper
+import unittest
+
+
+class RootwrapTestCase(unittest.TestCase):
+
+ def setUp(self):
+ super(RootwrapTestCase, self).setUp()
+ self.filters = [
+ filters.RegExpFilter("/bin/ls", "root", 'ls', '/[a-z]+'),
+ filters.CommandFilter("/usr/bin/foo_bar_not_exist", "root"),
+ filters.RegExpFilter("/bin/cat", "root", 'cat', '/[a-z]+'),
+ filters.CommandFilter("/nonexistant/cat", "root"),
+ filters.CommandFilter("/bin/cat", "root")] # Keep this one last
+
+ def test_RegExpFilter_match(self):
+ usercmd = ["ls", "/root"]
+ filtermatch = wrapper.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 = wrapper.match_filter(self.filters, usercmd)
+ self.assertTrue(filtermatch is None)
+
+ def test_missing_command(self):
+ valid_but_missing = ["foo_bar_not_exist"]
+ invalid = ["foo_bar_not_exist_and_not_matched"]
+ filtermatch = wrapper.match_filter(self.filters, valid_but_missing)
+ self.assertTrue(filtermatch is not None)
+ filtermatch = wrapper.match_filter(self.filters, invalid)
+ self.assertTrue(filtermatch is None)
+
+ def test_DnsmasqFilter(self):
+ usercmd = ['QUANTUM_RELAY_SOCKET_PATH=A', 'QUANTUM_NETWORK_ID=foobar',
+ 'dnsmasq', 'foo']
+ f = filters.DnsmasqFilter("/usr/bin/dnsmasq", "root")
+ self.assertTrue(f.match(usercmd))
+ self.assertEqual(f.get_command(usercmd), ['/usr/bin/dnsmasq', 'foo'])
+ env = f.get_environment(usercmd)
+ self.assertEqual(env.get('QUANTUM_RELAY_SOCKET_PATH'), 'A')
+ self.assertEqual(env.get('QUANTUM_NETWORK_ID'), 'foobar')
+
+ def test_KillFilter(self):
+ p = subprocess.Popen(["/bin/sleep", "5"])
+ 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) or f2.match(usercmd))
+ usercmd = ['kill', p.pid]
+ # Providing no signal should fail
+ 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("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) or f2.match(usercmd))
+ usercmd = ['kill', 999999]
+ # Nonexistant PID should fail
+ self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+ usercmd = ['kill', p.pid]
+ # Providing no signal should work
+ 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("root", "")
+ # Providing anything other than kill should be False
+ usercmd = ['notkill', 999999]
+ self.assertFalse(f.match(usercmd))
+ # Providing something that is not a pid should be False
+ usercmd = ['kill', 'notapid']
+ self.assertFalse(f.match(usercmd))
+
+ def test_ReadFileFilter(self):
+ goodfn = '/good/file.name'
+ f = filters.ReadFileFilter(goodfn)
+ usercmd = ['cat', '/bad/file']
+ self.assertFalse(f.match(['cat', '/bad/file']))
+ usercmd = ['cat', goodfn]
+ self.assertEqual(f.get_command(usercmd), ['/bin/cat', goodfn])
+ self.assertTrue(f.match(usercmd))
+
+ def test_skips(self):
+ # Check that all filters are skipped and that the last matches
+ usercmd = ["cat", "/"]
+ filtermatch = wrapper.match_filter(self.filters, usercmd)
+ self.assertTrue(filtermatch is self.filters[-1])
]
ProjectScripts = [
+ 'bin/quantum-rootwrap',
]
config_path = 'etc/quantum/'
DataFiles = [
(config_path,
['etc/quantum.conf',
+ 'etc/rootwrap.conf',
'etc/api-paste.ini',
'etc/policy.json',
'etc/dhcp_agent.ini']),