From 1266099049a900242c320f741c96da458578faee Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Sun, 11 Dec 2016 10:13:21 +0200 Subject: [PATCH] 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 --- .../nsxv3-ipam-support-137174152c65459d.yaml | 11 + setup.cfg | 1 + vmware_nsx/db/db.py | 25 ++ .../alembic_migrations/versions/CONTRACT_HEAD | 2 +- .../5c8f451290b7_nsx_ipam_table_rename.py | 33 +++ vmware_nsx/db/nsx_models.py | 9 + vmware_nsx/db/nsxv_db.py | 25 -- vmware_nsx/db/nsxv_models.py | 9 - vmware_nsx/services/ipam/common/__init__.py | 0 vmware_nsx/services/ipam/common/driver.py | 220 ++++++++++++++++++ vmware_nsx/services/ipam/nsx_v/driver.py | 204 +++------------- vmware_nsx/services/ipam/nsx_v3/README.rst | 13 ++ vmware_nsx/services/ipam/nsx_v3/__init__.py | 0 vmware_nsx/services/ipam/nsx_v3/driver.py | 200 ++++++++++++++++ .../unit/services/ipam/test_nsxv3_driver.py | 220 ++++++++++++++++++ 15 files changed, 759 insertions(+), 213 deletions(-) create mode 100644 releasenotes/notes/nsxv3-ipam-support-137174152c65459d.yaml create mode 100644 vmware_nsx/db/migration/alembic_migrations/versions/ocata/contract/5c8f451290b7_nsx_ipam_table_rename.py create mode 100644 vmware_nsx/services/ipam/common/__init__.py create mode 100644 vmware_nsx/services/ipam/common/driver.py create mode 100644 vmware_nsx/services/ipam/nsx_v3/README.rst create mode 100644 vmware_nsx/services/ipam/nsx_v3/__init__.py create mode 100644 vmware_nsx/services/ipam/nsx_v3/driver.py create mode 100644 vmware_nsx/tests/unit/services/ipam/test_nsxv3_driver.py diff --git a/releasenotes/notes/nsxv3-ipam-support-137174152c65459d.yaml b/releasenotes/notes/nsxv3-ipam-support-137174152c65459d.yaml new file mode 100644 index 0000000000..4135710c02 --- /dev/null +++ b/releasenotes/notes/nsxv3-ipam-support-137174152c65459d.yaml @@ -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. diff --git a/setup.cfg b/setup.cfg index 94ca88af2e..b1608cbe60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ neutron.qos.notification_drivers = vmware_nsxv3_message_queue = vmware_nsx.services.qos.nsx_v3.message_queue:NsxV3QosNotificationDriver neutron.ipam_drivers = 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 = shared = vmware_nsx.plugins.nsx_v.drivers.shared_router_driver:RouterSharedDriver distributed = vmware_nsx.plugins.nsx_v.drivers.distributed_router_driver:RouterDistributedDriver diff --git a/vmware_nsx/db/db.py b/vmware_nsx/db/db.py index fc54bc858d..a4a550b2b9 100644 --- a/vmware_nsx/db/db.py +++ b/vmware_nsx/db/db.py @@ -423,3 +423,28 @@ def save_sg_rule_mappings(session, rules): mapping = nsx_models.NeutronNsxRuleMapping( neutron_id=neutron_id, nsx_id=nsx_id) 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()) diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD index 11128373d4..da53a12ef3 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD @@ -1 +1 @@ -d49ac91b560e \ No newline at end of file +5c8f451290b7 \ No newline at end of file diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/ocata/contract/5c8f451290b7_nsx_ipam_table_rename.py b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/contract/5c8f451290b7_nsx_ipam_table_rename.py new file mode 100644 index 0000000000..690cfcdd4b --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/contract/5c8f451290b7_nsx_ipam_table_rename.py @@ -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') diff --git a/vmware_nsx/db/nsx_models.py b/vmware_nsx/db/nsx_models.py index e5f2ed6277..0a88c7c8cc 100644 --- a/vmware_nsx/db/nsx_models.py +++ b/vmware_nsx/db/nsx_models.py @@ -368,3 +368,12 @@ class NsxPortMirrorSessionMapping(model_base.BASEV2): nullable=False, primary_key=True) 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) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index 8a97edbc88..1a9298d4d2 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -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_dhcp_mtu.DHCP_MTU] = dhcp_mtu 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()) diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index 651a065d84..ac5aafa68e 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -347,12 +347,3 @@ class NsxvSubnetExtAttributes(model_base.BASEV2, models.TimestampMixin): models_v2.Subnet, backref=orm.backref("nsxv_subnet_attributes", lazy='joined', 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) diff --git a/vmware_nsx/services/ipam/common/__init__.py b/vmware_nsx/services/ipam/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/ipam/common/driver.py b/vmware_nsx/services/ipam/common/driver.py new file mode 100644 index 0000000000..cef01ddf83 --- /dev/null +++ b/vmware_nsx/services/ipam/common/driver.py @@ -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 diff --git a/vmware_nsx/services/ipam/nsx_v/driver.py b/vmware_nsx/services/ipam/nsx_v/driver.py index b641496605..270b84897b 100644 --- a/vmware_nsx/services/ipam/nsx_v/driver.py +++ b/vmware_nsx/services/ipam/nsx_v/driver.py @@ -17,66 +17,34 @@ import netaddr 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 multiprovidernet as mpnet 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 requests as ipam_req -from neutron.ipam import subnet_alloc 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.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 exceptions as vc_exc +from vmware_nsx.services.ipam.common import driver as common LOG = logging.getLogger(__name__) -class NsxvIpamBase(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 NsxVIpamBase(common.NsxIpamBase): @property def _vcns(self): p = self.get_core_plugin() 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(subnet_alloc.SubnetAllocator, NsxvIpamBase): +class NsxvIpamDriver(common.NsxAbstractIpamDriver, NsxVIpamBase): """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): """Return True if the network of the request is external or provider network @@ -110,19 +78,9 @@ class NsxvIpamDriver(subnet_alloc.SubnetAllocator, NsxvIpamBase): return (self._is_ext_or_provider_net(subnet_request) and not self._is_ipv6_subnet(subnet_request)) - def get_subnet_request_factory(self): - # override the OOB factory to add the network ID - return NsxvSubnetRequestFactory - - 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) + @property + def _subnet_class(self): + return NsxvIpamSubnet def allocate_backend_pool(self, subnet_request): """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 - 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(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) + def delete_backend_pool(self, nsx_pool_id): + 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 -class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase): - """Manage IP addresses for the NSX IPAM driver.""" +class NsxvIpamSubnet(common.NsxAbstractIpamSubnet, NsxVIpamBase): + """Manage IP addresses for the NSX-V 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 + 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) - @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""" - with locking.LockManager.get_lock('nsx-ipam-' + self._nsx_pool_id): - return self._allocate(address_request) - - def _allocate(self, address_request): + def backend_allocate(self, address_request): try: # allocate a specific IP if isinstance(address_request, ipam_req.SpecificAddressRequest): @@ -288,12 +159,7 @@ class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase): raise ipam_exc.IPAllocationFailed() return ip_address - def 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): + def backend_deallocate(self, address): try: self._vcns.release_ipam_ip_to_pool(self._nsx_pool_id, address) except vc_exc.VcnsApiException as e: @@ -306,10 +172,6 @@ class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase): subnet_id=self._subnet_id, ip_address=address) - def update_allocation_pools(self, pools, cidr): - # Not supported - pass - def _get_pool_cidr(self, pool): # rebuild the cidr from the pool range & prefix using the first # 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( self._tenant_id, self._subnet_id, 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 diff --git a/vmware_nsx/services/ipam/nsx_v3/README.rst b/vmware_nsx/services/ipam/nsx_v3/README.rst new file mode 100644 index 0000000000..5dca2a152c --- /dev/null +++ b/vmware_nsx/services/ipam/nsx_v3/README.rst @@ -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`` diff --git a/vmware_nsx/services/ipam/nsx_v3/__init__.py b/vmware_nsx/services/ipam/nsx_v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/ipam/nsx_v3/driver.py b/vmware_nsx/services/ipam/nsx_v3/driver.py new file mode 100644 index 0000000000..fffb0ba191 --- /dev/null +++ b/vmware_nsx/services/ipam/nsx_v3/driver.py @@ -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) diff --git a/vmware_nsx/tests/unit/services/ipam/test_nsxv3_driver.py b/vmware_nsx/tests/unit/services/ipam/test_nsxv3_driver.py new file mode 100644 index 0000000000..9bcd723df3 --- /dev/null +++ b/vmware_nsx/tests/unit/services/ipam/test_nsxv3_driver.py @@ -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')