Merge "NSX|v IPAM support for external & provider networks"
This commit is contained in:
commit
8294215cb4
@ -1 +1 @@
|
||||
1b4eaffe4f31
|
||||
6e6da8296c0e
|
||||
|
@ -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'),
|
||||
)
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
0
vmware_nsx/services/ipam/__init__.py
Normal file
0
vmware_nsx/services/ipam/__init__.py
Normal file
13
vmware_nsx/services/ipam/nsx_v/README.rst
Normal file
13
vmware_nsx/services/ipam/nsx_v/README.rst
Normal file
@ -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``
|
0
vmware_nsx/services/ipam/nsx_v/__init__.py
Normal file
0
vmware_nsx/services/ipam/nsx_v/__init__.py
Normal file
350
vmware_nsx/services/ipam/nsx_v/driver.py
Normal file
350
vmware_nsx/services/ipam/nsx_v/driver.py
Normal file
@ -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
|
@ -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 = (
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||
"<allocatedIpAddress><id>%(id)s</id>"
|
||||
"<ipAddress>%(ip)s</ipAddress>"
|
||||
"<gateway>%(gateway)s</gateway>"
|
||||
"<prefixLength>%(prefix)s</prefixLength>"
|
||||
"<subnetId>subnet-44</subnetId></allocatedIpAddress>")
|
||||
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)
|
||||
|
0
vmware_nsx/tests/unit/services/ipam/__init__.py
Normal file
0
vmware_nsx/tests/unit/services/ipam/__init__.py
Normal file
114
vmware_nsx/tests/unit/services/ipam/test_nsxv_driver.py
Normal file
114
vmware_nsx/tests/unit/services/ipam/test_nsxv_driver.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user