Add net & utils methods for routed nets & segments

Before adding a prefilter, we need to add new methods for asking Neutron
about the related aggregates we need to ask Placement if the user
creates or moves an instance with a requested network or a port.

NOTE(sbauza): Looks like mypy doesn't like the current methods in
nova.network.neutron, so we need to fix the issues before adding this
module to mypy-files.txt.

Partially-Implements : blueprint routed-networks-scheduling

Change-Id: Ie166f3b51fddeaf916cda7c5ac34bbcdda0fd17a
This commit is contained in:
Sylvain Bauza 2021-02-03 18:31:31 +01:00
parent e7aa436dc6
commit c4b28a5496
6 changed files with 353 additions and 0 deletions

View File

@ -1195,6 +1195,11 @@ class RequestFilterFailed(NovaException):
msg_fmt = _("Scheduling failed: %(reason)s")
class InvalidRoutedNetworkConfiguration(RequestFilterFailed):
msg_fmt = _("Neutron routed networks configuration is invalid: "
"%(reason)s.")
class MaxRetriesExceeded(NoValidHost):
msg_fmt = _("Exceeded maximum number of retries. %(reason)s")

View File

@ -29,3 +29,4 @@ MIGRATING_ATTR = 'migrating_to'
L3_NETWORK_TYPES = ['vxlan', 'gre', 'geneve']
ALLOCATION = 'allocation'
RESOURCE_REQUEST = 'resource_request'
SEGMENT = 'Segment'

View File

@ -21,6 +21,7 @@ API and utilities for nova-network interactions.
import copy
import functools
import time
import typing as ty
from keystoneauth1 import loading as ks_loading
from neutronclient.common import exceptions as neutron_client_exc
@ -1249,6 +1250,10 @@ class API(base.Base):
self._refresh_neutron_extensions_cache(context)
return constants.SUBSTR_PORT_FILTERING in self.extensions
def _has_segment_extension(self, context, neutron=None):
self._refresh_neutron_extensions_cache(context, neutron=neutron)
return constants.SEGMENT in self.extensions
def supports_port_binding_extension(self, context):
"""This is a simple check to see if the neutron "binding-extended"
extension exists and is enabled.
@ -3484,6 +3489,60 @@ class API(base.Base):
'for port %s.',
vif['id'], instance=instance)
def get_segment_ids_for_network(
self,
context: nova.context.RequestContext,
network_id: str,
) -> ty.List[str]:
"""Query the segmentation ids for the given network.
:param context: The request context.
:param network_id: The UUID of the network to be queried.
:returns: The list of segment UUIDs of the network or an empty list if
either Segment extension isn't enabled in Neutron or if the network
isn't configured for routing.
"""
if not self._has_segment_extension(context):
return []
client = get_client(context)
try:
# NOTE(sbauza): We can't use list_segments() directly because the
# API is borked and returns both segments but also segmentation IDs
# of a provider network if any.
subnets = client.list_subnets(network_id=network_id,
fields='segment_id')['subnets']
except neutron_client_exc.NeutronClientException as e:
raise exception.InvalidRoutedNetworkConfiguration(
'Failed to get segment IDs for network %s' % network_id) from e
# The segment field of an unconfigured subnet could be None
return [subnet['segment_id'] for subnet in subnets
if subnet['segment_id'] is not None]
def get_segment_id_for_subnet(
self,
context: nova.context.RequestContext,
subnet_id: str,
) -> ty.Optional[str]:
"""Query the segmentation id for the given subnet.
:param context: The request context.
:param subnet_id: The UUID of the subnet to be queried.
:returns: The segment UUID of the subnet or None if either Segment
extension isn't enabled in Neutron or the provided subnet doesn't
have segments (if the related network isn't configured for routing)
"""
if not self._has_segment_extension(context):
return None
client = get_client(context)
try:
subnet = client.show_subnet(subnet_id)['subnet']
except neutron_client_exc.NeutronClientException as e:
raise exception.InvalidRoutedNetworkConfiguration(
'Subnet %s not found' % subnet_id) from e
return subnet.get('segment_id')
def _ensure_requested_network_ordering(accessor, unordered, preferred):
"""Sort a list with respect to the preferred network ordering."""

View File

@ -1328,3 +1328,82 @@ def fill_provider_mapping_based_on_allocation(
# allocation_request_version key of the Selection object.
request_spec.map_requested_resources_to_providers(
allocation, provider_traits)
# FIXME(sbauza) : Move this method closer to the prefilter once split.
def get_aggregates_for_routed_network(
context, network_api, report_client, network_uuid):
"""Collects the aggregate UUIDs describing the segmentation of a routed
network from Nova perspective.
A routed network consists of multiple network segments. Each segment is
available on a given set of compute hosts. Such segmentation is modelled as
host aggregates from Nova perspective.
:param context: The security context
:param network_api: nova.network.neutron.API instance to be used to
communicate with Neutron
:param report_client: SchedulerReportClient instance to be used to
communicate with Placement
:param network_uuid: The UUID of the Neutron network to be translated to
aggregates
:returns: A list of aggregate UUIDs
:raises InvalidRoutedNetworkConfiguration: if something goes wrong when
try to find related aggregates
"""
aggregates = []
segment_ids = network_api.get_segment_ids_for_network(
context, network_uuid)
# Each segment is a resource provider in placement and is in an
# aggregate for the routed network, so we have to get the
# aggregates for each segment provider - and those aggregates are
# mirrored as nova host aggregates.
# NOTE(sbauza): In case of a network with non-configured routed segments,
# we will get an empty list of segment UUIDs, so we won't enter the loop.
for segment_id in segment_ids:
# TODO(sbauza): Don't use a private method.
agg_info = report_client._get_provider_aggregates(context, segment_id)
# @safe_connect can return None but we also want to hard-stop here if
# we can't find the aggregate that Neutron created for the segment.
if agg_info is None or not agg_info.aggregates:
raise exception.InvalidRoutedNetworkConfiguration(
'Failed to find aggregate related to segment %s' % segment_id)
aggregates.extend(agg_info.aggregates)
return aggregates
# FIXME(sbauza) : Move this method closer to the prefilter once split.
def get_aggregates_for_routed_subnet(
context, network_api, report_client, subnet_id):
"""Collects the aggregate UUIDs matching the segment that relates to a
particular subnet from a routed network.
A routed network consists of multiple network segments. Each segment is
available on a given set of compute hosts. Such segmentation is modelled as
host aggregates from Nova perspective.
:param context: The security context
:param network_api: nova.network.neutron.API instance to be used to
communicate with Neutron
:param report_client: SchedulerReportClient instance to be used to
communicate with Placement
:param subnet_id: The UUID of the Neutron subnet to be translated to
aggregate
:returns: A list of aggregate UUIDs
:raises InvalidRoutedNetworkConfiguration: if something goes wrong when
try to find related aggregates
"""
segment_id = network_api.get_segment_id_for_subnet(
context, subnet_id)
if segment_id:
# TODO(sbauza): Don't use a private method.
agg_info = report_client._get_provider_aggregates(context, segment_id)
# @safe_connect can return None but we also want to hard-stop here if
# we can't find the aggregate that Neutron created for the segment.
if agg_info is None or not agg_info.aggregates:
raise exception.InvalidRoutedNetworkConfiguration(
'Failed to find aggregate related to segment %s' % segment_id)
return agg_info.aggregates
return []

View File

@ -6268,6 +6268,105 @@ class TestAPI(TestAPIBase):
mock_get_client.assert_called_once_with(self.context, admin=True)
def test_get_segment_ids_for_network_no_segment_ext(self):
with mock.patch.object(
self.api, '_has_segment_extension', return_value=False
):
self.assertEqual(
[], self.api.get_segment_ids_for_network(self.context,
uuids.network_id))
@mock.patch.object(neutronapi, 'get_client')
def test_get_segment_ids_for_network_passes(self, mock_client):
subnets = {'subnets': [{'segment_id': uuids.segment_id}]}
mocked_client = mock.create_autospec(client.Client)
mock_client.return_value = mocked_client
mocked_client.list_subnets.return_value = subnets
with mock.patch.object(
self.api, '_has_segment_extension', return_value=True
):
res = self.api.get_segment_ids_for_network(
self.context, uuids.network_id)
self.assertEqual([uuids.segment_id], res)
mocked_client.list_subnets.assert_called_once_with(
network_id=uuids.network_id, fields='segment_id')
@mock.patch.object(neutronapi, 'get_client')
def test_get_segment_ids_for_network_with_no_segments(self, mock_client):
subnets = {'subnets': [{'segment_id': None}]}
mocked_client = mock.create_autospec(client.Client)
mock_client.return_value = mocked_client
mocked_client.list_subnets.return_value = subnets
with mock.patch.object(
self.api, '_has_segment_extension', return_value=True
):
res = self.api.get_segment_ids_for_network(
self.context, uuids.network_id)
self.assertEqual([], res)
mocked_client.list_subnets.assert_called_once_with(
network_id=uuids.network_id, fields='segment_id')
@mock.patch.object(neutronapi, 'get_client')
def test_get_segment_ids_for_network_fails(self, mock_client):
mocked_client = mock.create_autospec(client.Client)
mock_client.return_value = mocked_client
mocked_client.list_subnets.side_effect = (
exceptions.NeutronClientException(status_code=404))
with mock.patch.object(
self.api, '_has_segment_extension', return_value=True
):
self.assertRaises(exception.InvalidRoutedNetworkConfiguration,
self.api.get_segment_ids_for_network,
self.context, uuids.network_id)
def test_get_segment_id_for_subnet_no_segment_ext(self):
with mock.patch.object(
self.api, '_has_segment_extension', return_value=False
):
self.assertIsNone(
self.api.get_segment_id_for_subnet(self.context,
uuids.subnet_id))
@mock.patch.object(neutronapi, 'get_client')
def test_get_segment_id_for_subnet_passes(self, mock_client):
subnet = {'subnet': {'segment_id': uuids.segment_id}}
mocked_client = mock.create_autospec(client.Client)
mock_client.return_value = mocked_client
mocked_client.show_subnet.return_value = subnet
with mock.patch.object(
self.api, '_has_segment_extension', return_value=True
):
res = self.api.get_segment_id_for_subnet(
self.context, uuids.subnet_id)
self.assertEqual(uuids.segment_id, res)
mocked_client.show_subnet.assert_called_once_with(uuids.subnet_id)
@mock.patch.object(neutronapi, 'get_client')
def test_get_segment_id_for_subnet_with_no_segment(self, mock_client):
subnet = {'subnet': {}}
mocked_client = mock.create_autospec(client.Client)
mock_client.return_value = mocked_client
mocked_client.show_subnet.return_value = subnet
with mock.patch.object(
self.api, '_has_segment_extension', return_value=True
):
self.assertIsNone(
self.api.get_segment_id_for_subnet(self.context,
uuids.subnet_id))
@mock.patch.object(neutronapi, 'get_client')
def test_get_segment_id_for_subnet_fails(self, mock_client):
mocked_client = mock.create_autospec(client.Client)
mock_client.return_value = mocked_client
mocked_client.show_subnet.side_effect = (
exceptions.NeutronClientException(status_code=404))
with mock.patch.object(
self.api, '_has_segment_extension', return_value=True
):
self.assertRaises(exception.InvalidRoutedNetworkConfiguration,
self.api.get_segment_id_for_subnet,
self.context, uuids.subnet_id)
class TestAPIModuleMethods(test.NoDBTestCase):

View File

@ -23,7 +23,9 @@ from nova.compute import flavors
from nova.compute import utils as compute_utils
from nova import context as nova_context
from nova import exception
from nova.network import neutron
from nova import objects
from nova.scheduler.client import report
from nova.scheduler import utils as scheduler_utils
from nova import test
from nova.tests.unit import fake_instance
@ -407,3 +409,111 @@ class SchedulerUtilsTestCase(test.NoDBTestCase):
self.assertRaises(exception.NoValidHost,
scheduler_utils.setup_instance_group,
self.context, spec)
@mock.patch('nova.network.neutron.API.get_segment_ids_for_network')
def test_get_aggregates_for_routed_network(self, mock_get_segment_ids):
mock_get_segment_ids.return_value = [uuids.segment1, uuids.segment2]
report_client = report.SchedulerReportClient()
network_api = neutron.API()
def fake_get_provider_aggregates(context, segment_id):
agg = uuids.agg1 if segment_id == uuids.segment1 else uuids.agg2
agg_info = report.AggInfo(aggregates=[agg], generation=1)
return agg_info
with mock.patch.object(report_client, '_get_provider_aggregates',
side_effect=fake_get_provider_aggregates) as mock_get_aggs:
res = scheduler_utils.get_aggregates_for_routed_network(
self.context, network_api, report_client, uuids.network1)
self.assertEqual([uuids.agg1, uuids.agg2], res)
mock_get_segment_ids.assert_called_once_with(
self.context, uuids.network1)
mock_get_aggs.assert_has_calls(
[mock.call(self.context, uuids.segment1),
mock.call(self.context, uuids.segment2)])
@mock.patch('nova.network.neutron.API.get_segment_ids_for_network')
def test_get_aggregates_for_routed_network_none(self,
mock_get_segment_ids):
mock_get_segment_ids.return_value = []
report_client = report.SchedulerReportClient()
network_api = neutron.API()
self.assertEqual(
[],
scheduler_utils.get_aggregates_for_routed_network(
self.context, network_api, report_client, uuids.network1))
@mock.patch('nova.network.neutron.API.get_segment_ids_for_network')
def test_get_aggregates_for_routed_network_fails(self,
mock_get_segment_ids):
mock_get_segment_ids.return_value = [uuids.segment1]
report_client = report.SchedulerReportClient()
network_api = neutron.API()
# We could fail on some placement issue...
with mock.patch.object(report_client, '_get_provider_aggregates',
return_value=None):
self.assertRaises(
exception.InvalidRoutedNetworkConfiguration,
scheduler_utils.get_aggregates_for_routed_network,
self.context, network_api, report_client, uuids.network1)
# ... but we also want to fail if we can't find the related aggregate
agg_info = report.AggInfo(aggregates=set(), generation=1)
with mock.patch.object(report_client, '_get_provider_aggregates',
return_value=agg_info):
self.assertRaises(
exception.InvalidRoutedNetworkConfiguration,
scheduler_utils.get_aggregates_for_routed_network,
self.context, network_api, report_client, uuids.network1)
@mock.patch('nova.network.neutron.API.get_segment_id_for_subnet')
def test_get_aggregates_for_routed_subnet(self, mock_get_segment_ids):
mock_get_segment_ids.return_value = uuids.segment1
report_client = report.SchedulerReportClient()
network_api = neutron.API()
agg_info = report.AggInfo(aggregates=[uuids.agg1], generation=1)
with mock.patch.object(report_client, '_get_provider_aggregates',
return_value=agg_info) as mock_get_aggs:
res = scheduler_utils.get_aggregates_for_routed_subnet(
self.context, network_api, report_client,
uuids.subnet1)
self.assertEqual([uuids.agg1], res)
mock_get_segment_ids.assert_called_once_with(
self.context, uuids.subnet1)
mock_get_aggs.assert_called_once_with(self.context, uuids.segment1)
@mock.patch('nova.network.neutron.API.get_segment_id_for_subnet')
def test_get_aggregates_for_routed_subnet_none(self, mock_get_segment_ids):
mock_get_segment_ids.return_value = None
report_client = report.SchedulerReportClient()
network_api = neutron.API()
self.assertEqual(
[],
scheduler_utils.get_aggregates_for_routed_subnet(
self.context, network_api, report_client, uuids.subnet1))
@mock.patch('nova.network.neutron.API.get_segment_id_for_subnet')
def test_get_aggregates_for_routed_subnet_fails(self,
mock_get_segment_ids):
mock_get_segment_ids.return_value = uuids.segment1
report_client = report.SchedulerReportClient()
network_api = neutron.API()
# We could fail on some placement issue...
with mock.patch.object(report_client, '_get_provider_aggregates',
return_value=None):
self.assertRaises(
exception.InvalidRoutedNetworkConfiguration,
scheduler_utils.get_aggregates_for_routed_subnet,
self.context, network_api, report_client, uuids.subnet1)
# ... but we also want to fail if we can't find the related aggregate
agg_info = report.AggInfo(aggregates=set(), generation=1)
with mock.patch.object(report_client, '_get_provider_aggregates',
return_value=agg_info):
self.assertRaises(
exception.InvalidRoutedNetworkConfiguration,
scheduler_utils.get_aggregates_for_routed_subnet,
self.context, network_api, report_client, uuids.subnet1)