diff --git a/doc/source/admin/config-routed-networks.rst b/doc/source/admin/config-routed-networks.rst index 0a37c0e36ad..8c78d9a9ea2 100644 --- a/doc/source/admin/config-routed-networks.rst +++ b/doc/source/admin/config-routed-networks.rst @@ -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 diff --git a/doc/source/admin/intro-os-networking.rst b/doc/source/admin/intro-os-networking.rst index 89ae795e293..9b13d55d5ea 100644 --- a/doc/source/admin/intro-os-networking.rst +++ b/doc/source/admin/intro-os-networking.rst @@ -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: diff --git a/neutron/db/ipam_backend_mixin.py b/neutron/db/ipam_backend_mixin.py index b8b4ee4945b..34c785e6c0b 100644 --- a/neutron/db/ipam_backend_mixin.py +++ b/neutron/db/ipam_backend_mixin.py @@ -77,6 +77,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 @@ -642,7 +647,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 @@ -650,7 +656,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: subnet_dicts = [self._make_subnet_dict(subnet, context=context) 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: 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: diff --git a/neutron/db/ipam_pluggable_backend.py b/neutron/db/ipam_pluggable_backend.py index cf787b24717..6c379a1c74f 100644 --- a/neutron/db/ipam_pluggable_backend.py +++ b/neutron/db/ipam_pluggable_backend.py @@ -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 = [] diff --git a/neutron/objects/subnet.py b/neutron/objects/subnet.py index 878b32ebc4d..9a1dc8a6e06 100644 --- a/neutron/objects/subnet.py +++ b/neutron/objects/subnet.py @@ -298,7 +298,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( @@ -312,7 +313,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() @@ -334,7 +336,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 @@ -367,9 +370,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) diff --git a/neutron/tests/unit/db/test_ipam_backend_mixin.py b/neutron/tests/unit/db/test_ipam_backend_mixin.py index 804f6657219..4758c27e675 100644 --- a/neutron/tests/unit/db/test_ipam_backend_mixin.py +++ b/neutron/tests/unit/db/test_ipam_backend_mixin.py @@ -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, diff --git a/neutron/tests/unit/db/test_ipam_pluggable_backend.py b/neutron/tests/unit/db/test_ipam_pluggable_backend.py index 3151671e0cc..d962eb33abc 100644 --- a/neutron/tests/unit/db/test_ipam_pluggable_backend.py +++ b/neutron/tests/unit/db/test_ipam_pluggable_backend.py @@ -690,12 +690,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()