Merge "Support interface attach / detach with new resource request format"
This commit is contained in:
commit
3bca00dbce
|
@ -44,14 +44,9 @@ Extended resource request
|
||||||
|
|
||||||
Since neutron 19.0.0 (Xena), neutron implements an extended resource request
|
Since neutron 19.0.0 (Xena), neutron implements an extended resource request
|
||||||
format via the the ``port-resource-request-groups`` neutron API extension. As
|
format via the the ``port-resource-request-groups`` neutron API extension. As
|
||||||
of nova 24.0.0 (Xena), nova does not fully support the new extension. If the
|
of nova 24.0.0 (Xena), nova also supports this extension if every nova-compute
|
||||||
extension is enabled in neutron, then nova will reject interface attach
|
service is upgraded to Xena version and the ``[upgrade_levels]/compute``
|
||||||
operation. Admins should not enable this API extension in neutron.
|
configuration does not prevent the computes from using the latest RPC version.
|
||||||
|
|
||||||
Please note that Nova only supports the server create operation if every
|
|
||||||
nova-compute service also upgraded to Xena version and the
|
|
||||||
``[upgrade_levels]/compute`` configuration does not prevent
|
|
||||||
the computes from using the latest RPC version.
|
|
||||||
|
|
||||||
See :nova-doc:`the admin guide <admin/port_with_resource_request.html>` for
|
See :nova-doc:`the admin guide <admin/port_with_resource_request.html>` for
|
||||||
administrative details.
|
administrative details.
|
||||||
|
|
|
@ -71,12 +71,8 @@ Extended resource request
|
||||||
|
|
||||||
Since neutron 19.0.0 (Xena), neutron implements an extended resource request
|
Since neutron 19.0.0 (Xena), neutron implements an extended resource request
|
||||||
format via the the ``port-resource-request-groups`` neutron API extension. As
|
format via the the ``port-resource-request-groups`` neutron API extension. As
|
||||||
of nova 24.0.0 (Xena), Nova does not fully support the new extension. If the
|
of nova 24.0.0 (Xena), nova also supports this extension if every nova-compute
|
||||||
extension is enabled in neutron, then nova will reject interface attach
|
service is upgraded to Xena version and the
|
||||||
operation. Admins should not enable this API extension in neutron.
|
|
||||||
|
|
||||||
Please note that Nova only supports the server create operation if every
|
|
||||||
nova-compute service also upgraded to Xena version and the
|
|
||||||
:oslo.config:option:`upgrade_levels.compute` configuration does not prevent
|
:oslo.config:option:`upgrade_levels.compute` configuration does not prevent
|
||||||
the computes from using the latest RPC version.
|
the computes from using the latest RPC version.
|
||||||
|
|
||||||
|
|
|
@ -177,7 +177,9 @@ class InterfaceAttachmentController(wsgi.Controller):
|
||||||
exception.NetworksWithQoSPolicyNotSupported,
|
exception.NetworksWithQoSPolicyNotSupported,
|
||||||
exception.InterfaceAttachPciClaimFailed,
|
exception.InterfaceAttachPciClaimFailed,
|
||||||
exception.InterfaceAttachResourceAllocationFailed,
|
exception.InterfaceAttachResourceAllocationFailed,
|
||||||
exception.ForbiddenPortsWithAccelerator) as e:
|
exception.ForbiddenPortsWithAccelerator,
|
||||||
|
exception.ExtendedResourceRequestOldCompute,
|
||||||
|
) as e:
|
||||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||||
except (
|
except (
|
||||||
exception.InstanceIsLocked,
|
exception.InstanceIsLocked,
|
||||||
|
|
|
@ -113,6 +113,7 @@ SUPPORT_VNIC_TYPE_ACCELERATOR = 57
|
||||||
|
|
||||||
MIN_COMPUTE_BOOT_WITH_EXTENDED_RESOURCE_REQUEST = 58
|
MIN_COMPUTE_BOOT_WITH_EXTENDED_RESOURCE_REQUEST = 58
|
||||||
MIN_COMPUTE_MOVE_WITH_EXTENDED_RESOURCE_REQUEST = 59
|
MIN_COMPUTE_MOVE_WITH_EXTENDED_RESOURCE_REQUEST = 59
|
||||||
|
MIN_COMPUTE_INT_ATTACH_WITH_EXTENDED_RES_REQ = 60
|
||||||
|
|
||||||
# FIXME(danms): Keep a global cache of the cells we find the
|
# FIXME(danms): Keep a global cache of the cells we find the
|
||||||
# first time we look. This needs to be refreshed on a timer or
|
# first time we look. This needs to be refreshed on a timer or
|
||||||
|
@ -5097,20 +5098,37 @@ class API:
|
||||||
self.volume_api.attachment_delete(
|
self.volume_api.attachment_delete(
|
||||||
context, new_attachment_id)
|
context, new_attachment_id)
|
||||||
|
|
||||||
def support_port_attach(self, context, port):
|
def ensure_compute_version_for_resource_request(
|
||||||
"""Returns false if neutron is configured with extended resource
|
self, context, instance, port
|
||||||
request and the port has resource request.
|
):
|
||||||
|
"""Checks that the compute service version is new enough for the
|
||||||
This function is only here temporary to help mocking this check in the
|
resource request of the port.
|
||||||
functional test environment.
|
"""
|
||||||
"""
|
if self.network_api.has_extended_resource_request_extension(
|
||||||
if not self.network_api.has_extended_resource_request_extension(
|
|
||||||
context
|
context
|
||||||
):
|
):
|
||||||
return True
|
# TODO(gibi): Remove this check in Y where we can be sure that
|
||||||
|
# the compute is already upgraded to X.
|
||||||
|
res_req = port.get(constants.RESOURCE_REQUEST) or {}
|
||||||
|
groups = res_req.get('request_groups', [])
|
||||||
|
if groups:
|
||||||
|
svc = objects.Service.get_by_host_and_binary(
|
||||||
|
context, instance.host, 'nova-compute')
|
||||||
|
if svc.version < MIN_COMPUTE_INT_ATTACH_WITH_EXTENDED_RES_REQ:
|
||||||
|
raise exception.ExtendedResourceRequestOldCompute()
|
||||||
|
|
||||||
resource_request = port.get('resource_request', {})
|
else:
|
||||||
return not resource_request.get('request_groups', [])
|
# NOTE(gibi): Checking if the requested port has resource request
|
||||||
|
# as such ports are only supported if the compute service version
|
||||||
|
# is >= 55.
|
||||||
|
# TODO(gibi): Remove this check in X as there we can be sure
|
||||||
|
# that all computes are new enough.
|
||||||
|
if port.get(constants.RESOURCE_REQUEST):
|
||||||
|
svc = objects.Service.get_by_host_and_binary(
|
||||||
|
context, instance.host, 'nova-compute')
|
||||||
|
if svc.version < 55:
|
||||||
|
raise exception.AttachInterfaceWithQoSPolicyNotSupported(
|
||||||
|
instance_uuid=instance.uuid)
|
||||||
|
|
||||||
@check_instance_lock
|
@check_instance_lock
|
||||||
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED,
|
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED,
|
||||||
|
@ -5124,18 +5142,6 @@ class API:
|
||||||
|
|
||||||
if port_id:
|
if port_id:
|
||||||
port = self.network_api.show_port(context, port_id)['port']
|
port = self.network_api.show_port(context, port_id)['port']
|
||||||
# NOTE(gibi): Checking if the requested port has resource request
|
|
||||||
# as such ports are only supported if the compute service version
|
|
||||||
# is >= 55.
|
|
||||||
# TODO(gibi): Remove this check in X as there we can be sure
|
|
||||||
# that all computes are new enough.
|
|
||||||
if port.get(constants.RESOURCE_REQUEST):
|
|
||||||
svc = objects.Service.get_by_host_and_binary(
|
|
||||||
context, instance.host, 'nova-compute')
|
|
||||||
if svc.version < 55:
|
|
||||||
raise exception.AttachInterfaceWithQoSPolicyNotSupported(
|
|
||||||
instance_uuid=instance.uuid)
|
|
||||||
|
|
||||||
if port.get('binding:vnic_type', "normal") == "vdpa":
|
if port.get('binding:vnic_type', "normal") == "vdpa":
|
||||||
# FIXME(sean-k-mooney): Attach works but detach results in a
|
# FIXME(sean-k-mooney): Attach works but detach results in a
|
||||||
# QEMU error; blocked until this is resolved
|
# QEMU error; blocked until this is resolved
|
||||||
|
@ -5148,8 +5154,8 @@ class API:
|
||||||
network_model.VNIC_TYPE_ACCELERATOR_DIRECT_PHYSICAL):
|
network_model.VNIC_TYPE_ACCELERATOR_DIRECT_PHYSICAL):
|
||||||
raise exception.ForbiddenPortsWithAccelerator()
|
raise exception.ForbiddenPortsWithAccelerator()
|
||||||
|
|
||||||
if not self.support_port_attach(context, port):
|
self.ensure_compute_version_for_resource_request(
|
||||||
raise exception.AttachWithExtendedQoSPolicyNotSupported()
|
context, instance, port)
|
||||||
|
|
||||||
return self.compute_rpcapi.attach_interface(context,
|
return self.compute_rpcapi.attach_interface(context,
|
||||||
instance=instance, network_id=network_id, port_id=port_id,
|
instance=instance, network_id=network_id, port_id=port_id,
|
||||||
|
|
|
@ -7650,27 +7650,28 @@ class ComputeManager(manager.Manager):
|
||||||
if not request_groups:
|
if not request_groups:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# NOTE(gibi): we assume a single RequestGroup here as:
|
|
||||||
# 1) there can only be a single port per interface attach request
|
|
||||||
# 2) a single port can only request resources in a single RequestGroup
|
|
||||||
# as per the current neutron API.
|
|
||||||
# #2) might change in the future so both
|
|
||||||
# nova.network.neutron.API.create_resource_requests() and this function
|
|
||||||
# takes a list of groups
|
|
||||||
request_group = request_groups[0]
|
|
||||||
|
|
||||||
# restrict the resource request to the current compute node. The
|
# restrict the resource request to the current compute node. The
|
||||||
# compute node uuid is the uuid of the root provider of the node in
|
# compute node uuid is the uuid of the root provider of the node in
|
||||||
# placement
|
# placement
|
||||||
compute_node_uuid = objects.ComputeNode.get_by_nodename(
|
compute_node_uuid = objects.ComputeNode.get_by_nodename(
|
||||||
context, instance.node).uuid
|
context, instance.node).uuid
|
||||||
request_group.in_tree = compute_node_uuid
|
# we can have multiple request groups, it would be enough to restrict
|
||||||
|
# only one of them to the compute tree but for symetry we restrict
|
||||||
|
# all of them
|
||||||
|
for request_group in request_groups:
|
||||||
|
request_group.in_tree = compute_node_uuid
|
||||||
|
|
||||||
# NOTE(gibi): when support is added for attaching a cyborg based
|
# NOTE(gibi): group policy is mandatory in a resource request if there
|
||||||
# smart NIC the ResourceRequest could be extended to handle multiple
|
# are multiple groups. The policy can only come from the flavor today
|
||||||
# request groups.
|
# and a new flavor is not provided with an interface attach request and
|
||||||
rr = scheduler_utils.ResourceRequest.from_request_group(
|
# the instance's current flavor might not have a policy. Still we are
|
||||||
request_group, request_level_params)
|
# attaching a single port where currently the two possible groups
|
||||||
|
# (one for bandwidth and one for packet rate) will always be allocated
|
||||||
|
# from different providers. So both possible policies (none, isolated)
|
||||||
|
# are always fulfilled for this single port. We still has to specify
|
||||||
|
# one so we specify the least restrictive now.
|
||||||
|
rr = scheduler_utils.ResourceRequest.from_request_groups(
|
||||||
|
request_groups, request_level_params, group_policy='none')
|
||||||
res = self.reportclient.get_allocation_candidates(context, rr)
|
res = self.reportclient.get_allocation_candidates(context, rr)
|
||||||
alloc_reqs, provider_sums, version = res
|
alloc_reqs, provider_sums, version = res
|
||||||
|
|
||||||
|
@ -10293,14 +10294,20 @@ class ComputeManager(manager.Manager):
|
||||||
{'intf': vif['id']},
|
{'intf': vif['id']},
|
||||||
instance=instance)
|
instance=instance)
|
||||||
profile = vif.get('profile', {}) or {} # profile can be None
|
profile = vif.get('profile', {}) or {} # profile can be None
|
||||||
if profile.get('allocation'):
|
rps = profile.get('allocation')
|
||||||
|
if rps:
|
||||||
|
if isinstance(rps, dict):
|
||||||
|
# if extended resource request extension is enabled
|
||||||
|
# then we have a dict of providers, flatten it for the
|
||||||
|
# log.
|
||||||
|
rps = ','.join(rps.values())
|
||||||
LOG.error(
|
LOG.error(
|
||||||
'The bound port %(port_id)s is deleted in Neutron but '
|
'The bound port %(port_id)s is deleted in Neutron but '
|
||||||
'the resource allocation on the resource provider '
|
'the resource allocation on the resource providers '
|
||||||
'%(rp_uuid)s is leaked until the server '
|
'%(rp_uuid)s are leaked until the server '
|
||||||
'%(server_uuid)s is deleted.',
|
'%(server_uuid)s is deleted.',
|
||||||
{'port_id': vif['id'],
|
{'port_id': vif['id'],
|
||||||
'rp_uuid': vif['profile']['allocation'],
|
'rp_uuid': rps,
|
||||||
'server_uuid': instance.uuid})
|
'server_uuid': instance.uuid})
|
||||||
|
|
||||||
del network_info[index]
|
del network_info[index]
|
||||||
|
|
|
@ -1754,15 +1754,27 @@ class API:
|
||||||
if port:
|
if port:
|
||||||
# if there is resource associated to this port then that needs to
|
# if there is resource associated to this port then that needs to
|
||||||
# be deallocated so lets return info about such allocation
|
# be deallocated so lets return info about such allocation
|
||||||
resource_request = port.get(constants.RESOURCE_REQUEST)
|
resource_request = port.get(constants.RESOURCE_REQUEST) or {}
|
||||||
profile = get_binding_profile(port)
|
profile = get_binding_profile(port)
|
||||||
allocated_rp = profile.get(constants.ALLOCATION)
|
if self.has_extended_resource_request_extension(context, neutron):
|
||||||
if resource_request and allocated_rp:
|
# new format
|
||||||
port_allocation = {
|
groups = resource_request.get(constants.REQUEST_GROUPS)
|
||||||
allocated_rp: {
|
if groups:
|
||||||
"resources": resource_request.get("resources", {})
|
allocated_rps = profile.get(constants.ALLOCATION)
|
||||||
|
for group in groups:
|
||||||
|
allocated_rp = allocated_rps[group['id']]
|
||||||
|
port_allocation[allocated_rp] = {
|
||||||
|
"resources": group.get("resources", {})
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# legacy format
|
||||||
|
allocated_rp = profile.get(constants.ALLOCATION)
|
||||||
|
if resource_request and allocated_rp:
|
||||||
|
port_allocation = {
|
||||||
|
allocated_rp: {
|
||||||
|
"resources": resource_request.get("resources", {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
# Check the info_cache. If the port is still in the info_cache and
|
# Check the info_cache. If the port is still in the info_cache and
|
||||||
# in that cache there is allocation in the profile then we suspect
|
# in that cache there is allocation in the profile then we suspect
|
||||||
|
|
|
@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# NOTE(danms): This is the global service version counter
|
# NOTE(danms): This is the global service version counter
|
||||||
SERVICE_VERSION = 59
|
SERVICE_VERSION = 60
|
||||||
|
|
||||||
|
|
||||||
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
|
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
|
||||||
|
@ -209,6 +209,10 @@ SERVICE_VERSION_HISTORY = (
|
||||||
# Add support for server move operations with neutron extended resource
|
# Add support for server move operations with neutron extended resource
|
||||||
# request
|
# request
|
||||||
{'compute_rpc': '6.0'},
|
{'compute_rpc': '6.0'},
|
||||||
|
# Version 60: Compute RPC v6.0:
|
||||||
|
# Add support for interface attach operation with neutron extended resource
|
||||||
|
# request
|
||||||
|
{'compute_rpc': '6.0'},
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is used to raise an error at service startup if older than N-1 computes
|
# This is used to raise an error at service startup if older than N-1 computes
|
||||||
|
|
|
@ -199,17 +199,22 @@ class ResourceRequest(object):
|
||||||
return res_req
|
return res_req
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request_group(
|
def from_request_groups(
|
||||||
cls,
|
cls,
|
||||||
request_group: 'objects.RequestGroup',
|
request_groups: ty.List['objects.RequestGroup'],
|
||||||
request_level_params: 'objects.RequestLevelParams',
|
request_level_params: 'objects.RequestLevelParams',
|
||||||
|
group_policy: str,
|
||||||
) -> 'ResourceRequest':
|
) -> 'ResourceRequest':
|
||||||
"""Create a new instance of ResourceRequest from a RequestGroup."""
|
"""Create a new instance of ResourceRequest from a list of
|
||||||
|
RequestGroup objects.
|
||||||
|
"""
|
||||||
res_req = cls()
|
res_req = cls()
|
||||||
res_req._root_required = request_level_params.root_required
|
res_req._root_required = request_level_params.root_required
|
||||||
res_req._root_forbidden = request_level_params.root_forbidden
|
res_req._root_forbidden = request_level_params.root_forbidden
|
||||||
res_req._same_subtree = request_level_params.same_subtree
|
res_req._same_subtree = request_level_params.same_subtree
|
||||||
res_req._add_request_group(request_group)
|
res_req.group_policy = group_policy
|
||||||
|
for request_group in request_groups:
|
||||||
|
res_req._add_request_group(request_group)
|
||||||
res_req.strip_zeros()
|
res_req.strip_zeros()
|
||||||
return res_req
|
return res_req
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ Provides common functionality for integrated unit tests
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -258,9 +259,9 @@ class InstanceHelperMixin:
|
||||||
server['id'], expected_statuses, actual_status,
|
server['id'], expected_statuses, actual_status,
|
||||||
))
|
))
|
||||||
|
|
||||||
def _wait_for_log(self, log_line):
|
def _wait_for_log(self, log_line_regex):
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
if log_line in self.stdlog.logger.output:
|
if re.search(log_line_regex, self.stdlog.logger.output):
|
||||||
return
|
return
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import unittest
|
|
||||||
|
|
||||||
from keystoneauth1 import adapter
|
from keystoneauth1 import adapter
|
||||||
import mock
|
import mock
|
||||||
|
@ -1342,16 +1341,12 @@ class PortResourceRequestBasedSchedulingTest(
|
||||||
response = self.api.api_post('/os-server-external-events', events).body
|
response = self.api.api_post('/os-server-external-events', events).body
|
||||||
self.assertEqual(200, response['events'][0]['code'])
|
self.assertEqual(200, response['events'][0]['code'])
|
||||||
|
|
||||||
port_rp_uuid = self.ovs_bridge_rp_per_host[self.compute1_rp_uuid]
|
|
||||||
|
|
||||||
# 1) Nova logs an ERROR about the leak
|
# 1) Nova logs an ERROR about the leak
|
||||||
self._wait_for_log(
|
self._wait_for_log(
|
||||||
'ERROR [nova.compute.manager] The bound port %(port_id)s is '
|
'The bound port %(port_id)s is deleted in Neutron but the '
|
||||||
'deleted in Neutron but the resource allocation on the resource '
|
'resource allocation on the resource providers .* are leaked '
|
||||||
'provider %(rp_uuid)s is leaked until the server %(server_uuid)s '
|
'until the server %(server_uuid)s is deleted.'
|
||||||
'is deleted.'
|
|
||||||
% {'port_id': port['id'],
|
% {'port_id': port['id'],
|
||||||
'rp_uuid': port_rp_uuid,
|
|
||||||
'server_uuid': server['id']})
|
'server_uuid': server['id']})
|
||||||
|
|
||||||
allocations = self.placement.get(
|
allocations = self.placement.get(
|
||||||
|
@ -1635,8 +1630,6 @@ class ExtendedPortResourceRequestBasedSchedulingTestBase(
|
||||||
port['id'], group_req, pps_allocations)
|
port['id'], group_req, pps_allocations)
|
||||||
|
|
||||||
|
|
||||||
# TODO(gibi): The tests are failing today as we need to fix a bunch of TODOs in
|
|
||||||
# the code.
|
|
||||||
class MultiGroupResourceRequestBasedSchedulingTest(
|
class MultiGroupResourceRequestBasedSchedulingTest(
|
||||||
ExtendedPortResourceRequestBasedSchedulingTestBase,
|
ExtendedPortResourceRequestBasedSchedulingTestBase,
|
||||||
PortResourceRequestBasedSchedulingTest,
|
PortResourceRequestBasedSchedulingTest,
|
||||||
|
@ -1650,31 +1643,6 @@ class MultiGroupResourceRequestBasedSchedulingTest(
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.neutron = self.useFixture(
|
self.neutron = self.useFixture(
|
||||||
MultiGroupResourceRequestNeutronFixture(self))
|
MultiGroupResourceRequestNeutronFixture(self))
|
||||||
# Turn off the blanket rejections of the extended resource request in
|
|
||||||
# port attach. This test class wants to prove that the extended
|
|
||||||
# resource request is supported.
|
|
||||||
patcher = mock.patch(
|
|
||||||
'nova.compute.api.API.support_port_attach',
|
|
||||||
return_value=True,
|
|
||||||
)
|
|
||||||
self.addCleanup(patcher.stop)
|
|
||||||
patcher.start()
|
|
||||||
|
|
||||||
@unittest.expectedFailure
|
|
||||||
def test_interface_attach_with_resource_request(self):
|
|
||||||
super().test_interface_attach_with_resource_request()
|
|
||||||
|
|
||||||
@unittest.expectedFailure
|
|
||||||
def test_interface_attach_with_resource_request_no_candidates(self):
|
|
||||||
super().test_interface_attach_with_resource_request_no_candidates()
|
|
||||||
|
|
||||||
@unittest.expectedFailure
|
|
||||||
def test_interface_detach_with_port_with_bandwidth_request(self):
|
|
||||||
super().test_interface_detach_with_port_with_bandwidth_request()
|
|
||||||
|
|
||||||
@unittest.expectedFailure
|
|
||||||
def test_delete_bound_port_in_neutron_with_resource_request(self):
|
|
||||||
super().test_delete_bound_port_in_neutron_with_resource_request()
|
|
||||||
|
|
||||||
|
|
||||||
class ServerMoveWithPortResourceRequestTest(
|
class ServerMoveWithPortResourceRequestTest(
|
||||||
|
@ -2959,51 +2927,26 @@ class ExtendedResourceRequestOldCompute(
|
||||||
lambda server: shelve_offload_then_unshelve(server),
|
lambda server: shelve_offload_then_unshelve(server),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mock.patch('nova.objects.service.Service.get_by_host_and_binary')
|
||||||
class ExtendedResourceRequestTempNegativeTest(
|
def test_interface_attach(self, mock_get_service):
|
||||||
PortResourceRequestBasedSchedulingTestBase):
|
# service version 59 allows booting
|
||||||
"""A set of temporary tests to show that nova currently rejects requests
|
mock_get_service.return_value.version = 59
|
||||||
that uses the extended-resource-request Neutron API extension. These test
|
|
||||||
are expected to be removed when support for the extension is implemented
|
|
||||||
in nova.
|
|
||||||
"""
|
|
||||||
def _test_operation(self, op_name, op_callable):
|
|
||||||
# boot a server with a qos port still using the old Neutron resource
|
|
||||||
# request API extension
|
|
||||||
server = self._create_server(
|
server = self._create_server(
|
||||||
flavor=self.flavor,
|
flavor=self.flavor,
|
||||||
networks=[{'port': self.neutron.port_with_resource_request['id']}],
|
networks=[{'port': self.neutron.port_1['id']}],
|
||||||
)
|
)
|
||||||
self._wait_for_state_change(server, 'ACTIVE')
|
self._wait_for_state_change(server, "ACTIVE")
|
||||||
|
# for interface attach service version 60 would be needed
|
||||||
# enable the new extended-resource-request Neutron API extension by
|
|
||||||
# replacing the old neutron fixture with a new one that enables the
|
|
||||||
# extension. Note that we are carrying over the state of the neutron
|
|
||||||
# to the new extension to keep the port bound to the server.
|
|
||||||
self.neutron = self.useFixture(
|
|
||||||
ExtendedResourceRequestNeutronFixture.
|
|
||||||
create_with_existing_neutron_state(self.neutron))
|
|
||||||
|
|
||||||
# nova does not support this Neutron API extension yet so the
|
|
||||||
# operation fails
|
|
||||||
ex = self.assertRaises(
|
ex = self.assertRaises(
|
||||||
client.OpenStackApiException,
|
client.OpenStackApiException,
|
||||||
op_callable,
|
self._attach_interface,
|
||||||
server,
|
server,
|
||||||
|
self.neutron.port_with_sriov_resource_request['id'],
|
||||||
)
|
)
|
||||||
self.assertEqual(400, ex.response.status_code)
|
self.assertEqual(400, ex.response.status_code)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f'The {op_name} with port having extended resource request, like '
|
'The port-resource-request-groups neutron API extension is not '
|
||||||
f'a port with both QoS minimum bandwidth and packet rate '
|
'supported by old nova compute service. Upgrade your compute '
|
||||||
f'policies, is not yet supported.',
|
'services to Xena (24.0.0) or later.',
|
||||||
str(ex)
|
str(ex)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_interface_attach(self):
|
|
||||||
self._test_operation(
|
|
||||||
'interface attach server operation',
|
|
||||||
lambda server: self._attach_interface(
|
|
||||||
server,
|
|
||||||
self.neutron.port_with_sriov_resource_request['id'],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
|
@ -7267,6 +7267,10 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
|
||||||
mock_record.assert_called_once_with(
|
mock_record.assert_called_once_with(
|
||||||
self.context, instance, instance_actions.ATTACH_INTERFACE)
|
self.context, instance, instance_actions.ATTACH_INTERFACE)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.network.neutron.API.has_extended_resource_request_extension',
|
||||||
|
new=mock.Mock(return_value=False),
|
||||||
|
)
|
||||||
@mock.patch('nova.objects.service.Service.get_by_host_and_binary')
|
@mock.patch('nova.objects.service.Service.get_by_host_and_binary')
|
||||||
@mock.patch('nova.compute.api.API._record_action_start')
|
@mock.patch('nova.compute.api.API._record_action_start')
|
||||||
def test_attach_interface_qos_aware_port_old_compute(
|
def test_attach_interface_qos_aware_port_old_compute(
|
||||||
|
@ -7330,6 +7334,84 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
|
||||||
port_id=mock.sentinel.port_id, requested_ip=mock.sentinel.ip,
|
port_id=mock.sentinel.port_id, requested_ip=mock.sentinel.ip,
|
||||||
tag=mock.sentinel.tag)
|
tag=mock.sentinel.tag)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.network.neutron.API.has_extended_resource_request_extension',
|
||||||
|
new=mock.Mock(return_value=True),
|
||||||
|
)
|
||||||
|
@mock.patch('nova.objects.service.Service.get_by_host_and_binary')
|
||||||
|
@mock.patch('nova.compute.api.API._record_action_start')
|
||||||
|
def test_attach_interface_extended_qos_port_old_compute(
|
||||||
|
self, mock_record, mock_get_service
|
||||||
|
):
|
||||||
|
instance = self._create_instance_obj()
|
||||||
|
service = objects.Service()
|
||||||
|
service.version = 59
|
||||||
|
mock_get_service.return_value = service
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute_api.network_api, 'show_port',
|
||||||
|
return_value={
|
||||||
|
'port': {
|
||||||
|
constants.RESOURCE_REQUEST: {
|
||||||
|
'request_groups': [
|
||||||
|
{
|
||||||
|
'resources': {'CUSTOM_RESOURCE_CLASS': 42}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as mock_show_port:
|
||||||
|
self.assertRaises(
|
||||||
|
nova.exception.ExtendedResourceRequestOldCompute,
|
||||||
|
self.compute_api.attach_interface,
|
||||||
|
self.context, instance,
|
||||||
|
'foo_net_id', 'foo_port_id', None
|
||||||
|
)
|
||||||
|
mock_show_port.assert_called_once_with(self.context, 'foo_port_id')
|
||||||
|
mock_get_service.assert_called_once_with(
|
||||||
|
self.context, instance.host, 'nova-compute')
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.network.neutron.API.has_extended_resource_request_extension',
|
||||||
|
new=mock.Mock(return_value=True)
|
||||||
|
)
|
||||||
|
@mock.patch('nova.compute.rpcapi.ComputeAPI.attach_interface')
|
||||||
|
@mock.patch('nova.objects.service.Service.get_by_host_and_binary')
|
||||||
|
@mock.patch('nova.compute.api.API._record_action_start')
|
||||||
|
def test_attach_interface_extended_qos_port(
|
||||||
|
self, mock_record, mock_get_service, mock_attach
|
||||||
|
):
|
||||||
|
instance = self._create_instance_obj()
|
||||||
|
service = objects.Service()
|
||||||
|
service.version = 60
|
||||||
|
mock_get_service.return_value = service
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute_api.network_api, 'show_port',
|
||||||
|
return_value={
|
||||||
|
'port': {
|
||||||
|
constants.RESOURCE_REQUEST: {
|
||||||
|
'request_groups': [
|
||||||
|
{
|
||||||
|
'resources': {'CUSTOM_RESOURCE_CLASS': 42}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as mock_show_port:
|
||||||
|
self.compute_api.attach_interface(
|
||||||
|
self.context, instance, mock.sentinel.net_id,
|
||||||
|
mock.sentinel.port_id, mock.sentinel.ip, mock.sentinel.tag)
|
||||||
|
|
||||||
|
mock_show_port.assert_called_once_with(
|
||||||
|
self.context, mock.sentinel.port_id)
|
||||||
|
mock_get_service.assert_called_once_with(
|
||||||
|
self.context, instance.host, 'nova-compute')
|
||||||
|
mock_attach.assert_called_once_with(
|
||||||
|
self.context, instance=instance, network_id=mock.sentinel.net_id,
|
||||||
|
port_id=mock.sentinel.port_id, requested_ip=mock.sentinel.ip,
|
||||||
|
tag=mock.sentinel.tag)
|
||||||
|
|
||||||
@mock.patch('nova.compute.api.API._record_action_start')
|
@mock.patch('nova.compute.api.API._record_action_start')
|
||||||
@mock.patch.object(compute_rpcapi.ComputeAPI, 'detach_interface')
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'detach_interface')
|
||||||
def test_detach_interface(self, mock_detach, mock_record):
|
def test_detach_interface(self, mock_detach, mock_record):
|
||||||
|
|
|
@ -5412,6 +5412,10 @@ class TestAPI(TestAPIBase):
|
||||||
vif.destroy.assert_called_once_with()
|
vif.destroy.assert_called_once_with()
|
||||||
self.assertEqual({}, port_allocation)
|
self.assertEqual({}, port_allocation)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.network.neutron.API.has_extended_resource_request_extension',
|
||||||
|
new=mock.Mock(return_value=False),
|
||||||
|
)
|
||||||
@mock.patch('nova.network.neutron.API.get_instance_nw_info')
|
@mock.patch('nova.network.neutron.API.get_instance_nw_info')
|
||||||
@mock.patch('nova.network.neutron.API._delete_nic_metadata')
|
@mock.patch('nova.network.neutron.API._delete_nic_metadata')
|
||||||
@mock.patch.object(objects.VirtualInterface, 'get_by_uuid')
|
@mock.patch.object(objects.VirtualInterface, 'get_by_uuid')
|
||||||
|
@ -5458,6 +5462,62 @@ class TestAPI(TestAPIBase):
|
||||||
},
|
},
|
||||||
port_allocation)
|
port_allocation)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'nova.network.neutron.API.has_extended_resource_request_extension',
|
||||||
|
new=mock.Mock(return_value=True),
|
||||||
|
)
|
||||||
|
@mock.patch('nova.network.neutron.API.get_instance_nw_info')
|
||||||
|
@mock.patch('nova.network.neutron.API._delete_nic_metadata')
|
||||||
|
@mock.patch.object(objects.VirtualInterface, 'get_by_uuid')
|
||||||
|
@mock.patch('nova.network.neutron.get_client')
|
||||||
|
def test_deallocate_port_for_instance_port_with_extended_allocation(
|
||||||
|
self, mock_get_client, mock_get_vif_by_uuid, mock_del_nic_meta,
|
||||||
|
mock_netinfo):
|
||||||
|
mock_inst = mock.Mock(project_id="proj-1",
|
||||||
|
availability_zone='zone-1',
|
||||||
|
uuid='inst-1')
|
||||||
|
mock_inst.get_network_info.return_value = [
|
||||||
|
model.VIF(id=uuids.port_uid, preserve_on_delete=True)
|
||||||
|
]
|
||||||
|
vif = objects.VirtualInterface()
|
||||||
|
vif.tag = 'foo'
|
||||||
|
vif.destroy = mock.MagicMock()
|
||||||
|
mock_get_vif_by_uuid.return_value = vif
|
||||||
|
|
||||||
|
mock_client = mock.Mock()
|
||||||
|
mock_client.show_port.return_value = {
|
||||||
|
'port': {
|
||||||
|
constants.RESOURCE_REQUEST: {
|
||||||
|
'request_groups': [
|
||||||
|
{
|
||||||
|
'id': uuids.group1,
|
||||||
|
'resources': {
|
||||||
|
'NET_BW_EGR_KILOBIT_PER_SEC': 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'binding:profile': {
|
||||||
|
'allocation': {uuids.group1: uuids.rp1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_get_client.return_value = mock_client
|
||||||
|
|
||||||
|
_, port_allocation = self.api.deallocate_port_for_instance(
|
||||||
|
mock.sentinel.ctx, mock_inst, uuids.port_id)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
uuids.rp1: {
|
||||||
|
"resources": {
|
||||||
|
'NET_BW_EGR_KILOBIT_PER_SEC': 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
port_allocation
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch('nova.network.neutron.API.get_instance_nw_info')
|
@mock.patch('nova.network.neutron.API.get_instance_nw_info')
|
||||||
@mock.patch('nova.network.neutron.API._delete_nic_metadata')
|
@mock.patch('nova.network.neutron.API._delete_nic_metadata')
|
||||||
@mock.patch.object(objects.VirtualInterface, 'get_by_uuid')
|
@mock.patch.object(objects.VirtualInterface, 'get_by_uuid')
|
||||||
|
|
|
@ -1357,36 +1357,53 @@ class TestUtils(TestUtilsBase):
|
||||||
rr = utils.ResourceRequest.from_request_spec(rs)
|
rr = utils.ResourceRequest.from_request_spec(rs)
|
||||||
self.assertResourceRequestsEqual(expected, rr)
|
self.assertResourceRequestsEqual(expected, rr)
|
||||||
|
|
||||||
def test_resource_request_from_request_group(self):
|
def test_resource_request_from_request_groups(self):
|
||||||
rg = objects.RequestGroup.from_port_request(
|
rgs = objects.RequestGroup.from_extended_port_request(
|
||||||
self.context,
|
self.context,
|
||||||
uuids.port_id,
|
|
||||||
port_resource_request={
|
port_resource_request={
|
||||||
"resources": {
|
"request_groups": [
|
||||||
"NET_BW_IGR_KILOBIT_PER_SEC": 1000,
|
{
|
||||||
"NET_BW_EGR_KILOBIT_PER_SEC": 1000},
|
"id": "group1",
|
||||||
"required": ["CUSTOM_PHYSNET_2",
|
"resources": {
|
||||||
"CUSTOM_VNIC_TYPE_NORMAL"]
|
"NET_BW_IGR_KILOBIT_PER_SEC": 1000,
|
||||||
|
"NET_BW_EGR_KILOBIT_PER_SEC": 1000},
|
||||||
|
"required": ["CUSTOM_PHYSNET_2",
|
||||||
|
"CUSTOM_VNIC_TYPE_NORMAL"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "group2",
|
||||||
|
"resources": {
|
||||||
|
"NET_PACKET_RATE_KILOPACKET_PER_SEC": 100,
|
||||||
|
},
|
||||||
|
"required": ["CUSTOM_VNIC_TYPE_NORMAL"],
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
req_lvl_params = objects.RequestLevelParams(
|
req_lvl_params = objects.RequestLevelParams(
|
||||||
root_required={"CUSTOM_BLUE"},
|
root_required={"CUSTOM_BLUE"},
|
||||||
root_forbidden={"CUSTOM_DIRTY"},
|
root_forbidden={"CUSTOM_DIRTY"},
|
||||||
same_subtree=[[uuids.group1]],
|
same_subtree=[["group1", "group2"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
rr = utils.ResourceRequest.from_request_group(rg, req_lvl_params)
|
rr = utils.ResourceRequest.from_request_groups(
|
||||||
|
rgs, req_lvl_params, 'none')
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
f'limit=1000&'
|
'group_policy=none&'
|
||||||
f'required{uuids.port_id}='
|
'limit=1000&'
|
||||||
f'CUSTOM_PHYSNET_2%2C'
|
'requiredgroup1='
|
||||||
f'CUSTOM_VNIC_TYPE_NORMAL&'
|
'CUSTOM_PHYSNET_2%2C'
|
||||||
f'resources{uuids.port_id}='
|
'CUSTOM_VNIC_TYPE_NORMAL&'
|
||||||
f'NET_BW_EGR_KILOBIT_PER_SEC%3A1000%2C'
|
'requiredgroup2='
|
||||||
f'NET_BW_IGR_KILOBIT_PER_SEC%3A1000&'
|
'CUSTOM_VNIC_TYPE_NORMAL&'
|
||||||
f'root_required=CUSTOM_BLUE%2C%21CUSTOM_DIRTY&'
|
'resourcesgroup1='
|
||||||
f'same_subtree={uuids.group1}',
|
'NET_BW_EGR_KILOBIT_PER_SEC%3A1000%2C'
|
||||||
|
'NET_BW_IGR_KILOBIT_PER_SEC%3A1000&'
|
||||||
|
'resourcesgroup2='
|
||||||
|
'NET_PACKET_RATE_KILOPACKET_PER_SEC%3A100&'
|
||||||
|
'root_required=CUSTOM_BLUE%2C%21CUSTOM_DIRTY&'
|
||||||
|
'same_subtree=group1%2Cgroup2',
|
||||||
rr.to_querystring())
|
rr.to_querystring())
|
||||||
|
|
||||||
def test_resource_request_add_group_inserts_the_group(self):
|
def test_resource_request_add_group_inserts_the_group(self):
|
||||||
|
|
Loading…
Reference in New Issue