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
This commit is contained in:
John Dunning 2012-08-14 14:31:47 -04:00
parent 62efc6ed4e
commit 5dcf4e4521
26 changed files with 487 additions and 228 deletions

View File

@ -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:
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
(all other commands can be removed from this file)
/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),

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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
#-----------------------------------------------------------------------------

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

4
etc/rootwrap.conf Normal file
View File

@ -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

View File

@ -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']),
]

View File

@ -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:
else:
if len(args) != 2:
# Incorrect number of arguments
return False
elif '' not in self.args[0]:
# No signal, but list doesn't include empty string
if len(self.args) > 1:
# No signal requested, but filter requires specific signal
return False
try:
command = os.readlink("/proc/%d/exe" % int(args[1]))
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

View File

@ -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"),
]

View File

@ -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"),
]

View File

@ -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"),
]

View File

@ -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"),
]

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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])

View File

@ -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']),