]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
blueprint quantum-ovs-tunnel-agent
authorDave Lapsley <dlapsley@nicira.com>
Tue, 21 Feb 2012 07:41:14 +0000 (02:41 -0500)
committerDave Lapsley <dlapsley@nicira.com>
Thu, 23 Feb 2012 22:45:22 +0000 (17:45 -0500)
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

etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
quantum/plugins/openvswitch/ovs_quantum_plugin.py
quantum/plugins/openvswitch/run_tests.py [changed mode: 0644->0755]
quantum/plugins/openvswitch/tests/unit/remote-ip-file.txt [new file with mode: 0644]
quantum/plugins/openvswitch/tests/unit/test_tunnel.py [new file with mode: 0644]
tools/pip-requires

index 759691aeb7f57110b84aa561b320168bfa770013..13950ebadf3ba5d922b20222c3ad7f275709c1c5 100644 (file)
@@ -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
index 5cfbeac43eee2ce87c6ad97519293131e743630e..ef555a50128c1b7a2a99c25e332bbc107ee6b001 100755 (executable)
@@ -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)
index 6ae06805d0fecbc40e5aec691d7a863124d12ef7..6da2d8dbb62c3e9731eaebc43c6336ebd8b2b84d 100644 (file)
@@ -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
old mode 100644 (file)
new mode 100755 (executable)
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 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/openvswitch/tests/unit/test_tunnel.py b/quantum/plugins/openvswitch/tests/unit/test_tunnel.py
new file mode 100644 (file)
index 0000000..ce6179a
--- /dev/null
@@ -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()
index 572e82853416e77f26bdc96fe07da147a02da1c0..fcddb110236052aaec63787f67fbae315c53a0bb 100644 (file)
@@ -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