Merge "Introduce External IPAM Interface"
This commit is contained in:
commit
d7f4210ee3
205
neutron/ipam/__init__.py
Normal file
205
neutron/ipam/__init__.py
Normal file
@ -0,0 +1,205 @@
|
||||
# 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 netaddr
|
||||
|
||||
import six
|
||||
|
||||
from neutron.common import constants
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class SubnetPool(object):
|
||||
"""Represents a pool of IPs available inside an address scope."""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class SubnetRequest(object):
|
||||
"""Carries the data needed to make a subnet request
|
||||
|
||||
The data validated and carried by an instance of this class is the data
|
||||
that is common to any type of request. This class shouldn't be
|
||||
instantiated on its own. Rather, a subclass of this class should be used.
|
||||
"""
|
||||
def __init__(self, tenant_id, subnet_id,
|
||||
gateway_ip=None, allocation_pools=None):
|
||||
"""Initialize and validate
|
||||
|
||||
:param tenant_id: The tenant id who will own the subnet
|
||||
:type tenant_id: str uuid
|
||||
:param subnet_id: Neutron's subnet id
|
||||
:type subnet_id: str uuid
|
||||
:param gateway_ip: An IP to reserve for the subnet gateway.
|
||||
:type gateway_ip: None or convertible to netaddr.IPAddress
|
||||
:param allocation_pools: The pool from which IPAM should allocate
|
||||
addresses. The allocator *may* allow allocating addresses outside
|
||||
of this range if specifically requested.
|
||||
:type allocation_pools: A list of netaddr.IPRange. None if not
|
||||
specified.
|
||||
"""
|
||||
self._tenant_id = tenant_id
|
||||
self._subnet_id = subnet_id
|
||||
self._gateway_ip = None
|
||||
self._allocation_pools = None
|
||||
|
||||
if gateway_ip is not None:
|
||||
self._gateway_ip = netaddr.IPAddress(gateway_ip)
|
||||
|
||||
if allocation_pools is not None:
|
||||
allocation_pools = sorted(allocation_pools)
|
||||
previous = None
|
||||
for pool in allocation_pools:
|
||||
if not isinstance(pool, netaddr.ip.IPRange):
|
||||
raise TypeError("Ranges must be netaddr.IPRange")
|
||||
if previous and pool.first <= previous.last:
|
||||
raise ValueError("Ranges must not overlap")
|
||||
previous = pool
|
||||
if 1 < len(allocation_pools):
|
||||
# Checks that all the ranges are in the same IP version.
|
||||
# IPRange sorts first by ip version so we can get by with just
|
||||
# checking the first and the last range having sorted them
|
||||
# above.
|
||||
first_version = allocation_pools[0].version
|
||||
last_version = allocation_pools[-1].version
|
||||
if first_version != last_version:
|
||||
raise ValueError("Ranges must be in the same IP version")
|
||||
self._allocation_pools = allocation_pools
|
||||
|
||||
if self.gateway_ip and self.allocation_pools:
|
||||
if self.gateway_ip.version != self.allocation_pools[0].version:
|
||||
raise ValueError("Gateway IP version inconsistent with "
|
||||
"allocation pool version")
|
||||
|
||||
@property
|
||||
def tenant_id(self):
|
||||
return self._tenant_id
|
||||
|
||||
@property
|
||||
def subnet_id(self):
|
||||
return self._subnet_id
|
||||
|
||||
@property
|
||||
def gateway_ip(self):
|
||||
return self._gateway_ip
|
||||
|
||||
@property
|
||||
def allocation_pools(self):
|
||||
return self._allocation_pools
|
||||
|
||||
def _validate_with_subnet(self, subnet):
|
||||
if self.gateway_ip:
|
||||
if self.gateway_ip not in subnet:
|
||||
raise ValueError("gateway_ip is not in the subnet")
|
||||
|
||||
if self.allocation_pools:
|
||||
if subnet.version != self.allocation_pools[0].version:
|
||||
raise ValueError("allocation_pools use the wrong ip version")
|
||||
for pool in self.allocation_pools:
|
||||
if pool not in subnet:
|
||||
raise ValueError("allocation_pools are not in the subnet")
|
||||
|
||||
|
||||
class AnySubnetRequest(SubnetRequest):
|
||||
"""A template for allocating an unspecified subnet from IPAM
|
||||
|
||||
A driver may not implement this type of request. For example, The initial
|
||||
reference implementation will not support this. The API has no way of
|
||||
creating a subnet without a specific address until subnet-allocation is
|
||||
implemented.
|
||||
"""
|
||||
WILDCARDS = {constants.IPv4: '0.0.0.0',
|
||||
constants.IPv6: '::'}
|
||||
|
||||
def __init__(self, tenant_id, subnet_id, version, prefixlen,
|
||||
gateway_ip=None, allocation_pools=None):
|
||||
"""
|
||||
:param version: Either constants.IPv4 or constants.IPv6
|
||||
:param prefixlen: The prefix len requested. Must be within the min and
|
||||
max allowed.
|
||||
:type prefixlen: int
|
||||
"""
|
||||
super(AnySubnetRequest, self).__init__(
|
||||
tenant_id=tenant_id,
|
||||
subnet_id=subnet_id,
|
||||
gateway_ip=gateway_ip,
|
||||
allocation_pools=allocation_pools)
|
||||
|
||||
net = netaddr.IPNetwork(self.WILDCARDS[version] + '/' + str(prefixlen))
|
||||
self._validate_with_subnet(net)
|
||||
|
||||
self._prefixlen = prefixlen
|
||||
|
||||
@property
|
||||
def prefixlen(self):
|
||||
return self._prefixlen
|
||||
|
||||
|
||||
class SpecificSubnetRequest(SubnetRequest):
|
||||
"""A template for allocating a specified subnet from IPAM
|
||||
|
||||
The initial reference implementation will probably just allow any
|
||||
allocation, even overlapping ones. This can be expanded on by future
|
||||
blueprints.
|
||||
"""
|
||||
def __init__(self, tenant_id, subnet_id, subnet,
|
||||
gateway_ip=None, allocation_pools=None):
|
||||
"""
|
||||
:param subnet: The subnet requested. Can be IPv4 or IPv6. However,
|
||||
when IPAM tries to fulfill this request, the IP version must match
|
||||
the version of the address scope being used.
|
||||
:type subnet: netaddr.IPNetwork or convertible to one
|
||||
"""
|
||||
super(SpecificSubnetRequest, self).__init__(
|
||||
tenant_id=tenant_id,
|
||||
subnet_id=subnet_id,
|
||||
gateway_ip=gateway_ip,
|
||||
allocation_pools=allocation_pools)
|
||||
|
||||
self._subnet = netaddr.IPNetwork(subnet)
|
||||
self._validate_with_subnet(self._subnet)
|
||||
|
||||
@property
|
||||
def subnet(self):
|
||||
return self._subnet
|
||||
|
||||
@property
|
||||
def prefixlen(self):
|
||||
return self._subnet.prefixlen
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AddressRequest(object):
|
||||
"""Abstract base class for address requests"""
|
||||
|
||||
|
||||
class SpecificAddressRequest(AddressRequest):
|
||||
"""For requesting a specified address from IPAM"""
|
||||
def __init__(self, address):
|
||||
"""
|
||||
:param address: The address being requested
|
||||
:type address: A netaddr.IPAddress or convertible to one.
|
||||
"""
|
||||
super(SpecificAddressRequest, self).__init__()
|
||||
self._address = netaddr.IPAddress(address)
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return self._address
|
||||
|
||||
|
||||
class AnyAddressRequest(AddressRequest):
|
||||
"""Used to request any available address from the pool."""
|
||||
|
||||
|
||||
class RouterGatewayAddressRequest(AddressRequest):
|
||||
"""Used to request allocating the special router gateway address."""
|
123
neutron/ipam/driver.py
Normal file
123
neutron/ipam/driver.py
Normal file
@ -0,0 +1,123 @@
|
||||
# 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
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Pool(object):
|
||||
"""Interface definition for an IPAM driver.
|
||||
|
||||
There should be an instance of the driver for every subnet pool.
|
||||
"""
|
||||
|
||||
def __init__(self, subnet_pool_id):
|
||||
"""Initialize pool
|
||||
|
||||
:param subnet_pool_id: SubnetPool ID of the address space to use.
|
||||
:type subnet_pool_id: str uuid
|
||||
"""
|
||||
self._subnet_pool_id = subnet_pool_id
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, subnet_pool_id):
|
||||
"""Returns an instance of the configured IPAM driver
|
||||
|
||||
:param subnet_pool_id: Subnet pool ID of the address space to use.
|
||||
:type subnet_pool_id: str uuid
|
||||
:returns: An instance of Driver for the given subnet pool
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def allocate_subnet(self, request):
|
||||
"""Allocates a subnet based on the subnet request
|
||||
|
||||
:param request: Describes the allocation requested.
|
||||
:type request: An instance of a sub-class of SubnetRequest
|
||||
:returns: An instance of Subnet
|
||||
:raises: RequestNotSupported, IPAMAlreadyAllocated
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_subnet(self, subnet_id):
|
||||
"""Gets the matching subnet if it has been allocated
|
||||
|
||||
:param subnet_id: the subnet identifier
|
||||
:type subnet_id: str uuid
|
||||
:returns: An instance of IPAM Subnet
|
||||
:raises: IPAMAllocationNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_subnet(self, request):
|
||||
"""Updates an already allocated subnet
|
||||
|
||||
This is used to notify the external IPAM system of updates to a subnet.
|
||||
|
||||
:param request: Update the subnet to match this request
|
||||
:type request: An instance of a sub-class of SpecificSubnetRequest
|
||||
:returns: An instance of IPAM Subnet
|
||||
:raises: RequestNotSupported, IPAMAllocationNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_subnet(self, subnet_id):
|
||||
"""Removes an allocation
|
||||
|
||||
The initial reference implementation will probably do nothing.
|
||||
|
||||
:param subnet_id: the subnet identifier
|
||||
:type subnet_id: str uuid
|
||||
:raises: IPAMAllocationNotFound
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Subnet(object):
|
||||
"""Interface definition for an IPAM subnet
|
||||
|
||||
A subnet would typically be associated with a network but may not be. It
|
||||
could represent a dynamically routed IP address space in which case the
|
||||
normal network and broadcast addresses would be useable. It should always
|
||||
be a routable block of addresses and representable in CIDR notation.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def allocate(self, address_request):
|
||||
"""Allocates an IP address based on the request passed in
|
||||
|
||||
:param address_request: Specifies what to allocate.
|
||||
:type address_request: An instance of a subclass of AddressRequest
|
||||
:returns: A netaddr.IPAddress
|
||||
:raises: AddressNotAvailable, AddressOutsideAllocationPool,
|
||||
AddressOutsideSubnet
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def deallocate(self, address):
|
||||
"""Returns a previously allocated address to the pool
|
||||
|
||||
:param address: The address to give back.
|
||||
:type address: A netaddr.IPAddress or convertible to one.
|
||||
:returns: None
|
||||
:raises: IPAMAllocationNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_details(self):
|
||||
"""Returns the details of the subnet
|
||||
|
||||
:returns: An instance of SpecificSubnetRequest with the subnet detail.
|
||||
"""
|
0
neutron/tests/unit/ipam/__init__.py
Normal file
0
neutron/tests/unit/ipam/__init__.py
Normal file
188
neutron/tests/unit/ipam/test_ipam.py
Normal file
188
neutron/tests/unit/ipam/test_ipam.py
Normal file
@ -0,0 +1,188 @@
|
||||
# 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 neutron.common import constants
|
||||
from neutron import ipam
|
||||
from neutron.openstack.common import uuidutils
|
||||
from neutron.tests import base
|
||||
|
||||
|
||||
class IpamSubnetRequestTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IpamSubnetRequestTestCase, self).setUp()
|
||||
self.tenant_id = uuidutils.generate_uuid()
|
||||
self.subnet_id = uuidutils.generate_uuid()
|
||||
|
||||
|
||||
class TestIpamSubnetRequests(IpamSubnetRequestTestCase):
|
||||
|
||||
def test_subnet_request(self):
|
||||
pool = ipam.SubnetRequest(self.tenant_id,
|
||||
self.subnet_id)
|
||||
self.assertEqual(self.tenant_id, pool.tenant_id)
|
||||
self.assertEqual(self.subnet_id, pool.subnet_id)
|
||||
self.assertEqual(None, pool.gateway_ip)
|
||||
self.assertEqual(None, pool.allocation_pools)
|
||||
|
||||
def test_subnet_request_gateway(self):
|
||||
request = ipam.SubnetRequest(self.tenant_id,
|
||||
self.subnet_id,
|
||||
gateway_ip='1.2.3.1')
|
||||
self.assertEqual('1.2.3.1', str(request.gateway_ip))
|
||||
|
||||
def test_subnet_request_bad_gateway(self):
|
||||
self.assertRaises(netaddr.core.AddrFormatError,
|
||||
ipam.SubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
gateway_ip='1.2.3.')
|
||||
|
||||
def test_subnet_request_with_range(self):
|
||||
allocation_pools = [netaddr.IPRange('1.2.3.4', '1.2.3.5'),
|
||||
netaddr.IPRange('1.2.3.7', '1.2.3.9')]
|
||||
request = ipam.SubnetRequest(self.tenant_id,
|
||||
self.subnet_id,
|
||||
allocation_pools=allocation_pools)
|
||||
self.assertEqual(allocation_pools, request.allocation_pools)
|
||||
|
||||
def test_subnet_request_range_not_list(self):
|
||||
self.assertRaises(TypeError,
|
||||
ipam.SubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
allocation_pools=1)
|
||||
|
||||
def test_subnet_request_bad_range(self):
|
||||
self.assertRaises(TypeError,
|
||||
ipam.SubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
allocation_pools=['1.2.3.4'])
|
||||
|
||||
def test_subnet_request_different_versions(self):
|
||||
pools = [netaddr.IPRange('0.0.0.1', '0.0.0.2'),
|
||||
netaddr.IPRange('::1', '::2')]
|
||||
self.assertRaises(ValueError,
|
||||
ipam.SubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
allocation_pools=pools)
|
||||
|
||||
def test_subnet_request_overlap(self):
|
||||
pools = [netaddr.IPRange('0.0.0.10', '0.0.0.20'),
|
||||
netaddr.IPRange('0.0.0.8', '0.0.0.10')]
|
||||
self.assertRaises(ValueError,
|
||||
ipam.SubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
allocation_pools=pools)
|
||||
|
||||
|
||||
class TestIpamAnySubnetRequest(IpamSubnetRequestTestCase):
|
||||
|
||||
def test_subnet_request(self):
|
||||
request = ipam.AnySubnetRequest(self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv4,
|
||||
24,
|
||||
gateway_ip='0.0.0.1')
|
||||
self.assertEqual(24, request.prefixlen)
|
||||
|
||||
def test_subnet_request_bad_prefix_type(self):
|
||||
self.assertRaises(netaddr.core.AddrFormatError,
|
||||
ipam.AnySubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv4,
|
||||
'A')
|
||||
|
||||
def test_subnet_request_bad_prefix(self):
|
||||
self.assertRaises(netaddr.core.AddrFormatError,
|
||||
ipam.AnySubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv4,
|
||||
33)
|
||||
self.assertRaises(netaddr.core.AddrFormatError,
|
||||
ipam.AnySubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv6,
|
||||
129)
|
||||
|
||||
def test_subnet_request_bad_gateway(self):
|
||||
self.assertRaises(ValueError,
|
||||
ipam.AnySubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv6,
|
||||
64,
|
||||
gateway_ip='2000::1')
|
||||
|
||||
def test_subnet_request_allocation_pool_wrong_version(self):
|
||||
pools = [netaddr.IPRange('0.0.0.4', '0.0.0.5')]
|
||||
self.assertRaises(ValueError,
|
||||
ipam.AnySubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv6,
|
||||
64,
|
||||
allocation_pools=pools)
|
||||
|
||||
def test_subnet_request_allocation_pool_not_in_net(self):
|
||||
pools = [netaddr.IPRange('0.0.0.64', '0.0.0.128')]
|
||||
self.assertRaises(ValueError,
|
||||
ipam.AnySubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
constants.IPv4,
|
||||
25,
|
||||
allocation_pools=pools)
|
||||
|
||||
|
||||
class TestIpamSpecificSubnetRequest(IpamSubnetRequestTestCase):
|
||||
|
||||
def test_subnet_request(self):
|
||||
request = ipam.SpecificSubnetRequest(self.tenant_id,
|
||||
self.subnet_id,
|
||||
'1.2.3.0/24',
|
||||
gateway_ip='1.2.3.1')
|
||||
self.assertEqual(24, request.prefixlen)
|
||||
self.assertEqual(netaddr.IPAddress('1.2.3.1'), request.gateway_ip)
|
||||
self.assertEqual(netaddr.IPNetwork('1.2.3.0/24'), request.subnet)
|
||||
|
||||
def test_subnet_request_bad_gateway(self):
|
||||
self.assertRaises(ValueError,
|
||||
ipam.SpecificSubnetRequest,
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
'2001::1',
|
||||
gateway_ip='2000::1')
|
||||
|
||||
|
||||
class TestAddressRequest(base.BaseTestCase):
|
||||
|
||||
# This class doesn't test much. At least running through all of the
|
||||
# constructors may shake out some trivial bugs.
|
||||
def test_specific_address_ipv6(self):
|
||||
request = ipam.SpecificAddressRequest('2000::45')
|
||||
self.assertEqual(netaddr.IPAddress('2000::45'), request.address)
|
||||
|
||||
def test_specific_address_ipv4(self):
|
||||
request = ipam.SpecificAddressRequest('1.2.3.32')
|
||||
self.assertEqual(netaddr.IPAddress('1.2.3.32'), request.address)
|
||||
|
||||
def test_any_address(self):
|
||||
ipam.AnyAddressRequest()
|
Loading…
x
Reference in New Issue
Block a user