vmware-nsx/vmware_nsx/services/ipam/nsx_v3/driver.py

244 lines
10 KiB
Python

# 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 _
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
LOG = logging.getLogger(__name__)
class Nsxv3IpamDriver(common.NsxAbstractIpamDriver):
"""IPAM Driver For NSX-V3 networks."""
def __init__(self, subnetpool, context):
super(Nsxv3IpamDriver, self).__init__(subnetpool, context)
self.nsxlib_ipam = self._nsxlib.ip_pool
# Mark which updates to the pool are supported
self.support_update_gateway = True
self.support_update_pools = True
@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 _get_ranges_from_request(self, subnet_request):
if subnet_request.allocation_pools:
ranges = [
{'start': str(pool[0]), 'end': str(pool[-1])}
for pool in subnet_request.allocation_pools]
else:
ranges = []
return ranges
def _is_supported_net(self, subnet_request):
"""This driver doesn't support multicast cidrs"""
if not hasattr(subnet_request, "subnet_cidr"):
return True
net = netaddr.IPNetwork(subnet_request.subnet_cidr[0])
return not net.is_multicast()
def allocate_backend_pool(self, subnet_request):
"""Create a pool on the NSX backend and return its ID"""
# 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),
allocation_ranges=self._get_ranges_from_request(
subnet_request),
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("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("Failed to delete IPAM from backend: %s", e)
# Continue anyway, since this subnet was already removed
def update_backend_pool(self, nsx_pool_id, subnet_request):
update_args = {
'cidr': self._get_cidr_from_request(subnet_request),
'allocation_ranges': self._get_ranges_from_request(subnet_request),
'gateway_ip': subnet_request.gateway_ip}
try:
self.nsxlib_ipam.update(
nsx_pool_id, **update_args)
except nsx_lib_exc.ManagerError as e:
LOG.error("NSX IPAM failed to update pool %(id)s: "
" %(e)s; code %(code)s",
{'e': e,
'id': nsx_pool_id,
'code': e.error_code})
if (e.error_code == error.ERR_CODE_IPAM_RANGE_MODIFY or
e.error_code == error.ERR_CODE_IPAM_RANGE_DELETE or
e.error_code == error.ERR_CODE_IPAM_RANGE_SHRUNK):
# The change is not allowed: already allocated IPs out of
# the new range
raise ipam_exc.InvalidSubnetRequest(
reason=_("Already allocated IPs outside of the updated "
"pools"))
except Exception as e:
# unexpected error
msg = _('Failed to update subnet IPAM: %s') % e
raise ipam_exc.IpamValueInvalid(message=msg)
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 = self._nsxlib.ip_pool
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("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("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})
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:
# The NSX backend does not support allocation of specific IPs
# prior to version 2.0.
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_IPAM_IP_ALLOCATED:
# This IP is already in use
raise ipam_exc.IpAddressAlreadyAllocated(
ip=ip_address, subnet_id=self._subnet_id)
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)
# another backend error
raise ipam_exc.IPAllocationFailed()
except Exception as e:
LOG.error("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, ip_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("Skip deallocation of gateway-ip for pool %s",
self._nsx_pool_id)
return
try:
self.nsxlib_ipam.release(self._nsx_pool_id, ip_address)
except nsx_lib_exc.ManagerError as e:
# fail silently
LOG.error("NSX IPAM failed to free 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})
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)