NSX|V3 IPAM support

The NSX-V3 plugin will use the NSX-V3 backend IPAM.
An IP pool will be created for each subnet, and port IPs will be allocated
from this pool.
The current backend limitation is that we cannot allocate a specific IP,
so port create/update with fixed_ips will fail, unless the requested ip
is the subnet gateway ip.

To enable this option set 'ipam_driver = vmware_nsxv3_ipam' in the
neutron.conf

Change-Id: I5263555cbb776018a5d01f19d0997fd2adf6483d
This commit is contained in:
Adit Sarfaty 2016-12-11 10:13:21 +02:00 committed by garyk
parent 95231630f2
commit 1266099049
15 changed files with 759 additions and 213 deletions

View File

@ -0,0 +1,11 @@
---
prelude: >
The NSX-v3 plugin can use the platform IPAM for ip allocations for all
network types.
features:
- The NSX-v3 plugin can use the platform IPAM for ip allocations for all
network types.
In order to use this feature, the ipam_driver in the neutron.conf file
should be set to vmware_nsxv3_ipam.
Currently the plugin does not support allocating a specific address
from the pool depending on the NSX version.

View File

@ -37,6 +37,7 @@ neutron.qos.notification_drivers =
vmware_nsxv3_message_queue = vmware_nsx.services.qos.nsx_v3.message_queue:NsxV3QosNotificationDriver vmware_nsxv3_message_queue = vmware_nsx.services.qos.nsx_v3.message_queue:NsxV3QosNotificationDriver
neutron.ipam_drivers = neutron.ipam_drivers =
vmware_nsxv_ipam = vmware_nsx.services.ipam.nsx_v.driver:NsxvIpamDriver vmware_nsxv_ipam = vmware_nsx.services.ipam.nsx_v.driver:NsxvIpamDriver
vmware_nsxv3_ipam = vmware_nsx.services.ipam.nsx_v3.driver:Nsxv3IpamDriver
vmware_nsx.neutron.nsxv.router_type_drivers = vmware_nsx.neutron.nsxv.router_type_drivers =
shared = vmware_nsx.plugins.nsx_v.drivers.shared_router_driver:RouterSharedDriver shared = vmware_nsx.plugins.nsx_v.drivers.shared_router_driver:RouterSharedDriver
distributed = vmware_nsx.plugins.nsx_v.drivers.distributed_router_driver:RouterDistributedDriver distributed = vmware_nsx.plugins.nsx_v.drivers.distributed_router_driver:RouterDistributedDriver

View File

@ -423,3 +423,28 @@ def save_sg_rule_mappings(session, rules):
mapping = nsx_models.NeutronNsxRuleMapping( mapping = nsx_models.NeutronNsxRuleMapping(
neutron_id=neutron_id, nsx_id=nsx_id) neutron_id=neutron_id, nsx_id=nsx_id)
session.add(mapping) session.add(mapping)
def add_nsx_ipam_subnet_pool(session, subnet_id, nsx_pool_id):
with session.begin(subtransactions=True):
binding = nsx_models.NsxSubnetIpam(
subnet_id=subnet_id,
nsx_pool_id=nsx_pool_id)
session.add(binding)
return binding
def get_nsx_ipam_pool_for_subnet(session, subnet_id):
try:
entry = session.query(
nsx_models.NsxSubnetIpam).filter_by(
subnet_id=subnet_id).one()
return entry.nsx_pool_id
except exc.NoResultFound:
return
def del_nsx_ipam_subnet_pool(session, subnet_id, nsx_pool_id):
return (session.query(nsx_models.NsxSubnetIpam).
filter_by(subnet_id=subnet_id,
nsx_pool_id=nsx_pool_id).delete())

View File

@ -1 +1 @@
d49ac91b560e 5c8f451290b7

View File

@ -0,0 +1,33 @@
# Copyright 2016 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.
"""nsxv_subnet_ipam rename to nsx_subnet_ipam
Revision ID: 5c8f451290b7
Revises: d49ac91b560e
Create Date: 2016-12-25 11:08:30.300482
"""
# revision identifiers, used by Alembic.
revision = '5c8f451290b7'
down_revision = 'd49ac91b560e'
depends_on = ('6e6da8296c0e',)
from alembic import op
def upgrade():
op.rename_table('nsxv_subnet_ipam',
'nsx_subnet_ipam')

View File

@ -368,3 +368,12 @@ class NsxPortMirrorSessionMapping(model_base.BASEV2):
nullable=False, nullable=False,
primary_key=True) primary_key=True)
port_mirror_session_id = sa.Column(sa.String(36), nullable=False) port_mirror_session_id = sa.Column(sa.String(36), nullable=False)
class NsxSubnetIpam(model_base.BASEV2, models.TimestampMixin):
"""Map Subnets with their backend pool id."""
__tablename__ = 'nsx_subnet_ipam'
# the Subnet id is not a foreign key because the subnet is deleted
# before the pool does
subnet_id = sa.Column(sa.String(36), primary_key=True)
nsx_pool_id = sa.Column(sa.String(36), primary_key=True)

View File

@ -803,28 +803,3 @@ def update_nsxv_subnet_ext_attributes(session, subnet_id,
binding[ext_dns_search_domain.DNS_SEARCH_DOMAIN] = dns_search_domain binding[ext_dns_search_domain.DNS_SEARCH_DOMAIN] = dns_search_domain
binding[ext_dhcp_mtu.DHCP_MTU] = dhcp_mtu binding[ext_dhcp_mtu.DHCP_MTU] = dhcp_mtu
return binding return binding
def add_nsxv_ipam_subnet_pool(session, subnet_id, nsx_pool_id):
with session.begin(subtransactions=True):
binding = nsxv_models.NsxvSubnetIpam(
subnet_id=subnet_id,
nsx_pool_id=nsx_pool_id)
session.add(binding)
return binding
def get_nsxv_ipam_pool_for_subnet(session, subnet_id):
try:
entry = session.query(
nsxv_models.NsxvSubnetIpam).filter_by(
subnet_id=subnet_id).one()
return entry.nsx_pool_id
except exc.NoResultFound:
return
def del_nsxv_ipam_subnet_pool(session, subnet_id, nsx_pool_id):
return (session.query(nsxv_models.NsxvSubnetIpam).
filter_by(subnet_id=subnet_id,
nsx_pool_id=nsx_pool_id).delete())

View File

@ -347,12 +347,3 @@ class NsxvSubnetExtAttributes(model_base.BASEV2, models.TimestampMixin):
models_v2.Subnet, models_v2.Subnet,
backref=orm.backref("nsxv_subnet_attributes", lazy='joined', backref=orm.backref("nsxv_subnet_attributes", lazy='joined',
uselist=False, cascade='delete')) uselist=False, cascade='delete'))
class NsxvSubnetIpam(model_base.BASEV2, models.TimestampMixin):
"""Map Subnets with their backend pool id."""
__tablename__ = 'nsxv_subnet_ipam'
# the Subnet id is not a foreign key because the subnet is deleted
# before the pool does
subnet_id = sa.Column(sa.String(36), primary_key=True)
nsx_pool_id = sa.Column(sa.String(36), primary_key=True)

View File

@ -0,0 +1,220 @@
# Copyright 2016 VMware, 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 abc
import six
from neutron.ipam import driver as ipam_base
from neutron.ipam.drivers.neutrondb_ipam import driver as neutron_driver
from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import requests as ipam_req
from neutron.ipam import subnet_alloc
from neutron_lib.plugins import directory
from vmware_nsx.db import db as nsx_db
@six.add_metaclass(abc.ABCMeta)
class NsxIpamBase(object):
@classmethod
def get_core_plugin(cls):
return directory.get_plugin()
@classmethod
def _fetch_subnet(cls, context, id):
p = cls.get_core_plugin()
return p._get_subnet(context, id)
@classmethod
def _fetch_network(cls, context, id):
p = cls.get_core_plugin()
return p.get_network(context, id)
class NsxSubnetRequestFactory(ipam_req.SubnetRequestFactory, NsxIpamBase):
"""Builds request using subnet info, including the network id"""
@classmethod
def get_request(cls, context, subnet, subnetpool):
req = super(NsxSubnetRequestFactory, cls).get_request(
context, subnet, subnetpool)
# Add the network id into the request
if 'network_id' in subnet:
req.network_id = subnet['network_id']
return req
class NsxAbstractIpamDriver(subnet_alloc.SubnetAllocator, NsxIpamBase):
"""Abstract IPAM Driver For NSX."""
def __init__(self, subnetpool, context):
super(NsxAbstractIpamDriver, self).__init__(subnetpool, context)
# in case of unsupported networks (or pre-upgrade networks)
# the neutron internal driver will be used
self.default_ipam = neutron_driver.NeutronDbPool(subnetpool, context)
def _is_supported_net(self, subnet_request):
"""By default - all networks are supported"""
return True
def get_subnet_request_factory(self):
# override the OOB factory to add the network ID
return NsxSubnetRequestFactory
@abc.abstractproperty
def _subnet_class(self):
"""Return the class of the subnet that should be used."""
pass
def get_subnet(self, subnet_id):
"""Retrieve an IPAM subnet."""
nsx_pool_id = nsx_db.get_nsx_ipam_pool_for_subnet(
self._context.session, subnet_id)
if not nsx_pool_id:
# Unsupported (or pre-upgrade) network
return self.default_ipam.get_subnet(subnet_id)
return self._subnet_class.load(subnet_id, nsx_pool_id, self._context)
@abc.abstractmethod
def allocate_backend_pool(self, subnet_request):
"""Create a pool on the NSX backend and return its ID"""
pass
def allocate_subnet(self, subnet_request):
"""Create an IPAMSubnet object for the provided request."""
if not self._is_supported_net(subnet_request=subnet_request):
# fallback to the neutron internal driver implementation
return self.default_ipam.allocate_subnet(subnet_request)
if self._subnetpool:
subnet = super(NsxAbstractIpamDriver, self).allocate_subnet(
subnet_request)
subnet_request = subnet.get_details()
# SubnetRequest must be an instance of SpecificSubnet
if not isinstance(subnet_request, ipam_req.SpecificSubnetRequest):
raise ipam_exc.InvalidSubnetRequestType(
subnet_type=type(subnet_request))
# Add the pool to the NSX backend
nsx_pool_id = self.allocate_backend_pool(subnet_request)
# Add the pool to the DB
nsx_db.add_nsx_ipam_subnet_pool(self._context.session,
subnet_request.subnet_id,
nsx_pool_id)
# return the subnet object
return self._subnet_class.load(subnet_request.subnet_id, nsx_pool_id,
self._context,
tenant_id=subnet_request.tenant_id)
def _raise_update_not_supported(self):
msg = _('Changing the subnet range or gateway is not supported')
raise ipam_exc.IpamValueInvalid(message=msg)
def update_subnet(self, subnet_request):
"""Update subnet info in the IPAM driver.
The NSX backend does not support changing the ip pool cidr or gateway
"""
#TODO(asarfaty): the nsx-v3 backend does support update
nsx_pool_id = nsx_db.get_nsx_ipam_pool_for_subnet(
self._context.session, subnet_request.subnet_id)
if not nsx_pool_id:
# Unsupported (or pre-upgrade) network
return self.default_ipam.update_subnet(
subnet_request)
# get the current pool data
curr_subnet = self._subnet_class.load(
subnet_request.subnet_id, nsx_pool_id,
self._context, tenant_id=subnet_request.tenant_id).get_details()
# check that the gateway / cidr / pools did not change
if (subnet_request.gateway_ip and
str(subnet_request.gateway_ip) != str(curr_subnet.gateway_ip)):
self._raise_update_not_supported()
if subnet_request.prefixlen != curr_subnet.prefixlen:
self._raise_update_not_supported()
if (len(subnet_request.allocation_pools) !=
len(curr_subnet.allocation_pools)):
self._raise_update_not_supported()
for pool_ind in range(len(subnet_request.allocation_pools)):
pool_req = subnet_request.allocation_pools[pool_ind]
curr_pool = curr_subnet.allocation_pools[pool_ind]
if (pool_req.first != curr_pool.first or
pool_req.last != curr_pool.last):
self._raise_update_not_supported()
@abc.abstractmethod
def delete_backend_pool(self, nsx_pool_id):
pass
def remove_subnet(self, subnet_id):
"""Delete an IPAM subnet pool from backend & DB."""
nsx_pool_id = nsx_db.get_nsx_ipam_pool_for_subnet(
self._context.session, subnet_id)
if not nsx_pool_id:
# Unsupported (or pre-upgrade) network
self.default_ipam.remove_subnet(subnet_id)
return
# Delete from backend
self.delete_backend_pool(nsx_pool_id)
# delete pool from DB
nsx_db.del_nsx_ipam_subnet_pool(self._context.session,
subnet_id, nsx_pool_id)
class NsxAbstractIpamSubnet(ipam_base.Subnet, NsxIpamBase):
"""Manage IP addresses for the NSX IPAM driver."""
def __init__(self, subnet_id, nsx_pool_id, ctx, tenant_id):
self._subnet_id = subnet_id
self._nsx_pool_id = nsx_pool_id
self._context = ctx
self._tenant_id = tenant_id
@classmethod
def load(cls, neutron_subnet_id, nsx_pool_id, ctx, tenant_id=None):
"""Load an IPAM subnet object given its neutron ID."""
return cls(neutron_subnet_id, nsx_pool_id, ctx, tenant_id)
def allocate(self, address_request):
"""Allocate an IP from the pool"""
return self.backend_allocate(address_request)
@abc.abstractmethod
def backend_allocate(self, address_request):
pass
def deallocate(self, address):
"""Return an IP to the pool"""
self.backend_deallocate(address)
@abc.abstractmethod
def backend_deallocate(self, address):
pass
def update_allocation_pools(self, pools, cidr):
# Not supported
pass

View File

@ -17,66 +17,34 @@
import netaddr import netaddr
import xml.etree.ElementTree as et import xml.etree.ElementTree as et
from oslo_log import log as logging
from neutron.extensions import external_net as ext_net_extn from neutron.extensions import external_net as ext_net_extn
from neutron.extensions import multiprovidernet as mpnet from neutron.extensions import multiprovidernet as mpnet
from neutron.extensions import providernet as pnet from neutron.extensions import providernet as pnet
from neutron.ipam import driver as ipam_base
from neutron.ipam.drivers.neutrondb_ipam import driver as neutron_driver
from neutron.ipam import exceptions as ipam_exc from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import requests as ipam_req from neutron.ipam import requests as ipam_req
from neutron.ipam import subnet_alloc
from neutron_lib.api import validators from neutron_lib.api import validators
from neutron_lib.plugins import directory
from oslo_log import log as logging
from vmware_nsx._i18n import _, _LE from vmware_nsx._i18n import _, _LE
from vmware_nsx.common import locking
from vmware_nsx.db import nsxv_db
from vmware_nsx.plugins.nsx_v.vshield.common import constants from vmware_nsx.plugins.nsx_v.vshield.common import constants
from vmware_nsx.plugins.nsx_v.vshield.common import exceptions as vc_exc from vmware_nsx.plugins.nsx_v.vshield.common import exceptions as vc_exc
from vmware_nsx.services.ipam.common import driver as common
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class NsxvIpamBase(object): class NsxVIpamBase(common.NsxIpamBase):
@classmethod
def get_core_plugin(cls):
return directory.get_plugin()
@classmethod
def _fetch_subnet(cls, context, id):
p = cls.get_core_plugin()
return p._get_subnet(context, id)
@classmethod
def _fetch_network(cls, context, id):
p = cls.get_core_plugin()
return p.get_network(context, id)
@property @property
def _vcns(self): def _vcns(self):
p = self.get_core_plugin() p = self.get_core_plugin()
return p.nsx_v.vcns return p.nsx_v.vcns
def _get_vcns_error_code(self, e):
"""Get the error code out of VcnsApiException"""
try:
desc = et.fromstring(e.response)
return int(desc.find('errorCode').text)
except Exception:
LOG.error(_LE('IPAM pool: Error code not present. %s'),
e.response)
class NsxvIpamDriver(common.NsxAbstractIpamDriver, NsxVIpamBase):
class NsxvIpamDriver(subnet_alloc.SubnetAllocator, NsxvIpamBase):
"""IPAM Driver For NSX-V external & provider networks.""" """IPAM Driver For NSX-V external & provider networks."""
def __init__(self, subnetpool, context):
super(NsxvIpamDriver, self).__init__(subnetpool, context)
# in case of regular networks (not external, not provider net)
# or ipv6 networks, the neutron internal driver will be used
self.default_ipam = neutron_driver.NeutronDbPool(subnetpool, context)
def _is_ext_or_provider_net(self, subnet_request): def _is_ext_or_provider_net(self, subnet_request):
"""Return True if the network of the request is external or """Return True if the network of the request is external or
provider network provider network
@ -110,19 +78,9 @@ class NsxvIpamDriver(subnet_alloc.SubnetAllocator, NsxvIpamBase):
return (self._is_ext_or_provider_net(subnet_request) and return (self._is_ext_or_provider_net(subnet_request) and
not self._is_ipv6_subnet(subnet_request)) not self._is_ipv6_subnet(subnet_request))
def get_subnet_request_factory(self): @property
# override the OOB factory to add the network ID def _subnet_class(self):
return NsxvSubnetRequestFactory return NsxvIpamSubnet
def get_subnet(self, subnet_id):
"""Retrieve an IPAM subnet."""
nsx_pool_id = nsxv_db.get_nsxv_ipam_pool_for_subnet(
self._context.session, subnet_id)
if not nsx_pool_id:
# Unsupported (or pre-upgrade) network
return self.default_ipam.get_subnet(subnet_id)
return NsxvIpamSubnet.load(subnet_id, nsx_pool_id, self._context)
def allocate_backend_pool(self, subnet_request): def allocate_backend_pool(self, subnet_request):
"""Create a pool on the NSX backend and return its ID""" """Create a pool on the NSX backend and return its ID"""
@ -151,114 +109,27 @@ class NsxvIpamDriver(subnet_alloc.SubnetAllocator, NsxvIpamBase):
return nsx_pool_id return nsx_pool_id
def allocate_subnet(self, subnet_request): def delete_backend_pool(self, nsx_pool_id):
"""Create an IPAMSubnet object for the provided request.""" try:
if not self._is_supported_net(subnet_request=subnet_request): self._vcns.delete_ipam_ip_pool(nsx_pool_id)
# fallback to the neutron internal driver implementation except vc_exc.VcnsApiException as e:
return self.default_ipam.allocate_subnet(subnet_request) LOG.error(_LE("Failed to delete IPAM from backend: %s"), e)
# Continue anyway, since this subnet was already removed
if self._subnetpool:
subnet = super(NsxvIpamDriver, self).allocate_subnet(
subnet_request)
subnet_request = subnet.get_details()
# SubnetRequest must be an instance of SpecificSubnet
if not isinstance(subnet_request, ipam_req.SpecificSubnetRequest):
raise ipam_exc.InvalidSubnetRequestType(
subnet_type=type(subnet_request))
# Add the pool to the NSX backend
nsx_pool_id = self.allocate_backend_pool(subnet_request)
# Add the pool to the DB
nsxv_db.add_nsxv_ipam_subnet_pool(self._context.session,
subnet_request.subnet_id,
nsx_pool_id)
# return the subnet object
return NsxvIpamSubnet(subnet_request.subnet_id, nsx_pool_id,
self._context, subnet_request.tenant_id)
def _raise_update_not_supported(self):
msg = _('Changing the subnet range or gateway is not supported')
raise ipam_exc.IpamValueInvalid(message=msg)
def update_subnet(self, subnet_request):
"""Update subnet info in the IPAM driver.
The NSX backend does not support changing the ip pool cidr or gateway
"""
nsx_pool_id = nsxv_db.get_nsxv_ipam_pool_for_subnet(
self._context.session, subnet_request.subnet_id)
if not nsx_pool_id:
# Unsupported (or pre-upgrade) network
return self.default_ipam.update_subnet(
subnet_request)
# get the current pool data
curr_subnet = NsxvIpamSubnet(
subnet_request.subnet_id, nsx_pool_id,
self._context, subnet_request.tenant_id).get_details()
# check that the gateway / cidr / pools did not change
if str(subnet_request.gateway_ip) != str(curr_subnet.gateway_ip):
self._raise_update_not_supported()
if subnet_request.prefixlen != curr_subnet.prefixlen:
self._raise_update_not_supported()
if (len(subnet_request.allocation_pools) !=
len(curr_subnet.allocation_pools)):
self._raise_update_not_supported()
for pool_ind in range(len(subnet_request.allocation_pools)):
pool_req = subnet_request.allocation_pools[pool_ind]
curr_pool = curr_subnet.allocation_pools[pool_ind]
if (pool_req.first != curr_pool.first or
pool_req.last != curr_pool.last):
self._raise_update_not_supported()
def remove_subnet(self, subnet_id):
"""Delete an IPAM subnet pool from backend & DB."""
nsx_pool_id = nsxv_db.get_nsxv_ipam_pool_for_subnet(
self._context.session, subnet_id)
if not nsx_pool_id:
# Unsupported (or pre-upgrade) network
self.default_ipam.remove_subnet(subnet_id)
return
with locking.LockManager.get_lock('nsx-ipam-' + nsx_pool_id):
# Delete from backend
try:
self._vcns.delete_ipam_ip_pool(nsx_pool_id)
except vc_exc.VcnsApiException as e:
LOG.error(_LE("Failed to delete IPAM from backend: %s"), e)
# Continue anyway, since this subnet was already removed
# delete pool from DB
nsxv_db.del_nsxv_ipam_subnet_pool(self._context.session,
subnet_id, nsx_pool_id)
class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase): class NsxvIpamSubnet(common.NsxAbstractIpamSubnet, NsxVIpamBase):
"""Manage IP addresses for the NSX IPAM driver.""" """Manage IP addresses for the NSX-V IPAM driver."""
def __init__(self, subnet_id, nsx_pool_id, ctx, tenant_id): def _get_vcns_error_code(self, e):
self._subnet_id = subnet_id """Get the error code out of VcnsApiException"""
self._nsx_pool_id = nsx_pool_id try:
self._context = ctx desc = et.fromstring(e.response)
self._tenant_id = tenant_id return int(desc.find('errorCode').text)
except Exception:
LOG.error(_LE('IPAM pool: Error code not present. %s'),
e.response)
@classmethod def backend_allocate(self, address_request):
def load(cls, neutron_subnet_id, nsx_pool_id, ctx, tenant_id=None):
"""Load an IPAM subnet object given its neutron ID."""
return cls(neutron_subnet_id, nsx_pool_id, ctx, tenant_id)
def allocate(self, address_request):
"""Allocate an IP from the pool"""
with locking.LockManager.get_lock('nsx-ipam-' + self._nsx_pool_id):
return self._allocate(address_request)
def _allocate(self, address_request):
try: try:
# allocate a specific IP # allocate a specific IP
if isinstance(address_request, ipam_req.SpecificAddressRequest): if isinstance(address_request, ipam_req.SpecificAddressRequest):
@ -288,12 +159,7 @@ class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase):
raise ipam_exc.IPAllocationFailed() raise ipam_exc.IPAllocationFailed()
return ip_address return ip_address
def deallocate(self, address): def backend_deallocate(self, address):
"""Return an IP to the pool"""
with locking.LockManager.get_lock('nsx-ipam-' + self._nsx_pool_id):
self._deallocate(address)
def _deallocate(self, address):
try: try:
self._vcns.release_ipam_ip_to_pool(self._nsx_pool_id, address) self._vcns.release_ipam_ip_to_pool(self._nsx_pool_id, address)
except vc_exc.VcnsApiException as e: except vc_exc.VcnsApiException as e:
@ -306,10 +172,6 @@ class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase):
subnet_id=self._subnet_id, subnet_id=self._subnet_id,
ip_address=address) ip_address=address)
def update_allocation_pools(self, pools, cidr):
# Not supported
pass
def _get_pool_cidr(self, pool): def _get_pool_cidr(self, pool):
# rebuild the cidr from the pool range & prefix using the first # rebuild the cidr from the pool range & prefix using the first
# range in the pool, because they all should belong to the same cidr # range in the pool, because they all should belong to the same cidr
@ -334,17 +196,3 @@ class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase):
return ipam_req.SpecificSubnetRequest( return ipam_req.SpecificSubnetRequest(
self._tenant_id, self._subnet_id, self._tenant_id, self._subnet_id,
cidr, gateway_ip=gateway_ip, allocation_pools=pools) cidr, gateway_ip=gateway_ip, allocation_pools=pools)
class NsxvSubnetRequestFactory(ipam_req.SubnetRequestFactory, NsxvIpamBase):
"""Builds request using subnet info, including the network id"""
@classmethod
def get_request(cls, context, subnet, subnetpool):
req = super(NsxvSubnetRequestFactory, cls).get_request(
context, subnet, subnetpool)
# Add the network id into the request
if 'network_id' in subnet:
req.network_id = subnet['network_id']
return req

View File

@ -0,0 +1,13 @@
=================================================================
Enabling NSXv3 IPAM for external & provider networks in Devstack
=================================================================
1. Download DevStack
2. Update the ``local.conf`` file::
[[post-config|$NEUTRON_CONF]]
[DEFAULT]
ipam_driver = vmware_nsxv3_ipam
3. run ``stack.sh``

View File

@ -0,0 +1,200 @@
# Copyright 2016 VMware, 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 netaddr
from oslo_log import log as logging
from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import requests as ipam_req
from vmware_nsx._i18n import _, _LE, _LI, _LW
from vmware_nsx.services.ipam.common import driver as common
from vmware_nsxlib.v3 import exceptions as nsx_lib_exc
from vmware_nsxlib.v3 import nsx_constants as error
from vmware_nsxlib.v3 import resources
LOG = logging.getLogger(__name__)
class Nsxv3IpamDriver(common.NsxAbstractIpamDriver):
"""IPAM Driver For NSX-V3 external & provider networks."""
def __init__(self, subnetpool, context):
super(Nsxv3IpamDriver, self).__init__(subnetpool, context)
self.nsxlib_ipam = resources.IpPool(
self.get_core_plugin().nsxlib.client)
@property
def _subnet_class(self):
return Nsxv3IpamSubnet
def _get_cidr_from_request(self, subnet_request):
return "%s/%s" % (subnet_request.subnet_cidr[0],
subnet_request.prefixlen)
def allocate_backend_pool(self, subnet_request):
"""Create a pool on the NSX backend and return its ID"""
if subnet_request.allocation_pools:
ranges = [
{'start': str(pool[0]), 'end': str(pool[-1])}
for pool in subnet_request.allocation_pools]
else:
ranges = []
# name/description length on backend is long, so there is no problem
name = 'subnet_' + subnet_request.subnet_id
description = 'OS IP pool for subnet ' + subnet_request.subnet_id
try:
response = self.nsxlib_ipam.create(
self._get_cidr_from_request(subnet_request),
ranges=ranges,
display_name=name,
description=description,
gateway_ip=subnet_request.gateway_ip)
nsx_pool_id = response['id']
except Exception as e:
#TODO(asarfaty): handle specific errors
msg = _('Failed to create subnet IPAM: %s') % e
raise ipam_exc.IpamValueInvalid(message=msg)
return nsx_pool_id
def delete_backend_pool(self, nsx_pool_id):
# Because of the delete_subnet flow in the neutron plugin,
# some ports still hold IPs from this pool.
# Those ports be deleted shortly after this function.
# We need to release those IPs before deleting the backed pool,
# or else it will fail.
pool_allocations = self.nsxlib_ipam.get_allocations(nsx_pool_id)
if pool_allocations and pool_allocations.get('result_count'):
for allocation in pool_allocations.get('results', []):
ip_addr = allocation.get('allocation_id')
try:
self.nsxlib_ipam.release(nsx_pool_id, ip_addr)
except Exception as e:
LOG.warning(_LW("Failed to release ip %(ip)s from pool "
"%(pool)s: %(e)s"),
{'ip': ip_addr, 'pool': nsx_pool_id, 'e': e})
try:
self.nsxlib_ipam.delete(nsx_pool_id)
except Exception as e:
LOG.error(_LE("Failed to delete IPAM from backend: %s"), e)
# Continue anyway, since this subnet was already removed
class Nsxv3IpamSubnet(common.NsxAbstractIpamSubnet):
"""Manage IP addresses for the NSX V3 IPAM driver."""
def __init__(self, subnet_id, nsx_pool_id, ctx, tenant_id):
super(Nsxv3IpamSubnet, self).__init__(
subnet_id, nsx_pool_id, ctx, tenant_id)
self.nsxlib_ipam = resources.IpPool(
self.get_core_plugin().nsxlib.client)
def backend_allocate(self, address_request):
try:
# allocate a specific IP
if isinstance(address_request, ipam_req.SpecificAddressRequest):
# This handles both specific and automatic address requests
ip_address = str(address_request.address)
# If this is the subnet gateway IP - no need to allocate it
subnet = self.get_details()
if str(subnet.gateway_ip) == ip_address:
LOG.info(_LI("Skip allocation of gateway-ip for pool %s"),
self._nsx_pool_id)
return ip_address
else:
# Allocate any free IP
ip_address = None
response = self.nsxlib_ipam.allocate(self._nsx_pool_id,
ip_addr=ip_address)
ip_address = response['allocation_id']
except nsx_lib_exc.ManagerError as e:
LOG.error(_LE("NSX IPAM failed to allocate ip %(ip)s of subnet "
"%(id)s:"
" %(e)s; code %(code)s"),
{'e': e,
'ip': ip_address,
'id': self._subnet_id,
'code': e.error_code})
# Currently the backend does not support allocation of specific IPs
# When this support is added we should handle allocation errors.
if e.error_code == error.ERR_CODE_IPAM_POOL_EXHAUSTED:
# No more IP addresses available on the pool
raise ipam_exc.IpAddressGenerationFailure(
subnet_id=self._subnet_id)
if e.error_code == error.ERR_CODE_IPAM_SPECIFIC_IP:
msg = (_("NSX-V3 IPAM driver does not support allocation of a "
"specific ip %s for port") % ip_address)
raise NotImplementedError(msg)
if e.error_code == error.ERR_CODE_OBJECT_NOT_FOUND:
msg = (_("NSX-V3 IPAM failed to allocate: pool %s was not "
"found") % self._nsx_pool_id)
raise ipam_exc.IpamValueInvalid(message=msg)
else:
# another backend error
raise ipam_exc.IPAllocationFailed()
except Exception as e:
LOG.error(_LE("NSX IPAM failed to allocate ip %(ip)s of subnet "
"%(id)s:"
" %(e)s"),
{'e': e,
'ip': ip_address,
'id': self._subnet_id})
# handle unexpected failures
raise ipam_exc.IPAllocationFailed()
return ip_address
def backend_deallocate(self, address):
try:
self.nsxlib_ipam.release(self._nsx_pool_id, ip_addr=address)
except nsx_lib_exc.ManagerError as e:
# fail silently
LOG.error(_LE("NSX IPAM failed to free ip %(ip)s of subnet "
"%(id)s:"
" %(e)s; code %(code)s"),
{'e': e,
'ip': address,
'id': self._subnet_id,
'code': e.error_code})
def get_details(self):
"""Return subnet data as a SpecificSubnetRequest"""
# get the pool from the backend
try:
pool_details = self.nsxlib_ipam.get(self._nsx_pool_id)
except Exception as e:
msg = _('Failed to get details for nsx pool: %(id)s: '
'%(e)s') % {'id': self._nsx_pool_id, 'e': e}
raise ipam_exc.IpamValueInvalid(message=msg)
first_range = pool_details.get('subnets', [None])[0]
if not first_range:
msg = _('Failed to get details for nsx pool: %(id)s') % {
'id': self._nsx_pool_id}
raise ipam_exc.IpamValueInvalid(message=msg)
cidr = first_range.get('cidr')
gateway_ip = first_range.get('gateway_ip')
pools = []
for subnet in pool_details.get('subnets', []):
for ip_range in subnet.get('allocation_ranges', []):
pools.append(netaddr.IPRange(ip_range.get('start'),
ip_range.get('end')))
return ipam_req.SpecificSubnetRequest(
self._tenant_id, self._subnet_id,
cidr, gateway_ip=gateway_ip, allocation_pools=pools)

View File

@ -0,0 +1,220 @@
# Copyright 2016 VMware, 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
import netaddr
from oslo_config import cfg
from oslo_utils import uuidutils
from vmware_nsx.tests.unit.nsx_v3 import test_plugin
from vmware_nsxlib.v3 import exceptions as nsx_lib_exc
from vmware_nsxlib.v3 import nsx_constants as error
class MockIPPools(object):
def patch_nsxlib_ipam(self):
self.nsx_pools = {}
def _create_pool(*args, **kwargs):
pool_id = uuidutils.generate_uuid()
gateway_ip = None
if kwargs.get('gateway_ip'):
gateway_ip = str(kwargs['gateway_ip'])
subnet = {"allocation_ranges": kwargs.get('ranges'),
"gateway_ip": gateway_ip,
"cidr": args[0]}
pool = {'id': pool_id,
'subnets': [subnet]}
self.nsx_pools[pool_id] = {'pool': pool, 'allocated': []}
return {'id': pool_id}
def _delete_pool(pool_id):
del self.nsx_pools[pool_id]
def _get_pool(pool_id):
return self.nsx_pools[pool_id]['pool']
def _allocate_ip(*args, **kwargs):
#TODO(asarfaty): add support for specific ip allocation
if kwargs.get('ip_addr'):
raise nsx_lib_exc.ManagerError(
manager='dummy', operation='allocate',
details='allocating specific IP is not supported',
error_code=error.ERR_CODE_IPAM_SPECIFIC_IP)
nsx_pool = self.nsx_pools[args[0]]
# get an unused ip from the pool
ranges = nsx_pool['pool']['subnets'][0]['allocation_ranges']
for ip_range in ranges:
r = netaddr.IPRange(ip_range['start'], ip_range['end'])
for ip_addr in r:
if ip_addr not in nsx_pool['allocated']:
nsx_pool['allocated'].append(ip_addr)
return {'allocation_id': str(ip_addr)}
# no IP was found
raise nsx_lib_exc.ManagerError(
manager='dummy', operation='allocate',
details='All IPs in the pool are allocated',
error_code=error.ERR_CODE_IPAM_POOL_EXHAUSTED)
mock.patch(
"vmware_nsxlib.v3.resources.IpPool.get",
side_effect=_get_pool).start()
mock.patch(
"vmware_nsxlib.v3.resources.IpPool.create",
side_effect=_create_pool).start()
mock.patch(
"vmware_nsxlib.v3.resources.IpPool.delete",
side_effect=_delete_pool).start()
mock.patch(
"vmware_nsxlib.v3.resources.IpPool.allocate",
side_effect=_allocate_ip).start()
mock.patch(
"vmware_nsxlib.v3.resources.IpPool.release").start()
class TestNsxv3IpamSubnets(test_plugin.TestSubnetsV2, MockIPPools):
"""Run the nsxv3 plugin subnets tests with the ipam driver."""
def setUp(self):
cfg.CONF.set_override(
"ipam_driver",
"vmware_nsx.services.ipam.nsx_v3.driver.Nsxv3IpamDriver")
super(TestNsxv3IpamSubnets, self).setUp()
self.patch_nsxlib_ipam()
def test_update_subnet_from_gw_to_new_gw(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_subnet_gw_outside_cidr_returns_200(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_subnet_from_gw_to_no_gw(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_subnet_allocation_pools(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_subnet_allocation_pools_and_gateway_ip(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_subnet_gw_ip_in_use_by_router_returns_409(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_subnet_from_no_gw_to_no_gw(self):
self.skipTest('Update ipam subnet is not supported')
def _test_subnet_update_ipv4_and_ipv6_pd_subnets(self, ra_addr_mode):
self.skipTest('Update ipam subnet is not supported')
def test_subnet_with_allocation_range(self):
self.skipTest('Allocating a specific IP is not supported')
def test_delete_subnet_ipv6_slaac_port_exists(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_subnet_ipv6_slaac_with_port_on_network(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_subnet_ipv6_slaac_with_dhcp_port_on_network(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_subnet_dhcpv6_stateless_with_port_on_network(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_subnet_ipv6_slaac_with_port_not_found(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_subnet_ipv6_slaac_with_db_reference_error(self):
self.skipTest('Allocating a specific IP is not supported')
class TestNsxv3IpamPorts(test_plugin.TestPortsV2, MockIPPools):
"""Run the nsxv3 plugin ports tests with the ipam driver."""
def setUp(self):
cfg.CONF.set_override(
"ipam_driver",
"vmware_nsx.services.ipam.nsx_v3.driver.Nsxv3IpamDriver")
super(TestNsxv3IpamPorts, self).setUp()
self.patch_nsxlib_ipam()
def test_update_port_mac_v6_slaac(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_port_with_multiple_ipv4_and_ipv6_subnets(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_port_with_ipv6_slaac_subnet_in_fixed_ips(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_port_with_ipv6_pd_subnet_in_fixed_ips(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_port_anticipating_allocation(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_port_with_ipv6_slaac_subnet_in_fixed_ips(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_port_mac_ip(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_port_update_ips(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_port_excluding_ipv6_slaac_subnet_from_fixed_ips(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_port_update_ip(self):
self.skipTest('Allocating a specific IP is not supported')
def test_requested_subnet_id_v6_slaac(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_port_invalid_fixed_ip_address_v6_slaac(self):
self.skipTest('Allocating a specific IP is not supported')
def test_update_dhcp_port_with_exceeding_fixed_ips(self):
self.skipTest('Allocating a specific IP is not supported')
def test_requested_subnet_id_v4_and_v6_slaac(self):
self.skipTest('Allocating a specific IP is not supported')
def test_requested_ips_only(self):
self.skipTest('Allocating a specific IP is not supported')
def test_ip_allocation_for_ipv6_subnet_slaac_address_mode(self):
self.skipTest('Allocating a specific IP is not supported')
def test_ip_allocation_for_ipv6_2_subnet_slaac_mode(self):
self.skipTest('Allocating a specific IP is not supported')
def test_delete_port_with_ipv6_slaac_address(self):
self.skipTest('Allocating a specific IP is not supported')
def test_requested_duplicate_ip(self):
self.skipTest('Allocating a specific IP is not supported')
def test_create_port_invalid_fixed_ip_address_v6_pd_slaac(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_port_invalid_subnet_v6_pd_slaac(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_port_update_ip_address_only(self):
self.skipTest('Update ipam subnet is not supported')
def test_update_port_invalid_fixed_ip_address_v6_pd_slaac(self):
self.skipTest('Update ipam subnet is not supported')