From a5f97ccc3b2f055554d8b9476f5299637a9266d1 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 19 Nov 2011 18:17:03 +0900 Subject: [PATCH] plugin: introduce ryu plugin 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 Change-Id: Ia9fe87525cebccc87b7c18a533f48607272cd97f --- MANIFEST.in | 1 + bin/quantum-ryu-agent | 24 ++ etc/quantum/plugins/ryu/ryu.ini | 13 + quantum/db/api.py | 5 + quantum/plugins/ryu/README | 28 ++ quantum/plugins/ryu/__init__.py | 0 quantum/plugins/ryu/agent/__init__.py | 0 .../plugins/ryu/agent/ryu_quantum_agent.py | 312 ++++++++++++++++++ quantum/plugins/ryu/db/__init__.py | 0 quantum/plugins/ryu/db/api.py | 27 ++ quantum/plugins/ryu/db/models.py | 38 +++ quantum/plugins/ryu/nova/__init__.py | 0 quantum/plugins/ryu/nova/firewall.py | 29 ++ quantum/plugins/ryu/nova/linux_net.py | 90 +++++ quantum/plugins/ryu/nova/vif.py | 80 +++++ quantum/plugins/ryu/ofp_service_type.py | 19 ++ .../plugins/ryu/ovs_quantum_plugin_base.py | 172 ++++++++++ quantum/plugins/ryu/run_tests.py | 84 +++++ quantum/plugins/ryu/ryu_quantum_plugin.py | 68 ++++ quantum/plugins/ryu/tests/__init__.py | 0 quantum/plugins/ryu/tests/unit/__init__.py | 0 quantum/plugins/ryu/tests/unit/basetest.py | 43 +++ quantum/plugins/ryu/tests/unit/fake_plugin.py | 35 ++ .../plugins/ryu/tests/unit/fake_rest_nw_id.py | 17 + .../plugins/ryu/tests/unit/fake_ryu_client.py | 46 +++ .../ryu/tests/unit/test_plugin_base.py | 54 +++ .../plugins/ryu/tests/unit/test_ryu_driver.py | 73 ++++ quantum/plugins/ryu/tests/unit/utils.py | 73 ++++ setup.py | 12 +- tools/pip-requires | 1 + 30 files changed, 1340 insertions(+), 4 deletions(-) create mode 100755 bin/quantum-ryu-agent create mode 100644 etc/quantum/plugins/ryu/ryu.ini create mode 100644 quantum/plugins/ryu/README create mode 100644 quantum/plugins/ryu/__init__.py create mode 100644 quantum/plugins/ryu/agent/__init__.py create mode 100755 quantum/plugins/ryu/agent/ryu_quantum_agent.py create mode 100644 quantum/plugins/ryu/db/__init__.py create mode 100644 quantum/plugins/ryu/db/api.py create mode 100644 quantum/plugins/ryu/db/models.py create mode 100644 quantum/plugins/ryu/nova/__init__.py create mode 100644 quantum/plugins/ryu/nova/firewall.py create mode 100644 quantum/plugins/ryu/nova/linux_net.py create mode 100644 quantum/plugins/ryu/nova/vif.py create mode 100644 quantum/plugins/ryu/ofp_service_type.py create mode 100644 quantum/plugins/ryu/ovs_quantum_plugin_base.py create mode 100644 quantum/plugins/ryu/run_tests.py create mode 100644 quantum/plugins/ryu/ryu_quantum_plugin.py create mode 100644 quantum/plugins/ryu/tests/__init__.py create mode 100644 quantum/plugins/ryu/tests/unit/__init__.py create mode 100644 quantum/plugins/ryu/tests/unit/basetest.py create mode 100644 quantum/plugins/ryu/tests/unit/fake_plugin.py create mode 100644 quantum/plugins/ryu/tests/unit/fake_rest_nw_id.py create mode 100644 quantum/plugins/ryu/tests/unit/fake_ryu_client.py create mode 100644 quantum/plugins/ryu/tests/unit/test_plugin_base.py create mode 100644 quantum/plugins/ryu/tests/unit/test_ryu_driver.py create mode 100644 quantum/plugins/ryu/tests/unit/utils.py diff --git a/MANIFEST.in b/MANIFEST.in index e207f4284..088e93efb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 index 000000000..3c0d98f58 --- /dev/null +++ b/bin/quantum-ryu-agent @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# 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. + +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 index 000000000..6d732c9f5 --- /dev/null +++ b/etc/quantum/plugins/ryu/ryu.ini @@ -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://:@:/ +sql_connection = sqlite:// + +[OVS] +integration-bridge = br-int + +# openflow-controller = : +# openflow-rest-api = : +openflow-controller = 127.0.0.1:6633 +openflow-rest-api = 127.0.0.1:8080 diff --git a/quantum/db/api.py b/quantum/db/api.py index deef3c06e..1af4bf49b 100644 --- a/quantum/db/api.py +++ b/quantum/db/api.py @@ -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 index 000000000..889cbfaaf --- /dev/null +++ b/quantum/plugins/ryu/README @@ -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 index 000000000..e69de29bb diff --git a/quantum/plugins/ryu/agent/__init__.py b/quantum/plugins/ryu/agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/quantum/plugins/ryu/agent/ryu_quantum_agent.py b/quantum/plugins/ryu/agent/ryu_quantum_agent.py new file mode 100755 index 000000000..11a2e32f0 --- /dev/null +++ b/quantum/plugins/ryu/agent/ryu_quantum_agent.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Isaku Yamahata +# 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] " + 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 index 000000000..e69de29bb diff --git a/quantum/plugins/ryu/db/api.py b/quantum/plugins/ryu/db/api.py new file mode 100644 index 000000000..caa8b5738 --- /dev/null +++ b/quantum/plugins/ryu/db/api.py @@ -0,0 +1,27 @@ +# 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. + +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 index 000000000..e31f2051c --- /dev/null +++ b/quantum/plugins/ryu/db/models.py @@ -0,0 +1,38 @@ +# 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. + +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_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 "" % (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 index 000000000..e69de29bb diff --git a/quantum/plugins/ryu/nova/firewall.py b/quantum/plugins/ryu/nova/firewall.py new file mode 100644 index 000000000..c84f04682 --- /dev/null +++ b/quantum/plugins/ryu/nova/firewall.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 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. + +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 index 000000000..500d6f03c --- /dev/null +++ b/quantum/plugins/ryu/nova/linux_net.py @@ -0,0 +1,90 @@ +# 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. + +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 index 000000000..91c416ce0 --- /dev/null +++ b/quantum/plugins/ryu/nova/vif.py @@ -0,0 +1,80 @@ +# 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. + +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 index 000000000..86615ec92 --- /dev/null +++ b/quantum/plugins/ryu/ofp_service_type.py @@ -0,0 +1,19 @@ +# 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 + +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 index 000000000..9a49e0111 --- /dev/null +++ b/quantum/plugins/ryu/ovs_quantum_plugin_base.py @@ -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 index 000000000..c1e227e41 --- /dev/null +++ b/quantum/plugins/ryu/run_tests.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# 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. + + +"""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 index 000000000..49ceeaafd --- /dev/null +++ b/quantum/plugins/ryu/ryu_quantum_plugin.py @@ -0,0 +1,68 @@ +# 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 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 index 000000000..e69de29bb diff --git a/quantum/plugins/ryu/tests/unit/__init__.py b/quantum/plugins/ryu/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/quantum/plugins/ryu/tests/unit/basetest.py b/quantum/plugins/ryu/tests/unit/basetest.py new file mode 100644 index 000000000..0d2e007c2 --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/basetest.py @@ -0,0 +1,43 @@ +# 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. + +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 index 000000000..55c4853ed --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/fake_plugin.py @@ -0,0 +1,35 @@ +# 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. + +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 index 000000000..5a682cc29 --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/fake_rest_nw_id.py @@ -0,0 +1,17 @@ +# Copyright (C) 2011 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2011 Isaku Yamahata +# +# 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 . + +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 index 000000000..763b86cf4 --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/fake_ryu_client.py @@ -0,0 +1,46 @@ +# 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. + + +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 index 000000000..3accc143b --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/test_plugin_base.py @@ -0,0 +1,54 @@ +# 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. + +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 index 000000000..37dce46ea --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/test_ryu_driver.py @@ -0,0 +1,73 @@ +# 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. + +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 index 000000000..e7bf4d72c --- /dev/null +++ b/quantum/plugins/ryu/tests/unit/utils.py @@ -0,0 +1,73 @@ +# 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. + +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 diff --git a/setup.py b/setup.py index f971de6a4..0bc8d6aaf 100644 --- 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', ] }, diff --git a/tools/pip-requires b/tools/pip-requires index fcddb1102..f13ef0d37 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -13,6 +13,7 @@ webtest distribute>=0.6.24 coverage +mock>=0.7.1 nose nosexcover pep8==0.6.1