Merge "[OVN] Allow IP allocation with different segments for OVN service ports" into stable/train
This commit is contained in:
commit
cc7a29c4b7
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue