diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index 985e6ce83f..68077efd51 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -1b4eaffe4f31 +6e6da8296c0e diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/newton/expand/6e6da8296c0e_add_nsxv_ipam.py b/vmware_nsx/db/migration/alembic_migrations/versions/newton/expand/6e6da8296c0e_add_nsxv_ipam.py new file mode 100644 index 0000000000..3d877632da --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/newton/expand/6e6da8296c0e_add_nsxv_ipam.py @@ -0,0 +1,39 @@ +# 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. + +"""Add support for IPAM in NSXv + +Revision ID: 6e6da8296c0e +Revises: 1b4eaffe4f31 +Create Date: 2016-09-01 10:17:16.770021 + +""" + +revision = '6e6da8296c0e' +down_revision = '1b4eaffe4f31' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'nsxv_subnet_ipam', + sa.Column('subnet_id', sa.String(length=36), nullable=False), + sa.Column('nsx_pool_id', sa.String(length=36), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('subnet_id'), + sa.PrimaryKeyConstraint('nsx_pool_id'), + ) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index 4ec9333f1a..e7477b2672 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -796,3 +796,28 @@ 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 be0cf03976..1cd643bd6a 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -347,3 +347,12 @@ 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/plugins/nsx_v/vshield/common/constants.py b/vmware_nsx/plugins/nsx_v/vshield/common/constants.py index 11f2b14c19..4401246166 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/common/constants.py +++ b/vmware_nsx/plugins/nsx_v/vshield/common/constants.py @@ -54,6 +54,9 @@ NSX_ERROR_DHCP_OVERLAPPING_IP = 12501 NSX_ERROR_DHCP_DUPLICATE_HOSTNAME = 12504 NSX_ERROR_DHCP_DUPLICATE_MAC = 12518 +NSX_ERROR_IPAM_ALLOCATE_ALL_USED = 120051 +NSX_ERROR_IPAM_ALLOCATE_IP_USED = 120056 + SUFFIX_LENGTH = 8 #Edge size diff --git a/vmware_nsx/plugins/nsx_v/vshield/vcns.py b/vmware_nsx/plugins/nsx_v/vshield/vcns.py index 7036f0edf3..12e3483af6 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/vcns.py +++ b/vmware_nsx/plugins/nsx_v/vshield/vcns.py @@ -73,6 +73,10 @@ SYSCTL_SERVICE = 'systemcontrol/config' # L2 gateway constants BRIDGE = "bridging/config" +# IPAM constants +IPAM_POOL_SCOPE = "scope/globalroot-0" +IPAM_POOL_SERVICE = "ipam/pools" + # Self Signed Certificate constants CSR = "csr" CERTIFICATE = "certificate" @@ -949,3 +953,33 @@ class Vcns(object): profile_id, 'binding') return self.do_request(HTTP_POST, profiles_uri, request, format='xml', decode=False) + + def create_ipam_ip_pool(self, request): + uri = '%s/%s/%s' % (SERVICES_PREFIX, IPAM_POOL_SERVICE, + IPAM_POOL_SCOPE) + return self.do_request(HTTP_POST, uri, request, format='xml', + decode=False) + + def delete_ipam_ip_pool(self, pool_id): + uri = '%s/%s/%s' % (SERVICES_PREFIX, IPAM_POOL_SERVICE, pool_id) + return self.do_request(HTTP_DELETE, uri) + + def get_ipam_ip_pool(self, pool_id): + uri = '%s/%s/%s' % (SERVICES_PREFIX, IPAM_POOL_SERVICE, pool_id) + return self.do_request(HTTP_GET, uri, decode=True) + + def allocate_ipam_ip_from_pool(self, pool_id, ip_addr=None): + uri = '%s/%s/%s/%s' % (SERVICES_PREFIX, IPAM_POOL_SERVICE, pool_id, + 'ipaddresses') + if ip_addr: + request = {'ipAddressRequest': {'allocationMode': 'RESERVE', + 'ipAddress': ip_addr}} + else: + request = {'ipAddressRequest': {'allocationMode': 'ALLOCATE'}} + return self.do_request(HTTP_POST, uri, request, format='xml', + decode=False) + + def release_ipam_ip_to_pool(self, pool_id, ip_addr): + uri = '%s/%s/%s/%s/%s' % (SERVICES_PREFIX, IPAM_POOL_SERVICE, pool_id, + 'ipaddresses', ip_addr) + return self.do_request(HTTP_DELETE, uri) diff --git a/vmware_nsx/services/ipam/__init__.py b/vmware_nsx/services/ipam/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/ipam/nsx_v/README.rst b/vmware_nsx/services/ipam/nsx_v/README.rst new file mode 100644 index 0000000000..47be1330ed --- /dev/null +++ b/vmware_nsx/services/ipam/nsx_v/README.rst @@ -0,0 +1,13 @@ +================================================================ + Enabling NSXv IPAM for external & provider networks in Devstack +================================================================ + +1. Download DevStack + +2. Update the ``local.conf`` file:: + + [[post-config|$NEUTRON_CONF]] + [DEFAULT] + ipam_driver = vmware_nsxv_ipam + +3. run ``stack.sh`` diff --git a/vmware_nsx/services/ipam/nsx_v/__init__.py b/vmware_nsx/services/ipam/nsx_v/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/ipam/nsx_v/driver.py b/vmware_nsx/services/ipam/nsx_v/driver.py new file mode 100644 index 0000000000..5fa312ae98 --- /dev/null +++ b/vmware_nsx/services/ipam/nsx_v/driver.py @@ -0,0 +1,350 @@ +# 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 +import xml.etree.ElementTree as et + +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 import manager +from neutron_lib.api import validators +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 + +LOG = logging.getLogger(__name__) + + +class NsxvIpamBase(object): + @classmethod + def get_core_plugin(cls): + return manager.NeutronManager.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 + 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): + """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 + """ + network_id = subnet_request.network_id + if network_id: + network = self._fetch_network(self._context, network_id) + if network.get(ext_net_extn.EXTERNAL): + # external network + return True + if (validators.is_attr_set(network.get(mpnet.SEGMENTS)) or + validators.is_attr_set(network.get(pnet.NETWORK_TYPE))): + # provider network + return True + + return False + + def _is_ipv6_subnet(self, subnet_request): + """Return True if the network of the request is an ipv6 network""" + if isinstance(subnet_request, ipam_req.SpecificSubnetRequest): + return subnet_request.subnet_cidr.version == 6 + else: + if subnet_request.allocation_pools: + for pool in subnet_request.allocation_pools: + if pool.version == 6: + return True + return False + + def _is_supported_net(self, subnet_request): + """This driver supports only ipv4 external/provider networks""" + 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) + + def allocate_backend_pool(self, subnet_request): + """Create a pool on the NSX backend and return its ID""" + if subnet_request.allocation_pools: + ranges = [ + {'ipRangeDto': + {'startAddress': netaddr.IPAddress(pool.first), + 'endAddress': netaddr.IPAddress(pool.last)}} + for pool in subnet_request.allocation_pools] + else: + ranges = [] + + request = {'ipamAddressPool': + # max name length on backend is 255, so there is no problem here + {'name': 'subnet_' + subnet_request.subnet_id, + 'prefixLength': subnet_request.prefixlen, + 'gateway': subnet_request.gateway_ip, + 'ipRanges': ranges}} + + try: + response = self._vcns.create_ipam_ip_pool(request) + nsx_pool_id = response[1] + except vc_exc.VcnsApiException as e: + msg = _('Failed to create subnet IPAM: %s') % e + raise ipam_exc.IpamValueInvalid(message=msg) + + 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) + + +class NsxvIpamSubnet(ipam_base.Subnet, NsxvIpamBase): + """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""" + with locking.LockManager.get_lock('nsx-ipam-' + self._nsx_pool_id): + return self._allocate(address_request) + + def _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) + self._vcns.allocate_ipam_ip_from_pool(self._nsx_pool_id, + ip_addr=ip_address) + else: + # Allocate any free IP + response = self._vcns.allocate_ipam_ip_from_pool( + self._nsx_pool_id)[1] + # get the ip from the response + root = et.fromstring(response) + ip_address = root.find('ipAddress').text + except vc_exc.VcnsApiException as e: + # handle backend failures + error_code = self._get_vcns_error_code(e) + if error_code == constants.NSX_ERROR_IPAM_ALLOCATE_IP_USED: + # This IP is already in use + raise ipam_exc.IpAddressAlreadyAllocated( + ip=ip_address, subnet_id=self._subnet_id) + if error_code == constants.NSX_ERROR_IPAM_ALLOCATE_ALL_USED: + # No more IP addresses available on the pool + raise ipam_exc.IpAddressGenerationFailure( + subnet_id=self._subnet_id) + else: + 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): + try: + self._vcns.release_ipam_ip_to_pool(self._nsx_pool_id, address) + except vc_exc.VcnsApiException as e: + LOG.error(_LE("NSX IPAM failed to free ip %(ip)s of subnet %(id):" + " %(e)S"), + {'e': e.response, + 'ip': address, + 'id': self._subnet_id}) + raise ipam_exc.IpAddressAllocationNotFound( + 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 + cidr = '%s/%s' % (pool['ipRanges'][0]['startAddress'], + pool['prefixLength']) + # convert to a proper cidr + cidr = netaddr.IPNetwork(cidr).cidr + return str(cidr) + + def get_details(self): + """Return subnet data as a SpecificSubnetRequest""" + # get the pool from the backend + pool_details = self._vcns.get_ipam_ip_pool(self._nsx_pool_id)[1] + gateway_ip = pool_details['gateway'] + # rebuild the cidr from the range & prefix + cidr = self._get_pool_cidr(pool_details) + pools = [] + for ip_range in pool_details['ipRanges']: + pools.append(netaddr.IPRange(ip_range['startAddress'], + ip_range['endAddress'])) + + 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/tests/unit/nsx_v/vshield/fake_vcns.py b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py index f21ffba9f5..59cf359230 100644 --- a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py +++ b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py @@ -13,6 +13,7 @@ # under the License. import copy +import netaddr from oslo_serialization import jsonutils from oslo_utils import uuidutils @@ -20,6 +21,7 @@ import six import xml.etree.ElementTree as ET from vmware_nsx._i18n import _ +from vmware_nsx.plugins.nsx_v.vshield.common import constants from vmware_nsx.plugins.nsx_v.vshield.common import exceptions SECTION_LOCATION_HEADER = '/api/4.0/firewall/globalroot-0/config/%s/%s' @@ -68,6 +70,7 @@ class FakeVcns(object): self._sections = {'section_ids': 0, 'rule_ids': 0, 'names': set()} self._dhcp_bindings = {} self._spoofguard_policies = [] + self._ipam_pools = {} def set_fake_nsx_api(self, fake_nsx_api): self._fake_nsx_api = fake_nsx_api @@ -1121,6 +1124,7 @@ class FakeVcns(object): self._securitygroups = {'ids': 0, 'names': set()} self._sections = {'section_ids': 0, 'rule_ids': 0, 'names': set()} self._dhcp_bindings = {} + self._ipam_pools = {} def validate_datacenter_moid(self, object_id): return True @@ -1207,3 +1211,133 @@ class FakeVcns(object): response = '' headers = {'status': 200} return (headers, response) + + def create_ipam_ip_pool(self, request): + pool_id = uuidutils.generate_uuid() + # format the request before saving it: + fixed_request = request['ipamAddressPool'] + ranges = fixed_request['ipRanges'] + for i in range(len(ranges)): + ranges[i] = ranges[i]['ipRangeDto'] + self._ipam_pools[pool_id] = {'request': fixed_request, + 'allocated': []} + header = {'status': 200} + response = pool_id + return (header, response) + + def delete_ipam_ip_pool(self, pool_id): + response = '' + if pool_id in self._ipam_pools: + pool = self._ipam_pools.pop(pool_id) + if len(pool['allocated']) > 0: + header = {'status': 400} + msg = ("Unable to delete IP pool %s. IP addresses from this " + "pool are being used." % pool_id) + response = self._get_bad_req_response( + msg, 120053, 'core-services') + else: + header = {'status': 200} + return (header, response) + else: + header = {'status': 400} + msg = ("Unable to delete IP pool %s. Pool does not exist." % + pool_id) + response = self._get_bad_req_response( + msg, 120054, 'core-services') + return self.return_helper(header, response) + + def get_ipam_ip_pool(self, pool_id): + if pool_id in self._ipam_pools: + header = {'status': 200} + response = self._ipam_pools[pool_id]['request'] + else: + header = {'status': 400} + msg = ("Unable to retrieve IP pool %s. Pool does not exist." % + pool_id) + response = self._get_bad_req_response( + msg, 120054, 'core-services') + return self.return_helper(header, response) + + def _allocate_ipam_add_ip_and_return(self, pool, ip_addr): + # build the response + response_text = ( + "" + "%(id)s" + "%(ip)s" + "%(gateway)s" + "%(prefix)s" + "subnet-44") + response_args = {'id': len(pool['allocated']), + 'gateway': pool['request']['gateway'], + 'prefix': pool['request']['prefixLength']} + + response_args['ip'] = ip_addr + response = response_text % response_args + + # add the ip to the list of allocated ips + pool['allocated'].append(ip_addr) + + header = {'status': 200} + return (header, response) + + def allocate_ipam_ip_from_pool(self, pool_id, ip_addr=None): + if pool_id in self._ipam_pools: + pool = self._ipam_pools[pool_id] + if ip_addr: + # verify that this ip was not yet allocated + if ip_addr in pool['allocated']: + header = {'status': 400} + msg = ("Unable to allocate IP from pool %(pool)s. " + "IP %(ip)s already in use." % + {'pool': pool_id, 'ip': ip_addr}) + response = self._get_bad_req_response( + msg, constants.NSX_ERROR_IPAM_ALLOCATE_IP_USED, + 'core-services') + else: + return self._allocate_ipam_add_ip_and_return( + pool, ip_addr) + else: + # get an unused ip from the pool + for ip_range in pool['request']['ipRanges']: + r = netaddr.IPRange(ip_range['startAddress'], + ip_range['endAddress']) + for ip_addr in r: + if ip_addr not in pool['allocated']: + return self._allocate_ipam_add_ip_and_return( + pool, ip_addr) + # if we got here - no ip was found + header = {'status': 400} + msg = ("Unable to allocate IP from pool %(pool)s. " + "All IPs have been used." % + {'pool': pool_id}) + response = self._get_bad_req_response( + msg, constants.NSX_ERROR_IPAM_ALLOCATE_ALL_USED, + 'core-services') + else: + header = {'status': 400} + msg = ("Unable to allocate IP from pool %s. Pool does not " + "exist." % pool_id) + response = self._get_bad_req_response( + msg, 120054, 'core-services') + return self.return_helper(header, response) + + def release_ipam_ip_to_pool(self, pool_id, ip_addr): + if pool_id in self._ipam_pools: + pool = self._ipam_pools[pool_id] + if ip_addr not in pool['allocated']: + header = {'status': 400} + msg = ("IP %(ip)s was not allocated from pool %(pool)s." % + {'ip': ip_addr, 'pool': pool_id}) + response = self._get_bad_req_response( + msg, 120056, 'core-services') + else: + pool.remove(ip_addr) + response = '' + header = {'status': 200} + else: + header = {'status': 400} + msg = ("Unable to release IP to pool %s. Pool does not exist." % + pool_id) + response = self._get_bad_req_response( + msg, 120054, 'core-services') + return self.return_helper(header, response) diff --git a/vmware_nsx/tests/unit/services/ipam/__init__.py b/vmware_nsx/tests/unit/services/ipam/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/tests/unit/services/ipam/test_nsxv_driver.py b/vmware_nsx/tests/unit/services/ipam/test_nsxv_driver.py new file mode 100644 index 0000000000..b8315568a2 --- /dev/null +++ b/vmware_nsx/tests/unit/services/ipam/test_nsxv_driver.py @@ -0,0 +1,114 @@ +# 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. +from oslo_config import cfg + +from vmware_nsx.tests.unit.nsx_v import test_plugin + +from neutron.extensions import providernet as pnet + + +class TestNsxvIpamSubnets(test_plugin.TestSubnetsV2): + """Run the nsxv plugin subnets tests with the ipam driver""" + def setUp(self): + cfg.CONF.set_override( + "ipam_driver", + "vmware_nsx.services.ipam.nsx_v.driver.NsxvIpamDriver") + super(TestNsxvIpamSubnets, self).setUp() + + def provider_net(self): + name = 'dvs-provider-net' + providernet_args = {pnet.NETWORK_TYPE: 'vlan', + pnet.SEGMENTATION_ID: 43, + pnet.PHYSICAL_NETWORK: 'dvs-uuid'} + return self.network(name=name, do_delete=False, + providernet_args=providernet_args, + arg_list=(pnet.NETWORK_TYPE, + pnet.SEGMENTATION_ID, + pnet.PHYSICAL_NETWORK)) + + def test_provider_net_use_driver(self): + with self.provider_net() as net: + before = len(self.fc2._ipam_pools) + with self.subnet(network=net, cidr='10.10.10.0/29', + enable_dhcp=False): + self.assertEqual(before + 1, len(self.fc2._ipam_pools)) + + def test_ext_net_use_driver(self): + with self.network(router__external=True) as net: + before = len(self.fc2._ipam_pools) + with self.subnet(network=net, cidr='10.10.10.0/29', + enable_dhcp=False): + self.assertEqual(before + 1, len(self.fc2._ipam_pools)) + + def test_regular_net_dont_use_driver(self): + with self.network() as net: + before = len(self.fc2._ipam_pools) + with self.subnet(network=net, cidr='10.10.10.0/29', + enable_dhcp=False): + self.assertEqual(before, len(self.fc2._ipam_pools)) + + def test_no_more_ips(self): + # create a small provider network, and use all the IPs + with self.provider_net() as net: + with self.subnet(network=net, cidr='10.10.10.0/29', + enable_dhcp=False) as subnet: + # create ports on this subnet until there are no more free ips + # legal ips are 10.10.10.2 - 10.10.10.6 + fixed_ips = [{'subnet_id': subnet['subnet']['id']}] + for counter in range(5): + port_res = self._create_port( + self.fmt, net['network']['id'], fixed_ips=fixed_ips) + port = self.deserialize('json', port_res) + self.assertIn('port', port) + + # try to create another one - should fail + port_res = self._create_port( + self.fmt, net['network']['id'], fixed_ips=fixed_ips) + port = self.deserialize('json', port_res) + self.assertIn('NeutronError', port) + self.assertIn('message', port['NeutronError']) + self.assertTrue(('No more IP addresses available' in + port['NeutronError']['message'])) + + def test_use_same_ips(self): + # create a provider network and try to allocate the same ip twice + with self.provider_net() as net: + with self.subnet(network=net, cidr='10.10.10.0/24', + enable_dhcp=False) as subnet: + fixed_ips = [{'ip_address': '10.10.10.2', + 'subnet_id': subnet['subnet']['id']}] + # First port should succeed + port_res = self._create_port( + self.fmt, net['network']['id'], fixed_ips=fixed_ips) + port = self.deserialize('json', port_res) + self.assertIn('port', port) + + # try to create another one - should fail + port_res = self._create_port( + self.fmt, net['network']['id'], fixed_ips=fixed_ips) + port = self.deserialize('json', port_res) + self.assertIn('NeutronError', port) + self.assertIn('message', port['NeutronError']) + self.assertTrue(('already allocated in subnet' in + port['NeutronError']['message'])) + + +class TestNsxvIpamPorts(test_plugin.TestPortsV2): + """Run the nsxv plugin ports tests with the ipam driver""" + def setUp(self): + cfg.CONF.set_override( + "ipam_driver", + "vmware_nsx.services.ipam.nsx_v.driver.NsxvIpamDriver") + super(TestNsxvIpamPorts, self).setUp()