Merge "[OVN] Allow IP allocation with different segments for OVN service ports" into stable/ussuri
This commit is contained in:
commit
617af86131
|
@ -27,7 +27,10 @@ A routed provider network enables a single provider network to represent
|
|||
multiple layer-2 networks (broadcast domains) or segments and enables the
|
||||
operator to present one network to users. However, the particular IP
|
||||
addresses available to an instance depend on the segment of the network
|
||||
available on the particular compute node.
|
||||
available on the particular compute node. Neutron port could be associated
|
||||
with only one network segment, but there is an exception for OVN distributed
|
||||
services like OVN Metadata.
|
||||
|
||||
|
||||
Similar to conventional networking, layer-2 (switching) handles transit of
|
||||
traffic between ports on the same segment and layer-3 (routing) handles
|
||||
|
|
|
@ -118,6 +118,9 @@ Routed provider networks offer performance at scale that is difficult to
|
|||
achieve with a plain provider network at the expense of guaranteed layer-2
|
||||
connectivity.
|
||||
|
||||
Neutron port could be associated with only one network segment,
|
||||
but there is an exception for OVN distributed services like OVN Metadata.
|
||||
|
||||
See :ref:`config-routed-provider-networks` for more information.
|
||||
|
||||
.. _intro-os-networking-selfservice:
|
||||
|
|
|
@ -81,6 +81,11 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
raise exc.InvalidAllocationPool(pool=ip_pool)
|
||||
return ip_range_pools
|
||||
|
||||
@staticmethod
|
||||
def _is_distributed_service(port):
|
||||
return (port.get('device_owner') == const.DEVICE_OWNER_DHCP and
|
||||
port.get('device_id').startswith('ovn'))
|
||||
|
||||
def delete_subnet(self, context, subnet_id):
|
||||
pass
|
||||
|
||||
|
@ -646,7 +651,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
return fixed_ip_list
|
||||
|
||||
def _ipam_get_subnets(self, context, network_id, host, service_type=None,
|
||||
fixed_configured=False, fixed_ips=None):
|
||||
fixed_configured=False, fixed_ips=None,
|
||||
distributed_service=False):
|
||||
"""Return eligible subnets
|
||||
|
||||
If no eligible subnets are found, determine why and potentially raise
|
||||
|
@ -654,7 +660,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
"""
|
||||
subnets = subnet_obj.Subnet.find_candidate_subnets(
|
||||
context, network_id, host, service_type, fixed_configured,
|
||||
fixed_ips)
|
||||
fixed_ips, distributed_service=distributed_service)
|
||||
if subnets:
|
||||
msg = ('This subnet is being modified by another concurrent '
|
||||
'operation')
|
||||
|
@ -722,7 +728,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
if old_ips and new_host_requested and not fixed_ips_requested:
|
||||
valid_subnets = self._ipam_get_subnets(
|
||||
context, old_port['network_id'], host,
|
||||
service_type=old_port.get('device_owner'))
|
||||
service_type=old_port.get('device_owner'),
|
||||
distributed_service=self._is_distributed_service(old_port))
|
||||
valid_subnet_ids = {s['id'] for s in valid_subnets}
|
||||
for fixed_ip in old_ips:
|
||||
if fixed_ip['subnet_id'] not in valid_subnet_ids:
|
||||
|
|
|
@ -231,12 +231,14 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
p = port['port']
|
||||
fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED
|
||||
fixed_ips = p['fixed_ips'] if fixed_configured else []
|
||||
subnets = self._ipam_get_subnets(context,
|
||||
network_id=p['network_id'],
|
||||
host=p.get(portbindings.HOST_ID),
|
||||
service_type=p.get('device_owner'),
|
||||
fixed_configured=fixed_configured,
|
||||
fixed_ips=fixed_ips)
|
||||
subnets = self._ipam_get_subnets(
|
||||
context,
|
||||
network_id=p['network_id'],
|
||||
host=p.get(portbindings.HOST_ID),
|
||||
service_type=p.get('device_owner'),
|
||||
fixed_configured=fixed_configured,
|
||||
fixed_ips=fixed_ips,
|
||||
distributed_service=self._is_distributed_service(p))
|
||||
|
||||
v4, v6_stateful, v6_stateless = self._classify_subnets(
|
||||
context, subnets)
|
||||
|
@ -348,7 +350,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
subnets = self._ipam_get_subnets(
|
||||
context, network_id=port['network_id'], host=host,
|
||||
service_type=port.get('device_owner'), fixed_configured=True,
|
||||
fixed_ips=changes.add + changes.original)
|
||||
fixed_ips=changes.add + changes.original,
|
||||
distributed_service=self._is_distributed_service(port))
|
||||
except ipam_exc.DeferIpam:
|
||||
subnets = []
|
||||
|
||||
|
|
|
@ -321,7 +321,8 @@ class Subnet(base.NeutronDbObject):
|
|||
|
||||
@classmethod
|
||||
def find_candidate_subnets(cls, context, network_id, host, service_type,
|
||||
fixed_configured, fixed_ips):
|
||||
fixed_configured, fixed_ips,
|
||||
distributed_service=False):
|
||||
"""Find canditate subnets for the network, host, and service_type"""
|
||||
query = cls.query_subnets_on_network(context, network_id)
|
||||
query = SubnetServiceType.query_filter_service_subnets(
|
||||
|
@ -335,7 +336,8 @@ class Subnet(base.NeutronDbObject):
|
|||
# on port update with binding:host_id set. Allocation _cannot_
|
||||
# be deferred as requested fixed_ips would then be lost.
|
||||
return cls._query_filter_by_fixed_ips_segment(
|
||||
query, fixed_ips).all()
|
||||
query, fixed_ips,
|
||||
allow_multiple_segments=distributed_service).all()
|
||||
# If the host isn't known, we can't allocate on a routed network.
|
||||
# So, exclude any subnets attached to segments.
|
||||
return cls._query_exclude_subnets_on_segments(query).all()
|
||||
|
@ -357,7 +359,8 @@ class Subnet(base.NeutronDbObject):
|
|||
return [subnet for subnet, _mapping in results]
|
||||
|
||||
@classmethod
|
||||
def _query_filter_by_fixed_ips_segment(cls, query, fixed_ips):
|
||||
def _query_filter_by_fixed_ips_segment(cls, query, fixed_ips,
|
||||
allow_multiple_segments=False):
|
||||
"""Excludes subnets not on the same segment as fixed_ips
|
||||
|
||||
:raises: FixedIpsSubnetsNotOnSameSegment
|
||||
|
@ -390,9 +393,12 @@ class Subnet(base.NeutronDbObject):
|
|||
if subnet and subnet.segment_id not in segment_ids:
|
||||
segment_ids.append(subnet.segment_id)
|
||||
|
||||
if 1 < len(segment_ids):
|
||||
if 1 < len(segment_ids) and not allow_multiple_segments:
|
||||
raise segment_exc.FixedIpsSubnetsNotOnSameSegment()
|
||||
|
||||
if allow_multiple_segments:
|
||||
return query
|
||||
|
||||
segment_id = None if not segment_ids else segment_ids[0]
|
||||
return query.filter(cls.db_model.segment_id == segment_id)
|
||||
|
||||
|
|
|
@ -86,6 +86,18 @@ class TestIpamBackendMixin(base.BaseTestCase):
|
|||
self.mixin._get_subnet_object = mock.Mock(
|
||||
side_effect=_get_subnet_object)
|
||||
|
||||
def test__is_distributed_service(self):
|
||||
port = {'device_owner':
|
||||
'%snova' % constants.DEVICE_OWNER_COMPUTE_PREFIX,
|
||||
'device_id': uuidutils.generate_uuid()}
|
||||
self.assertFalse(self.mixin._is_distributed_service(port))
|
||||
port = {'device_owner': constants.DEVICE_OWNER_DHCP,
|
||||
'device_id': uuidutils.generate_uuid()}
|
||||
self.assertFalse(self.mixin._is_distributed_service(port))
|
||||
port = {'device_owner': constants.DEVICE_OWNER_DHCP,
|
||||
'device_id': 'ovnmeta-%s' % uuidutils.generate_uuid()}
|
||||
self.assertTrue(self.mixin._is_distributed_service(port))
|
||||
|
||||
def _test_get_changed_ips_for_port(self, expected, original_ips,
|
||||
new_ips, owner):
|
||||
change = self.mixin._get_changed_ips_for_port(self.ctx,
|
||||
|
|
|
@ -692,12 +692,48 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
|||
mocks['ipam']._ipam_get_subnets.assert_called_once_with(
|
||||
context, network_id=port_dict['network_id'], fixed_configured=True,
|
||||
fixed_ips=[ip_dict], host=None,
|
||||
service_type=port_dict['device_owner'])
|
||||
service_type=port_dict['device_owner'],
|
||||
distributed_service=False)
|
||||
# Validate port_dict is passed into address_factory
|
||||
address_factory.get_request.assert_called_once_with(context,
|
||||
port_dict,
|
||||
ip_dict)
|
||||
|
||||
@mock.patch('neutron.ipam.driver.Pool')
|
||||
def test_update_ips_for_port_ovn_distributed_svc(self, pool_mock):
|
||||
address_factory = mock.Mock()
|
||||
mocks = self._prepare_mocks_with_pool_mock(
|
||||
pool_mock, address_factory=address_factory)
|
||||
context = mock.Mock()
|
||||
new_ips = mock.Mock()
|
||||
original_ips = mock.Mock()
|
||||
mac = mock.Mock()
|
||||
|
||||
ip_dict = {'ip_address': '192.1.1.10',
|
||||
'subnet_id': uuidutils.generate_uuid()}
|
||||
changes = ipam_pluggable_backend.IpamPluggableBackend.Changes(
|
||||
add=[ip_dict], original=[], remove=[])
|
||||
changes_mock = mock.Mock(return_value=changes)
|
||||
fixed_ips_mock = mock.Mock(return_value=changes.add)
|
||||
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
|
||||
mocks['ipam']._get_changed_ips_for_port = changes_mock
|
||||
mocks['ipam']._ipam_get_subnets = mock.Mock(return_value=[])
|
||||
mocks['ipam']._test_fixed_ips_for_port = fixed_ips_mock
|
||||
mocks['ipam']._update_ips_for_pd_subnet = mock.Mock(return_value=[])
|
||||
|
||||
port_dict = {
|
||||
'device_owner': constants.DEVICE_OWNER_DHCP,
|
||||
'device_id': 'ovnmeta-%s' % uuidutils.generate_uuid(),
|
||||
'network_id': uuidutils.generate_uuid()}
|
||||
|
||||
mocks['ipam']._update_ips_for_port(context, port_dict, None,
|
||||
original_ips, new_ips, mac)
|
||||
mocks['ipam']._ipam_get_subnets.assert_called_once_with(
|
||||
context, network_id=port_dict['network_id'], fixed_configured=True,
|
||||
fixed_ips=[ip_dict], host=None,
|
||||
service_type=port_dict['device_owner'],
|
||||
distributed_service=True)
|
||||
|
||||
@mock.patch('neutron.ipam.driver.Pool')
|
||||
def test_update_ips_for_port_passes_port_id_to_factory(self, pool_mock):
|
||||
port_id = uuidutils.generate_uuid()
|
||||
|
|
|
@ -2002,6 +2002,116 @@ class TestOVNMechanismDriverSegment(test_segment.HostSegmentMappingTestCase):
|
|||
lport_name=ovn_utils.ovn_provnet_port_name(seg_2['id']),
|
||||
lswitch_name=ovn_utils.ovn_name(net['id']))
|
||||
|
||||
def _test_segments_helper(self):
|
||||
ovn_conf.cfg.CONF.set_override('ovn_metadata_enabled', True,
|
||||
group='ovn')
|
||||
|
||||
# Create first segment and associate subnet to it.
|
||||
with self.network() as n:
|
||||
self.net = n
|
||||
self.seg_1 = self._test_create_segment(
|
||||
network_id=self.net['network']['id'], physical_network='phys_net1',
|
||||
segmentation_id=200, network_type='vlan')['segment']
|
||||
with self.subnet(network=self.net, cidr='10.0.1.0/24',
|
||||
segment_id=self.seg_1['id']) as subnet:
|
||||
self.sub_1 = subnet
|
||||
|
||||
# Create second segment and subnet linked to it
|
||||
self.seg_2 = self._test_create_segment(
|
||||
network_id=self.net['network']['id'], physical_network='phys_net2',
|
||||
segmentation_id=300, network_type='vlan')['segment']
|
||||
with self.subnet(network=self.net, cidr='10.0.2.0/24',
|
||||
segment_id=self.seg_2['id']) as subnet:
|
||||
self.sub_2 = subnet
|
||||
|
||||
def test_create_segments_subnet_metadata_ip_allocation(self):
|
||||
self._test_segments_helper()
|
||||
ovn_nb_api = self.mech_driver._nb_ovn
|
||||
|
||||
# Assert that metadata address has been allocated from previously
|
||||
# created subnet.
|
||||
self.assertIn(
|
||||
'10.0.1.2',
|
||||
ovn_nb_api.set_lswitch_port.call_args_list[0][1]['addresses'][0])
|
||||
|
||||
# Assert that the second subnet address also has been allocated for
|
||||
# metadata port.
|
||||
self.assertIn(
|
||||
'10.0.2.2',
|
||||
ovn_nb_api.set_lswitch_port.call_args_list[1][1]['addresses'][0])
|
||||
# Assert also that the first subnet address is in this update
|
||||
self.assertIn(
|
||||
'10.0.1.2',
|
||||
ovn_nb_api.set_lswitch_port.call_args_list[1][1]['addresses'][0])
|
||||
self.assertEqual(
|
||||
ovn_nb_api.set_lswitch_port.call_count, 2)
|
||||
|
||||
# Make sure both updates where on same metadata port
|
||||
args_list = ovn_nb_api.set_lswitch_port.call_args_list
|
||||
self.assertEqual(
|
||||
'ovnmeta-%s' % self.net['network']['id'],
|
||||
args_list[1][1]['external_ids']['neutron:device_id'])
|
||||
self.assertEqual(
|
||||
args_list[1][1]['external_ids']['neutron:device_id'],
|
||||
args_list[0][1]['external_ids']['neutron:device_id'])
|
||||
self.assertEqual(
|
||||
args_list[1][1]['external_ids']['neutron:device_owner'],
|
||||
args_list[0][1]['external_ids']['neutron:device_owner'])
|
||||
self.assertEqual(
|
||||
const.DEVICE_OWNER_DHCP,
|
||||
args_list[1][1]['external_ids']['neutron:device_owner'])
|
||||
|
||||
def test_create_segments_mixed_allocation_prohibited(self):
|
||||
self._test_segments_helper()
|
||||
|
||||
# Try to create 'normal' port with ip address
|
||||
# allocations from multiple segments
|
||||
kwargs = {'fixed_ips': [{'ip_address': '10.0.1.100',
|
||||
'subnet_id': self.sub_1['subnet']['id']},
|
||||
{'ip_address': '10.0.2.100',
|
||||
'subnet_id': self.sub_2['subnet']['id']}]}
|
||||
|
||||
# Make sure that this allocation is prohibited.
|
||||
self._create_port(
|
||||
self.fmt, self.net['network']['id'],
|
||||
arg_list=('fixed_ips',), **kwargs,
|
||||
expected_res_status=400)
|
||||
|
||||
def test_create_delete_segment_distributed_service_port_not_touched(self):
|
||||
self._test_segments_helper()
|
||||
ovn_nb_api = self.mech_driver._nb_ovn
|
||||
|
||||
# Delete second subnet
|
||||
self._delete('subnets', self.sub_2['subnet']['id'])
|
||||
# Make sure that shared metadata port wasn't deleted.
|
||||
ovn_nb_api.delete_lswitch_port.assert_not_called()
|
||||
|
||||
# Delete first subnet
|
||||
self._delete('subnets', self.sub_1['subnet']['id'])
|
||||
# Make sure that the metadata port wasn't deleted.
|
||||
ovn_nb_api.delete_lswitch_port.assert_not_called()
|
||||
|
||||
# Delete both segments
|
||||
self._delete('segments', self.seg_2['id'])
|
||||
self._delete('segments', self.seg_1['id'])
|
||||
|
||||
# Make sure that the metadata port wasn't deleted.
|
||||
deleted_ports = [
|
||||
port[1]['lport_name']
|
||||
for port in ovn_nb_api.delete_lswitch_port.call_args_list]
|
||||
self.assertNotIn(
|
||||
'ovnmeta-%s' % self.net['network']['id'],
|
||||
deleted_ports)
|
||||
self.assertEqual(
|
||||
2,
|
||||
ovn_nb_api.delete_lswitch_port.call_count)
|
||||
|
||||
# Only on network deletion the metadata port is deleted.
|
||||
self._delete('networks', self.net['network']['id'])
|
||||
self.assertEqual(
|
||||
3,
|
||||
ovn_nb_api.delete_lswitch_port.call_count)
|
||||
|
||||
|
||||
@mock.patch.object(n_net, 'get_random_mac', lambda *_: '01:02:03:04:05:06')
|
||||
class TestOVNMechanismDriverDHCPOptions(OVNMechanismDriverTestCase):
|
||||
|
|
Loading…
Reference in New Issue