From 0eeb236962e645f7bef9cae7373b1a667a73577c Mon Sep 17 00:00:00 2001 From: Mohammad Banikazemi Date: Thu, 27 Feb 2014 06:45:17 -0500 Subject: [PATCH] Adds the new IBM SDN-VE plugin It adds a new plugin for SDN-VE, the IBM SDN controller. The plugin supports the core API and the port binding and L3 extensions. Implements: blueprint ibm-sdn-ve-plugin DocImpact Change-Id: I92619a95bca2ae0c37e7fdd39da30119b43d1ad6 --- .../plugins/ibm/sdnve_neutron_plugin.ini | 49 ++ .../176a85fc7d79_add_portbindings_db.py | 3 +- ...c149aca4_agents_unique_by_type_and_host.py | 1 + .../511471cc46b_agent_ext_model_supp.py | 3 +- .../versions/folsom_initial.py | 2 + neutron/plugins/ibm/README | 6 + neutron/plugins/ibm/__init__.py | 0 neutron/plugins/ibm/agent/__init__.py | 0 .../plugins/ibm/agent/sdnve_neutron_agent.py | 247 +++++++ neutron/plugins/ibm/common/__init__.py | 0 neutron/plugins/ibm/common/config.py | 74 ++ neutron/plugins/ibm/common/constants.py | 32 + neutron/plugins/ibm/common/exceptions.py | 28 + neutron/plugins/ibm/sdnve_api.py | 387 +++++++++++ neutron/plugins/ibm/sdnve_api_fake.py | 64 ++ neutron/plugins/ibm/sdnve_neutron_plugin.py | 649 ++++++++++++++++++ neutron/tests/unit/ibm/__init__.py | 0 neutron/tests/unit/ibm/test_sdnve_agent.py | 121 ++++ neutron/tests/unit/ibm/test_sdnve_api.py | 139 ++++ neutron/tests/unit/ibm/test_sdnve_plugin.py | 126 ++++ setup.cfg | 4 + 21 files changed, 1933 insertions(+), 2 deletions(-) create mode 100644 etc/neutron/plugins/ibm/sdnve_neutron_plugin.ini create mode 100644 neutron/plugins/ibm/README create mode 100644 neutron/plugins/ibm/__init__.py create mode 100644 neutron/plugins/ibm/agent/__init__.py create mode 100644 neutron/plugins/ibm/agent/sdnve_neutron_agent.py create mode 100644 neutron/plugins/ibm/common/__init__.py create mode 100644 neutron/plugins/ibm/common/config.py create mode 100644 neutron/plugins/ibm/common/constants.py create mode 100644 neutron/plugins/ibm/common/exceptions.py create mode 100644 neutron/plugins/ibm/sdnve_api.py create mode 100644 neutron/plugins/ibm/sdnve_api_fake.py create mode 100644 neutron/plugins/ibm/sdnve_neutron_plugin.py create mode 100644 neutron/tests/unit/ibm/__init__.py create mode 100644 neutron/tests/unit/ibm/test_sdnve_agent.py create mode 100644 neutron/tests/unit/ibm/test_sdnve_api.py create mode 100644 neutron/tests/unit/ibm/test_sdnve_plugin.py diff --git a/etc/neutron/plugins/ibm/sdnve_neutron_plugin.ini b/etc/neutron/plugins/ibm/sdnve_neutron_plugin.ini new file mode 100644 index 000000000..8cb32ea1d --- /dev/null +++ b/etc/neutron/plugins/ibm/sdnve_neutron_plugin.ini @@ -0,0 +1,49 @@ +[sdnve] +# (ListOpt) The IP address of one (or more) SDN-VE controllers +# Default value is : controller_ips = 127.0.0.1 +# Example: controller_ips = 127.0.0.1,127.0.0.2 +# (StrOpt) The integration bridge for OF based implementation +# The default value for integration_bridge is None +# Example: integration_bridge = br-int +# (ListOpt) The interface mapping connecting the integration +# bridge to external network as a list of physical network names and +# interfaces: : +# Example: interface_mappings = default:eth2 +# (BoolOpt) Used to reset the integration bridge, if exists +# The default value for reset_bridge is True +# Example: reset_bridge = False +# (BoolOpt) Used to set the OVS controller as out-of-band +# The default value for out_of_band is True +# Example: out_of_band = False +# +# (BoolOpt) The fake controller for testing purposes +# Default value is : use_fake_controller = False +# (StrOpt) The port number for use with controller +# The default value for the port is 8443 +# Example: port = 8443 +# (StrOpt) The userid for use with controller +# The default value for the userid is admin +# Example: userid = sdnve_user +# (StrOpt) The password for use with controller +# The default value for the password is admin +# Example: password = sdnve_password +# +# (StrOpt) The default type of tenants (and associated resources) +# Default value for OF is: default_tenant = OF +# Example: default_tenant = OVERLAY +# (StrOpt) The string in tenant description that indicates +# Default value for OF tenants: of_signature = SDNVE-OF +# (StrOpt) The string in tenant description that indicates +# Default value for OVERLAY tenants: overlay_signature = SDNVE-OVERLAY + +[sdnve_agent] +# (IntOpt) Agent's polling interval in seconds +# polling_interval = 2 +# (StrOpt) What to use for root helper +# The default value: root_helper = 'sudo' +# (BoolOpt) Whether to use rpc or not +# The default value: rpc = True + +[securitygroup] +# The security group is not supported: +# firewall_driver = neutron.agent.firewall.NoopFirewallDriver diff --git a/neutron/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py b/neutron/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py index 82e40a1ce..fdf62e037 100644 --- a/neutron/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py +++ b/neutron/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py @@ -36,7 +36,8 @@ migration_for_plugins = [ 'neutron.plugins.nicira.NeutronPlugin.NvpPluginV2', 'neutron.plugins.nicira.NeutronServicePlugin.NvpAdvancedPlugin', 'neutron.plugins.vmware.plugin.NsxPlugin', - 'neutron.plugins.vmware.plugin.NsxServicePlugin' + 'neutron.plugins.vmware.plugin.NsxServicePlugin', + 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', ] from alembic import op diff --git a/neutron/db/migration/alembic_migrations/versions/1fcfc149aca4_agents_unique_by_type_and_host.py b/neutron/db/migration/alembic_migrations/versions/1fcfc149aca4_agents_unique_by_type_and_host.py index b563f9c89..4fe6e1398 100644 --- a/neutron/db/migration/alembic_migrations/versions/1fcfc149aca4_agents_unique_by_type_and_host.py +++ b/neutron/db/migration/alembic_migrations/versions/1fcfc149aca4_agents_unique_by_type_and_host.py @@ -37,6 +37,7 @@ migration_for_plugins = [ 'neutron.plugins.vmware.plugin.NsxPlugin', 'neutron.plugins.vmware.plugin.NsxServicePlugin', 'neutron.services.loadbalancer.plugin.LoadBalancerPlugin', + 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', ] from alembic import op diff --git a/neutron/db/migration/alembic_migrations/versions/511471cc46b_agent_ext_model_supp.py b/neutron/db/migration/alembic_migrations/versions/511471cc46b_agent_ext_model_supp.py index ec21abda5..a1154af57 100644 --- a/neutron/db/migration/alembic_migrations/versions/511471cc46b_agent_ext_model_supp.py +++ b/neutron/db/migration/alembic_migrations/versions/511471cc46b_agent_ext_model_supp.py @@ -38,7 +38,8 @@ migration_for_plugins = [ 'neutron.plugins.nicira.NeutronServicePlugin.NvpAdvancedPlugin', 'neutron.plugins.vmware.plugin.NsxPlugin', 'neutron.plugins.vmware.plugin.NsxServicePlugin', - 'neutron.services.loadbalancer.plugin.LoadBalancerPlugin' + 'neutron.services.loadbalancer.plugin.LoadBalancerPlugin', + 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', ] from alembic import op diff --git a/neutron/db/migration/alembic_migrations/versions/folsom_initial.py b/neutron/db/migration/alembic_migrations/versions/folsom_initial.py index 7289e9c27..e75b63c91 100644 --- a/neutron/db/migration/alembic_migrations/versions/folsom_initial.py +++ b/neutron/db/migration/alembic_migrations/versions/folsom_initial.py @@ -37,6 +37,7 @@ PLUGINS = { 'plumgrid': 'neutron.plugins.plumgrid.plumgrid_plugin.plumgrid_plugin.' 'NeutronPluginPLUMgridV2', 'ryu': 'neutron.plugins.ryu.ryu_neutron_plugin.RyuNeutronPluginV2', + 'ibm': 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', } L3_CAPABLE = [ @@ -48,6 +49,7 @@ L3_CAPABLE = [ PLUGINS['ryu'], PLUGINS['brocade'], PLUGINS['plumgrid'], + PLUGINS['ibm'], ] FOLSOM_QUOTA = [ diff --git a/neutron/plugins/ibm/README b/neutron/plugins/ibm/README new file mode 100644 index 000000000..732fd7776 --- /dev/null +++ b/neutron/plugins/ibm/README @@ -0,0 +1,6 @@ +IBM SDN-VE Neutron Plugin + +This plugin implements Neutron v2 APIs. + +For more details on how to use it please refer to the following page: +http://wiki.openstack.org/wiki/IBM-Neutron diff --git a/neutron/plugins/ibm/__init__.py b/neutron/plugins/ibm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/ibm/agent/__init__.py b/neutron/plugins/ibm/agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/ibm/agent/sdnve_neutron_agent.py b/neutron/plugins/ibm/agent/sdnve_neutron_agent.py new file mode 100644 index 000000000..a67976e50 --- /dev/null +++ b/neutron/plugins/ibm/agent/sdnve_neutron_agent.py @@ -0,0 +1,247 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + + +import socket +import time + +import eventlet +from oslo.config import cfg + +from neutron.agent.linux import ip_lib +from neutron.agent.linux import ovs_lib +from neutron.agent import rpc as agent_rpc +from neutron.common import config as logging_config +from neutron.common import legacy +from neutron.common import topics +from neutron.common import utils as q_utils +from neutron import context +from neutron.openstack.common import log as logging +from neutron.openstack.common.rpc import dispatcher +from neutron.plugins.ibm.common import config # noqa +from neutron.plugins.ibm.common import constants + + +LOG = logging.getLogger(__name__) + + +class SdnvePluginApi(agent_rpc.PluginApi): + + def sdnve_info(self, context, info): + return self.call(context, + self.make_msg('sdnve_info', info=info), + topic=self.topic) + + +class SdnveNeutronAgent(): + + RPC_API_VERSION = '1.1' + + def __init__(self, integ_br, interface_mappings, + info, root_helper, polling_interval, + controller_ip, reset_br, out_of_band): + '''The agent initialization. + + Sets the following parameters and sets up the integration + bridge and physical interfaces if need be. + :param integ_br: name of the integration bridge. + :param interface_mappings: interfaces to physical networks. + :param info: local IP address of this hypervisor. + :param root_helper: utility to use when running shell cmds. + :param polling_interval: interval (secs) to poll DB. + :param controller_ip: Ip address of SDN-VE controller. + ''' + + self.root_helper = root_helper + self.int_bridge_name = integ_br + self.controller_ip = controller_ip + self.interface_mappings = interface_mappings + self.polling_interval = polling_interval + self.info = info + self.reset_br = reset_br + self.out_of_band = out_of_band + + if self.int_bridge_name: + self.int_br = self.setup_integration_br(integ_br, reset_br, + out_of_band, + self.controller_ip) + self.setup_physical_interfaces(self.interface_mappings) + else: + self.int_br = None + + self.setup_rpc() + + def setup_rpc(self): + if self.int_br: + mac = self.int_br.get_local_port_mac() + self.agent_id = '%s%s' % ('sdnve', (mac.replace(":", ""))) + else: + nameaddr = socket.gethostbyname(socket.gethostname()) + self.agent_id = '%s%s' % ('sdnve_', (nameaddr.replace(".", "_"))) + + self.topic = topics.AGENT + self.plugin_rpc = SdnvePluginApi(topics.PLUGIN) + self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN) + + self.context = context.get_admin_context_without_session() + self.dispatcher = self.create_rpc_dispatcher() + consumers = [[constants.INFO, topics.UPDATE]] + + self.connection = agent_rpc.create_consumers(self.dispatcher, + self.topic, + consumers) + + # Plugin calls the agents through the following + def info_update(self, context, **kwargs): + LOG.debug(_("info_update received")) + info = kwargs.get('info', {}) + new_controller = info.get('new_controller') + out_of_band = info.get('out_of_band') + if self.int_br and new_controller: + LOG.debug(_("info_update received. New controller" + "is to be set to: %s"), new_controller) + self.int_br.run_vsctl(["set-controller", + self.int_bridge_name, + "tcp:" + new_controller]) + if out_of_band: + LOG.debug(_("info_update received. New controller" + "is set to be out of band")) + self.int_br.set_db_attribute("controller", + self.int_bridge_name, + "connection-mode", + "out-of-band") + + def create_rpc_dispatcher(self): + return dispatcher.RpcDispatcher([self]) + + def setup_integration_br(self, bridge_name, reset_br, out_of_band, + controller_ip=None): + '''Sets up the integration bridge. + + Create the bridge and remove all existing flows if reset_br is True. + Otherwise, creates the bridge if not already existing. + :param bridge_name: the name of the integration bridge. + :param reset_br: A boolean to rest the bridge if True. + :param out_of_band: A boolean inidicating controller is out of band. + :param controller_ip: IP address to use as the bridge controller. + :returns: the integration bridge + ''' + + int_br = ovs_lib.OVSBridge(bridge_name, self.root_helper) + if reset_br: + int_br.reset_bridge() + int_br.remove_all_flows() + else: + int_br.create() + + # set the controller + if controller_ip: + int_br.run_vsctl( + ["set-controller", bridge_name, "tcp:" + controller_ip]) + if out_of_band: + int_br.set_db_attribute("controller", bridge_name, + "connection-mode", "out-of-band") + + return int_br + + def setup_physical_interfaces(self, interface_mappings): + '''Sets up the physical network interfaces. + + Link physical interfaces to the integration bridge. + :param interface_mappings: map physical net names to interface names. + ''' + + for physical_network, interface in interface_mappings.iteritems(): + LOG.info(_("Mapping physical network %(physical_network)s to " + "interface %(interface)s"), + {'physical_network': physical_network, + 'interface': interface}) + # Connect the physical interface to the bridge + if not ip_lib.device_exists(interface, self.root_helper): + LOG.error(_("Interface %(interface)s for physical network " + "%(physical_network)s does not exist. Agent " + "terminated!"), + {'physical_network': physical_network, + 'interface': interface}) + raise SystemExit(1) + self.int_br.add_port(interface) + + def sdnve_info(self): + details = self.plugin_rpc.sdnve_info( + self.context, + {'info': self.info}) + return details + + def rpc_loop(self): + + while True: + start = time.time() + LOG.debug(_("Agent in the rpc loop.")) + + # sleep till end of polling interval + elapsed = (time.time() - start) + if (elapsed < self.polling_interval): + time.sleep(self.polling_interval - elapsed) + else: + LOG.info(_("Loop iteration exceeded interval " + "(%(polling_interval)s vs. %(elapsed)s)!"), + {'polling_interval': self.polling_interval, + 'elapsed': elapsed}) + + def daemon_loop(self): + self.rpc_loop() + + +def create_agent_config_map(config): + + interface_mappings = q_utils.parse_mappings( + config.SDNVE.interface_mappings) + + controller_ips = config.SDNVE.controller_ips + LOG.info(_("Controller IPs: %s"), controller_ips) + controller_ip = controller_ips[0] + + return { + 'integ_br': config.SDNVE.integration_bridge, + 'interface_mappings': interface_mappings, + 'controller_ip': controller_ip, + 'info': config.SDNVE.info, + 'root_helper': config.SDNVE_AGENT.root_helper, + 'polling_interval': config.SDNVE_AGENT.polling_interval, + 'reset_br': config.SDNVE.reset_bridge, + 'out_of_band': config.SDNVE.out_of_band} + + +def main(): + eventlet.monkey_patch() + cfg.CONF.register_opts(ip_lib.OPTS) + cfg.CONF(project='neutron') + logging_config.setup_logging(cfg.CONF) + legacy.modernize_quantum_config(cfg.CONF) + + try: + agent_config = create_agent_config_map(cfg.CONF) + except ValueError as e: + LOG.exception(_("%s Agent terminated!"), e) + raise SystemExit(1) + + plugin = SdnveNeutronAgent(**agent_config) + + # Start everything. + LOG.info(_("Agent initialized successfully, now running... ")) + plugin.daemon_loop() diff --git a/neutron/plugins/ibm/common/__init__.py b/neutron/plugins/ibm/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/ibm/common/config.py b/neutron/plugins/ibm/common/config.py new file mode 100644 index 000000000..b32c7b630 --- /dev/null +++ b/neutron/plugins/ibm/common/config.py @@ -0,0 +1,74 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + + +from oslo.config import cfg + + +DEFAULT_INTERFACE_MAPPINGS = [] +DEFAULT_CONTROLLER_IPS = ['127.0.0.1'] + +sdnve_opts = [ + cfg.BoolOpt('use_fake_controller', default=False, + help=_("If set to True uses a fake controller.")), + cfg.StrOpt('base_url', default='/one/nb/v2/', + help=_("Base URL for SDN-VE controller REST API")), + cfg.ListOpt('controller_ips', default=DEFAULT_CONTROLLER_IPS, + help=_("List of IP addresses of SDN-VE controller(s)")), + cfg.StrOpt('info', default='sdnve_info_string', + help=_("SDN-VE RPC subject")), + cfg.StrOpt('port', default='8443', + help=_("SDN-VE controller port number")), + cfg.StrOpt('format', default='json', + help=_("SDN-VE request/response format")), + cfg.StrOpt('userid', default='admin', + help=_("SDN-VE administrator user id")), + cfg.StrOpt('password', default='admin', + help=_("SDN-VE administrator password")), + cfg.StrOpt('integration_bridge', default=None, + help=_("Integration bridge to use")), + cfg.BoolOpt('reset_bridge', default=True, + help=_("Reset the integration bridge before use")), + cfg.BoolOpt('out_of_band', default=True, + help=_("Indicating if controller is out of band or not")), + cfg.ListOpt('interface_mappings', + default=DEFAULT_INTERFACE_MAPPINGS, + help=_("List of :")), + cfg.StrOpt('default_tenant_type', default='OF', + help=_("Tenant type: OF (default) and OVERLAY")), + cfg.StrOpt('overlay_signature', default='SDNVE-OVERLAY', + help=_("The string in tenant description that indicates " + "the tenant is a OVERLAY tenant")), + cfg.StrOpt('of_signature', default='SDNVE-OF', + help=_("The string in tenant description that indicates " + "the tenant is a OF tenant")), +] + +sdnve_agent_opts = [ + cfg.IntOpt('polling_interval', default=2, + help=_("Agent polling interval if necessary")), + cfg.StrOpt('root_helper', default='sudo', + help=_("Using root helper")), + cfg.BoolOpt('rpc', default=True, + help=_("Whether using rpc")), + +] + + +cfg.CONF.register_opts(sdnve_opts, "SDNVE") +cfg.CONF.register_opts(sdnve_agent_opts, "SDNVE_AGENT") diff --git a/neutron/plugins/ibm/common/constants.py b/neutron/plugins/ibm/common/constants.py new file mode 100644 index 000000000..3acf9baff --- /dev/null +++ b/neutron/plugins/ibm/common/constants.py @@ -0,0 +1,32 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + + +import httplib + +# Topic for info notifications between the plugin and agent +INFO = 'info' + +TENANT_TYPE_OF = 'OF' +TENANT_TYPE_OVERLAY = 'OVERLAY' + +HTTP_ACCEPTABLE = [httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT + ] diff --git a/neutron/plugins/ibm/common/exceptions.py b/neutron/plugins/ibm/common/exceptions.py new file mode 100644 index 000000000..d2e5e7ed8 --- /dev/null +++ b/neutron/plugins/ibm/common/exceptions.py @@ -0,0 +1,28 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + +from neutron.common import exceptions + + +class SdnveException(exceptions.NeutronException): + message = _("An unexpected error occurred in the SDN-VE Plugin. " + "Here is the error message: %(msg)s") + + +class BadInputException(exceptions.BadRequest): + message = _("The input does not contain nececessary info: %(msg)s") diff --git a/neutron/plugins/ibm/sdnve_api.py b/neutron/plugins/ibm/sdnve_api.py new file mode 100644 index 000000000..43ba132da --- /dev/null +++ b/neutron/plugins/ibm/sdnve_api.py @@ -0,0 +1,387 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + + +import httplib +import urllib + +import httplib2 +from keystoneclient.v2_0 import client as keyclient +from oslo.config import cfg + +from neutron.api.v2 import attributes +from neutron.openstack.common import log as logging +from neutron.plugins.ibm.common import config # noqa +from neutron.plugins.ibm.common import constants +from neutron.wsgi import Serializer + +LOG = logging.getLogger(__name__) + +SDNVE_VERSION = '2.0' +SDNVE_ACTION_PREFIX = '/sdnve' +SDNVE_RETRIES = 0 +SDNVE_RETRIY_INTERVAL = 1 +SDNVE_TENANT_TYPE_OVERLAY = u'DOVE' +SDNVE_URL = 'https://%s:%s%s' + + +class RequestHandler(object): + '''Handles processeing requests to and responses from controller.''' + + def __init__(self, controller_ips=None, port=None, ssl=None, + base_url=None, userid=None, password=None, + timeout=10, formats=None): + '''Initializes the RequestHandler for communication with controller + + Following keyword arguments are used; if not specified, default + values are used. + :param port: Username for authentication. + :param timeout: Time out for http requests. + :param userid: User id for accessing controller. + :param password: Password for accessing the controlelr. + :param base_url: The base url for the controller. + :param controller_ips: List of controller IP addresses. + :param formats: Supported formats. + ''' + self.port = port or cfg.CONF.SDNVE.port + self.timeout = timeout + self._s_meta = None + self.connection = None + self.httpclient = httplib2.Http( + disable_ssl_certificate_validation=True) + self.cookie = None + + userid = userid or cfg.CONF.SDNVE.userid + password = password or cfg.CONF.SDNVE.password + if (userid and password): + self.httpclient.add_credentials(userid, password) + + self.base_url = base_url or cfg.CONF.SDNVE.base_url + self.controller_ips = controller_ips or cfg.CONF.SDNVE.controller_ips + + LOG.info(_("The IP addr of available SDN-VE controllers: %s"), + self.controller_ips) + self.controller_ip = self.controller_ips[0] + LOG.info(_("The SDN-VE controller IP address: %s"), + self.controller_ip) + + self.new_controller = False + self.format = formats or cfg.CONF.SDNVE.format + + self.version = SDNVE_VERSION + self.action_prefix = SDNVE_ACTION_PREFIX + self.retries = SDNVE_RETRIES + self.retry_interval = SDNVE_RETRIY_INTERVAL + + def serialize(self, data): + '''Serializes a dictionary with a single key.''' + + if isinstance(data, dict): + return Serializer().serialize(data, self.content_type()) + elif data: + raise TypeError(_("unable to serialize object type: '%s'") % + type(data)) + + def deserialize(self, data, status_code): + '''Deserializes an xml or json string into a dictionary.''' + + # NOTE(mb): Temporary fix for backend controller requirement + data = data.replace("router_external", "router:external") + + if status_code == httplib.NO_CONTENT: + return data + try: + deserialized_data = Serializer( + metadata=self._s_meta).deserialize(data, self.content_type()) + deserialized_data = deserialized_data['body'] + except Exception: + deserialized_data = data + + return deserialized_data + + def content_type(self, format=None): + '''Returns the mime-type for either 'xml' or 'json'.''' + + return 'application/%s' % (format or self.format) + + def delete(self, url, body=None, headers=None, params=None): + return self.do_request("DELETE", url, body=body, + headers=headers, params=params) + + def get(self, url, body=None, headers=None, params=None): + return self.do_request("GET", url, body=body, + headers=headers, params=params) + + def post(self, url, body=None, headers=None, params=None): + return self.do_request("POST", url, body=body, + headers=headers, params=params) + + def put(self, url, body=None, headers=None, params=None): + return self.do_request("PUT", url, body=body, + headers=headers, params=params) + + def do_request(self, method, url, body=None, headers=None, + params=None, connection_type=None): + + status_code = -1 + replybody_deserialized = '' + + if body: + body = self.serialize(body) + + self.headers = headers or {'Content-Type': self.content_type()} + if self.cookie: + self.headers['cookie'] = self.cookie + + if self.controller_ip != self.controller_ips[0]: + controllers = [self.controller_ip] + else: + controllers = [] + controllers.extend(self.controller_ips) + + for controller_ip in controllers: + serverurl = SDNVE_URL % (controller_ip, self.port, self.base_url) + myurl = serverurl + url + if params and isinstance(params, dict): + myurl += '?' + urllib.urlencode(params, doseq=1) + + try: + LOG.debug(_("Sending request to SDN-VE. url: " + "%(myurl)s method: %(method)s body: " + "%(body)s header: %(header)s "), + {'myurl': myurl, 'method': method, + 'body': body, 'header': self.headers}) + resp, replybody = self.httpclient.request( + myurl, method=method, body=body, headers=self.headers) + LOG.debug(("Response recd from SDN-VE. resp: %(resp)s" + "body: %(body)s"), + {'resp': resp.status, 'body': replybody}) + status_code = resp.status + + except Exception as e: + LOG.error(_("Error: Could not reach server: %(url)s " + "Exception: %(excp)s."), + {'url': myurl, 'excp': e}) + self.cookie = None + continue + + if status_code not in constants.HTTP_ACCEPTABLE: + LOG.debug(_("Error message: %(reply)s -- Status: %(status)s"), + {'reply': replybody, 'status': status_code}) + else: + LOG.debug(_("Received response status: %s"), status_code) + + if resp.get('set-cookie'): + self.cookie = resp['set-cookie'] + replybody_deserialized = self.deserialize( + replybody, + status_code) + LOG.debug(_("Deserialized body: %s"), replybody_deserialized) + if controller_ip != self.controller_ip: + # bcast the change of controller + self.new_controller = True + self.controller_ip = controller_ip + + return (status_code, replybody_deserialized) + + return (httplib.REQUEST_TIMEOUT, 'Could not reach server(s)') + + +class Client(RequestHandler): + '''Client for SDNVE controller.''' + + def __init__(self): + '''Initialize a new SDNVE client.''' + super(Client, self).__init__() + + self.keystoneclient = KeystoneClient() + + resource_path = { + 'network': "ln/networks/", + 'subnet': "ln/subnets/", + 'port': "ln/ports/", + 'tenant': "ln/tenants/", + 'router': "ln/routers/", + 'floatingip': "ln/floatingips/", + } + + def process_request(self, body): + '''Processes requests according to requirements of controller.''' + if self.format == 'json': + body = dict( + (k.replace(':', '_'), v) for k, v in body.items() + if attributes.is_attr_set(v)) + + def sdnve_list(self, resource, **params): + '''Fetches a list of resources.''' + + res = self.resource_path.get(resource, None) + if not res: + LOG.info(_("Bad resource for forming a list request")) + return 0, '' + + return self.get(res, params=params) + + def sdnve_show(self, resource, specific, **params): + '''Fetches information of a certain resource.''' + + res = self.resource_path.get(resource, None) + if not res: + LOG.info(_("Bad resource for forming a show request")) + return 0, '' + + return self.get(res + specific, params=params) + + def sdnve_create(self, resource, body): + '''Creates a new resource.''' + + res = self.resource_path.get(resource, None) + if not res: + LOG.info(_("Bad resource for forming a create request")) + return 0, '' + + self.process_request(body) + status, data = self.post(res, body=body) + return (status, data) + + def sdnve_update(self, resource, specific, body=None): + '''Updates a resource.''' + + res = self.resource_path.get(resource, None) + if not res: + LOG.info(_("Bad resource for forming a update request")) + return 0, '' + + self.process_request(body) + return self.put(res + specific, body=body) + + def sdnve_delete(self, resource, specific): + '''Deletes the specified resource.''' + + res = self.resource_path.get(resource, None) + if not res: + LOG.info(_("Bad resource for forming a delete request")) + return 0, '' + + return self.delete(res + specific) + + def _tenant_id_conversion(self, osid): + return osid + + def sdnve_get_tenant_byid(self, os_tenant_id): + sdnve_tenant_id = self._tenant_id_conversion(os_tenant_id) + resp, content = self.sdnve_show('tenant', sdnve_tenant_id) + if resp in constants.HTTP_ACCEPTABLE: + tenant_id = content.get('id') + tenant_type = content.get('network_type') + if tenant_type == SDNVE_TENANT_TYPE_OVERLAY: + tenant_type = constants.TENANT_TYPE_OVERLAY + return tenant_id, tenant_type + return None, None + + def sdnve_check_and_create_tenant(self, os_tenant_id, network_type=None): + + if not os_tenant_id: + return + tenant_id, tenant_type = self.sdnve_get_tenant_byid(os_tenant_id) + if tenant_id: + if not network_type: + return tenant_id + if tenant_type != network_type: + LOG.info(_("Non matching tenant and network types: " + "%(ttype)s %(ntype)s"), + {'ttype': tenant_type, 'ntype': network_type}) + return + return tenant_id + + # Have to create a new tenant + sdnve_tenant_id = self._tenant_id_conversion(os_tenant_id) + if not network_type: + network_type = self.keystoneclient.get_tenant_type(os_tenant_id) + if network_type == constants.TENANT_TYPE_OVERLAY: + network_type = SDNVE_TENANT_TYPE_OVERLAY + + pinn_desc = ("Created by SDN-VE Neutron Plugin, OS project name = " + + self.keystoneclient.get_tenant_name(os_tenant_id)) + + res, content = self.sdnve_create('tenant', + {'id': sdnve_tenant_id, + 'name': os_tenant_id, + 'network_type': network_type, + 'description': pinn_desc}) + if res not in constants.HTTP_ACCEPTABLE: + return + + return sdnve_tenant_id + + def sdnve_get_controller(self): + if self.new_controller: + self.new_controller = False + return self.controller_ip + + +class KeystoneClient(object): + + def __init__(self, username=None, tenant_name=None, password=None, + auth_url=None): + + keystone_conf = cfg.CONF.keystone_authtoken + keystone_auth_url = ('%s://%s:%s/v2.0/' % + (keystone_conf.auth_protocol, + keystone_conf.auth_host, + keystone_conf.auth_port)) + + username = username or keystone_conf.admin_user + tenant_name = tenant_name or keystone_conf.admin_tenant_name + password = password or keystone_conf.admin_password + auth_url = auth_url or keystone_auth_url + + self.overlay_signature = cfg.CONF.SDNVE.overlay_signature + self.of_signature = cfg.CONF.SDNVE.of_signature + self.default_tenant_type = cfg.CONF.SDNVE.default_tenant_type + + self.client = keyclient.Client(username=username, + password=password, + tenant_name=tenant_name, + auth_url=auth_url) + + def get_tenant_byid(self, id): + + try: + return self.client.tenants.get(id) + except Exception: + LOG.exception(_("Did not find tenant: %r"), id) + + def get_tenant_type(self, id): + + tenant = self.get_tenant_byid(id) + if tenant: + description = tenant.description + if description: + if (description.find(self.overlay_signature) >= 0): + return constants.TENANT_TYPE_OVERLAY + if (description.find(self.of_signature) >= 0): + return constants.TENANT_TYPE_OF + return self.default_tenant_type + + def get_tenant_name(self, id): + + tenant = self.get_tenant_byid(id) + if tenant: + return tenant.name + return 'not found' diff --git a/neutron/plugins/ibm/sdnve_api_fake.py b/neutron/plugins/ibm/sdnve_api_fake.py new file mode 100644 index 000000000..74cfc8386 --- /dev/null +++ b/neutron/plugins/ibm/sdnve_api_fake.py @@ -0,0 +1,64 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + +from neutron.openstack.common import log as logging +from neutron.plugins.ibm.common import constants + +LOG = logging.getLogger(__name__) + +HTTP_OK = 200 + + +class FakeClient(): + + '''Fake Client for SDNVE controller.''' + + def __init__(self, **kwargs): + LOG.info(_('Fake SDNVE controller initialized')) + + def sdnve_list(self, resource, **_params): + LOG.info(_('Fake SDNVE controller: list')) + return (HTTP_OK, None) + + def sdnve_show(self, resource, specific, **_params): + LOG.info(_('Fake SDNVE controller: show')) + return (HTTP_OK, None) + + def sdnve_create(self, resource, body): + LOG.info(_('Fake SDNVE controller: create')) + return (HTTP_OK, None) + + def sdnve_update(self, resource, specific, body=None): + LOG.info(_('Fake SDNVE controller: update')) + return (HTTP_OK, None) + + def sdnve_delete(self, resource, specific): + LOG.info(_('Fake SDNVE controller: delete')) + return (HTTP_OK, None) + + def sdnve_get_tenant_byid(self, id): + LOG.info(_('Fake SDNVE controller: get tenant by id')) + return id, constants.TENANT_TYPE_OF + + def sdnve_check_and_create_tenant(self, id, network_type=None): + LOG.info(_('Fake SDNVE controller: check and create tenant')) + return id + + def sdnve_get_controller(self): + LOG.info(_('Fake SDNVE controller: get controller')) + return None diff --git a/neutron/plugins/ibm/sdnve_neutron_plugin.py b/neutron/plugins/ibm/sdnve_neutron_plugin.py new file mode 100644 index 000000000..1e4afe267 --- /dev/null +++ b/neutron/plugins/ibm/sdnve_neutron_plugin.py @@ -0,0 +1,649 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp. + + +import functools + +from oslo.config import cfg + +from neutron.common import constants as q_const +from neutron.common import exceptions as q_exc +from neutron.common import rpc as q_rpc +from neutron.common import topics +from neutron.db import agents_db +from neutron.db import db_base_plugin_v2 +from neutron.db import external_net_db +from neutron.db import l3_gwmode_db +from neutron.db import portbindings_db +from neutron.extensions import portbindings +from neutron.openstack.common import excutils +from neutron.openstack.common import log as logging +from neutron.openstack.common import rpc +from neutron.openstack.common.rpc import proxy +from neutron.plugins.ibm.common import config # noqa +from neutron.plugins.ibm.common import constants +from neutron.plugins.ibm.common import exceptions as sdnve_exc +from neutron.plugins.ibm import sdnve_api as sdnve +from neutron.plugins.ibm import sdnve_api_fake as sdnve_fake + +LOG = logging.getLogger(__name__) + + +class SdnveRpcCallbacks(): + + def __init__(self, notifier): + self.notifier = notifier # used to notify the agent + + def create_rpc_dispatcher(self): + '''Get the rpc dispatcher for this manager. + If a manager would like to set an rpc API version, or support more than + one class as the target of rpc messages, override this method. + ''' + return q_rpc.PluginRpcDispatcher([self, + agents_db.AgentExtRpcCallback()]) + + def sdnve_info(self, rpc_context, **kwargs): + '''Update new information.''' + info = kwargs.get('info') + # Notify all other listening agents + self.notifier.info_update(rpc_context, info) + return info + + +class AgentNotifierApi(proxy.RpcProxy): + '''Agent side of the SDN-VE rpc API.''' + + BASE_RPC_API_VERSION = '1.0' + + def __init__(self, topic): + super(AgentNotifierApi, self).__init__( + topic=topic, default_version=self.BASE_RPC_API_VERSION) + + self.topic_info_update = topics.get_topic_name(topic, + constants.INFO, + topics.UPDATE) + + def info_update(self, context, info): + self.fanout_cast(context, + self.make_msg('info_update', + info=info), + topic=self.topic_info_update) + + +def _ha(func): + '''Supports the high availability feature of the controller.''' + + @functools.wraps(func) + def hawrapper(self, *args, **kwargs): + '''This wrapper sets the new controller if necessary + + When a controller is detected to be not responding, and a + new controller is chosen to be used in its place, this decorator + makes sure the existing integration bridges are set to point + to the new controleer by calling the set_controller method. + ''' + ret_func = func(self, *args, **kwargs) + self.set_controller(args[0]) + return ret_func + return hawrapper + + +class SdnvePluginV2(db_base_plugin_v2.NeutronDbPluginV2, + external_net_db.External_net_db_mixin, + portbindings_db.PortBindingMixin, + l3_gwmode_db.L3_NAT_db_mixin, + ): + + ''' + Implement the Neutron abstractions using SDN-VE SDN Controller. + ''' + + __native_bulk_support = False + __native_pagination_support = False + __native_sorting_support = False + + supported_extension_aliases = ["binding", "router", "external-net"] + + def __init__(self, configfile=None): + self.base_binding_dict = { + portbindings.VIF_TYPE: portbindings.VIF_TYPE_OVS, + portbindings.VIF_DETAILS: {portbindings.CAP_PORT_FILTER: False}} + + super(SdnvePluginV2, self).__init__() + self.setup_rpc() + self.sdnve_controller_select() + if self.fake_controller: + self.sdnve_client = sdnve_fake.FakeClient() + else: + self.sdnve_client = sdnve.Client() + + def sdnve_controller_select(self): + self.fake_controller = cfg.CONF.SDNVE.use_fake_controller + + def setup_rpc(self): + # RPC support + self.topic = topics.PLUGIN + self.conn = rpc.create_connection(new=True) + self.notifier = AgentNotifierApi(topics.AGENT) + self.callbacks = SdnveRpcCallbacks(self.notifier) + self.dispatcher = self.callbacks.create_rpc_dispatcher() + self.conn.create_consumer(self.topic, self.dispatcher, + fanout=False) + # Consume from all consumers in a thread + self.conn.consume_in_thread() + + def _update_base_binding_dict(self, tenant_type): + if tenant_type == constants.TENANT_TYPE_OVERLAY: + self.base_binding_dict[ + portbindings.VIF_TYPE] = portbindings.VIF_TYPE_BRIDGE + if tenant_type == constants.TENANT_TYPE_OF: + self.base_binding_dict[ + portbindings.VIF_TYPE] = portbindings.VIF_TYPE_OVS + + def set_controller(self, context): + LOG.info(_("Set a new controller if needed.")) + new_controller = self.sdnve_client.sdnve_get_controller() + if new_controller: + self.notifier.info_update( + context, + {'new_controller': new_controller}) + LOG.info(_("Set the controller to a new controller: %s"), + new_controller) + + def _process_request(self, request, current): + new_request = dict( + (k, v) for k, v in request.items() + if v != current.get(k)) + + msg = _("Original SDN-VE HTTP request: %(orig)s; New request: %(new)s") + LOG.debug(msg, {'orig': request, 'new': new_request}) + return new_request + + # + # Network + # + + @_ha + def create_network(self, context, network): + LOG.debug(_("Create network in progress: %r"), network) + session = context.session + + tenant_id = self._get_tenant_id_for_create(context, network['network']) + # Create a new SDN-VE tenant if need be + sdnve_tenant = self.sdnve_client.sdnve_check_and_create_tenant( + tenant_id) + if sdnve_tenant is None: + raise sdnve_exc.SdnveException( + msg=_('Create net failed: no SDN-VE tenant.')) + + with session.begin(subtransactions=True): + net = super(SdnvePluginV2, self).create_network(context, network) + self._process_l3_create(context, net, network['network']) + + # Create SDN-VE network + (res, data) = self.sdnve_client.sdnve_create('network', net) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).delete_network(context, net['id']) + raise sdnve_exc.SdnveException( + msg=(_('Create net failed in SDN-VE: %s') % res)) + + LOG.debug(_("Created network: %s"), net['id']) + return net + + @_ha + def update_network(self, context, id, network): + LOG.debug(_("Update network in progress: %r"), network) + session = context.session + + processed_request = {} + with session.begin(subtransactions=True): + original_network = super(SdnvePluginV2, self).get_network( + context, id) + processed_request['network'] = self._process_request( + network['network'], original_network) + net = super(SdnvePluginV2, self).update_network( + context, id, network) + self._process_l3_update(context, net, network['network']) + + if processed_request['network']: + (res, data) = self.sdnve_client.sdnve_update( + 'network', id, processed_request['network']) + if res not in constants.HTTP_ACCEPTABLE: + net = super(SdnvePluginV2, self).update_network( + context, id, {'network': original_network}) + raise sdnve_exc.SdnveException( + msg=(_('Update net failed in SDN-VE: %s') % res)) + + return net + + @_ha + def delete_network(self, context, id): + LOG.debug(_("Delete network in progress: %s"), id) + super(SdnvePluginV2, self).delete_network(context, id) + + (res, data) = self.sdnve_client.sdnve_delete('network', id) + if res not in constants.HTTP_ACCEPTABLE: + LOG.error( + _("Delete net failed after deleting the network in DB: %s"), + res) + + @_ha + def get_network(self, context, id, fields=None): + LOG.debug(_("Get network in progress: %s"), id) + return super(SdnvePluginV2, self).get_network(context, id, fields) + + @_ha + def get_networks(self, context, filters=None, fields=None, sorts=None, + limit=None, marker=None, page_reverse=False): + LOG.debug(_("Get networks in progress")) + return super(SdnvePluginV2, self).get_networks( + context, filters, fields, sorts, limit, marker, page_reverse) + + # + # Port + # + + @_ha + def create_port(self, context, port): + LOG.debug(_("Create port in progress: %r"), port) + session = context.session + + # Set port status as 'ACTIVE' to avoid needing the agent + port['port']['status'] = q_const.PORT_STATUS_ACTIVE + port_data = port['port'] + + with session.begin(subtransactions=True): + port = super(SdnvePluginV2, self).create_port(context, port) + if 'id' not in port: + return port + # If the tenant_id is set to '' by create_port, add the id to + # the request being sent to the controller as the controller + # requires a tenant id + tenant_id = port.get('tenant_id') + if not tenant_id: + LOG.debug(_("Create port does not have tenant id info")) + original_network = super(SdnvePluginV2, self).get_network( + context, port['network_id']) + original_tenant_id = original_network['tenant_id'] + port['tenant_id'] = original_tenant_id + LOG.debug( + _("Create port does not have tenant id info; " + "obtained is: %s"), + port['tenant_id']) + + os_tenant_id = tenant_id + id_na, tenant_type = self.sdnve_client.sdnve_get_tenant_byid( + os_tenant_id) + self._update_base_binding_dict(tenant_type) + self._process_portbindings_create_and_update(context, + port_data, port) + + # NOTE(mb): Remove this block when controller is updated + # Remove the information that the controller does not accept + sdnve_port = port.copy() + sdnve_port.pop('device_id', None) + sdnve_port.pop('device_owner', None) + + (res, data) = self.sdnve_client.sdnve_create('port', sdnve_port) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).delete_port(context, port['id']) + raise sdnve_exc.SdnveException( + msg=(_('Create port failed in SDN-VE: %s') % res)) + + LOG.debug(_("Created port: %s"), port.get('id', 'id not found')) + return port + + @_ha + def update_port(self, context, id, port): + LOG.debug(_("Update port in progress: %r"), port) + session = context.session + + processed_request = {} + with session.begin(subtransactions=True): + original_port = super(SdnvePluginV2, self).get_port( + context, id) + processed_request['port'] = self._process_request( + port['port'], original_port) + updated_port = super(SdnvePluginV2, self).update_port( + context, id, port) + + os_tenant_id = updated_port['tenant_id'] + id_na, tenant_type = self.sdnve_client.sdnve_get_tenant_byid( + os_tenant_id) + self._update_base_binding_dict(tenant_type) + self._process_portbindings_create_and_update(context, + port['port'], + updated_port) + + if processed_request['port']: + (res, data) = self.sdnve_client.sdnve_update( + 'port', id, processed_request['port']) + if res not in constants.HTTP_ACCEPTABLE: + updated_port = super(SdnvePluginV2, self).update_port( + context, id, {'port': original_port}) + raise sdnve_exc.SdnveException( + msg=(_('Update port failed in SDN-VE: %s') % res)) + + return updated_port + + @_ha + def delete_port(self, context, id, l3_port_check=True): + LOG.debug(_("Delete port in progress: %s"), id) + + # if needed, check to see if this is a port owned by + # an l3-router. If so, we should prevent deletion. + if l3_port_check: + self.prevent_l3_port_deletion(context, id) + self.disassociate_floatingips(context, id) + + super(SdnvePluginV2, self).delete_port(context, id) + + (res, data) = self.sdnve_client.sdnve_delete('port', id) + if res not in constants.HTTP_ACCEPTABLE: + LOG.error( + _("Delete port operation failed in SDN-VE " + "after deleting the port from DB: %s"), res) + + # + # Subnet + # + + @_ha + def create_subnet(self, context, subnet): + LOG.debug(_("Create subnet in progress: %r"), subnet) + new_subnet = super(SdnvePluginV2, self).create_subnet(context, subnet) + + # Note(mb): Use of null string currently required by controller + sdnve_subnet = new_subnet.copy() + if subnet.get('gateway_ip') is None: + sdnve_subnet['gateway_ip'] = 'null' + (res, data) = self.sdnve_client.sdnve_create('subnet', sdnve_subnet) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).delete_subnet(context, + new_subnet['id']) + raise sdnve_exc.SdnveException( + msg=(_('Create subnet failed in SDN-VE: %s') % res)) + + LOG.debug(_("Subnet created: %s"), new_subnet['id']) + + return new_subnet + + @_ha + def update_subnet(self, context, id, subnet): + LOG.debug(_("Update subnet in progress: %r"), subnet) + session = context.session + + processed_request = {} + with session.begin(subtransactions=True): + original_subnet = super(SdnvePluginV2, self).get_subnet( + context, id) + processed_request['subnet'] = self._process_request( + subnet['subnet'], original_subnet) + updated_subnet = super(SdnvePluginV2, self).update_subnet( + context, id, subnet) + + if processed_request['subnet']: + # Note(mb): Use of string containing null required by controller + if 'gateway_ip' in processed_request['subnet']: + if processed_request['subnet'].get('gateway_ip') is None: + processed_request['subnet']['gateway_ip'] = 'null' + (res, data) = self.sdnve_client.sdnve_update( + 'subnet', id, processed_request['subnet']) + if res not in constants.HTTP_ACCEPTABLE: + for key in subnet['subnet'].keys(): + subnet['subnet'][key] = original_subnet[key] + super(SdnvePluginV2, self).update_subnet( + context, id, subnet) + raise sdnve_exc.SdnveException( + msg=(_('Update subnet failed in SDN-VE: %s') % res)) + + return updated_subnet + + @_ha + def delete_subnet(self, context, id): + LOG.debug(_("Delete subnet in progress: %s"), id) + super(SdnvePluginV2, self).delete_subnet(context, id) + + (res, data) = self.sdnve_client.sdnve_delete('subnet', id) + if res not in constants.HTTP_ACCEPTABLE: + LOG.error(_("Delete subnet operation failed in SDN-VE after " + "deleting the subnet from DB: %s"), res) + + # + # Router + # + + @_ha + def create_router(self, context, router): + LOG.debug(_("Create router in progress: %r"), router) + + if router['router']['admin_state_up'] is False: + LOG.warning(_('Ignoring admin_state_up=False for router=%r. ' + 'Overriding with True'), router) + router['router']['admin_state_up'] = True + + tenant_id = self._get_tenant_id_for_create(context, router['router']) + # Create a new Pinnaacles tenant if need be + sdnve_tenant = self.sdnve_client.sdnve_check_and_create_tenant( + tenant_id) + if sdnve_tenant is None: + raise sdnve_exc.SdnveException( + msg=_('Create router failed: no SDN-VE tenant.')) + + new_router = super(SdnvePluginV2, self).create_router(context, router) + # Create Sdnve router + (res, data) = self.sdnve_client.sdnve_create('router', new_router) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).delete_router(context, new_router['id']) + raise sdnve_exc.SdnveException( + msg=(_('Create router failed in SDN-VE: %s') % res)) + + LOG.debug(_("Router created: %r"), new_router) + return new_router + + @_ha + def update_router(self, context, id, router): + LOG.debug(_("Update router in progress: id=%(id)s " + "router=%(router)r"), + {'id': id, 'router': router}) + session = context.session + + processed_request = {} + if not router['router'].get('admin_state_up', True): + raise q_exc.NotImplementedError(_('admin_state_up=False ' + 'routers are not ' + 'supported.')) + + with session.begin(subtransactions=True): + original_router = super(SdnvePluginV2, self).get_router( + context, id) + processed_request['router'] = self._process_request( + router['router'], original_router) + updated_router = super(SdnvePluginV2, self).update_router( + context, id, router) + + if processed_request['router']: + (res, data) = self.sdnve_client.sdnve_update( + 'router', id, processed_request['router']) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).update_router( + context, id, {'router': original_router}) + raise sdnve_exc.SdnveException( + msg=(_('Update router failed in SDN-VE: %s') % res)) + + return updated_router + + @_ha + def delete_router(self, context, id): + LOG.debug(_("Delete router in progress: %s"), id) + + super(SdnvePluginV2, self).delete_router(context, id) + + (res, data) = self.sdnve_client.sdnve_delete('router', id) + if res not in constants.HTTP_ACCEPTABLE: + LOG.error( + _("Delete router operation failed in SDN-VE after " + "deleting the router in DB: %s"), res) + + @_ha + def add_router_interface(self, context, router_id, interface_info): + LOG.debug(_("Add router interface in progress: " + "router_id=%(router_id)s " + "interface_info=%(interface_info)r"), + {'router_id': router_id, 'interface_info': interface_info}) + + new_interface = super(SdnvePluginV2, self).add_router_interface( + context, router_id, interface_info) + LOG.debug( + _("SdnvePluginV2.add_router_interface called. Port info: %s"), + new_interface) + request_info = interface_info.copy() + request_info['port_id'] = new_interface['port_id'] + # Add the subnet_id to the request sent to the controller + if 'subnet_id' not in interface_info: + request_info['subnet_id'] = new_interface['subnet_id'] + + (res, data) = self.sdnve_client.sdnve_update( + 'router', router_id + '/add_router_interface', request_info) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).remove_router_interface( + context, router_id, interface_info) + raise sdnve_exc.SdnveException( + msg=(_('Update router-add-interface failed in SDN-VE: %s') % + res)) + + LOG.debug(_("Added router interface: %r"), new_interface) + return new_interface + + def _add_router_interface_only(self, context, router_id, interface_info): + LOG.debug(_("Add router interface only called: " + "router_id=%(router_id)s " + "interface_info=%(interface_info)r"), + {'router_id': router_id, 'interface_info': interface_info}) + + port_id = interface_info.get('port_id') + if port_id: + (res, data) = self.sdnve_client.sdnve_update( + 'router', router_id + '/add_router_interface', interface_info) + if res not in constants.HTTP_ACCEPTABLE: + LOG.error(_("SdnvePluginV2._add_router_interface_only: " + "failed to add the interface in the roll back." + " of a remove_router_interface operation")) + + @_ha + def remove_router_interface(self, context, router_id, interface_info): + LOG.debug(_("Remove router interface in progress: " + "router_id=%(router_id)s " + "interface_info=%(interface_info)r"), + {'router_id': router_id, 'interface_info': interface_info}) + + subnet_id = interface_info.get('subnet_id') + if not subnet_id: + portid = interface_info.get('port_id') + if not portid: + raise sdnve_exc.BadInputException(msg=_('No port ID')) + myport = super(SdnvePluginV2, self).get_port(context, portid) + LOG.debug(_("SdnvePluginV2.remove_router_interface port: %s"), + myport) + myfixed_ips = myport.get('fixed_ips') + if not myfixed_ips: + raise sdnve_exc.BadInputException(msg=_('No fixed IP')) + subnet_id = myfixed_ips[0].get('subnet_id') + if subnet_id: + interface_info['subnet_id'] = subnet_id + LOG.debug( + _("SdnvePluginV2.remove_router_interface subnet_id: %s"), + subnet_id) + + (res, data) = self.sdnve_client.sdnve_update( + 'router', router_id + '/remove_router_interface', interface_info) + + if res not in constants.HTTP_ACCEPTABLE: + raise sdnve_exc.SdnveException( + msg=(_('Update router-remove-interface failed SDN-VE: %s') % + res)) + + session = context.session + with session.begin(subtransactions=True): + try: + info = super(SdnvePluginV2, self).remove_router_interface( + context, router_id, interface_info) + except Exception: + with excutils.save_and_reraise_exception(): + self._add_router_interface_only(context, + router_id, interface_info) + + return info + + # + # Floating Ip + # + + @_ha + def create_floatingip(self, context, floatingip): + LOG.debug(_("Create floatingip in progress: %r"), + floatingip) + new_floatingip = super(SdnvePluginV2, self).create_floatingip( + context, floatingip) + + (res, data) = self.sdnve_client.sdnve_create( + 'floatingip', {'floatingip': new_floatingip}) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).delete_floatingip( + context, new_floatingip['id']) + raise sdnve_exc.SdnveException( + msg=(_('Creating floating ip operation failed ' + 'in SDN-VE controller: %s') % res)) + + LOG.debug(_("Created floatingip : %r"), new_floatingip) + return new_floatingip + + @_ha + def update_floatingip(self, context, id, floatingip): + LOG.debug(_("Update floatingip in progress: %r"), floatingip) + session = context.session + + processed_request = {} + with session.begin(subtransactions=True): + original_floatingip = super( + SdnvePluginV2, self).get_floatingip(context, id) + processed_request['floatingip'] = self._process_request( + floatingip['floatingip'], original_floatingip) + updated_floatingip = super( + SdnvePluginV2, self).update_floatingip(context, id, floatingip) + + if processed_request['floatingip']: + (res, data) = self.sdnve_client.sdnve_update( + 'floatingip', id, + {'floatingip': processed_request['floatingip']}) + if res not in constants.HTTP_ACCEPTABLE: + super(SdnvePluginV2, self).update_floatingip( + context, id, {'floatingip': original_floatingip}) + raise sdnve_exc.SdnveException( + msg=(_('Update floating ip failed in SDN-VE: %s') % res)) + + return updated_floatingip + + @_ha + def delete_floatingip(self, context, id): + LOG.debug(_("Delete floatingip in progress: %s"), id) + super(SdnvePluginV2, self).delete_floatingip(context, id) + + (res, data) = self.sdnve_client.sdnve_delete('floatingip', id) + if res not in constants.HTTP_ACCEPTABLE: + LOG.error(_("Delete floatingip failed in SDN-VE: %s"), res) diff --git a/neutron/tests/unit/ibm/__init__.py b/neutron/tests/unit/ibm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/tests/unit/ibm/test_sdnve_agent.py b/neutron/tests/unit/ibm/test_sdnve_agent.py new file mode 100644 index 000000000..3042d3d8c --- /dev/null +++ b/neutron/tests/unit/ibm/test_sdnve_agent.py @@ -0,0 +1,121 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp + + +import contextlib + +import mock +from oslo.config import cfg + +from neutron.agent.linux import ip_lib +from neutron.plugins.ibm.agent import sdnve_neutron_agent +from neutron.tests import base + + +NOTIFIER = ('neutron.plugins.ibm.' + 'sdnve_neutron_plugin.AgentNotifierApi') + + +class CreateAgentConfigMap(base.BaseTestCase): + + def test_create_agent_config_map_succeeds(self): + self.assertTrue(sdnve_neutron_agent.create_agent_config_map(cfg.CONF)) + + def test_create_agent_config_using_controller_ips(self): + self.addCleanup(cfg.CONF.reset) + cfg.CONF.set_override('controller_ips', + ['10.10.10.1', '10.10.10.2'], group='SDNVE') + cfgmap = sdnve_neutron_agent.create_agent_config_map(cfg.CONF) + self.assertEqual(cfgmap['controller_ip'], '10.10.10.1') + + def test_create_agent_config_using_interface_mappings(self): + self.addCleanup(cfg.CONF.reset) + cfg.CONF.set_override('interface_mappings', + ['interface1 : eth1', 'interface2 : eth2'], + group='SDNVE') + cfgmap = sdnve_neutron_agent.create_agent_config_map(cfg.CONF) + self.assertEqual(cfgmap['interface_mappings'], + {'interface1': 'eth1', 'interface2': 'eth2'}) + + +class TestSdnveNeutronAgent(base.BaseTestCase): + + def setUp(self): + super(TestSdnveNeutronAgent, self).setUp() + self.addCleanup(cfg.CONF.reset) + notifier_p = mock.patch(NOTIFIER) + notifier_cls = notifier_p.start() + self.notifier = mock.Mock() + notifier_cls.return_value = self.notifier + # Avoid rpc initialization for unit tests + cfg.CONF.set_override('rpc_backend', + 'neutron.openstack.common.rpc.impl_fake') + cfg.CONF.set_override('integration_bridge', + 'br_int', group='SDNVE') + kwargs = sdnve_neutron_agent.create_agent_config_map(cfg.CONF) + + class MockFixedIntervalLoopingCall(object): + def __init__(self, f): + self.f = f + + def start(self, interval=0): + self.f() + + with contextlib.nested( + mock.patch('neutron.plugins.ibm.agent.sdnve_neutron_agent.' + 'SdnveNeutronAgent.setup_integration_br', + return_value=mock.Mock()), + mock.patch('neutron.openstack.common.loopingcall.' + 'FixedIntervalLoopingCall', + new=MockFixedIntervalLoopingCall)): + self.agent = sdnve_neutron_agent.SdnveNeutronAgent(**kwargs) + + def test_setup_physical_interfaces(self): + with mock.patch.object(self.agent.int_br, + 'add_port') as add_port_func: + with mock.patch.object(ip_lib, + 'device_exists', + return_valxue=True): + self.agent.setup_physical_interfaces({"interface1": "eth1"}) + add_port_func.assert_called_once_with('eth1') + + def test_setup_physical_interfaces_none(self): + with mock.patch.object(self.agent.int_br, + 'add_port') as add_port_func: + with mock.patch.object(ip_lib, + 'device_exists', + return_valxue=True): + self.agent.setup_physical_interfaces({}) + self.assertFalse(add_port_func.called) + + def test_get_info_set_controller(self): + with mock.patch.object(self.agent.int_br, + 'run_vsctl') as run_vsctl_func: + kwargs = {} + kwargs['info'] = {'new_controller': '10.10.10.1'} + self.agent.info_update('dummy', **kwargs) + run_vsctl_func.assert_called_one_with(['set-controller', + 'br_int', + 'tcp:10.10.10.1']) + + def test_get_info(self): + with mock.patch.object(self.agent.int_br, + 'run_vsctl') as run_vsctl_func: + kwargs = {} + self.agent.info_update('dummy', **kwargs) + self.assertFalse(run_vsctl_func.called) diff --git a/neutron/tests/unit/ibm/test_sdnve_api.py b/neutron/tests/unit/ibm/test_sdnve_api.py new file mode 100644 index 000000000..ba76e0556 --- /dev/null +++ b/neutron/tests/unit/ibm/test_sdnve_api.py @@ -0,0 +1,139 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp + + +import mock +from oslo.config import cfg + +from neutron.openstack.common import uuidutils +from neutron.plugins.ibm.common import constants +from neutron.plugins.ibm import sdnve_api +from neutron.tests import base + +RESOURCE_PATH = { + 'network': "ln/networks/", +} +RESOURCE = 'network' +HTTP_OK = 200 +TENANT_ID = uuidutils.generate_uuid() + + +class TestSdnveApi(base.BaseTestCase): + + def setUp(self): + super(TestSdnveApi, self).setUp() + self.addCleanup(cfg.CONF.reset) + + class MockKeystoneClient(object): + def __init__(self, **kwargs): + pass + + def get_tenant_name(self, id): + return 'test tenant name' + + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'KeystoneClient', + new=MockKeystoneClient): + self.api = sdnve_api.Client() + + def mock_do_request(self, method, url, body=None, headers=None, + params=None, connection_type=None): + return (HTTP_OK, url) + + def mock_do_request_tenant(self, method, url, body=None, headers=None, + params=None, connection_type=None): + return (HTTP_OK, {'id': TENANT_ID, + 'network_type': constants.TENANT_TYPE_OF}) + + def mock_do_request_no_tenant(self, method, url, body=None, headers=None, + params=None, connection_type=None): + return (None, None) + + def mock_process_request(self, body): + return body + + def test_sdnve_api_list(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request): + result = self.api.sdnve_list(RESOURCE) + self.assertEqual(result, (HTTP_OK, RESOURCE_PATH[RESOURCE])) + + def test_sdnve_api_show(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request): + result = self.api.sdnve_show(RESOURCE, TENANT_ID) + self.assertEqual(result, + (HTTP_OK, RESOURCE_PATH[RESOURCE] + TENANT_ID)) + + def test_sdnve_api_create(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.process_request', + new=self.mock_process_request): + result = self.api.sdnve_create(RESOURCE, '') + self.assertEqual(result, (HTTP_OK, RESOURCE_PATH[RESOURCE])) + + def test_sdnve_api_update(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.process_request', + new=self.mock_process_request): + result = self.api.sdnve_update(RESOURCE, TENANT_ID, '') + self.assertEqual(result, + (HTTP_OK, + RESOURCE_PATH[RESOURCE] + TENANT_ID)) + + def test_sdnve_api_delete(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request): + result = self.api.sdnve_delete(RESOURCE, TENANT_ID) + self.assertEqual(result, + (HTTP_OK, RESOURCE_PATH[RESOURCE] + TENANT_ID)) + + def test_sdnve_get_tenant_by_id(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request_tenant): + id = TENANT_ID + result = self.api.sdnve_get_tenant_byid(id) + self.assertEqual(result, + (TENANT_ID, constants.TENANT_TYPE_OF)) + + def test_sdnve_check_and_create_tenant(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request_tenant): + id = TENANT_ID + result = self.api.sdnve_check_and_create_tenant(id) + self.assertEqual(result, TENANT_ID) + + def test_sdnve_check_and_create_tenant_fail(self): + with mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client.do_request', + new=self.mock_do_request_no_tenant): + id = TENANT_ID + result = self.api.sdnve_check_and_create_tenant( + id, constants.TENANT_TYPE_OF) + self.assertIsNone(result) diff --git a/neutron/tests/unit/ibm/test_sdnve_plugin.py b/neutron/tests/unit/ibm/test_sdnve_plugin.py new file mode 100644 index 000000000..4e4c967cc --- /dev/null +++ b/neutron/tests/unit/ibm/test_sdnve_plugin.py @@ -0,0 +1,126 @@ +# Copyright 2014 IBM Corp. +# +# 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: Mohammad Banikazemi, IBM Corp + + +import contextlib +import mock + +from neutron.extensions import portbindings +from neutron.tests.unit import _test_extension_portbindings as test_bindings +from neutron.tests.unit import test_db_plugin as test_plugin +from neutron.tests.unit import test_l3_plugin as test_l3_plugin + +from neutron.plugins.ibm.common import constants + + +_plugin_name = ('neutron.plugins.ibm.' + 'sdnve_neutron_plugin.SdnvePluginV2') +HTTP_OK = 200 + + +class MockClient(object): + def sdnve_list(self, resource, **params): + return (HTTP_OK, 'body') + + def sdnve_show(self, resource, specific, **params): + return (HTTP_OK, 'body') + + def sdnve_create(self, resource, body): + return (HTTP_OK, 'body') + + def sdnve_update(self, resource, specific, body=None): + return (HTTP_OK, 'body') + + def sdnve_delete(self, resource, specific): + return (HTTP_OK, 'body') + + def sdnve_get_tenant_byid(self, os_tenant_id): + return (os_tenant_id, constants.TENANT_TYPE_OF) + + def sdnve_check_and_create_tenant( + self, os_tenant_id, network_type=None): + return os_tenant_id + + def sdnve_get_controller(self): + return + + +class MockKeystoneClient(object): + def __init__(self, **kwargs): + pass + + def get_tenant_type(self, id): + return constants.TENANT_TYPE_OF + + def get_tenant_name(self, id): + return "tenant name" + + +class IBMPluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): + def setUp(self): + with contextlib.nested( + mock.patch('neutron.plugins.ibm.sdnve_api.' + 'KeystoneClient', + new=MockKeystoneClient), + mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client', + new=MockClient)): + super(IBMPluginV2TestCase, self).setUp(plugin=_plugin_name) + + +class TestIBMBasicGet(test_plugin.TestBasicGet, + IBMPluginV2TestCase): + pass + + +class TestIBMV2HTTPResponse(test_plugin.TestV2HTTPResponse, + IBMPluginV2TestCase): + pass + + +class TestIBMNetworksV2(test_plugin.TestNetworksV2, + IBMPluginV2TestCase): + pass + + +class TestIBMPortsV2(test_plugin.TestPortsV2, + IBMPluginV2TestCase): + pass + + +class TestIBMSubnetsV2(test_plugin.TestSubnetsV2, + IBMPluginV2TestCase): + pass + + +class TestIBMPortBinding(IBMPluginV2TestCase, + test_bindings.PortBindingsTestCase): + VIF_TYPE = portbindings.VIF_TYPE_OVS + + +class IBMPluginRouterTestCase(test_l3_plugin.L3NatDBIntTestCase): + + def setUp(self): + with contextlib.nested( + mock.patch('neutron.plugins.ibm.sdnve_api.' + 'KeystoneClient', + new=MockKeystoneClient), + mock.patch('neutron.plugins.ibm.sdnve_api.' + 'Client', + new=MockClient)): + super(IBMPluginRouterTestCase, self).setUp(plugin=_plugin_name) diff --git a/setup.cfg b/setup.cfg index 27f70b6e0..885ebde4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ data_files = etc/neutron/plugins/brocade = etc/neutron/plugins/brocade/brocade.ini etc/neutron/plugins/cisco = etc/neutron/plugins/cisco/cisco_plugins.ini etc/neutron/plugins/hyperv = etc/neutron/plugins/hyperv/hyperv_neutron_plugin.ini + etc/neutron/plugins/ibm = etc/neutron/plugins/ibm/sdnve_neutron_plugin.ini etc/neutron/plugins/linuxbridge = etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini etc/neutron/plugins/metaplugin = etc/neutron/plugins/metaplugin/metaplugin.ini etc/neutron/plugins/midonet = etc/neutron/plugins/midonet/midonet.ini @@ -85,6 +86,7 @@ console_scripts = neutron-debug = neutron.debug.shell:main neutron-dhcp-agent = neutron.agent.dhcp_agent:main neutron-hyperv-agent = neutron.plugins.hyperv.agent.hyperv_neutron_agent:main + neutron-ibm-agent = neutron.plugins.ibm.agent.sdnve_neutron_agent:main neutron-l3-agent = neutron.agent.l3_agent:main neutron-lbaas-agent = neutron.services.loadbalancer.agent.agent:main neutron-linuxbridge-agent = neutron.plugins.linuxbridge.agent.linuxbridge_neutron_agent:main @@ -106,6 +108,7 @@ console_scripts = quantum-debug = neutron.debug.shell:main quantum-dhcp-agent = neutron.agent.dhcp_agent:main quantum-hyperv-agent = neutron.plugins.hyperv.agent.hyperv_neutron_agent:main + quantum-ibm-agent = neutron.plugins.ibm.agent.sdnve_neutron_agent:main quantum-l3-agent = neutron.agent.l3_agent:main quantum-lbaas-agent = neutron.services.loadbalancer.agent.agent:main quantum-linuxbridge-agent = neutron.plugins.linuxbridge.agent.linuxbridge_neutron_agent:main @@ -127,6 +130,7 @@ neutron.core_plugins = cisco = neutron.plugins.cisco.network_plugin:PluginV2 embrane = neutron.plugins.embrane.plugins.embrane_ovs_plugin:EmbraneOvsPlugin hyperv = neutron.plugins.hyperv.hyperv_neutron_plugin:HyperVNeutronPlugin + ibm = neutron.plugins.ibm.sdnve_neutron_plugin:SdnvePluginV2 linuxbridge = neutron.plugins.linuxbridge.lb_neutron_plugin:LinuxBridgePluginV2 midonet = neutron.plugins.midonet.plugin:MidonetPluginV2 ml2 = neutron.plugins.ml2.plugin:Ml2Plugin -- 2.45.2