diff --git a/MANIFEST.in b/MANIFEST.in index e207f42845e..088e93efb4b 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 00000000000..3c0d98f5892 --- /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 00000000000..6d732c9f5a7 --- /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 deef3c06e57..1af4bf49b98 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 00000000000..889cbfaaf01 --- /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 00000000000..e69de29bb2d diff --git a/quantum/plugins/ryu/agent/__init__.py b/quantum/plugins/ryu/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..11a2e32f02f --- /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 00000000000..e69de29bb2d diff --git a/quantum/plugins/ryu/db/api.py b/quantum/plugins/ryu/db/api.py new file mode 100644 index 00000000000..caa8b573854 --- /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 00000000000..e31f2051c71 --- /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 00000000000..e69de29bb2d diff --git a/quantum/plugins/ryu/nova/firewall.py b/quantum/plugins/ryu/nova/firewall.py new file mode 100644 index 00000000000..c84f046821f --- /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 00000000000..500d6f03ce9 --- /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 00000000000..91c416ce0e2 --- /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 00000000000..86615ec92b4 --- /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 00000000000..9a49e011165 --- /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 00000000000..c1e227e4189 --- /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 00000000000..49ceeaafd2b --- /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 00000000000..e69de29bb2d diff --git a/quantum/plugins/ryu/tests/unit/__init__.py b/quantum/plugins/ryu/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/quantum/plugins/ryu/tests/unit/basetest.py b/quantum/plugins/ryu/tests/unit/basetest.py new file mode 100644 index 00000000000..0d2e007c24a --- /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 00000000000..55c4853ed20 --- /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 00000000000..5a682cc29da --- /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 00000000000..763b86cf417 --- /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 00000000000..3accc143b17 --- /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 00000000000..37dce46eae7 --- /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 00000000000..e7bf4d72c50 --- /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 f971de6a4fe..0bc8d6aaf06 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 fcddb110236..f13ef0d37da 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