From: Dave Lapsley Date: Tue, 21 Feb 2012 07:41:14 +0000 (-0500) Subject: blueprint quantum-ovs-tunnel-agent X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=c812b9dc0345426cbff2a9085cc5acde885a8b1b;p=openstack-build%2Fneutron-build.git blueprint quantum-ovs-tunnel-agent Enhance existing Quantum OVS Plugin with a tunneling agent that enables Hypervisors to be connected via GRE tunnels. The new agent can be enabled/disabled via configuration file and provides backwards compatibility with existing non-tunneling OVS Agent. Change-Id: Id3b79430726b162fcb84f99df152d88a5766328f --- diff --git a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini index 759691aeb..13950ebad 100644 --- a/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini +++ b/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini @@ -1,7 +1,53 @@ [DATABASE] # This line MUST be changed to actually run the plugin. # Example: sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum -sql_connection = sqlite:// +# Replace 127.0.0.1 above with the IP address of the database used by the +# main quantum server. (Leave it as is if the database runs on this host.) +sql_connection=sqlite:// [OVS] +# This enables the new OVSQuantumTunnelAgent which enables tunneling +# between hybervisors. Leave it set to False or omit for legacy behavior. +enable-tunneling = False + +# Do not change this parameter unless you have a good reason to. +# This is the name of the OVS integration bridge. There is one per hypervisor. +# The integration bridge acts as a virtual "patch port". All VM VIFs are +# attached to this bridge and then "patched" according to their network +# connectivity. integration-bridge = br-int + +# Uncomment this line if enable-tunneling is True above. +# In most cases, the default value should be fine. +# tunnel-bridge = br-tun + +# Uncomment this line if enable-tunneling is True above. +# This file contains a list of IP addresses (one per line) that point to +# hypervisors to which tunnels should be connected. It is best to use +# an absolute path to this file. +# remote-ip-file = /opt/stack/remote-ips.txt + +# Uncomment this line if enable-tunneling is True above. +# Set local-ip to be the local IP address of this hypervisor. +# local-ip = 10.0.0.3 + +#----------------------------------------------------------------------------- +# Sample Configurations. +#----------------------------------------------------------------------------- +# +# 1. Without tunneling. +# [DATABASE] +# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum +# [OVS] +# enable-tunneling = False +# integration-bridge = br-int +# +# 2. With tunneling. +# [DATABASE] +# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum +# [OVS] +# enable-tunneling = True +# integration-bridge = br-int +# tunnel-bridge = br-tun +# remote-ip-file = /opt/stack/remote-ips.txt +# local-ip = 10.0.0.3 diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py index 5cfbeac43..ef555a501 100755 --- a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -17,6 +17,7 @@ # @author: Somik Behera, Nicira Networks, Inc. # @author: Brad Hall, Nicira Networks, Inc. # @author: Dan Wendlandt, Nicira Networks, Inc. +# @author: Dave Lapsley, Nicira Networks, Inc. import ConfigParser import logging as LOG @@ -29,9 +30,15 @@ from sqlalchemy.ext.sqlsoup import SqlSoup from subprocess import * +# Global constants. OP_STATUS_UP = "UP" OP_STATUS_DOWN = "DOWN" +# A placeholder for dead vlans. +DEAD_VLAN_TAG = "4095" + +REFRESH_INTERVAL = 2 + # A class to represent a VIF (i.e., a port that has 'iface-id' and 'vif-mac' # attributes set). @@ -114,6 +121,23 @@ class OVSBridge: flow_str = ",".join(all_args) self.run_ofctl("del-flows", [flow_str]) + def add_tunnel_port(self, port_name, remote_ip): + self.run_vsctl(["add-port", self.br_name, port_name]) + self.set_db_attribute("Interface", port_name, "type", "gre") + self.set_db_attribute("Interface", port_name, "options", "remote_ip=" + + remote_ip) + self.set_db_attribute("Interface", port_name, "options", "in_key=flow") + self.set_db_attribute("Interface", port_name, "options", + "out_key=flow") + return self.get_port_ofport(port_name) + + def add_patch_port(self, local_name, remote_name): + self.run_vsctl(["add-port", self.br_name, local_name]) + self.set_db_attribute("Interface", local_name, "type", "patch") + self.set_db_attribute("Interface", local_name, "options", "peer=" + + remote_name) + return self.get_port_ofport(local_name) + def db_get_map(self, table, record, column): str = self.run_vsctl(["get", table, record, column]).rstrip("\n\r") return self.db_str_to_map(str) @@ -169,14 +193,26 @@ class OVSBridge: return edge_ports -class OVSQuantumAgent: +class LocalVLANMapping: + def __init__(self, vlan, lsw_id, vif_ids=None): + if vif_ids is None: + vif_ids = [] + self.vlan = vlan + self.lsw_id = lsw_id + self.vif_ids = vif_ids + + def __str__(self): + return "lv-id = %s ls-id = %s" % (self.vlan, self.lsw_id) + + +class OVSQuantumAgent(object): def __init__(self, integ_br): self.setup_integration_br(integ_br) def port_bound(self, port, vlan_id): self.int_br.set_db_attribute("Port", port.port_name, "tag", - str(vlan_id)) + str(vlan_id)) self.int_br.delete_flows(match="in_port=%s" % port.ofport) def port_unbound(self, port, still_exists): @@ -223,7 +259,7 @@ class OVSQuantumAgent: else: # no binding, put him on the 'dead vlan' self.int_br.set_db_attribute("Port", p.port_name, "tag", - "4095") + DEAD_VLAN_TAG) self.int_br.add_flow(priority=2, match="in_port=%s" % p.ofport, actions="drop") @@ -241,14 +277,14 @@ class OVSQuantumAgent: # If we don't have a binding we have to stick it on # the dead vlan net_id = all_bindings[p.vif_id].network_id - vlan_id = vlan_bindings.get(net_id, "4095") + vlan_id = vlan_bindings.get(net_id, DEAD_VLAN_TAG) self.port_bound(p, vlan_id) if p.vif_id in all_bindings: all_bindings[p.vif_id].op_status = OP_STATUS_UP LOG.info("Adding binding to net-id = %s " \ "for %s on vlan %s" % (new_b, str(p), vlan_id)) - for vif_id in old_vif_ports.keys(): + for vif_id in old_vif_ports: if vif_id not in new_vif_ports: LOG.info("Port Disappeared: %s" % vif_id) if vif_id in old_local_bindings: @@ -260,7 +296,295 @@ class OVSQuantumAgent: old_vif_ports = new_vif_ports old_local_bindings = new_local_bindings db.commit() - time.sleep(2) + time.sleep(REFRESH_INTERVAL) + + +class OVSQuantumTunnelAgent(object): + '''Implements OVS-based tunneling. + + Two local bridges are created: an integration bridge (defaults to 'br-int') + and a tunneling bridge (defaults to 'br-tun'). + + All VM VIFs are plugged into the integration bridge. VMs for a given tenant + share a common "local" VLAN (i.e. not propagated externally). The VLAN id + of this local VLAN is mapped to a Logical Switch (LS) identifier and is + used to differentiate tenant traffic on inter-HV tunnels. + + A mesh of tunnels is created to other Hypervisors in the cloud. These + tunnels originate and terminate on the tunneling bridge of each hypervisor. + + Port patching is done to connect local VLANs on the integration bridge + to inter-hypervisor tunnels on the tunnel bridge. + ''' + + # Lower bound on available vlans. + MIN_VLAN_TAG = 1 + + # Upper bound on available vlans. + MAX_VLAN_TAG = 4094 + + def __init__(self, integ_br, tun_br, remote_ip_file, local_ip): + '''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.available_local_vlans = set( + xrange(OVSQuantumTunnelAgent.MIN_VLAN_TAG, + OVSQuantumTunnelAgent.MAX_VLAN_TAG)) + self.setup_integration_br(integ_br) + self.local_vlan_map = {} + self.setup_tunnel_br(tun_br, remote_ip_file, local_ip) + + def provision_local_vlan(self, net_uuid, lsw_id): + '''Provisions a local VLAN. + + :param net_uuid: the uuid of the network associated with this vlan. + :param lsw_id: the logical switch id of this vlan.''' + if not self.available_local_vlans: + raise Exception("No local VLANs available for ls-id = %s" % lsw_id) + lvid = self.available_local_vlans.pop() + LOG.info("Assigning %s as local vlan for net-id=%s" % (lvid, net_uuid)) + self.local_vlan_map[net_uuid] = LocalVLANMapping(lvid, lsw_id) + + # outbound + self.tun_br.add_flow(priority=4, match="in_port=%s,dl_vlan=%s" % + (self.patch_int_ofport, lvid), + actions="set_tunnel:%s,normal" % (lsw_id)) + + # inbound + self.tun_br.add_flow(priority=3, match="tun_id=%s" % lsw_id, + actions="mod_vlan_vid:%s,output:%s" % (lvid, + self.patch_int_ofport)) + + def reclaim_local_vlan(self, net_uuid, lvm): + '''Reclaim a local VLAN. + + :param net_uuid: the network uuid associated with this vlan. + :param lvm: a LocalVLANMapping object that tracks (vlan, lsw_id, + vif_ids) mapping.''' + LOG.info("reclaming vlan = %s from net-id = %s" % (lvm.vlan, net_uuid)) + self.tun_br.delete_flows(match="tun_id=%s" % lvm.lsw_id) + self.tun_br.delete_flows(match="dl_vlan=%s" % lvm.vlan) + del self.local_vlan_map[net_uuid] + self.available_local_vlans.add(lvm.vlan) + + def port_bound(self, port, net_uuid, lsw_id): + '''Bind port to net_uuid/lsw_id. + + :param port: a VifPort object. + :param net_uuid: the net_uuid this port is to be associated with. + :param lsw_id: the logical switch this port is to be associated with. + ''' + if net_uuid not in self.local_vlan_map: + self.provision_local_vlan(net_uuid, lsw_id) + lvm = self.local_vlan_map[net_uuid] + lvm.vif_ids.append(port.vif_id) + + self.int_br.set_db_attribute("Port", port.port_name, "tag", + str(lvm.vlan)) + self.int_br.delete_flows(match="in_port=%s" % port.ofport) + + def port_unbound(self, port, net_uuid): + '''Unbind port. + + Removes corresponding local vlan mapping object if this is its last + VIF. + + :param port: a VifPort object. + :param net_uuid: the net_uuid this port is associated with.''' + if net_uuid not in self.local_vlan_map: + LOG.info('port_unbound() net_uuid %s not in local_vlan_map' + % net_uuid) + return + lvm = self.local_vlan_map[net_uuid] + + if port.vif_id in lvm.vif_ids: + lvm.vif_ids.remove(port.vif_id) + else: + LOG.info('port_unbound: vid_id %s not in list' % port.vif_id) + + if not lvm.vif_ids: + self.reclaim_local_vlan(net_uuid, lvm) + + def port_dead(self, port): + '''Once a port has no binding, put it on the "dead vlan". + + :param port: a VifPort object.''' + self.int_br.set_db_attribute("Port", port.port_name, "tag", + DEAD_VLAN_TAG) + self.int_br.add_flow(priority=2, + match="in_port=%s" % port.ofport, actions="drop") + + def setup_integration_br(self, integ_br): + '''Setup the integration bridge. + + 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.delete_port("patch-tun") + self.patch_tun_ofport = self.int_br.add_patch_port("patch-tun", + "patch-int") + self.int_br.remove_all_flows() + # switch all traffic using L2 learning + self.int_br.add_flow(priority=1, actions="normal") + + def setup_tunnel_br(self, tun_br, remote_ip_file, local_ip): + '''Setup the tunnel bridge. + + Reads in list of IP addresses. Creates GRE tunnels to each of these + addresses and then clears out existing flows. local_ip is the address + of the local node. A tunnel is not created to this IP address. + + :param tun_br: the name of the tunnel bridge. + :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.reset_bridge() + self.patch_int_ofport = self.tun_br.add_patch_port("patch-int", + "patch-tun") + try: + with open(remote_ip_file, 'r') as f: + remote_ip_list = f.readlines() + clean_ips = (x.rstrip() for x in remote_ip_list) + tunnel_ips = (x for x in clean_ips if x != local_ip and x) + for i, remote_ip in enumerate(tunnel_ips): + self.tun_br.add_tunnel_port("gre-" + str(i), remote_ip) + except Exception, e: + LOG.error("Error configuring tunnels: '%s' %s" + % (remote_ip_file, str(e))) + raise + + self.tun_br.remove_all_flows() + # default drop + self.tun_br.add_flow(priority=1, actions="drop") + + def get_db_port_bindings(self, db): + '''Get database port bindings from central Quantum database. + + The central quantum database 'ovs_quantum' resides on the openstack + mysql server. + + :returns: a dictionary containing port bindings.''' + ports = [] + try: + ports = db.ports.all() + except Exception, e: + LOG.info("Exception accessing db.ports: %s" % e) + + return dict([(port.interface_id, port) for port in ports]) + + def get_db_vlan_bindings(self, db): + '''Get database vlan bindings from central Quantum database. + + The central quantum database 'ovs_quantum' resides on the openstack + mysql server. + + :returns: a dictionary containing vlan bindings.''' + lsw_id_binds = [] + try: + lsw_id_binds.extend(db.vlan_bindings.all()) + except Exception, e: + LOG.info("Exception accessing db.vlan_bindings: %s" % e) + + return dict([(bind.network_id, bind.vlan_id) + for bind in lsw_id_binds]) + + def daemon_loop(self, db): + '''Main processing loop (not currently used). + + :param db: reference to database layer. + ''' + old_local_bindings = {} + old_vif_ports = {} + + while True: + # Get bindings from db. + all_bindings = self.get_db_port_bindings(db) + all_bindings_vif_port_ids = set(all_bindings.keys()) + lsw_id_bindings = self.get_db_vlan_bindings(db) + + # Get bindings from OVS bridge. + vif_ports = self.int_br.get_vif_ports() + new_vif_ports = dict([(p.vif_id, p) for p in vif_ports]) + new_vif_ports_ids = set(new_vif_ports.keys()) + + old_vif_ports_ids = set(old_vif_ports.keys()) + dead_vif_ports_ids = new_vif_ports_ids - all_bindings_vif_port_ids + dead_vif_ports = [new_vif_ports[p] for p in dead_vif_ports_ids] + disappeared_vif_ports_ids = old_vif_ports_ids - new_vif_ports_ids + new_local_bindings_ids = all_bindings_vif_port_ids.intersection( + new_vif_ports_ids) + new_local_bindings = dict([(p, all_bindings.get(p)) + for p in new_vif_ports_ids]) + new_bindings = set((p, old_local_bindings.get(p), + new_local_bindings.get(p)) for p in new_vif_ports_ids) + changed_bindings = set([b for b in new_bindings + if b[2] != b[1]]) + + LOG.debug('all_bindings: %s' % all_bindings) + LOG.debug('lsw_id_bindings: %s' % lsw_id_bindings) + LOG.debug('old_vif_ports_ids: %s' % old_vif_ports_ids) + LOG.debug('dead_vif_ports_ids: %s' % dead_vif_ports_ids) + LOG.debug('old_vif_ports_ids: %s' % old_vif_ports_ids) + LOG.debug('new_local_bindings_ids: %s' % new_local_bindings_ids) + LOG.debug('new_local_bindings: %s' % new_local_bindings) + LOG.debug('new_bindings: %s' % new_bindings) + LOG.debug('changed_bindings: %s' % changed_bindings) + + # Take action. + for p in dead_vif_ports: + LOG.info("No quantum binding for port " + str(p) + + "putting on dead vlan") + self.port_dead(p) + + for b in changed_bindings: + port_id, old_port, new_port = b + p = new_vif_ports[port_id] + if old_port: + old_net_uuid = old_port.network_id + LOG.info("Removing binding to net-id = " + + old_net_uuid + " for " + str(p) + + " added to dead vlan") + self.port_unbound(p, old_net_uuid) + if not new_port: + self.port_dead(p) + + if new_port: + new_net_uuid = new_port.network_id + if new_net_uuid not in lsw_id_bindings: + LOG.warn("No ls-id binding found for net-id '%s'" % + new_net_uuid) + continue + + lsw_id = lsw_id_bindings[new_net_uuid] + try: + self.port_bound(p, new_net_uuid, lsw_id) + LOG.info("Port " + str(p) + " on net-id = " + + new_net_uuid + " bound to " + + str(self.local_vlan_map[new_net_uuid])) + except Exception, e: + LOG.info("Unable to bind Port " + str(p) + + " on netid = " + new_net_uuid + " to " + + str(self.local_vlan_map[new_net_uuid])) + + for vif_id in disappeared_vif_ports_ids: + LOG.info("Port Disappeared: " + vif_id) + old_port = old_local_bindings.get(vif_id) + if old_port: + try: + self.port_unbound(old_vif_ports[vif_id], + old_port.network_id) + except Exception: + LOG.info("Unable to unbind Port " + str(p) + + " on net-id = " + old_port.network_uuid) + + old_vif_ports = new_vif_ports + old_local_bindings = new_local_bindings + time.sleep(REFRESH_INTERVAL) def main(): @@ -285,17 +609,67 @@ def main(): try: config.read(config_file) except Exception, e: - LOG.error("Unable to parse config file \"%s\": %s" % (config_file, - str(e))) + LOG.error("Unable to parse config file \"%s\": %s" + % (config_file, str(e))) + raise e + + # Determine which agent type to use. + enable_tunneling = False + try: + enable_tunneling = config.getboolean("OVS", "enable-tunneling") + except Exception, e: + pass - integ_br = config.get("OVS", "integration-bridge") + # Get common parameters. + try: + integ_br = config.get("OVS", "integration-bridge") + if not len(integ_br): + raise Exception('Empty integration-bridge in configuration file.') - options = {"sql_connection": config.get("DATABASE", "sql_connection")} - db = SqlSoup(options["sql_connection"]) + db_connection_url = config.get("DATABASE", "sql_connection") + if not len(db_connection_url): + raise Exception('Empty db_connection_url in configuration file.') + except Exception, e: + LOG.error("Error parsing common params in config_file: '%s': %s" + % (config_file, str(e))) + sys.exit(1) + + if enable_tunneling: + # Get parameters for OVSQuantumTunnelAgent + try: + # Mandatory parameter. + tun_br = config.get("OVS", "tunnel-bridge") + if not len(tun_br): + raise Exception('Empty tunnel-bridge in configuration file.') + + # Mandatory parameter. + remote_ip_file = config.get("OVS", "remote-ip-file") + if not len(remote_ip_file): + raise Exception('Empty remote-ip-file in configuration file.') + + # Mandatory parameter. + remote_ip_file = config.get("OVS", "remote-ip-file") + local_ip = config.get("OVS", "local-ip") + if not len(local_ip): + raise Exception('Empty local-ip in configuration file.') + except Exception, e: + LOG.error("Error parsing tunnel params in config_file: '%s': %s" + % (config_file, str(e))) + sys.exit(1) + + plugin = OVSQuantumTunnelAgent(integ_br, tun_br, remote_ip_file, + local_ip) + else: + # Get parameters for OVSQuantumAgent. + plugin = OVSQuantumAgent(integ_br) + + # Start everything. + options = {"sql_connection": db_connection_url} + db = SqlSoup(options["sql_connection"]) LOG.info("Connecting to database \"%s\" on %s" % (db.engine.url.database, db.engine.url.host)) - plugin = OVSQuantumAgent(integ_br) + plugin.daemon_loop(db) sys.exit(0) diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 6ae06805d..6da2d8dbb 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -16,6 +16,7 @@ # @author: Somik Behera, Nicira Networks, Inc. # @author: Brad Hall, Nicira Networks, Inc. # @author: Dan Wendlandt, Nicira Networks, Inc. +# @author: Dave Lapsley, Nicira Networks, Inc. import ConfigParser import logging as LOG diff --git a/quantum/plugins/openvswitch/run_tests.py b/quantum/plugins/openvswitch/run_tests.py old mode 100644 new mode 100755 diff --git a/quantum/plugins/openvswitch/tests/unit/remote-ip-file.txt b/quantum/plugins/openvswitch/tests/unit/remote-ip-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/quantum/plugins/openvswitch/tests/unit/test_tunnel.py b/quantum/plugins/openvswitch/tests/unit/test_tunnel.py new file mode 100644 index 000000000..ce6179a17 --- /dev/null +++ b/quantum/plugins/openvswitch/tests/unit/test_tunnel.py @@ -0,0 +1,202 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 Nicira Networks, Inc. +# 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: Dave Lapsley, Nicira Networks, Inc. + +import logging +import mox +import os +import unittest +from agent import ovs_quantum_agent + +LOG = logging.getLogger("quantum.plugins.openvswitch.tests.unit.test_tunnel") +LOG.setLevel(logging.INFO) + +LOCAL_DIR = os.path.dirname(__file__) +REMOTE_IP_FILE = LOCAL_DIR + '/remote-ip-file.txt' + +# Useful global dummy variables. +NET_UUID = '3faeebfe-5d37-11e1-a64b-000c29d5f0a7' +LS_ID = '42' +LV_ID = 42 +LV_IDS = [42, 43] +LVM = ovs_quantum_agent.LocalVLANMapping(LV_ID, LS_ID, LV_IDS) +VIF_ID = '404deaec-5d37-11e1-a64b-000c29d5f0a8' +VIF_MAC = '3c:09:24:1e:78:23' +VIF_PORT = ovs_quantum_agent.VifPort('port', 'ofport', VIF_ID, VIF_MAC, + 'switch') + + +class DummyPort: + def __init__(self, interface_id): + self.interface_id = interface_id + + +class DummyVlanBinding: + def __init__(self, network_id, vlan_id): + self.network_id = network_id + self.vlan_id = vlan_id + + +class TunnelTest(unittest.TestCase): + + def setUp(self): + print LOCAL_DIR + self.mox = mox.Mox() + + self.INT_BRIDGE = 'integration_bridge' + self.TUN_BRIDGE = 'tunnel_bridge' + self.INT_OFPORT = 'PATCH_INT_OFPORT' + 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.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.reset_bridge() + self.mock_tun_bridge.add_patch_port( + 'patch-int', 'patch-tun').AndReturn(self.INT_OFPORT) + self.mock_tun_bridge.remove_all_flows() + self.mock_tun_bridge.add_flow(priority=1, actions='drop') + + def tearDown(self): + self.mox.UnsetStubs() + + def testConstruct(self): + self.mox.ReplayAll() + + b = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + self.mox.VerifyAll() + + def testProvisionLocalVlan(self): + match_string = 'in_port=%s,dl_vlan=%s' % (self.INT_OFPORT, LV_ID) + action_string = 'set_tunnel:%s,normal' % LS_ID + self.mock_tun_bridge.add_flow(priority=4, match=match_string, + actions=action_string) + + match_string = 'tun_id=%s' % LS_ID + action_string = 'mod_vlan_vid:%s,output:%s' % (LV_ID, self.INT_OFPORT) + self.mock_tun_bridge.add_flow(priority=3, match=match_string, + actions=action_string) + + self.mox.ReplayAll() + + a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + a.available_local_vlans = set([LV_ID]) + a.provision_local_vlan(NET_UUID, LS_ID) + self.mox.VerifyAll() + + def testReclaimLocalVlan(self): + match_string = 'tun_id=%s' % LVM.lsw_id + self.mock_tun_bridge.delete_flows(match=match_string) + + match_string = 'dl_vlan=%s' % LVM.vlan + self.mock_tun_bridge.delete_flows(match=match_string) + + self.mox.ReplayAll() + a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + a.available_local_vlans = set() + a.local_vlan_map[NET_UUID] = LVM + a.reclaim_local_vlan(NET_UUID, LVM) + self.assertTrue(LVM.vlan in a.available_local_vlans) + self.mox.VerifyAll() + + def testPortBound(self): + self.mock_int_bridge.set_db_attribute('Port', VIF_PORT.port_name, + 'tag', str(LVM.vlan)) + self.mock_int_bridge.delete_flows(match='in_port=%s' % VIF_PORT.ofport) + + self.mox.ReplayAll() + a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + a.local_vlan_map[NET_UUID] = LVM + a.port_bound(VIF_PORT, NET_UUID, LS_ID) + self.mox.VerifyAll() + + def testPortUnbound(self): + self.mox.ReplayAll() + a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + a.available_local_vlans = set([LV_ID]) + a.local_vlan_map[NET_UUID] = LVM + a.port_unbound(VIF_PORT, NET_UUID) + self.mox.VerifyAll() + + def testPortDead(self): + self.mock_int_bridge.set_db_attribute('Port', VIF_PORT.port_name, + 'tag', ovs_quantum_agent.DEAD_VLAN_TAG) + + match_string = 'in_port=%s' % VIF_PORT.ofport + self.mock_int_bridge.add_flow(priority=2, match=match_string, + actions='drop') + + self.mox.ReplayAll() + a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + a.available_local_vlans = set([LV_ID]) + a.local_vlan_map[NET_UUID] = LVM + a.port_dead(VIF_PORT) + self.mox.VerifyAll() + + def testDbBindings(self): + db = self.mox.CreateMockAnything() + db.ports = self.mox.CreateMockAnything() + interface_ids = ['interface-id-%d' % x for x in range(3)] + db.ports.all().AndReturn([DummyPort(x) for x in interface_ids]) + + db.vlan_bindings = self.mox.CreateMockAnything() + vlan_bindings = [ + ['network-id-%d' % x, 'vlan-id-%d' % x] for x in range(3)] + db.vlan_bindings.all().AndReturn( + [DummyVlanBinding(*x) for x in vlan_bindings]) + + self.mox.ReplayAll() + a = ovs_quantum_agent.OVSQuantumTunnelAgent(self.INT_BRIDGE, + self.TUN_BRIDGE, + REMOTE_IP_FILE, + '10.0.0.1') + + all_bindings = a.get_db_port_bindings(db) + lsw_id_bindings = a.get_db_vlan_bindings(db) + + for interface_id, port in all_bindings.iteritems(): + self.assertTrue(interface_id in interface_ids) + + for network_id, vlan_id in lsw_id_bindings.iteritems(): + self.assertTrue(network_id in [x[0] for x in vlan_bindings]) + self.assertTrue(vlan_id in [x[1] for x in vlan_bindings]) + + self.mox.VerifyAll() diff --git a/tools/pip-requires b/tools/pip-requires index 572e82853..fcddb1102 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -3,6 +3,7 @@ PasteDeploy==1.5.0 Routes>=1.12.3 eventlet>=0.9.12 lxml==2.3 +mox==0.5.3 python-gflags==1.3 simplejson sqlalchemy