Add root_helper to quantum agents.

When running commands that require root privileges, the linuxbridge,
openvswitch, and ryu agent now prepend the commands with the value of
the root_helper config variable. This is set to "sudo" in the plugins'
.ini files, allowing the agent to run as a non-root user with
appropriate sudo privilidges.

If root_helper is changed to "sudo quantum-rootwrap",
then the command being run will be filtered against lists of each
agent's valid commands in quantum/rootwrap. See
http://wiki.openstack.org/Packager/Rootwrap for details.

Fixes bug 948467.

Change-Id: I549515068a4ce8ae480905ec5eaab6257445d0c3
Signed-off-by: Bob Kukura <rkukura@redhat.com>
This commit is contained in:
Bob Kukura 2012-03-13 17:23:06 -04:00
parent f88a1f7582
commit a06b316cb4
16 changed files with 508 additions and 38 deletions

73
bin/quantum-rootwrap Executable file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python
# 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.
"""Root wrapper for Quantum
Uses modules in quantum.rootwrap containing filters for commands
that quantum agents are 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 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.
"""
import os
import subprocess
import sys
RC_UNAUTHORIZED = 99
RC_NOCOMMAND = 98
if __name__ == '__main__':
# Split arguments, require at least a command
execname = sys.argv.pop(0)
if len(sys.argv) == 0:
print "%s: %s" % (execname, "No command specified")
sys.exit(RC_NOCOMMAND)
userargs = sys.argv[:]
# 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))
if os.path.exists(os.path.join(possible_topdir, "quantum", "__init__.py")):
sys.path.insert(0, possible_topdir)
from quantum.rootwrap import wrapper
# Execute command if it matches any of the loaded filters
filters = wrapper.load_filters()
filtermatch = wrapper.match_filter(filters, userargs)
if filtermatch:
obj = subprocess.Popen(filtermatch.get_command(userargs),
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr,
env=filtermatch.get_environment(userargs))
obj.wait()
sys.exit(obj.returncode)
print "Unauthorized command: %s" % ' '.join(userargs)
sys.exit(RC_UNAUTHORIZED)

View File

@ -22,3 +22,6 @@ physical_interface = eth1
[AGENT] [AGENT]
#agent's polling interval in seconds #agent's polling interval in seconds
polling_interval = 2 polling_interval = 2
# Change to "sudo quantum-rootwrap" to limit commands that can be run
# as root.
root_helper = sudo

View File

@ -31,6 +31,11 @@ integration-bridge = br-int
# Set local-ip to be the local IP address of this hypervisor. # Set local-ip to be the local IP address of this hypervisor.
# local-ip = 10.0.0.3 # local-ip = 10.0.0.3
[AGENT]
# Change to "sudo quantum-rootwrap" to limit commands that can be run
# as root.
root_helper = sudo
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Sample Configurations. # Sample Configurations.
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@ -41,6 +46,8 @@ integration-bridge = br-int
# [OVS] # [OVS]
# enable-tunneling = False # enable-tunneling = False
# integration-bridge = br-int # integration-bridge = br-int
# [AGENT]
# root_helper = sudo
# #
# 2. With tunneling. # 2. With tunneling.
# [DATABASE] # [DATABASE]
@ -51,3 +58,5 @@ integration-bridge = br-int
# tunnel-bridge = br-tun # tunnel-bridge = br-tun
# remote-ip-file = /opt/stack/remote-ips.txt # remote-ip-file = /opt/stack/remote-ips.txt
# local-ip = 10.0.0.3 # local-ip = 10.0.0.3
# [AGENT]
# root_helper = sudo

View File

@ -11,3 +11,8 @@ integration-bridge = br-int
# openflow-rest-api = <host IP address of ofp rest api service>:<port: 8080> # openflow-rest-api = <host IP address of ofp rest api service>:<port: 8080>
openflow-controller = 127.0.0.1:6633 openflow-controller = 127.0.0.1:6633
openflow-rest-api = 127.0.0.1:8080 openflow-rest-api = 127.0.0.1:8080
[AGENT]
# Change to "sudo quantum-rootwrap" to limit commands that can be run
# as root.
root_helper = sudo

View File

@ -116,9 +116,20 @@ mysql> FLUSH PRIVILEGES;
to the compute node. to the compute node.
$ Run the following: $ Run the following:
sudo python linuxbridge_quantum_agent.py linuxbridge_conf.ini python linuxbridge_quantum_agent.py linuxbridge_conf.ini
(Use --verbose option to see the logs) (Use --verbose option to see the logs)
Note that the the user running the agent must have sudo priviliges
to run various networking commands. Also, the agent can be
configured to use quantum-rootwrap, limiting what commands it can
run via sudo. See http://wiki.openstack.org/Packager/Rootwrap for
details on rootwrap.
As an alternative to coping the agent python file, if quantum is
installed on the compute node, the agent can be run as
bin/quantum-linuxbridge-agent.
# -- Running Tests # -- Running Tests
(Note: The plugin ships with a default SQLite in-memory database configuration, (Note: The plugin ships with a default SQLite in-memory database configuration,

View File

@ -30,6 +30,7 @@ import ConfigParser
import logging as LOG import logging as LOG
import MySQLdb import MySQLdb
import os import os
import shlex
import signal import signal
import sqlite3 import sqlite3
import sys import sys
@ -53,16 +54,18 @@ DB_CONNECTION = None
class LinuxBridge: class LinuxBridge:
def __init__(self, br_name_prefix, physical_interface): def __init__(self, br_name_prefix, physical_interface, root_helper):
self.br_name_prefix = br_name_prefix self.br_name_prefix = br_name_prefix
self.physical_interface = physical_interface self.physical_interface = physical_interface
self.root_helper = root_helper
def run_cmd(self, args): def run_cmd(self, args):
LOG.debug("Running command: " + " ".join(args)) cmd = shlex.split(self.root_helper) + args
p = Popen(args, stdout=PIPE) LOG.debug("Running command: " + " ".join(cmd))
p = Popen(cmd, stdout=PIPE)
retval = p.communicate()[0] retval = p.communicate()[0]
if p.returncode == -(signal.SIGALRM): if p.returncode == -(signal.SIGALRM):
LOG.debug("Timeout running command: " + " ".join(args)) LOG.debug("Timeout running command: " + " ".join(cmd))
if retval: if retval:
LOG.debug("Command returned: %s" % retval) LOG.debug("Command returned: %s" % retval)
return retval return retval
@ -287,12 +290,15 @@ class LinuxBridge:
class LinuxBridgeQuantumAgent: class LinuxBridgeQuantumAgent:
def __init__(self, br_name_prefix, physical_interface, polling_interval): def __init__(self, br_name_prefix, physical_interface, polling_interval,
root_helper):
self.polling_interval = int(polling_interval) self.polling_interval = int(polling_interval)
self.root_helper = root_helper
self.setup_linux_bridge(br_name_prefix, physical_interface) self.setup_linux_bridge(br_name_prefix, physical_interface)
def setup_linux_bridge(self, br_name_prefix, physical_interface): def setup_linux_bridge(self, br_name_prefix, physical_interface):
self.linux_br = LinuxBridge(br_name_prefix, physical_interface) self.linux_br = LinuxBridge(br_name_prefix, physical_interface,
self.root_helper)
def process_port_binding(self, port_id, network_id, interface_id, def process_port_binding(self, port_id, network_id, interface_id,
vlan_id): vlan_id):
@ -439,6 +445,7 @@ def main():
br_name_prefix = BRIDGE_NAME_PREFIX br_name_prefix = BRIDGE_NAME_PREFIX
physical_interface = config.get("LINUX_BRIDGE", "physical_interface") physical_interface = config.get("LINUX_BRIDGE", "physical_interface")
polling_interval = config.get("AGENT", "polling_interval") polling_interval = config.get("AGENT", "polling_interval")
root_helper = config.get("AGENT", "root_helper")
'Establish database connection and load models' 'Establish database connection and load models'
global DB_CONNECTION global DB_CONNECTION
DB_CONNECTION = config.get("DATABASE", "connection") DB_CONNECTION = config.get("DATABASE", "connection")
@ -462,7 +469,7 @@ def main():
try: try:
plugin = LinuxBridgeQuantumAgent(br_name_prefix, physical_interface, plugin = LinuxBridgeQuantumAgent(br_name_prefix, physical_interface,
polling_interval) polling_interval, root_helper)
LOG.info("Agent initialized successfully, now running...") LOG.info("Agent initialized successfully, now running...")
plugin.daemon_loop(conn) plugin.daemon_loop(conn)
finally: finally:

View File

@ -22,6 +22,7 @@ import logging as LOG
import unittest import unittest
import sys import sys
import os import os
import shlex
import signal import signal
from subprocess import * from subprocess import *
@ -392,20 +393,24 @@ class LinuxBridgeAgentTest(unittest.TestCase):
self.physical_interface = config.get("LINUX_BRIDGE", self.physical_interface = config.get("LINUX_BRIDGE",
"physical_interface") "physical_interface")
self.polling_interval = config.get("AGENT", "polling_interval") self.polling_interval = config.get("AGENT", "polling_interval")
self.root_helper = config.get("AGENT", "root_helper")
except Exception, e: except Exception, e:
LOG.error("Unable to parse config file \"%s\": \nException%s" LOG.error("Unable to parse config file \"%s\": \nException%s"
% (self.config_file, str(e))) % (self.config_file, str(e)))
sys.exit(1) sys.exit(1)
self._linuxbridge = linux_agent.LinuxBridge(self.br_name_prefix, self._linuxbridge = linux_agent.LinuxBridge(self.br_name_prefix,
self.physical_interface) self.physical_interface,
self.root_helper)
self._linuxbridge_quantum_agent = linux_agent.LinuxBridgeQuantumAgent( self._linuxbridge_quantum_agent = linux_agent.LinuxBridgeQuantumAgent(
self.br_name_prefix, self.br_name_prefix,
self.physical_interface, self.physical_interface,
self.polling_interval) self.polling_interval,
self.root_helper)
def run_cmd(self, args): def run_cmd(self, args):
LOG.debug("Running command: " + " ".join(args)) cmd = shlex.split(self.root_helper) + args
p = Popen(args, stdout=PIPE) LOG.debug("Running command: " + " ".join(cmd))
p = Popen(cmd, stdout=PIPE)
retval = p.communicate()[0] retval = p.communicate()[0]
if p.returncode == -(signal.SIGALRM): if p.returncode == -(signal.SIGALRM):
LOG.debug("Timeout running command: " + " ".join(args)) LOG.debug("Timeout running command: " + " ".join(args))

View File

@ -21,6 +21,7 @@
import ConfigParser import ConfigParser
import logging as LOG import logging as LOG
import shlex
import sys import sys
import time import time
import signal import signal
@ -57,15 +58,17 @@ class VifPort:
class OVSBridge: class OVSBridge:
def __init__(self, br_name): def __init__(self, br_name, root_helper):
self.br_name = br_name self.br_name = br_name
self.root_helper = root_helper
def run_cmd(self, args): def run_cmd(self, args):
# LOG.debug("## running command: " + " ".join(args)) cmd = shlex.split(self.root_helper) + args
p = Popen(args, stdout=PIPE) LOG.debug("## running command: " + " ".join(cmd))
p = Popen(cmd, stdout=PIPE)
retval = p.communicate()[0] retval = p.communicate()[0]
if p.returncode == -(signal.SIGALRM): if p.returncode == -(signal.SIGALRM):
LOG.debug("## timeout running command: " + " ".join(args)) LOG.debug("## timeout running command: " + " ".join(cmd))
return retval return retval
def run_vsctl(self, args): def run_vsctl(self, args):
@ -207,7 +210,8 @@ class LocalVLANMapping:
class OVSQuantumAgent(object): class OVSQuantumAgent(object):
def __init__(self, integ_br): def __init__(self, integ_br, root_helper):
self.root_helper = root_helper
self.setup_integration_br(integ_br) self.setup_integration_br(integ_br)
def port_bound(self, port, vlan_id): def port_bound(self, port, vlan_id):
@ -220,7 +224,7 @@ class OVSQuantumAgent(object):
self.int_br.clear_db_attribute("Port", port.port_name, "tag") self.int_br.clear_db_attribute("Port", port.port_name, "tag")
def setup_integration_br(self, integ_br): def setup_integration_br(self, integ_br):
self.int_br = OVSBridge(integ_br) self.int_br = OVSBridge(integ_br, self.root_helper)
self.int_br.remove_all_flows() self.int_br.remove_all_flows()
# switch all traffic using L2 learning # switch all traffic using L2 learning
self.int_br.add_flow(priority=1, actions="normal") self.int_br.add_flow(priority=1, actions="normal")
@ -323,13 +327,15 @@ class OVSQuantumTunnelAgent(object):
# Upper bound on available vlans. # Upper bound on available vlans.
MAX_VLAN_TAG = 4094 MAX_VLAN_TAG = 4094
def __init__(self, integ_br, tun_br, remote_ip_file, local_ip): def __init__(self, integ_br, tun_br, remote_ip_file, local_ip,
root_helper):
'''Constructor. '''Constructor.
:param integ_br: name of the integration bridge. :param integ_br: name of the integration bridge.
:param tun_br: name of the tunnel bridge. :param tun_br: name of the tunnel bridge.
:param remote_ip_file: name of file containing list of hypervisor IPs. :param remote_ip_file: name of file containing list of hypervisor IPs.
:param local_ip: local IP address of this hypervisor.''' :param local_ip: local IP address of this hypervisor.'''
self.root_helper = root_helper
self.available_local_vlans = set( self.available_local_vlans = set(
xrange(OVSQuantumTunnelAgent.MIN_VLAN_TAG, xrange(OVSQuantumTunnelAgent.MIN_VLAN_TAG,
OVSQuantumTunnelAgent.MAX_VLAN_TAG)) OVSQuantumTunnelAgent.MAX_VLAN_TAG))
@ -423,7 +429,7 @@ class OVSQuantumTunnelAgent(object):
Create patch ports and remove all existing flows. Create patch ports and remove all existing flows.
:param integ_br: the name of the integration bridge.''' :param integ_br: the name of the integration bridge.'''
self.int_br = OVSBridge(integ_br) self.int_br = OVSBridge(integ_br, self.root_helper)
self.int_br.delete_port("patch-tun") self.int_br.delete_port("patch-tun")
self.patch_tun_ofport = self.int_br.add_patch_port("patch-tun", self.patch_tun_ofport = self.int_br.add_patch_port("patch-tun",
"patch-int") "patch-int")
@ -442,7 +448,7 @@ class OVSQuantumTunnelAgent(object):
:param remote_ip_file: path to file that contains list of destination :param remote_ip_file: path to file that contains list of destination
IP addresses. IP addresses.
:param local_ip: the ip address of this node.''' :param local_ip: the ip address of this node.'''
self.tun_br = OVSBridge(tun_br) self.tun_br = OVSBridge(tun_br, self.root_helper)
self.tun_br.reset_bridge() self.tun_br.reset_bridge()
self.patch_int_ofport = self.tun_br.add_patch_port("patch-int", self.patch_int_ofport = self.tun_br.add_patch_port("patch-int",
"patch-tun") "patch-tun")
@ -630,6 +636,8 @@ def main():
if not len(db_connection_url): if not len(db_connection_url):
raise Exception('Empty db_connection_url in configuration file.') raise Exception('Empty db_connection_url in configuration file.')
root_helper = config.get("AGENT", "root_helper")
except Exception, e: except Exception, e:
LOG.error("Error parsing common params in config_file: '%s': %s" LOG.error("Error parsing common params in config_file: '%s': %s"
% (config_file, str(e))) % (config_file, str(e)))
@ -659,10 +667,10 @@ def main():
sys.exit(1) sys.exit(1)
plugin = OVSQuantumTunnelAgent(integ_br, tun_br, remote_ip_file, plugin = OVSQuantumTunnelAgent(integ_br, tun_br, remote_ip_file,
local_ip) local_ip, root_helper)
else: else:
# Get parameters for OVSQuantumAgent. # Get parameters for OVSQuantumAgent.
plugin = OVSQuantumAgent(integ_br) plugin = OVSQuantumAgent(integ_br, root_helper)
# Start everything. # Start everything.
options = {"sql_connection": db_connection_url} options = {"sql_connection": db_connection_url}

View File

@ -63,14 +63,16 @@ class TunnelTest(unittest.TestCase):
self.TUN_OFPORT = 'PATCH_TUN_OFPORT' self.TUN_OFPORT = 'PATCH_TUN_OFPORT'
self.mox.StubOutClassWithMocks(ovs_quantum_agent, 'OVSBridge') self.mox.StubOutClassWithMocks(ovs_quantum_agent, 'OVSBridge')
self.mock_int_bridge = ovs_quantum_agent.OVSBridge(self.INT_BRIDGE) self.mock_int_bridge = ovs_quantum_agent.OVSBridge(self.INT_BRIDGE,
'sudo')
self.mock_int_bridge.delete_port('patch-tun') self.mock_int_bridge.delete_port('patch-tun')
self.mock_int_bridge.add_patch_port( self.mock_int_bridge.add_patch_port(
'patch-tun', 'patch-int').AndReturn(self.TUN_OFPORT) 'patch-tun', 'patch-int').AndReturn(self.TUN_OFPORT)
self.mock_int_bridge.remove_all_flows() self.mock_int_bridge.remove_all_flows()
self.mock_int_bridge.add_flow(priority=1, actions='normal') self.mock_int_bridge.add_flow(priority=1, actions='normal')
self.mock_tun_bridge = ovs_quantum_agent.OVSBridge(self.TUN_BRIDGE) self.mock_tun_bridge = ovs_quantum_agent.OVSBridge(self.TUN_BRIDGE,
'sudo')
self.mock_tun_bridge.reset_bridge() self.mock_tun_bridge.reset_bridge()
self.mock_tun_bridge.add_patch_port( self.mock_tun_bridge.add_patch_port(
'patch-int', 'patch-tun').AndReturn(self.INT_OFPORT) 'patch-int', 'patch-tun').AndReturn(self.INT_OFPORT)
@ -86,7 +88,8 @@ class TunnelTest(unittest.TestCase):
b = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, b = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
self.mox.VerifyAll() self.mox.VerifyAll()
def testProvisionLocalVlan(self): def testProvisionLocalVlan(self):
@ -105,7 +108,8 @@ class TunnelTest(unittest.TestCase):
a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
a.available_local_vlans = set([LV_ID]) a.available_local_vlans = set([LV_ID])
a.provision_local_vlan(NET_UUID, LS_ID) a.provision_local_vlan(NET_UUID, LS_ID)
self.mox.VerifyAll() self.mox.VerifyAll()
@ -121,7 +125,8 @@ class TunnelTest(unittest.TestCase):
a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
a.available_local_vlans = set() a.available_local_vlans = set()
a.local_vlan_map[NET_UUID] = LVM a.local_vlan_map[NET_UUID] = LVM
a.reclaim_local_vlan(NET_UUID, LVM) a.reclaim_local_vlan(NET_UUID, LVM)
@ -137,7 +142,8 @@ class TunnelTest(unittest.TestCase):
a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
a.local_vlan_map[NET_UUID] = LVM a.local_vlan_map[NET_UUID] = LVM
a.port_bound(VIF_PORT, NET_UUID, LS_ID) a.port_bound(VIF_PORT, NET_UUID, LS_ID)
self.mox.VerifyAll() self.mox.VerifyAll()
@ -147,7 +153,8 @@ class TunnelTest(unittest.TestCase):
a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
a.available_local_vlans = set([LV_ID]) a.available_local_vlans = set([LV_ID])
a.local_vlan_map[NET_UUID] = LVM a.local_vlan_map[NET_UUID] = LVM
a.port_unbound(VIF_PORT, NET_UUID) a.port_unbound(VIF_PORT, NET_UUID)
@ -165,7 +172,8 @@ class TunnelTest(unittest.TestCase):
a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
a.available_local_vlans = set([LV_ID]) a.available_local_vlans = set([LV_ID])
a.local_vlan_map[NET_UUID] = LVM a.local_vlan_map[NET_UUID] = LVM
a.port_dead(VIF_PORT) a.port_dead(VIF_PORT)
@ -187,7 +195,8 @@ class TunnelTest(unittest.TestCase):
a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE,
self.TUN_BRIDGE, self.TUN_BRIDGE,
REMOTE_IP_FILE, REMOTE_IP_FILE,
'10.0.0.1') '10.0.0.1',
'sudo')
all_bindings = a.get_db_port_bindings(db) all_bindings = a.get_db_port_bindings(db)
lsw_id_bindings = a.get_db_vlan_bindings(db) lsw_id_bindings = a.get_db_vlan_bindings(db)

View File

@ -58,8 +58,9 @@ class VifPort:
class OVSBridge: class OVSBridge:
def __init__(self, br_name): def __init__(self, br_name, root_helper):
self.br_name = br_name self.br_name = br_name
self.root_helper = root_helper
self.datapath_id = None self.datapath_id = None
def find_datapath_id(self): def find_datapath_id(self):
@ -71,10 +72,11 @@ class OVSBridge:
self.datapath_id = dp_id self.datapath_id = dp_id
def run_cmd(self, args): def run_cmd(self, args):
pipe = Popen(args, stdout=PIPE) cmd = shlex.split(self.root_helper) + args
pipe = Popen(cmd, stdout=PIPE)
retval = pipe.communicate()[0] retval = pipe.communicate()[0]
if pipe.returncode == -(signal.SIGALRM): if pipe.returncode == -(signal.SIGALRM):
LOG.debug("## timeout running command: " + " ".join(args)) LOG.debug("## timeout running command: " + " ".join(cmd))
return retval return retval
def run_vsctl(self, args): def run_vsctl(self, args):
@ -190,7 +192,8 @@ def check_ofp_mode(db):
class OVSQuantumOFPRyuAgent: class OVSQuantumOFPRyuAgent:
def __init__(self, integ_br, db): def __init__(self, integ_br, db, root_helper):
self.root_helper = root_helper
(ofp_controller_addr, ofp_rest_api_addr) = check_ofp_mode(db) (ofp_controller_addr, ofp_rest_api_addr) = check_ofp_mode(db)
self.nw_id_external = rest_nw_id.NW_ID_EXTERNAL self.nw_id_external = rest_nw_id.NW_ID_EXTERNAL
@ -198,7 +201,7 @@ class OVSQuantumOFPRyuAgent:
self._setup_integration_br(integ_br, ofp_controller_addr) self._setup_integration_br(integ_br, ofp_controller_addr)
def _setup_integration_br(self, integ_br, ofp_controller_addr): def _setup_integration_br(self, integ_br, ofp_controller_addr):
self.int_br = OVSBridge(integ_br) self.int_br = OVSBridge(integ_br, self.root_helper)
self.int_br.find_datapath_id() self.int_br.find_datapath_id()
self.int_br.set_controller(ofp_controller_addr) self.int_br.set_controller(ofp_controller_addr)
for port in self.int_br.get_external_ports(): for port in self.int_br.get_external_ports():
@ -297,12 +300,14 @@ def main():
integ_br = config.get("OVS", "integration-bridge") integ_br = config.get("OVS", "integration-bridge")
root_helper = config.get("AGENT", "root_helper")
options = {"sql_connection": config.get("DATABASE", "sql_connection")} options = {"sql_connection": config.get("DATABASE", "sql_connection")}
db = SqlSoup(options["sql_connection"]) db = SqlSoup(options["sql_connection"])
LOG.info("Connecting to database \"%s\" on %s", LOG.info("Connecting to database \"%s\" on %s",
db.engine.url.database, db.engine.url.host) db.engine.url.database, db.engine.url.host)
plugin = OVSQuantumOFPRyuAgent(integ_br, db) plugin = OVSQuantumOFPRyuAgent(integ_br, db, root_helper)
plugin.daemon_loop(db) plugin.daemon_loop(db)
sys.exit(0) sys.exit(0)

View File

@ -0,0 +1,16 @@
# 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.

143
quantum/rootwrap/filters.py Normal file
View File

@ -0,0 +1,143 @@
# 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.
import os
import re
class CommandFilter(object):
"""Command filter only checking that the 1st argument matches exec_path"""
def __init__(self, exec_path, run_as, *args):
self.exec_path = exec_path
self.run_as = run_as
self.args = args
def match(self, userargs):
"""Only check that the first argument (command) matches exec_path"""
if (os.path.basename(self.exec_path) == userargs[0]):
return True
return False
def get_command(self, userargs):
"""Returns command to execute (with sudo -u if run_as != root)."""
if (self.run_as != 'root'):
# Used to run commands at lesser privileges
return ['sudo', '-u', self.run_as, self.exec_path] + userargs[1:]
return [self.exec_path] + userargs[1:]
def get_environment(self, userargs):
"""Returns specific environment to set, None if none"""
return None
class RegExpFilter(CommandFilter):
"""Command filter doing regexp matching for every argument"""
def match(self, userargs):
# Early skip if command or number of args don't match
if (len(self.args) != len(userargs)):
# DENY: argument numbers don't match
return False
# Compare each arg (anchoring pattern explicitly at end of string)
for (pattern, arg) in zip(self.args, userargs):
try:
if not re.match(pattern + '$', arg):
break
except re.error:
# DENY: Badly-formed filter
return False
else:
# ALLOW: All arguments matched
return True
# DENY: Some arguments did not match
return False
class DnsmasqFilter(CommandFilter):
"""Specific filter for the dnsmasq call (which includes env)"""
def match(self, userargs):
if (userargs[0].startswith("FLAGFILE=") and
userargs[1].startswith("NETWORK_ID=") and
userargs[2] == "dnsmasq"):
return True
return False
def get_command(self, userargs):
return [self.exec_path] + userargs[3:]
def get_environment(self, userargs):
env = os.environ.copy()
env['FLAGFILE'] = userargs[0].split('=')[-1]
env['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.
This filter relies on /proc to accurately determine affected
executable, so it will only work on procfs-capable systems (not OSX).
"""
def match(self, userargs):
if userargs[0] != "kill":
return False
args = list(userargs)
if len(args) == 3:
signal = args.pop(1)
if signal not in self.args[0]:
# Requested signal not in accepted list
return False
else:
if len(args) != 2:
# Incorrect number of arguments
return False
if '' not in self.args[0]:
# No signal, but list doesn't include empty string
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
return False
except (ValueError, OSError):
# Incorrect PID
return False
return True
class ReadFileFilter(CommandFilter):
"""Specific filter for the utils.read_file_as_root call"""
def __init__(self, file_path, *args):
self.file_path = file_path
super(ReadFileFilter, self).__init__("/bin/cat", "root", *args)
def match(self, userargs):
if userargs[0] != 'cat':
return False
if userargs[1] != self.file_path:
return False
if len(userargs) != 2:
return False
return True

View File

@ -0,0 +1,46 @@
# 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

@ -0,0 +1,36 @@
# 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

@ -0,0 +1,31 @@
# 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

@ -0,0 +1,63 @@
# 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.
import os
import sys
FILTERS_MODULES = ['quantum.rootwrap.linuxbridge-agent',
'quantum.rootwrap.openvswitch-agent',
'quantum.rootwrap.ryu-agent',
]
def load_filters():
"""Load filters from modules present in quantum.rootwrap."""
filters = []
for modulename in FILTERS_MODULES:
try:
__import__(modulename)
module = sys.modules[modulename]
filters = filters + module.filterlist
except ImportError:
# It's OK to have missing filters, since filter modules
# may be shipped with specific nodes
pass
return filters
def match_filter(filters, userargs):
"""
Checks user command and arguments through command filters and
returns the first matching filter, or None is none matched.
"""
found_filter = None
for f in filters:
if f.match(userargs):
# Try other filters if executable is absent
if not os.access(f.exec_path, os.X_OK):
if not found_filter:
found_filter = f
continue
# Otherwise return matching filter for execution
return f
# No filter matched or first missing executable
return found_filter