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:
parent
62efc6ed4e
commit
5dcf4e4521
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
@ -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
|
||||
|
22
etc/quantum/rootwrap.d/dhcp.filters
Normal file
22
etc/quantum/rootwrap.d/dhcp.filters
Normal 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
|
21
etc/quantum/rootwrap.d/iptables-firewall.filters
Normal file
21
etc/quantum/rootwrap.d/iptables-firewall.filters
Normal 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
|
28
etc/quantum/rootwrap.d/l3.filters
Normal file
28
etc/quantum/rootwrap.d/l3.filters
Normal 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
|
17
etc/quantum/rootwrap.d/linuxbridge-plugin.filters
Normal file
17
etc/quantum/rootwrap.d/linuxbridge-plugin.filters
Normal 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
|
15
etc/quantum/rootwrap.d/nec-plugin.filters
Normal file
15
etc/quantum/rootwrap.d/nec-plugin.filters
Normal 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
|
23
etc/quantum/rootwrap.d/openvswitch-plugin.filters
Normal file
23
etc/quantum/rootwrap.d/openvswitch-plugin.filters
Normal 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
|
25
etc/quantum/rootwrap.d/ryu-plugin.filters
Normal file
25
etc/quantum/rootwrap.d/ryu-plugin.filters
Normal 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
4
etc/rootwrap.conf
Normal 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
|
@ -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']),
|
||||
]
|
@ -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
|
||||
|
@ -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"),
|
||||
]
|
@ -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"),
|
||||
]
|
@ -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"),
|
||||
]
|
@ -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"),
|
||||
]
|
@ -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):
|
||||
|
12
quantum/tests/etc/rootwrap.d/quantum.test.filters
Normal file
12
quantum/tests/etc/rootwrap.d/quantum.test.filters
Normal 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
|
78
quantum/tests/unit/_test_rootwrap_exec.py
Normal file
78
quantum/tests/unit/_test_rootwrap_exec.py
Normal 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)
|
115
quantum/tests/unit/test_rootwrap.py
Normal file
115
quantum/tests/unit/test_rootwrap.py
Normal 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])
|
2
setup.py
2
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']),
|
||||
|
Loading…
Reference in New Issue
Block a user