]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Initial implemention of MetaPlugin
authorNachi Ueno <nachi@nttmcl.com>
Thu, 19 Jul 2012 07:00:05 +0000 (07:00 +0000)
committerNachi Ueno <nachi@nttmcl.com>
Mon, 13 Aug 2012 06:19:31 +0000 (06:19 +0000)
This plugin supports multiple plugins at same time. This plugin is for L3 connectivility
between networks which are realized by different plugins. This plugin add new attribute 'flavor:id'.
flavor:id correspond to specific plugin. flavor-plugin mapping could be configureable by plugin_list config.
This plugin also support extensions. We can map extension to plugin by using extension_map config.

Implements blueprint metaplugin

Change-Id: Ia94d2349fb3ce9f121bbd2505324ae6f0c34247a

21 files changed:
etc/quantum/plugins/metaplugin/metaplugin.ini [new file with mode: 0644]
quantum/agent/linux/interface.py
quantum/extensions/flavor.py [new file with mode: 0644]
quantum/plugins/metaplugin/README [new file with mode: 0644]
quantum/plugins/metaplugin/__init__.py [new file with mode: 0644]
quantum/plugins/metaplugin/agent/linuxbridge_quantum_agent.py [new file with mode: 0755]
quantum/plugins/metaplugin/agent/ovs_quantum_agent.py [new file with mode: 0755]
quantum/plugins/metaplugin/common/__init__.py [new file with mode: 0644]
quantum/plugins/metaplugin/common/config.py [new file with mode: 0644]
quantum/plugins/metaplugin/meta_db_v2.py [new file with mode: 0644]
quantum/plugins/metaplugin/meta_models_v2.py [new file with mode: 0644]
quantum/plugins/metaplugin/meta_quantum_plugin.py [new file with mode: 0644]
quantum/plugins/metaplugin/proxy_quantum_plugin.py [new file with mode: 0644]
quantum/plugins/metaplugin/run_tests.py [new file with mode: 0755]
quantum/plugins/metaplugin/tests/__init__.py [new file with mode: 0644]
quantum/plugins/metaplugin/tests/unit/__init__.py [new file with mode: 0644]
quantum/plugins/metaplugin/tests/unit/basetest.py [new file with mode: 0644]
quantum/plugins/metaplugin/tests/unit/fake_plugin.py [new file with mode: 0644]
quantum/plugins/metaplugin/tests/unit/test_plugin_base.py [new file with mode: 0644]
quantum/tests/unit/test_linux_interface.py
setup.py

diff --git a/etc/quantum/plugins/metaplugin/metaplugin.ini b/etc/quantum/plugins/metaplugin/metaplugin.ini
new file mode 100644 (file)
index 0000000..93366ca
--- /dev/null
@@ -0,0 +1,26 @@
+[DATABASE]
+# This line MUST be changed to actually run the plugin.
+# Example:
+# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum
+# Replace 127.0.0.1 above with the IP address of the database used by the
+# main quantum server. (Leave it as is if the database runs on this host.)
+sql_connection = mysql://root:password@localhost/quantum_metaplugin?charset=utf8
+
+# Database reconnection retry times - in event connectivity is lost
+# set to -1 implgies an infinite retry count
+# sql_max_retries = 10
+# Database reconnection interval in seconds - in event connectivity is lost
+reconnect_interval = 2
+
+[META]
+## This is list of flavor:quantum_plugins
+# extension method is used in the order of this list
+plugin_list= 'openvswitch:quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2,linuxbridge:quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2'
+
+# Default value of flavor
+default_flavor = 'openvswitch'
+
+# supported extentions
+supported_extension_aliases = 'providernet'
+# specific method map for each flavor to extensions
+extension_map = 'get_port_stats:nvp'
index 023c76c382d31a5845b5e1343fbb26c87d3391af..07d2d6234e09a796d477fc7e92fcb78003e6ef16 100644 (file)
@@ -23,8 +23,8 @@ import netaddr
 from quantum.agent.linux import ip_lib
 from quantum.agent.linux import ovs_lib
 from quantum.agent.linux import utils
-from quantum.common import exceptions
 from quantum.openstack.common import cfg
+from quantum.openstack.common import importutils
 
 LOG = logging.getLogger(__name__)
 
@@ -36,7 +36,9 @@ OPTS = [
                help='MTU setting for device.'),
     cfg.StrOpt('ryu_api_host',
                default='127.0.0.1:8080',
-               help='Openflow Ryu REST API host:port')
+               help='Openflow Ryu REST API host:port'),
+    cfg.StrOpt('meta_flavor_driver_mappings',
+               help='Mapping between flavor and LinuxInterfaceDriver')
 ]
 
 
@@ -213,3 +215,62 @@ class RyuInterfaceDriver(OVSInterfaceDriver):
         datapath_id = ovs_br.get_datapath_id()
         port_no = ovs_br.get_port_ofport(device_name)
         self.ryu_client.create_port(network_id, datapath_id, port_no)
+
+
+class MetaInterfaceDriver(LinuxInterfaceDriver):
+    def __init__(self, conf):
+        super(MetaInterfaceDriver, self).__init__(conf)
+        from quantumclient.v2_0 import client
+        self.quantum = client.Client(
+            username=self.conf.admin_user,
+            password=self.conf.admin_password,
+            tenant_name=self.conf.admin_tenant_name,
+            auth_url=self.conf.auth_url,
+            auth_strategy=self.conf.auth_strategy,
+            auth_region=self.conf.auth_region
+        )
+        self.flavor_driver_map = {}
+        for flavor, driver_name in [
+                driver_set.split(':')
+                for driver_set in
+                self.conf.meta_flavor_driver_mappings.split(',')]:
+            self.flavor_driver_map[flavor] =\
+                self._load_driver(driver_name)
+
+    def _get_driver_by_network_id(self, network_id):
+        network = self.quantum.show_network(network_id)
+        flavor = network['network']['flavor:id']
+        return self.flavor_driver_map[flavor]
+
+    def _get_driver_by_device_name(self, device_name):
+        device = ip_lib.IPDevice(device_name, self.conf.root_helper)
+        mac_address = device.link.address
+        ports = self.quantum.list_ports(mac_address=mac_address)
+        if not 'ports' in ports or len(ports['ports']) < 1:
+            raise Exception('No port for this device %s' % device_name)
+        return self._get_driver_by_network_id(ports['ports'][0]['network_id'])
+
+    def get_device_name(self, port):
+        driver = self._get_driver_by_network_id(port.network_id)
+        return driver.get_device_name(port)
+
+    def plug(self, network_id, port_id, device_name, mac_address):
+        driver = self._get_driver_by_network_id(network_id)
+        return driver.plug(network_id, port_id, device_name, mac_address)
+
+    def unplug(self, device_name):
+        driver = self._get_driver_by_device_name(device_name)
+        return driver.unplug(device_name)
+
+    def _load_driver(self, driver_provider):
+        LOG.debug("Driver location:%s", driver_provider)
+        # If the plugin can't be found let them know gracefully
+        try:
+            LOG.info("Loading Driver: %s" % driver_provider)
+            plugin_klass = importutils.import_class(driver_provider)
+        except ClassNotFound:
+            LOG.exception("Error loading driver")
+            raise Exception("driver_provider not found.  You can install a "
+                            "Driver with: pip install <plugin-name>\n"
+                            "Example: pip install quantum-sample-driver")
+        return plugin_klass(self.conf)
diff --git a/quantum/extensions/flavor.py b/quantum/extensions/flavor.py
new file mode 100644 (file)
index 0000000..d3f2a1b
--- /dev/null
@@ -0,0 +1,59 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Nachi Ueno, NTT MCL, 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.
+#
+
+import logging
+
+from quantum.api.v2 import attributes
+
+LOG = logging.getLogger(__name__)
+
+FLAVOR_ATTRIBUTE = {
+    'networks': {
+    'flavor:id': {'allow_post': True,
+                  'allow_put': False,
+                  'is_visible': True,
+                  'default': attributes.ATTR_NOT_SPECIFIED}
+    }
+}
+
+
+class Flavor(object):
+    @classmethod
+    def get_name(cls):
+        return "Flavor for each network"
+
+    @classmethod
+    def get_alias(cls):
+        return "flavor"
+
+    @classmethod
+    def get_description(cls):
+        return "Flavor"
+
+    @classmethod
+    def get_namespace(cls):
+        return "http://docs.openstack.org/ext/flavor/api/v1.0"
+
+    @classmethod
+    def get_updated(cls):
+        return "2012-07-20T10:00:00-00:00"
+
+    def get_extended_attributes(self, version):
+        if version == "2.0":
+            return FLAVOR_ATTRIBUTE
+        else:
+            return {}
diff --git a/quantum/plugins/metaplugin/README b/quantum/plugins/metaplugin/README
new file mode 100644 (file)
index 0000000..7fc0a6d
--- /dev/null
@@ -0,0 +1,81 @@
+# -- Background
+
+This plugin support multiple plugin at same time. This plugin is for L3 connectivility
+between networks which are realized by different plugins.This plugin add new attribute 'flavor:id'.
+flavor:id correspond to specific plugin ( flavor-plugin mapping could be configureable by plugin_list config.
+This plugin also support extensions. We can map extension to plugin by using extension_map config.
+
+[DATABASE]
+# This line MUST be changed to actually run the plugin.
+# Example:
+# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum
+# Replace 127.0.0.1 above with the IP address of the database used by the
+# main quantum server. (Leave it as is if the database runs on this host.)
+sql_connection = mysql://root:password@localhost/quantum_metaplugin?charset=utf8
+
+# Database reconnection retry times - in event connectivity is lost
+# set to -1 implgies an infinite retry count
+# sql_max_retries = 10
+# Database reconnection interval in seconds - in event connectivity is lost
+reconnect_interval = 2
+
+[META]
+## This is list of flavor:quantum_plugins
+# extension method is used in the order of this list
+plugin_list= 'openvswitch:quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2,linuxbridge:quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2'
+
+# Default value of flavor
+default_flavor = 'openvswitch'
+
+# supported extentions
+supported_extension_aliases = 'providernet'
+# specific method map for each flavor to extensions
+extension_map = 'get_port_stats:nvp'
+
+# -- BridgeDriver Configration
+# In order to use metaplugin, you should use MetaDriver. Following configation is needed.
+
+[DEFAULT]
+# Meta Plugin
+# Mapping between flavor and driver
+meta_flavor_driver_mappings = openvswitch:quantum.agent.linux.interface.OVSInterfaceDriver, linuxbridge:quantum.agent.linux.interface.BridgeInterfaceDriver
+# interface driver for MetaPlugin
+interface_driver = quantum.agent.linux.interface.MetaInterfaceDriver
+
+# -- Agent
+Agents for Metaplugin are in quantum/plugins/metaplugin/agent
+linuxbridge_quantum_agent and ovs_quantum_agent is available.
+
+# -- Extensions
+
+- flavor
+MetaPlugin supports flavor and provider net extension.
+Metaplugin select plugin_list using flavor.
+One plugin may use multiple flavor value. If the plugin support flavor, it may provide
+multiple flavor of network.
+
+- Attribute extension
+Each plugin can use attribute extension such as provider_net, if you specify that in supported_extension_aliases.
+
+- providernet
+Vlan ID range of each plugin should be different, since Metaplugin dose not manage that.
+
+#- limitations
+
+Basically, All plugin should inherit QuantumDBPluginV2.
+Metaplugin assumes all plugin share same Database expecially for IPAM part in QuantumV2 API.
+You can use another plugin if you use ProxyPluginV2, which proxies request to the another quantum server.
+
+Example flavor configration for ProxyPluginV2
+
+meta_flavor_driver_mappings = "openvswitch:quantum.agent.linux.interface.OVSInterfaceDriver,proxy:quantum.plugins.metaplugin.proxy_quantum_plugin.ProxyPluginV2"
+
+[Proxy]
+auth_url = http://10.0.0.1:35357/v2.0
+auth_region = RegionOne
+admin_tenant_name = service
+admin_user =      quantum
+admin_password = password
+
+
+
diff --git a/quantum/plugins/metaplugin/__init__.py b/quantum/plugins/metaplugin/__init__.py
new file mode 100644 (file)
index 0000000..d8bce77
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
diff --git a/quantum/plugins/metaplugin/agent/linuxbridge_quantum_agent.py b/quantum/plugins/metaplugin/agent/linuxbridge_quantum_agent.py
new file mode 100755 (executable)
index 0000000..fff37ff
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Cisco Systems, Inc.
+# Copyright 2012 NTT MCL, 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.
+#
+#
+# Performs per host Linux Bridge configuration for Quantum.
+# Based on the structure of the OpenVSwitch agent in the
+# Quantum OpenVSwitch Plugin.
+# @author: Sumit Naiksatam, Cisco Systems, Inc.
+
+import logging
+import sys
+import time
+
+from sqlalchemy.ext.sqlsoup import SqlSoup
+
+from quantum.openstack.common import cfg
+from quantum.common import config as logging_config
+from quantum.plugins.linuxbridge.common import config
+import quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent as lb
+
+from quantum.agent.linux import utils
+
+logging.basicConfig()
+LOG = logging.getLogger(__name__)
+
+BRIDGE_NAME_PREFIX = "brq"
+VLAN_BINDINGS = "vlan_bindings"
+PORT_BINDINGS = "port_bindings"
+OP_STATUS_UP = "UP"
+OP_STATUS_DOWN = "DOWN"
+# Default inteval values
+DEFAULT_POLLING_INTERVAL = 2
+DEFAULT_RECONNECT_INTERVAL = 2
+
+
+class MetaLinuxBridgeQuantumAgent(lb.LinuxBridgeQuantumAgent):
+
+    def manage_networks_on_host(self, db,
+                                old_vlan_bindings,
+                                old_port_bindings):
+        vlan_bindings = {}
+        try:
+            flavor_key = db.flavors.network_id
+            vlan_key = db.vlan_bindings.network_id
+            query = db.session.query(db.vlan_bindings)
+            joined = query.join((db.flavors,
+                                 flavor_key == vlan_key))
+            where = db.flavors.flavor == 'linuxbridge'
+            vlan_binds = joined.filter(where).all()
+        except Exception as e:
+            LOG.info("Unable to get vlan bindings! Exception: %s" % e)
+            self.db_connected = False
+            return {VLAN_BINDINGS: {},
+                    PORT_BINDINGS: []}
+
+        vlans_string = ""
+        for bind in vlan_binds:
+            entry = {'network_id': bind.network_id, 'vlan_id': bind.vlan_id}
+            vlan_bindings[bind.network_id] = entry
+            vlans_string = "%s %s" % (vlans_string, entry)
+
+        port_bindings = []
+        try:
+            flavor_key = db.flavors.network_id
+            port_key = db.ports.network_id
+            query = db.session.query(db.ports)
+            joined = query.join((db.flavors,
+                                 flavor_key == port_key))
+            where = db.flavors.flavor == 'linuxbridge'
+            port_binds = joined.filter(where).all()
+        except Exception as e:
+            LOG.info("Unable to get port bindings! Exception: %s" % e)
+            self.db_connected = False
+            return {VLAN_BINDINGS: {},
+                    PORT_BINDINGS: []}
+
+        all_bindings = {}
+        for bind in port_binds:
+            append_entry = False
+            if self.target_v2_api:
+                all_bindings[bind.id] = bind
+                entry = {'network_id': bind.network_id,
+                         'uuid': bind.id,
+                         'status': bind.status,
+                         'interface_id': bind.id}
+                append_entry = bind.admin_state_up
+            else:
+                all_bindings[bind.uuid] = bind
+                entry = {'network_id': bind.network_id, 'state': bind.state,
+                         'op_status': bind.op_status, 'uuid': bind.uuid,
+                         'interface_id': bind.interface_id}
+                append_entry = bind.state == 'ACTIVE'
+            if append_entry:
+                port_bindings.append(entry)
+
+        plugged_interfaces = []
+        ports_string = ""
+        for pb in port_bindings:
+            ports_string = "%s %s" % (ports_string, pb)
+            port_id = pb['uuid']
+            interface_id = pb['interface_id']
+
+            vlan_id = str(vlan_bindings[pb['network_id']]['vlan_id'])
+            if self.process_port_binding(port_id,
+                                         pb['network_id'],
+                                         interface_id,
+                                         vlan_id):
+                if self.target_v2_api:
+                    all_bindings[port_id].status = OP_STATUS_UP
+                else:
+                    all_bindings[port_id].op_status = OP_STATUS_UP
+
+            plugged_interfaces.append(interface_id)
+
+        if old_port_bindings != port_bindings:
+            LOG.debug("Port-bindings: %s" % ports_string)
+
+        self.process_unplugged_interfaces(plugged_interfaces)
+
+        if old_vlan_bindings != vlan_bindings:
+            LOG.debug("VLAN-bindings: %s" % vlans_string)
+
+        self.process_deleted_networks(vlan_bindings)
+
+        try:
+            db.commit()
+        except Exception as e:
+            LOG.info("Unable to update database! Exception: %s" % e)
+            db.rollback()
+            vlan_bindings = {}
+            port_bindings = []
+
+        return {VLAN_BINDINGS: vlan_bindings,
+                PORT_BINDINGS: port_bindings}
+
+
+def main():
+    cfg.CONF(args=sys.argv, project='quantum')
+
+    # (TODO)  - swap with common logging
+    logging_config.setup_logging(cfg.CONF)
+
+    br_name_prefix = BRIDGE_NAME_PREFIX
+    physical_interface = cfg.CONF.LINUX_BRIDGE.physical_interface
+    polling_interval = cfg.CONF.AGENT.polling_interval
+    reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
+    root_helper = cfg.CONF.AGENT.root_helper
+    'Establish database connection and load models'
+    db_connection_url = cfg.CONF.DATABASE.sql_connection
+    plugin = MetaLinuxBridgeQuantumAgent(br_name_prefix, physical_interface,
+                                         polling_interval, reconnect_interval,
+                                         root_helper,
+                                         cfg.CONF.AGENT.target_v2_api)
+    LOG.info("Agent initialized successfully, now running... ")
+    plugin.daemon_loop(db_connection_url)
+
+    sys.exit(0)
+
+if __name__ == "__main__":
+    main()
diff --git a/quantum/plugins/metaplugin/agent/ovs_quantum_agent.py b/quantum/plugins/metaplugin/agent/ovs_quantum_agent.py
new file mode 100755 (executable)
index 0000000..2d42c03
--- /dev/null
@@ -0,0 +1,194 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# 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: Somik Behera, Nicira Networks, Inc.
+# @author: Brad Hall, Nicira Networks, Inc.
+# @author: Dan Wendlandt, Nicira Networks, Inc.
+# @author: Dave Lapsley, Nicira Networks, Inc.
+# @author: Aaron Rosen, Nicira Networks, Inc.
+
+import logging
+import sys
+import time
+
+from sqlalchemy.ext import sqlsoup
+
+from quantum.agent.linux import ovs_lib
+from quantum.common import config as logging_config
+from quantum.openstack.common import cfg
+from quantum.plugins.openvswitch.common import config
+from quantum.plugins.openvswitch.agent.ovs_quantum_agent import OVSQuantumAgent
+
+logging.basicConfig()
+LOG = logging.getLogger(__name__)
+
+# Global constants.
+OP_STATUS_UP = "UP"
+OP_STATUS_DOWN = "DOWN"
+
+# A placeholder for dead vlans.
+DEAD_VLAN_TAG = "4095"
+
+# Default interval values
+DEFAULT_POLLING_INTERVAL = 2
+DEFAULT_RECONNECT_INTERVAL = 2
+
+
+class MetaOVSQuantumAgent(OVSQuantumAgent):
+
+    def daemon_loop(self, db_connection_url):
+        '''Main processing loop for Non-Tunneling Agent.
+
+        :param options: database information - in the event need to reconnect
+        '''
+        self.local_vlan_map = {}
+        old_local_bindings = {}
+        old_vif_ports = {}
+        db_connected = False
+
+        while True:
+            if not db_connected:
+                time.sleep(self.reconnect_interval)
+                db = sqlsoup.SqlSoup(db_connection_url)
+                db_connected = True
+                LOG.info("Connecting to database \"%s\" on %s" %
+                         (db.engine.url.database, db.engine.url.host))
+
+            all_bindings = {}
+            try:
+                flavor_key = db.flavors.network_id
+                port_key = db.ports.network_id
+                query = db.session.query(db.ports)
+                joined = query.join((db.flavors,
+                                     flavor_key == port_key))
+                where = db.flavors.flavor == 'openvswitch'
+                ports = joined.filter(where).all()
+            except Exception, e:
+                LOG.info("Unable to get port bindings! Exception: %s" % e)
+                db_connected = False
+                continue
+
+            for port in ports:
+                if self.target_v2_api:
+                    all_bindings[port.id] = port
+                else:
+                    all_bindings[port.interface_id] = port
+
+            vlan_bindings = {}
+            try:
+                flavor_key = db.flavors.network_id
+                vlan_key = db.vlan_bindings.network_id
+                query = db.session.query(db.vlan_bindings)
+                joined = query.join((db.flavors,
+                                     flavor_key == vlan_key))
+                where = db.flavors.flavor == 'openvswitch'
+                vlan_binds = joined.filter(where).all()
+            except Exception, e:
+                LOG.info("Unable to get vlan bindings! Exception: %s" % e)
+                db_connected = False
+                continue
+
+            for bind in vlan_binds:
+                vlan_bindings[bind.network_id] = bind.vlan_id
+
+            new_vif_ports = {}
+            new_local_bindings = {}
+            vif_ports = self.int_br.get_vif_ports()
+            for p in vif_ports:
+                new_vif_ports[p.vif_id] = p
+                if p.vif_id in all_bindings:
+                    net_id = all_bindings[p.vif_id].network_id
+                    new_local_bindings[p.vif_id] = net_id
+                else:
+                    # no binding, put him on the 'dead vlan'
+                    self.int_br.set_db_attribute("Port", p.port_name, "tag",
+                                                 DEAD_VLAN_TAG)
+                    self.int_br.add_flow(priority=2,
+                                         in_port=p.ofport,
+                                         actions="drop")
+
+                old_b = old_local_bindings.get(p.vif_id, None)
+                new_b = new_local_bindings.get(p.vif_id, None)
+
+                if old_b != new_b:
+                    if old_b is not None:
+                        LOG.info("Removing binding to net-id = %s for %s"
+                                 % (old_b, str(p)))
+                        self.port_unbound(p, True)
+                        if p.vif_id in all_bindings:
+                            all_bindings[p.vif_id].status = OP_STATUS_DOWN
+                    if new_b is not None:
+                        # If we don't have a binding we have to stick it on
+                        # the dead vlan
+                        net_id = all_bindings[p.vif_id].network_id
+                        vlan_id = vlan_bindings.get(net_id, DEAD_VLAN_TAG)
+                        self.port_bound(p, vlan_id)
+                        if p.vif_id in all_bindings:
+                            all_bindings[p.vif_id].status = OP_STATUS_UP
+                        LOG.info(("Adding binding to net-id = %s "
+                                  "for %s on vlan %s") %
+                                 (new_b, str(p), vlan_id))
+
+            for vif_id in old_vif_ports:
+                if vif_id not in new_vif_ports:
+                    LOG.info("Port Disappeared: %s" % vif_id)
+                    if vif_id in old_local_bindings:
+                        old_b = old_local_bindings[vif_id]
+                        self.port_unbound(old_vif_ports[vif_id], False)
+                    if vif_id in all_bindings:
+                        all_bindings[vif_id].status = OP_STATUS_DOWN
+
+            old_vif_ports = new_vif_ports
+            old_local_bindings = new_local_bindings
+            try:
+                db.commit()
+            except Exception, e:
+                LOG.info("Unable to commit to database! Exception: %s" % e)
+                db.rollback()
+                old_local_bindings = {}
+                old_vif_ports = {}
+
+            time.sleep(self.polling_interval)
+
+
+def main():
+    cfg.CONF(args=sys.argv, project='quantum')
+
+    # (TODO) gary - swap with common logging
+    logging_config.setup_logging(cfg.CONF)
+
+    # Determine which agent type to use.
+    enable_tunneling = cfg.CONF.OVS.enable_tunneling
+    integ_br = cfg.CONF.OVS.integration_bridge
+    db_connection_url = cfg.CONF.DATABASE.sql_connection
+    polling_interval = cfg.CONF.AGENT.polling_interval
+    reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
+    root_helper = cfg.CONF.AGENT.root_helper
+
+    # Determine API Version to use
+    target_v2_api = cfg.CONF.AGENT.target_v2_api
+
+    # Get parameters for OVSQuantumAgent.
+    plugin = MetaOVSQuantumAgent(integ_br, root_helper, polling_interval,
+                                 reconnect_interval, target_v2_api)
+
+    # Start everything.
+    plugin.daemon_loop(db_connection_url)
+
+    sys.exit(0)
+
+if __name__ == "__main__":
+    main()
diff --git a/quantum/plugins/metaplugin/common/__init__.py b/quantum/plugins/metaplugin/common/__init__.py
new file mode 100644 (file)
index 0000000..d8bce77
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
diff --git a/quantum/plugins/metaplugin/common/config.py b/quantum/plugins/metaplugin/common/config.py
new file mode 100644 (file)
index 0000000..f5a4103
--- /dev/null
@@ -0,0 +1,45 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+from quantum.openstack.common import cfg
+
+
+database_opts = [
+    cfg.StrOpt('sql_connection', default='sqlite://'),
+    cfg.IntOpt('sql_max_retries', default=-1),
+    cfg.IntOpt('reconnect_interval', default=2),
+]
+
+meta_plugin_opts = [
+    cfg.StrOpt('plugin_list', default=''),
+    cfg.StrOpt('default_flavor', default=''),
+    cfg.StrOpt('supported_extension_aliases', default=''),
+    cfg.StrOpt('extension_map', default='')
+]
+
+proxy_plugin_opts = [
+    cfg.StrOpt('admin_user'),
+    cfg.StrOpt('admin_password'),
+    cfg.StrOpt('admin_tenant_name'),
+    cfg.StrOpt('auth_url'),
+    cfg.StrOpt('auth_strategy', default='keystone'),
+    cfg.StrOpt('auth_region'),
+]
+
+cfg.CONF.register_opts(database_opts, "DATABASE")
+cfg.CONF.register_opts(meta_plugin_opts, "META")
+cfg.CONF.register_opts(proxy_plugin_opts, "PROXY")
diff --git a/quantum/plugins/metaplugin/meta_db_v2.py b/quantum/plugins/metaplugin/meta_db_v2.py
new file mode 100644 (file)
index 0000000..5d2fcc6
--- /dev/null
@@ -0,0 +1,40 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+from sqlalchemy.orm import exc
+
+import quantum.db.api as db
+from quantum.plugins.metaplugin import meta_models_v2
+
+
+def get_flavor_by_network(net_id):
+    session = db.get_session()
+    try:
+        binding = (session.query(meta_models_v2.Flavor).
+                   filter_by(network_id=net_id).
+                   one())
+    except exc.NoResultFound:
+        return None
+    return binding.flavor
+
+
+def add_flavor_binding(flavor, net_id):
+    session = db.get_session()
+    binding = meta_models_v2.Flavor(flavor=flavor, network_id=net_id)
+    session.add(binding)
+    session.flush()
+    return binding
diff --git a/quantum/plugins/metaplugin/meta_models_v2.py b/quantum/plugins/metaplugin/meta_models_v2.py
new file mode 100644 (file)
index 0000000..6f91dfa
--- /dev/null
@@ -0,0 +1,32 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+import sqlalchemy as sa
+from sqlalchemy import Column, String
+
+from quantum.db import models_v2
+
+
+class Flavor(models_v2.model_base.BASEV2):
+    """Represents a binding of network_id to flavor."""
+    flavor = Column(String(255))
+    network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id',
+                                                        ondelete="CASCADE"),
+                           primary_key=True)
+
+    def __repr__(self):
+        return "<Flavor(%s,%s)>" % (self.flavor, self.network_id)
diff --git a/quantum/plugins/metaplugin/meta_quantum_plugin.py b/quantum/plugins/metaplugin/meta_quantum_plugin.py
new file mode 100644 (file)
index 0000000..7b2300b
--- /dev/null
@@ -0,0 +1,216 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+import logging
+
+from quantum.common import exceptions as exc
+
+from quantum.api.v2 import attributes
+from quantum.common.utils import find_config_file
+from quantum.db import api as db
+from quantum.db import db_base_plugin_v2
+from quantum.db import models_v2
+from quantum.openstack.common import cfg
+from quantum.openstack.common import importutils
+from quantum.plugins.metaplugin.common import config
+from quantum.plugins.metaplugin import meta_db_v2
+from quantum.plugins.metaplugin.meta_models_v2 import Flavor
+from quantum import policy
+
+LOG = logging.getLogger("metaplugin")
+
+
+class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
+    def __init__(self, configfile=None):
+        LOG.debug("Start initializing metaplugin")
+        options = {"sql_connection": cfg.CONF.DATABASE.sql_connection}
+        options.update({'base': models_v2.model_base.BASEV2})
+        sql_max_retries = cfg.CONF.DATABASE.sql_max_retries
+        options.update({"sql_max_retries": sql_max_retries})
+        reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
+        options.update({"reconnect_interval": reconnect_interval})
+        self.supported_extension_aliases = \
+            cfg.CONF.META.supported_extension_aliases.split(',')
+        self.supported_extension_aliases.append('flavor')
+
+        # Ignore config option overapping
+        def _is_opt_registered(opts, opt):
+            if opt.dest in opts:
+                return True
+            else:
+                return False
+
+        cfg._is_opt_registered = _is_opt_registered
+
+        # Keep existing tables if multiple plugin use same table name.
+        db.model_base.QuantumBase.__table_args__ = {'keep_existing': True}
+
+        self.plugins = {}
+
+        plugin_list = [plugin_set.split(':')
+                       for plugin_set
+                       in cfg.CONF.META.plugin_list.split(',')]
+        for flavor, plugin_provider in plugin_list:
+            self.plugins[flavor] = self._load_plugin(plugin_provider)
+
+        self.extension_map = {}
+        if not cfg.CONF.META.extension_map == '':
+            extension_list = [method_set.split(':')
+                              for method_set
+                              in cfg.CONF.META.extension_map.split(',')]
+            for method_name, flavor in extension_list:
+                self.extension_map[method_name] = flavor
+
+        self.default_flavor = cfg.CONF.META.default_flavor
+
+        if not self.default_flavor in self.plugins:
+            raise exc.Invalid('default_flavor %s is not plugin list' %
+                              self.default_flavor)
+
+    def _load_plugin(self, plugin_provider):
+        LOG.debug("Plugin location:%s", plugin_provider)
+        # If the plugin can't be found let them know gracefully
+        try:
+            LOG.info("Loading Plugin: %s" % plugin_provider)
+            plugin_klass = importutils.import_class(plugin_provider)
+        except exc.ClassNotFound:
+            LOG.exception("Error loading plugin")
+            raise Exception("Plugin not found.  You can install a "
+                            "plugin with: pip install <plugin-name>\n"
+                            "Example: pip install quantum-sample-plugin")
+        return plugin_klass()
+
+    def _get_plugin(self, flavor):
+        if not flavor in self.plugins:
+            raise Exception("Plugin for flavor %s not found." % flavor)
+        return self.plugins[flavor]
+
+    def __getattr__(self, key):
+        # At first,  try to pickup extension command from extension_map
+
+        if key in self.extension_map:
+            flavor = self.extension_map[key]
+            plugin = self._get_plugin(flavor)
+            if plugin and hasattr(plugin, key):
+                return getattr(plugin, key)
+
+        # Second, try to match extension method in order of pluign list
+
+        for flavor, plugin in self.plugins.items():
+            if hasattr(plugin, key):
+                return getattr(plugin, key)
+
+        # if no plugin support the method, then raise
+        raise AttributeError
+
+    def _extend_network_dict(self, context, network):
+        network['flavor:id'] = self._get_flavor_by_network_id(network['id'])
+
+    def create_network(self, context, network):
+        n = network['network']
+        flavor = n.get('flavor:id')
+        if not str(flavor) in self.plugins:
+            flavor = self.default_flavor
+        plugin = self._get_plugin(flavor)
+        net = plugin.create_network(context, network)
+        LOG.debug("Created network: %s with flavor %s " % (net['id'], flavor))
+        try:
+            meta_db_v2.add_flavor_binding(flavor, str(net['id']))
+        except Exception as e:
+            LOG.error('failed to add flavor bindings')
+            plugin.delete_network(context, net['id'])
+            raise Exception('Failed to create network')
+
+        LOG.debug("Created network: %s" % net['id'])
+        self._extend_network_dict(context, net)
+        return net
+
+    def delete_network(self, context, id):
+        flavor = meta_db_v2.get_flavor_by_network(id)
+        plugin = self._get_plugin(flavor)
+        return plugin.delete_network(context, id)
+
+    def get_network(self, context, id, fields=None, verbose=None):
+        flavor = meta_db_v2.get_flavor_by_network(id)
+        plugin = self._get_plugin(flavor)
+        net = plugin.get_network(context, id, fields, verbose)
+        if not fields or 'flavor:id' in fields:
+            self._extend_network_dict(context, net)
+        return net
+
+    def get_networks_with_flavor(self, context, filters=None,
+                                 fields=None, verbose=None):
+        collection = self._model_query(context, models_v2.Network)
+        collection = collection.join(Flavor,
+                                     models_v2.Network.id == Flavor.network_id)
+        if filters:
+            for key, value in filters.iteritems():
+                if key == 'flavor:id':
+                    column = Flavor.flavor
+                else:
+                    column = getattr(models_v2.Network, key, None)
+                if column:
+                    collection = collection.filter(column.in_(value))
+        return [self._make_network_dict(c, fields) for c in collection.all()]
+
+    def get_networks(self, context, filters=None, fields=None, verbose=None):
+        nets = self.get_networks_with_flavor(context, filters,
+                                             None, verbose)
+        return [self.get_network(context, net['id'],
+                                 fields, verbose)
+                for net in nets]
+
+    def _get_flavor_by_network_id(self, network_id):
+        return meta_db_v2.get_flavor_by_network(network_id)
+
+    def _get_plugin_by_network_id(self, network_id):
+        flavor = self._get_flavor_by_network_id(network_id)
+        return self._get_plugin(flavor)
+
+    def create_port(self, context, port):
+        p = port['port']
+        if not 'network_id' in p:
+            raise exc.NotFound
+        plugin = self._get_plugin_by_network_id(p['network_id'])
+        return plugin.create_port(context, port)
+
+    def update_port(self, context, id, port):
+        port_in_db = self.get_port(context, id)
+        plugin = self._get_plugin_by_network_id(port_in_db['network_id'])
+        return plugin.update_port(context, id, port)
+
+    def delete_port(self, context, id):
+        port_in_db = self.get_port(context, id)
+        plugin = self._get_plugin_by_network_id(port_in_db['network_id'])
+        return plugin.delete_port(context, id)
+
+    def create_subnet(self, context, subnet):
+        s = subnet['subnet']
+        if not 'network_id' in s:
+            raise exc.NotFound
+        plugin = self._get_plugin_by_network_id(s['network_id'])
+        return plugin.create_subnet(context, subnet)
+
+    def update_subnet(self, context, id, subnet):
+        s = self.get_subnet(context, id)
+        plugin = self._get_plugin_by_network_id(s['network_id'])
+        return plugin.update_subnet(context, id, subnet)
+
+    def delete_subnet(self, context, id):
+        s = self.get_subnet(context, id)
+        plugin = self._get_plugin_by_network_id(s['network_id'])
+        return plugin.delete_subnet(context, id)
diff --git a/quantum/plugins/metaplugin/proxy_quantum_plugin.py b/quantum/plugins/metaplugin/proxy_quantum_plugin.py
new file mode 100644 (file)
index 0000000..8464146
--- /dev/null
@@ -0,0 +1,132 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+from quantum.db import api as db
+from quantum.db import db_base_plugin_v2
+from quantum.db import models_v2
+from quantum.openstack.common import cfg
+from quantumclient.common import exceptions
+from quantumclient.v2_0 import client
+
+
+class ProxyPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
+    def __init__(self, configfile=None):
+        options = {"sql_connection": cfg.CONF.DATABASE.sql_connection}
+        options.update({'base': models_v2.model_base.BASEV2})
+        sql_max_retries = cfg.CONF.DATABASE.sql_max_retries
+        options.update({"sql_max_retries": sql_max_retries})
+        reconnect_interval = cfg.CONF.DATABASE.reconnect_interval
+        options.update({"reconnect_interval": reconnect_interval})
+        db.configure_db(options)
+        self.quantum = client.Client(
+            username=cfg.CONF.PROXY.admin_user,
+            password=cfg.CONF.PROXY.admin_password,
+            tenant_name=cfg.CONF.PROXY.admin_tenant_name,
+            auth_url=cfg.CONF.PROXY.auth_url,
+            auth_strategy=cfg.CONF.PROXY.auth_strategy,
+            auth_region=cfg.CONF.PROXY.auth_region
+        )
+
+    def _get_client(self):
+        return self.quantum
+
+    def create_subnet(self, context, subnet):
+        subnet_remote = self._get_client().create_subnet(subnet)
+        subnet['subnet']['id'] = subnet_remote['id']
+        tenant_id = self._get_tenant_id_for_create(context, subnet['subnet'])
+        subnet['subnet']['tenant_id'] = tenant_id
+        try:
+            subnet_in_db = super(ProxyPluginV2, self).create_subnet(
+                context, subnet)
+        except:
+            self._get_client().delete_subnet(subnet_remote['id'])
+        return subnet_in_db
+
+    def update_subnet(self, context, id, subnet):
+        subnet_in_db = super(ProxyPluginV2, self).update_subnet(
+            context, id, subnet)
+        try:
+            self._get_client().update_subnet(id, subnet)
+        except Exception as e:
+            LOG.error("update subnet failed: %e" % e)
+        return subnet_in_db
+
+    def delete_subnet(self, context, id):
+        try:
+            self._get_client().delete_subnet(id)
+        except exceptions.NotFound:
+            LOG.warn("subnet in remote have already deleted")
+            pass
+        return super(ProxyPluginV2, self).delete_subnet(context, id)
+
+    def create_network(self, context, network):
+        network_remote = self._get_client().create_network(network)
+        network['network']['id'] = network_remote['id']
+        tenant_id = self._get_tenant_id_for_create(context, network['network'])
+        network['network']['tenant_id'] = tenant_id
+        try:
+            network_in_db = super(ProxyPluginV2, self).create_network(
+                context, network)
+        except:
+            self._get_client().delete_network(network_remote['id'])
+        return network_in_db
+
+    def update_network(self, context, id, network):
+        network_in_db = super(ProxyPluginV2, self).update_network(
+            context, id, network)
+        try:
+            self._get_client().update_network(id, network)
+        except Exception as e:
+            LOG.error("update network failed: %e" % e)
+        return network_in_db
+
+    def delete_network(self, context, id):
+        try:
+            self._get_client().delete_network(id)
+        except exceptions.NetworkNotFound:
+            LOG.warn("network in remote have already deleted")
+            pass
+        return super(ProxyPluginV2, self).delete_network(context, id)
+
+    def create_port(self, context, port):
+        port_remote = self._get_client().create_port(port)
+        port['port']['id'] = port_remote['id']
+        tenant_id = self._get_tenant_id_for_create(context, port['port'])
+        port['port']['tenant_id'] = tenant_id
+        try:
+            port_in_db = super(ProxyPluginV2, self).create_port(
+                context, port)
+        except:
+            self._get_client().delete_port(port_remote['id'])
+        return port_in_db
+
+    def update_port(self, context, id, port):
+        port_in_db = super(ProxyPluginV2, self).update_port(
+            context, id, port)
+        try:
+            self._get_client().update_port(id, port)
+        except Exception as e:
+            LOG.error("update port failed: %e" % e)
+        return port_in_db
+
+    def delete_port(self, context, id):
+        try:
+            self._get_client().delete_port(id)
+        except exceptions.portNotFound:
+            LOG.warn("port in remote have already deleted")
+            pass
+        return super(ProxyPluginV2, self).delete_port(context, id)
diff --git a/quantum/plugins/metaplugin/run_tests.py b/quantum/plugins/metaplugin/run_tests.py
new file mode 100755 (executable)
index 0000000..0576f11
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack, LLC
+# 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 Meta plugin
+
+This file should be run from the top dir in the quantum directory
+
+To run all tests::
+    PLUGIN_DIR=quantum/plugins/metaplugin ./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__))
+
+from quantum.common.test_lib import run_tests, test_config
+
+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'] = "meta_quantum_plugin.MetaPluginV2"
+
+    cwd = os.getcwd()
+
+    working_dir = os.path.abspath("quantum/plugins/metaplugin")
+    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/metaplugin/tests/__init__.py b/quantum/plugins/metaplugin/tests/__init__.py
new file mode 100644 (file)
index 0000000..d8bce77
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
diff --git a/quantum/plugins/metaplugin/tests/unit/__init__.py b/quantum/plugins/metaplugin/tests/unit/__init__.py
new file mode 100644 (file)
index 0000000..d8bce77
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
diff --git a/quantum/plugins/metaplugin/tests/unit/basetest.py b/quantum/plugins/metaplugin/tests/unit/basetest.py
new file mode 100644 (file)
index 0000000..06b9d76
--- /dev/null
@@ -0,0 +1,44 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+import mox
+import stubout
+import unittest
+
+import quantum.db.api as db
+from quantum.db import models_v2
+from quantum.plugins.metaplugin.tests.unit import utils
+
+
+class BaseMetaTest(unittest.TestCase):
+    """base test class for MetaPlugin unit tests"""
+    def setUp(self):
+        config = utils.get_config()
+        options = {"sql_connection": config.get("DATABASE", "sql_connection")}
+        options.update({'base': models_v2.model_base.BASEV2})
+        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/metaplugin/tests/unit/fake_plugin.py b/quantum/plugins/metaplugin/tests/unit/fake_plugin.py
new file mode 100644 (file)
index 0000000..8fd252c
--- /dev/null
@@ -0,0 +1,57 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, Inc.
+#
+#    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.common import exceptions as q_exc
+from quantum.common.utils import find_config_file
+from quantum.db import api as db
+from quantum.db import db_base_plugin_v2
+from quantum.db import models_v2
+
+
+class Fake1(db_base_plugin_v2.QuantumDbPluginV2):
+    def fake_func(self):
+        return 'fake1'
+
+    def create_network(self, context, network):
+        net = super(Fake1, self).create_network(context, network)
+        return net
+
+    def delete_network(self, context, id):
+        return super(Fake1, self).delete_network(context, id)
+
+    def create_port(self, context, port):
+        port['port']['device_id'] = self.fake_func()
+        port = super(Fake1, self).create_port(context, port)
+        return port
+
+    def create_subnet(self, context, subnet):
+        subnet = super(Fake1, self).create_subnet(context, subnet)
+        return subnet
+
+    def update_port(self, context, id, port):
+        port = super(Fake1, self).update_port(context, id, port)
+        return port
+
+    def delete_port(self, context, id):
+        return super(Fake1, self).delete_port(context, id)
+
+
+class Fake2(Fake1):
+    def fake_func(self):
+        return 'fake2'
+
+    def fake_func2(self):
+        return 'fake2'
diff --git a/quantum/plugins/metaplugin/tests/unit/test_plugin_base.py b/quantum/plugins/metaplugin/tests/unit/test_plugin_base.py
new file mode 100644 (file)
index 0000000..0b313ab
--- /dev/null
@@ -0,0 +1,268 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012, Nachi Ueno, NTT MCL, 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.
+
+import os
+
+import mox
+import mock
+import uuid
+
+from quantum.common import config
+from quantum.common.exceptions import NotImplementedError
+from quantum.db import api as db
+from quantum.openstack.common import cfg
+from quantum.plugins.metaplugin.meta_quantum_plugin import MetaPluginV2
+from quantum.plugins.metaplugin.proxy_quantum_plugin import ProxyPluginV2
+from quantum.plugins.metaplugin.tests.unit.basetest import BaseMetaTest
+from quantum.plugins.metaplugin.tests.unit import fake_plugin
+from quantum import context
+
+CONF_FILE = ""
+ROOTDIR = os.path.dirname(os.path.dirname(__file__))
+ETCDIR = os.path.join(ROOTDIR, 'etc')
+META_PATH = "quantum.plugins.metaplugin"
+FAKE_PATH = "%s.tests.unit" % META_PATH
+PROXY_PATH = "%s.proxy_quantum_plugin.ProxyPluginV2" % META_PATH
+PLUGIN_LIST = \
+    'fake1:%s.fake_plugin.Fake1,fake2:%s.fake_plugin.Fake2,proxy:%s' % \
+    (FAKE_PATH, FAKE_PATH, PROXY_PATH)
+
+
+def etcdir(*p):
+    return os.path.join(ETCDIR, *p)
+
+
+class PluginBaseTest(BaseMetaTest):
+    """Class conisting of MetaQuantumPluginV2 unit tests"""
+
+    def setUp(self):
+        super(PluginBaseTest, self).setUp()
+        db._ENGINE = None
+        db._MAKER = None
+        self.fake_tenant_id = str(uuid.uuid4())
+        self.context = context.get_admin_context()
+
+        args = ['--config-file', etcdir('quantum.conf.test')]
+        #config.parse(args=args)
+        # Update the plugin
+        cfg.CONF.set_override('auth_url', 'http://localhost:35357/v2.0',
+                                          'PROXY')
+        cfg.CONF.set_override('auth_region', 'RegionOne', 'PROXY')
+        cfg.CONF.set_override('admin_user', 'quantum', 'PROXY')
+        cfg.CONF.set_override('admin_password', 'password', 'PROXY')
+        cfg.CONF.set_override('admin_tenant_name', 'service', 'PROXY')
+        cfg.CONF.set_override('plugin_list', PLUGIN_LIST, 'META')
+        cfg.CONF.set_override('default_flavor', 'fake2', 'META')
+        cfg.CONF.set_override('base_mac', "12:34:56:78:90:ab")
+
+        self.client_cls_p = mock.patch('quantumclient.v2_0.client.Client')
+        client_cls = self.client_cls_p.start()
+        self.client_inst = mock.Mock()
+        client_cls.return_value = self.client_inst
+        self.client_inst.create_network.return_value = \
+            {'id': 'fake_id'}
+        self.client_inst.create_port.return_value = \
+            {'id': 'fake_id'}
+        self.client_inst.create_subnet.return_value = \
+            {'id': 'fake_id'}
+        self.client_inst.update_network.return_value = \
+            {'id': 'fake_id'}
+        self.client_inst.update_port.return_value = \
+            {'id': 'fake_id'}
+        self.client_inst.update_subnet.return_value = \
+            {'id': 'fake_id'}
+        self.client_inst.delete_network.return_value = True
+        self.client_inst.delete_port.return_value = True
+        self.client_inst.delete_subnet.return_value = True
+        self.plugin = MetaPluginV2(configfile=None)
+
+    def _fake_network(self, flavor):
+        data = {'network': {'name': flavor,
+                            'admin_state_up': True,
+                            'tenant_id': self.fake_tenant_id,
+                            'flavor:id': flavor}}
+        return data
+
+    def _fake_port(self, net_id):
+        return {'port': {'name': net_id,
+                         'network_id': net_id,
+                         'admin_state_up': True,
+                         'device_id': 'bad_device_id',
+                         'admin_state_up': True,
+                         'fixed_ips': [],
+                         'mac_address':
+                         self.plugin._generate_mac(self.context, net_id),
+                         'tenant_id': self.fake_tenant_id}}
+
+    def _fake_subnet(self, net_id):
+        allocation_pools = [{'start': '10.0.0.2',
+                             'end': '10.0.0.254'}]
+        return {'subnet': {'name': net_id,
+                           'network_id': net_id,
+                           'gateway_ip': '10.0.0.1',
+                           'cidr': '10.0.0.0/24',
+                           'allocation_pools': allocation_pools,
+                           'enable_dhcp': True,
+                           'ip_version': 4}}
+
+    def test_create_delete_network(self):
+        network1 = self._fake_network('fake1')
+        ret1 = self.plugin.create_network(self.context, network1)
+        self.assertEqual('fake1', ret1['flavor:id'])
+
+        network2 = self._fake_network('fake2')
+        ret2 = self.plugin.create_network(self.context, network2)
+        self.assertEqual('fake2', ret2['flavor:id'])
+
+        network3 = self._fake_network('proxy')
+        ret3 = self.plugin.create_network(self.context, network3)
+        self.assertEqual('proxy', ret3['flavor:id'])
+
+        db_ret1 = self.plugin.get_network(self.context, ret1['id'])
+        self.assertEqual('fake1', db_ret1['name'])
+
+        db_ret2 = self.plugin.get_network(self.context, ret2['id'])
+        self.assertEqual('fake2', db_ret2['name'])
+
+        db_ret3 = self.plugin.get_network(self.context, ret3['id'])
+        self.assertEqual('proxy', db_ret3['name'])
+
+        db_ret4 = self.plugin.get_networks(self.context)
+        self.assertEqual(3, len(db_ret4))
+
+        db_ret5 = self.plugin.get_networks(self.context,
+                                           {'flavor:id': ['fake1']})
+        self.assertEqual(1, len(db_ret5))
+        self.assertEqual('fake1', db_ret5[0]['name'])
+        self.plugin.delete_network(self.context, ret1['id'])
+        self.plugin.delete_network(self.context, ret2['id'])
+        self.plugin.delete_network(self.context, ret3['id'])
+
+    def test_create_delete_port(self):
+        network1 = self._fake_network('fake1')
+        network_ret1 = self.plugin.create_network(self.context, network1)
+        network2 = self._fake_network('fake2')
+        network_ret2 = self.plugin.create_network(self.context, network2)
+        network3 = self._fake_network('proxy')
+        network_ret3 = self.plugin.create_network(self.context, network3)
+
+        port1 = self._fake_port(network_ret1['id'])
+        port2 = self._fake_port(network_ret2['id'])
+        port3 = self._fake_port(network_ret3['id'])
+
+        port1_ret = self.plugin.create_port(self.context, port1)
+        port2_ret = self.plugin.create_port(self.context, port2)
+        port3_ret = self.plugin.create_port(self.context, port3)
+
+        self.assertEqual('fake1', port1_ret['device_id'])
+        self.assertEqual('fake2', port2_ret['device_id'])
+        self.assertEqual('bad_device_id', port3_ret['device_id'])
+
+        port_in_db1 = self.plugin.get_port(self.context, port1_ret['id'])
+        port_in_db2 = self.plugin.get_port(self.context, port2_ret['id'])
+        port_in_db3 = self.plugin.get_port(self.context, port3_ret['id'])
+
+        self.assertEqual('fake1', port_in_db1['device_id'])
+        self.assertEqual('fake2', port_in_db2['device_id'])
+        self.assertEqual('bad_device_id', port_in_db3['device_id'])
+
+        port1['port']['admin_state_up'] = False
+        port2['port']['admin_state_up'] = False
+        port3['port']['admin_state_up'] = False
+        self.plugin.update_port(self.context, port1_ret['id'], port1)
+        self.plugin.update_port(self.context, port2_ret['id'], port2)
+        self.plugin.update_port(self.context, port3_ret['id'], port3)
+        port_in_db1 = self.plugin.get_port(self.context, port1_ret['id'])
+        port_in_db2 = self.plugin.get_port(self.context, port2_ret['id'])
+        port_in_db3 = self.plugin.get_port(self.context, port3_ret['id'])
+        self.assertEqual(False, port_in_db1['admin_state_up'])
+        self.assertEqual(False, port_in_db2['admin_state_up'])
+        self.assertEqual(False, port_in_db3['admin_state_up'])
+
+        self.plugin.delete_port(self.context, port1_ret['id'])
+        self.plugin.delete_port(self.context, port2_ret['id'])
+        self.plugin.delete_port(self.context, port3_ret['id'])
+
+        self.plugin.delete_network(self.context, network_ret1['id'])
+        self.plugin.delete_network(self.context, network_ret2['id'])
+        self.plugin.delete_network(self.context, network_ret3['id'])
+
+    def test_create_delete_subnet(self):
+        network1 = self._fake_network('fake1')
+        network_ret1 = self.plugin.create_network(self.context, network1)
+        network2 = self._fake_network('fake2')
+        network_ret2 = self.plugin.create_network(self.context, network2)
+        network3 = self._fake_network('proxy')
+        network_ret3 = self.plugin.create_network(self.context, network3)
+
+        subnet1 = self._fake_subnet(network_ret1['id'])
+        subnet2 = self._fake_subnet(network_ret2['id'])
+        subnet3 = self._fake_subnet(network_ret3['id'])
+
+        subnet1_ret = self.plugin.create_subnet(self.context, subnet1)
+        subnet2_ret = self.plugin.create_subnet(self.context, subnet2)
+        subnet3_ret = self.plugin.create_subnet(self.context, subnet3)
+        self.assertEqual(network_ret1['id'], subnet1_ret['network_id'])
+        self.assertEqual(network_ret2['id'], subnet2_ret['network_id'])
+        self.assertEqual(network_ret3['id'], subnet3_ret['network_id'])
+
+        subnet_in_db1 = self.plugin.get_subnet(self.context, subnet1_ret['id'])
+        subnet_in_db2 = self.plugin.get_subnet(self.context, subnet2_ret['id'])
+        subnet_in_db3 = self.plugin.get_subnet(self.context, subnet3_ret['id'])
+
+        subnet1['subnet']['ip_version'] = 6
+        subnet1['subnet']['allocation_pools'].pop()
+        subnet2['subnet']['ip_version'] = 6
+        subnet2['subnet']['allocation_pools'].pop()
+        subnet3['subnet']['ip_version'] = 6
+        subnet3['subnet']['allocation_pools'].pop()
+
+        self.plugin.update_subnet(self.context,
+                                  subnet1_ret['id'], subnet1)
+        self.plugin.update_subnet(self.context,
+                                  subnet2_ret['id'], subnet2)
+        self.plugin.update_subnet(self.context,
+                                  subnet3_ret['id'], subnet3)
+        subnet_in_db1 = self.plugin.get_subnet(self.context, subnet1_ret['id'])
+        subnet_in_db2 = self.plugin.get_subnet(self.context, subnet2_ret['id'])
+        subnet_in_db3 = self.plugin.get_subnet(self.context, subnet3_ret['id'])
+
+        self.assertEqual(6, subnet_in_db1['ip_version'])
+        self.assertEqual(6, subnet_in_db2['ip_version'])
+        self.assertEqual(6, subnet_in_db3['ip_version'])
+
+        self.plugin.delete_subnet(self.context, subnet1_ret['id'])
+        self.plugin.delete_subnet(self.context, subnet2_ret['id'])
+        self.plugin.delete_subnet(self.context, subnet3_ret['id'])
+
+        self.plugin.delete_network(self.context, network_ret1['id'])
+        self.plugin.delete_network(self.context, network_ret2['id'])
+        self.plugin.delete_network(self.context, network_ret3['id'])
+
+    def test_extension_method(self):
+        self.assertEqual('fake1', self.plugin.fake_func())
+        self.assertEqual('fake2', self.plugin.fake_func2())
+
+    def test_extension_not_implemented_method(self):
+        try:
+            self.plugin.not_implemented()
+        except AttributeError:
+            return
+        except:
+            self.fail("AttributeError Error is not raised")
+
+        self.fail("No Error is not raised")
index 13fe3da5abc44d657dad9430cc7722db6741b76b..888b46c42b7fa093c187c467befd10062ba4a6e5 100644 (file)
@@ -24,6 +24,7 @@ from quantum.agent.linux import interface
 from quantum.agent.linux import ip_lib
 from quantum.agent.linux import utils
 from quantum.openstack.common import cfg
+from quantum.agent.dhcp_agent import DeviceManager
 
 
 class BaseChild(interface.LinuxInterfaceDriver):
@@ -332,3 +333,55 @@ class TestRyuInterfaceDriver(TestBase):
         expected.extend([mock.call().device().link.set_up()])
 
         self.ip.assert_has_calls(expected)
+
+
+class TestMetaInterfaceDriver(TestBase):
+    def setUp(self):
+        super(TestMetaInterfaceDriver, self).setUp()
+        self.conf.register_opts(DeviceManager.OPTS)
+        self.client_cls_p = mock.patch('quantumclient.v2_0.client.Client')
+        client_cls = self.client_cls_p.start()
+        self.client_inst = mock.Mock()
+        client_cls.return_value = self.client_inst
+
+        fake_network = {'network': {'flavor:id': 'fake1'}}
+        fake_port = {'ports':
+                     [{'mac_address':
+                      'aa:bb:cc:dd:ee:ffa', 'network_id': 'test'}]}
+
+        self.client_inst.list_ports.return_value = fake_port
+        self.client_inst.show_network.return_value = fake_network
+
+        self.conf.set_override('auth_url', 'http://localhost:35357/v2.0')
+        self.conf.set_override('auth_region', 'RegionOne')
+        self.conf.set_override('admin_user', 'quantum')
+        self.conf.set_override('admin_password', 'password')
+        self.conf.set_override('admin_tenant_name', 'service')
+        self.conf.set_override(
+            'meta_flavor_driver_mappings',
+            'fake1:quantum.agent.linux.interface.OVSInterfaceDriver,'
+            'fake2:quantum.agent.linux.interface.BridgeInterfaceDriver')
+
+    def tearDown(self):
+        self.client_cls_p.stop()
+        super(TestMetaInterfaceDriver, self).tearDown()
+
+    def test_get_driver_by_network_id(self):
+        meta_interface = interface.MetaInterfaceDriver(self.conf)
+        driver = meta_interface._get_driver_by_network_id('test')
+        self.assertTrue(isinstance(
+            driver,
+            interface.OVSInterfaceDriver))
+
+    def test_get_driver_by_device_name(self):
+        device_address_p = mock.patch(
+            'quantum.agent.linux.ip_lib.IpLinkCommand.address')
+        device_address = device_address_p.start()
+        device_address.return_value = 'aa:bb:cc:dd:ee:ffa'
+
+        meta_interface = interface.MetaInterfaceDriver(self.conf)
+        driver = meta_interface._get_driver_by_device_name('test')
+        self.assertTrue(isinstance(
+            driver,
+            interface.OVSInterfaceDriver))
+        device_address_p.stop()
index 60caf27d0e92fa80e5472c60bb1fb29b1ef8c7b2..78238fd6f5819f920bbbd361624fe4466f4d1333 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -48,6 +48,7 @@ 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'
+meta_plugin_config_path = 'etc/quantum/plugins/metaplugin'
 
 DataFiles = [
     (config_path,
@@ -70,6 +71,8 @@ DataFiles = [
     (nvp_plugin_config_path,
         ['etc/quantum/plugins/nicira/nvp.ini']),
     (ryu_plugin_config_path, ['etc/quantum/plugins/ryu/ryu.ini']),
+    (meta_plugin_config_path,
+        ['etc/quantum/plugins/metaplugin/metaplugin.ini'])
 ]
 
 setuptools.setup(