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:
parent
e7aa436dc6
commit
c4b28a5496
|
@ -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")
|
||||
|
||||
|
|
|
@ -29,3 +29,4 @@ MIGRATING_ATTR = 'migrating_to'
|
|||
L3_NETWORK_TYPES = ['vxlan', 'gre', 'geneve']
|
||||
ALLOCATION = 'allocation'
|
||||
RESOURCE_REQUEST = 'resource_request'
|
||||
SEGMENT = 'Segment'
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue