]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Initial cut of openvswitch plugin
authorBrad Hall <bhall@nicira.com>
Sat, 4 Jun 2011 03:56:32 +0000 (20:56 -0700)
committerBrad Hall <bhall@nicira.com>
Sat, 4 Jun 2011 03:56:32 +0000 (20:56 -0700)
quantum/plugins/openvswitch/Makefile [new file with mode: 0644]
quantum/plugins/openvswitch/README [new file with mode: 0644]
quantum/plugins/openvswitch/__init__.py [new file with mode: 0644]
quantum/plugins/openvswitch/agent/install.sh [new file with mode: 0644]
quantum/plugins/openvswitch/agent/ovs_quantum_agent.py [new file with mode: 0755]
quantum/plugins/openvswitch/agent/set_external_ids.sh [new file with mode: 0755]
quantum/plugins/openvswitch/ovs_db.py [new file with mode: 0644]
quantum/plugins/openvswitch/ovs_models.py [new file with mode: 0644]
quantum/plugins/openvswitch/ovs_quantum_plugin.ini [new file with mode: 0644]
quantum/plugins/openvswitch/ovs_quantum_plugin.py [new file with mode: 0644]

diff --git a/quantum/plugins/openvswitch/Makefile b/quantum/plugins/openvswitch/Makefile
new file mode 100644 (file)
index 0000000..7cd5e62
--- /dev/null
@@ -0,0 +1,30 @@
+QUANTUM_PATH=../../../
+
+# TODO(bgh): DIST_DIR and target for plugin
+
+AGENT_DIST_DIR=ovs_quantum_agent
+AGENT_DIST_TARBALL=ovs_quantum_agent.tgz
+
+agent-dist: distclean
+       mkdir $(AGENT_DIST_DIR)
+       cp agent/*.py $(AGENT_DIST_DIR)
+       cp agent/*.sh $(AGENT_DIST_DIR)
+       cp README $(AGENT_DIST_DIR)
+       cp ovs_quantum_plugin.ini $(AGENT_DIST_DIR)
+       tar -zcvf $(AGENT_DIST_TARBALL) $(AGENT_DIST_DIR)/
+       @echo "Agent tarball created: $(AGENT_DIST_TARBALL)"
+       @echo "See README for installation details"
+
+all:
+
+clean:
+       $(find . -name *.pyc | xargs rm)
+
+distclean:
+       -rm -rf $(AGENT_DIST_DIR)
+       -rm -f $(AGENT_DIST_TARBALL)
+
+check:
+       PYTHONPATH=$(QUANTUM_PATH):. python ovs_quantum_plugin.py
+
+PHONY: agent-dist check clean distclean
diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README
new file mode 100644 (file)
index 0000000..ee2248a
--- /dev/null
@@ -0,0 +1,37 @@
+To Run:
+
+1) On the "Openstack Controller" host:
+
+MySQL should be installed on the host, and all plugins and clients must be
+configured with access to the database.
+
+To prep mysql, run:
+
+mysql -u root -p -e "create database ovs_naas"
+
+2) Edit the configuration file (src/ovs/plugins/ovs_quantum_plugin.ini)
+
+- Make sure it matches your mysql configuration.  This file must be updated
+  with the addresses and credentials to access the database.
+
+3) Create the agent distribution tarball
+
+$ make agent-dist
+
+4) Copy the resulting tarball to your xenserver
+
+5) Unpack the tarball and run install.sh.  This will install all of the
+necessary pieces into /etc/xapi.d/plugins.
+
+6) Run the agent (example below):
+
+# /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini
+
+7) Run ovs_quantum_plugin.py via the quantum plugin framework cli.
+
+- Edit quantum/plugins.ini to point to where the plugin and configuration
+  files live
+
+$ PYTHONPATH=$HOME/src/quantum-framework/quantum:$PYTHONPATH python quantum/cli.py
+
+This will show all of the available commands.
diff --git a/quantum/plugins/openvswitch/__init__.py b/quantum/plugins/openvswitch/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/openvswitch/agent/install.sh b/quantum/plugins/openvswitch/agent/install.sh
new file mode 100644 (file)
index 0000000..2f2b038
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+CONF_FILE=/etc/xapi.d/plugins/ovs_quantum_plugin.ini
+
+if [ ! -d /etc/xapi.d/plugins ]; then
+       echo "Am I on a xenserver? I can't find the plugins directory!"
+       exit 1
+fi
+
+# Make sure we have mysql-python
+rpm -qa | grep MYyQL-python >/dev/null 2>&1
+if [ $? -ne 0 ]; then
+       echo "MySQL-python not found; installing."
+       yum -y install MySQL-python
+       if [ $? -ne 0 ]; then
+               echo "Failed to install MYSQL-python; agent will not work."
+               exit 1
+       fi
+fi
+
+cp ovs_quantum_agent.py /etc/xapi.d/plugins
+cp ovs_quantum_plugin.ini /etc/xapi.d/plugins
+cp set_external_ids.sh /etc/xapi.d/plugins
+
+xe network-list name-label="integration-bridge" | grep xapi >/dev/null 2>&1
+if [ $? -ne 0 ]; then
+       echo "No integration bridge found.  Creating."
+       xe network-create name-label="integration-bridge"
+fi
+
+BR=$(xe network-list name-label="integration-bridge" | grep "bridge.*:" | awk '{print $4}')
+CONF_BR=$(grep integration-bridge ${CONF_FILE} | cut -d= -f2)
+if [ "X$BR" != "X$CONF_BR" ]; then
+       echo "Integration bridge doesn't match configuration file; fixing."
+       sed -i -e "s/^integration-bridge =.*$/integration-bridge = ${BR}/g" $CONF_FILE
+fi
+
+echo "Make sure to edit: $CONF_FILE"
diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py
new file mode 100755 (executable)
index 0000000..9769ab9
--- /dev/null
@@ -0,0 +1,272 @@
+#!/usr/bin/env python
+
+import ConfigParser
+import logging as LOG
+import MySQLdb
+import os
+import sys
+import time
+
+from optparse import OptionParser
+from subprocess import *
+
+# A class to represent a VIF (i.e., a port that has 'iface-id' and 'vif-mac'
+# attributes set).
+class VifPort:
+    def __init__(self, port_name, ofport, vif_id, vif_mac, switch):
+        self.port_name = port_name
+        self.ofport = ofport
+        self.vif_id = vif_id
+        self.vif_mac = vif_mac
+        self.switch = switch
+    def __str__(self):
+        return "iface-id=" + self.vif_id + ", vif_mac=" + \
+          self.vif_mac + ", port_name=" + self.port_name + \
+          ", ofport=" + self.ofport + ", bridge name = " + self.switch.br_name
+
+class OVSBridge:
+    def __init__(self, br_name):
+        self.br_name = br_name
+
+    def run_cmd(self, args):
+        # LOG.debug("## running command: " + " ".join(args))
+        return Popen(args, stdout=PIPE).communicate()[0]
+
+    def run_vsctl(self, args):
+        full_args = ["ovs-vsctl" ] + args
+        return self.run_cmd(full_args)
+
+    def reset_bridge(self):
+        self.run_vsctl([ "--" , "--if-exists", "del-br", self.br_name])
+        self.run_vsctl(["add-br", self.br_name])
+
+    def delete_port(self, port_name):
+        self.run_vsctl([ "--" , "--if-exists", "del-port", self.br_name,
+          port_name])
+
+    def set_db_attribute(self, table_name, record, column, value):
+        args = [ "set", table_name, record, "%s=%s" % (column,value) ]
+        self.run_vsctl(args)
+
+    def clear_db_attribute(self, table_name,record, column):
+        args = [ "clear", table_name, record, column ]
+        self.run_vsctl(args)
+
+    def run_ofctl(self, cmd, args):
+        full_args = ["ovs-ofctl", cmd, self.br_name ] + args
+        return self.run_cmd(full_args)
+
+    def remove_all_flows(self):
+        self.run_ofctl("del-flows", [])
+
+    def get_port_ofport(self, port_name):
+        return self.db_get_val("Interface", port_name, "ofport")
+
+    def add_flow(self,**dict):
+        if "actions" not in dict:
+            raise Exception("must specify one or more actions")
+        if "priority" not in dict:
+            dict["priority"] = "0"
+
+        flow_str = "priority=%s" % dict["priority"]
+        if "match" in dict:
+            flow_str += "," + dict["match"]
+        flow_str += ",actions=%s" % (dict["actions"])
+        self.run_ofctl("add-flow", [ flow_str ] )
+
+    def delete_flows(self,**dict):
+        all_args = []
+        if "priority" in dict:
+            all_args.append("priority=%s" % dict["priority"])
+        if "match" in dict:
+            all_args.append(dict["match"])
+        if "actions" in dict:
+            all_args.append("actions=%s" % (dict["actions"]))
+        flow_str = ",".join(all_args)
+        self.run_ofctl("del-flows", [ flow_str ] )
+
+    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)
+
+    def db_get_val(self, table, record, column):
+        return self.run_vsctl([ "get" , table, record, column ]).rstrip("\n\r")
+
+    def db_str_to_map(self, full_str):
+        list = full_str.strip("{}").split(", ")
+        ret = {}
+        for e in list:
+            if e.find("=") == -1:
+                continue
+            arr = e.split("=")
+            ret[arr[0]] = arr[1].strip("\"")
+        return ret
+
+    def get_port_name_list(self):
+        res = self.run_vsctl([ "list-ports", self.br_name])
+        return res.split("\n")[0:-1]
+
+    def get_port_stats(self, port_name):
+        return self.db_get_map("Interface", port_name, "statistics")
+
+    # returns a VIF object for each VIF port
+    def get_vif_ports(self):
+        edge_ports = []
+        port_names = self.get_port_name_list()
+        for name in port_names:
+            external_ids = self.db_get_map("Interface",name,"external_ids")
+            if "iface-id" in external_ids and "attached-mac" in external_ids:
+                ofport = self.db_get_val("Interface",name,"ofport")
+                p = VifPort(name, ofport, external_ids["iface-id"],
+                        external_ids["attached-mac"], self)
+                edge_ports.append(p)
+            else:
+                # iface-id might not be set.  See if we can figure it out and
+                # set it here.
+                external_ids = self.db_get_map("Interface",name,"external_ids")
+                if "attached-mac" not in external_ids:
+                    continue
+                vif_uuid = external_ids.get("xs-vif-uuid", "")
+                if len(vif_uuid) == 0:
+                    continue
+                LOG.debug("iface-id not set, got vif-uuid: %s" % vif_uuid)
+                res = os.popen("xe vif-param-get param-name=other-config uuid=%s | grep nicira-iface-id | awk '{print $2}'" % vif_uuid).readline()
+                res = res.strip()
+                if len(res) == 0:
+                    continue
+                external_ids["iface-id"] = res
+                LOG.info("Setting interface \"%s\" iface-id to \"%s\"" % (name, res))
+                self.set_db_attribute("Interface", name,
+                  "external-ids:iface-id", res)
+                ofport = self.db_get_val("Interface",name,"ofport")
+                p = VifPort(name, ofport, external_ids["iface-id"],
+                        external_ids["attached-mac"], self)
+                edge_ports.append(p)
+        return edge_ports
+
+class OVSNaaSPlugin:
+    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))
+
+    def port_unbound(self, port, still_exists):
+        if 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.remove_all_flows()
+        # drop all traffic on the 'dead vlan'
+        self.int_br.add_flow(priority=2, match="dl_vlan=4095", actions="drop")
+        # switch all other traffic using L2 learning
+        self.int_br.add_flow(priority=1, actions="normal")
+        # FIXME send broadcast everywhere, regardless of tenant
+        #int_br.add_flow(priority=3, match="dl_dst=ff:ff:ff:ff:ff:ff", actions="normal")
+
+    def daemon_loop(self, conn):
+        self.local_vlan_map = {}
+        old_local_bindings = {}
+        old_vif_ports = {}
+
+        while True:
+            cursor = conn.cursor()
+            cursor.execute("SELECT * FROM network_bindings")
+            rows = cursor.fetchall()
+            cursor.close()
+            all_bindings = {}
+            for r in rows:
+                all_bindings[r[2]] = r[1]
+
+            cursor = conn.cursor()
+            cursor.execute("SELECT * FROM vlan_bindings")
+            rows = cursor.fetchall()
+            cursor.close()
+            vlan_bindings = {}
+            for r in rows:
+                vlan_bindings[r[1]] = r[0]
+
+            new_vif_ports = {}
+            new_local_bindings = {}
+            vif_ports = self.int_br.get_vif_ports()
+            for p in vif_ports:
+                new_vif_ports[p.vif_id] = p
+                if p.vif_id in all_bindings:
+                    new_local_bindings[p.vif_id] = all_bindings[p.vif_id]
+                else:
+                    # no binding, put him on the 'dead vlan'
+                    self.int_br.set_db_attribute("Port", p.port_name, "tag",
+                      "4095")
+                old_b = old_local_bindings.get(p.vif_id,None)
+                new_b = new_local_bindings.get(p.vif_id,None)
+                if old_b != new_b:
+                    if old_b is not None:
+                        LOG.info("Removing binding to net-id = %s for %s"
+                          % (old_b, str(p)))
+                        self.port_unbound(p, True)
+                    if new_b is not None:
+                        LOG.info("Adding binding to net-id = %s for %s" \
+                          % (new_b, str(p)))
+                        # If we don't have a binding we have to stick it on
+                        # the dead vlan
+                        vlan_id = vlan_bindings.get(all_bindings[p.vif_id],
+                          "4095")
+                        self.port_bound(p, vlan_id)
+            for vif_id in old_vif_ports.keys():
+                if vif_id not in new_vif_ports:
+                    LOG.info("Port Disappeared: %s" % vif_id)
+                    if vif_id in old_local_bindings:
+                        old_b = old_local_bindings[vif_id]
+                        self.port_unbound(old_vif_ports[vif_id], False)
+
+            old_vif_ports = new_vif_ports
+            old_local_bindings = new_local_bindings
+            self.int_br.run_cmd(["bash",
+              "/etc/xapi.d/plugins/set_external_ids.sh"])
+            time.sleep(2)
+
+if __name__ == "__main__":
+    usagestr = "%prog [OPTIONS] <config file>"
+    parser = OptionParser(usage=usagestr)
+    parser.add_option("-v", "--verbose", dest="verbose",
+      action="store_true", default=False, help="turn on verbose logging")
+
+    options, args = parser.parse_args()
+
+    if options.verbose:
+        LOG.basicConfig(level=LOG.DEBUG)
+    else:
+        LOG.basicConfig(level=LOG.WARN)
+
+    if len(args) != 1:
+        parser.print_help()
+        sys.exit(1)
+
+    config_file = args[0]
+    config = ConfigParser.ConfigParser()
+    try:
+        config.read(config_file)
+    except Exception, e:
+        LOG.error("Unable to parse config file \"%s\": %s" % (config_file,
+          str(e)))
+
+    integ_br = config.get("OVS", "integration-bridge")
+
+    db_name = config.get("DATABASE", "name")
+    db_user = config.get("DATABASE", "user")
+    db_pass = config.get("DATABASE", "pass")
+    db_host = config.get("DATABASE", "host")
+    conn = None
+    try:
+        LOG.info("Connecting to database \"%s\" on %s" % (db_name, db_host))
+        conn = MySQLdb.connect(host=db_host, user=db_user,
+          passwd=db_pass, db=db_name)
+        plugin = OVSNaaSPlugin(integ_br)
+        plugin.daemon_loop(conn)
+    finally:
+        if conn:
+            conn.close()
+
+    sys.exit(0)
diff --git a/quantum/plugins/openvswitch/agent/set_external_ids.sh b/quantum/plugins/openvswitch/agent/set_external_ids.sh
new file mode 100755 (executable)
index 0000000..f56dc4b
--- /dev/null
@@ -0,0 +1,15 @@
+
+VIFLIST=`xe vif-list params=uuid --minimal | sed s/,/" "/g`
+for VIF_UUID in $VIFLIST; do
+DEVICE_NUM=`xe vif-list params=device uuid=$VIF_UUID --minimal`
+  VM_NAME=`xe vif-list params=vm-name-label uuid=$VIF_UUID --minimal`
+  NAME="$VM_NAME-eth$DEVICE_NUM"
+  echo "Vif: $VIF_UUID is '$NAME'"
+  xe vif-param-set uuid=$VIF_UUID other-config:nicira-iface-id="$NAME"
+done
+
+ps auxw | grep -v grep | grep ovs-xapi-sync > /dev/null 2>&1
+if [ $? -eq 0 ]; then
+       killall -HUP ovs-xapi-sync
+fi
+
diff --git a/quantum/plugins/openvswitch/ovs_db.py b/quantum/plugins/openvswitch/ovs_db.py
new file mode 100644 (file)
index 0000000..8b41062
--- /dev/null
@@ -0,0 +1,52 @@
+from sqlalchemy.orm import exc
+
+import quantum.db.api as db
+import quantum.db.models as models
+import ovs_models
+
+def get_vlans():
+    session = db.get_session()
+    try:
+        bindings = session.query(ovs_models.VlanBinding).\
+          all()
+    except exc.NoResultFound:
+        return []
+    res = []
+    for x in bindings:
+       res.append((x.vlan_id, x.network_id))
+    return res
+
+def add_vlan_binding(vlanid, netid):
+    session = db.get_session()
+    binding = ovs_models.VlanBinding(vlanid, netid)
+    session.add(binding)
+    session.flush()
+    return binding.vlan_id
+
+def remove_vlan_binding(netid):
+    session = db.get_session()
+    try:
+        binding = session.query(ovs_models.VlanBinding).\
+          filter_by(network_id=netid).\
+          one()
+        session.delete(binding)
+    except exc.NoResultFound:
+            pass
+    session.flush()
+
+def update_network_binding(netid, ifaceid):
+    session = db.get_session()
+    # Add to or delete from the bindings table
+    if ifaceid == None:
+        try:
+            binding = session.query(ovs_models.NetworkBinding).\
+              filter_by(network_id=netid).\
+              one()
+            session.delete(binding)
+        except exc.NoResultFound:
+            raise Exception("No binding found with network_id = %s" % netid)
+    else:
+        binding = ovs_models.NetworkBinding(netid, ifaceid)
+        session.add(binding)
+
+    session.flush()
diff --git a/quantum/plugins/openvswitch/ovs_models.py b/quantum/plugins/openvswitch/ovs_models.py
new file mode 100644 (file)
index 0000000..7e8b2f5
--- /dev/null
@@ -0,0 +1,38 @@
+import uuid
+
+from sqlalchemy import Column, Integer, String, ForeignKey
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relation
+
+from quantum.db.models import BASE
+
+class NetworkBinding(BASE):
+    """Represents a binding of network_id, vif_id"""
+    __tablename__ = 'network_bindings'
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    network_id = Column(String(255))
+    vif_id = Column(String(255))
+
+    def __init__(self, network_id, vif_id):
+        self.network_id = network_id
+        self.vif_id = vif_id
+
+    def __repr__(self):
+        return "<NetworkBinding(%s,%s)>" % \
+          (self.network_id, self.vif_id)
+
+class VlanBinding(BASE):
+    """Represents a binding of network_id, vlan_id"""
+    __tablename__ = 'vlan_bindings'
+
+    vlan_id = Column(Integer, primary_key=True)
+    network_id = Column(String(255))
+
+    def __init__(self, vlan_id, network_id):
+        self.network_id = network_id
+        self.vlan_id = vlan_id
+
+    def __repr__(self):
+        return "<VlanBinding(%s,%s)>" % \
+          (self.vlan_id, self.network_id)
diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.ini b/quantum/plugins/openvswitch/ovs_quantum_plugin.ini
new file mode 100644 (file)
index 0000000..0c75b3f
--- /dev/null
@@ -0,0 +1,9 @@
+[DATABASE]
+name = ovs_naas
+user = root
+pass = foobar
+host = 127.0.0.1
+port = 3306
+
+[OVS]
+integration-bridge = xapi1
diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py
new file mode 100644 (file)
index 0000000..e8111c6
--- /dev/null
@@ -0,0 +1,296 @@
+import ConfigParser
+import logging as LOG
+import os
+import sys
+import unittest
+
+from quantum.quantum_plugin_base import QuantumPluginBase
+import quantum.db.api as db
+import ovs_db
+
+# TODO(bgh): Make sure we delete from network bindings when deleting a port,
+# network, etc.
+
+CONF_FILE="ovs_quantum_plugin.ini"
+
+LOG.basicConfig(level=LOG.DEBUG)
+LOG.getLogger("ovs_quantum_plugin")
+
+def find_config(basepath):
+    for root, dirs, files in os.walk(basepath):
+        if CONF_FILE in files:
+            return os.path.join(root, CONF_FILE)
+    return None
+
+class VlanMap(object):
+    vlans = {}
+    def __init__(self):
+        for x in xrange(2, 4094):
+            self.vlans[x] = None
+    def set(self, vlan_id, network_id):
+        self.vlans[vlan_id] = network_id
+    def acquire(self, network_id):
+        for x in xrange(2, 4094):
+            if self.vlans[x] == None:
+                self.vlans[x] = network_id
+                # LOG.debug("VlanMap::acquire %s -> %s" % (x, network_id))
+                return x
+        raise Exception("No free vlans..")
+    def get(self, vlan_id):
+        return self.vlans[vlan_id]
+    def release(self, network_id):
+        for x in self.vlans.keys():
+            if self.vlans[x] == network_id:
+                self.vlans[x] = None
+                # LOG.debug("VlanMap::release %s" % (x))
+                return
+        raise Exception("No vlan found with network \"%s\"" % network_id)
+
+class OVSQuantumPlugin(QuantumPluginBase):
+    def __init__(self, configfile=None):
+        config = ConfigParser.ConfigParser()
+        if configfile == None:
+            if os.path.exists(CONF_FILE):
+                configfile = CONF_FILE
+            else:
+                configfile = find_config(os.path.abspath(os.path.dirname(__file__)))
+        if configfile == None:
+            raise Exception("Configuration file \"%s\" doesn't exist" %
+              (configfile))
+        LOG.info("Using configuration file: %s" % configfile)
+        config.read(configfile)
+        LOG.debug("Config: %s" % config)
+
+        DB_NAME = config.get("DATABASE", "name")
+        DB_USER = config.get("DATABASE", "user")
+        DB_PASS = config.get("DATABASE", "pass")
+        DB_HOST = config.get("DATABASE", "host")
+        options = {"sql_connection": "mysql://%s:%s@%s/%s" % (DB_USER,
+          DB_PASS, DB_HOST, DB_NAME)}
+        db.configure_db(options)
+
+        self.vmap = VlanMap()
+        # Populate the map with anything that is already present in the
+        # database
+        vlans = ovs_db.get_vlans()
+        for x in vlans:
+            vlan_id, network_id = x
+            # LOG.debug("Adding already populated vlan %s -> %s" % (vlan_id, network_id))
+            self.vmap.set(vlan_id, network_id)
+
+    def get_all_networks(self, tenant_id):
+        nets = []
+        for x in db.network_list(tenant_id):
+            LOG.debug("Adding network: %s" % x.uuid)
+            d = {}
+            d["net-id"] = str(x.uuid)
+            d["net-name"] = x.name
+            nets.append(d)
+        return nets
+
+    def create_network(self, tenant_id, net_name):
+        d = {}
+        try:
+             res = db.network_create(tenant_id, net_name)
+             LOG.debug("Created newtork: %s" % res)
+        except Exception, e:
+            LOG.error("Error: %s" % str(e))
+            return d
+        d["net-id"] = str(res.uuid)
+        d["net-name"] = res.name
+        vlan_id = self.vmap.acquire(str(res.uuid))
+        ovs_db.add_vlan_binding(vlan_id, str(res.uuid))
+        return d
+
+    def delete_network(self, tenant_id, net_id):
+        net = db.network_destroy(net_id)
+        d = {}
+        d["net-id"] = net.uuid
+        ovs_db.remove_vlan_binding(net_id)
+        self.vmap.release(net_id)
+        return d
+
+    def get_network_details(self, tenant_id, net_id):
+        network = db.network_get(net_id)
+        d = {}
+        d["net-id"] = str(network.uuid)
+        d["net-name"] = network.name
+        d["net-ports"] = self.get_all_ports(tenant_id, net_id)
+        return d
+
+    def rename_network(self, tenant_id, net_id, new_name):
+        try:
+            net = db.network_rename(net_id, tenant_id, new_name)
+        except Exception, e:
+            raise Exception("Failed to rename network: %s" % str(e))
+        d = {}
+        d["net-id"] = str(net.uuid)
+        d["net-name"] = net.name
+        return d
+
+    def get_all_ports(self, tenant_id, net_id):
+        ids = []
+        ports = db.port_list(net_id)
+        for x in ports:
+            LOG.debug("Appending port: %s" % x.uuid)
+            d = {}
+            d["port-id"] = str(x.uuid)
+            ids.append(d)
+        return ids
+
+    def create_port(self, tenant_id, net_id, port_state=None):
+        LOG.debug("Creating port with network_id: %s" % net_id)
+        port = db.port_create(net_id)
+        d = {}
+        d["port-id"] = str(port.uuid)
+        LOG.debug("-> %s" % (port.uuid))
+        return d
+
+    def delete_port(self, tenant_id, net_id, port_id):
+        try:
+            port = db.port_destroy(port_id)
+        except Exception, e:
+            raise Exception("Failed to delete port: %s" % str(e))
+        d = {}
+        d["port-id"] = str(port.uuid)
+        return d
+
+    def update_port(self, tenant_id, net_id, port_id, port_state):
+        """
+        Updates the state of a port on the specified Virtual Network.
+        """
+        LOG.debug("update_port() called\n")
+        port = db.port_get(port_id)
+        port['port-state'] = port_state
+        return port
+
+    def get_port_details(self, tenant_id, net_id, port_id):
+        port = db.port_get(port_id)
+        rv = {"port-id": port.uuid, "attachment": port.interface_id,
+          "net-id": port.network_id, "port-state": "UP"}
+        return rv
+
+    def get_all_attached_interfaces(self, tenant_id, net_id):
+        ports = db.port_list(net_id)
+        ifaces = []
+        for p in ports:
+            ifaces.append(p.interface_id)
+        return ifaces
+
+    def plug_interface(self, tenant_id, net_id, port_id, remote_iface_id):
+        db.port_set_attachment(port_id, remote_iface_id)
+        ovs_db.update_network_binding(net_id, remote_iface_id)
+
+    def unplug_interface(self, tenant_id, net_id, port_id):
+        db.port_set_attachment(port_id, "None")
+        ovs_db.update_network_binding(net_id, remote_iface_id)
+
+    def get_interface_details(self, tenant_id, net_id, port_id):
+        res = db.port_get(port_id)
+        return res.interface_id
+
+class VlanMapTest(unittest.TestCase):
+    def setUp(self):
+        self.vmap = VlanMap()
+    def tearDown(self):
+        pass
+    def testAddVlan(self):
+        vlan_id = self.vmap.acquire("foobar")
+        self.assertTrue(vlan_id == 2)
+    def testReleaseVlan(self):
+        vlan_id = self.vmap.acquire("foobar")
+        self.vmap.release("foobar")
+        self.assertTrue(self.vmap.get(vlan_id) == None)
+
+# TODO(bgh): Make the tests use a sqlite database instead of mysql
+class OVSPluginTest(unittest.TestCase):
+    def setUp(self):
+        self.quantum = OVSQuantumPlugin()
+        self.tenant_id = "testtenant"
+
+    def testCreateNetwork(self):
+        net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
+        self.assertTrue(net1["net-name"] == "plugin_test1")
+
+    def testGetNetworks(self):
+        net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
+        net2 = self.quantum.create_network(self.tenant_id, "plugin_test2")
+        nets = self.quantum.get_all_networks(self.tenant_id)
+        count = 0
+        for x in nets:
+            print x
+            if "plugin_test" in x["net-name"]:
+                count += 1
+        self.assertTrue(count == 2)
+
+    def testDeleteNetwork(self):
+        net = self.quantum.create_network(self.tenant_id, "plugin_test1")
+        self.quantum.delete_network(self.tenant_id, net["net-id"])
+        nets = self.quantum.get_all_networks(self.tenant_id)
+        count = 0
+        for x in nets:
+            print x
+            if "plugin_test" in x["net-name"]:
+                count += 1
+        self.assertTrue(count == 0)
+
+    def testRenameNetwork(self):
+        net = self.quantum.create_network(self.tenant_id, "plugin_test1")
+        net = self.quantum.rename_network(self.tenant_id, net["net-id"],
+          "plugin_test_renamed")
+        self.assertTrue(net["net-name"] == "plugin_test_renamed")
+
+    def testCreatePort(self):
+        net1 = self.quantum.create_network(self.tenant_id, "plugin_test1")
+        port = self.quantum.create_port(self.tenant_id, net1["net-id"])
+        ports = self.quantum.get_all_ports(self.tenant_id, net1["net-id"])
+        count = 0
+        for p in ports:
+            count += 1
+        self.assertTrue(count == 1)
+
+    def testDeletePort(self):
+        pass
+
+    def testGetPorts(self):
+        pass
+
+    def testPlugInterface(self):
+        pass
+
+    def testUnPlugInterface(self):
+        pass
+
+    def tearDown(self):
+        networks = self.quantum.get_all_networks(self.tenant_id)
+        print networks
+        # Clean up any test networks lying around
+        for net in networks:
+            id = net["net-id"]
+            name = net["net-name"]
+            if "plugin_test" in name:
+                # Clean up any test ports lying around
+                ports = self.quantum.get_all_ports(self.tenant_id, id)
+                print ports
+                for p in ports:
+                    self.quantum.delete_port(self.tenant_id, id, p["port-id"])
+                self.quantum.delete_network(self.tenant_id, id)
+
+if __name__ == "__main__":
+    suite = unittest.TestLoader().loadTestsFromTestCase(OVSPluginTest)
+    unittest.TextTestRunner(verbosity=2).run(suite)
+    suite = unittest.TestLoader().loadTestsFromTestCase(VlanMapTest)
+    unittest.TextTestRunner(verbosity=2).run(suite)
+
+    # TODO(bgh) move to unit tets
+    if False:
+        quantum.plug_interface(tenant_id, net1, port, "vif1.1")
+        portdetails = quantum.get_port_details(tenant_id, net1, port)
+        LOG.DEBUG(portdetails)
+        LOG.info("=== PORT: %s" % quantum.get_port_details(tenant_id, net1, port))
+        assert(portdetails["interface_id"] == "vif1.1")
+        networks = quantum.get_all_networks(tenant_id)
+        LOG.debug(networks)
+        for nid, name in networks.iteritems():
+            ports = quantum.get_all_ports(tenant_id, nid)
+            LOG.debug(ports)