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:
Balazs Gibizer 2022-06-17 18:41:52 +02:00
parent 2b447b7236
commit 953f1eef19
10 changed files with 631 additions and 12 deletions

View File

@ -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>`_

View File

@ -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

View 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

View File

@ -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."""

View File

@ -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")

View File

@ -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:

View File

@ -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(

View 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": [],
},
)

View File

@ -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 = {

View File

@ -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):