Merge "Add Pluggable IPAM Backend Part 1"

This commit is contained in:
Jenkins 2015-07-14 16:20:49 +00:00 committed by Gerrit Code Review
commit d4c5e961ad
4 changed files with 399 additions and 18 deletions

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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)