From b1fa5ef2ca3ff24a9c7d970e47e095a1d721cf8d Mon Sep 17 00:00:00 2001 From: Nachi Ueno Date: Thu, 19 Jul 2012 07:00:05 +0000 Subject: [PATCH] Initial implemention of MetaPlugin 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 --- etc/quantum/plugins/metaplugin/metaplugin.ini | 26 ++ quantum/agent/linux/interface.py | 65 ++++- quantum/extensions/flavor.py | 59 ++++ quantum/plugins/metaplugin/README | 81 ++++++ quantum/plugins/metaplugin/__init__.py | 16 ++ .../agent/linuxbridge_quantum_agent.py | 176 ++++++++++++ .../metaplugin/agent/ovs_quantum_agent.py | 194 +++++++++++++ quantum/plugins/metaplugin/common/__init__.py | 16 ++ quantum/plugins/metaplugin/common/config.py | 45 +++ quantum/plugins/metaplugin/meta_db_v2.py | 40 +++ quantum/plugins/metaplugin/meta_models_v2.py | 32 +++ .../plugins/metaplugin/meta_quantum_plugin.py | 216 ++++++++++++++ .../metaplugin/proxy_quantum_plugin.py | 132 +++++++++ quantum/plugins/metaplugin/run_tests.py | 57 ++++ quantum/plugins/metaplugin/tests/__init__.py | 16 ++ .../plugins/metaplugin/tests/unit/__init__.py | 16 ++ .../plugins/metaplugin/tests/unit/basetest.py | 44 +++ .../metaplugin/tests/unit/fake_plugin.py | 57 ++++ .../metaplugin/tests/unit/test_plugin_base.py | 268 ++++++++++++++++++ quantum/tests/unit/test_linux_interface.py | 53 ++++ setup.py | 3 + 21 files changed, 1610 insertions(+), 2 deletions(-) create mode 100644 etc/quantum/plugins/metaplugin/metaplugin.ini create mode 100644 quantum/extensions/flavor.py create mode 100644 quantum/plugins/metaplugin/README create mode 100644 quantum/plugins/metaplugin/__init__.py create mode 100755 quantum/plugins/metaplugin/agent/linuxbridge_quantum_agent.py create mode 100755 quantum/plugins/metaplugin/agent/ovs_quantum_agent.py create mode 100644 quantum/plugins/metaplugin/common/__init__.py create mode 100644 quantum/plugins/metaplugin/common/config.py create mode 100644 quantum/plugins/metaplugin/meta_db_v2.py create mode 100644 quantum/plugins/metaplugin/meta_models_v2.py create mode 100644 quantum/plugins/metaplugin/meta_quantum_plugin.py create mode 100644 quantum/plugins/metaplugin/proxy_quantum_plugin.py create mode 100755 quantum/plugins/metaplugin/run_tests.py create mode 100644 quantum/plugins/metaplugin/tests/__init__.py create mode 100644 quantum/plugins/metaplugin/tests/unit/__init__.py create mode 100644 quantum/plugins/metaplugin/tests/unit/basetest.py create mode 100644 quantum/plugins/metaplugin/tests/unit/fake_plugin.py create mode 100644 quantum/plugins/metaplugin/tests/unit/test_plugin_base.py diff --git a/etc/quantum/plugins/metaplugin/metaplugin.ini b/etc/quantum/plugins/metaplugin/metaplugin.ini new file mode 100644 index 000000000..93366ca1f --- /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 023c76c38..07d2d6234 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 000000000..d3f2a1b90 --- /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 000000000..7fc0a6d00 --- /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 000000000..d8bce7745 --- /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 000000000..fff37ff75 --- /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 000000000..2d42c0375 --- /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 000000000..d8bce7745 --- /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 000000000..f5a4103c7 --- /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 000000000..5d2fcc685 --- /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 000000000..6f91dfae2 --- /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 000000000..7b2300bd6 --- /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 000000000..846414641 --- /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 000000000..0576f1186 --- /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 000000000..d8bce7745 --- /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 000000000..d8bce7745 --- /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 000000000..06b9d7662 --- /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 000000000..8fd252c47 --- /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 000000000..0b313ab0f --- /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 13fe3da5a..888b46c42 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 60caf27d0..78238fd6f 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( -- 2.45.2