From e2bab25f54e61b098984e9b15985e7b8b9c2e2e3 Mon Sep 17 00:00:00 2001 From: ronak Date: Thu, 16 Jan 2014 10:26:01 -0800 Subject: [PATCH] Nuage Networks Plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuage networks’ openstack networking plugin enables integration of openstack with Nuage Networks’ Virtual Service Platform (VSP) Change-Id: If20b385b78a350cb9aae2c70b6a44888e74c23bc Implements: blueprint nuage-networks-plugin --- etc/neutron/plugins/nuage/nuage_plugin.ini | 10 + .../versions/e766b19a3bb_nuage_initial.py | 120 +++ neutron/plugins/nuage/__init__.py | 0 neutron/plugins/nuage/common/__init__.py | 0 neutron/plugins/nuage/common/config.py | 47 ++ neutron/plugins/nuage/common/constants.py | 26 + neutron/plugins/nuage/common/exceptions.py | 24 + neutron/plugins/nuage/extensions/__init__.py | 0 .../plugins/nuage/extensions/netpartition.py | 107 +++ .../plugins/nuage/extensions/nuage_router.py | 73 ++ .../plugins/nuage/extensions/nuage_subnet.py | 59 ++ neutron/plugins/nuage/nuage_models.py | 74 ++ neutron/plugins/nuage/nuagedb.py | 133 ++++ neutron/plugins/nuage/plugin.py | 697 ++++++++++++++++++ neutron/tests/unit/nuage/__init__.py | 0 neutron/tests/unit/nuage/fake_nuageclient.py | 85 +++ neutron/tests/unit/nuage/test_netpartition.py | 93 +++ neutron/tests/unit/nuage/test_nuage_plugin.py | 162 ++++ 18 files changed, 1710 insertions(+) create mode 100644 etc/neutron/plugins/nuage/nuage_plugin.ini create mode 100644 neutron/db/migration/alembic_migrations/versions/e766b19a3bb_nuage_initial.py create mode 100644 neutron/plugins/nuage/__init__.py create mode 100644 neutron/plugins/nuage/common/__init__.py create mode 100644 neutron/plugins/nuage/common/config.py create mode 100644 neutron/plugins/nuage/common/constants.py create mode 100644 neutron/plugins/nuage/common/exceptions.py create mode 100644 neutron/plugins/nuage/extensions/__init__.py create mode 100644 neutron/plugins/nuage/extensions/netpartition.py create mode 100644 neutron/plugins/nuage/extensions/nuage_router.py create mode 100644 neutron/plugins/nuage/extensions/nuage_subnet.py create mode 100644 neutron/plugins/nuage/nuage_models.py create mode 100644 neutron/plugins/nuage/nuagedb.py create mode 100644 neutron/plugins/nuage/plugin.py create mode 100644 neutron/tests/unit/nuage/__init__.py create mode 100644 neutron/tests/unit/nuage/fake_nuageclient.py create mode 100644 neutron/tests/unit/nuage/test_netpartition.py create mode 100644 neutron/tests/unit/nuage/test_nuage_plugin.py diff --git a/etc/neutron/plugins/nuage/nuage_plugin.ini b/etc/neutron/plugins/nuage/nuage_plugin.ini new file mode 100644 index 00000000000..994d1206ce2 --- /dev/null +++ b/etc/neutron/plugins/nuage/nuage_plugin.ini @@ -0,0 +1,10 @@ +# Please fill in the correct data for all the keys below and uncomment key-value pairs +[restproxy] +#default_net_partition_name = +#auth_resource = /auth +#server = ip:port +#organization = org +#serverauth = uname:pass +#serverssl = True +#base_uri = /base + diff --git a/neutron/db/migration/alembic_migrations/versions/e766b19a3bb_nuage_initial.py b/neutron/db/migration/alembic_migrations/versions/e766b19a3bb_nuage_initial.py new file mode 100644 index 00000000000..3562c7d943f --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/e766b19a3bb_nuage_initial.py @@ -0,0 +1,120 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""nuage_initial + +Revision ID: e766b19a3bb +Revises: 1b2580001654 +Create Date: 2014-02-14 18:03:14.841064 + +""" + +# revision identifiers, used by Alembic. +revision = 'e766b19a3bb' +down_revision = '1b2580001654' + +migration_for_plugins = [ + 'neutron.plugins.nuage.plugin.NuagePlugin' +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration +from neutron.db.migration.alembic_migrations import common_ext_ops + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + common_ext_ops.upgrade_l3() + + op.create_table( + 'quotas', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('resource', sa.String(length=255), nullable=True), + sa.Column('limit', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_table( + 'net_partitions', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=64), nullable=True), + sa.Column('l3dom_tmplt_id', sa.String(length=36), nullable=True), + sa.Column('l2dom_tmplt_id', sa.String(length=36), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_table( + 'port_mapping', + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('nuage_vport_id', sa.String(length=36), nullable=True), + sa.Column('nuage_vif_id', sa.String(length=36), nullable=True), + sa.Column('static_ip', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('port_id'), + ) + op.create_table( + 'subnet_l2dom_mapping', + sa.Column('subnet_id', sa.String(length=36), nullable=False), + sa.Column('net_partition_id', sa.String(length=36), nullable=True), + sa.Column('nuage_subnet_id', sa.String(length=36), nullable=True), + sa.Column('nuage_l2dom_tmplt_id', sa.String(length=36), + nullable=True), + sa.Column('nuage_user_id', sa.String(length=36), nullable=True), + sa.Column('nuage_group_id', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['net_partition_id'], ['net_partitions.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['subnet_id'], ['subnets.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('subnet_id'), + ) + op.create_table( + 'net_partition_router_mapping', + sa.Column('net_partition_id', sa.String(length=36), nullable=False), + sa.Column('router_id', sa.String(length=36), nullable=False), + sa.Column('nuage_router_id', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['net_partition_id'], ['net_partitions.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['router_id'], ['routers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('router_id'), + ) + op.create_table( + 'router_zone_mapping', + sa.Column('router_id', sa.String(length=36), nullable=False), + sa.Column('nuage_zone_id', sa.String(length=36), nullable=True), + sa.Column('nuage_user_id', sa.String(length=36), nullable=True), + sa.Column('nuage_group_id', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['router_id'], ['routers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('router_id'), + ) + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_table('router_zone_mapping') + op.drop_table('net_partition_router_mapping') + op.drop_table('subnet_l2dom_mapping') + op.drop_table('port_mapping') + op.drop_table('net_partitions') + op.drop_table('quotas') + + common_ext_ops.downgrade_l3() diff --git a/neutron/plugins/nuage/__init__.py b/neutron/plugins/nuage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/nuage/common/__init__.py b/neutron/plugins/nuage/common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/nuage/common/config.py b/neutron/plugins/nuage/common/config.py new file mode 100644 index 00000000000..cd5a8a80a3b --- /dev/null +++ b/neutron/plugins/nuage/common/config.py @@ -0,0 +1,47 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + +from oslo.config import cfg + + +restproxy_opts = [ + cfg.StrOpt('server', default='localhost:8800', + help=_("IP Address and Port of Nuage's VSD server")), + cfg.StrOpt('serverauth', default='username:password', + secret=True, + help=_("Username and password for authentication")), + cfg.BoolOpt('serverssl', default=False, + help=_("Boolean for SSL connection with VSD server")), + cfg.StrOpt('base_uri', default='/', + help=_("Nuage provided base uri to reach out to VSD")), + cfg.StrOpt('organization', default='system', + help=_("Organization name in which VSD will orchestrate " + "network resources using openstack")), + cfg.StrOpt('auth_resource', default='', + help=_("Nuage provided uri for initial authorization to " + "access VSD")), + cfg.StrOpt('default_net_partition_name', + default='OpenStackDefaultNetPartition', + help=_("Default Network partition in which VSD will " + "orchestrate network resources using openstack")), + cfg.IntOpt('default_floatingip_quota', + default=254, + help=_("Per Net Partition quota of floating ips")), +] + + +def nuage_register_cfg_opts(): + cfg.CONF.register_opts(restproxy_opts, "RESTPROXY") diff --git a/neutron/plugins/nuage/common/constants.py b/neutron/plugins/nuage/common/constants.py new file mode 100644 index 00000000000..f35481299f2 --- /dev/null +++ b/neutron/plugins/nuage/common/constants.py @@ -0,0 +1,26 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + +from neutron.common import constants + +AUTO_CREATE_PORT_OWNERS = [ + constants.DEVICE_OWNER_DHCP, + constants.DEVICE_OWNER_ROUTER_INTF, + constants.DEVICE_OWNER_ROUTER_GW, + constants.DEVICE_OWNER_FLOATINGIP +] + +NOVA_PORT_OWNER_PREF = 'compute:' diff --git a/neutron/plugins/nuage/common/exceptions.py b/neutron/plugins/nuage/common/exceptions.py new file mode 100644 index 00000000000..2e115889679 --- /dev/null +++ b/neutron/plugins/nuage/common/exceptions.py @@ -0,0 +1,24 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + + +''' Nuage specific exceptions ''' + +from neutron.common import exceptions as n_exc + + +class OperationNotSupported(n_exc.InvalidConfigurationOption): + message = _("Nuage Plugin does not support this operation: %(msg)s") diff --git a/neutron/plugins/nuage/extensions/__init__.py b/neutron/plugins/nuage/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/nuage/extensions/netpartition.py b/neutron/plugins/nuage/extensions/netpartition.py new file mode 100644 index 00000000000..ee228eb4aa7 --- /dev/null +++ b/neutron/plugins/nuage/extensions/netpartition.py @@ -0,0 +1,107 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + +from abc import abstractmethod + +from neutron.api import extensions +from neutron.api.v2 import base +from neutron import manager +from neutron import quota + + +# Attribute Map +RESOURCE_ATTRIBUTE_MAP = { + 'net_partitions': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': False, + 'is_visible': True, 'default': '', + 'validate': {'type:name_not_default': None}}, + 'description': {'allow_post': True, 'allow_put': False, + 'is_visible': True, 'default': '', + 'validate': {'type:string_or_none': None}}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'is_visible': True}, + }, +} + + +class Netpartition(object): + """Extension class supporting net_partition. + """ + + @classmethod + def get_name(cls): + return "NetPartition" + + @classmethod + def get_alias(cls): + return "net-partition" + + @classmethod + def get_description(cls): + return "NetPartition" + + @classmethod + def get_namespace(cls): + return "http://nuagenetworks.net/ext/net_partition/api/v1.0" + + @classmethod + def get_updated(cls): + return "2014-01-01T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + exts = [] + plugin = manager.NeutronManager.get_plugin() + resource_name = 'net_partition' + collection_name = resource_name.replace('_', '-') + "s" + params = RESOURCE_ATTRIBUTE_MAP.get(resource_name + "s", dict()) + quota.QUOTAS.register_resource_by_name(resource_name) + controller = base.create_resource(collection_name, + resource_name, + plugin, params, allow_bulk=True) + ex = extensions.ResourceExtension(collection_name, + controller) + exts.append(ex) + + return exts + + +class NetPartitionPluginBase(object): + + @abstractmethod + def create_net_partition(self, context, router): + pass + + @abstractmethod + def update_net_partition(self, context, id, router): + pass + + @abstractmethod + def get_net_partition(self, context, id, fields=None): + pass + + @abstractmethod + def delete_net_partition(self, context, id): + pass + + @abstractmethod + def get_net_partitions(self, context, filters=None, fields=None): + pass diff --git a/neutron/plugins/nuage/extensions/nuage_router.py b/neutron/plugins/nuage/extensions/nuage_router.py new file mode 100644 index 00000000000..55d4e58d346 --- /dev/null +++ b/neutron/plugins/nuage/extensions/nuage_router.py @@ -0,0 +1,73 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + + +EXTENDED_ATTRIBUTES_2_0 = { + 'routers': { + 'net_partition': { + 'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'default': None, + 'validate': {'type:string_or_none': None} + }, + 'rd': { + 'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'default': None, + 'validate': {'type:string_or_none': None} + }, + 'rt': { + 'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'default': None, + 'validate': {'type:string_or_none': None} + }, + }, +} + + +class Nuage_router(object): + """Extension class supporting nuage router. + """ + + @classmethod + def get_name(cls): + return "Nuage router" + + @classmethod + def get_alias(cls): + return "nuage-router" + + @classmethod + def get_description(cls): + return "Nuage Router" + + @classmethod + def get_namespace(cls): + return "http://nuagenetworks.net/ext/routers/api/v1.0" + + @classmethod + def get_updated(cls): + return "2014-01-01T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/plugins/nuage/extensions/nuage_subnet.py b/neutron/plugins/nuage/extensions/nuage_subnet.py new file mode 100644 index 00000000000..b3705d5f68e --- /dev/null +++ b/neutron/plugins/nuage/extensions/nuage_subnet.py @@ -0,0 +1,59 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + + +EXTENDED_ATTRIBUTES_2_0 = { + 'subnets': { + 'net_partition': { + 'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'default': None, + 'validate': {'type:string_or_none': None} + }, + }, +} + + +class Nuage_subnet(object): + """Extension class supporting Nuage subnet. + """ + + @classmethod + def get_name(cls): + return "Nuage subnet" + + @classmethod + def get_alias(cls): + return "nuage-subnet" + + @classmethod + def get_description(cls): + return "Nuage subnet" + + @classmethod + def get_namespace(cls): + return "http://nuagenetworks.net/ext/subnets/api/v1.0" + + @classmethod + def get_updated(cls): + return "2014-01-01T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/plugins/nuage/nuage_models.py b/neutron/plugins/nuage/nuage_models.py new file mode 100644 index 00000000000..53df04ac841 --- /dev/null +++ b/neutron/plugins/nuage/nuage_models.py @@ -0,0 +1,74 @@ +# Copyright 2014 Alcatel-Lucent USA 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: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + +from sqlalchemy import Boolean, Column, ForeignKey, String + +from neutron.db import model_base +from neutron.db import models_v2 + + +class NetPartition(model_base.BASEV2, models_v2.HasId): + __tablename__ = 'net_partitions' + name = Column(String(64)) + l3dom_tmplt_id = Column(String(36)) + l2dom_tmplt_id = Column(String(36)) + + +class NetPartitionRouter(model_base.BASEV2): + __tablename__ = "net_partition_router_mapping" + net_partition_id = Column(String(36), + ForeignKey('net_partitions.id', + ondelete="CASCADE"), + primary_key=True) + router_id = Column(String(36), + ForeignKey('routers.id', ondelete="CASCADE"), + primary_key=True) + nuage_router_id = Column(String(36)) + + +class RouterZone(model_base.BASEV2): + __tablename__ = "router_zone_mapping" + router_id = Column(String(36), + ForeignKey('routers.id', ondelete="CASCADE"), + primary_key=True) + nuage_zone_id = Column(String(36)) + nuage_user_id = Column(String(36)) + nuage_group_id = Column(String(36)) + + +class SubnetL2Domain(model_base.BASEV2): + __tablename__ = 'subnet_l2dom_mapping' + subnet_id = Column(String(36), + ForeignKey('subnets.id', ondelete="CASCADE"), + primary_key=True) + net_partition_id = Column(String(36), + ForeignKey('net_partitions.id', + ondelete="CASCADE")) + nuage_subnet_id = Column(String(36)) + nuage_l2dom_tmplt_id = Column(String(36)) + nuage_user_id = Column(String(36)) + nuage_group_id = Column(String(36)) + + +class PortVPortMapping(model_base.BASEV2): + __tablename__ = 'port_mapping' + port_id = Column(String(36), + ForeignKey('ports.id', ondelete="CASCADE"), + primary_key=True) + nuage_vport_id = Column(String(36)) + nuage_vif_id = Column(String(36)) + static_ip = Column(Boolean()) diff --git a/neutron/plugins/nuage/nuagedb.py b/neutron/plugins/nuage/nuagedb.py new file mode 100644 index 00000000000..fedd13d75a6 --- /dev/null +++ b/neutron/plugins/nuage/nuagedb.py @@ -0,0 +1,133 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + +from neutron.db import db_base_plugin_v2 +from neutron.plugins.nuage import nuage_models + + +def add_entrouter_mapping(session, np_id, + router_id, + n_l3id): + ent_rtr_mapping = nuage_models.NetPartitionRouter(net_partition_id=np_id, + router_id=router_id, + nuage_router_id=n_l3id) + session.add(ent_rtr_mapping) + + +def add_rtrzone_mapping(session, neutron_router_id, + nuage_zone_id, + nuage_user_id=None, + nuage_group_id=None): + rtr_zone_mapping = nuage_models.RouterZone(router_id=neutron_router_id, + nuage_zone_id=nuage_zone_id, + nuage_user_id=nuage_user_id, + nuage_group_id=nuage_group_id) + session.add(rtr_zone_mapping) + + +def add_subnetl2dom_mapping(session, neutron_subnet_id, + nuage_sub_id, + np_id, + l2dom_id=None, + nuage_user_id=None, + nuage_group_id=None): + subnet_l2dom = nuage_models.SubnetL2Domain(subnet_id=neutron_subnet_id, + nuage_subnet_id=nuage_sub_id, + net_partition_id=np_id, + nuage_l2dom_tmplt_id=l2dom_id, + nuage_user_id=nuage_user_id, + nuage_group_id=nuage_group_id) + session.add(subnet_l2dom) + + +def update_subnetl2dom_mapping(subnet_l2dom, + new_dict): + subnet_l2dom.update(new_dict) + + +def add_port_vport_mapping(session, port_id, nuage_vport_id, + nuage_vif_id, static_ip): + port_mapping = nuage_models.PortVPortMapping(port_id=port_id, + nuage_vport_id=nuage_vport_id, + nuage_vif_id=nuage_vif_id, + static_ip=static_ip) + session.add(port_mapping) + return port_mapping + + +def update_port_vport_mapping(port_mapping, + new_dict): + port_mapping.update(new_dict) + + +def get_port_mapping_by_id(session, id): + query = session.query(nuage_models.PortVPortMapping) + return query.filter_by(port_id=id).first() + + +def get_ent_rtr_mapping_by_rtrid(session, rtrid): + query = session.query(nuage_models.NetPartitionRouter) + return query.filter_by(router_id=rtrid).first() + + +def get_rtr_zone_mapping(session, router_id): + query = session.query(nuage_models.RouterZone) + return query.filter_by(router_id=router_id).first() + + +def get_subnet_l2dom_by_id(session, id): + query = session.query(nuage_models.SubnetL2Domain) + return query.filter_by(subnet_id=id).first() + + +def add_net_partition(session, netpart_id, + l3dom_id, l2dom_id, + ent_name): + net_partitioninst = nuage_models.NetPartition(id=netpart_id, + name=ent_name, + l3dom_tmplt_id=l3dom_id, + l2dom_tmplt_id=l2dom_id) + session.add(net_partitioninst) + return net_partitioninst + + +def delete_net_partition(session, net_partition): + session.delete(net_partition) + + +def get_ent_rtr_mapping_by_entid(session, + entid): + query = session.query(nuage_models.NetPartitionRouter) + return query.filter_by(net_partition_id=entid).all() + + +def get_net_partition_by_name(session, name): + query = session.query(nuage_models.NetPartition) + return query.filter_by(name=name).first() + + +def get_net_partition_by_id(session, id): + query = session.query(nuage_models.NetPartition) + return query.filter_by(id=id).first() + + +def get_net_partitions(session, filters=None, fields=None): + query = session.query(nuage_models.NetPartition) + common_db = db_base_plugin_v2.CommonDbMixin() + query = common_db._apply_filters_to_query(query, + nuage_models.NetPartition, + filters) + return query diff --git a/neutron/plugins/nuage/plugin.py b/neutron/plugins/nuage/plugin.py new file mode 100644 index 00000000000..df04f9ba432 --- /dev/null +++ b/neutron/plugins/nuage/plugin.py @@ -0,0 +1,697 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + + +import re + +import netaddr +from oslo.config import cfg +from sqlalchemy.orm import exc + +from neutron.api import extensions as neutron_extensions +from neutron.api.v2 import attributes +from neutron.common import constants as os_constants +from neutron.common import exceptions as q_exc +from neutron.db import api as db +from neutron.db import db_base_plugin_v2 +from neutron.db import external_net_db +from neutron.db import l3_db +from neutron.db import models_v2 +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.plugins.nuage.common import config +from neutron.plugins.nuage.common import constants +from neutron.plugins.nuage.common import exceptions as nuage_exc +from neutron.plugins.nuage import extensions +from neutron.plugins.nuage.extensions import netpartition +from neutron.plugins.nuage import nuagedb +from neutron import policy + + +class NuagePlugin(db_base_plugin_v2.NeutronDbPluginV2, + external_net_db.External_net_db_mixin, + l3_db.L3_NAT_db_mixin, + netpartition.NetPartitionPluginBase): + """Class that implements Nuage Networks' plugin functionality.""" + supported_extension_aliases = ["router", "binding", "external-net", + "net-partition", "nuage-router", + "nuage-subnet", "quotas"] + + binding_view = "extension:port_binding:view" + + def __init__(self): + super(NuagePlugin, self).__init__() + neutron_extensions.append_api_extensions_path(extensions.__path__) + config.nuage_register_cfg_opts() + self.nuageclient_init() + net_partition = cfg.CONF.RESTPROXY.default_net_partition_name + self._create_default_net_partition(net_partition) + + def nuageclient_init(self): + server = cfg.CONF.RESTPROXY.server + serverauth = cfg.CONF.RESTPROXY.serverauth + serverssl = cfg.CONF.RESTPROXY.serverssl + base_uri = cfg.CONF.RESTPROXY.base_uri + auth_resource = cfg.CONF.RESTPROXY.auth_resource + organization = cfg.CONF.RESTPROXY.organization + nuageclient = importutils.import_module('nuagenetlib.nuageclient') + self.nuageclient = nuageclient.NuageClient(server, base_uri, + serverssl, serverauth, + auth_resource, + organization) + + def _resource_finder(self, context, for_resource, resource, user_req): + match = re.match(attributes.UUID_PATTERN, user_req[resource]) + if match: + obj_lister = getattr(self, "get_%s" % resource) + found_resource = obj_lister(context, user_req[resource]) + if not found_resource: + msg = (_("%(resource)s with id %(resource_id)s does not " + "exist") % {'resource': resource, + 'resource_id': user_req[resource]}) + raise q_exc.BadRequest(resource=for_resource, msg=msg) + else: + filter = {'name': [user_req[resource]]} + obj_lister = getattr(self, "get_%ss" % resource) + found_resource = obj_lister(context, filters=filter) + if not found_resource: + msg = (_("Either %(resource)s %(req_resource)s not found " + "or you dont have credential to access it") + % {'resource': resource, + 'req_resource': user_req[resource]}) + raise q_exc.BadRequest(resource=for_resource, msg=msg) + if len(found_resource) > 1: + msg = (_("More than one entry found for %(resource)s " + "%(req_resource)s. Use id instead") + % {'resource': resource, + 'req_resource': user_req[resource]}) + raise q_exc.BadRequest(resource=for_resource, msg=msg) + found_resource = found_resource[0] + return found_resource + + def _update_port_ip(self, context, port, new_ip): + subid = port['fixed_ips'][0]['subnet_id'] + new_fixed_ips = {} + new_fixed_ips['subnet_id'] = subid + new_fixed_ips['ip_address'] = new_ip + ips, prev_ips = self._update_ips_for_port(context, + port["network_id"], + port['id'], + port["fixed_ips"], + [new_fixed_ips]) + + # Update ips if necessary + for ip in ips: + allocated = models_v2.IPAllocation( + network_id=port['network_id'], port_id=port['id'], + ip_address=ip['ip_address'], subnet_id=ip['subnet_id']) + context.session.add(allocated) + + def _create_update_port(self, context, port, + port_mapping, subnet_mapping): + filters = {'device_id': [port['device_id']]} + ports = self.get_ports(context, filters) + netpart_id = subnet_mapping['net_partition_id'] + net_partition = nuagedb.get_net_partition_by_id(context.session, + netpart_id) + params = { + 'id': port['device_id'], + 'mac': port['mac_address'], + 'parent_id': subnet_mapping['nuage_subnet_id'], + 'net_partition': net_partition, + 'ip': None, + 'no_of_ports': len(ports), + 'tenant': port['tenant_id'] + } + if port_mapping['static_ip']: + params['ip'] = port['fixed_ips'][0]['ip_address'] + + nuage_vm = self.nuageclient.create_vms(params) + if nuage_vm: + if port['fixed_ips'][0]['ip_address'] != str(nuage_vm['ip']): + self._update_port_ip(context, port, nuage_vm['ip']) + port_dict = { + 'nuage_vport_id': nuage_vm['vport_id'], + 'nuage_vif_id': nuage_vm['vif_id'] + } + nuagedb.update_port_vport_mapping(port_mapping, + port_dict) + + def create_port(self, context, port): + session = context.session + with session.begin(subtransactions=True): + p = port['port'] + port = super(NuagePlugin, self).create_port(context, port) + device_owner = port.get('device_owner', None) + if (device_owner and + device_owner not in constants.AUTO_CREATE_PORT_OWNERS): + if 'fixed_ips' not in port or len(port['fixed_ips']) == 0: + return self._extend_port_dict_binding(context, port) + subnet_id = port['fixed_ips'][0]['subnet_id'] + subnet_mapping = nuagedb.get_subnet_l2dom_by_id(session, + subnet_id) + if subnet_mapping: + static_ip = False + if (attributes.is_attr_set(p['fixed_ips']) and + 'ip_address' in p['fixed_ips'][0]): + static_ip = True + nuage_vport_id = None + nuage_vif_id = None + port_mapping = nuagedb.add_port_vport_mapping( + session, + port['id'], + nuage_vport_id, + nuage_vif_id, + static_ip) + port_prefix = constants.NOVA_PORT_OWNER_PREF + if port['device_owner'].startswith(port_prefix): + #This request is coming from nova + try: + self._create_update_port(context, port, + port_mapping, + subnet_mapping) + except Exception: + with excutils.save_and_reraise_exception(): + super(NuagePlugin, self).delete_port( + context, + port['id']) + return self._extend_port_dict_binding(context, port) + + def update_port(self, context, id, port): + p = port['port'] + if p.get('device_owner', '').startswith( + constants.NOVA_PORT_OWNER_PREF): + session = context.session + with session.begin(subtransactions=True): + port = self._get_port(context, id) + port.update(p) + if 'fixed_ips' not in port or len(port['fixed_ips']) == 0: + return self._make_port_dict(port) + subnet_id = port['fixed_ips'][0]['subnet_id'] + subnet_mapping = nuagedb.get_subnet_l2dom_by_id(session, + subnet_id) + if not subnet_mapping: + msg = (_("Subnet %s not found on VSD") % subnet_id) + raise q_exc.BadRequest(resource='port', msg=msg) + port_mapping = nuagedb.get_port_mapping_by_id(session, + id) + if not port_mapping: + msg = (_("Port-Mapping for port %s not " + " found on VSD") % id) + raise q_exc.BadRequest(resource='port', msg=msg) + if not port_mapping['nuage_vport_id']: + self._create_update_port(context, port, + port_mapping, subnet_mapping) + updated_port = self._make_port_dict(port) + else: + updated_port = super(NuagePlugin, self).update_port(context, id, + port) + return updated_port + + def delete_port(self, context, id, l3_port_check=True): + if l3_port_check: + self.prevent_l3_port_deletion(context, id) + port = self._get_port(context, id) + port_mapping = nuagedb.get_port_mapping_by_id(context.session, + id) + # This is required for to pass ut test_floatingip_port_delete + self.disassociate_floatingips(context, id) + if not port['fixed_ips']: + return super(NuagePlugin, self).delete_port(context, id) + + sub_id = port['fixed_ips'][0]['subnet_id'] + subnet_mapping = nuagedb.get_subnet_l2dom_by_id(context.session, + sub_id) + if not subnet_mapping: + return super(NuagePlugin, self).delete_port(context, id) + + netpart_id = subnet_mapping['net_partition_id'] + net_partition = nuagedb.get_net_partition_by_id(context.session, + netpart_id) + # Need to call this explicitly to delete vport_vporttag_mapping + if constants.NOVA_PORT_OWNER_PREF in port['device_owner']: + # This was a VM Port + filters = {'device_id': [port['device_id']]} + ports = self.get_ports(context, filters) + params = { + 'no_of_ports': len(ports), + 'net_partition': net_partition, + 'tenant': port['tenant_id'], + 'mac': port['mac_address'], + 'nuage_vif_id': port_mapping['nuage_vif_id'], + 'id': port['device_id'] + } + self.nuageclient.delete_vms(params) + super(NuagePlugin, self).delete_port(context, id) + + def _check_view_auth(self, context, resource, action): + return policy.check(context, action, resource) + + def _extend_port_dict_binding(self, context, port): + if self._check_view_auth(context, port, self.binding_view): + port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_OVS + port[portbindings.VIF_DETAILS] = { + portbindings.CAP_PORT_FILTER: False + } + return port + + def get_port(self, context, id, fields=None): + port = super(NuagePlugin, self).get_port(context, id, fields) + return self._fields(self._extend_port_dict_binding(context, port), + fields) + + def get_ports(self, context, filters=None, fields=None): + ports = super(NuagePlugin, self).get_ports(context, filters, fields) + return [self._fields(self._extend_port_dict_binding(context, port), + fields) for port in ports] + + def _check_router_subnet_for_tenant(self, context): + # Search router and subnet tables. + # If no entry left delete user and group from VSD + filters = {'tenant_id': [context.tenant]} + routers = self.get_routers(context, filters=filters) + subnets = self.get_subnets(context, filters=filters) + return bool(routers or subnets) + + def create_network(self, context, network): + net = network['network'] + with context.session.begin(subtransactions=True): + net = super(NuagePlugin, self).create_network(context, + network) + self._process_l3_create(context, net, network['network']) + return net + + def update_network(self, context, id, network): + with context.session.begin(subtransactions=True): + net = super(NuagePlugin, self).update_network(context, id, + network) + self._process_l3_update(context, net, network['network']) + return net + + def delete_network(self, context, id): + filter = {'network_id': [id]} + subnets = self.get_subnets(context, filters=filter) + for subnet in subnets: + self.delete_subnet(context, subnet['id']) + super(NuagePlugin, self).delete_network(context, id) + + def _get_net_partition_for_subnet(self, context, subnet): + subn = subnet['subnet'] + ent = subn.get('net_partition', None) + if not ent: + def_net_part = cfg.CONF.RESTPROXY.default_net_partition_name + net_partition = nuagedb.get_net_partition_by_name(context.session, + def_net_part) + else: + net_partition = self._resource_finder(context, 'subnet', + 'net_partition', subn) + if not net_partition: + msg = _('Either net_partition is not provided with subnet OR ' + 'default net_partition is not created at the start') + raise q_exc.BadRequest(resource='subnet', msg=msg) + return net_partition + + def _validate_create_subnet(self, subnet): + if ('host_routes' in subnet and + attributes.is_attr_set(subnet['host_routes'])): + msg = 'host_routes extensions not supported for subnets' + raise nuage_exc.OperationNotSupported(msg=msg) + if subnet['gateway_ip'] is None: + msg = "no-gateway option not supported with subnets" + raise nuage_exc.OperationNotSupported(msg=msg) + + def create_subnet(self, context, subnet): + subn = subnet['subnet'] + net_id = subn['network_id'] + + if self._network_is_external(context, net_id): + return super(NuagePlugin, self).create_subnet(context, subnet) + + self._validate_create_subnet(subn) + + net_partition = self._get_net_partition_for_subnet(context, subnet) + neutron_subnet = super(NuagePlugin, self).create_subnet(context, + subnet) + net = netaddr.IPNetwork(neutron_subnet['cidr']) + params = { + 'net_partition': net_partition, + 'tenant_id': neutron_subnet['tenant_id'], + 'net': net + } + try: + nuage_subnet = self.nuageclient.create_subnet(neutron_subnet, + params) + except Exception: + with excutils.save_and_reraise_exception(): + super(NuagePlugin, self).delete_subnet(context, + neutron_subnet['id']) + + if nuage_subnet: + l2dom_id = str(nuage_subnet['nuage_l2template_id']) + user_id = nuage_subnet['nuage_userid'] + group_id = nuage_subnet['nuage_groupid'] + id = nuage_subnet['nuage_l2domain_id'] + session = context.session + with session.begin(subtransactions=True): + nuagedb.add_subnetl2dom_mapping(session, + neutron_subnet['id'], + id, + net_partition['id'], + l2dom_id=l2dom_id, + nuage_user_id=user_id, + nuage_group_id=group_id) + return neutron_subnet + + def delete_subnet(self, context, id): + subnet = self.get_subnet(context, id) + if self._network_is_external(context, subnet['network_id']): + return super(NuagePlugin, self).delete_subnet(context, id) + + subnet_l2dom = nuagedb.get_subnet_l2dom_by_id(context.session, id) + if subnet_l2dom: + template_id = subnet_l2dom['nuage_l2dom_tmplt_id'] + try: + self.nuageclient.delete_subnet(subnet_l2dom['nuage_subnet_id'], + template_id) + except Exception: + msg = (_('Unable to complete operation on subnet %s.' + 'One or more ports have an IP allocation ' + 'from this subnet.') % id) + raise q_exc.BadRequest(resource='subnet', msg=msg) + super(NuagePlugin, self).delete_subnet(context, id) + if subnet_l2dom and not self._check_router_subnet_for_tenant(context): + self.nuageclient.delete_user(subnet_l2dom['nuage_user_id']) + self.nuageclient.delete_group(subnet_l2dom['nuage_group_id']) + + def add_router_interface(self, context, router_id, interface_info): + session = context.session + with session.begin(subtransactions=True): + rtr_if_info = super(NuagePlugin, + self).add_router_interface(context, + router_id, + interface_info) + subnet_id = rtr_if_info['subnet_id'] + subn = self.get_subnet(context, subnet_id) + + rtr_zone_mapping = nuagedb.get_rtr_zone_mapping(session, + router_id) + ent_rtr_mapping = nuagedb.get_ent_rtr_mapping_by_rtrid(session, + router_id) + subnet_l2dom = nuagedb.get_subnet_l2dom_by_id(session, + subnet_id) + if not rtr_zone_mapping or not ent_rtr_mapping: + super(NuagePlugin, + self).remove_router_interface(context, + router_id, + interface_info) + msg = (_("Router %s does not hold default zone OR " + "net_partition mapping. Router-IF add failed") + % router_id) + raise q_exc.BadRequest(resource='router', msg=msg) + + if not subnet_l2dom: + super(NuagePlugin, + self).remove_router_interface(context, + router_id, + interface_info) + msg = (_("Subnet %s does not hold Nuage VSD reference. " + "Router-IF add failed") % subnet_id) + raise q_exc.BadRequest(resource='subnet', msg=msg) + + if (subnet_l2dom['net_partition_id'] != + ent_rtr_mapping['net_partition_id']): + super(NuagePlugin, + self).remove_router_interface(context, + router_id, + interface_info) + msg = (_("Subnet %(subnet)s and Router %(router)s belong to " + "different net_partition Router-IF add " + "not permitted") % {'subnet': subnet_id, + 'router': router_id}) + raise q_exc.BadRequest(resource='subnet', msg=msg) + nuage_subnet_id = subnet_l2dom['nuage_subnet_id'] + nuage_l2dom_tmplt_id = subnet_l2dom['nuage_l2dom_tmplt_id'] + if self.nuageclient.vms_on_l2domain(nuage_subnet_id): + super(NuagePlugin, + self).remove_router_interface(context, + router_id, + interface_info) + msg = (_("Subnet %s has one or more active VMs " + "Router-IF add not permitted") % subnet_id) + raise q_exc.BadRequest(resource='subnet', msg=msg) + self.nuageclient.delete_subnet(nuage_subnet_id, + nuage_l2dom_tmplt_id) + net = netaddr.IPNetwork(subn['cidr']) + params = { + 'net': net, + 'zone_id': rtr_zone_mapping['nuage_zone_id'] + } + if not attributes.is_attr_set(subn['gateway_ip']): + subn['gateway_ip'] = str(netaddr.IPAddress(net.first + 1)) + try: + nuage_subnet = self.nuageclient.create_domain_subnet(subn, + params) + except Exception: + with excutils.save_and_reraise_exception(): + super(NuagePlugin, + self).remove_router_interface(context, + router_id, + interface_info) + if nuage_subnet: + ns_dict = {} + ns_dict['nuage_subnet_id'] = nuage_subnet['nuage_subnetid'] + ns_dict['nuage_l2dom_tmplt_id'] = None + nuagedb.update_subnetl2dom_mapping(subnet_l2dom, + ns_dict) + return rtr_if_info + + def remove_router_interface(self, context, router_id, interface_info): + if 'subnet_id' in interface_info: + subnet_id = interface_info['subnet_id'] + subnet = self.get_subnet(context, subnet_id) + found = False + try: + filters = {'device_id': [router_id], + 'device_owner': + [os_constants.DEVICE_OWNER_ROUTER_INTF], + 'network_id': [subnet['network_id']]} + ports = self.get_ports(context, filters) + + for p in ports: + if p['fixed_ips'][0]['subnet_id'] == subnet_id: + found = True + break + except exc.NoResultFound: + msg = (_("No router interface found for Router %s. " + "Router-IF delete failed") % router_id) + raise q_exc.BadRequest(resource='router', msg=msg) + + if not found: + msg = (_("No router interface found for Router %s. " + "Router-IF delete failed") % router_id) + raise q_exc.BadRequest(resource='router', msg=msg) + elif 'port_id' in interface_info: + port_db = self._get_port(context, interface_info['port_id']) + if not port_db: + msg = (_("No router interface found for Router %s. " + "Router-IF delete failed") % router_id) + raise q_exc.BadRequest(resource='router', msg=msg) + subnet_id = port_db['fixed_ips'][0]['subnet_id'] + + session = context.session + with session.begin(subtransactions=True): + subnet_l2dom = nuagedb.get_subnet_l2dom_by_id(session, + subnet_id) + nuage_subn_id = subnet_l2dom['nuage_subnet_id'] + if self.nuageclient.vms_on_l2domain(nuage_subn_id): + msg = (_("Subnet %s has one or more active VMs " + "Router-IF delete not permitted") % subnet_id) + raise q_exc.BadRequest(resource='subnet', msg=msg) + + neutron_subnet = self.get_subnet(context, subnet_id) + ent_rtr_mapping = nuagedb.get_ent_rtr_mapping_by_rtrid( + context.session, + router_id) + if not ent_rtr_mapping: + msg = (_("Router %s does not hold net_partition " + "assoc on Nuage VSD. Router-IF delete failed") + % router_id) + raise q_exc.BadRequest(resource='router', msg=msg) + net = netaddr.IPNetwork(neutron_subnet['cidr']) + net_part_id = ent_rtr_mapping['net_partition_id'] + net_partition = self.get_net_partition(context, + net_part_id) + params = { + 'net_partition': net_partition, + 'tenant_id': neutron_subnet['tenant_id'], + 'net': net + } + nuage_subnet = self.nuageclient.create_subnet(neutron_subnet, + params) + self.nuageclient.delete_domain_subnet(nuage_subn_id) + info = super(NuagePlugin, + self).remove_router_interface(context, router_id, + interface_info) + if nuage_subnet: + tmplt_id = str(nuage_subnet['nuage_l2template_id']) + ns_dict = {} + ns_dict['nuage_subnet_id'] = nuage_subnet['nuage_l2domain_id'] + ns_dict['nuage_l2dom_tmplt_id'] = tmplt_id + nuagedb.update_subnetl2dom_mapping(subnet_l2dom, + ns_dict) + return info + + def _get_net_partition_for_router(self, context, router): + rtr = router['router'] + ent = rtr.get('net_partition', None) + if not ent: + def_net_part = cfg.CONF.RESTPROXY.default_net_partition_name + net_partition = nuagedb.get_net_partition_by_name(context.session, + def_net_part) + else: + net_partition = self._resource_finder(context, 'router', + 'net_partition', rtr) + if not net_partition: + msg = _("Either net_partition is not provided with router OR " + "default net_partition is not created at the start") + raise q_exc.BadRequest(resource='router', msg=msg) + return net_partition + + def create_router(self, context, router): + net_partition = self._get_net_partition_for_router(context, router) + neutron_router = super(NuagePlugin, self).create_router(context, + router) + params = { + 'net_partition': net_partition, + 'tenant_id': neutron_router['tenant_id'] + } + try: + nuage_router = self.nuageclient.create_router(neutron_router, + router['router'], + params) + except Exception: + with excutils.save_and_reraise_exception(): + super(NuagePlugin, self).delete_router(context, + neutron_router['id']) + if nuage_router: + user_id = nuage_router['nuage_userid'] + group_id = nuage_router['nuage_groupid'] + with context.session.begin(subtransactions=True): + nuagedb.add_entrouter_mapping(context.session, + net_partition['id'], + neutron_router['id'], + nuage_router['nuage_domain_id']) + nuagedb.add_rtrzone_mapping(context.session, + neutron_router['id'], + nuage_router['nuage_def_zone_id'], + nuage_user_id=user_id, + nuage_group_id=group_id) + return neutron_router + + def delete_router(self, context, id): + session = context.session + ent_rtr_mapping = nuagedb.get_ent_rtr_mapping_by_rtrid(session, + id) + if ent_rtr_mapping: + nuage_router_id = ent_rtr_mapping['nuage_router_id'] + self.nuageclient.delete_router(nuage_router_id) + router_zone = nuagedb.get_rtr_zone_mapping(session, id) + super(NuagePlugin, self).delete_router(context, id) + if router_zone and not self._check_router_subnet_for_tenant(context): + self.nuageclient.delete_user(router_zone['nuage_user_id']) + self.nuageclient.delete_group(router_zone['nuage_group_id']) + + def _make_net_partition_dict(self, net_partition, fields=None): + res = { + 'id': net_partition['id'], + 'name': net_partition['name'], + 'l3dom_tmplt_id': net_partition['l3dom_tmplt_id'], + 'l2dom_tmplt_id': net_partition['l2dom_tmplt_id'], + } + return self._fields(res, fields) + + def _create_net_partition(self, session, net_part_name): + fip_quota = cfg.CONF.RESTPROXY.default_floatingip_quota + params = { + "name": net_part_name, + "fp_quota": str(fip_quota) + } + nuage_net_partition = self.nuageclient.create_net_partition(params) + net_partitioninst = None + if nuage_net_partition: + nuage_entid = nuage_net_partition['nuage_entid'] + l3dom_id = nuage_net_partition['l3dom_id'] + l2dom_id = nuage_net_partition['l2dom_id'] + with session.begin(): + net_partitioninst = nuagedb.add_net_partition(session, + nuage_entid, + l3dom_id, + l2dom_id, + net_part_name) + if not net_partitioninst: + return {} + return self._make_net_partition_dict(net_partitioninst) + + def _create_default_net_partition(self, default_net_part): + self.nuageclient.check_del_def_net_partition(default_net_part) + session = db.get_session() + net_partition = nuagedb.get_net_partition_by_name(session, + default_net_part) + if net_partition: + with session.begin(subtransactions=True): + nuagedb.delete_net_partition(session, net_partition) + + self._create_net_partition(session, default_net_part) + + def create_net_partition(self, context, net_partition): + ent = net_partition['net_partition'] + session = context.session + return self._create_net_partition(session, ent["name"]) + + def delete_net_partition(self, context, id): + ent_rtr_mapping = nuagedb.get_ent_rtr_mapping_by_entid( + context.session, + id) + if ent_rtr_mapping: + msg = (_("One or more router still attached to " + "net_partition %s.") % id) + raise q_exc.BadRequest(resource='net_partition', msg=msg) + net_partition = nuagedb.get_net_partition_by_id(context.session, id) + if not net_partition: + msg = (_("NetPartition with %s does not exist") % id) + raise q_exc.BadRequest(resource='net_partition', msg=msg) + l3dom_tmplt_id = net_partition['l3dom_tmplt_id'] + l2dom_tmplt_id = net_partition['l2dom_tmplt_id'] + self.nuageclient.delete_net_partition(net_partition['id'], + l3dom_id=l3dom_tmplt_id, + l2dom_id=l2dom_tmplt_id) + with context.session.begin(subtransactions=True): + nuagedb.delete_net_partition(context.session, + net_partition) + + def get_net_partition(self, context, id, fields=None): + net_partition = nuagedb.get_net_partition_by_id(context.session, + id) + return self._make_net_partition_dict(net_partition) + + def get_net_partitions(self, context, filters=None, fields=None): + net_partitions = nuagedb.get_net_partitions(context.session, + filters=filters, + fields=fields) + return [self._make_net_partition_dict(net_partition, fields) + for net_partition in net_partitions] diff --git a/neutron/tests/unit/nuage/__init__.py b/neutron/tests/unit/nuage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/nuage/fake_nuageclient.py b/neutron/tests/unit/nuage/fake_nuageclient.py new file mode 100644 index 00000000000..47c9dc300e7 --- /dev/null +++ b/neutron/tests/unit/nuage/fake_nuageclient.py @@ -0,0 +1,85 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Aniket Dandekar, Nuage Networks, Alcatel-Lucent USA Inc. + +import uuid + + +class FakeNuageClient(object): + def __init__(self, server, base_uri, serverssl, + serverauth, auth_resource, organization): + pass + + def rest_call(self, action, resource, data, extra_headers=None): + pass + + def vms_on_l2domain(self, l2dom_id): + pass + + def create_subnet(self, neutron_subnet, params): + nuage_subnet = { + 'nuage_l2template_id': str(uuid.uuid4()), + 'nuage_userid': str(uuid.uuid4()), + 'nuage_groupid': str(uuid.uuid4()), + 'nuage_l2domain_id': str(uuid.uuid4()) + } + return nuage_subnet + + def delete_subnet(self, id, template_id): + pass + + def create_router(self, neutron_router, router, params): + nuage_router = { + 'nuage_userid': str(uuid.uuid4()), + 'nuage_groupid': str(uuid.uuid4()), + 'nuage_domain_id': str(uuid.uuid4()), + 'nuage_def_zone_id': str(uuid.uuid4()), + } + return nuage_router + + def delete_router(self, id): + pass + + def delete_user(self, id): + pass + + def delete_group(self, id): + pass + + def create_domain_subnet(self, neutron_subnet, params): + pass + + def delete_domain_subnet(self, id): + pass + + def create_net_partition(self, params): + fake_net_partition = { + 'nuage_entid': str(uuid.uuid4()), + 'l3dom_id': str(uuid.uuid4()), + 'l2dom_id': str(uuid.uuid4()), + } + return fake_net_partition + + def delete_net_partition(self, id, l3dom_id=None, l2dom_id=None): + pass + + def check_del_def_net_partition(self, ent_name): + pass + + def create_vms(self, params): + pass + + def delete_vms(self, params): + pass diff --git a/neutron/tests/unit/nuage/test_netpartition.py b/neutron/tests/unit/nuage/test_netpartition.py new file mode 100644 index 00000000000..da4aa38f738 --- /dev/null +++ b/neutron/tests/unit/nuage/test_netpartition.py @@ -0,0 +1,93 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Nuage Networks, Alcatel-Lucent USA Inc. + +import contextlib +import uuid +import webob.exc + +from neutron.plugins.nuage.extensions import netpartition as netpart_ext +from neutron.tests.unit.nuage import test_nuage_plugin +from neutron.tests.unit import test_extensions + + +class NetPartitionTestExtensionManager(object): + def get_resources(self): + return netpart_ext.Netpartition.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class NetPartitionTestCase(test_nuage_plugin.NuagePluginV2TestCase): + def setUp(self): + ext_mgr = NetPartitionTestExtensionManager() + super(NetPartitionTestCase, self).setUp() + self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) + + def _make_netpartition(self, fmt, name): + data = { + 'net_partition': { + 'name': name, + 'tenant_id': uuid.uuid4() + } + } + netpart_req = self.new_create_request('net-partitions', data, fmt) + resp = netpart_req.get_response(self.ext_api) + if resp.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=resp.status_int) + return self.deserialize(fmt, resp) + + def _del_netpartition(self, id): + self._delete('net-partitions', id) + + @contextlib.contextmanager + def netpartition(self, name='netpartition1', + do_delete=True, + fmt=None, + **kwargs): + netpart = self._make_netpartition(fmt or self.fmt, name) + + try: + yield netpart + finally: + if do_delete: + self._del_netpartition(netpart['net_partition']['id']) + + def test_create_netpartition(self): + name = 'netpart1' + keys = [('name', name)] + with self.netpartition(name=name) as netpart: + for k, v in keys: + self.assertEqual(netpart['net_partition'][k], v) + + def test_delete_netpartition(self): + name = 'netpart1' + netpart = self._make_netpartition(self.fmt, name) + req = self.new_delete_request('net-partitions', + netpart['net_partition']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code) + + def test_show_netpartition(self): + with self.netpartition(name='netpart1') as npart: + req = self.new_show_request('net-partitions', + npart['net_partition']['id']) + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + self.assertEqual(res['net_partition']['name'], + npart['net_partition']['name']) diff --git a/neutron/tests/unit/nuage/test_nuage_plugin.py b/neutron/tests/unit/nuage/test_nuage_plugin.py new file mode 100644 index 00000000000..259a3637d1d --- /dev/null +++ b/neutron/tests/unit/nuage/test_nuage_plugin.py @@ -0,0 +1,162 @@ +# Copyright 2014 Alcatel-Lucent USA Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Ronak Shah, Aniket Dandekar, Nuage Networks, Alcatel-Lucent USA Inc. + +import os + +import mock +from oslo.config import cfg + +from neutron.extensions import portbindings +from neutron.plugins.nuage import extensions +from neutron.plugins.nuage import plugin as nuage_plugin +from neutron.tests.unit import _test_extension_portbindings as test_bindings +from neutron.tests.unit.nuage import fake_nuageclient +from neutron.tests.unit import test_db_plugin +from neutron.tests.unit import test_l3_plugin + +API_EXT_PATH = os.path.dirname(extensions.__file__) +FAKE_DEFAULT_ENT = 'default' +NUAGE_PLUGIN_PATH = 'neutron.plugins.nuage.plugin' +FAKE_SERVER = '1.1.1.1' +FAKE_SERVER_AUTH = 'user:pass' +FAKE_SERVER_SSL = False +FAKE_BASE_URI = '/base/' +FAKE_AUTH_RESOURCE = '/auth' +FAKE_ORGANIZATION = 'fake_org' + +_plugin_name = ('%s.NuagePlugin' % NUAGE_PLUGIN_PATH) + + +class NuagePluginV2TestCase(test_db_plugin.NeutronDbPluginV2TestCase): + def setUp(self, plugin=_plugin_name, + ext_mgr=None, service_plugins=None): + def mock_nuageClient_init(self): + server = FAKE_SERVER + serverauth = FAKE_SERVER_AUTH + serverssl = FAKE_SERVER_SSL + base_uri = FAKE_BASE_URI + auth_resource = FAKE_AUTH_RESOURCE + organization = FAKE_ORGANIZATION + self.nuageclient = None + self.nuageclient = fake_nuageclient.FakeNuageClient(server, + base_uri, + serverssl, + serverauth, + auth_resource, + organization) + + with mock.patch.object(nuage_plugin.NuagePlugin, + 'nuageclient_init', new=mock_nuageClient_init): + cfg.CONF.set_override('api_extensions_path', + API_EXT_PATH) + super(NuagePluginV2TestCase, self).setUp(plugin=plugin, + ext_mgr=ext_mgr) + + +class TestNuageBasicGet(NuagePluginV2TestCase, + test_db_plugin.TestBasicGet): + pass + + +class TestNuageV2HTTPResponse(NuagePluginV2TestCase, + test_db_plugin.TestV2HTTPResponse): + pass + + +class TestNuageNetworksV2(NuagePluginV2TestCase, + test_db_plugin.TestNetworksV2): + pass + + +class TestNuageSubnetsV2(NuagePluginV2TestCase, + test_db_plugin.TestSubnetsV2): + def test_create_subnet_bad_hostroutes(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_create_subnet_inconsistent_ipv4_hostroute_dst_v6(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_create_subnet_inconsistent_ipv4_hostroute_np_v6(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_update_subnet_adding_additional_host_routes_and_dns(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_update_subnet_inconsistent_ipv6_hostroute_dst_v4(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_update_subnet_inconsistent_ipv6_hostroute_np_v4(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_create_subnet_with_one_host_route(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_create_subnet_with_two_host_routes(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_create_subnet_with_too_many_routes(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_update_subnet_route(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_update_subnet_route_to_None(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_update_subnet_route_with_too_many_entries(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_delete_subnet_with_route(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_delete_subnet_with_dns_and_route(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_validate_subnet_host_routes_exhausted(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_validate_subnet_dns_nameservers_exhausted(self): + self.skipTest("Plugin does not support Neutron Subnet host-routes") + + def test_create_subnet_with_none_gateway(self): + self.skipTest("Plugin does not support " + "Neutron Subnet no-gateway option") + + def test_create_subnet_with_none_gateway_fully_allocated(self): + self.skipTest("Plugin does not support Neutron " + "Subnet no-gateway option") + + def test_create_subnet_with_none_gateway_allocation_pool(self): + self.skipTest("Plugin does not support Neutron " + "Subnet no-gateway option") + + +class TestNuagePluginPortBinding(NuagePluginV2TestCase, + test_bindings.PortBindingsTestCase): + VIF_TYPE = portbindings.VIF_TYPE_OVS + + def setUp(self): + super(TestNuagePluginPortBinding, self).setUp() + + +class TestNuagePortsV2(NuagePluginV2TestCase, + test_db_plugin.TestPortsV2): + pass + + +class TestNuageL3NatTestCase(NuagePluginV2TestCase, + test_l3_plugin.L3NatDBIntTestCase): + pass