--- /dev/null
+# Copyright (c) 2015 Infoblox 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_log import log as logging
+from oslo_utils import excutils
+
+from neutron.common import exceptions as n_exc
+from neutron.db import ipam_backend_mixin
+from neutron.i18n import _LE
+from neutron.ipam import exceptions as ipam_exc
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
+
+ def _get_failed_ips(self, all_ips, success_ips):
+ ips_list = (ip_dict['ip_address'] for ip_dict in success_ips)
+ return (ip_dict['ip_address'] for ip_dict in all_ips
+ if ip_dict['ip_address'] not in ips_list)
+
+ def _ipam_deallocate_ips(self, context, ipam_driver, port, ips,
+ revert_on_fail=True):
+ """Deallocate set of ips over IPAM.
+
+ If any single ip deallocation fails, tries to allocate deallocated
+ ip addresses with fixed ip request
+ """
+ deallocated = []
+
+ try:
+ for ip in ips:
+ try:
+ ipam_subnet = ipam_driver.get_subnet(ip['subnet_id'])
+ ipam_subnet.deallocate(ip['ip_address'])
+ deallocated.append(ip)
+ except n_exc.SubnetNotFound:
+ LOG.debug("Subnet was not found on ip deallocation: %s",
+ ip)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.debug("An exception occurred during IP deallocation.")
+ if revert_on_fail and deallocated:
+ LOG.debug("Reverting deallocation")
+ self._ipam_allocate_ips(context, ipam_driver, port,
+ deallocated, revert_on_fail=False)
+ elif not revert_on_fail and ips:
+ addresses = ', '.join(self._get_failed_ips(ips,
+ deallocated))
+ LOG.error(_LE("IP deallocation failed on "
+ "external system for %s"), addresses)
+ return deallocated
+
+ def _ipam_try_allocate_ip(self, context, ipam_driver, port, ip_dict):
+ factory = ipam_driver.get_address_request_factory()
+ ip_request = factory.get_request(context, port, ip_dict)
+ ipam_subnet = ipam_driver.get_subnet(ip_dict['subnet_id'])
+ return ipam_subnet.allocate(ip_request)
+
+ def _ipam_allocate_single_ip(self, context, ipam_driver, port, subnets):
+ """Allocates single ip from set of subnets
+
+ Raises n_exc.IpAddressGenerationFailure if allocation failed for
+ all subnets.
+ """
+ for subnet in subnets:
+ try:
+ return [self._ipam_try_allocate_ip(context, ipam_driver,
+ port, subnet),
+ subnet]
+ except ipam_exc.IpAddressGenerationFailure:
+ continue
+ raise n_exc.IpAddressGenerationFailure(
+ net_id=port['network_id'])
+
+ def _ipam_allocate_ips(self, context, ipam_driver, port, ips,
+ revert_on_fail=True):
+ """Allocate set of ips over IPAM.
+
+ If any single ip allocation fails, tries to deallocate all
+ allocated ip addresses.
+ """
+ allocated = []
+
+ # we need to start with entries that asked for a specific IP in case
+ # those IPs happen to be next in the line for allocation for ones that
+ # didn't ask for a specific IP
+ ips.sort(key=lambda x: 'ip_address' not in x)
+ try:
+ for ip in ips:
+ # By default IP info is dict, used to allocate single ip
+ # from single subnet.
+ # IP info can be list, used to allocate single ip from
+ # multiple subnets (i.e. first successful ip allocation
+ # is returned)
+ ip_list = [ip] if isinstance(ip, dict) else ip
+ ip_address, ip_subnet = self._ipam_allocate_single_ip(
+ context, ipam_driver, port, ip_list)
+ allocated.append({'ip_address': ip_address,
+ 'subnet_cidr': ip_subnet['subnet_cidr'],
+ 'subnet_id': ip_subnet['subnet_id']})
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.debug("An exception occurred during IP allocation.")
+
+ if revert_on_fail and allocated:
+ LOG.debug("Reverting allocation")
+ self._ipam_deallocate_ips(context, ipam_driver, port,
+ allocated, revert_on_fail=False)
+ elif not revert_on_fail and ips:
+ addresses = ', '.join(self._get_failed_ips(ips,
+ allocated))
+ LOG.error(_LE("IP allocation failed on "
+ "external system for %s"), addresses)
+
+ return allocated
--- /dev/null
+# Copyright (c) 2015 Infoblox 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 mock
+import netaddr
+
+from oslo_utils import uuidutils
+
+from neutron.common import exceptions as n_exc
+from neutron.common import ipv6_utils
+from neutron.db import ipam_pluggable_backend
+from neutron.ipam import requests as ipam_req
+from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base
+
+
+class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
+ def setUp(self):
+ super(TestDbBasePluginIpam, self).setUp()
+ self.tenant_id = uuidutils.generate_uuid()
+ self.subnet_id = uuidutils.generate_uuid()
+
+ def _prepare_mocks(self):
+ mocks = {
+ 'driver': mock.Mock(),
+ 'subnet': mock.Mock(),
+ 'subnet_request': ipam_req.SpecificSubnetRequest(
+ self.tenant_id,
+ self.subnet_id,
+ '10.0.0.0/24',
+ '10.0.0.1',
+ [netaddr.IPRange('10.0.0.2', '10.0.0.254')]),
+ }
+ mocks['driver'].get_subnet.return_value = mocks['subnet']
+ mocks['driver'].allocate_subnet.return_value = mocks['subnet']
+ mocks['driver'].get_subnet_request_factory = (
+ ipam_req.SubnetRequestFactory)
+ mocks['driver'].get_address_request_factory = (
+ ipam_req.AddressRequestFactory)
+ mocks['subnet'].get_details.return_value = mocks['subnet_request']
+ return mocks
+
+ def _prepare_ipam(self):
+ mocks = self._prepare_mocks()
+ mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
+ return mocks
+
+ def _get_allocate_mock(self, auto_ip='10.0.0.2',
+ fail_ip='127.0.0.1',
+ error_message='SomeError'):
+ def allocate_mock(request):
+ if type(request) == ipam_req.SpecificAddressRequest:
+ if request.address == netaddr.IPAddress(fail_ip):
+ raise n_exc.InvalidInput(error_message=error_message)
+ else:
+ return str(request.address)
+ else:
+ return auto_ip
+
+ return allocate_mock
+
+ def _validate_allocate_calls(self, expected_calls, mocks):
+ assert mocks['subnet'].allocate.called
+
+ actual_calls = mocks['subnet'].allocate.call_args_list
+ self.assertEqual(len(expected_calls), len(actual_calls))
+
+ i = 0
+ for call in expected_calls:
+ if call['ip_address']:
+ self.assertEqual(ipam_req.SpecificAddressRequest,
+ type(actual_calls[i][0][0]))
+ self.assertEqual(netaddr.IPAddress(call['ip_address']),
+ actual_calls[i][0][0].address)
+ else:
+ self.assertEqual(ipam_req.AnyAddressRequest,
+ type(actual_calls[i][0][0]))
+ i += 1
+
+ def _convert_to_ips(self, data):
+ ips = [{'ip_address': ip,
+ 'subnet_id': data[ip][1],
+ 'subnet_cidr': data[ip][0]} for ip in data]
+ return sorted(ips, key=lambda t: t['subnet_cidr'])
+
+ def _gen_subnet_id(self):
+ return uuidutils.generate_uuid()
+
+ def test_deallocate_single_ip(self):
+ mocks = self._prepare_ipam()
+ ip = '192.168.12.45'
+ data = {ip: ['192.168.12.0/24', self._gen_subnet_id()]}
+ ips = self._convert_to_ips(data)
+
+ mocks['ipam']._ipam_deallocate_ips(mock.ANY, mocks['driver'],
+ mock.ANY, ips)
+
+ mocks['driver'].get_subnet.assert_called_once_with(data[ip][1])
+ mocks['subnet'].deallocate.assert_called_once_with(ip)
+
+ def test_deallocate_multiple_ips(self):
+ mocks = self._prepare_ipam()
+ data = {'192.168.43.15': ['192.168.43.0/24', self._gen_subnet_id()],
+ '172.23.158.84': ['172.23.128.0/17', self._gen_subnet_id()],
+ '8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
+ ips = self._convert_to_ips(data)
+
+ mocks['ipam']._ipam_deallocate_ips(mock.ANY, mocks['driver'],
+ mock.ANY, ips)
+
+ get_calls = [mock.call(data[ip][1]) for ip in data]
+ mocks['driver'].get_subnet.assert_has_calls(get_calls, any_order=True)
+
+ ip_calls = [mock.call(ip) for ip in data]
+ mocks['subnet'].deallocate.assert_has_calls(ip_calls, any_order=True)
+
+ def _single_ip_allocate_helper(self, mocks, ip, network, subnet):
+ ips = [{'subnet_cidr': network,
+ 'subnet_id': subnet}]
+ if ip:
+ ips[0]['ip_address'] = ip
+
+ allocated_ips = mocks['ipam']._ipam_allocate_ips(
+ mock.ANY, mocks['driver'], mock.ANY, ips)
+
+ mocks['driver'].get_subnet.assert_called_once_with(subnet)
+
+ assert mocks['subnet'].allocate.called
+ request = mocks['subnet'].allocate.call_args[0][0]
+
+ return {'ips': allocated_ips,
+ 'request': request}
+
+ def test_allocate_single_fixed_ip(self):
+ mocks = self._prepare_ipam()
+ ip = '192.168.15.123'
+ mocks['subnet'].allocate.return_value = ip
+
+ results = self._single_ip_allocate_helper(mocks,
+ ip,
+ '192.168.15.0/24',
+ self._gen_subnet_id())
+
+ self.assertEqual(ipam_req.SpecificAddressRequest,
+ type(results['request']))
+ self.assertEqual(netaddr.IPAddress(ip), results['request'].address)
+
+ self.assertEqual(ip, results['ips'][0]['ip_address'],
+ 'Should allocate the same ip as passed')
+
+ def test_allocate_single_any_ip(self):
+ mocks = self._prepare_ipam()
+ network = '192.168.15.0/24'
+ ip = '192.168.15.83'
+ mocks['subnet'].allocate.return_value = ip
+
+ results = self._single_ip_allocate_helper(mocks, '', network,
+ self._gen_subnet_id())
+
+ self.assertEqual(ipam_req.AnyAddressRequest, type(results['request']))
+ self.assertEqual(ip, results['ips'][0]['ip_address'])
+
+ def test_allocate_eui64_ip(self):
+ mocks = self._prepare_ipam()
+ ip = {'subnet_id': self._gen_subnet_id(),
+ 'subnet_cidr': '2001:470:abcd::/64',
+ 'mac': '6c:62:6d:de:cf:49',
+ 'eui64_address': True}
+ eui64_ip = ipv6_utils.get_ipv6_addr_by_EUI64(ip['subnet_cidr'],
+ ip['mac'])
+ mocks['ipam']._ipam_allocate_ips(mock.ANY, mocks['driver'],
+ mock.ANY, [ip])
+
+ request = mocks['subnet'].allocate.call_args[0][0]
+ self.assertEqual(ipam_req.AutomaticAddressRequest, type(request))
+ self.assertEqual(eui64_ip, request.address)
+
+ def test_allocate_multiple_ips(self):
+ mocks = self._prepare_ipam()
+ data = {'': ['172.23.128.0/17', self._gen_subnet_id()],
+ '192.168.43.15': ['192.168.43.0/24', self._gen_subnet_id()],
+ '8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
+ ips = self._convert_to_ips(data)
+ mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
+ auto_ip='172.23.128.94')
+
+ mocks['ipam']._ipam_allocate_ips(
+ mock.ANY, mocks['driver'], mock.ANY, ips)
+ get_calls = [mock.call(data[ip][1]) for ip in data]
+ mocks['driver'].get_subnet.assert_has_calls(get_calls, any_order=True)
+
+ self._validate_allocate_calls(ips, mocks)
+
+ def test_allocate_multiple_ips_with_exception(self):
+ mocks = self._prepare_ipam()
+
+ auto_ip = '172.23.128.94'
+ fail_ip = '192.168.43.15'
+ data = {'': ['172.23.128.0/17', self._gen_subnet_id()],
+ fail_ip: ['192.168.43.0/24', self._gen_subnet_id()],
+ '8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
+ ips = self._convert_to_ips(data)
+ mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
+ auto_ip=auto_ip, fail_ip=fail_ip)
+
+ # Exception should be raised on attempt to allocate second ip.
+ # Revert action should be performed for the already allocated ips,
+ # In this test case only one ip should be deallocated
+ # and original error should be reraised
+ self.assertRaises(n_exc.InvalidInput,
+ mocks['ipam']._ipam_allocate_ips,
+ mock.ANY,
+ mocks['driver'],
+ mock.ANY,
+ ips)
+
+ # get_subnet should be called only for the first two networks
+ get_calls = [mock.call(data[ip][1]) for ip in ['', fail_ip]]
+ mocks['driver'].get_subnet.assert_has_calls(get_calls, any_order=True)
+
+ # Allocate should be called for the first two ips only
+ self._validate_allocate_calls(ips[:-1], mocks)
+ # Deallocate should be called for the first ip only
+ mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
def test_specific_address_request_is_loaded(self):
for address in ('10.12.0.15', 'fffe::1'):
+ ip = {'ip_address': address}
self.assertIsInstance(
- ipam_req.AddressRequestFactory.get_request(None,
- None,
- address),
+ ipam_req.AddressRequestFactory.get_request(None, None, ip),
ipam_req.SpecificAddressRequest)
def test_any_address_request_is_loaded(self):
for addr in [None, '']:
+ ip = {'ip_address': addr}
self.assertIsInstance(
- ipam_req.AddressRequestFactory.get_request(None,
- None,
- addr),
+ ipam_req.AddressRequestFactory.get_request(None, None, ip),
ipam_req.AnyAddressRequest)
+ def test_automatic_address_request_is_loaded(self):
+ ip = {'mac': '6c:62:6d:de:cf:49',
+ 'subnet_cidr': '2001:470:abcd::/64',
+ 'eui64_address': True}
+ self.assertIsInstance(
+ ipam_req.AddressRequestFactory.get_request(None, None, ip),
+ ipam_req.AutomaticAddressRequest)
+
class TestSubnetRequestFactory(IpamSubnetRequestTestCase):
subnet, subnetpool = self._build_subnet_dict(cidr=address)
self.assertIsInstance(
ipam_req.SubnetRequestFactory.get_request(None,
- subnet,
- subnetpool),
+ subnet,
+ subnetpool),
ipam_req.SpecificSubnetRequest)
def test_any_address_request_is_loaded_for_ipv4(self):
subnet, subnetpool = self._build_subnet_dict(cidr=None, ip_version=4)
self.assertIsInstance(
ipam_req.SubnetRequestFactory.get_request(None,
- subnet,
- subnetpool),
+ subnet,
+ subnetpool),
ipam_req.AnySubnetRequest)
def test_any_address_request_is_loaded_for_ipv6(self):
subnet, subnetpool = self._build_subnet_dict(cidr=None, ip_version=6)
self.assertIsInstance(
ipam_req.SubnetRequestFactory.get_request(None,
- subnet,
- subnetpool),
+ subnet,
+ subnetpool),
ipam_req.AnySubnetRequest)
def test_args_are_passed_to_specific_request(self):
subnet, subnetpool = self._build_subnet_dict()
request = ipam_req.SubnetRequestFactory.get_request(None,
- subnet,
- subnetpool)
+ subnet,
+ subnetpool)
self.assertIsInstance(request,
ipam_req.SpecificSubnetRequest)
self.assertEqual(self.tenant_id, request.tenant_id)