summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Dunning <jrd@jrd.org>2012-08-14 14:31:47 -0400
committerJohn Dunning <jrd@jrd.org>2012-08-30 16:55:00 -0400
commit193d6997276d3efcfaf99e5fcca1354c8c8c4670 (patch)
tree331f542bf690d41c67122b8bddeedbd5007cc061
parent9198ca803a3fddc91a1d45d3cd9a37b7aeca2215 (diff)
downloadneutron-193d6997276d3efcfaf99e5fcca1354c8c8c4670.tar.gz
Update rootwrap; track changes in nova/cinder
Fix bug 1037815 Summary: Copy/paste the essential parts of the rootwrap mechanism from nova/cinder into quantum. This includes the core changes to filter.py and wrapper.py which deal with loading filters from files pointed to by rootwrap.conf Detailed changes: Transliterate the old rootwrap/*-agent.py files to new format, and put the results in etc/quantum/rootwrap.d Delete the *-agent.py files. Add conf to point to etc/quantum/rootwrap.d Add a unit test cribbed from nova to exercise the filter mechanism Add a unit test to exercise the actual filtered execution Note that as written, this patch does not set the default execute mechanism (in the agent .ini files) to rootwrap, leaves it as sudo. That can be done in a followon change, or in distro specific packaging. Note also that there is still work to do around finishing and testing the filter specs themselves. We've decided that that is out of scope for this patch. Change-Id: I9aba6adc5ba40b6145be5fa38c5ece3b666ae5ca
-rwxr-xr-xbin/quantum-rootwrap41
-rw-r--r--etc/dhcp_agent.ini5
-rw-r--r--etc/l3_agent.ini6
-rw-r--r--etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini7
-rw-r--r--etc/quantum/plugins/nec/nec.ini5
-rw-r--r--etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini5
-rw-r--r--etc/quantum/plugins/ryu/ryu.ini5
-rw-r--r--etc/quantum/rootwrap.d/dhcp.filters22
-rw-r--r--etc/quantum/rootwrap.d/iptables-firewall.filters21
-rw-r--r--etc/quantum/rootwrap.d/l3.filters28
-rw-r--r--etc/quantum/rootwrap.d/linuxbridge-plugin.filters17
-rw-r--r--etc/quantum/rootwrap.d/nec-plugin.filters15
-rw-r--r--etc/quantum/rootwrap.d/openvswitch-plugin.filters23
-rw-r--r--etc/quantum/rootwrap.d/ryu-plugin.filters25
-rw-r--r--etc/rootwrap.conf4
-rw-r--r--quantum/rootwrap/dhcp-agent.py26
-rw-r--r--quantum/rootwrap/filters.py57
-rwxr-xr-xquantum/rootwrap/iptables-firewall-agent.py34
-rw-r--r--quantum/rootwrap/linuxbridge-agent.py46
-rw-r--r--quantum/rootwrap/openvswitch-agent.py36
-rw-r--r--quantum/rootwrap/ryu-agent.py31
-rw-r--r--quantum/rootwrap/wrapper.py51
-rw-r--r--quantum/tests/etc/rootwrap.d/quantum.test.filters12
-rw-r--r--quantum/tests/unit/_test_rootwrap_exec.py78
-rw-r--r--quantum/tests/unit/test_rootwrap.py115
-rw-r--r--setup.py2
26 files changed, 488 insertions, 229 deletions
diff --git a/bin/quantum-rootwrap b/bin/quantum-rootwrap
index dcdccb9a38..bb7fbc0d5d 100755
--- a/bin/quantum-rootwrap
+++ b/bin/quantum-rootwrap
@@ -18,20 +18,25 @@
"""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
@@ -39,16 +44,30 @@ 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))
@@ -58,7 +77,7 @@ if __name__ == '__main__':
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),
diff --git a/etc/dhcp_agent.ini b/etc/dhcp_agent.ini
index 8ecc9d0f96..efb540e8ec 100644
--- a/etc/dhcp_agent.ini
+++ b/etc/dhcp_agent.ini
@@ -25,3 +25,8 @@ dhcp_driver = quantum.agent.linux.dhcp.Dnsmasq
# 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
diff --git a/etc/l3_agent.ini b/etc/l3_agent.ini
index b119c8fdec..121c4b47c9 100644
--- a/etc/l3_agent.ini
+++ b/etc/l3_agent.ini
@@ -2,7 +2,7 @@
# 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
@@ -17,3 +17,7 @@ admin_tenant_name = %SERVICE_TENANT_NAME%
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
diff --git a/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini b/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini
index 239d1f92fc..b571cc0a6e 100644
--- a/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini
+++ b/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini
@@ -29,8 +29,9 @@ reconnect_interval = 2
[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
diff --git a/etc/quantum/plugins/nec/nec.ini b/etc/quantum/plugins/nec/nec.ini
index e21a2b7c95..9dc9f6cdc6 100644
--- a/etc/quantum/plugins/nec/nec.ini
+++ b/etc/quantum/plugins/nec/nec.ini
@@ -24,8 +24,9 @@ integration_bridge = br-int
[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]
diff --git a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
index 2c77db535e..178b49a722 100644
--- a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
+++ b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
@@ -38,8 +38,9 @@ tunnel_bridge = br-tun
[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
#-----------------------------------------------------------------------------
diff --git a/etc/quantum/plugins/ryu/ryu.ini b/etc/quantum/plugins/ryu/ryu.ini
index a90e8466b9..42e5525de7 100644
--- a/etc/quantum/plugins/ryu/ryu.ini
+++ b/etc/quantum/plugins/ryu/ryu.ini
@@ -13,6 +13,7 @@ openflow_controller = 127.0.0.1:6633
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
diff --git a/etc/quantum/rootwrap.d/dhcp.filters b/etc/quantum/rootwrap.d/dhcp.filters
new file mode 100644
index 0000000000..7a9fa8a444
--- /dev/null
+++ b/etc/quantum/rootwrap.d/dhcp.filters
@@ -0,0 +1,22 @@
+# 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
diff --git a/etc/quantum/rootwrap.d/iptables-firewall.filters b/etc/quantum/rootwrap.d/iptables-firewall.filters
new file mode 100644
index 0000000000..2049e0e9fa
--- /dev/null
+++ b/etc/quantum/rootwrap.d/iptables-firewall.filters
@@ -0,0 +1,21 @@
+# 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
diff --git a/etc/quantum/rootwrap.d/l3.filters b/etc/quantum/rootwrap.d/l3.filters
new file mode 100644
index 0000000000..e471217dc6
--- /dev/null
+++ b/etc/quantum/rootwrap.d/l3.filters
@@ -0,0 +1,28 @@
+# 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
diff --git a/etc/quantum/rootwrap.d/linuxbridge-plugin.filters b/etc/quantum/rootwrap.d/linuxbridge-plugin.filters
new file mode 100644
index 0000000000..591f69e4f6
--- /dev/null
+++ b/etc/quantum/rootwrap.d/linuxbridge-plugin.filters
@@ -0,0 +1,17 @@
+# 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
diff --git a/etc/quantum/rootwrap.d/nec-plugin.filters b/etc/quantum/rootwrap.d/nec-plugin.filters
new file mode 100644
index 0000000000..6d8f9c2a11
--- /dev/null
+++ b/etc/quantum/rootwrap.d/nec-plugin.filters
@@ -0,0 +1,15 @@
+# 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
diff --git a/etc/quantum/rootwrap.d/openvswitch-plugin.filters b/etc/quantum/rootwrap.d/openvswitch-plugin.filters
new file mode 100644
index 0000000000..bcb9527e23
--- /dev/null
+++ b/etc/quantum/rootwrap.d/openvswitch-plugin.filters
@@ -0,0 +1,23 @@
+# 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
diff --git a/etc/quantum/rootwrap.d/ryu-plugin.filters b/etc/quantum/rootwrap.d/ryu-plugin.filters
new file mode 100644
index 0000000000..696c7d39a4
--- /dev/null
+++ b/etc/quantum/rootwrap.d/ryu-plugin.filters
@@ -0,0 +1,25 @@
+# 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
diff --git a/etc/rootwrap.conf b/etc/rootwrap.conf
new file mode 100644
index 0000000000..0a1048e31f
--- /dev/null
+++ b/etc/rootwrap.conf
@@ -0,0 +1,4 @@
+[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
diff --git a/quantum/rootwrap/dhcp-agent.py b/quantum/rootwrap/dhcp-agent.py
deleted file mode 100644
index 2ba63a1764..0000000000
--- a/quantum/rootwrap/dhcp-agent.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# 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']),
-]
diff --git a/quantum/rootwrap/filters.py b/quantum/rootwrap/filters.py
index 510d10f391..8b3b89ba2c 100644
--- a/quantum/rootwrap/filters.py
+++ b/quantum/rootwrap/filters.py
@@ -71,10 +71,28 @@ class RegExpFilter(CommandFilter):
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
@@ -83,39 +101,46 @@ class DnsmasqFilter(CommandFilter):
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
diff --git a/quantum/rootwrap/iptables-firewall-agent.py b/quantum/rootwrap/iptables-firewall-agent.py
deleted file mode 100755
index 83f714735c..0000000000
--- a/quantum/rootwrap/iptables-firewall-agent.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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"),
-]
diff --git a/quantum/rootwrap/linuxbridge-agent.py b/quantum/rootwrap/linuxbridge-agent.py
deleted file mode 100644
index 326f088556..0000000000
--- a/quantum/rootwrap/linuxbridge-agent.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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"),
-]
diff --git a/quantum/rootwrap/openvswitch-agent.py b/quantum/rootwrap/openvswitch-agent.py
deleted file mode 100644
index 9d4d2ef35d..0000000000
--- a/quantum/rootwrap/openvswitch-agent.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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"),
-]
diff --git a/quantum/rootwrap/ryu-agent.py b/quantum/rootwrap/ryu-agent.py
deleted file mode 100644
index 54581d168c..0000000000
--- a/quantum/rootwrap/ryu-agent.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# 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"),
-]
diff --git a/quantum/rootwrap/wrapper.py b/quantum/rootwrap/wrapper.py
index 45c414f9f6..58b45bbc79 100644
--- a/quantum/rootwrap/wrapper.py
+++ b/quantum/rootwrap/wrapper.py
@@ -16,29 +16,42 @@
# 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):
diff --git a/quantum/tests/etc/rootwrap.d/quantum.test.filters b/quantum/tests/etc/rootwrap.d/quantum.test.filters
new file mode 100644
index 0000000000..dc02011c5c
--- /dev/null
+++ b/quantum/tests/etc/rootwrap.d/quantum.test.filters
@@ -0,0 +1,12 @@
+# 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
diff --git a/quantum/tests/unit/_test_rootwrap_exec.py b/quantum/tests/unit/_test_rootwrap_exec.py
new file mode 100644
index 0000000000..66fe434d77
--- /dev/null
+++ b/quantum/tests/unit/_test_rootwrap_exec.py
@@ -0,0 +1,78 @@
+# 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)
diff --git a/quantum/tests/unit/test_rootwrap.py b/quantum/tests/unit/test_rootwrap.py
new file mode 100644
index 0000000000..a238e7358d
--- /dev/null
+++ b/quantum/tests/unit/test_rootwrap.py
@@ -0,0 +1,115 @@
+# 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])
diff --git a/setup.py b/setup.py
index 1a59f0c638..7abb848b84 100644
--- a/setup.py
+++ b/setup.py
@@ -39,6 +39,7 @@ EagerResources = [
]
ProjectScripts = [
+ 'bin/quantum-rootwrap',
]
config_path = 'etc/quantum/'
@@ -54,6 +55,7 @@ nec_plugin_config_path = 'etc/quantum/plugins/nec'
DataFiles = [
(config_path,
['etc/quantum.conf',
+ 'etc/rootwrap.conf',
'etc/api-paste.ini',
'etc/policy.json',
'etc/dhcp_agent.ini']),