Merge "Add Pluggable IPAM Backend Part 1"
This commit is contained in:
commit
d4c5e961ad
|
@ -0,0 +1,129 @@
|
|||
# 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
|
|
@ -255,11 +255,22 @@ class AddressRequestFactory(object):
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def get_request(cls, context, port, ip):
|
||||
if not ip:
|
||||
return AnyAddressRequest()
|
||||
def get_request(cls, context, port, ip_dict):
|
||||
"""
|
||||
:param context: context (not used here, but can be used in sub-classes)
|
||||
:param port: port dict (not used here, but can be used in sub-classes)
|
||||
:param ip_dict: dict that can contain 'ip_address', 'mac' and
|
||||
'subnet_cidr' keys. Request to generate is selected depending on
|
||||
this ip_dict keys.
|
||||
:return: returns prepared AddressRequest (specific or any)
|
||||
"""
|
||||
if ip_dict.get('ip_address'):
|
||||
return SpecificAddressRequest(ip_dict['ip_address'])
|
||||
elif ip_dict.get('eui64_address'):
|
||||
return AutomaticAddressRequest(prefix=ip_dict['subnet_cidr'],
|
||||
mac=ip_dict['mac'])
|
||||
else:
|
||||
return SpecificAddressRequest(ip)
|
||||
return AnyAddressRequest()
|
||||
|
||||
|
||||
class SubnetRequestFactory(object):
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
# 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)
|
|
@ -291,20 +291,26 @@ class TestAddressRequestFactory(base.BaseTestCase):
|
|||
|
||||
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):
|
||||
|
||||
|
@ -331,31 +337,31 @@ 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)
|
||||
|
|
Loading…
Reference in New Issue