]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Update rootwrap; track changes in nova/cinder
authorJohn Dunning <jrd@jrd.org>
Tue, 14 Aug 2012 18:31:47 +0000 (14:31 -0400)
committerJohn Dunning <jrd@jrd.org>
Thu, 30 Aug 2012 20:55:00 +0000 (16:55 -0400)
Fix bug 1037815

Summary: Copy/paste the essential parts of the rootwrap
  mechanism from nova/cinder into quantum.  This includes
  the core changes to filter.py and wrapper.py which deal
  with loading filters from files pointed to by
  rootwrap.conf
Detailed changes:
  Transliterate the old rootwrap/*-agent.py files to
  new format, and put the results in etc/quantum/rootwrap.d
  Delete the *-agent.py files.
  Add conf to point to etc/quantum/rootwrap.d
  Add a unit test cribbed from nova to exercise the filter
  mechanism
  Add a unit test to exercise the actual filtered execution
Note that as written, this patch does not set the default
  execute mechanism (in the agent .ini files) to rootwrap,
  leaves it as sudo.  That can be done in a followon
  change, or in distro specific packaging.
Note also that there is still work to do around finishing
  and testing the filter specs themselves.  We've decided
  that that is out of scope for this patch.

Change-Id: I9aba6adc5ba40b6145be5fa38c5ece3b666ae5ca

26 files changed:
bin/quantum-rootwrap
etc/dhcp_agent.ini
etc/l3_agent.ini
etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini
etc/quantum/plugins/nec/nec.ini
etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
etc/quantum/plugins/ryu/ryu.ini
etc/quantum/rootwrap.d/dhcp.filters [new file with mode: 0644]
etc/quantum/rootwrap.d/iptables-firewall.filters [new file with mode: 0644]
etc/quantum/rootwrap.d/l3.filters [new file with mode: 0644]
etc/quantum/rootwrap.d/linuxbridge-plugin.filters [new file with mode: 0644]
etc/quantum/rootwrap.d/nec-plugin.filters [new file with mode: 0644]
etc/quantum/rootwrap.d/openvswitch-plugin.filters [new file with mode: 0644]
etc/quantum/rootwrap.d/ryu-plugin.filters [new file with mode: 0644]
etc/rootwrap.conf [new file with mode: 0644]
quantum/rootwrap/dhcp-agent.py [deleted file]
quantum/rootwrap/filters.py
quantum/rootwrap/iptables-firewall-agent.py [deleted file]
quantum/rootwrap/linuxbridge-agent.py [deleted file]
quantum/rootwrap/openvswitch-agent.py [deleted file]
quantum/rootwrap/ryu-agent.py [deleted file]
quantum/rootwrap/wrapper.py
quantum/tests/etc/rootwrap.d/quantum.test.filters [new file with mode: 0644]
quantum/tests/unit/_test_rootwrap_exec.py [new file with mode: 0644]
quantum/tests/unit/test_rootwrap.py [new file with mode: 0644]
setup.py

index dcdccb9a38069ad68997d639f47e008b8002a296..bb7fbc0d5dd3201ccc32865bcf54dee45674a7ac 100755 (executable)
 
 """Root wrapper for Quantum
 
-   Uses modules in quantum.rootwrap containing filters for commands
-   that quantum agents are allowed to run as another user.
+   Filters which commands quantum is allowed to run as another user.
 
-   To switch to using this, you should:
-   * Set "--root_helper=sudo quantum-rootwrap" in the agents config file.
-   * Allow quantum to run quantum-rootwrap as root in quantum_sudoers:
-     quantum ALL = (root) NOPASSWD: /usr/bin/quantum-rootwrap
-     (all other commands can be removed from this file)
+   To use this, you should set the following in quantum.conf and the
+   various .ini files for the agent plugins:
+   root_helper=sudo quantum-rootwrap /etc/quantum/rootwrap.conf
 
+   You also need to let the quantum user run quantum-rootwrap as root in
+     /etc/sudoers:
+   quantum ALL = (root) NOPASSWD: /usr/bin/quantum-rootwrap
+                                  /etc/quantum/rootwrap.conf *
+
+   Filter specs live in /etc/quantum/rootwrap.d/*.filters, or
+   other locations pointed to by /etc/quantum/rootwrap.conf.
    To make allowed commands node-specific, your packaging should only
-   install quantum/rootwrap/quantum-*-agent.py on compute nodes where
-   agents that need root privileges are run.
+   install apropriate .filters for commands which are needed on each
+   node.
 """
 
+import ConfigParser
 import os
 import subprocess
 import sys
@@ -39,16 +44,30 @@ import sys
 
 RC_UNAUTHORIZED = 99
 RC_NOCOMMAND = 98
+RC_BADCONFIG = 97
+
 
 if __name__ == '__main__':
     # Split arguments, require at least a command
     execname = sys.argv.pop(0)
-    if len(sys.argv) == 0:
+    # argv[0] required; path to conf file
+    if len(sys.argv) < 2:
         print "%s: %s" % (execname, "No command specified")
         sys.exit(RC_NOCOMMAND)
 
+    configfile = sys.argv.pop(0)
     userargs = sys.argv[:]
 
+    # Load configuration
+    config = ConfigParser.RawConfigParser()
+    config.read(configfile)
+    try:
+        filters_path = config.get("DEFAULT", "filters_path").split(",")
+        filters = None
+    except ConfigParser.Error:
+        print "%s: Incorrect configuration file: %s" % (execname, configfile)
+        sys.exit(RC_BADCONFIG)
+
     # Add ../ to sys.path to allow running from branch
     possible_topdir = os.path.normpath(os.path.join(os.path.abspath(execname),
                                                     os.pardir, os.pardir))
@@ -58,7 +77,7 @@ if __name__ == '__main__':
     from quantum.rootwrap import wrapper
 
     # Execute command if it matches any of the loaded filters
-    filters = wrapper.load_filters()
+    filters = wrapper.load_filters(filters_path)
     filtermatch = wrapper.match_filter(filters, userargs)
     if filtermatch:
         obj = subprocess.Popen(filtermatch.get_command(userargs),
index 8ecc9d0f9684a6a02f213889395a1f910dfdeda1..efb540e8ec7f60abc745e78a3b84276c1fbb9bb7 100644 (file)
@@ -25,3 +25,8 @@ dhcp_driver = quantum.agent.linux.dhcp.Dnsmasq
 # Allow overlapping IP (Must have kernel build with CONFIG_NET_NS=y and
 # iproute2 package that supports namespaces).
 # use_namespaces = True
+
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
+root_helper = sudo
index b119c8fdecc3e7afcbb5acce88e28f6c8037ce75..121c4b47c9c40f187aa2740a1a7d6301f94725d9 100644 (file)
@@ -2,7 +2,7 @@
 # Show debugging output in log (sets DEBUG log level output)
 # debug = True
 
-# L3 requires that an inteface driver be set.  Choose the one that best
+# L3 requires that an interface driver be set.  Choose the one that best
 # matches your plugin.
 
 # OVS
@@ -17,3 +17,7 @@ admin_tenant_name = %SERVICE_TENANT_NAME%
 admin_user = %SERVICE_USER%
 admin_password = %SERVICE_PASSWORD%
 
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
+root_helper = sudo
index 239d1f92fcfbdb74221771cef5362da055d90c37..b571cc0a6e775ff88cf0574a2a18d7a317d44977 100644 (file)
@@ -29,8 +29,9 @@ reconnect_interval = 2
 [AGENT]
 # Agent's polling interval in seconds
 polling_interval = 2
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
-root_helper = sudo
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
+root_helper = "sudo"
 # Use RPC messaging to interface between agent and plugin
 # rpc = True
index e21a2b7c9509a317afd9a5e11fb8b954d337b4e7..9dc9f6cdc67de2f5c51368072063bb775fa5cba8 100644 (file)
@@ -24,8 +24,9 @@ integration_bridge = br-int
 [AGENT]
 # Agent's polling interval in seconds
 polling_interval = 2
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
 root_helper = sudo
 
 [OFC]
index 2c77db535ec6cd147ee155a1b699dab5b5a56644..178b49a7227e96ab8c6e7e3b35d679b7cca90187 100644 (file)
@@ -38,8 +38,9 @@ tunnel_bridge = br-tun
 [AGENT]
 # Agent's polling interval in seconds
 polling_interval = 2
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
 root_helper = sudo
 
 #-----------------------------------------------------------------------------
index a90e8466b9b27e2dd9da3346b9f63f59554f6370..42e5525de7eba068b208770dd513cbfd4ee40bec 100644 (file)
@@ -13,6 +13,7 @@ openflow_controller = 127.0.0.1:6633
 openflow_rest_api = 127.0.0.1:8080
 
 [AGENT]
-# Change to "sudo quantum-rootwrap" to limit commands that can be run
-# as root.
+# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
+# root filter facility.
+# Change to "sudo" to skip the filtering and just run the comand directly
 root_helper = sudo
diff --git a/etc/quantum/rootwrap.d/dhcp.filters b/etc/quantum/rootwrap.d/dhcp.filters
new file mode 100644 (file)
index 0000000..7a9fa8a
--- /dev/null
@@ -0,0 +1,22 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# dhcp-agent
+ip_exec_dnsmasq: DnsmasqFilter, /sbin/ip, root
+dnsmasq: DnsmasqFilter, /sbin/dnsmasq, root
+dnsmasq_usr: DnsmasqFilter, /usr/sbin/dnsmasq, root
+# dhcp-agent uses kill as well, that's handled by the generic KillFilter
+# it looks like these are the only signals needed, per
+# quantum/agent/linux/dhcp.py
+kill_dnsmasq: KillFilter, root, /sbin/dnsmasq, -9, -HUP
+kill_dnsmasq_usr: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
+
+# dhcp-agent uses cat
+cat: RegExpFilter, /bin/cat, root, cat, /proc/\d+/cmdline
diff --git a/etc/quantum/rootwrap.d/iptables-firewall.filters b/etc/quantum/rootwrap.d/iptables-firewall.filters
new file mode 100644 (file)
index 0000000..2049e0e
--- /dev/null
@@ -0,0 +1,21 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# quantum/agent/linux/iptables_manager.py
+#   "iptables-save", ...
+iptables-save: CommandFilter, /sbin/iptables-save, root
+iptables-restore: CommandFilter, /sbin/iptables-restore, root
+ip6tables-save: CommandFilter, /sbin/ip6tables-save, root
+ip6tables-restore: CommandFilter, /sbin/ip6tables-restore, root
+
+# quantum/agent/linux/iptables_manager.py
+#   "iptables", "-A", ...
+iptables: CommandFilter, /sbin/iptables, root
+ip6tables: CommandFilter, /sbin/ip6tables, root
diff --git a/etc/quantum/rootwrap.d/l3.filters b/etc/quantum/rootwrap.d/l3.filters
new file mode 100644 (file)
index 0000000..e471217
--- /dev/null
@@ -0,0 +1,28 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# l3_agent
+sysctl: CommandFilter, /sbin/sysctl, root
+
+# ip_lib
+ip: CommandFilter, /sbin/ip, root
+ip_usr: CommandFilter, /usr/sbin/ip, root
+
+# ovs_lib (if OVSInterfaceDriver is used)
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
+
+# iptables_manager
+iptables-save: CommandFilter, /sbin/iptables-save, root
+iptables-restore: CommandFilter, /sbin/iptables-restore, root
+ip6tables-save: CommandFilter, /sbin/ip6tables-save, root
+ip6tables-restore: CommandFilter, /sbin/ip6tables-restore, root
diff --git a/etc/quantum/rootwrap.d/linuxbridge-plugin.filters b/etc/quantum/rootwrap.d/linuxbridge-plugin.filters
new file mode 100644 (file)
index 0000000..591f69e
--- /dev/null
@@ -0,0 +1,17 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# linuxbridge-agent
+# unclear whether both variants are necessary, but I'm transliterating
+# from the old mechanism
+brctl: CommandFilter, /sbin/brctl, root
+brctl_usr: CommandFilter, /usr/sbin/brctl, root
+ip: CommandFilter, /sbin/ip, root
+ip_usr: CommandFilter, /usr/sbin/ip, root
diff --git a/etc/quantum/rootwrap.d/nec-plugin.filters b/etc/quantum/rootwrap.d/nec-plugin.filters
new file mode 100644 (file)
index 0000000..6d8f9c2
--- /dev/null
@@ -0,0 +1,15 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# nec_quantum_agent
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
diff --git a/etc/quantum/rootwrap.d/openvswitch-plugin.filters b/etc/quantum/rootwrap.d/openvswitch-plugin.filters
new file mode 100644 (file)
index 0000000..bcb9527
--- /dev/null
@@ -0,0 +1,23 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# openvswitch-agent
+# unclear whether both variants are necessary, but I'm transliterating
+# from the old mechanism
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
+ovs-ofctl: CommandFilter, /bin/ovs-ofctl, root
+ovs-ofctl_usr: CommandFilter, /usr/bin/ovs-ofctl, root
+ovs-ofctl_sbin: CommandFilter, /sbin/ovs-ofctl, root
+ovs-ofctl_sbin_usr: CommandFilter, /usr/sbin/ovs-ofctl, root
+xe: CommandFilter, /sbin/xe, root
+xe_usr: CommandFilter, /usr/sbin/xe, root
diff --git a/etc/quantum/rootwrap.d/ryu-plugin.filters b/etc/quantum/rootwrap.d/ryu-plugin.filters
new file mode 100644 (file)
index 0000000..696c7d3
--- /dev/null
@@ -0,0 +1,25 @@
+# quantum-rootwrap command filters for nodes on which quantum is
+# expected to control network
+#
+# This file should be owned by (and only-writeable by) the root user
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# ryu-agent
+# unclear whether both variants are necessary, but I'm transliterating
+# from the old mechanism
+
+# quantum/plugins/ryu/agent/ryu_quantum_agent.py:
+#   "ovs-vsctl", "--timeout=2", ...
+ovs-vsctl: CommandFilter, /bin/ovs-vsctl, root
+ovs-vsctl_usr: CommandFilter, /usr/bin/ovs-vsctl, root
+ovs-vsctl_sbin: CommandFilter, /sbin/ovs-vsctl, root
+ovs-vsctl_sbin_usr: CommandFilter, /usr/sbin/ovs-vsctl, root
+
+# quantum/plugins/ryu/agent/ryu_quantum_agent.py:
+#   "xe", "vif-param-get", ...
+xe: CommandFilter, /bin/xe, root
+xe_usr: CommandFilter, /usr/bin/xe, root
diff --git a/etc/rootwrap.conf b/etc/rootwrap.conf
new file mode 100644 (file)
index 0000000..0a1048e
--- /dev/null
@@ -0,0 +1,4 @@
+[DEFAULT]
+# List of directories to load filter definitions from (separated by ',').
+# These directories MUST all be only writeable by root !
+filters_path=/etc/quantum/rootwrap.d,/usr/share/quantum/filters
diff --git a/quantum/rootwrap/dhcp-agent.py b/quantum/rootwrap/dhcp-agent.py
deleted file mode 100644 (file)
index 2ba63a1..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 Openstack, LLC.
-# All Rights Reserved.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-
-from quantum.rootwrap import filters
-
-filterlist = [
-    # quantum/agent/linux/dhcp.py:
-    #   "dnsmasq", "--no-hosts", ...
-    filters.CommandFilter("/usr/sbin/dnsmasq", "root"),
-    filters.KillFilter("/bin/kill", "root", [''], ['/usr/sbin/dnsmasq']),
-]
index 510d10f39105afb7ab7e13927d4855f352fc0247..8b3b89ba2c2539a8c14953850a9974f73ee67761 100644 (file)
@@ -71,10 +71,28 @@ class RegExpFilter(CommandFilter):
 class DnsmasqFilter(CommandFilter):
     """Specific filter for the dnsmasq call (which includes env)"""
 
+    def is_dnsmasq_cmd(self, argv):
+        if (argv[0] == "dnsmasq"):
+            return True
+        return False
+
+    def is_ip_netns_cmd(self, argv):
+        if ((argv[0] == "ip") and
+            (argv[1] == "netns") and
+            (argv[2] == "exec")):
+            return True
+        return False
+
     def match(self, userargs):
-        if ((userargs[0].startswith("FLAGFILE=") and
-             userargs[1].startswith("NETWORK_ID=") and
-             userargs[2] == "dnsmasq")):
+        """This matches the combination of the leading env
+        vars, plus either "dnsmasq" (for the case where we're
+        not using netns) or "ip" "netns" "exec" <foo> "dnsmasq"
+        (for the case where we are)"""
+        if ((userargs[0].startswith("QUANTUM_RELAY_SOCKET_PATH=") and
+             userargs[1].startswith("QUANTUM_NETWORK_ID=") and
+             (self.is_dnsmasq_cmd(userargs[2:]) or
+              (self.is_ip_netns_cmd(userargs[2:]) and
+               self.is_dnsmasq_cmd(userargs[6:]))))):
             return True
         return False
 
@@ -83,39 +101,46 @@ class DnsmasqFilter(CommandFilter):
 
     def get_environment(self, userargs):
         env = os.environ.copy()
-        env['FLAGFILE'] = userargs[0].split('=')[-1]
-        env['NETWORK_ID'] = userargs[1].split('=')[-1]
+        env['QUANTUM_RELAY_SOCKET_PATH'] = userargs[0].split('=')[-1]
+        env['QUANTUM_NETWORK_ID'] = userargs[1].split('=')[-1]
         return env
 
 
 class KillFilter(CommandFilter):
     """Specific filter for the kill calls.
-       1st argument is a list of accepted signals (emptystring means no signal)
-       2nd argument is a list of accepted affected executables.
+       1st argument is the user to run /bin/kill under
+       2nd argument is the location of the affected executable
+       Subsequent arguments list the accepted signals (if any)
 
        This filter relies on /proc to accurately determine affected
        executable, so it will only work on procfs-capable systems (not OSX).
     """
 
+    def __init__(self, *args):
+        super(KillFilter, self).__init__("/bin/kill", *args)
+
     def match(self, userargs):
         if userargs[0] != "kill":
             return False
         args = list(userargs)
         if len(args) == 3:
+            # this means we're asking for a specific signal
             signal = args.pop(1)
-            if signal not in self.args[0]:
+            if signal not in self.args[1:]:
                 # Requested signal not in accepted list
                 return False
-        elif len(args) != 2:
-            # Incorrect number of arguments
-            return False
-        elif '' not in self.args[0]:
-            # No signal, but list doesn't include empty string
-            return False
+        else:
+            if len(args) != 2:
+                # Incorrect number of arguments
+                return False
+            if len(self.args) > 1:
+                # No signal requested, but filter requires specific signal
+                return False
+
         try:
             command = os.readlink("/proc/%d/exe" % int(args[1]))
-            if command not in self.args[1]:
-                # Affected executable not in accepted list
+            if command != self.args[0]:
+                # Affected executable doesn't match
                 return False
         except (ValueError, OSError):
             # Incorrect PID
diff --git a/quantum/rootwrap/iptables-firewall-agent.py b/quantum/rootwrap/iptables-firewall-agent.py
deleted file mode 100755 (executable)
index 83f7147..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2012 Locaweb.
-# All Rights Reserved.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License");
-#    you may not use this file except in compliance with the License.
-#    You may obtain a copy of the License at
-#
-#        http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS,
-#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#    See the License for the specific language governing permissions and
-#    limitations under the License.
-#
-# @author: Juliano Martinez, Locaweb.
-
-from quantum.rootwrap import filters
-
-filterlist = [
-    # quantum/agent/linux/iptables_manager.py
-    #   "iptables-save", ...
-    filters.CommandFilter("/sbin/iptables-save", "root"),
-    filters.CommandFilter("/sbin/iptables-restore", "root"),
-    filters.CommandFilter("/sbin/ip6tables-save", "root"),
-    filters.CommandFilter("/sbin/ip6tables-restore", "root"),
-
-    # quantum/agent/linux/iptables_manager.py
-    #   "iptables", "-A", ...
-    filters.CommandFilter("/sbin/iptables", "root"),
-    filters.CommandFilter("/sbin/ip6tables", "root"),
-]
diff --git a/quantum/rootwrap/linuxbridge-agent.py b/quantum/rootwrap/linuxbridge-agent.py
deleted file mode 100644 (file)
index 326f088..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 Openstack, LLC.
-# All Rights Reserved.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-
-from quantum.rootwrap import filters
-
-filterlist = [
-    # quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py:
-    #   'brctl', 'addbr', bridge_name
-    #   'brctl', 'addif', bridge_name, interface
-    #   'brctl', 'addif', bridge_name, tap_device_name
-    #   'brctl', 'delbr', bridge_name
-    #   'brctl', 'delif', bridge_name, interface_name
-    #   'brctl', 'delif', current_bridge_name, ...
-    #   'brctl', 'setfd', bridge_name, ...
-    #   'brctl', 'stp', bridge_name, 'off'
-    filters.CommandFilter("/usr/sbin/brctl", "root"),
-    filters.CommandFilter("/sbin/brctl", "root"),
-
-    # quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py:
-    #   'ip', 'link', 'add', 'link', ...
-    #   'ip', 'link', 'delete', interface
-    #   'ip', 'link', 'set', bridge_name, 'down'
-    #   'ip', 'link', 'set', bridge_name, 'up'
-    #   'ip', 'link', 'set', interface, 'down'
-    #   'ip', 'link', 'set', interface, 'up'
-    #   'ip', 'link', 'show', 'dev', device
-    #   'ip', 'tuntap'
-    #   'ip', 'tuntap'
-    filters.CommandFilter("/usr/sbin/ip", "root"),
-    filters.CommandFilter("/sbin/ip", "root"),
-]
diff --git a/quantum/rootwrap/openvswitch-agent.py b/quantum/rootwrap/openvswitch-agent.py
deleted file mode 100644 (file)
index 9d4d2ef..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 Openstack, LLC.
-# All Rights Reserved.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-
-from quantum.rootwrap import filters
-
-filterlist = [
-    # quantum/plugins/openvswitch/agent/ovs_quantum_agent.py:
-    #   "ovs-vsctl", "--timeout=2", ...
-    filters.CommandFilter("/usr/bin/ovs-vsctl", "root"),
-    filters.CommandFilter("/bin/ovs-vsctl", "root"),
-
-    # quantum/plugins/openvswitch/agent/ovs_quantum_agent.py:
-    #   "ovs-ofctl", cmd, self.br_name, args
-    filters.CommandFilter("/usr/bin/ovs-ofctl", "root"),
-    filters.CommandFilter("/bin/ovs-ofctl", "root"),
-
-    # quantum/plugins/openvswitch/agent/ovs_quantum_agent.py:
-    #   "xe", "vif-param-get", ...
-    filters.CommandFilter("/usr/bin/xe", "root"),
-    filters.CommandFilter("/usr/sbin/xe", "root"),
-]
diff --git a/quantum/rootwrap/ryu-agent.py b/quantum/rootwrap/ryu-agent.py
deleted file mode 100644 (file)
index 54581d1..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 Openstack, LLC.
-# All Rights Reserved.
-#
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-
-from quantum.rootwrap import filters
-
-filterlist = [
-    # quantum/plugins/ryu/agent/ryu_quantum_agent.py:
-    #   "ovs-vsctl", "--timeout=2", ...
-    filters.CommandFilter("/usr/bin/ovs-vsctl", "root"),
-    filters.CommandFilter("/bin/ovs-vsctl", "root"),
-
-    # quantum/plugins/ryu/agent/ryu_quantum_agent.py:
-    #   "xe", "vif-param-get", ...
-    filters.CommandFilter("/usr/bin/xe", "root"),
-    filters.CommandFilter("/usr/sbin/xe", "root"),
-]
index 45c414f9f6ed24ab57097633903ff46be0ad6bcb..58b45bbc79921b62068f308b9557eb7d7c79edaf 100644 (file)
 #    under the License.
 
 
+import ConfigParser
 import os
+import string
 import sys
 
+# this import has the effect of defining global var "filters",
+# referenced by build_filter(), below.  It gets set up by
+# quantum-rootwrap, when we load_filters().
+from quantum.rootwrap import filters
 
-FILTERS_MODULES = ['quantum.rootwrap.linuxbridge-agent',
-                   'quantum.rootwrap.openvswitch-agent',
-                   'quantum.rootwrap.ryu-agent',
-                   'quantum.rootwrap.iptables-firewall-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 build_filter(class_name, *args):
+    """Returns a filter object of class class_name"""
+    if not hasattr(filters, class_name):
+        # TODO(jrd): Log the error (whenever quantum-rootwrap has a log file)
+        return None
+    filterclass = getattr(filters, class_name)
+    return filterclass(*args)
+
+
+def load_filters(filters_path):
+    """Load filters from a list of directories"""
+    filterlist = []
+    for filterdir in filters_path:
+        if not os.path.isdir(filterdir):
+            continue
+        for filterfile in os.listdir(filterdir):
+            filterconfig = ConfigParser.RawConfigParser()
+            filterconfig.read(os.path.join(filterdir, filterfile))
+            for (name, value) in filterconfig.items("Filters"):
+                filterdefinition = [string.strip(s) for s in value.split(',')]
+                newfilter = build_filter(*filterdefinition)
+                if newfilter is None:
+                    continue
+                filterlist.append(newfilter)
+    return filterlist
 
 
 def match_filter(filters, userargs):
diff --git a/quantum/tests/etc/rootwrap.d/quantum.test.filters b/quantum/tests/etc/rootwrap.d/quantum.test.filters
new file mode 100644 (file)
index 0000000..dc02011
--- /dev/null
@@ -0,0 +1,12 @@
+# quantum-rootwrap command filters for the unit test
+
+# this file goes with quantum/tests/unit/_test_rootwrap_exec.py.
+# See the comments there about how to run that unit tests
+
+# format seems to be
+# cmd-name: filter-name, raw-command, user, args
+
+[Filters]
+
+# a test filter for the RootwrapTest unit test
+bash: CommandFilter, /usr/bin/bash, root
diff --git a/quantum/tests/unit/_test_rootwrap_exec.py b/quantum/tests/unit/_test_rootwrap_exec.py
new file mode 100644 (file)
index 0000000..66fe434
--- /dev/null
@@ -0,0 +1,78 @@
+# Copyright 2012 OpenStack LLC
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+import unittest
+import mock
+from quantum.agent.linux import utils
+import os
+import logging
+
+
+LOG = logging.getLogger('quantum.tests.database_stubs')
+
+
+class RootwrapTestExec(unittest.TestCase):
+    """Simple unit test to test the basic rootwrap mechanism
+
+    Essentially hello-world.  Just run a command as root and check that
+    it actually *did* run as root, and generated the right output.
+
+    NB that this is named _test_rootwrap so as not to get run by default
+    from scripts like tox.  That's because it actually executes a sudo'ed
+    command, and that won't work in the automated test environment, at
+    least as it stands today.  To run this, rename it to
+    test_rootwrap.py, or run it by hand.
+    """
+
+    def setUp(self):
+        self.cwd = os.getcwd() + "/../../.."
+        # stuff a stupid bash script into /tmp, so that the next
+        # method can execute it.
+        self.test_file = '/tmp/rootwrap-test.sh'
+        with open(self.test_file, 'w') as f:
+            f.write('#!/bin/bash\n')
+            f.write('ID=`id | sed \'s/uid=//\' | sed \'s/(.*//\' `\n')
+            f.write("echo $ID $1\
+\" Now is the time for all good men to come \
+to the aid of their party.\"\n")
+        # we need a temporary conf file, pointing into pwd for the filter
+        # specs. there's probably a better way to do this, but I couldn't
+        # figure it out.  08/15/12 -- jrd
+        self.conf_file = '/tmp/rootwrap.conf'
+        with open(self.conf_file, 'w') as f:
+            f.write("# temporary conf file for rootwrap-test, " +
+                    "generated by test_rootwrap.py\n")
+            f.write("[DEFAULT]\n")
+            f.write("filters_path=" + self.cwd +
+                    "/quantum/tests/etc/rootwrap.d/")
+        # now set the root helper to sudo our rootwrap script,
+        #  with the new conf
+        self.root_helper = "sudo " + self.cwd + "/bin/quantum-rootwrap "
+        self.root_helper += self.conf_file
+
+    def runTest(self):
+        try:
+            result = utils.execute(["bash", self.test_file, 'arg'],
+                                   self.root_helper)
+            self.assertEqual(result,
+                             "0 arg Now is the time for all good men to \
+come to the aid of their party.")
+        except Exception, ex:
+            LOG.exception("Losing in rootwrap test")
+
+    def tearDown(self):
+        os.remove(self.test_file)
+        os.remove(self.conf_file)
diff --git a/quantum/tests/unit/test_rootwrap.py b/quantum/tests/unit/test_rootwrap.py
new file mode 100644 (file)
index 0000000..a238e73
--- /dev/null
@@ -0,0 +1,115 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    Copyright 2011 OpenStack LLC
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import os
+import subprocess
+
+from quantum.rootwrap import filters
+from quantum.rootwrap import wrapper
+import unittest
+
+
+class RootwrapTestCase(unittest.TestCase):
+
+    def setUp(self):
+        super(RootwrapTestCase, self).setUp()
+        self.filters = [
+            filters.RegExpFilter("/bin/ls", "root", 'ls', '/[a-z]+'),
+            filters.CommandFilter("/usr/bin/foo_bar_not_exist", "root"),
+            filters.RegExpFilter("/bin/cat", "root", 'cat', '/[a-z]+'),
+            filters.CommandFilter("/nonexistant/cat", "root"),
+            filters.CommandFilter("/bin/cat", "root")]  # Keep this one last
+
+    def test_RegExpFilter_match(self):
+        usercmd = ["ls", "/root"]
+        filtermatch = wrapper.match_filter(self.filters, usercmd)
+        self.assertFalse(filtermatch is None)
+        self.assertEqual(filtermatch.get_command(usercmd),
+                         ["/bin/ls", "/root"])
+
+    def test_RegExpFilter_reject(self):
+        usercmd = ["ls", "root"]
+        filtermatch = wrapper.match_filter(self.filters, usercmd)
+        self.assertTrue(filtermatch is None)
+
+    def test_missing_command(self):
+        valid_but_missing = ["foo_bar_not_exist"]
+        invalid = ["foo_bar_not_exist_and_not_matched"]
+        filtermatch = wrapper.match_filter(self.filters, valid_but_missing)
+        self.assertTrue(filtermatch is not None)
+        filtermatch = wrapper.match_filter(self.filters, invalid)
+        self.assertTrue(filtermatch is None)
+
+    def test_DnsmasqFilter(self):
+        usercmd = ['QUANTUM_RELAY_SOCKET_PATH=A', 'QUANTUM_NETWORK_ID=foobar',
+                   'dnsmasq', 'foo']
+        f = filters.DnsmasqFilter("/usr/bin/dnsmasq", "root")
+        self.assertTrue(f.match(usercmd))
+        self.assertEqual(f.get_command(usercmd), ['/usr/bin/dnsmasq', 'foo'])
+        env = f.get_environment(usercmd)
+        self.assertEqual(env.get('QUANTUM_RELAY_SOCKET_PATH'), 'A')
+        self.assertEqual(env.get('QUANTUM_NETWORK_ID'), 'foobar')
+
+    def test_KillFilter(self):
+        p = subprocess.Popen(["/bin/sleep", "5"])
+        f = filters.KillFilter("root", "/bin/sleep", "-9", "-HUP")
+        f2 = filters.KillFilter("root", "/usr/bin/sleep", "-9", "-HUP")
+        usercmd = ['kill', '-ALRM', p.pid]
+        # Incorrect signal should fail
+        self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+        usercmd = ['kill', p.pid]
+        # Providing no signal should fail
+        self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+        # Providing matching signal should be allowed
+        usercmd = ['kill', '-9', p.pid]
+        self.assertTrue(f.match(usercmd) or f2.match(usercmd))
+
+        f = filters.KillFilter("root", "/bin/sleep")
+        f2 = filters.KillFilter("root", "/usr/bin/sleep")
+        usercmd = ['kill', os.getpid()]
+        # Our own PID does not match /bin/sleep, so it should fail
+        self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+        usercmd = ['kill', 999999]
+        # Nonexistant PID should fail
+        self.assertFalse(f.match(usercmd) or f2.match(usercmd))
+        usercmd = ['kill', p.pid]
+        # Providing no signal should work
+        self.assertTrue(f.match(usercmd) or f2.match(usercmd))
+
+    def test_KillFilter_no_raise(self):
+        """Makes sure ValueError from bug 926412 is gone"""
+        f = filters.KillFilter("root", "")
+        # Providing anything other than kill should be False
+        usercmd = ['notkill', 999999]
+        self.assertFalse(f.match(usercmd))
+        # Providing something that is not a pid should be False
+        usercmd = ['kill', 'notapid']
+        self.assertFalse(f.match(usercmd))
+
+    def test_ReadFileFilter(self):
+        goodfn = '/good/file.name'
+        f = filters.ReadFileFilter(goodfn)
+        usercmd = ['cat', '/bad/file']
+        self.assertFalse(f.match(['cat', '/bad/file']))
+        usercmd = ['cat', goodfn]
+        self.assertEqual(f.get_command(usercmd), ['/bin/cat', goodfn])
+        self.assertTrue(f.match(usercmd))
+
+    def test_skips(self):
+        # Check that all filters are skipped and that the last matches
+        usercmd = ["cat", "/"]
+        filtermatch = wrapper.match_filter(self.filters, usercmd)
+        self.assertTrue(filtermatch is self.filters[-1])
index 1a59f0c638133ed85c0b792ba92defcca6cc932b..7abb848b84af469f9e550c2c5bd2e55eee4af693 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -39,6 +39,7 @@ EagerResources = [
 ]
 
 ProjectScripts = [
+    'bin/quantum-rootwrap',
 ]
 
 config_path = 'etc/quantum/'
@@ -54,6 +55,7 @@ nec_plugin_config_path = 'etc/quantum/plugins/nec'
 DataFiles = [
     (config_path,
         ['etc/quantum.conf',
+         'etc/rootwrap.conf',
          'etc/api-paste.ini',
          'etc/policy.json',
          'etc/dhcp_agent.ini']),