Merge "[OVN] Allow IP allocation with different segments for OVN service ports" into stable/train

This commit is contained in:
Zuul 2020-07-28 11:12:30 +00:00 committed by Gerrit Code Review
commit cc7a29c4b7
7 changed files with 86 additions and 16 deletions

View File

@ -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 multiple layer-2 networks (broadcast domains) or segments and enables the
operator to present one network to users. However, the particular IP operator to present one network to users. However, the particular IP
addresses available to an instance depend on the segment of the network 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 Similar to conventional networking, layer-2 (switching) handles transit of
traffic between ports on the same segment and layer-3 (routing) handles traffic between ports on the same segment and layer-3 (routing) handles

View File

@ -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 achieve with a plain provider network at the expense of guaranteed layer-2
connectivity. 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. See :ref:`config-routed-provider-networks` for more information.
.. _intro-os-networking-selfservice: .. _intro-os-networking-selfservice:

View File

@ -77,6 +77,11 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
raise exc.InvalidAllocationPool(pool=ip_pool) raise exc.InvalidAllocationPool(pool=ip_pool)
return ip_range_pools 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): def delete_subnet(self, context, subnet_id):
pass pass
@ -642,7 +647,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
return fixed_ip_list return fixed_ip_list
def _ipam_get_subnets(self, context, network_id, host, service_type=None, 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 """Return eligible subnets
If no eligible subnets are found, determine why and potentially raise If no eligible subnets are found, determine why and potentially raise
@ -650,7 +656,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
""" """
subnets = subnet_obj.Subnet.find_candidate_subnets( subnets = subnet_obj.Subnet.find_candidate_subnets(
context, network_id, host, service_type, fixed_configured, context, network_id, host, service_type, fixed_configured,
fixed_ips) fixed_ips, distributed_service=distributed_service)
if subnets: if subnets:
subnet_dicts = [self._make_subnet_dict(subnet, context=context) subnet_dicts = [self._make_subnet_dict(subnet, context=context)
for subnet in subnets] for subnet in subnets]
@ -712,7 +718,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
if old_ips and new_host_requested and not fixed_ips_requested: if old_ips and new_host_requested and not fixed_ips_requested:
valid_subnets = self._ipam_get_subnets( valid_subnets = self._ipam_get_subnets(
context, old_port['network_id'], host, 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} valid_subnet_ids = {s['id'] for s in valid_subnets}
for fixed_ip in old_ips: for fixed_ip in old_ips:
if fixed_ip['subnet_id'] not in valid_subnet_ids: if fixed_ip['subnet_id'] not in valid_subnet_ids:

View File

@ -231,12 +231,14 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
p = port['port'] p = port['port']
fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED
fixed_ips = p['fixed_ips'] if fixed_configured else [] fixed_ips = p['fixed_ips'] if fixed_configured else []
subnets = self._ipam_get_subnets(context, subnets = self._ipam_get_subnets(
network_id=p['network_id'], context,
host=p.get(portbindings.HOST_ID), network_id=p['network_id'],
service_type=p.get('device_owner'), host=p.get(portbindings.HOST_ID),
fixed_configured=fixed_configured, service_type=p.get('device_owner'),
fixed_ips=fixed_ips) fixed_configured=fixed_configured,
fixed_ips=fixed_ips,
distributed_service=self._is_distributed_service(p))
v4, v6_stateful, v6_stateless = self._classify_subnets( v4, v6_stateful, v6_stateless = self._classify_subnets(
context, subnets) context, subnets)
@ -348,7 +350,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
subnets = self._ipam_get_subnets( subnets = self._ipam_get_subnets(
context, network_id=port['network_id'], host=host, context, network_id=port['network_id'], host=host,
service_type=port.get('device_owner'), fixed_configured=True, 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: except ipam_exc.DeferIpam:
subnets = [] subnets = []

View File

@ -298,7 +298,8 @@ class Subnet(base.NeutronDbObject):
@classmethod @classmethod
def find_candidate_subnets(cls, context, network_id, host, service_type, 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""" """Find canditate subnets for the network, host, and service_type"""
query = cls.query_subnets_on_network(context, network_id) query = cls.query_subnets_on_network(context, network_id)
query = SubnetServiceType.query_filter_service_subnets( query = SubnetServiceType.query_filter_service_subnets(
@ -312,7 +313,8 @@ class Subnet(base.NeutronDbObject):
# on port update with binding:host_id set. Allocation _cannot_ # on port update with binding:host_id set. Allocation _cannot_
# be deferred as requested fixed_ips would then be lost. # be deferred as requested fixed_ips would then be lost.
return cls._query_filter_by_fixed_ips_segment( 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. # If the host isn't known, we can't allocate on a routed network.
# So, exclude any subnets attached to segments. # So, exclude any subnets attached to segments.
return cls._query_exclude_subnets_on_segments(query).all() return cls._query_exclude_subnets_on_segments(query).all()
@ -334,7 +336,8 @@ class Subnet(base.NeutronDbObject):
return [subnet for subnet, _mapping in results] return [subnet for subnet, _mapping in results]
@classmethod @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 """Excludes subnets not on the same segment as fixed_ips
:raises: FixedIpsSubnetsNotOnSameSegment :raises: FixedIpsSubnetsNotOnSameSegment
@ -367,9 +370,12 @@ class Subnet(base.NeutronDbObject):
if subnet and subnet.segment_id not in segment_ids: if subnet and subnet.segment_id not in segment_ids:
segment_ids.append(subnet.segment_id) 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() raise segment_exc.FixedIpsSubnetsNotOnSameSegment()
if allow_multiple_segments:
return query
segment_id = None if not segment_ids else segment_ids[0] segment_id = None if not segment_ids else segment_ids[0]
return query.filter(cls.db_model.segment_id == segment_id) return query.filter(cls.db_model.segment_id == segment_id)

View File

@ -86,6 +86,18 @@ class TestIpamBackendMixin(base.BaseTestCase):
self.mixin._get_subnet_object = mock.Mock( self.mixin._get_subnet_object = mock.Mock(
side_effect=_get_subnet_object) 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, def _test_get_changed_ips_for_port(self, expected, original_ips,
new_ips, owner): new_ips, owner):
change = self.mixin._get_changed_ips_for_port(self.ctx, change = self.mixin._get_changed_ips_for_port(self.ctx,

View File

@ -690,12 +690,48 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
mocks['ipam']._ipam_get_subnets.assert_called_once_with( mocks['ipam']._ipam_get_subnets.assert_called_once_with(
context, network_id=port_dict['network_id'], fixed_configured=True, context, network_id=port_dict['network_id'], fixed_configured=True,
fixed_ips=[ip_dict], host=None, 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 # Validate port_dict is passed into address_factory
address_factory.get_request.assert_called_once_with(context, address_factory.get_request.assert_called_once_with(context,
port_dict, port_dict,
ip_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') @mock.patch('neutron.ipam.driver.Pool')
def test_update_ips_for_port_passes_port_id_to_factory(self, pool_mock): def test_update_ips_for_port_passes_port_id_to_factory(self, pool_mock):
port_id = uuidutils.generate_uuid() port_id = uuidutils.generate_uuid()