Basics for PCI Placement reporting
A new PCI resource handler is added to the update_available_resources code path update the ProviderTree with PCI device RPs, inventories and traits. It is a bit different than the other Placement inventory reporter. It does not run in the virt driver level as PCI is tracked in a generic way in the PCI tracker in the resource tracker. So the virt specific information is already parsed and abstracted by the resource tracker. Another difference is that to support rolling upgrade the PCI handler code needs to be prepared for situations where the scheduler does not create PCI allocations even after some of the compute already started reporting inventories and started healing PCI allocations. So the code is not prepared to do a single, one shot, reshape at startup, but instead to do a continuous healing of the allocations. We can remove this continuous healing after the PCI prefilter will be made mandatory in a future release. The whole PCI placement reporting behavior is disabled by default while it is incomplete. When it is functionally complete a new [pci]report_in_placement config option will be added to allow enabling the feature. This config is intentionally not added by this patch as we don't want to allow enabling this logic yet. blueprint: pci-device-tracking-in-placement Change-Id: If975c3ec09ffa95f647eb4419874aa8417a59721
This commit is contained in:
parent
2b447b7236
commit
953f1eef19
@ -347,3 +347,20 @@ policy for any neutron SR-IOV interfaces attached by the user:
|
||||
You can also configure this for PCI passthrough devices by specifying the
|
||||
policy in the alias configuration via :oslo.config:option:`pci.alias`. For more
|
||||
information, refer to :oslo.config:option:`the documentation <pci.alias>`.
|
||||
|
||||
|
||||
PCI tracking in Placement
|
||||
-------------------------
|
||||
Since nova 26.0.0 (Zed) PCI passthrough device inventories are tracked in
|
||||
Placement. If a PCI device exists on the hypervisor and
|
||||
matches one of the device specifications configured via
|
||||
:oslo.config:option:`pci.device_spec` then Placement will have a representation
|
||||
of the device. Each PCI device of type ``type-PCI`` and ``type-PF`` will be
|
||||
modeled as a Placement resource provider (RP) with the name
|
||||
``<hypervisor_hostname>_<pci_address>``. A devices with type ``type-VF`` is
|
||||
represented by its parent PCI device, the PF, as resource provider.
|
||||
|
||||
By default nova will use ``CUSTOM_PCI_<vendor_id>_<product_id>`` as the
|
||||
resource class in PCI inventories in Placement.
|
||||
|
||||
For deeper technical details please read the `nova specification. <https://specs.openstack.org/openstack/nova-specs/specs/zed/approved/pci-device-tracking-in-placement.html>`_
|
||||
|
@ -1,4 +1,5 @@
|
||||
nova/compute/manager.py
|
||||
nova/compute/pci_placement_translator.py
|
||||
nova/crypto.py
|
||||
nova/limit/local.py
|
||||
nova/limit/placement.py
|
||||
|
231
nova/compute/pci_placement_translator.py
Normal file
231
nova/compute/pci_placement_translator.py
Normal file
@ -0,0 +1,231 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import typing as ty
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from nova.compute import provider_tree
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova.objects import fields
|
||||
from nova.objects import pci_device
|
||||
from nova.pci import manager as pci_manager
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Devs with this type are in one to one mapping with an RP in placement
|
||||
PARENT_TYPES = (
|
||||
fields.PciDeviceType.STANDARD, fields.PciDeviceType.SRIOV_PF)
|
||||
# Devs with these type need to have a parent and that parent is the one
|
||||
# that mapped to a placement RP
|
||||
CHILD_TYPES = (
|
||||
fields.PciDeviceType.SRIOV_VF, fields.PciDeviceType.VDPA)
|
||||
|
||||
|
||||
def _is_placement_tracking_enabled() -> bool:
|
||||
# This is false to act as a feature flag while we develop the feature
|
||||
# step by step. It will be replaced with a config check when the feature is
|
||||
# ready for production.
|
||||
#
|
||||
# return CONF.pci.report_in_placement
|
||||
|
||||
# Test code will mock this function to enable the feature in the test env
|
||||
return False
|
||||
|
||||
|
||||
def _get_rc_for_dev(dev: pci_device.PciDevice) -> str:
|
||||
return f"CUSTOM_PCI_{dev.vendor_id}_{dev.product_id}"
|
||||
|
||||
|
||||
class PciResourceProvider:
|
||||
"""A PCI Resource Provider"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.parent_dev = None
|
||||
self.children_devs: ty.List[pci_device.PciDevice] = []
|
||||
self.resource_class: ty.Optional[str] = None
|
||||
self.traits: ty.Optional[ty.Set[str]] = None
|
||||
|
||||
@property
|
||||
def devs(self) -> ty.List[pci_device.PciDevice]:
|
||||
return [self.parent_dev] if self.parent_dev else self.children_devs
|
||||
|
||||
def add_child(self, dev: pci_device.PciDevice) -> None:
|
||||
rc = _get_rc_for_dev(dev)
|
||||
self.children_devs.append(dev)
|
||||
self.resource_class = rc
|
||||
self.traits = set()
|
||||
|
||||
def add_parent(self, dev: pci_device.PciDevice) -> None:
|
||||
self.parent_dev = dev
|
||||
self.resource_class = _get_rc_for_dev(dev)
|
||||
self.traits = set()
|
||||
|
||||
def update_provider_tree(
|
||||
self, provider_tree: provider_tree.ProviderTree
|
||||
) -> None:
|
||||
provider_tree.update_inventory(
|
||||
self.name,
|
||||
# NOTE(gibi): The rest of the inventory fields (reserved,
|
||||
# allocation_ratio, etc.) are defaulted by placement and the
|
||||
# default value make sense for PCI devices, i.e. no overallocation
|
||||
# and PCI can be allocated one by one.
|
||||
# Also, this way if the operator sets reserved value in placement
|
||||
# for the PCI inventories directly then nova will not override that
|
||||
# value periodically.
|
||||
{
|
||||
self.resource_class: {
|
||||
"total": len(self.devs),
|
||||
"max_unit": len(self.devs),
|
||||
}
|
||||
},
|
||||
)
|
||||
provider_tree.update_traits(self.name, self.traits)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"RP({self.name}, {self.resource_class}={len(self.devs)}, "
|
||||
f"traits={','.join(self.traits or set())})"
|
||||
)
|
||||
|
||||
|
||||
class PlacementView:
|
||||
"""The PCI Placement view"""
|
||||
|
||||
def __init__(self, hypervisor_hostname: str) -> None:
|
||||
self.rps: ty.Dict[str, PciResourceProvider] = {}
|
||||
self.root_rp_name = hypervisor_hostname
|
||||
|
||||
def _get_rp_name_for_address(self, addr: str) -> str:
|
||||
return f"{self.root_rp_name}_{addr.upper()}"
|
||||
|
||||
def _ensure_rp(self, rp_name: str) -> PciResourceProvider:
|
||||
return self.rps.setdefault(rp_name, PciResourceProvider(rp_name))
|
||||
|
||||
def _add_child(self, dev: pci_device.PciDevice) -> None:
|
||||
if not dev.parent_addr:
|
||||
msg = _(
|
||||
"Missing parent address for PCI device s(dev)% with "
|
||||
"type s(type)s"
|
||||
) % {
|
||||
"dev": dev.address,
|
||||
"type": dev.dev_type,
|
||||
}
|
||||
raise exception.PlacementPciException(error=msg)
|
||||
|
||||
rp_name = self._get_rp_name_for_address(dev.parent_addr)
|
||||
self._ensure_rp(rp_name).add_child(dev)
|
||||
|
||||
def _add_parent(self, dev: pci_device.PciDevice) -> None:
|
||||
rp_name = self._get_rp_name_for_address(dev.address)
|
||||
self._ensure_rp(rp_name).add_parent(dev)
|
||||
|
||||
def add_dev(self, dev: pci_device.PciDevice) -> None:
|
||||
if dev.dev_type in PARENT_TYPES:
|
||||
self._add_parent(dev)
|
||||
elif dev.dev_type in CHILD_TYPES:
|
||||
self._add_child(dev)
|
||||
else:
|
||||
msg = _(
|
||||
"Unhandled PCI device type %(type)s for %(dev)s. Please "
|
||||
"report a bug."
|
||||
) % {
|
||||
"type": dev.dev_type,
|
||||
"dev": dev.address,
|
||||
}
|
||||
raise exception.PlacementPciException(error=msg)
|
||||
|
||||
if 'instance_uuid' in dev and dev.instance_uuid:
|
||||
# The device is allocated to an instance, so we need to make sure
|
||||
# the device will be allocated to the instance in placement too
|
||||
# FIXME(gibi): During migration the source host allocation should
|
||||
# be tight to the migration_uuid as consumer in placement. But
|
||||
# the PciDevice.instance_uuid is still pointing to the
|
||||
# instance_uuid both on the source and the dest. So we need to
|
||||
# check for running migrations.
|
||||
pass
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"Placement PCI view on {self.root_rp_name}: "
|
||||
f"{', '.join(str(rp) for rp in self.rps.values())}"
|
||||
)
|
||||
|
||||
def update_provider_tree(
|
||||
self, provider_tree: provider_tree.ProviderTree
|
||||
) -> None:
|
||||
for rp_name, rp in self.rps.items():
|
||||
if not provider_tree.exists(rp_name):
|
||||
provider_tree.new_child(rp_name, self.root_rp_name)
|
||||
|
||||
rp.update_provider_tree(provider_tree)
|
||||
|
||||
|
||||
def update_provider_tree_for_pci(
|
||||
provider_tree: provider_tree.ProviderTree,
|
||||
nodename: str,
|
||||
pci_tracker: pci_manager.PciDevTracker,
|
||||
allocations: dict,
|
||||
) -> bool:
|
||||
"""Based on the PciDevice objects in the pci_tracker it calculates what
|
||||
inventories and allocations needs to exist in placement and create the
|
||||
missing peaces.
|
||||
|
||||
It returns True if not just the provider_tree but also allocations needed
|
||||
to be changed.
|
||||
|
||||
:param allocations:
|
||||
Dict of allocation data of the form:
|
||||
{ $CONSUMER_UUID: {
|
||||
# The shape of each "allocations" dict below is identical
|
||||
# to the return from GET /allocations/{consumer_uuid}
|
||||
"allocations": {
|
||||
$RP_UUID: {
|
||||
"generation": $RP_GEN,
|
||||
"resources": {
|
||||
$RESOURCE_CLASS: $AMOUNT,
|
||||
...
|
||||
},
|
||||
},
|
||||
...
|
||||
},
|
||||
"project_id": $PROJ_ID,
|
||||
"user_id": $USER_ID,
|
||||
"consumer_generation": $CONSUMER_GEN,
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
if not _is_placement_tracking_enabled():
|
||||
# If tracking is not enabled we just return without touching anything
|
||||
return False
|
||||
|
||||
LOG.debug(
|
||||
'Collecting PCI inventories and allocations to track them in Placement'
|
||||
)
|
||||
|
||||
pv = PlacementView(nodename)
|
||||
for dev in pci_tracker.pci_devs:
|
||||
pv.add_dev(dev)
|
||||
|
||||
LOG.info("Placement PCI resource view: %s", pv)
|
||||
|
||||
pv.update_provider_tree(provider_tree)
|
||||
# FIXME(gibi): Check allocations too based on pci_dev.instance_uuid and
|
||||
# if here was any update then we have to return True to trigger a reshape.
|
||||
|
||||
return False
|
@ -30,6 +30,7 @@ import retrying
|
||||
|
||||
from nova.compute import claims
|
||||
from nova.compute import monitors
|
||||
from nova.compute import pci_placement_translator
|
||||
from nova.compute import provider_config
|
||||
from nova.compute import stats as compute_stats
|
||||
from nova.compute import task_states
|
||||
@ -1216,7 +1217,9 @@ class ResourceTracker(object):
|
||||
context, compute_node.uuid, name=compute_node.hypervisor_hostname)
|
||||
# Let the virt driver rearrange the provider tree and set/update
|
||||
# the inventory, traits, and aggregates throughout.
|
||||
allocs = None
|
||||
allocs = self.reportclient.get_allocations_for_provider_tree(
|
||||
context, nodename)
|
||||
driver_reshaped = False
|
||||
try:
|
||||
self.driver.update_provider_tree(prov_tree, nodename)
|
||||
except exception.ReshapeNeeded:
|
||||
@ -1227,10 +1230,9 @@ class ResourceTracker(object):
|
||||
LOG.info("Performing resource provider inventory and "
|
||||
"allocation data migration during compute service "
|
||||
"startup or fast-forward upgrade.")
|
||||
allocs = self.reportclient.get_allocations_for_provider_tree(
|
||||
context, nodename)
|
||||
self.driver.update_provider_tree(prov_tree, nodename,
|
||||
allocations=allocs)
|
||||
self.driver.update_provider_tree(
|
||||
prov_tree, nodename, allocations=allocs)
|
||||
driver_reshaped = True
|
||||
|
||||
# Inject driver capabilities traits into the provider
|
||||
# tree. We need to determine the traits that the virt
|
||||
@ -1251,15 +1253,39 @@ class ResourceTracker(object):
|
||||
context, nodename, provider_tree=prov_tree)
|
||||
prov_tree.update_traits(nodename, traits)
|
||||
|
||||
# NOTE(gibi): Tracking PCI in placement is different from other
|
||||
# resources.
|
||||
#
|
||||
# While driver.update_provider_tree is used to let the virt driver
|
||||
# create any kind of placement model for a resource the PCI data
|
||||
# modelling is done virt driver independently by the PCI tracker.
|
||||
# So the placement reporting needs to be also done here in the resource
|
||||
# tracker independently of the virt driver.
|
||||
#
|
||||
# Additionally, when PCI tracking in placement was introduced there was
|
||||
# already PCI allocations in nova. So both the PCI inventories and
|
||||
# allocations needs to be healed. Moreover, to support rolling upgrade
|
||||
# the placement prefilter for PCI devices was not turned on by default
|
||||
# at the first release of this feature. Therefore, there could be new
|
||||
# PCI allocation without placement being involved until the prefilter
|
||||
# is enabled. So we need to be ready to heal PCI allocations at
|
||||
# every call not just at startup.
|
||||
pci_reshaped = pci_placement_translator.update_provider_tree_for_pci(
|
||||
prov_tree, nodename, self.pci_tracker, allocs)
|
||||
|
||||
self.provider_tree = prov_tree
|
||||
|
||||
# This merges in changes from the provider config files loaded in init
|
||||
self._merge_provider_configs(self.provider_configs, prov_tree)
|
||||
|
||||
# Flush any changes. If we processed ReshapeNeeded above, allocs is not
|
||||
# None, and this will hit placement's POST /reshaper route.
|
||||
self.reportclient.update_from_provider_tree(context, prov_tree,
|
||||
allocations=allocs)
|
||||
# Flush any changes. If we either processed ReshapeNeeded above or
|
||||
# update_provider_tree_for_pci did reshape, then we need to pass allocs
|
||||
# to update_from_provider_tree to hit placement's POST /reshaper route.
|
||||
self.reportclient.update_from_provider_tree(
|
||||
context,
|
||||
prov_tree,
|
||||
allocations=allocs if driver_reshaped or pci_reshaped else None
|
||||
)
|
||||
|
||||
def _update(self, context, compute_node, startup=False):
|
||||
"""Update partial stats locally and populate them to Scheduler."""
|
||||
|
@ -2427,3 +2427,8 @@ class ProviderConfigException(NovaException):
|
||||
"""
|
||||
msg_fmt = _("An error occurred while processing "
|
||||
"a provider config file: %(error)s")
|
||||
|
||||
|
||||
class PlacementPciException(NovaException):
|
||||
msg_fmt = _(
|
||||
"Failed to gather or report PCI resources to Placement: %(error)s")
|
||||
|
2
nova/tests/fixtures/libvirt.py
vendored
2
nova/tests/fixtures/libvirt.py
vendored
@ -534,7 +534,7 @@ class HostPCIDevicesInfo(object):
|
||||
"""
|
||||
self.devices = {}
|
||||
|
||||
if not (num_vfs or num_pfs) and not num_mdevcap:
|
||||
if not (num_vfs or num_pfs or num_pci) and not num_mdevcap:
|
||||
return
|
||||
|
||||
if num_vfs and not num_pfs:
|
||||
|
@ -652,12 +652,16 @@ class PlacementHelperMixin:
|
||||
'/resource_providers', version='1.14'
|
||||
).body['resource_providers']
|
||||
|
||||
def _get_all_rp_uuids_in_a_tree(self, in_tree_rp_uuid):
|
||||
def _get_all_rps_in_a_tree(self, in_tree_rp_uuid):
|
||||
rps = self.placement.get(
|
||||
'/resource_providers?in_tree=%s' % in_tree_rp_uuid,
|
||||
version='1.20',
|
||||
).body['resource_providers']
|
||||
return [rp['uuid'] for rp in rps]
|
||||
return rps
|
||||
|
||||
def _get_all_rp_uuids_in_a_tree(self, in_tree_rp_uuid):
|
||||
return [
|
||||
rp['uuid'] for rp in self._get_all_rps_in_a_tree(in_tree_rp_uuid)]
|
||||
|
||||
def _post_resource_provider(self, rp_name):
|
||||
return self.placement.post(
|
||||
|
110
nova/tests/functional/libvirt/test_pci_in_placement.py
Normal file
110
nova/tests/functional/libvirt/test_pci_in_placement.py
Normal file
@ -0,0 +1,110 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from unittest import mock
|
||||
|
||||
import fixtures
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from nova.tests.fixtures import libvirt as fakelibvirt
|
||||
from nova.tests.functional.libvirt import test_pci_sriov_servers
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlacementPCIReportingTests(test_pci_sriov_servers._PCIServersTestBase):
|
||||
PCI_RC = f"CUSTOM_PCI_{fakelibvirt.PCI_VEND_ID}_{fakelibvirt.PCI_PROD_ID}"
|
||||
PF_RC = f"CUSTOM_PCI_{fakelibvirt.PCI_VEND_ID}_{fakelibvirt.PF_PROD_ID}"
|
||||
VF_RC = f"CUSTOM_PCI_{fakelibvirt.PCI_VEND_ID}_{fakelibvirt.VF_PROD_ID}"
|
||||
|
||||
# Just placeholders to satisfy the base class. The real value will be
|
||||
# redefined by the tests
|
||||
PCI_DEVICE_SPEC = []
|
||||
PCI_ALIAS = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
patcher = mock.patch(
|
||||
"nova.compute.pci_placement_translator."
|
||||
"_is_placement_tracking_enabled",
|
||||
return_value=True
|
||||
)
|
||||
self.addCleanup(patcher.stop)
|
||||
patcher.start()
|
||||
|
||||
# These tests should not depend on the host's sysfs
|
||||
self.useFixture(
|
||||
fixtures.MockPatch('nova.pci.utils.is_physical_function'))
|
||||
|
||||
@staticmethod
|
||||
def _to_device_spec_conf(spec_list):
|
||||
return [jsonutils.dumps(x) for x in spec_list]
|
||||
|
||||
def test_new_compute_init_with_pci_devs(self):
|
||||
"""A brand new compute is started with multiple pci devices configured
|
||||
for nova.
|
||||
"""
|
||||
# The fake libvirt will emulate on the host:
|
||||
# * two type-PCI devs (slot 0 and 1)
|
||||
# * two type-PFs (slot 2 and 3) with two type-VFs each
|
||||
pci_info = fakelibvirt.HostPCIDevicesInfo(
|
||||
num_pci=2, num_pfs=2, num_vfs=4)
|
||||
|
||||
# the emulated devices will then be filtered by the device_spec:
|
||||
device_spec = self._to_device_spec_conf(
|
||||
[
|
||||
# PCI_PROD_ID will match two type-PCI devs (slot 0, 1)
|
||||
{
|
||||
"vendor_id": fakelibvirt.PCI_VEND_ID,
|
||||
"product_id": fakelibvirt.PCI_PROD_ID,
|
||||
},
|
||||
# PF_PROD_ID + slot 2 will match one PF but not their children
|
||||
# VFs
|
||||
{
|
||||
"vendor_id": fakelibvirt.PCI_VEND_ID,
|
||||
"product_id": fakelibvirt.PF_PROD_ID,
|
||||
"address": "0000:81:02.0",
|
||||
},
|
||||
# VF_PROD_ID + slot 3 will match two VFs but not their parent
|
||||
# PF
|
||||
{
|
||||
"vendor_id": fakelibvirt.PCI_VEND_ID,
|
||||
"product_id": fakelibvirt.VF_PROD_ID,
|
||||
"address": "0000:81:03.*",
|
||||
},
|
||||
]
|
||||
)
|
||||
self.flags(group='pci', device_spec=device_spec)
|
||||
self.start_compute(hostname="compute1", pci_info=pci_info)
|
||||
|
||||
# Finally we assert that only the filtered devices are reported to
|
||||
# placement.
|
||||
self.assert_placement_pci_view(
|
||||
"compute1",
|
||||
inventories={
|
||||
"0000:81:00.0": {self.PCI_RC: 1},
|
||||
"0000:81:01.0": {self.PCI_RC: 1},
|
||||
"0000:81:02.0": {self.PF_RC: 1},
|
||||
# Note that the VF inventory is reported on the parent PF
|
||||
"0000:81:03.0": {self.VF_RC: 2},
|
||||
},
|
||||
traits={
|
||||
"0000:81:00.0": [],
|
||||
"0000:81:01.0": [],
|
||||
"0000:81:02.0": [],
|
||||
"0000:81:03.0": [],
|
||||
},
|
||||
)
|
@ -74,6 +74,50 @@ class _PCIServersTestBase(base.ServersTestBase):
|
||||
self.assertEqual(total, len(devices))
|
||||
self.assertEqual(free, len([d for d in devices if d.is_available()]))
|
||||
|
||||
def _get_rp_by_name(self, name, rps):
|
||||
for rp in rps:
|
||||
if rp["name"] == name:
|
||||
return rp
|
||||
self.fail(f'RP {name} is not found in Placement {rps}')
|
||||
|
||||
def assert_placement_pci_view(self, hostname, inventories, traits):
|
||||
compute_rp_uuid = self.compute_rp_uuids[hostname]
|
||||
rps = self._get_all_rps_in_a_tree(compute_rp_uuid)
|
||||
|
||||
# rps also contains the root provider so we subtract 1
|
||||
self.assertEqual(
|
||||
len(inventories),
|
||||
len(rps) - 1,
|
||||
f"Number of RPs on {hostname} doesn't match. "
|
||||
f"Expected {list(inventories)} actual {[rp['name'] for rp in rps]}"
|
||||
)
|
||||
|
||||
for rp_name, inv in inventories.items():
|
||||
real_rp_name = f'{hostname}_{rp_name}'
|
||||
rp = self._get_rp_by_name(real_rp_name, rps)
|
||||
rp_inv = self._get_provider_inventory(rp['uuid'])
|
||||
|
||||
self.assertEqual(
|
||||
len(inv),
|
||||
len(rp_inv),
|
||||
f"Number of inventories on {real_rp_name} are not as "
|
||||
f"expected. Expected {inv}, actual {rp_inv}"
|
||||
)
|
||||
for rc, total in inv.items():
|
||||
self.assertEqual(
|
||||
total,
|
||||
rp_inv[rc]["total"])
|
||||
self.assertEqual(
|
||||
total,
|
||||
rp_inv[rc]["max_unit"])
|
||||
|
||||
rp_traits = self._get_provider_traits(rp['uuid'])
|
||||
self.assertEqual(
|
||||
set(traits[rp_name]),
|
||||
set(rp_traits),
|
||||
f"Traits on RP {real_rp_name} does not match with expectation"
|
||||
)
|
||||
|
||||
|
||||
class _PCIServersWithMigrationTestBase(_PCIServersTestBase):
|
||||
|
||||
@ -1643,6 +1687,17 @@ class PCIServersTest(_PCIServersTestBase):
|
||||
'name': ALIAS_NAME,
|
||||
}
|
||||
)]
|
||||
PCI_RC = f"CUSTOM_PCI_{fakelibvirt.PCI_VEND_ID}_{fakelibvirt.PCI_PROD_ID}"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
patcher = mock.patch(
|
||||
"nova.compute.pci_placement_translator."
|
||||
"_is_placement_tracking_enabled",
|
||||
return_value=True
|
||||
)
|
||||
self.addCleanup(patcher.stop)
|
||||
patcher.start()
|
||||
|
||||
def test_create_server_with_pci_dev_and_numa(self):
|
||||
"""Verifies that an instance can be booted with cpu pinning and with an
|
||||
@ -1654,6 +1709,12 @@ class PCIServersTest(_PCIServersTestBase):
|
||||
pci_info = fakelibvirt.HostPCIDevicesInfo(num_pci=1, numa_node=1)
|
||||
self.start_compute(pci_info=pci_info)
|
||||
|
||||
self.assert_placement_pci_view(
|
||||
"compute1",
|
||||
inventories={"0000:81:00.0": {self.PCI_RC: 1}},
|
||||
traits={"0000:81:00.0": []},
|
||||
)
|
||||
|
||||
# create a flavor
|
||||
extra_spec = {
|
||||
'hw:cpu_policy': 'dedicated',
|
||||
@ -1673,6 +1734,12 @@ class PCIServersTest(_PCIServersTestBase):
|
||||
pci_info = fakelibvirt.HostPCIDevicesInfo(num_pci=1, numa_node=0)
|
||||
self.start_compute(pci_info=pci_info)
|
||||
|
||||
self.assert_placement_pci_view(
|
||||
"compute1",
|
||||
inventories={"0000:81:00.0": {self.PCI_RC: 1}},
|
||||
traits={"0000:81:00.0": []},
|
||||
)
|
||||
|
||||
# boot one instance with no PCI device to "fill up" NUMA node 0
|
||||
extra_spec = {'hw:cpu_policy': 'dedicated'}
|
||||
flavor_id = self._create_flavor(vcpu=4, extra_spec=extra_spec)
|
||||
@ -1695,10 +1762,23 @@ class PCIServersTest(_PCIServersTestBase):
|
||||
self.start_compute(
|
||||
hostname='test_compute0',
|
||||
pci_info=fakelibvirt.HostPCIDevicesInfo(num_pci=1))
|
||||
|
||||
self.assert_placement_pci_view(
|
||||
"test_compute0",
|
||||
inventories={"0000:81:00.0": {self.PCI_RC: 1}},
|
||||
traits={"0000:81:00.0": []},
|
||||
)
|
||||
|
||||
self.start_compute(
|
||||
hostname='test_compute1',
|
||||
pci_info=fakelibvirt.HostPCIDevicesInfo(num_pci=1))
|
||||
|
||||
self.assert_placement_pci_view(
|
||||
"test_compute1",
|
||||
inventories={"0000:81:00.0": {self.PCI_RC: 1}},
|
||||
traits={"0000:81:00.0": []},
|
||||
)
|
||||
|
||||
# create a server
|
||||
extra_spec = {'pci_passthrough:alias': f'{self.ALIAS_NAME}:1'}
|
||||
flavor_id = self._create_flavor(extra_spec=extra_spec)
|
||||
@ -1720,7 +1800,17 @@ class PCIServersTest(_PCIServersTestBase):
|
||||
self.start_compute(
|
||||
hostname='test_compute0',
|
||||
pci_info=fakelibvirt.HostPCIDevicesInfo(num_pci=1))
|
||||
self.assert_placement_pci_view(
|
||||
"test_compute0",
|
||||
inventories={"0000:81:00.0": {self.PCI_RC: 1}},
|
||||
traits={"0000:81:00.0": []},
|
||||
)
|
||||
self.start_compute(hostname='test_compute1')
|
||||
self.assert_placement_pci_view(
|
||||
"test_compute1",
|
||||
inventories={},
|
||||
traits={},
|
||||
)
|
||||
|
||||
# Boot a server with a single PCI device.
|
||||
extra_spec = {'pci_passthrough:alias': f'{self.ALIAS_NAME}:1'}
|
||||
@ -1782,6 +1872,17 @@ class PCIServersTest(_PCIServersTestBase):
|
||||
for hostname in ('test_compute0', 'test_compute1'):
|
||||
pci_info = fakelibvirt.HostPCIDevicesInfo(num_pci=2)
|
||||
self.start_compute(hostname=hostname, pci_info=pci_info)
|
||||
self.assert_placement_pci_view(
|
||||
hostname,
|
||||
inventories={
|
||||
"0000:81:00.0": {self.PCI_RC: 1},
|
||||
"0000:81:01.0": {self.PCI_RC: 1},
|
||||
},
|
||||
traits={
|
||||
"0000:81:00.0": [],
|
||||
"0000:81:01.0": [],
|
||||
},
|
||||
)
|
||||
|
||||
# boot an instance with a PCI device on each host
|
||||
extra_spec = {
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import ddt
|
||||
from unittest import mock
|
||||
|
||||
from keystoneauth1 import exceptions as ks_exc
|
||||
@ -1512,6 +1513,7 @@ class TestInitComputeNode(BaseTestCase):
|
||||
self.assertNotIn(_NODENAME, self.rt.old_resources)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestUpdateComputeNode(BaseTestCase):
|
||||
@mock.patch('nova.compute.resource_tracker.ResourceTracker.'
|
||||
'_sync_compute_service_disabled_trait', new=mock.Mock())
|
||||
@ -1769,6 +1771,128 @@ class TestUpdateComputeNode(BaseTestCase):
|
||||
# The retry is restricted to _update_to_placement
|
||||
self.assertEqual(1, mock_resource_change.call_count)
|
||||
|
||||
@mock.patch(
|
||||
'nova.compute.resource_tracker.ResourceTracker.'
|
||||
'_sync_compute_service_disabled_trait',
|
||||
new=mock.Mock()
|
||||
)
|
||||
@mock.patch(
|
||||
'nova.compute.resource_tracker.ResourceTracker._resource_change',
|
||||
new=mock.Mock(return_value=False)
|
||||
)
|
||||
@mock.patch(
|
||||
'nova.compute.pci_placement_translator.update_provider_tree_for_pci')
|
||||
def test_update_pci_reporting(self, mock_update_provider_tree_for_pci):
|
||||
"""Assert that resource tracker calls update_provider_tree_for_pci
|
||||
and that call did not change any allocations so
|
||||
update_from_provider_tree called without triggering reshape
|
||||
"""
|
||||
compute_obj = _COMPUTE_NODE_FIXTURES[0].obj_clone()
|
||||
self._setup_rt()
|
||||
ptree = self._setup_ptree(compute_obj)
|
||||
# simulate that pci reporting did not touch allocations
|
||||
mock_update_provider_tree_for_pci.return_value = False
|
||||
|
||||
self.rt._update(mock.sentinel.ctx, compute_obj)
|
||||
|
||||
mock_get_allocs = (
|
||||
self.report_client_mock.get_allocations_for_provider_tree)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
mock.sentinel.ctx, compute_obj.hypervisor_hostname)
|
||||
mock_update_provider_tree_for_pci.assert_called_once_with(
|
||||
ptree,
|
||||
compute_obj.hypervisor_hostname,
|
||||
self.rt.pci_tracker,
|
||||
mock_get_allocs.return_value,
|
||||
)
|
||||
upt = self.rt.reportclient.update_from_provider_tree
|
||||
upt.assert_called_once_with(mock.sentinel.ctx, ptree, allocations=None)
|
||||
|
||||
@mock.patch(
|
||||
'nova.compute.resource_tracker.ResourceTracker.'
|
||||
'_sync_compute_service_disabled_trait',
|
||||
new=mock.Mock()
|
||||
)
|
||||
@mock.patch(
|
||||
'nova.compute.resource_tracker.ResourceTracker._resource_change',
|
||||
new=mock.Mock(return_value=False)
|
||||
)
|
||||
@mock.patch(
|
||||
'nova.compute.pci_placement_translator.update_provider_tree_for_pci')
|
||||
def test_update_pci_reporting_reshape(
|
||||
self, mock_update_provider_tree_for_pci
|
||||
):
|
||||
"""Assert that resource tracker calls update_provider_tree_for_pci
|
||||
and that call changed allocations so
|
||||
update_from_provider_tree called with allocations to trigger reshape
|
||||
"""
|
||||
compute_obj = _COMPUTE_NODE_FIXTURES[0].obj_clone()
|
||||
self._setup_rt()
|
||||
ptree = self._setup_ptree(compute_obj)
|
||||
# simulate that pci reporting changed some allocations
|
||||
mock_update_provider_tree_for_pci.return_value = True
|
||||
|
||||
self.rt._update(mock.sentinel.ctx, compute_obj)
|
||||
|
||||
mock_get_allocs = (
|
||||
self.report_client_mock.get_allocations_for_provider_tree)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
mock.sentinel.ctx, compute_obj.hypervisor_hostname)
|
||||
mock_update_provider_tree_for_pci.assert_called_once_with(
|
||||
ptree,
|
||||
compute_obj.hypervisor_hostname,
|
||||
self.rt.pci_tracker,
|
||||
mock_get_allocs.return_value,
|
||||
)
|
||||
upt = self.rt.reportclient.update_from_provider_tree
|
||||
upt.assert_called_once_with(
|
||||
mock.sentinel.ctx, ptree, allocations=mock_get_allocs.return_value)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch(
|
||||
'nova.compute.resource_tracker.ResourceTracker.'
|
||||
'_sync_compute_service_disabled_trait',
|
||||
new=mock.Mock()
|
||||
)
|
||||
@mock.patch(
|
||||
'nova.compute.resource_tracker.ResourceTracker._resource_change',
|
||||
new=mock.Mock(return_value=False)
|
||||
)
|
||||
@mock.patch(
|
||||
'nova.compute.pci_placement_translator.update_provider_tree_for_pci')
|
||||
def test_update_pci_reporting_driver_reshape(
|
||||
self, pci_reshape, mock_update_provider_tree_for_pci
|
||||
):
|
||||
"""Assert that resource tracker first called the
|
||||
driver.update_provider_tree and that needed reshape so the allocations
|
||||
are pulled. Then independently of update_provider_tree_for_pci the
|
||||
update_from_provider_tree is called with the allocations to trigger
|
||||
reshape in placement
|
||||
"""
|
||||
compute_obj = _COMPUTE_NODE_FIXTURES[0].obj_clone()
|
||||
self._setup_rt()
|
||||
ptree = self._setup_ptree(compute_obj)
|
||||
# simulate that the driver requests reshape
|
||||
self.driver_mock.update_provider_tree.side_effect = [
|
||||
exc.ReshapeNeeded, None]
|
||||
mock_update_provider_tree_for_pci.return_value = pci_reshape
|
||||
|
||||
self.rt._update(mock.sentinel.ctx, compute_obj, startup=True)
|
||||
|
||||
mock_get_allocs = (
|
||||
self.report_client_mock.get_allocations_for_provider_tree)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
mock.sentinel.ctx, compute_obj.hypervisor_hostname)
|
||||
mock_update_provider_tree_for_pci.assert_called_once_with(
|
||||
ptree,
|
||||
compute_obj.hypervisor_hostname,
|
||||
self.rt.pci_tracker,
|
||||
mock_get_allocs.return_value,
|
||||
)
|
||||
upt = self.rt.reportclient.update_from_provider_tree
|
||||
upt.assert_called_once_with(
|
||||
mock.sentinel.ctx, ptree, allocations=mock_get_allocs.return_value)
|
||||
|
||||
@mock.patch('nova.objects.Service.get_by_compute_host',
|
||||
return_value=objects.Service(disabled=True))
|
||||
def test_sync_compute_service_disabled_trait_add(self, mock_get_by_host):
|
||||
|
Loading…
Reference in New Issue
Block a user