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
--- /dev/null
+[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'
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__)
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')
]
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)
--- /dev/null
+# 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 {}
--- /dev/null
+# -- 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
+
+
+
--- /dev/null
+# 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.
--- /dev/null
+#!/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()
--- /dev/null
+#!/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()
--- /dev/null
+# 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.
--- /dev/null
+# 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")
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+#!/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)
--- /dev/null
+# 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.
--- /dev/null
+# 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.
--- /dev/null
+# 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()
--- /dev/null
+# 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'
--- /dev/null
+# 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")
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):
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()
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,
(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(