diff --git a/bin/quantum-rootwrap b/bin/quantum-rootwrap index dcdccb9a3..bb7fbc0d5 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 8ecc9d0f9..efb540e8e 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 b119c8fde..121c4b47c 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 239d1f92f..b571cc0a6 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 e21a2b7c9..9dc9f6cdc 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 2c77db535..178b49a72 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 a90e8466b..42e5525de 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 000000000..7a9fa8a44 --- /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 000000000..2049e0e9f --- /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 000000000..e471217dc --- /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 000000000..591f69e4f --- /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 000000000..6d8f9c2a1 --- /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 000000000..bcb9527e2 --- /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 000000000..696c7d39a --- /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 000000000..0a1048e31 --- /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 2ba63a176..000000000 --- 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 510d10f39..8b3b89ba2 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" "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 83f714735..000000000 --- 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 326f08855..000000000 --- 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 9d4d2ef35..000000000 --- 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 54581d168..000000000 --- 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 45c414f9f..58b45bbc7 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 - -FILTERS_MODULES = ['quantum.rootwrap.linuxbridge-agent', - 'quantum.rootwrap.openvswitch-agent', - 'quantum.rootwrap.ryu-agent', - 'quantum.rootwrap.iptables-firewall-agent'] +# 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 -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 000000000..dc02011c5 --- /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 000000000..66fe434d7 --- /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 000000000..a238e7358 --- /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 1a59f0c63..7abb848b8 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']),