diff --git a/bin/quantum-rootwrap b/bin/quantum-rootwrap new file mode 100755 index 0000000000..dcdccb9a38 --- /dev/null +++ b/bin/quantum-rootwrap @@ -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) diff --git a/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini b/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini index 8bead52e99..dd25e07866 100644 --- a/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini +++ b/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini @@ -22,3 +22,6 @@ physical_interface = eth1 [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 diff --git a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini index d8d5f21a29..a7a7f1a6ba 100644 --- a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini +++ b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini @@ -31,6 +31,11 @@ integration-bridge = br-int # Set local-ip to be the local IP address of this hypervisor. # 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. #----------------------------------------------------------------------------- @@ -41,6 +46,8 @@ integration-bridge = br-int # [OVS] # enable-tunneling = False # integration-bridge = br-int +# [AGENT] +# root_helper = sudo # # 2. With tunneling. # [DATABASE] @@ -51,3 +58,5 @@ integration-bridge = br-int # tunnel-bridge = br-tun # remote-ip-file = /opt/stack/remote-ips.txt # local-ip = 10.0.0.3 +# [AGENT] +# root_helper = sudo diff --git a/etc/quantum/plugins/ryu/ryu.ini b/etc/quantum/plugins/ryu/ryu.ini index 6d732c9f5a..2d5a2c5d7c 100644 --- a/etc/quantum/plugins/ryu/ryu.ini +++ b/etc/quantum/plugins/ryu/ryu.ini @@ -11,3 +11,8 @@ integration-bridge = br-int # openflow-rest-api = : 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. +root_helper = sudo diff --git a/quantum/plugins/linuxbridge/README b/quantum/plugins/linuxbridge/README index 39b8146b58..3d38bebc50 100644 --- a/quantum/plugins/linuxbridge/README +++ b/quantum/plugins/linuxbridge/README @@ -116,9 +116,20 @@ mysql> FLUSH PRIVILEGES; to the compute node. $ 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) + 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 (Note: The plugin ships with a default SQLite in-memory database configuration, diff --git a/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py b/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py index a9fa0b9c1c..45cd2b62db 100755 --- a/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py +++ b/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py @@ -30,6 +30,7 @@ import ConfigParser import logging as LOG import MySQLdb import os +import shlex import signal import sqlite3 import sys @@ -53,16 +54,18 @@ DB_CONNECTION = None 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.physical_interface = physical_interface + self.root_helper = root_helper def run_cmd(self, args): - LOG.debug("Running command: " + " ".join(args)) - p = Popen(args, stdout=PIPE) + cmd = shlex.split(self.root_helper) + args + LOG.debug("Running command: " + " ".join(cmd)) + p = Popen(cmd, stdout=PIPE) retval = p.communicate()[0] if p.returncode == -(signal.SIGALRM): - LOG.debug("Timeout running command: " + " ".join(args)) + LOG.debug("Timeout running command: " + " ".join(cmd)) if retval: LOG.debug("Command returned: %s" % retval) return retval @@ -287,12 +290,15 @@ class LinuxBridge: 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.root_helper = root_helper self.setup_linux_bridge(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, vlan_id): @@ -439,6 +445,7 @@ def main(): br_name_prefix = BRIDGE_NAME_PREFIX physical_interface = config.get("LINUX_BRIDGE", "physical_interface") polling_interval = config.get("AGENT", "polling_interval") + root_helper = config.get("AGENT", "root_helper") 'Establish database connection and load models' global DB_CONNECTION DB_CONNECTION = config.get("DATABASE", "connection") @@ -462,7 +469,7 @@ def main(): try: plugin = LinuxBridgeQuantumAgent(br_name_prefix, physical_interface, - polling_interval) + polling_interval, root_helper) LOG.info("Agent initialized successfully, now running...") plugin.daemon_loop(conn) finally: diff --git a/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py b/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py index 9452189407..09d30bf8d7 100644 --- a/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py +++ b/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py @@ -22,6 +22,7 @@ import logging as LOG import unittest import sys import os +import shlex import signal from subprocess import * @@ -392,20 +393,24 @@ class LinuxBridgeAgentTest(unittest.TestCase): self.physical_interface = config.get("LINUX_BRIDGE", "physical_interface") self.polling_interval = config.get("AGENT", "polling_interval") + self.root_helper = config.get("AGENT", "root_helper") except Exception, e: LOG.error("Unable to parse config file \"%s\": \nException%s" % (self.config_file, str(e))) sys.exit(1) 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.br_name_prefix, self.physical_interface, - self.polling_interval) + self.polling_interval, + self.root_helper) def run_cmd(self, args): - LOG.debug("Running command: " + " ".join(args)) - p = Popen(args, stdout=PIPE) + cmd = shlex.split(self.root_helper) + args + LOG.debug("Running command: " + " ".join(cmd)) + p = Popen(cmd, stdout=PIPE) retval = p.communicate()[0] if p.returncode == -(signal.SIGALRM): LOG.debug("Timeout running command: " + " ".join(args)) diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py index ef555a5012..9774c2e854 100755 --- a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -21,6 +21,7 @@ import ConfigParser import logging as LOG +import shlex import sys import time import signal @@ -57,15 +58,17 @@ class VifPort: class OVSBridge: - def __init__(self, br_name): + def __init__(self, br_name, root_helper): self.br_name = br_name + self.root_helper = root_helper def run_cmd(self, args): - # LOG.debug("## running command: " + " ".join(args)) - p = Popen(args, stdout=PIPE) + cmd = shlex.split(self.root_helper) + args + LOG.debug("## running command: " + " ".join(cmd)) + p = Popen(cmd, stdout=PIPE) retval = p.communicate()[0] if p.returncode == -(signal.SIGALRM): - LOG.debug("## timeout running command: " + " ".join(args)) + LOG.debug("## timeout running command: " + " ".join(cmd)) return retval def run_vsctl(self, args): @@ -207,7 +210,8 @@ class LocalVLANMapping: 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) 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") 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() # switch all traffic using L2 learning self.int_br.add_flow(priority=1, actions="normal") @@ -323,13 +327,15 @@ class OVSQuantumTunnelAgent(object): # Upper bound on available vlans. 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. :param integ_br: name of the integration bridge. :param tun_br: name of the tunnel bridge. :param remote_ip_file: name of file containing list of hypervisor IPs. :param local_ip: local IP address of this hypervisor.''' + self.root_helper = root_helper self.available_local_vlans = set( xrange(OVSQuantumTunnelAgent.MIN_VLAN_TAG, OVSQuantumTunnelAgent.MAX_VLAN_TAG)) @@ -423,7 +429,7 @@ class OVSQuantumTunnelAgent(object): Create patch ports and remove all existing flows. :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.patch_tun_ofport = self.int_br.add_patch_port("patch-tun", "patch-int") @@ -442,7 +448,7 @@ class OVSQuantumTunnelAgent(object): :param remote_ip_file: path to file that contains list of destination IP addresses. :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.patch_int_ofport = self.tun_br.add_patch_port("patch-int", "patch-tun") @@ -630,6 +636,8 @@ def main(): if not len(db_connection_url): raise Exception('Empty db_connection_url in configuration file.') + root_helper = config.get("AGENT", "root_helper") + except Exception, e: LOG.error("Error parsing common params in config_file: '%s': %s" % (config_file, str(e))) @@ -659,10 +667,10 @@ def main(): sys.exit(1) plugin = OVSQuantumTunnelAgent(integ_br, tun_br, remote_ip_file, - local_ip) + local_ip, root_helper) else: # Get parameters for OVSQuantumAgent. - plugin = OVSQuantumAgent(integ_br) + plugin = OVSQuantumAgent(integ_br, root_helper) # Start everything. options = {"sql_connection": db_connection_url} diff --git a/quantum/plugins/openvswitch/tests/unit/test_tunnel.py b/quantum/plugins/openvswitch/tests/unit/test_tunnel.py index ce6179a178..ee408f929b 100644 --- a/quantum/plugins/openvswitch/tests/unit/test_tunnel.py +++ b/quantum/plugins/openvswitch/tests/unit/test_tunnel.py @@ -63,14 +63,16 @@ class TunnelTest(unittest.TestCase): self.TUN_OFPORT = 'PATCH_TUN_OFPORT' 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.add_patch_port( 'patch-tun', 'patch-int').AndReturn(self.TUN_OFPORT) self.mock_int_bridge.remove_all_flows() 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.add_patch_port( 'patch-int', 'patch-tun').AndReturn(self.INT_OFPORT) @@ -86,7 +88,8 @@ class TunnelTest(unittest.TestCase): b = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') self.mox.VerifyAll() def testProvisionLocalVlan(self): @@ -105,7 +108,8 @@ class TunnelTest(unittest.TestCase): a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') a.available_local_vlans = set([LV_ID]) a.provision_local_vlan(NET_UUID, LS_ID) self.mox.VerifyAll() @@ -121,7 +125,8 @@ class TunnelTest(unittest.TestCase): a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') a.available_local_vlans = set() a.local_vlan_map[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, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') a.local_vlan_map[NET_UUID] = LVM a.port_bound(VIF_PORT, NET_UUID, LS_ID) self.mox.VerifyAll() @@ -147,7 +153,8 @@ class TunnelTest(unittest.TestCase): a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') a.available_local_vlans = set([LV_ID]) a.local_vlan_map[NET_UUID] = LVM a.port_unbound(VIF_PORT, NET_UUID) @@ -165,7 +172,8 @@ class TunnelTest(unittest.TestCase): a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') a.available_local_vlans = set([LV_ID]) a.local_vlan_map[NET_UUID] = LVM a.port_dead(VIF_PORT) @@ -187,7 +195,8 @@ class TunnelTest(unittest.TestCase): a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, self.TUN_BRIDGE, REMOTE_IP_FILE, - '10.0.0.1') + '10.0.0.1', + 'sudo') all_bindings = a.get_db_port_bindings(db) lsw_id_bindings = a.get_db_vlan_bindings(db) diff --git a/quantum/plugins/ryu/agent/ryu_quantum_agent.py b/quantum/plugins/ryu/agent/ryu_quantum_agent.py index 11a2e32f02..77569fb53e 100755 --- a/quantum/plugins/ryu/agent/ryu_quantum_agent.py +++ b/quantum/plugins/ryu/agent/ryu_quantum_agent.py @@ -58,8 +58,9 @@ class VifPort: class OVSBridge: - def __init__(self, br_name): + def __init__(self, br_name, root_helper): self.br_name = br_name + self.root_helper = root_helper self.datapath_id = None def find_datapath_id(self): @@ -71,10 +72,11 @@ class OVSBridge: self.datapath_id = dp_id 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] if pipe.returncode == -(signal.SIGALRM): - LOG.debug("## timeout running command: " + " ".join(args)) + LOG.debug("## timeout running command: " + " ".join(cmd)) return retval def run_vsctl(self, args): @@ -190,7 +192,8 @@ def check_ofp_mode(db): 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) 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) 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.set_controller(ofp_controller_addr) for port in self.int_br.get_external_ports(): @@ -297,12 +300,14 @@ def main(): integ_br = config.get("OVS", "integration-bridge") + root_helper = config.get("AGENT", "root_helper") + options = {"sql_connection": config.get("DATABASE", "sql_connection")} db = SqlSoup(options["sql_connection"]) LOG.info("Connecting to database \"%s\" on %s", db.engine.url.database, db.engine.url.host) - plugin = OVSQuantumOFPRyuAgent(integ_br, db) + plugin = OVSQuantumOFPRyuAgent(integ_br, db, root_helper) plugin.daemon_loop(db) sys.exit(0) diff --git a/quantum/rootwrap/__init__.py b/quantum/rootwrap/__init__.py new file mode 100644 index 0000000000..9bd7a21b89 --- /dev/null +++ b/quantum/rootwrap/__init__.py @@ -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. diff --git a/quantum/rootwrap/filters.py b/quantum/rootwrap/filters.py new file mode 100644 index 0000000000..48076f462e --- /dev/null +++ b/quantum/rootwrap/filters.py @@ -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 diff --git a/quantum/rootwrap/linuxbridge-agent.py b/quantum/rootwrap/linuxbridge-agent.py new file mode 100644 index 0000000000..5421ff1f8f --- /dev/null +++ b/quantum/rootwrap/linuxbridge-agent.py @@ -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"), + ] diff --git a/quantum/rootwrap/openvswitch-agent.py b/quantum/rootwrap/openvswitch-agent.py new file mode 100644 index 0000000000..0c00968dd3 --- /dev/null +++ b/quantum/rootwrap/openvswitch-agent.py @@ -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"), + ] diff --git a/quantum/rootwrap/ryu-agent.py b/quantum/rootwrap/ryu-agent.py new file mode 100644 index 0000000000..d413ccf13c --- /dev/null +++ b/quantum/rootwrap/ryu-agent.py @@ -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"), + ] diff --git a/quantum/rootwrap/wrapper.py b/quantum/rootwrap/wrapper.py new file mode 100644 index 0000000000..bcb0122fb4 --- /dev/null +++ b/quantum/rootwrap/wrapper.py @@ -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