]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
plugin: introduce ryu plugin
authorIsaku Yamahata <yamahata@valinux.co.jp>
Sat, 19 Nov 2011 09:17:03 +0000 (18:17 +0900)
committerIsaku Yamahata <yamahata@valinux.co.jp>
Sun, 26 Feb 2012 04:30:25 +0000 (13:30 +0900)
blueprint ovs-driver-extention
This patch implements the blueprint ovs-driver-extention
https://blueprints.launchpad.net/quantum/+spec/ovs-driver-extension

This patch factors out ovs common logic from ovs plugin into ovscommon
and adds Ryu NOS plugin.
This patch enhances ovs plugin for generic OVS controller support and

This patch is to add ofp controller support to OVS.
Store ofp controller address in ovs quantum data base.
- nova firewall_driver
- nova linuxnet_interface_driver

There may be ports unmanaged by nova/quantum. Those ports are used
to connect vm to outside of physical machine. They needs special care.

---
Changes 12 -> 13:
- rebased to 543e150d6dc9144ebcc588b7d2bd66374a107730
  changed files are only MANIFEST.in, setup.py, tools/pip-requres

Changes 11 -> 12:
- ryu agent
  eliminated from quantum.common import exceptions as exc
- ryu.db.api
  eliminated ofp_has_servers
- ryu.nova
  eliminated from quantum.plugins.ryu.nova import ovs_utils
  and eliminate ovs_utils

Chnages 10 -> 11:
- rebased to a945d1a30478c644d307c77a8a85f3a08e5a834e
- more Maru's review
- setup.py: fix setup() argument
  This isn't directly related to ryu plugin though
- improve fake ini file when unit test
  remove fake ini file after unit tests.
  use StringIO when no file is required.
- LOG: don't use %

Chnages 8 -> 9 -> 10:
- minor fixes: forgot to commit some hunks

Chnages 7 -> 8:
- rebased to d6bf2b76162ba806b2ad1f636f6273e47e03a117
- catch up d6bf2b76162ba806b2ad1f636f6273e47e03a117 change
  introduced bin/quantum_ryu_agent
- addressed Maru's review
  - avoid custom patching, use mock for test
    and added mox and mock to pip-requires
  - more pep8
  - avoid \ for line continuation
  - avoid single char variables
  - db.api: first() -> one()
  - utilize implicit conversion
    var is not None -> var
  - and more...

Changes 6 -> 7:
- update comment in ryu/run_tests.py
- make unit tests pass without ryu installed
  i.e.
  PLUGIN_DIR=quantum/plugins/ryu/ ./run_tests.sh
  works now

Chages 5 -> 6:
- remove comment

Change 4 -> 5:
- eliminate relative imports
- copyright
- doc string
- naming (s/CONF_FILE/conf_file/g)
- add " check to ryu/nova/ovs_utils
- ryu/nova/linux_net: comment
- ryu agent: eliminated unused methods
- updated ryu/README: add http://www.osrg.net/ryu/using_with_openstack.html
- added unit tests

Changes 3 -> 4:
- reflected Dan's review
- on-OVS in ryu.ini
- update @author
- some naming

Changes 2 -> 3:
- rebased to 04d144ae0b2ad5618847d1784cea48a08d53a46a
- abandoned to share code and duplicated codes from openvswitch plugin
  for ovs plugin stability.
- dropped setup_ryu.sh and added README
- update nova driver to catch up upstream change (gflags -> cfg)

Changes 1 -> 2:
- unbreak openvswtich unit test
- MANIFEST.in

Changes 3 -> new 1:
- rebased to 1eb3c693b5f6f3f301047100c36c7915434f8be7
- factor out common loginc from openvswitch plugin into ovscommon
- Introduced a new independent ryu plugin
- try new review due to the previous effort was marked abandoned.
  > https://review.openstack.org/#change,3055
  > Change-Id: I17801a7a74d4087838a8a26c1b1f97f28c2dcef3

Changes:
- rebased to 9c5c2caef13fa58234987527ab6caff829a37050
- some clean ups

Signed-off-by: Isaku Yamahata <yamahata@valinux.co.jp>
Change-Id: Ia9fe87525cebccc87b7c18a533f48607272cd97f

30 files changed:
MANIFEST.in
bin/quantum-ryu-agent [new file with mode: 0755]
etc/quantum/plugins/ryu/ryu.ini [new file with mode: 0644]
quantum/db/api.py
quantum/plugins/ryu/README [new file with mode: 0644]
quantum/plugins/ryu/__init__.py [new file with mode: 0644]
quantum/plugins/ryu/agent/__init__.py [new file with mode: 0644]
quantum/plugins/ryu/agent/ryu_quantum_agent.py [new file with mode: 0755]
quantum/plugins/ryu/db/__init__.py [new file with mode: 0644]
quantum/plugins/ryu/db/api.py [new file with mode: 0644]
quantum/plugins/ryu/db/models.py [new file with mode: 0644]
quantum/plugins/ryu/nova/__init__.py [new file with mode: 0644]
quantum/plugins/ryu/nova/firewall.py [new file with mode: 0644]
quantum/plugins/ryu/nova/linux_net.py [new file with mode: 0644]
quantum/plugins/ryu/nova/vif.py [new file with mode: 0644]
quantum/plugins/ryu/ofp_service_type.py [new file with mode: 0644]
quantum/plugins/ryu/ovs_quantum_plugin_base.py [new file with mode: 0644]
quantum/plugins/ryu/run_tests.py [new file with mode: 0644]
quantum/plugins/ryu/ryu_quantum_plugin.py [new file with mode: 0644]
quantum/plugins/ryu/tests/__init__.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/__init__.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/basetest.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/fake_plugin.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/fake_rest_nw_id.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/fake_ryu_client.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/test_plugin_base.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/test_ryu_driver.py [new file with mode: 0644]
quantum/plugins/ryu/tests/unit/utils.py [new file with mode: 0644]
setup.py
tools/pip-requires

index e207f42845ee8b6e126792de37f638828a192a0a..088e93efb4b58210913caf7fe67cfa3ad62eea37 100644 (file)
@@ -9,6 +9,7 @@ include etc/quantum/plugins/cisco/*.ini
 include etc/quantum/plugins/cisco/quantum.conf.ciscoext
 include etc/quantum/plugins/linuxbridge/*.ini
 include etc/quantum/plugins/nicira/*
+include etc/quantum/plugins/ryu/*.ini
 include quantum/plugins/*/README
 include quantum/plugins/openvswitch/Makefile
 include quantum/plugins/openvswitch/agent/xenserver_install.sh
diff --git a/bin/quantum-ryu-agent b/bin/quantum-ryu-agent
new file mode 100755 (executable)
index 0000000..3c0d98f
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import os
+import sys
+sys.path.insert(0, os.getcwd())
+from quantum.plugins.ryu.agent.ryu_quantum_agent import main
+
+main()
diff --git a/etc/quantum/plugins/ryu/ryu.ini b/etc/quantum/plugins/ryu/ryu.ini
new file mode 100644 (file)
index 0000000..6d732c9
--- /dev/null
@@ -0,0 +1,13 @@
+[DATABASE]
+# This line MUST be changed to actually run the plugin.
+# Example: sql_connection = mysql://root:nova@127.0.0.1:3306/ryu_quantum
+#sql_connection = mysql://<user>:<pass>@<IP>:<port>/<dbname>
+sql_connection = sqlite://
+
+[OVS]
+integration-bridge = br-int
+
+# openflow-controller = <host IP address of ofp controller>:<port: 6633>
+# 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
index deef3c06e57c93c681db027a000e4e40836abc70..1af4bf49b98211e491036f6e075f730928dfd9e9 100644 (file)
@@ -91,6 +91,11 @@ def network_create(tenant_id, name, op_status=OperationalStatus.UNKNOWN):
         return net
 
 
+def network_all_tenant_list():
+    session = get_session()
+    return session.query(models.Network).all()
+
+
 def network_list(tenant_id):
     session = get_session()
     return session.query(models.Network).\
diff --git a/quantum/plugins/ryu/README b/quantum/plugins/ryu/README
new file mode 100644 (file)
index 0000000..889cbfa
--- /dev/null
@@ -0,0 +1,28 @@
+Quantum plugin for Ryu Network Operating System
+This directory includes quantum plugin for Ryu Network Operating System.
+
+# -- Installation
+
+For how to install/set up this plugin with Ryu and Open Stack, please refer to
+http://www.osrg.net/ryu/using_with_openstack.html
+
+# -- Ryu General
+
+For general Ryu stuff, please refer to
+http://www.osrg.net/ryu/
+
+Ryu is available at github
+git://github.com/osrg/ryu.git
+https://github.com/osrg/ryu
+
+The mailing is at
+ryu-devel@lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/ryu-devel
+
+# -- unit test
+
+In order to run unit tests for Ryu plugin
+PLUGIN_DIR=quantum/plugins/ryu ./run_tests.sh
+NOTE: In order to run unit tests, sqlite3 is additionally needed.
+
+Enjoy!
diff --git a/quantum/plugins/ryu/__init__.py b/quantum/plugins/ryu/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/ryu/agent/__init__.py b/quantum/plugins/ryu/agent/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/ryu/agent/ryu_quantum_agent.py b/quantum/plugins/ryu/agent/ryu_quantum_agent.py
new file mode 100755 (executable)
index 0000000..11a2e32
--- /dev/null
@@ -0,0 +1,312 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# Based on openvswitch agent.
+#
+# Copyright 2011 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: Isaku Yamahata
+import ConfigParser
+import logging as LOG
+import signal
+import sys
+import time
+from optparse import OptionParser
+from sqlalchemy.ext.sqlsoup import SqlSoup
+from subprocess import PIPE, Popen
+
+from ryu.app import rest_nw_id
+from ryu.app.client import OFPClient
+
+
+OP_STATUS_UP = "UP"
+OP_STATUS_DOWN = "DOWN"
+
+
+class VifPort:
+    """
+    A class to represent a VIF (i.e., a port that has 'iface-id' and 'vif-mac'
+    attributes set).
+    """
+    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=%s, vif_mac=%s, port_name=%s, ofport=%s, "
+                "bridge name = %s" % (self.vif_id,
+                                      self.vif_mac,
+                                      self.port_name,
+                                      self.ofport,
+                                      self.switch.br_name))
+
+
+class OVSBridge:
+    def __init__(self, br_name):
+        self.br_name = br_name
+        self.datapath_id = None
+
+    def find_datapath_id(self):
+        # ovs-vsctl get Bridge br-int datapath_id
+        res = self.run_vsctl(["get", "Bridge", self.br_name, "datapath_id"])
+
+        # remove preceding/trailing double quotes
+        dp_id = res.strip().strip('"')
+        self.datapath_id = dp_id
+
+    def run_cmd(self, args):
+        pipe = Popen(args, stdout=PIPE)
+        retval = pipe.communicate()[0]
+        if pipe.returncode == -(signal.SIGALRM):
+            LOG.debug("## timeout running command: " + " ".join(args))
+        return retval
+
+    def run_vsctl(self, args):
+        full_args = ["ovs-vsctl", "--timeout=2"] + args
+        return self.run_cmd(full_args)
+
+    def set_controller(self, target):
+        methods = ("ssl", "tcp", "unix", "pssl", "ptcp", "punix")
+        args = target.split(":")
+        if not args[0] in methods:
+            target = "tcp:" + target
+        self.run_vsctl(["set-controller", self.br_name, target])
+
+    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")
+
+    @staticmethod
+    def db_str_to_map(full_str):
+        list = full_str.strip("{}").split(", ")
+        ret = {}
+        for elem in list:
+            if elem.find("=") == -1:
+                continue
+            arr = elem.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")[:-1]
+
+    def get_xapi_iface_id(self, xs_vif_uuid):
+        return self.run_cmd(
+                        ["xe",
+                        "vif-param-get",
+                        "param-name=other-config",
+                        "param-key=nicira-iface-id",
+                        "uuid=%s" % xs_vif_uuid]).strip()
+
+    def _vifport(self, name, external_ids):
+        ofport = self.db_get_val("Interface", name, "ofport")
+        return VifPort(name, ofport, external_ids["iface-id"],
+                       external_ids["attached-mac"], self)
+
+    def _get_ports(self, get_port):
+        ports = []
+        port_names = self.get_port_name_list()
+        for name in port_names:
+            port = get_port(name)
+            if port:
+                ports.append(port)
+
+        return ports
+
+    def _get_vif_port(self, name):
+        external_ids = self.db_get_map("Interface", name, "external_ids")
+        if "iface-id" in external_ids and "attached-mac" in external_ids:
+            return self._vifport(name, external_ids)
+        elif ("xs-vif-uuid" in external_ids and
+              "attached-mac" in external_ids):
+            # if this is a xenserver and iface-id is not automatically
+            # synced to OVS from XAPI, we grab it from XAPI directly
+            ofport = self.db_get_val("Interface", name, "ofport")
+            iface_id = self.get_xapi_iface_id(external_ids["xs-vif-uuid"])
+            return VifPort(name, ofport, iface_id,
+                           external_ids["attached-mac"], self)
+
+    def get_vif_ports(self):
+        "returns a VIF object for each VIF port"
+        return self._get_ports(self._get_vif_port)
+
+    def _get_external_port(self, name):
+        external_ids = self.db_get_map("Interface", name, "external_ids")
+        if external_ids:
+            return
+
+        ofport = self.db_get_val("Interface", name, "ofport")
+        return VifPort(name, ofport, None, None, self)
+
+    def get_external_ports(self):
+        return self._get_ports(self._get_external_port)
+
+
+def check_ofp_mode(db):
+    LOG.debug("checking db")
+
+    servers = db.ofp_server.all()
+
+    ofp_controller_addr = None
+    ofp_rest_api_addr = None
+    for serv in servers:
+        if serv.host_type == "REST_API":
+            ofp_rest_api_addr = serv.address
+        elif serv.host_type == "controller":
+            ofp_controller_addr = serv.address
+        else:
+            LOG.warn("ignoring unknown server type %s", serv)
+
+    LOG.debug("controller %s", ofp_controller_addr)
+    LOG.debug("api %s", ofp_rest_api_addr)
+    if not ofp_controller_addr:
+        raise RuntimeError("OF controller isn't specified")
+    if not ofp_rest_api_addr:
+        raise RuntimeError("Ryu rest API port isn't specified")
+
+    LOG.debug("going to ofp controller mode %s %s",
+              ofp_controller_addr, ofp_rest_api_addr)
+    return (ofp_controller_addr, ofp_rest_api_addr)
+
+
+class OVSQuantumOFPRyuAgent:
+    def __init__(self, integ_br, db):
+        (ofp_controller_addr, ofp_rest_api_addr) = check_ofp_mode(db)
+
+        self.nw_id_external = rest_nw_id.NW_ID_EXTERNAL
+        self.api = OFPClient(ofp_rest_api_addr)
+        self._setup_integration_br(integ_br, ofp_controller_addr)
+
+    def _setup_integration_br(self, integ_br, ofp_controller_addr):
+        self.int_br = OVSBridge(integ_br)
+        self.int_br.find_datapath_id()
+        self.int_br.set_controller(ofp_controller_addr)
+        for port in self.int_br.get_external_ports():
+            self._port_update(self.nw_id_external, port)
+
+    def _port_update(self, network_id, port):
+        self.api.update_port(network_id, port.switch.datapath_id, port.ofport)
+
+    def _all_bindings(self, db):
+        """return interface id -> port which include network id bindings"""
+        return dict((port.interface_id, port) for port in db.ports.all())
+
+    def daemon_loop(self, db):
+        # on startup, register all existing ports
+        all_bindings = self._all_bindings(db)
+
+        local_bindings = {}
+        vif_ports = {}
+        for port in self.int_br.get_vif_ports():
+            vif_ports[port.vif_id] = port
+            if port.vif_id in all_bindings:
+                net_id = all_bindings[port.vif_id].network_id
+                local_bindings[port.vif_id] = net_id
+                self._port_update(net_id, port)
+                all_bindings[port.vif_id].op_status = OP_STATUS_UP
+                LOG.info("Updating binding to net-id = %s for %s",
+                         net_id, str(port))
+        db.commit()
+
+        old_vif_ports = vif_ports
+        old_local_bindings = local_bindings
+
+        while True:
+            all_bindings = self._all_bindings(db)
+
+            new_vif_ports = {}
+            new_local_bindings = {}
+            for port in self.int_br.get_vif_ports():
+                new_vif_ports[port.vif_id] = port
+                if port.vif_id in all_bindings:
+                    net_id = all_bindings[port.vif_id].network_id
+                    new_local_bindings[port.vif_id] = net_id
+
+                old_b = old_local_bindings.get(port.vif_id)
+                new_b = new_local_bindings.get(port.vif_id)
+                if old_b == new_b:
+                    continue
+
+                if not old_b:
+                    LOG.info("Removing binding to net-id = %s for %s",
+                             old_b, str(port))
+                    if port.vif_id in all_bindings:
+                        all_bindings[port.vif_id].op_status = OP_STATUS_DOWN
+                if not new_b:
+                    if port.vif_id in all_bindings:
+                        all_bindings[port.vif_id].op_status = OP_STATUS_UP
+                    LOG.info("Adding binding to net-id = %s for %s",
+                             new_b, str(port))
+
+            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 all_bindings:
+                        all_bindings[vif_id].op_status = OP_STATUS_DOWN
+
+            old_vif_ports = new_vif_ports
+            old_local_bindings = new_local_bindings
+            db.commit()
+            time.sleep(2)
+
+
+def 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")
+
+    options = {"sql_connection": config.get("DATABASE", "sql_connection")}
+    db = SqlSoup(options["sql_connection"])
+
+    LOG.info("Connecting to database \"%s\" on %s",
+             db.engine.url.database, db.engine.url.host)
+    plugin = OVSQuantumOFPRyuAgent(integ_br, db)
+    plugin.daemon_loop(db)
+
+    sys.exit(0)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/quantum/plugins/ryu/db/__init__.py b/quantum/plugins/ryu/db/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/ryu/db/api.py b/quantum/plugins/ryu/db/api.py
new file mode 100644 (file)
index 0000000..caa8b57
--- /dev/null
@@ -0,0 +1,27 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import quantum.db.api as db
+from quantum.plugins.ryu.db import models
+
+
+def set_ofp_servers(hosts):
+    session = db.get_session()
+    session.query(models.OFPServer).delete()
+    for (host_address, host_type) in hosts:
+        host = models.OFPServer(host_address, host_type)
+        session.add(host)
+    session.flush()
diff --git a/quantum/plugins/ryu/db/models.py b/quantum/plugins/ryu/db/models.py
new file mode 100644 (file)
index 0000000..e31f205
--- /dev/null
@@ -0,0 +1,38 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from sqlalchemy import Column, Integer, String
+from sqlalchemy.ext.declarative import declarative_base
+
+from quantum.db.models import BASE
+
+
+class OFPServer(BASE):
+    """Openflow Server/API address"""
+    __tablename__ = 'ofp_server'
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    address = Column(String(255))       # netloc <host ip address>:<port>
+    host_type = Column(String(255))     # server type
+                                        # Controller, REST_API
+
+    def __init__(self, address, host_type):
+        self.address = address
+        self.host_type = host_type
+
+    def __repr__(self):
+        return "<OFPServer(%s,%s,%s)>" % (self.id, self.address,
+                                          self.host_type)
diff --git a/quantum/plugins/ryu/nova/__init__.py b/quantum/plugins/ryu/nova/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/ryu/nova/firewall.py b/quantum/plugins/ryu/nova/firewall.py
new file mode 100644 (file)
index 0000000..c84f046
--- /dev/null
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright (c) 2012 Isaku Yamahata <yamahata at private email ne jp>
+#                                   <yamahata at valinux co jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import inspect
+
+from nova.virt import firewall
+
+
+class NopFirewallDriver(firewall.FirewallDriver):
+    def __init__(self, *args, **kwargs):
+        super(NopFirewallDriver, self).__init__()
+        for key, _val in inspect.getmembers(self, inspect.ismethod):
+            if key.startswith('__') or key.endswith('__'):
+                continue
+            setattr(self, key, (lambda _self, *_args, **_kwargs: True))
diff --git a/quantum/plugins/ryu/nova/linux_net.py b/quantum/plugins/ryu/nova/linux_net.py
new file mode 100644 (file)
index 0000000..500d6f0
--- /dev/null
@@ -0,0 +1,90 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+#                               <yamahata at valinux co jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from nova import flags
+from nova import log as logging
+from nova import utils
+from nova.openstack.common import cfg
+from ryu.app.client import OFPClient
+
+LOG = logging.getLogger(__name__)
+
+ryu_linux_net_opt = cfg.StrOpt('linuxnet_ovs_ryu_api_host',
+                               default='127.0.0.1:8080',
+                               help='Openflow Ryu REST API host:port')
+
+FLAGS = flags.FLAGS
+FLAGS.add_option(ryu_linux_net_opt)
+
+
+def _get_datapath_id(bridge_name):
+    out, _err = utils.execute('ovs-vsctl', 'get', 'Bridge',
+                              bridge_name, 'datapath_id', run_as_root=True)
+    return out.strip().strip('"')
+
+
+def _get_port_no(dev):
+    out, _err = utils.execute('ovs-vsctl', 'get', 'Interface', dev,
+                              'ofport', run_as_root=True)
+    return int(out.strip())
+
+
+# In order to avoid circular import, dynamically import the base class,
+# nova.network.linux_net.LinuxOVSInterfaceDriver
+# and use composition instead of inheritance.
+# The following inheritance code doesn't work due to circular import.
+#    from nova.network import linux_net as nova_linux_net
+#    class LinuxOVSRyuInterfaceDriver(nova_linux_net.LinuxOVSInterfaceDriver):
+#
+# nova.network.linux_net imports FLAGS.linuxnet_interface_driver
+# We are being imported from linux_net so that linux_net can't be imported
+# here due to circular import.
+# Another approach would be to factor out nova.network.linux_net so that
+# linux_net doesn't import FLAGS.linuxnet_interface_driver or it loads
+# lazily FLAGS.linuxnet_interface_driver.
+
+
+class LinuxOVSRyuInterfaceDriver(object):
+    def __init__(self):
+        from nova.network import linux_net as nova_linux_net
+        self.parent = nova_linux_net.LinuxOVSInterfaceDriver()
+
+        LOG.debug('ryu rest host %s', FLAGS.linuxnet_ovs_ryu_api_host)
+        self.ryu_client = OFPClient(FLAGS.linuxnet_ovs_ryu_api_host)
+        self.datapath_id = _get_datapath_id(
+            FLAGS.linuxnet_ovs_integration_bridge)
+
+        if nova_linux_net.binary_name == 'nova-network':
+            for tables in [nova_linux_net.iptables_manager.ipv4,
+                           nova_linux_net.iptables_manager.ipv6]:
+                tables['filter'].add_rule('FORWARD',
+                        '--in-interface gw-+ --out-interface gw-+ -j DROP')
+            nova_linux_net.iptables_manager.apply()
+
+    def plug(self, network, mac_address, gateway=True):
+        LOG.debug("network %s mac_adress %s gateway %s",
+                  network, mac_address, gateway)
+        ret = self.parent.plug(network, mac_address, gateway)
+        port_no = _get_port_no(self.get_dev(network))
+        self.ryu_client.create_port(network['uuid'], self.datapath_id, port_no)
+        return ret
+
+    def unplug(self, network):
+        return self.parent.unplug(network)
+
+    def get_dev(self, network):
+        return self.parent.get_dev(network)
diff --git a/quantum/plugins/ryu/nova/vif.py b/quantum/plugins/ryu/nova/vif.py
new file mode 100644 (file)
index 0000000..91c416c
--- /dev/null
@@ -0,0 +1,80 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+#                               <yamahata at valinux co jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import httplib
+
+from nova import flags
+from nova import log as logging
+from nova import utils
+from nova.openstack.common import cfg
+from nova.virt.libvirt import vif as libvirt_vif
+from ryu.app.client import OFPClient
+
+
+LOG = logging.getLogger(__name__)
+
+ryu_libvirt_ovs_driver_opt = cfg.StrOpt('libvirt_ovs_ryu_api_host',
+                                        default='127.0.0.1:8080',
+                                        help='Openflow Ryu REST API host:port')
+
+FLAGS = flags.FLAGS
+FLAGS.add_option(ryu_libvirt_ovs_driver_opt)
+
+
+def _get_datapath_id(bridge_name):
+    out, _err = utils.execute('ovs-vsctl', 'get', 'Bridge',
+                              bridge_name, 'datapath_id', run_as_root=True)
+    return out.strip().strip('"')
+
+
+def _get_port_no(dev):
+    out, _err = utils.execute('ovs-vsctl', 'get', 'Interface', dev,
+                              'ofport', run_as_root=True)
+    return int(out.strip())
+
+
+class LibvirtOpenVswitchOFPRyuDriver(libvirt_vif.LibvirtOpenVswitchDriver):
+    def __init__(self, **kwargs):
+        super(LibvirtOpenVswitchOFPRyuDriver, self).__init__()
+        LOG.debug('ryu rest host %s', FLAGS.libvirt_ovs_bridge)
+        self.ryu_client = OFPClient(FLAGS.libvirt_ovs_ryu_api_host)
+        self.datapath_id = _get_datapath_id(FLAGS.libvirt_ovs_bridge)
+
+    def _get_port_no(self, mapping):
+        iface_id = mapping['vif_uuid']
+        dev = self.get_dev_name(iface_id)
+        return _get_port_no(dev)
+
+    def plug(self, instance, network, mapping):
+        result = super(LibvirtOpenVswitchOFPRyuDriver, self).plug(
+            instance, network, mapping)
+        port_no = self._get_port_no(mapping)
+        self.ryu_client.create_port(network['id'],
+                                    self.datapath_id, port_no)
+        return result
+
+    def unplug(self, instance, network, mapping):
+        port_no = self._get_port_no(mapping)
+        try:
+            self.ryu_client.delete_port(network['id'],
+                                        self.datapath_id, port_no)
+        except httplib.HTTPException as e:
+            res = e.args[0]
+            if res.status != httplib.NOT_FOUND:
+                raise
+        super(LibvirtOpenVswitchOFPRyuDriver, self).unplug(instance, network,
+                                                           mapping)
diff --git a/quantum/plugins/ryu/ofp_service_type.py b/quantum/plugins/ryu/ofp_service_type.py
new file mode 100644 (file)
index 0000000..86615ec
--- /dev/null
@@ -0,0 +1,19 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at valinux co jp>
+# 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: Isaku Yamahata
+
+CONTROLLER = 'controller'
+REST_API = 'REST_API'
diff --git a/quantum/plugins/ryu/ovs_quantum_plugin_base.py b/quantum/plugins/ryu/ovs_quantum_plugin_base.py
new file mode 100644 (file)
index 0000000..9a49e01
--- /dev/null
@@ -0,0 +1,172 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata
+# 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: Isaku Yamahata
+import ConfigParser
+import logging as LOG
+import os
+from abc import ABCMeta, abstractmethod
+
+import quantum.db.api as db
+from quantum.api.api_common import OperationalStatus
+from quantum.common import exceptions as q_exc
+from quantum.manager import find_config
+from quantum.quantum_plugin_base import QuantumPluginBase
+
+
+LOG.getLogger(__name__)
+
+
+class OVSQuantumPluginDriverBase(object):
+    """
+    Base class for OVS quantum plugin driver
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def create_network(self, net):
+        pass
+
+    @abstractmethod
+    def delete_network(self, net):
+        pass
+
+
+class OVSQuantumPluginBase(QuantumPluginBase):
+    """
+    Base class for OVS-based plugin which referes to a subclass of
+    OVSQuantumPluginDriverBase which is defined above.
+    Subclass of OVSQuantumPluginBase must set self.driver to a subclass of
+    OVSQuantumPluginDriverBase.
+    """
+    def __init__(self, conf_file, mod_file, configfile=None):
+        super(OVSQuantumPluginBase, self).__init__()
+        config = ConfigParser.ConfigParser()
+        if configfile is None:
+            if conf_file and os.path.exists(conf_file):
+                configfile = conf_file
+            else:
+                configfile = find_config(os.path.abspath(
+                        os.path.dirname(mod_file)))
+        if configfile is None:
+            raise Exception("Configuration file \"%s\" doesn't exist" %
+              (configfile))
+        LOG.debug("Using configuration file: %s", configfile)
+        config.read(configfile)
+        LOG.debug("Config: %s", config)
+
+        options = {"sql_connection": config.get("DATABASE", "sql_connection")}
+        db.configure_db(options)
+
+        self.config = config
+        # Subclass must set self.driver to its own OVSQuantumPluginDriverBase
+        self.driver = None
+
+    def get_all_networks(self, tenant_id, **kwargs):
+        nets = []
+        for net in db.network_list(tenant_id):
+            LOG.debug("Adding network: %s", net.uuid)
+            nets.append(self._make_net_dict(str(net.uuid), net.name,
+                                            None, net.op_status))
+        return nets
+
+    def _make_net_dict(self, net_id, net_name, ports, op_status):
+        res = {'net-id': net_id,
+               'net-name': net_name,
+               'net-op-status': op_status}
+        if ports:
+            res['net-ports'] = ports
+        return res
+
+    def create_network(self, tenant_id, net_name, **kwargs):
+        net = db.network_create(tenant_id, net_name,
+                                op_status=OperationalStatus.UP)
+        LOG.debug("Created network: %s", net)
+        self.driver.create_network(net)
+        return self._make_net_dict(str(net.uuid), net.name, [], net.op_status)
+
+    def delete_network(self, tenant_id, net_id):
+        net = db.network_get(net_id)
+
+        # Verify that no attachments are plugged into the network
+        for port in db.port_list(net_id):
+            if port.interface_id:
+                raise q_exc.NetworkInUse(net_id=net_id)
+        net = db.network_destroy(net_id)
+        self.driver.delete_network(net)
+        return self._make_net_dict(str(net.uuid), net.name, [], net.op_status)
+
+    def get_network_details(self, tenant_id, net_id):
+        net = db.network_get(net_id)
+        ports = self.get_all_ports(tenant_id, net_id)
+        return self._make_net_dict(str(net.uuid), net.name,
+                                   ports, net.op_status)
+
+    def update_network(self, tenant_id, net_id, **kwargs):
+        net = db.network_update(net_id, tenant_id, **kwargs)
+        return self._make_net_dict(str(net.uuid), net.name,
+                                   None, net.op_status)
+
+    def _make_port_dict(self, port):
+        if port.state == "ACTIVE":
+            op_status = port.op_status
+        else:
+            op_status = OperationalStatus.DOWN
+
+        return {'port-id': str(port.uuid),
+                'port-state': port.state,
+                'port-op-status': op_status,
+                'net-id': port.network_id,
+                'attachment': port.interface_id}
+
+    def get_all_ports(self, tenant_id, net_id, **kwargs):
+        ports = db.port_list(net_id)
+        # This plugin does not perform filtering at the moment
+        return [{'port-id': str(port.uuid)} for port in ports]
+
+    def create_port(self, tenant_id, net_id, port_state=None, **kwargs):
+        LOG.debug("Creating port with network_id: %s", net_id)
+        port = db.port_create(net_id, port_state,
+                              op_status=OperationalStatus.DOWN)
+        return self._make_port_dict(port)
+
+    def delete_port(self, tenant_id, net_id, port_id):
+        port = db.port_destroy(port_id, net_id)
+        return self._make_port_dict(port)
+
+    def update_port(self, tenant_id, net_id, port_id, **kwargs):
+        """
+        Updates the state of a port on the specified Virtual Network.
+        """
+        LOG.debug("update_port() called\n")
+        port = db.port_get(port_id, net_id)
+        db.port_update(port_id, net_id, **kwargs)
+        return self._make_port_dict(port)
+
+    def get_port_details(self, tenant_id, net_id, port_id):
+        port = db.port_get(port_id, net_id)
+        return self._make_port_dict(port)
+
+    def plug_interface(self, tenant_id, net_id, port_id, remote_iface_id):
+        db.port_set_attachment(port_id, net_id, remote_iface_id)
+
+    def unplug_interface(self, tenant_id, net_id, port_id):
+        db.port_set_attachment(port_id, net_id, "")
+        db.port_update(port_id, net_id, op_status=OperationalStatus.DOWN)
+
+    def get_interface_details(self, tenant_id, net_id, port_id):
+        res = db.port_get(port_id, net_id)
+        return res.interface_id
diff --git a/quantum/plugins/ryu/run_tests.py b/quantum/plugins/ryu/run_tests.py
new file mode 100644 (file)
index 0000000..c1e227e
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Isaku Yamahata  <yamahata at private email ne jp>
+# 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.
+
+
+"""Unittest runner for quantum Ryu plugin
+
+This file should be run from the top dir in the quantum directory
+
+To run all tests::
+    PLUGIN_DIR=quantum/plugins/ryu ./run_tests.sh
+"""
+
+import os
+import sys
+
+from nose import config
+from nose import core
+
+sys.path.append(os.getcwd())
+sys.path.append(os.path.dirname(__file__))
+
+
+import quantum.tests.unit
+from quantum.api.api_common import OperationalStatus
+from quantum.common.test_lib import run_tests, test_config
+from quantum.plugins.ryu.tests.unit.utils import patch_fake_ryu_client
+
+
+if __name__ == '__main__':
+    exit_status = False
+
+    # if a single test case was specified,
+    # we should only invoked the tests once
+    invoke_once = len(sys.argv) > 1
+
+    test_config['plugin_name'] = "ryu_quantum_plugin.RyuQuantumPlugin"
+    test_config['default_net_op_status'] = OperationalStatus.UP
+    test_config['default_port_op_status'] = OperationalStatus.DOWN
+
+    cwd = os.getcwd()
+    # patch modules for ryu.app.client and ryu.app.rest_nw_id
+    # With those, plugin can be tested without ryu installed
+    with patch_fake_ryu_client():
+        # to find quantum/etc/plugin/ryu/ryu.ini before chdir()
+        import ryu_quantum_plugin
+
+        c = config.Config(stream=sys.stdout,
+                          env=os.environ,
+                          verbosity=3,
+                          includeExe=True,
+                          traverseNamespace=True,
+                          plugins=core.DefaultPluginManager())
+        c.configureWhere(quantum.tests.unit.__path__)
+
+        exit_status = run_tests(c)
+
+    if invoke_once:
+        sys.exit(0)
+
+    os.chdir(cwd)
+
+    working_dir = os.path.abspath("quantum/plugins/ryu")
+    c = config.Config(stream=sys.stdout,
+                      env=os.environ,
+                      verbosity=3,
+                      workingDir=working_dir)
+    exit_status = exit_status or run_tests(c)
+
+    sys.exit(exit_status)
diff --git a/quantum/plugins/ryu/ryu_quantum_plugin.py b/quantum/plugins/ryu/ryu_quantum_plugin.py
new file mode 100644 (file)
index 0000000..49ceeaa
--- /dev/null
@@ -0,0 +1,68 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+#                               <yamahata at valinux co jp>
+# 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: Isaku Yamahata
+
+import quantum.db.api as db
+from quantum.common import exceptions as q_exc
+from quantum.common.config import find_config_file
+from quantum.plugins.ryu import ofp_service_type
+from quantum.plugins.ryu import ovs_quantum_plugin_base
+from quantum.plugins.ryu.db import api as db_api
+
+
+from ryu.app import client
+from ryu.app import rest_nw_id
+
+
+CONF_FILE = find_config_file({"plugin": "ryu"}, None, "ryu.ini")
+
+
+class OFPRyuDriver(ovs_quantum_plugin_base.OVSQuantumPluginDriverBase):
+    def __init__(self, config):
+        super(OFPRyuDriver, self).__init__()
+        ofp_con_host = config.get("OVS", "openflow-controller")
+        ofp_api_host = config.get("OVS", "openflow-rest-api")
+
+        if ofp_con_host is None or ofp_api_host is None:
+            raise q_exc.Invalid("invalid configuration. check ryu.ini")
+
+        hosts = [(ofp_con_host, ofp_service_type.CONTROLLER),
+                 (ofp_api_host, ofp_service_type.REST_API)]
+        db_api.set_ofp_servers(hosts)
+
+        self.client = client.OFPClient(ofp_api_host)
+        self.client.update_network(rest_nw_id.NW_ID_EXTERNAL)
+
+        # register known all network list on startup
+        self._create_all_tenant_network()
+
+    def _create_all_tenant_network(self):
+        networks = db.network_all_tenant_list()
+        for net in networks:
+            self.client.update_network(net.uuid)
+
+    def create_network(self, net):
+        self.client.create_network(net.uuid)
+
+    def delete_network(self, net):
+        self.client.delete_network(net.uuid)
+
+
+class RyuQuantumPlugin(ovs_quantum_plugin_base.OVSQuantumPluginBase):
+    def __init__(self, configfile=None):
+        super(RyuQuantumPlugin, self).__init__(CONF_FILE, __file__, configfile)
+        self.driver = OFPRyuDriver(self.config)
diff --git a/quantum/plugins/ryu/tests/__init__.py b/quantum/plugins/ryu/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/ryu/tests/unit/__init__.py b/quantum/plugins/ryu/tests/unit/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/ryu/tests/unit/basetest.py b/quantum/plugins/ryu/tests/unit/basetest.py
new file mode 100644 (file)
index 0000000..0d2e007
--- /dev/null
@@ -0,0 +1,43 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mox
+import stubout
+import unittest
+
+import quantum.db.api as db
+import quantum.plugins.ryu.db.models    # for ryu specific tables
+from quantum.plugins.ryu.tests.unit import utils
+
+
+class BaseRyuTest(unittest.TestCase):
+    """base test class for Ryu unit tests"""
+    def setUp(self):
+        config = utils.get_config()
+        options = {"sql_connection": config.get("DATABASE", "sql_connection")}
+        db.configure_db(options)
+
+        self.config = config
+        self.mox = mox.Mox()
+        self.stubs = stubout.StubOutForTesting()
+
+    def tearDown(self):
+        self.mox.UnsetStubs()
+        self.stubs.UnsetAll()
+        self.stubs.SmartUnsetAll()
+        self.mox.VerifyAll()
+        db.clear_db()
diff --git a/quantum/plugins/ryu/tests/unit/fake_plugin.py b/quantum/plugins/ryu/tests/unit/fake_plugin.py
new file mode 100644 (file)
index 0000000..55c4853
--- /dev/null
@@ -0,0 +1,35 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+#                               <yamahata at valinux co jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from quantum.plugins.ryu import ovs_quantum_plugin_base
+
+
+class FakePluginDriver(ovs_quantum_plugin_base.OVSQuantumPluginDriverBase):
+    def __init__(self, config):
+        super(FakePluginDriver, self).__init__()
+
+    def create_network(self, net):
+        pass
+
+    def delete_network(self, net):
+        pass
+
+
+class FakePlugin(ovs_quantum_plugin_base.OVSQuantumPluginBase):
+    def __init__(self, configfile=None):
+        super(FakePlugin, self).__init__(None, __file__, configfile)
+        self.driver = FakePluginDriver(self.config)
diff --git a/quantum/plugins/ryu/tests/unit/fake_rest_nw_id.py b/quantum/plugins/ryu/tests/unit/fake_rest_nw_id.py
new file mode 100644 (file)
index 0000000..5a682cc
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright (C) 2011 Nippon Telegraph and Telephone Corporation.
+# Copyright (C) 2011 Isaku Yamahata <yamahata at valinux co jp>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+NW_ID_EXTERNAL = '__NW_ID_EXTERNAL__'
+NW_ID_UNKNOWN = '__NW_ID_UNKNOWN__'
diff --git a/quantum/plugins/ryu/tests/unit/fake_ryu_client.py b/quantum/plugins/ryu/tests/unit/fake_ryu_client.py
new file mode 100644 (file)
index 0000000..763b86c
--- /dev/null
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# 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.
+
+
+class OFPClient(object):
+    def __init__(self, address):
+        super(OFPClient, self).__init__()
+        self.address = address
+
+    def get_networks(self):
+        pass
+
+    def create_network(self, network_id):
+        pass
+
+    def update_network(self, network_id):
+        pass
+
+    def delete_network(self, network_id):
+        pass
+
+    def get_ports(self, network_id):
+        pass
+
+    def create_port(self, network_id, dpid, port):
+        pass
+
+    def update_port(self, network_id, dpid, port):
+        pass
+
+    def delete_port(self, network_id, dpid, port):
+        pass
diff --git a/quantum/plugins/ryu/tests/unit/test_plugin_base.py b/quantum/plugins/ryu/tests/unit/test_plugin_base.py
new file mode 100644 (file)
index 0000000..3accc14
--- /dev/null
@@ -0,0 +1,54 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mox
+import os
+
+from quantum.plugins.ryu.tests.unit import fake_plugin
+from quantum.plugins.ryu.tests.unit import utils
+from quantum.plugins.ryu.tests.unit.basetest import BaseRyuTest
+
+
+class PluginBaseTest(BaseRyuTest):
+    """Class conisting of OVSQuantumPluginBase unit tests"""
+    def setUp(self):
+        super(PluginBaseTest, self).setUp()
+        self.ini_file = utils.create_fake_ryu_ini()
+
+    def tearDown(self):
+        os.unlink(self.ini_file)
+        super(PluginBaseTest, self).tearDown()
+
+    def test_create_delete_network(self):
+        # mox.StubOutClassWithMocks can't be used for class with metaclass
+        # overrided
+        driver_mock = self.mox.CreateMock(fake_plugin.FakePluginDriver)
+        self.mox.StubOutWithMock(fake_plugin, 'FakePluginDriver',
+                                 use_mock_anything=True)
+
+        fake_plugin.FakePluginDriver(mox.IgnoreArg()).AndReturn(driver_mock)
+        driver_mock.create_network(mox.IgnoreArg())
+        driver_mock.delete_network(mox.IgnoreArg())
+        self.mox.ReplayAll()
+        plugin = fake_plugin.FakePlugin(configfile=self.ini_file)
+
+        tenant_id = 'tenant_id'
+        net_name = 'net_name'
+        ret = plugin.create_network(tenant_id, net_name)
+
+        plugin.delete_network(tenant_id, ret['net-id'])
+        self.mox.VerifyAll()
diff --git a/quantum/plugins/ryu/tests/unit/test_ryu_driver.py b/quantum/plugins/ryu/tests/unit/test_ryu_driver.py
new file mode 100644 (file)
index 0000000..37dce46
--- /dev/null
@@ -0,0 +1,73 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import uuid
+
+import quantum.db.api as db
+from quantum.plugins.ryu.tests.unit import utils
+from quantum.plugins.ryu.tests.unit.basetest import BaseRyuTest
+from quantum.plugins.ryu.tests.unit.utils import patch_fake_ryu_client
+
+
+class RyuDriverTest(BaseRyuTest):
+    """Class conisting of OFPRyuDriver unit tests"""
+    def setUp(self):
+        super(RyuDriverTest, self).setUp()
+
+        # fake up ryu.app.client and ryu.app.rest_nw_id
+        # With those, plugin can be tested without ryu installed
+        self.module_patcher = patch_fake_ryu_client()
+        self.module_patcher.start()
+
+    def tearDown(self):
+        self.module_patcher.stop()
+        super(RyuDriverTest, self).tearDown()
+
+    def test_ryu_driver(self):
+        from ryu.app import client as client_mod
+        from ryu.app import rest_nw_id as rest_nw_id_mod
+
+        self.mox.StubOutClassWithMocks(client_mod, 'OFPClient')
+        client_mock = client_mod.OFPClient(utils.FAKE_REST_ADDR)
+
+        self.mox.StubOutWithMock(client_mock, 'update_network')
+        self.mox.StubOutWithMock(client_mock, 'create_network')
+        self.mox.StubOutWithMock(client_mock, 'delete_network')
+        client_mock.update_network(rest_nw_id_mod.NW_ID_EXTERNAL)
+        uuid0 = '01234567-89ab-cdef-0123-456789abcdef'
+
+        def fake_uuid4():
+            return uuid0
+
+        self.stubs.Set(uuid, 'uuid4', fake_uuid4)
+        uuid1 = '12345678-9abc-def0-1234-56789abcdef0'
+        net1 = utils.Net(uuid1)
+
+        client_mock.update_network(uuid0)
+        client_mock.create_network(uuid1)
+        client_mock.delete_network(uuid1)
+        self.mox.ReplayAll()
+
+        db.network_create('test', uuid0)
+
+        from quantum.plugins.ryu import ryu_quantum_plugin
+        ryu_driver = ryu_quantum_plugin.OFPRyuDriver(self.config)
+        ryu_driver.create_network(net1)
+        ryu_driver.delete_network(net1)
+        self.mox.VerifyAll()
+
+        db.network_destroy(uuid0)
diff --git a/quantum/plugins/ryu/tests/unit/utils.py b/quantum/plugins/ryu/tests/unit/utils.py
new file mode 100644 (file)
index 0000000..e7bf4d7
--- /dev/null
@@ -0,0 +1,73 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Isaku Yamahata <yamahata at private email ne jp>
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import ConfigParser
+import imp
+import os
+import tempfile
+from StringIO import StringIO
+
+import mock
+
+from quantum.plugins.ryu.tests.unit import fake_rest_nw_id
+from quantum.plugins.ryu.tests.unit import fake_ryu_client
+
+FAKE_CONTROLLER_ADDR = '127.0.0.1:6633'
+FAKE_REST_ADDR = '127.0.0.1:8080'
+FAKE_RYU_INI_TEMPLATE = """
+[DATABASE]
+sql_connection = sqlite:///:memory:
+
+[OVS]
+integration-bridge = br-int
+openflow-controller = %s
+openflow-rest-api = %s
+""" % (FAKE_CONTROLLER_ADDR, FAKE_REST_ADDR)
+
+
+def create_fake_ryu_ini():
+    fd, file_name = tempfile.mkstemp(suffix='.ini')
+    tmp_file = os.fdopen(fd, 'w')
+    tmp_file.write(FAKE_RYU_INI_TEMPLATE)
+    tmp_file.close()
+    return file_name
+
+
+def get_config():
+    config = ConfigParser.ConfigParser()
+    buf_file = StringIO(FAKE_RYU_INI_TEMPLATE)
+    config.readfp(buf_file)
+    buf_file.close()
+    return config
+
+
+def patch_fake_ryu_client():
+    ryu_mod = imp.new_module('ryu')
+    ryu_app_mod = imp.new_module('ryu.app')
+    ryu_mod.app = ryu_app_mod
+    ryu_app_mod.client = fake_ryu_client
+    ryu_app_mod.rest_nw_id = fake_rest_nw_id
+    return mock.patch.dict('sys.modules',
+                           {'ryu': ryu_mod,
+                            'ryu.app': ryu_app_mod,
+                            'ryu.app.client': fake_ryu_client,
+                            'ryu.app.rest_nw_id': fake_rest_nw_id})
+
+
+class Net(object):
+    def __init__(self, uuid):
+        self.uuid = uuid
index f971de6a4fe0519303173a9c10521f9792e62257..0bc8d6aaf061a0af123d741b2fd2f39c012c2ae6 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -72,6 +72,7 @@ ovs_plugin_config_path = 'etc/quantum/plugins/openvswitch'
 cisco_plugin_config_path = 'etc/quantum/plugins/cisco'
 linuxbridge_plugin_config_path = 'etc/quantum/plugins/linuxbridge'
 nvp_plugin_config_path = 'etc/quantum/plugins/nicira'
+ryu_plugin_config_path = 'etc/quantum/plugins/ryu'
 
 DataFiles = [
     (config_path,
@@ -90,6 +91,7 @@ DataFiles = [
         ['etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini']),
     (nvp_plugin_config_path,
         ['etc/quantum/plugins/nicira/nvp.ini']),
+    (ryu_plugin_config_path, ['etc/quantum/plugins/ryu/ryu.ini']),
 ]
 
 setup(
@@ -109,10 +111,12 @@ setup(
     eager_resources=EagerResources,
     entry_points={
         'console_scripts': [
-            'quantum-linuxbridge-agent = \
-quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
-            'quantum-openvswitch-agent = \
-quantum.plugins.openvswitch.agent.ovs_quantum_agent:main',
+            'quantum-linuxbridge-agent =' \
+            'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
+            'quantum-openvswitch-agent =' \
+            'quantum.plugins.openvswitch.agent.ovs_quantum_agent:main',
+            'quantum-ryu-agent = ' \
+            'quantum.plugins.ryu.agent.ryu_quantum_agent:main',
             'quantum-server = quantum.server:main',
         ]
     },
index fcddb110236052aaec63787f67fbae315c53a0bb..f13ef0d37dae0481348234915e56d0384708e6b9 100644 (file)
@@ -13,6 +13,7 @@ webtest
 distribute>=0.6.24
 
 coverage
+mock>=0.7.1
 nose
 nosexcover
 pep8==0.6.1