--- /dev/null
+# 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."""
--- /dev/null
+# 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.
+ """
--- /dev/null
+# 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()