diff --git a/doc/source/admin/pci-passthrough.rst b/doc/source/admin/pci-passthrough.rst index 1523727499c5..62c714e4af66 100644 --- a/doc/source/admin/pci-passthrough.rst +++ b/doc/source/admin/pci-passthrough.rst @@ -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 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 +``_``. 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__`` as the +resource class in PCI inventories in Placement. + +For deeper technical details please read the `nova specification. `_ diff --git a/mypy-files.txt b/mypy-files.txt index 1b56b5e8ea18..5a3b9ab33998 100644 --- a/mypy-files.txt +++ b/mypy-files.txt @@ -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 diff --git a/nova/compute/pci_placement_translator.py b/nova/compute/pci_placement_translator.py new file mode 100644 index 000000000000..3dfbfd0bb0ba --- /dev/null +++ b/nova/compute/pci_placement_translator.py @@ -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 diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index 058777d1ed08..8f2fdd2aa528 100644 --- a/nova/compute/resource_tracker.py +++ b/nova/compute/resource_tracker.py @@ -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.""" diff --git a/nova/exception.py b/nova/exception.py index eedf2a4e7d55..58aced18c540 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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") diff --git a/nova/tests/fixtures/libvirt.py b/nova/tests/fixtures/libvirt.py index 07650f598497..b20ce17b5f2a 100644 --- a/nova/tests/fixtures/libvirt.py +++ b/nova/tests/fixtures/libvirt.py @@ -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: diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index fa80c860ef8e..91d99d7ec893 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -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( diff --git a/nova/tests/functional/libvirt/test_pci_in_placement.py b/nova/tests/functional/libvirt/test_pci_in_placement.py new file mode 100644 index 000000000000..a75b701b6b86 --- /dev/null +++ b/nova/tests/functional/libvirt/test_pci_in_placement.py @@ -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": [], + }, + ) diff --git a/nova/tests/functional/libvirt/test_pci_sriov_servers.py b/nova/tests/functional/libvirt/test_pci_sriov_servers.py index f95ff95a4879..b4a837ee5227 100644 --- a/nova/tests/functional/libvirt/test_pci_sriov_servers.py +++ b/nova/tests/functional/libvirt/test_pci_sriov_servers.py @@ -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 = { diff --git a/nova/tests/unit/compute/test_resource_tracker.py b/nova/tests/unit/compute/test_resource_tracker.py index 819dca9177db..f64c9719efa6 100644 --- a/nova/tests/unit/compute/test_resource_tracker.py +++ b/nova/tests/unit/compute/test_resource_tracker.py @@ -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):