Merge "NSX|v3: provider networks updates"

This commit is contained in:
Jenkins 2017-09-12 15:56:58 +00:00 committed by Gerrit Code Review
commit bd6de55f7c
7 changed files with 333 additions and 45 deletions

View File

@ -0,0 +1,8 @@
---
prelude: >
Adding support for Geneve and nSX-network provider networks.
features:
- |
Deprecating the VXLAN provider network type.
Adding Geneve provider networks (with overlay transport zone).
Adding nsx-net provider networks attached to an existing nsx

View File

@ -67,7 +67,8 @@ class NsxV3NetworkTypes(object):
"""Allowed provider network types for the NSXv3 Plugin.""" """Allowed provider network types for the NSXv3 Plugin."""
FLAT = 'flat' FLAT = 'flat'
VLAN = 'vlan' VLAN = 'vlan'
VXLAN = 'vxlan' GENEVE = 'geneve'
NSX_NETWORK = 'nsx-net'
def is_nsx_version_1_1_0(nsx_version): def is_nsx_version_1_1_0(nsx_version):

View File

@ -1 +1 @@
84ceffa27115 a1be06050b41

View File

@ -0,0 +1,60 @@
# Copyright 2017 VMware, 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.
"""update nsx binding types
Revision ID: a1be06050b41
Revises: 84ceffa27115
Create Date: 2017-09-04 23:58:22.003350
"""
# revision identifiers, used by Alembic.
revision = 'a1be06050b41'
down_revision = '84ceffa27115'
depends_on = ('aede17d51d0f')
from alembic import op
import sqlalchemy as sa
from neutron.db import migration as neutron_op
all_tz_binding_type_enum = sa.Enum('flat', 'vlan', 'stt', 'gre', 'l3_ext',
'vxlan', 'geneve', 'portgroup', 'nsx-net',
name='tz_network_bindings_binding_type')
new_tz_binding_type_enum = sa.Enum('flat', 'vlan', 'stt', 'gre', 'l3_ext',
'geneve', 'portgroup', 'nsx-net',
name='tz_network_bindings_binding_type')
def upgrade():
# add the new network types to the enum
neutron_op.alter_enum_add_value(
'tz_network_bindings',
'binding_type',
all_tz_binding_type_enum,
False)
# change existing entries with type 'vxlan' to 'geneve'
op.execute("UPDATE tz_network_bindings SET binding_type='geneve' "
"where binding_type='vxlan'")
# remove 'vxlan' from the enum
op.alter_column(
'tz_network_bindings',
'binding_type',
type_=new_tz_binding_type_enum,
existing_type=all_tz_binding_type_enum,
existing_nullable=False)

View File

@ -43,9 +43,9 @@ class TzNetworkBinding(model_base.BASEV2, models.TimestampMixin):
network_id = sa.Column(sa.String(36), network_id = sa.Column(sa.String(36),
sa.ForeignKey('networks.id', ondelete="CASCADE"), sa.ForeignKey('networks.id', ondelete="CASCADE"),
primary_key=True) primary_key=True)
# 'flat', 'vlan', 'stt', 'gre', 'l3_ext', 'vxlan', 'portgroup' # 'flat', 'vlan', 'stt', 'gre', 'l3_ext', 'geneve', 'portgroup', 'nsx-net'
binding_type = sa.Column(sa.Enum('flat', 'vlan', 'stt', 'gre', 'l3_ext', binding_type = sa.Column(sa.Enum('flat', 'vlan', 'stt', 'gre', 'l3_ext',
'vxlan', 'portgroup', 'geneve', 'portgroup', 'nsx-net',
name='tz_network_bindings_binding_type'), name='tz_network_bindings_binding_type'),
nullable=False, primary_key=True) nullable=False, primary_key=True)
phy_uuid = sa.Column(sa.String(36), primary_key=True, default='') phy_uuid = sa.Column(sa.String(36), primary_key=True, default='')

View File

@ -693,12 +693,30 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
if bindings: if bindings:
raise n_exc.VlanIdInUse( raise n_exc.VlanIdInUse(
vlan_id=vlan_id, physical_network=physical_net) vlan_id=vlan_id, physical_network=physical_net)
elif net_type == utils.NsxV3NetworkTypes.VXLAN: elif net_type == utils.NsxV3NetworkTypes.GENEVE:
if vlan_id: if vlan_id:
err_msg = (_("Segmentation ID cannot be specified with " err_msg = (_("Segmentation ID cannot be specified with "
"%s network type") % "%s network type") %
utils.NsxV3NetworkTypes.VXLAN) utils.NsxV3NetworkTypes.GENEVE)
tz_type = self.nsxlib.transport_zone.TRANSPORT_TYPE_OVERLAY tz_type = self.nsxlib.transport_zone.TRANSPORT_TYPE_OVERLAY
elif net_type == utils.NsxV3NetworkTypes.NSX_NETWORK:
# Linking neutron networks to an existing NSX logical switch
if physical_net is None:
err_msg = (_("Physical network must be specified with "
"%s network type") % net_type)
# Validate the logical switch existence
try:
self.nsxlib.logical_switch.get(physical_net)
except nsx_lib_exc.ResourceNotFound:
err_msg = (_('Logical switch %s does not exist') %
physical_net)
# make sure no other neutron network is using it
bindings = (
nsx_db.get_network_bindings_by_vlanid_and_physical_net(
context.elevated().session, 0, physical_net))
if bindings:
err_msg = (_('Logical switch %s is already used by '
'another network') % physical_net)
else: else:
err_msg = (_('%(net_type_param)s %(net_type_value)s not ' err_msg = (_('%(net_type_param)s %(net_type_value)s not '
'supported') % 'supported') %
@ -718,7 +736,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
physical_net = az._default_overlay_tz_uuid physical_net = az._default_overlay_tz_uuid
# validate the transport zone existence and type # validate the transport zone existence and type
if not err_msg and is_provider_net and physical_net: if (not err_msg and is_provider_net and physical_net and
net_type != utils.NsxV3NetworkTypes.NSX_NETWORK):
try: try:
backend_type = self.nsxlib.transport_zone.get_transport_type( backend_type = self.nsxlib.transport_zone.get_transport_type(
physical_net) physical_net)
@ -752,6 +771,12 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
def _create_network_at_the_backend(self, context, net_data, az): def _create_network_at_the_backend(self, context, net_data, az):
is_provider_net, net_type, physical_net, vlan_id = ( is_provider_net, net_type, physical_net, vlan_id = (
self._validate_provider_create(context, net_data, az)) self._validate_provider_create(context, net_data, az))
if is_provider_net and net_type == utils.NsxV3NetworkTypes.NSX_NETWORK:
# Network already exists on the NSX backend
nsx_id = physical_net
else:
# Create network on the backend
neutron_net_id = net_data.get('id') or uuidutils.generate_uuid() neutron_net_id = net_data.get('id') or uuidutils.generate_uuid()
# To ensure that the correct tag will be set # To ensure that the correct tag will be set
net_data['id'] = neutron_net_id net_data['id'] = neutron_net_id
@ -763,8 +788,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
project_name=context.tenant_name) project_name=context.tenant_name)
admin_state = net_data.get('admin_state_up', True) admin_state = net_data.get('admin_state_up', True)
# Create network on the backend
LOG.debug('create_network: %(net_name)s, %(physical_net)s, ' LOG.debug('create_network: %(net_name)s, %(physical_net)s, '
'%(tags)s, %(admin_state)s, %(vlan_id)s', '%(tags)s, %(admin_state)s, %(vlan_id)s',
{'net_name': net_name, {'net_name': net_name,
@ -777,18 +800,39 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
admin_state=admin_state, admin_state=admin_state,
vlan_id=vlan_id, vlan_id=vlan_id,
description=net_data.get('description')) description=net_data.get('description'))
nsx_id = nsx_result['id']
return (is_provider_net, return (is_provider_net,
net_type, net_type,
physical_net, physical_net,
vlan_id, vlan_id,
nsx_result['id']) nsx_id)
def _is_overlay_network(self, context, network_id): def _is_overlay_network(self, context, network_id):
"""Return True if this is an overlay network
1. No binding ("normal" overlay networks will have no binding)
2. Geneve network
3. nsx network where the backend network is connected to an overlay TZ
"""
bindings = nsx_db.get_network_bindings(context.session, network_id) bindings = nsx_db.get_network_bindings(context.session, network_id)
# With NSX plugin, "normal" overlay networks will have no binding # With NSX plugin, "normal" overlay networks will have no binding
return (not bindings or if not bindings:
bindings[0].binding_type == utils.NsxV3NetworkTypes.VXLAN) return True
binding = bindings[0]
if binding.binding_type == utils.NsxV3NetworkTypes.GENEVE:
return True
if binding.binding_type == utils.NsxV3NetworkTypes.NSX_NETWORK:
# check the backend network
# TODO(asarfaty): Keep TZ type in DB to avoid going to the backend
ls = self.nsxlib.logical_switch.get(binding.phy_uuid)
tz = ls.get('transport_zone_id')
if tz:
backend_type = self.nsxlib.transport_zone.get_transport_type(
tz)
return (backend_type ==
self.nsxlib.transport_zone.TRANSPORT_TYPE_OVERLAY)
return False
def _extend_network_dict_provider(self, context, network, bindings=None): def _extend_network_dict_provider(self, context, network, bindings=None):
if 'id' not in network: if 'id' not in network:
@ -824,6 +868,13 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
return super(NsxV3Plugin, self).get_subnets( return super(NsxV3Plugin, self).get_subnets(
context, filters, fields, sorts, limit, marker, page_reverse) context, filters, fields, sorts, limit, marker, page_reverse)
def _network_is_nsx_net(self, context, network_id):
bindings = nsx_db.get_network_bindings(context.session, network_id)
if not bindings:
return False
return (bindings[0].binding_type ==
utils.NsxV3NetworkTypes.NSX_NETWORK)
def create_network(self, context, network): def create_network(self, context, network):
net_data = network['network'] net_data = network['network']
external = net_data.get(ext_net_extn.EXTERNAL) external = net_data.get(ext_net_extn.EXTERNAL)
@ -979,17 +1030,27 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
self._disable_native_dhcp(context, network_id) self._disable_native_dhcp(context, network_id)
nsx_net_id = self._get_network_nsx_id(context, network_id) nsx_net_id = self._get_network_nsx_id(context, network_id)
is_nsx_net = self._network_is_nsx_net(context, network_id)
is_overlay_network = self._is_overlay_network(context, network_id)
# First call DB operation for delete network as it will perform # First call DB operation for delete network as it will perform
# checks on active ports # checks on active ports
self._retry_delete_network(context, network_id) self._retry_delete_network(context, network_id)
if not self._network_is_external(context, network_id): if (not self._network_is_external(context, network_id) and
not is_nsx_net):
# TODO(salv-orlando): Handle backend failure, possibly without # TODO(salv-orlando): Handle backend failure, possibly without
# requiring us to un-delete the DB object. For instance, ignore # requiring us to un-delete the DB object. For instance, ignore
# failures occurring if logical switch is not found # failures occurring if logical switch is not found
self.nsxlib.logical_switch.delete(nsx_net_id) self.nsxlib.logical_switch.delete(nsx_net_id)
else: else:
if (cfg.CONF.nsx_v3.native_dhcp_metadata and is_nsx_net and
is_overlay_network):
# Delete the mdproxy port manually
port_id = self.nsxlib.get_id_by_resource_and_tag(
self.nsxlib.logical_port.resource_type,
'os-neutron-net-id', network_id)
if port_id:
self.nsxlib.logical_port.delete(port_id)
# TODO(berlin): delete subnets public announce on the network # TODO(berlin): delete subnets public announce on the network
pass
def _get_network_nsx_id(self, context, neutron_id): def _get_network_nsx_id(self, context, neutron_id):
# get the nsx switch id from the DB mapping # get the nsx switch id from the DB mapping
@ -1010,6 +1071,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
# Neutron does not support changing provider network values # Neutron does not support changing provider network values
providernet._raise_if_updates_provider_attributes(net_data) providernet._raise_if_updates_provider_attributes(net_data)
extern_net = self._network_is_external(context, id) extern_net = self._network_is_external(context, id)
is_nsx_net = self._network_is_nsx_net(context, id)
if extern_net: if extern_net:
self._assert_on_external_net_with_qos(net_data) self._assert_on_external_net_with_qos(net_data)
updated_net = super(NsxV3Plugin, self).update_network(context, id, updated_net = super(NsxV3Plugin, self).update_network(context, id,
@ -1022,7 +1084,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
self._process_l3_update(context, updated_net, network['network']) self._process_l3_update(context, updated_net, network['network'])
self._extend_network_dict_provider(context, updated_net) self._extend_network_dict_provider(context, updated_net)
if (not extern_net and if (not extern_net and not is_nsx_net and
('name' in net_data or 'admin_state_up' in net_data or ('name' in net_data or 'admin_state_up' in net_data or
'description' in net_data)): 'description' in net_data)):
try: try:
@ -3186,7 +3248,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
def _validate_multiple_subnets_routers(self, context, router_id, net_id): def _validate_multiple_subnets_routers(self, context, router_id, net_id):
network = self.get_network(context, net_id) network = self.get_network(context, net_id)
net_type = network.get(pnet.NETWORK_TYPE) net_type = network.get(pnet.NETWORK_TYPE)
if (net_type and net_type != utils.NsxV3NetworkTypes.VXLAN): if (net_type and net_type != utils.NsxV3NetworkTypes.GENEVE):
err_msg = (_("Only overlay networks can be attached to a logical " err_msg = (_("Only overlay networks can be attached to a logical "
"router. Network %(net_id)s is a %(net_type)s based " "router. Network %(net_id)s is a %(net_type)s based "
"network") % {'net_id': net_id, 'net_type': net_type}) "network") % {'net_id': net_id, 'net_type': net_type})

View File

@ -78,6 +78,10 @@ def _mock_create_firewall_rules(*args):
]} ]}
def _return_id_key(*args, **kwargs):
return {'id': uuidutils.generate_uuid()}
def _mock_nsx_backend_calls(): def _mock_nsx_backend_calls():
mock.patch("vmware_nsxlib.v3.client.NSX3Client").start() mock.patch("vmware_nsxlib.v3.client.NSX3Client").start()
@ -85,9 +89,6 @@ def _mock_nsx_backend_calls():
'resource_type': 'FakeResource', 'resource_type': 'FakeResource',
'id': uuidutils.generate_uuid()} 'id': uuidutils.generate_uuid()}
def _return_id_key(*args, **kwargs):
return {'id': uuidutils.generate_uuid()}
def _return_id(*args, **kwargs): def _return_id(*args, **kwargs):
return uuidutils.generate_uuid() return uuidutils.generate_uuid()
@ -113,11 +114,6 @@ def _mock_nsx_backend_calls():
"get_id_by_name_or_id", "get_id_by_name_or_id",
return_value=uuidutils.generate_uuid()).start() return_value=uuidutils.generate_uuid()).start()
mock.patch(
"vmware_nsxlib.v3.core_resources.NsxLibTransportZone."
"get_id_by_name_or_id",
return_value=uuidutils.generate_uuid()).start()
mock.patch( mock.patch(
"vmware_nsxlib.v3.core_resources.NsxLibBridgeEndpoint.create", "vmware_nsxlib.v3.core_resources.NsxLibBridgeEndpoint.create",
side_effect=_return_id_key).start() side_effect=_return_id_key).start()
@ -284,6 +280,167 @@ class TestNetworksV2(test_plugin.TestNetworksV2, NsxV3PluginTestCaseMixin):
networks = self.plugin.get_networks(ctx) networks = self.plugin.get_networks(ctx)
self.assertListEqual([], networks) self.assertListEqual([], networks)
def test_create_provider_flat_network(self):
providernet_args = {pnet.NETWORK_TYPE: 'flat'}
with mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'create', side_effect=_return_id_key) as nsx_create, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'delete') as nsx_delete, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='VLAN'),\
self.network(name='flat_net',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE, )) as net:
self.assertEqual('flat', net['network'].get(pnet.NETWORK_TYPE))
# make sure the network is created at the backend
nsx_create.assert_called_once()
# Delete the network and make sure it is deleted from the backend
req = self.new_delete_request('networks', net['network']['id'])
res = req.get_response(self.api)
self.assertEqual(exc.HTTPNoContent.code, res.status_int)
nsx_delete.assert_called_once()
def test_create_provider_flat_network_with_physical_net(self):
physical_network = nsx_v3_mocks.DEFAULT_TIER0_ROUTER_UUID
providernet_args = {pnet.NETWORK_TYPE: 'flat',
pnet.PHYSICAL_NETWORK: physical_network}
with mock.patch(
'vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='VLAN'),\
self.network(name='flat_net',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK)) as net:
self.assertEqual('flat', net['network'].get(pnet.NETWORK_TYPE))
def test_create_provider_flat_network_with_vlan(self):
providernet_args = {pnet.NETWORK_TYPE: 'flat',
pnet.SEGMENTATION_ID: 11}
with mock.patch('vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='VLAN'):
result = self._create_network(fmt='json', name='bad_flat_net',
admin_state_up=True,
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.SEGMENTATION_ID))
data = self.deserialize('json', result)
# should fail
self.assertEqual('InvalidInput', data['NeutronError']['type'])
def test_create_provider_geneve_network(self):
providernet_args = {pnet.NETWORK_TYPE: 'geneve'}
with mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'create', side_effect=_return_id_key) as nsx_create, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'delete') as nsx_delete, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='OVERLAY'),\
self.network(name='geneve_net',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE, )) as net:
self.assertEqual('geneve', net['network'].get(pnet.NETWORK_TYPE))
# make sure the network is created at the backend
nsx_create.assert_called_once()
# Delete the network and make sure it is deleted from the backend
req = self.new_delete_request('networks', net['network']['id'])
res = req.get_response(self.api)
self.assertEqual(exc.HTTPNoContent.code, res.status_int)
nsx_delete.assert_called_once()
def test_create_provider_geneve_network_with_physical_net(self):
physical_network = nsx_v3_mocks.DEFAULT_TIER0_ROUTER_UUID
providernet_args = {pnet.NETWORK_TYPE: 'geneve',
pnet.PHYSICAL_NETWORK: physical_network}
with mock.patch(
'vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='OVERLAY'),\
self.network(name='geneve_net',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE, )) as net:
self.assertEqual('geneve', net['network'].get(pnet.NETWORK_TYPE))
def test_create_provider_geneve_network_with_vlan(self):
providernet_args = {pnet.NETWORK_TYPE: 'geneve',
pnet.SEGMENTATION_ID: 11}
with mock.patch(
'vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='OVERLAY'):
result = self._create_network(fmt='json', name='bad_geneve_net',
admin_state_up=True,
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.SEGMENTATION_ID))
data = self.deserialize('json', result)
# should fail
self.assertEqual('InvalidInput', data['NeutronError']['type'])
def test_create_provider_vlan_network(self):
providernet_args = {pnet.NETWORK_TYPE: 'vlan',
pnet.SEGMENTATION_ID: 11}
with mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'create', side_effect=_return_id_key) as nsx_create, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'delete') as nsx_delete, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibTransportZone.'
'get_transport_type', return_value='VLAN'),\
self.network(name='vlan_net',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.SEGMENTATION_ID)) as net:
self.assertEqual('vlan', net['network'].get(pnet.NETWORK_TYPE))
# make sure the network is created at the backend
nsx_create.assert_called_once()
# Delete the network and make sure it is deleted from the backend
req = self.new_delete_request('networks', net['network']['id'])
res = req.get_response(self.api)
self.assertEqual(exc.HTTPNoContent.code, res.status_int)
nsx_delete.assert_called_once()
def test_create_provider_nsx_network(self):
physical_network = 'Fake logical switch'
providernet_args = {pnet.NETWORK_TYPE: 'nsx-net',
pnet.PHYSICAL_NETWORK: physical_network}
with mock.patch(
'vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.create',
side_effect=nsxlib_exc.ResourceNotFound) as nsx_create, \
mock.patch('vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.'
'delete') as nsx_delete, \
self.network(name='nsx_net',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK)) as net:
self.assertEqual('nsx-net', net['network'].get(pnet.NETWORK_TYPE))
self.assertEqual(physical_network,
net['network'].get(pnet.PHYSICAL_NETWORK))
# make sure the network is NOT created at the backend
nsx_create.assert_not_called()
# Delete the network. It should NOT deleted from the backend
req = self.new_delete_request('networks', net['network']['id'])
res = req.get_response(self.api)
self.assertEqual(exc.HTTPNoContent.code, res.status_int)
nsx_delete.assert_not_called()
def test_create_provider_bad_nsx_network(self):
physical_network = 'Bad logical switch'
providernet_args = {pnet.NETWORK_TYPE: 'nsx-net',
pnet.PHYSICAL_NETWORK: physical_network}
with mock.patch(
"vmware_nsxlib.v3.core_resources.NsxLibLogicalSwitch.get",
side_effect=nsxlib_exc.ResourceNotFound):
result = self._create_network(fmt='json', name='bad_nsx_net',
admin_state_up=True,
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK))
data = self.deserialize('json', result)
# should fail
self.assertEqual('InvalidInput', data['NeutronError']['type'])
class TestSubnetsV2(test_plugin.TestSubnetsV2, NsxV3PluginTestCaseMixin): class TestSubnetsV2(test_plugin.TestSubnetsV2, NsxV3PluginTestCaseMixin):