From 683323f3593a9123a6e87d6b0f50bb0679f13e56 Mon Sep 17 00:00:00 2001 From: Hemanth Ravi Date: Sun, 26 Jan 2014 16:51:06 -0800 Subject: [PATCH] One Convergence Neutron Plugin Implementation One Convergence Neutron Plugin implements Neutron API to provide a network virtualization solution. The plugin works with One Convergence NVSD controller to provide the functionality. This checkin implements the Neutron core APIs and the plugin will be extended to support the L3 and service plugin extension APIs. Change-Id: Ic8a0dc0f5950d41b9b253c0d61b6812dbfd161c7 Implements: blueprint oc-nvsd-neutron-plugin --- .../plugins/oneconvergence/nvsdplugin.ini | 23 ++ .../versions/128e042a2b68_ext_gw_mode.py | 3 +- .../176a85fc7d79_add_portbindings_db.py | 1 + .../1c33fa3cd1a1_extra_route_config.py | 3 +- ...c149aca4_agents_unique_by_type_and_host.py | 1 + .../2eeaf963a447_floatingip_status.py | 1 + .../versions/3cb5d900c5de_security_groups.py | 1 + .../versions/4692d074d587_agent_scheduler.py | 1 + .../511471cc46b_agent_ext_model_supp.py | 1 + .../versions/folsom_initial.py | 3 + neutron/plugins/oneconvergence/README | 32 ++ neutron/plugins/oneconvergence/__init__.py | 0 .../plugins/oneconvergence/lib/__init__.py | 0 neutron/plugins/oneconvergence/lib/config.py | 41 +++ .../plugins/oneconvergence/lib/exception.py | 55 ++++ neutron/plugins/oneconvergence/lib/nvsdlib.py | 262 +++++++++++++++ .../oneconvergence/lib/plugin_helper.py | 186 +++++++++++ neutron/plugins/oneconvergence/plugin.py | 300 ++++++++++++++++++ neutron/tests/unit/oneconvergence/__init__.py | 0 .../unit/oneconvergence/test_nvsd_plugin.py | 109 +++++++ .../tests/unit/oneconvergence/test_nvsdlib.py | 186 +++++++++++ .../unit/oneconvergence/test_plugin_helper.py | 60 ++++ setup.cfg | 2 + 23 files changed, 1269 insertions(+), 2 deletions(-) create mode 100644 etc/neutron/plugins/oneconvergence/nvsdplugin.ini create mode 100644 neutron/plugins/oneconvergence/README create mode 100644 neutron/plugins/oneconvergence/__init__.py create mode 100644 neutron/plugins/oneconvergence/lib/__init__.py create mode 100644 neutron/plugins/oneconvergence/lib/config.py create mode 100644 neutron/plugins/oneconvergence/lib/exception.py create mode 100644 neutron/plugins/oneconvergence/lib/nvsdlib.py create mode 100644 neutron/plugins/oneconvergence/lib/plugin_helper.py create mode 100644 neutron/plugins/oneconvergence/plugin.py create mode 100644 neutron/tests/unit/oneconvergence/__init__.py create mode 100644 neutron/tests/unit/oneconvergence/test_nvsd_plugin.py create mode 100644 neutron/tests/unit/oneconvergence/test_nvsdlib.py create mode 100644 neutron/tests/unit/oneconvergence/test_plugin_helper.py diff --git a/etc/neutron/plugins/oneconvergence/nvsdplugin.ini b/etc/neutron/plugins/oneconvergence/nvsdplugin.ini new file mode 100644 index 0000000000..67335e0ccd --- /dev/null +++ b/etc/neutron/plugins/oneconvergence/nvsdplugin.ini @@ -0,0 +1,23 @@ +[nvsd] +# Configure the NVSD controller. The plugin proxies the api calls using +# to NVSD controller which implements the required functionality. + +# IP address of NVSD controller api server +# nvsd_ip = + +# Port number of NVSD controller api server +# nvsd_port = 8082 + +# Authentication credentials to access the api server +# nvsd_user = +# nvsd_passwd = + +# API request timeout in seconds +# request_timeout = + +# Maximum number of retry attempts to login to the NVSD controller +# Specify 0 to retry until success (default) +# nvsd_retries = 0 + +[database] +# connection = mysql://root:@127.0.0.1/?charset=utf8 diff --git a/neutron/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py b/neutron/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py index 28f6108e22..b2aa6443d4 100644 --- a/neutron/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py +++ b/neutron/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py @@ -42,7 +42,8 @@ migration_for_plugins = [ 'neutron.plugins.vmware.plugin.NsxPlugin', 'neutron.plugins.vmware.plugin.NsxServicePlugin', 'neutron.plugins.embrane.plugins.embrane_ovs_plugin.EmbraneOvsPlugin', - 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2' + 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] from alembic import op 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 fdf62e0375..c1289250d7 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 @@ -38,6 +38,7 @@ migration_for_plugins = [ 'neutron.plugins.vmware.plugin.NsxPlugin', 'neutron.plugins.vmware.plugin.NsxServicePlugin', 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] from alembic import op diff --git a/neutron/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py b/neutron/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py index 07dd4a9458..8c1f2a5630 100644 --- a/neutron/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py +++ b/neutron/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py @@ -38,7 +38,8 @@ migration_for_plugins = [ 'neutron.plugins.nicira.NeutronServicePlugin.NvpAdvancedPlugin', 'neutron.plugins.ryu.ryu_neutron_plugin.RyuNeutronPluginV2', 'neutron.plugins.vmware.plugin.NsxPlugin', - 'neutron.plugins.vmware.plugin.NsxServicePlugin' + 'neutron.plugins.vmware.plugin.NsxServicePlugin', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] 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 4fe6e1398b..a55f0b49ee 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 @@ -38,6 +38,7 @@ migration_for_plugins = [ 'neutron.plugins.vmware.plugin.NsxServicePlugin', 'neutron.services.loadbalancer.plugin.LoadBalancerPlugin', 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] from alembic import op diff --git a/neutron/db/migration/alembic_migrations/versions/2eeaf963a447_floatingip_status.py b/neutron/db/migration/alembic_migrations/versions/2eeaf963a447_floatingip_status.py index c9068397f5..006b5282f2 100644 --- a/neutron/db/migration/alembic_migrations/versions/2eeaf963a447_floatingip_status.py +++ b/neutron/db/migration/alembic_migrations/versions/2eeaf963a447_floatingip_status.py @@ -44,6 +44,7 @@ migration_for_plugins = [ 'neutron.plugins.nec.nec_plugin.NECPluginV2', 'neutron.plugins.nicira.NeutronPlugin.NvpPluginV2', 'neutron.plugins.nicira.NeutronServicePlugin.NvpAdvancedPlugin', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', 'neutron.plugins.openvswitch.ovs_neutron_plugin.OVSNeutronPluginV2', 'neutron.plugins.plumgrid.plumgrid_plugin.plumgrid_plugin.' 'NeutronPluginPLUMgridV2', diff --git a/neutron/db/migration/alembic_migrations/versions/3cb5d900c5de_security_groups.py b/neutron/db/migration/alembic_migrations/versions/3cb5d900c5de_security_groups.py index 5959efd3d0..057a360aa6 100644 --- a/neutron/db/migration/alembic_migrations/versions/3cb5d900c5de_security_groups.py +++ b/neutron/db/migration/alembic_migrations/versions/3cb5d900c5de_security_groups.py @@ -38,6 +38,7 @@ migration_for_plugins = [ 'neutron.plugins.ryu.ryu_neutron_plugin.RyuNeutronPluginV2', 'neutron.plugins.vmware.plugin.NsxPlugin', 'neutron.plugins.vmware.plugin.NsxServicePlugin', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] from alembic import op diff --git a/neutron/db/migration/alembic_migrations/versions/4692d074d587_agent_scheduler.py b/neutron/db/migration/alembic_migrations/versions/4692d074d587_agent_scheduler.py index 86d14f7907..fdf13639fc 100644 --- a/neutron/db/migration/alembic_migrations/versions/4692d074d587_agent_scheduler.py +++ b/neutron/db/migration/alembic_migrations/versions/4692d074d587_agent_scheduler.py @@ -38,6 +38,7 @@ migration_for_plugins = [ 'neutron.plugins.nec.nec_plugin.NECPluginV2', 'neutron.plugins.vmware.plugin.NsxPlugin', 'neutron.plugins.vmware.plugin.NsxServicePlugin', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] 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 a1154af57f..12d75aa9ef 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 @@ -40,6 +40,7 @@ migration_for_plugins = [ 'neutron.plugins.vmware.plugin.NsxServicePlugin', 'neutron.services.loadbalancer.plugin.LoadBalancerPlugin', 'neutron.plugins.ibm.sdnve_neutron_plugin.SdnvePluginV2', + 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', ] 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 e75b63c91c..15a0e69046 100644 --- a/neutron/db/migration/alembic_migrations/versions/folsom_initial.py +++ b/neutron/db/migration/alembic_migrations/versions/folsom_initial.py @@ -33,6 +33,7 @@ PLUGINS = { 'ml2': 'neutron.plugins.ml2.plugin.Ml2Plugin', 'nec': 'neutron.plugins.nec.nec_plugin.NECPluginV2', 'nvp': 'neutron.plugins.nicira.NeutronPlugin.NvpPluginV2', + 'ocnvsd': 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2', 'ovs': 'neutron.plugins.openvswitch.ovs_neutron_plugin.OVSNeutronPluginV2', 'plumgrid': 'neutron.plugins.plumgrid.plumgrid_plugin.plumgrid_plugin.' 'NeutronPluginPLUMgridV2', @@ -45,6 +46,7 @@ L3_CAPABLE = [ PLUGINS['meta'], PLUGINS['ml2'], PLUGINS['nec'], + PLUGINS['ocnvsd'], PLUGINS['ovs'], PLUGINS['ryu'], PLUGINS['brocade'], @@ -56,6 +58,7 @@ FOLSOM_QUOTA = [ PLUGINS['lbr'], PLUGINS['ml2'], PLUGINS['nvp'], + PLUGINS['ocnvsd'], PLUGINS['ovs'], ] diff --git a/neutron/plugins/oneconvergence/README b/neutron/plugins/oneconvergence/README new file mode 100644 index 0000000000..0169624124 --- /dev/null +++ b/neutron/plugins/oneconvergence/README @@ -0,0 +1,32 @@ +One Convergence Neutron Plugin to implement the Neutron v2.0 API. The plugin +works with One Convergence NVSD controller to provide network virtualization +functionality. + +The plugin is enabled with the following configuration line in neutron.conf: + +core_plugin = neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2 + +The configuration parameters required for the plugin are specified in the file +etc/neutron/plugins/oneconvergence/nvsdplugin.ini. The configuration file contains +description of the different parameters. + +To enable One Convergence Neutron Plugin with devstack and configure the required +parameters, use the following lines in localrc: + +Q_PLUGIN=oneconvergence + +disable_service n-net +disable_service q-agt +enable_service q-dhcp +enable_service q-svc +enable_service q-l3 +enable_service q-meta +enable_service neutron + +NVSD_IP= +NVSD_PORT= +NVSD_USER= +NVSD_PASSWD= + +The NVSD controller configuration should be specified in nvsdplugin.ini before +invoking stack.sh. diff --git a/neutron/plugins/oneconvergence/__init__.py b/neutron/plugins/oneconvergence/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/plugins/oneconvergence/lib/__init__.py b/neutron/plugins/oneconvergence/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/plugins/oneconvergence/lib/config.py b/neutron/plugins/oneconvergence/lib/config.py new file mode 100644 index 0000000000..f6eae2607b --- /dev/null +++ b/neutron/plugins/oneconvergence/lib/config.py @@ -0,0 +1,41 @@ +# Copyright 2014 OneConvergence, 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. +# + +""" Register the configuration options""" + +from oslo.config import cfg + + +NVSD_OPT = [ + cfg.StrOpt('nvsd_ip', + default='127.0.0.1', + help=_("NVSD Controller IP address")), + cfg.IntOpt('nvsd_port', + default=8082, + help=_("NVSD Controller Port number")), + cfg.StrOpt('nvsd_user', + default='ocplugin', + help=_("NVSD Controller username")), + cfg.StrOpt('nvsd_passwd', + default='oc123', secret=True, + help=_("NVSD Controller password")), + cfg.IntOpt('request_timeout', + default=30, + help=_("NVSD controller REST API request timeout in seconds")), + cfg.IntOpt('nvsd_retries', default=0, + help=_("Number of login retries to NVSD controller")) +] + +cfg.CONF.register_opts(NVSD_OPT, "nvsd") diff --git a/neutron/plugins/oneconvergence/lib/exception.py b/neutron/plugins/oneconvergence/lib/exception.py new file mode 100644 index 0000000000..b6864b13f1 --- /dev/null +++ b/neutron/plugins/oneconvergence/lib/exception.py @@ -0,0 +1,55 @@ +# Copyright 2014 OneConvergence, 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. +# + +"""NVSD Exception Definitions.""" + +from neutron.common import exceptions as n_exc + + +class NVSDAPIException(n_exc.NeutronException): + '''Base NVSDplugin Exception.''' + message = _("An unknown nvsd plugin exception occurred: %(reason)s") + + +class RequestTimeout(NVSDAPIException): + message = _("The request has timed out.") + + +class UnAuthorizedException(NVSDAPIException): + message = _("Invalid access credentials to the Server.") + + +class NotFoundException(NVSDAPIException): + message = _("A resource is not found: %(reason)s") + + +class BadRequestException(NVSDAPIException): + message = _("Request sent to server is invalid: %(reason)s") + + +class ServerException(NVSDAPIException): + message = _("Internal Server Error: %(reason)s") + + +class ConnectionClosedException(NVSDAPIException): + message = _("Connection is closed by the server.") + + +class ForbiddenException(NVSDAPIException): + message = _("The request is forbidden access to the resource: %(reason)s") + + +class InternalServerError(NVSDAPIException): + message = _("Internal Server Error from NVSD controller: %(reason)s") diff --git a/neutron/plugins/oneconvergence/lib/nvsdlib.py b/neutron/plugins/oneconvergence/lib/nvsdlib.py new file mode 100644 index 0000000000..bbf4e5b3f3 --- /dev/null +++ b/neutron/plugins/oneconvergence/lib/nvsdlib.py @@ -0,0 +1,262 @@ +# Copyright 2014 OneConvergence, 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: Kedar Kulkarni, One Convergence, Inc. + +"""Intermidiate NVSD Library.""" + +from neutron.openstack.common import excutils +from neutron.openstack.common import jsonutils as json +from neutron.openstack.common import log as logging +import neutron.plugins.oneconvergence.lib.exception as nvsdexception +from neutron.plugins.oneconvergence.lib import plugin_helper + +LOG = logging.getLogger(__name__) + +NETWORKS_URI = "/pluginhandler/ocplugin/tenant/%s/lnetwork/" +NETWORK_URI = NETWORKS_URI + "%s" +GET_ALL_NETWORKS = "/pluginhandler/ocplugin/tenant/getallnetworks" + +SUBNETS_URI = NETWORK_URI + "/lsubnet/" +SUBNET_URI = SUBNETS_URI + "%s" +GET_ALL_SUBNETS = "/pluginhandler/ocplugin/tenant/getallsubnets" + +PORTS_URI = NETWORK_URI + "/lport/" +PORT_URI = PORTS_URI + "%s" + +METHODS = {"POST": "create", + "PUT": "update", + "DELETE": "delete", + "GET": "get"} + + +class NVSDApi(object): + + def build_error_msg(self, method, resource, tenant_id, resource_id): + if method == "POST": + msg = _("Could not create a %(resource)s under tenant " + "%(tenant_id)s") % {'resource': resource, + 'tenant_id': tenant_id} + elif resource_id: + msg = _("Failed to %(method)s %(resource)s " + "id=%(resource_id)s") % {'method': METHODS[method], + 'resource': resource, + 'resource_id': resource_id + } + else: + msg = _("Failed to %(method)s %(resource)s") % { + 'method': METHODS[method], 'resource': resource} + return msg + + def set_connection(self): + self.nvsdcontroller = plugin_helper.initialize_plugin_helper() + self.nvsdcontroller.login() + + def send_request(self, method, uri, body=None, resource=None, + tenant_id='', resource_id=None): + """Issue a request to NVSD controller.""" + + try: + result = self.nvsdcontroller.request(method, uri, body=body) + except nvsdexception.NVSDAPIException as e: + with excutils.save_and_reraise_exception() as ctxt: + msg = self.build_error_msg(method, resource, tenant_id, + resource_id) + LOG.error(msg) + # Modifying the reason message without disturbing the exception + # info + ctxt.value = type(e)(reason=msg) + return result + + def create_network(self, network): + + tenant_id = network['tenant_id'] + router_external = network['router:external'] is True + + network_obj = { + "name": network['name'], + "tenant_id": tenant_id, + "shared": network['shared'], + "admin_state_up": network['admin_state_up'], + "router:external": router_external + } + + uri = NETWORKS_URI % tenant_id + + response = self.send_request("POST", uri, body=json.dumps(network_obj), + resource='network', tenant_id=tenant_id) + + nvsd_net = response.json() + + LOG.debug(_("Network %(id)s created under tenant %(tenant_id)s"), + {'id': nvsd_net['id'], 'tenant_id': tenant_id}) + + return nvsd_net + + def update_network(self, network, network_update): + + tenant_id = network['tenant_id'] + network_id = network['id'] + + uri = NETWORK_URI % (tenant_id, network_id) + + self.send_request("PUT", uri, + body=json.dumps(network_update), + resource='network', tenant_id=tenant_id, + resource_id=network_id) + + LOG.debug(_("Network %(id)s updated under tenant %(tenant_id)s"), + {'id': network_id, 'tenant_id': tenant_id}) + + def delete_network(self, network, subnets=[]): + + tenant_id = network['tenant_id'] + network_id = network['id'] + + ports = self._get_ports(tenant_id, network_id) + + for port in ports: + self.delete_port(port['id'], port) + + for subnet in subnets: + self.delete_subnet(subnet) + + path = NETWORK_URI % (tenant_id, network_id) + + self.send_request("DELETE", path, resource='network', + tenant_id=tenant_id, resource_id=network_id) + + LOG.debug(_("Network %(id)s deleted under tenant %(tenant_id)s"), + {'id': network_id, 'tenant_id': tenant_id}) + + def create_subnet(self, subnet): + + tenant_id = subnet['tenant_id'] + network_id = subnet['network_id'] + + uri = SUBNETS_URI % (tenant_id, network_id) + + self.send_request("POST", uri, body=json.dumps(subnet), + resource='subnet', tenant_id=tenant_id) + + LOG.debug(_("Subnet %(id)s created under tenant %(tenant_id)s"), + {'id': subnet['id'], 'tenant_id': tenant_id}) + + def delete_subnet(self, subnet): + + tenant_id = subnet['tenant_id'] + network_id = subnet['network_id'] + subnet_id = subnet['id'] + + uri = SUBNET_URI % (tenant_id, network_id, subnet_id) + + self.send_request("DELETE", uri, resource='subnet', + tenant_id=tenant_id, resource_id=subnet_id) + + LOG.debug(_("Subnet %(id)s deleted under tenant %(tenant_id)s"), + {'id': subnet_id, 'tenant_id': tenant_id}) + + def update_subnet(self, subnet, subnet_update): + + tenant_id = subnet['tenant_id'] + network_id = subnet['network_id'] + subnet_id = subnet['id'] + + uri = SUBNET_URI % (tenant_id, network_id, subnet_id) + + self.send_request("PUT", uri, + body=json.dumps(subnet_update), + resource='subnet', tenant_id=tenant_id, + resource_id=subnet_id) + + LOG.debug(_("Subnet %(id)s updated under tenant %(tenant_id)s"), + {'id': subnet_id, 'tenant_id': tenant_id}) + + def create_port(self, tenant_id, port): + + network_id = port["network_id"] + fixed_ips = port.get("fixed_ips") + ip_address = None + subnet_id = None + + if fixed_ips: + ip_address = fixed_ips[0].get("ip_address") + subnet_id = fixed_ips[0].get("subnet_id") + + lport = { + "id": port["id"], + "name": port["name"], + "device_id": port["device_id"], + "device_owner": port["device_owner"], + "mac_address": port["mac_address"], + "ip_address": ip_address, + "subnet_id": subnet_id, + "admin_state_up": port["admin_state_up"], + "network_id": network_id, + "status": port["status"] + } + + path = PORTS_URI % (tenant_id, network_id) + + self.send_request("POST", path, body=json.dumps(lport), + resource='port', tenant_id=tenant_id) + + LOG.debug(_("Port %(id)s created under tenant %(tenant_id)s"), + {'id': port['id'], 'tenant_id': tenant_id}) + + def update_port(self, tenant_id, port, port_update): + + network_id = port['network_id'] + port_id = port['id'] + + lport = {} + for k in ('admin_state_up', 'name', 'device_id', 'device_owner'): + if k in port_update: + lport[k] = port_update[k] + + fixed_ips = port_update.get('fixed_ips', None) + if fixed_ips: + lport["ip_address"] = fixed_ips[0].get("ip_address") + lport["subnet_id"] = fixed_ips[0].get("subnet_id") + + uri = PORT_URI % (tenant_id, network_id, port_id) + + self.send_request("PUT", uri, body=json.dumps(lport), + resource='port', tenant_id=tenant_id, + resource_id=port_id) + + LOG.debug(_("Port %(id)s updated under tenant %(tenant_id)s"), + {'id': port_id, 'tenant_id': tenant_id}) + + def delete_port(self, port_id, port): + + tenant_id = port['tenant_id'] + network_id = port['network_id'] + + uri = PORT_URI % (tenant_id, network_id, port_id) + + self.send_request("DELETE", uri, resource='port', tenant_id=tenant_id, + resource_id=port_id) + + LOG.debug(_("Port %(id)s deleted under tenant %(tenant_id)s"), + {'id': port_id, 'tenant_id': tenant_id}) + + def _get_ports(self, tenant_id, network_id): + + uri = PORTS_URI % (tenant_id, network_id) + + response = self.send_request("GET", uri, resource='ports', + tenant_id=tenant_id) + + return response.json() diff --git a/neutron/plugins/oneconvergence/lib/plugin_helper.py b/neutron/plugins/oneconvergence/lib/plugin_helper.py new file mode 100644 index 0000000000..060f60600f --- /dev/null +++ b/neutron/plugins/oneconvergence/lib/plugin_helper.py @@ -0,0 +1,186 @@ +# Copyright 2014 OneConvergence, 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: Kedar Kulkarni, One Convergence, Inc. + +"""Library to talk to NVSD controller.""" + +import httplib +import time +from urlparse import urljoin + +from oslo.config import cfg +import requests + +from neutron.openstack.common import jsonutils as json +from neutron.openstack.common import log as logging +import neutron.plugins.oneconvergence.lib.exception as exception + +LOG = logging.getLogger(__name__) + + +def initialize_plugin_helper(): + nvsdcontroller = NVSDController() + return nvsdcontroller + + +class NVSDController(object): + + """Encapsulates the NVSD Controller details.""" + + def __init__(self): + + self._host = cfg.CONF.nvsd.nvsd_ip + self._port = cfg.CONF.nvsd.nvsd_port + self._user = cfg.CONF.nvsd.nvsd_user + self._password = cfg.CONF.nvsd.nvsd_passwd + self._retries = cfg.CONF.nvsd.nvsd_retries + self._request_timeout = float(cfg.CONF.nvsd.request_timeout) + self.api_url = 'http://' + self._host + ':' + str(self._port) + + self.pool = requests.Session() + + self.auth_token = None + + def do_request(self, method, url=None, headers=None, data=None, + timeout=10): + response = self.pool.request(method, url=url, + headers=headers, data=data, + timeout=self._request_timeout) + return response + + def login(self): + """Login to NVSD Controller.""" + + headers = {"Content-Type": "application/json"} + + login_url = urljoin(self.api_url, + "/pluginhandler/ocplugin/authmgmt/login") + + data = json.dumps({"user_name": self._user, "passwd": self._password}) + + attempts = 0 + + while True: + if attempts < self._retries: + attempts += 1 + elif self._retries == 0: + attempts = 0 + else: + msg = _("Unable to connect to NVSD controller. Exiting after " + "%(retries)s attempts") % {'retries': self._retries} + LOG.error(msg) + raise exception.ServerException(reason=msg) + try: + response = self.do_request("POST", url=login_url, + headers=headers, data=data, + timeout=self._request_timeout) + break + except Exception as e: + LOG.error(_("Login Failed: %s"), e) + LOG.error(_("Unable to establish connection" + " with Controller %s"), self.api_url) + LOG.error(_("Retrying after 1 second...")) + time.sleep(1) + + if response.status_code == requests.codes.ok: + LOG.debug(_("Login Successful %(uri)s " + "%(status)s"), {'uri': self.api_url, + 'status': response.status_code}) + self.auth_token = json.loads(response.content)["session_uuid"] + LOG.debug(_("AuthToken = %s"), self.auth_token) + else: + LOG.error(_("login failed")) + + return + + def request(self, method, url, body="", content_type="application/json"): + """Issue a request to NVSD controller.""" + + if self.auth_token is None: + LOG.warning(_("No Token, Re-login")) + self.login() + + headers = {"Content-Type": content_type} + + uri = urljoin(url, "?authToken=%s" % self.auth_token) + + url = urljoin(self.api_url, uri) + + request_ok = False + response = None + + try: + response = self.do_request(method, url=url, + headers=headers, data=body, + timeout=self._request_timeout) + + LOG.debug(_("request: %(method)s %(uri)s successful"), + {'method': method, 'uri': self.api_url + uri}) + request_ok = True + except httplib.IncompleteRead as e: + response = e.partial + request_ok = True + except Exception as e: + LOG.error(_("request: Request failed from " + "Controller side :%s"), e) + + if response is None: + # Timeout. + LOG.error(_("Response is Null, Request timed out: %(method)s to " + "%(uri)s"), {'method': method, 'uri': uri}) + self.auth_token = None + raise exception.RequestTimeout() + + status = response.status_code + if status == requests.codes.unauthorized: + self.auth_token = None + # Raise an exception to inform that the request failed. + raise exception.UnAuthorizedException() + + if status in self.error_codes: + LOG.error(_("Request %(method)s %(uri)s body = %(body)s failed " + "with status %(status)s"), {'method': method, + 'uri': uri, 'body': body, + 'status': status}) + LOG.error(_("%s"), response.reason) + raise self.error_codes[status]() + elif status not in (requests.codes.ok, requests.codes.created, + requests.codes.no_content): + LOG.error(_("%(method)s to %(url)s, unexpected response code: " + "%(status)d"), {'method': method, 'url': url, + 'status': status}) + return + + if not request_ok: + LOG.error(_("Request failed from Controller side with " + "Status=%s"), status) + raise exception.ServerException() + else: + LOG.debug(_("Success: %(method)s %(url)s status=%(status)s"), + {'method': method, 'url': self.api_url + uri, + 'status': status}) + response.body = response.content + return response + + error_codes = { + 404: exception.NotFoundException, + 409: exception.BadRequestException, + 500: exception.InternalServerError, + 503: exception.ServerException, + 403: exception.ForbiddenException, + 301: exception.NVSDAPIException, + 307: exception.NVSDAPIException, + 400: exception.NVSDAPIException, + } diff --git a/neutron/plugins/oneconvergence/plugin.py b/neutron/plugins/oneconvergence/plugin.py new file mode 100644 index 0000000000..59646cb216 --- /dev/null +++ b/neutron/plugins/oneconvergence/plugin.py @@ -0,0 +1,300 @@ +# Copyright 2014 OneConvergence, 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: Kedar Kulkarni, One Convergence, Inc. + +"""Implementation of OneConvergence Neutron Plugin.""" + +from oslo.config import cfg + +from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api +from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api +from neutron.common import constants as q_const +from neutron.common import exceptions as nexception +from neutron.common import rpc as q_rpc +from neutron.common import topics +from neutron.db import agents_db +from neutron.db import agentschedulers_db +from neutron.db import db_base_plugin_v2 +from neutron.db import dhcp_rpc_base +from neutron.db import external_net_db +from neutron.db import extraroute_db +from neutron.db import l3_agentschedulers_db +from neutron.db import l3_gwmode_db +from neutron.db import l3_rpc_base +from neutron.db import portbindings_base +from neutron.db import quota_db # noqa +from neutron.extensions import portbindings +from neutron.openstack.common import excutils +from neutron.openstack.common import importutils +from neutron.openstack.common import log as logging +from neutron.openstack.common import rpc +from neutron.plugins.common import constants as svc_constants +import neutron.plugins.oneconvergence.lib.config # noqa +import neutron.plugins.oneconvergence.lib.exception as nvsdexception +from neutron.plugins.oneconvergence.lib import nvsdlib as nvsd_lib + +LOG = logging.getLogger(__name__) +IPv6 = 6 + + +class NVSDRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin, + l3_rpc_base.L3RpcCallbackMixin): + + """Agent callback.""" + + RPC_API_VERSION = '1.1' + + def create_rpc_dispatcher(self): + """Get the rpc dispatcher for this manager.""" + return q_rpc.PluginRpcDispatcher([self, + agents_db.AgentExtRpcCallback()]) + + +class OneConvergencePluginV2(db_base_plugin_v2.NeutronDbPluginV2, + extraroute_db.ExtraRoute_db_mixin, + l3_agentschedulers_db.L3AgentSchedulerDbMixin, + agentschedulers_db.DhcpAgentSchedulerDbMixin, + external_net_db.External_net_db_mixin, + l3_gwmode_db.L3_NAT_db_mixin, + portbindings_base.PortBindingBaseMixin): + + """L2 Virtual Network Plugin. + + OneConvergencePluginV2 is a Neutron plugin that provides L2 Virtual Network + functionality. + """ + + __native_bulk_support = True + __native_pagination_support = True + __native_sorting_support = True + + supported_extension_aliases = ['agent', + 'binding', + 'dhcp_agent_scheduler', + 'ext-gw-mode', + 'external-net', + 'extraroute', + 'l3_agent_scheduler', + 'quotas', + 'router', + ] + + def __init__(self): + + super(OneConvergencePluginV2, self).__init__() + + self.oneconvergence_init() + + self.base_binding_dict = { + portbindings.VIF_TYPE: portbindings.VIF_TYPE_OVS} + + portbindings_base.register_port_dict_function() + + self.setup_rpc() + + self.network_scheduler = importutils.import_object( + cfg.CONF.network_scheduler_driver) + self.router_scheduler = importutils.import_object( + cfg.CONF.router_scheduler_driver) + + def oneconvergence_init(self): + """Initialize the connections and set the log levels for the plugin.""" + + self.nvsdlib = nvsd_lib.NVSDApi() + self.nvsdlib.set_connection() + + def setup_rpc(self): + # RPC support + self.service_topics = {svc_constants.CORE: topics.PLUGIN, + svc_constants.L3_ROUTER_NAT: topics.L3PLUGIN} + self.conn = rpc.create_connection(new=True) + self.agent_notifiers[q_const.AGENT_TYPE_DHCP] = ( + dhcp_rpc_agent_api.DhcpAgentNotifyAPI() + ) + self.agent_notifiers[q_const.AGENT_TYPE_L3] = ( + l3_rpc_agent_api.L3AgentNotify + ) + self.callbacks = NVSDRpcCallbacks() + self.dispatcher = self.callbacks.create_rpc_dispatcher() + for svc_topic in self.service_topics.values(): + self.conn.create_consumer(svc_topic, self.dispatcher, fanout=False) + + # Consume from all consumers in a thread + self.conn.consume_in_thread() + + def create_network(self, context, network): + + net = self.nvsdlib.create_network(network['network']) + + network['network']['id'] = net['id'] + + try: + neutron_net = super(OneConvergencePluginV2, + self).create_network(context, network) + + #following call checks whether the network is external or not and + #if it is external then adds this network to externalnetworks + #table of neutron db + self._process_l3_create(context, neutron_net, network['network']) + except nvsdexception.NVSDAPIException: + with excutils.save_and_reraise_exception(): + self.nvsdlib.delete_network(net) + + return neutron_net + + def update_network(self, context, net_id, network): + + with context.session.begin(subtransactions=True): + + neutron_net = super(OneConvergencePluginV2, + self).update_network(context, net_id, network) + + self.nvsdlib.update_network(neutron_net, network['network']) + # updates neutron database e.g. externalnetworks table. + self._process_l3_update(context, neutron_net, network['network']) + + return neutron_net + + def delete_network(self, context, net_id): + + with context.session.begin(subtransactions=True): + network = self._get_network(context, net_id) + #get all the subnets under the network to delete them + subnets = self._get_subnets_by_network(context, net_id) + + super(OneConvergencePluginV2, self).delete_network(context, + net_id) + + self.nvsdlib.delete_network(network, subnets) + + def create_subnet(self, context, subnet): + + if subnet['subnet']['ip_version'] == IPv6: + raise nexception.InvalidInput( + error_message="NVSDPlugin doesn't support IPv6.") + + neutron_subnet = super(OneConvergencePluginV2, + self).create_subnet(context, subnet) + + try: + self.nvsdlib.create_subnet(neutron_subnet) + except nvsdexception.NVSDAPIException: + with excutils.save_and_reraise_exception(): + #Log the message and delete the subnet from the neutron + super(OneConvergencePluginV2, + self).delete_subnet(context, neutron_subnet['id']) + LOG.error(_("Failed to create subnet, " + "deleting it from neutron")) + + return neutron_subnet + + def delete_subnet(self, context, subnet_id): + + neutron_subnet = self._get_subnet(context, subnet_id) + + with context.session.begin(subtransactions=True): + + super(OneConvergencePluginV2, self).delete_subnet(context, + subnet_id) + + self.nvsdlib.delete_subnet(neutron_subnet) + + def update_subnet(self, context, subnet_id, subnet): + + with context.session.begin(subtransactions=True): + + neutron_subnet = super(OneConvergencePluginV2, + self).update_subnet(context, subnet_id, + subnet) + + self.nvsdlib.update_subnet(neutron_subnet, subnet) + return neutron_subnet + + def create_port(self, context, port): + + network = {} + + network_id = port['port']['network_id'] + + with context.session.begin(subtransactions=True): + + # Invoke the Neutron API for creating port + neutron_port = super(OneConvergencePluginV2, + self).create_port(context, port) + + self._process_portbindings_create_and_update(context, + port['port'], + neutron_port) + + if port['port']['device_owner'] in ('network:router_gateway', + 'network:floatingip'): + # for l3 requests, tenant_id will be None/'' + network = self._get_network(context, network_id) + + tenant_id = network['tenant_id'] + else: + tenant_id = port['port']['tenant_id'] + + port_id = neutron_port['id'] + + try: + self.nvsdlib.create_port(tenant_id, neutron_port) + except nvsdexception.NVSDAPIException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Deleting newly created " + "neutron port %s"), port_id) + super(OneConvergencePluginV2, self).delete_port(context, + port_id) + + return neutron_port + + def update_port(self, context, port_id, port): + + with context.session.begin(subtransactions=True): + + neutron_port = super(OneConvergencePluginV2, + self).update_port(context, port_id, port) + + if neutron_port['tenant_id'] == '': + network = self._get_network(context, + neutron_port['network_id']) + tenant_id = network['tenant_id'] + else: + tenant_id = neutron_port['tenant_id'] + + self.nvsdlib.update_port(tenant_id, neutron_port, port['port']) + + self._process_portbindings_create_and_update(context, + port['port'], + neutron_port) + return neutron_port + + def delete_port(self, context, port_id, l3_port_check=True): + + if l3_port_check: + self.prevent_l3_port_deletion(context, port_id) + + neutron_port = self._get_port(context, port_id) + + with context.session.begin(subtransactions=True): + + self.disassociate_floatingips(context, port_id) + + super(OneConvergencePluginV2, self).delete_port(context, port_id) + + network = self._get_network(context, neutron_port['network_id']) + neutron_port['tenant_id'] = network['tenant_id'] + + self.nvsdlib.delete_port(port_id, neutron_port) diff --git a/neutron/tests/unit/oneconvergence/__init__.py b/neutron/tests/unit/oneconvergence/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py b/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py new file mode 100644 index 0000000000..e503a5ede4 --- /dev/null +++ b/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py @@ -0,0 +1,109 @@ +# Copyright 2014 OneConvergence, 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. +# + +"""Test Library for OneConvergencePlugin.""" + +import contextlib +import uuid + +import mock +from oslo.config import cfg + +from neutron import context +from neutron.extensions import portbindings +from neutron.manager import NeutronManager +from neutron.plugins.oneconvergence import plugin as nvsd_plugin +from neutron.tests.unit import _test_extension_portbindings as test_bindings +from neutron.tests.unit import test_db_plugin as test_plugin + +PLUGIN_NAME = 'neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2' + + +class OneConvergencePluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): + _plugin_name = PLUGIN_NAME + + def setUp(self): + def mocked_oneconvergence_init(self): + def side_effect(*args, **kwargs): + return {'id': str(uuid.uuid4())} + + self.nvsdlib = mock.Mock() + self.nvsdlib.create_network.side_effect = side_effect + + self.addCleanup(mock.patch.stopall) + + with mock.patch.object(nvsd_plugin.OneConvergencePluginV2, + 'oneconvergence_init', + new=mocked_oneconvergence_init): + super(OneConvergencePluginV2TestCase, + self).setUp(self._plugin_name) + + +class TestOneConvergencePluginNetworksV2(test_plugin.TestNetworksV2, + OneConvergencePluginV2TestCase): + pass + + +class TestOneConvergencePluginSubnetsV2(test_plugin.TestSubnetsV2, + OneConvergencePluginV2TestCase): + def test_update_subnet_inconsistent_ipv6_gatewayv4(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_create_subnet_with_v6_allocation_pool(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_update_subnet_inconsistent_ipv6_hostroute_dst_v4(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_update_subnet_inconsistent_ipv6_hostroute_np_v4(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + +class TestOneConvergencePluginPortsV2(test_plugin.TestPortsV2, + test_bindings.PortBindingsTestCase, + OneConvergencePluginV2TestCase): + VIF_TYPE = portbindings.VIF_TYPE_OVS + + def test_requested_subnet_id_v4_and_v6(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_port_vif_details(self): + plugin = NeutronManager.get_plugin() + with self.port(name='name') as port1: + ctx = context.get_admin_context() + port = plugin.get_port(ctx, port1['port']['id']) + self.assertEqual(port['binding:vif_type'], + portbindings.VIF_TYPE_OVS) + + def test_ports_vif_details(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + plugin = NeutronManager.get_plugin() + with contextlib.nested(self.port(), self.port()) as (port1, port2): + ctx = context.get_admin_context() + ports = plugin.get_ports(ctx) + self.assertEqual(len(ports), 2) + for port in ports: + self.assertEqual(port['binding:vif_type'], + portbindings.VIF_TYPE_OVS) + + +class TestOneConvergenceBasicGet(test_plugin.TestBasicGet, + OneConvergencePluginV2TestCase): + pass + + +class TestOneConvergenceV2HTTPResponse(test_plugin.TestV2HTTPResponse, + OneConvergencePluginV2TestCase): + pass diff --git a/neutron/tests/unit/oneconvergence/test_nvsdlib.py b/neutron/tests/unit/oneconvergence/test_nvsdlib.py new file mode 100644 index 0000000000..3ebc6d9aa7 --- /dev/null +++ b/neutron/tests/unit/oneconvergence/test_nvsdlib.py @@ -0,0 +1,186 @@ +# Copyright 2014 OneConvergence, 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 mock + +from neutron.openstack.common import jsonutils as json +from neutron.plugins.oneconvergence.lib import nvsdlib +from neutron.tests import base + +NETWORKS_URI = "/pluginhandler/ocplugin/tenant/%s/lnetwork/" +NETWORK_URI = NETWORKS_URI + "%s" +GET_ALL_NETWORKS = "/pluginhandler/ocplugin/tenant/getallnetworks" + +SUBNETS_URI = NETWORK_URI + "/lsubnet/" +SUBNET_URI = SUBNETS_URI + "%s" +GET_ALL_SUBNETS = "/pluginhandler/ocplugin/tenant/getallsubnets" + +PORTS_URI = NETWORK_URI + "/lport/" +PORT_URI = PORTS_URI + "%s" + +TEST_NET = 'test-network' +TEST_SUBNET = 'test-subnet' +TEST_PORT = 'test-port' +TEST_TENANT = 'test-tenant' + + +class TestNVSDApi(base.BaseTestCase): + + def setUp(self): + super(TestNVSDApi, self).setUp() + self.nvsdlib = nvsdlib.NVSDApi() + + def test_create_network(self): + network_obj = { + "name": 'test-net', + "tenant_id": TEST_TENANT, + "shared": False, + "admin_state_up": True, + "router:external": False + } + resp = mock.Mock() + resp.json.return_value = {'id': 'uuid'} + with mock.patch.object(self.nvsdlib, 'send_request', + return_value=resp) as send_request: + uri = NETWORKS_URI % TEST_TENANT + net = self.nvsdlib.create_network(network_obj) + send_request.assert_called_once_with("POST", uri, + body=json.dumps(network_obj), + resource='network', + tenant_id=TEST_TENANT) + self.assertEqual(net, {'id': 'uuid'}) + + def test_update_network(self): + network = {'id': TEST_NET, + 'tenant_id': TEST_TENANT} + update_network = {'name': 'new_name'} + uri = NETWORK_URI % (TEST_TENANT, TEST_NET) + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + self.nvsdlib.update_network(network, update_network) + send_request.assert_called_once_with( + "PUT", uri, body=json.dumps(update_network), + resource='network', tenant_id=TEST_TENANT, + resource_id=TEST_NET) + + def test_delete_network(self): + network = {'id': TEST_NET, + 'tenant_id': TEST_TENANT} + + uri = NETWORK_URI % (TEST_TENANT, TEST_NET) + + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + with mock.patch.object(self.nvsdlib, '_get_ports'): + self.nvsdlib.delete_network(network) + send_request.assert_called_once_with( + "DELETE", uri, resource='network', + tenant_id=TEST_TENANT, resource_id=TEST_NET) + + def test_create_port(self): + path = PORTS_URI % (TEST_TENANT, TEST_NET) + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + fixed_ips = [{'ip_address': '10.0.0.2', + 'subnet_id': TEST_SUBNET}] + + lport = { + "id": TEST_PORT, + "name": 'test', + "device_id": "device_id", + "device_owner": "device_owner", + "mac_address": "mac_address", + "fixed_ips": fixed_ips, + "admin_state_up": True, + "network_id": TEST_NET, + "status": 'ACTIVE' + } + self.nvsdlib.create_port(TEST_TENANT, lport) + expected = {"id": TEST_PORT, "name": 'test', + "device_id": "device_id", + "device_owner": "device_owner", + "mac_address": "mac_address", + "ip_address": '10.0.0.2', + "subnet_id": TEST_SUBNET, + "admin_state_up": True, + "network_id": TEST_NET, + "status": 'ACTIVE'} + send_request.assert_called_once_with("POST", path, + body=json.dumps(expected), + resource='port', + tenant_id=TEST_TENANT) + + def test_update_port(self): + port = {'id': TEST_PORT, + 'network_id': TEST_NET} + + port_update = {'name': 'new-name'} + uri = PORT_URI % (TEST_TENANT, TEST_NET, TEST_PORT) + + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + self.nvsdlib.update_port(TEST_TENANT, port, port_update) + send_request.assert_called_once_with("PUT", uri, + body=json.dumps(port_update), + resource='port', + resource_id='test-port', + tenant_id=TEST_TENANT) + + def test_delete_port(self): + port = {'network_id': TEST_NET, + 'tenant_id': TEST_TENANT} + uri = PORT_URI % (TEST_TENANT, TEST_NET, TEST_PORT) + + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + self.nvsdlib.delete_port(TEST_PORT, port) + send_request.assert_called_once_with("DELETE", uri, + resource='port', + tenant_id=TEST_TENANT, + resource_id=TEST_PORT) + + def test_create_subnet(self): + subnet = {'id': TEST_SUBNET, + 'tenant_id': TEST_TENANT, + 'network_id': TEST_NET} + uri = SUBNETS_URI % (TEST_TENANT, TEST_NET) + + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + self.nvsdlib.create_subnet(subnet) + send_request.assert_called_once_with("POST", uri, + body=json.dumps(subnet), + resource='subnet', + tenant_id=TEST_TENANT) + + def test_update_subnet(self): + subnet = {'id': TEST_SUBNET, + 'tenant_id': TEST_TENANT, + 'network_id': TEST_NET} + subnet_update = {'name': 'new-name'} + uri = SUBNET_URI % (TEST_TENANT, TEST_NET, TEST_SUBNET) + + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + self.nvsdlib.update_subnet(subnet, subnet_update) + send_request.assert_called_once_with( + "PUT", uri, body=json.dumps(subnet_update), resource='subnet', + tenant_id=TEST_TENANT, resource_id=TEST_SUBNET) + + def test_delete_subnet(self): + subnet = {'id': TEST_SUBNET, + 'tenant_id': TEST_TENANT, + 'network_id': TEST_NET} + uri = SUBNET_URI % (TEST_TENANT, TEST_NET, TEST_SUBNET) + + with mock.patch.object(self.nvsdlib, 'send_request') as send_request: + self.nvsdlib.delete_subnet(subnet) + send_request.assert_called_once_with("DELETE", uri, + resource='subnet', + tenant_id=TEST_TENANT, + resource_id=TEST_SUBNET) diff --git a/neutron/tests/unit/oneconvergence/test_plugin_helper.py b/neutron/tests/unit/oneconvergence/test_plugin_helper.py new file mode 100644 index 0000000000..21031a79cf --- /dev/null +++ b/neutron/tests/unit/oneconvergence/test_plugin_helper.py @@ -0,0 +1,60 @@ +# Copyright 2014 OneConvergence, 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: Kedar Kulkarni, One Convergence, Inc. +import mock +import requests + +from neutron.openstack.common import jsonutils as json +from neutron.plugins.oneconvergence.lib import config # noqa +from neutron.plugins.oneconvergence.lib import plugin_helper as client +from neutron.tests import base + + +class TestPluginHelper(base.BaseTestCase): + def setUp(self): + super(TestPluginHelper, self).setUp() + self.nvsdcontroller = client.NVSDController() + + def get_response(self, *args, **kwargs): + response = mock.Mock() + response.status_code = requests.codes.ok + response.content = json.dumps({'session_uuid': 'new_auth_token'}) + return response + + def test_login(self): + login_url = ('http://127.0.0.1:8082/pluginhandler/ocplugin/' + 'authmgmt/login') + headers = {'Content-Type': 'application/json'} + data = json.dumps({"user_name": "ocplugin", "passwd": "oc123"}) + timeout = 30.0 + + with mock.patch.object(self.nvsdcontroller, 'do_request', + side_effect=self.get_response) as do_request: + self.nvsdcontroller.login() + do_request.assert_called_once_with('POST', url=login_url, + headers=headers, data=data, + timeout=timeout) + + def test_request(self): + with mock.patch.object(self.nvsdcontroller, 'do_request', + side_effect=self.get_response) as do_request: + self.nvsdcontroller.login() + self.nvsdcontroller.request("POST", "/some_url") + self.assertEqual(do_request.call_count, 2) + do_request.assert_called_with( + 'POST', + url='http://127.0.0.1:8082/some_url?authToken=new_auth_token', + headers={'Content-Type': 'application/json'}, data='', + timeout=30.0) diff --git a/setup.cfg b/setup.cfg index ef14663453..274a490df7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,6 +68,7 @@ data_files = etc/neutron/plugins/mlnx = etc/neutron/plugins/mlnx/mlnx_conf.ini etc/neutron/plugins/nec = etc/neutron/plugins/nec/nec.ini etc/neutron/plugins/nicira = etc/neutron/plugins/nicira/nvp.ini + etc/neutron/plugins/oneconvergence = etc/neutron/plugins/oneconvergence/nvsdplugin.ini etc/neutron/plugins/openvswitch = etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini etc/neutron/plugins/plumgrid = etc/neutron/plugins/plumgrid/plumgrid.ini etc/neutron/plugins/ryu = etc/neutron/plugins/ryu/ryu.ini @@ -144,6 +145,7 @@ neutron.core_plugins = mlnx = neutron.plugins.mlnx.mlnx_plugin:MellanoxEswitchPlugin nec = neutron.plugins.nec.nec_plugin:NECPluginV2 nicira = neutron.plugins.nicira.NeutronPlugin:NvpPluginV2 + oneconvergence = neutron.plugins.oneconvergence.plugin.OneConvergencePluginV2 openvswitch = neutron.plugins.openvswitch.ovs_neutron_plugin:OVSNeutronPluginV2 plumgrid = neutron.plugins.plumgrid.plumgrid_plugin.plumgrid_plugin:NeutronPluginPLUMgridV2 ryu = neutron.plugins.ryu.ryu_neutron_plugin:RyuNeutronPluginV2