Skip to content

Commit

Permalink
Add root_helper to quantum agents.
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Bob Kukura committed Mar 14, 2012
1 parent f88a1f7 commit a06b316
Show file tree
Hide file tree
Showing 16 changed files with 508 additions and 38 deletions.
73 changes: 73 additions & 0 deletions 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)
3 changes: 3 additions & 0 deletions etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini
Expand Up @@ -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
9 changes: 9 additions & 0 deletions etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
Expand Up @@ -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.
#-----------------------------------------------------------------------------
Expand All @@ -41,6 +46,8 @@ integration-bridge = br-int
# [OVS]
# enable-tunneling = False
# integration-bridge = br-int
# [AGENT]
# root_helper = sudo
#
# 2. With tunneling.
# [DATABASE]
Expand All @@ -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
5 changes: 5 additions & 0 deletions etc/quantum/plugins/ryu/ryu.ini
Expand Up @@ -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
13 changes: 12 additions & 1 deletion quantum/plugins/linuxbridge/README
Expand Up @@ -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,
Expand Down
21 changes: 14 additions & 7 deletions quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py
Expand Up @@ -30,6 +30,7 @@
import logging as LOG
import MySQLdb
import os
import shlex
import signal
import sqlite3
import sys
Expand All @@ -53,16 +54,18 @@


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
Expand Down Expand Up @@ -287,12 +290,15 @@ def delete_vlan(self, interface):

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):
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down
13 changes: 9 additions & 4 deletions quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py
Expand Up @@ -22,6 +22,7 @@
import unittest
import sys
import os
import shlex
import signal
from subprocess import *

Expand Down Expand Up @@ -392,20 +393,24 @@ def setUp(self):
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))
Expand Down
30 changes: 19 additions & 11 deletions quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
Expand Up @@ -21,6 +21,7 @@

import ConfigParser
import logging as LOG
import shlex
import sys
import time
import signal
Expand Down Expand Up @@ -57,15 +58,17 @@ def __str__(self):


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):
Expand Down Expand Up @@ -207,7 +210,8 @@ def __str__(self):

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):
Expand All @@ -220,7 +224,7 @@ def port_unbound(self, port, still_exists):
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")
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -423,7 +429,7 @@ def setup_integration_br(self, integ_br):
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")
Expand All @@ -442,7 +448,7 @@ def setup_tunnel_br(self, tun_br, remote_ip_file, local_ip):
: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")
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit a06b316

Please sign in to comment.