]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add root_helper to quantum agents.
authorBob Kukura <rkukura@redhat.com>
Tue, 13 Mar 2012 21:23:06 +0000 (17:23 -0400)
committerBob Kukura <rkukura@redhat.com>
Wed, 14 Mar 2012 23:44:19 +0000 (19:44 -0400)
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>
16 files changed:
bin/quantum-rootwrap [new file with mode: 0755]
etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini
etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
etc/quantum/plugins/ryu/ryu.ini
quantum/plugins/linuxbridge/README
quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py
quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
quantum/plugins/openvswitch/tests/unit/test_tunnel.py
quantum/plugins/ryu/agent/ryu_quantum_agent.py
quantum/rootwrap/__init__.py [new file with mode: 0644]
quantum/rootwrap/filters.py [new file with mode: 0644]
quantum/rootwrap/linuxbridge-agent.py [new file with mode: 0644]
quantum/rootwrap/openvswitch-agent.py [new file with mode: 0644]
quantum/rootwrap/ryu-agent.py [new file with mode: 0644]
quantum/rootwrap/wrapper.py [new file with mode: 0644]

diff --git a/bin/quantum-rootwrap b/bin/quantum-rootwrap
new file mode 100755 (executable)
index 0000000..dcdccb9
--- /dev/null
@@ -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)
index 8bead52e99a448456b4ab31f8125e306d7a0c086..dd25e078668aad42ca72f44ac87131db6a9f89c9 100644 (file)
@@ -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
index d8d5f21a29b35f2448b209530d255ee977f1fdc1..a7a7f1a6bacc5c18cd3fe924dc54570d502eca44 100644 (file)
@@ -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
index 6d732c9f5a7209ea7de573509b4a0ca82ad3ba21..2d5a2c5d7ce0b56035d1b52b8ae4efa0ce9f1293 100644 (file)
@@ -11,3 +11,8 @@ integration-bridge = br-int
 # openflow-rest-api = <host IP address of ofp rest api service>:<port: 8080>
 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
index 39b8146b58938970e915da1741ba4f0419551da9..3d38bebc505e073988edabaa4d65fb414430190b 100644 (file)
@@ -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,
index a9fa0b9c1c416f7864d3bb1d3ee42a52a4d2df2c..45cd2b62db9bb7abea5868291b705d1dc18904b1 100755 (executable)
@@ -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:
index 94521894076eb508c39d568dd32bea0736df8692..09d30bf8d7c22978a83497c0866d5584eff4e898 100644 (file)
@@ -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))
index ef555a50128c1b7a2a99c25e332bbc107ee6b001..9774c2e8549cec44e575e4c388555b0791f179e3 100755 (executable)
@@ -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}
index ce6179a17882fd6468680fd9bd29a9d8ba61f5ac..ee408f929bcca450b753ff68114efa68850f6095 100644 (file)
@@ -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)
index 11a2e32f02fd33b788a0131a7186bfec5d1ff895..77569fb53e94938900f76c183d15c39e6f08e48f 100755 (executable)
@@ -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 (file)
index 0000000..9bd7a21
--- /dev/null
@@ -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 (file)
index 0000000..48076f4
--- /dev/null
@@ -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 (file)
index 0000000..5421ff1
--- /dev/null
@@ -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 (file)
index 0000000..0c00968
--- /dev/null
@@ -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 (file)
index 0000000..d413ccf
--- /dev/null
@@ -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 (file)
index 0000000..bcb0122
--- /dev/null
@@ -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