diff --git a/etc/quantum/plugins/metaplugin/metaplugin.ini b/etc/quantum/plugins/metaplugin/metaplugin.ini new file mode 100644 index 0000000000..93366ca1fe --- /dev/null +++ b/etc/quantum/plugins/metaplugin/metaplugin.ini @@ -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' diff --git a/quantum/agent/linux/interface.py b/quantum/agent/linux/interface.py index 023c76c382..07d2d6234e 100644 --- a/quantum/agent/linux/interface.py +++ b/quantum/agent/linux/interface.py @@ -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 \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 index 0000000000..d3f2a1b902 --- /dev/null +++ b/quantum/extensions/flavor.py @@ -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 index 0000000000..7fc0a6d000 --- /dev/null +++ b/quantum/plugins/metaplugin/README @@ -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 index 0000000000..d8bce77454 --- /dev/null +++ b/quantum/plugins/metaplugin/__init__.py @@ -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 index 0000000000..fff37ff756 --- /dev/null +++ b/quantum/plugins/metaplugin/agent/linuxbridge_quantum_agent.py @@ -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 index 0000000000..2d42c03759 --- /dev/null +++ b/quantum/plugins/metaplugin/agent/ovs_quantum_agent.py @@ -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 index 0000000000..d8bce77454 --- /dev/null +++ b/quantum/plugins/metaplugin/common/__init__.py @@ -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 index 0000000000..f5a4103c76 --- /dev/null +++ b/quantum/plugins/metaplugin/common/config.py @@ -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 index 0000000000..5d2fcc685b --- /dev/null +++ b/quantum/plugins/metaplugin/meta_db_v2.py @@ -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 index 0000000000..6f91dfae2e --- /dev/null +++ b/quantum/plugins/metaplugin/meta_models_v2.py @@ -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 "" % (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 index 0000000000..7b2300bd6a --- /dev/null +++ b/quantum/plugins/metaplugin/meta_quantum_plugin.py @@ -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 \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 index 0000000000..846414641d --- /dev/null +++ b/quantum/plugins/metaplugin/proxy_quantum_plugin.py @@ -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 index 0000000000..0576f1186a --- /dev/null +++ b/quantum/plugins/metaplugin/run_tests.py @@ -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 index 0000000000..d8bce77454 --- /dev/null +++ b/quantum/plugins/metaplugin/tests/__init__.py @@ -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 index 0000000000..d8bce77454 --- /dev/null +++ b/quantum/plugins/metaplugin/tests/unit/__init__.py @@ -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 index 0000000000..06b9d7662d --- /dev/null +++ b/quantum/plugins/metaplugin/tests/unit/basetest.py @@ -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 index 0000000000..8fd252c473 --- /dev/null +++ b/quantum/plugins/metaplugin/tests/unit/fake_plugin.py @@ -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 index 0000000000..0b313ab0f0 --- /dev/null +++ b/quantum/plugins/metaplugin/tests/unit/test_plugin_base.py @@ -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") diff --git a/quantum/tests/unit/test_linux_interface.py b/quantum/tests/unit/test_linux_interface.py index 13fe3da5ab..888b46c42b 100644 --- a/quantum/tests/unit/test_linux_interface.py +++ b/quantum/tests/unit/test_linux_interface.py @@ -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() diff --git a/setup.py b/setup.py index 60caf27d0e..78238fd6f5 100644 --- 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(