From ba3388d666ad344e87de3bbf8f76ab494bb4421f Mon Sep 17 00:00:00 2001 From: Wang Huaqiang Date: Thu, 14 May 2020 15:55:24 +0800 Subject: [PATCH] objects: Introduce the 'CPUAllocationPolicy.MIXED' enum Introduce a 'mixed' instance CPU allocation policy and will be worked with upcoming patches, for purpose of creating an instance combined shared CPUs with dedicated or realtime CPUs. In an instance mixed with different type of CPUs, the shared CPU shared CPU time slots with other instances, and also might be a CPU with less or un-guaranteed hardware resources, which implies to have no guarantee for the behavior of the workload running on it. If we call the shared CPU as 'low priority' CPU, then the realtime or dedicated CPU could be called as 'high priority' CPU, user could assign more hardware CPU resources or place some guaranteed resource to it to let the workload to entail high performance or stable service quality. Based on https://review.opendev.org/714704 Part of blueprint use-pcpu-and-vcpu-in-one-instance Change-Id: I99cfee14bb105a8792651129426c0c5a3749796d Signed-off-by: Wang Huaqiang --- .../ImageMetaPropsPayload.json | 2 +- .../InstanceNUMACellPayload.json | 2 +- nova/api/openstack/compute/servers.py | 2 + nova/api/validation/extra_specs/hw.py | 7 +- nova/exception.py | 11 +++ nova/notifications/objects/image.py | 3 +- nova/notifications/objects/request_spec.py | 3 +- nova/objects/fields.py | 3 +- nova/objects/image_meta.py | 10 ++- nova/objects/instance_numa.py | 13 ++- .../test_instance.py | 4 +- .../validation/extra_specs/test_validators.py | 1 + nova/tests/unit/compute/test_compute.py | 9 +- .../objects/test_notification.py | 4 +- nova/tests/unit/objects/test_objects.py | 4 +- nova/tests/unit/virt/test_hardware.py | 87 +++++++++++++++++++ nova/virt/hardware.py | 58 ++++++++++++- 17 files changed, 203 insertions(+), 20 deletions(-) diff --git a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json index 33e71ec36a01..6cd0c651ac22 100644 --- a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json +++ b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json @@ -4,5 +4,5 @@ "hw_architecture": "x86_64" }, "nova_object.name": "ImageMetaPropsPayload", - "nova_object.version": "1.3" + "nova_object.version": "1.4" } diff --git a/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json b/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json index 4ea2df6a5059..221d0d1b9f1d 100644 --- a/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json +++ b/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json @@ -1,5 +1,5 @@ { - "nova_object.version": "1.1", + "nova_object.version": "1.2", "nova_object.namespace": "nova", "nova_object.name": "InstanceNUMACellPayload", "nova_object.data": { diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 06975c4e12f5..171baab5d6ff 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -87,6 +87,8 @@ INVALID_FLAVOR_IMAGE_EXCEPTIONS = ( exception.PciRequestAliasNotDefined, exception.RealtimeConfigurationInvalid, exception.RealtimeMaskNotFoundOrInvalid, + exception.RequiredMixedInstancePolicy, + exception.RequiredMixedOrRealtimeCPUMask, ) MIN_COMPUTE_MOVE_BANDWIDTH = 39 diff --git a/nova/api/validation/extra_specs/hw.py b/nova/api/validation/extra_specs/hw.py index fd4aae4cb4cf..fde3675db412 100644 --- a/nova/api/validation/extra_specs/hw.py +++ b/nova/api/validation/extra_specs/hw.py @@ -63,14 +63,17 @@ cpu_policy_validators = [ 'CPUs can run on. If ``shared`` (default), guest CPUs can be ' 'overallocated but cannot float across host cores. If ' '``dedicated``, guest CPUs cannot be overallocated but are ' - 'individually pinned to their own host core.' + 'individually pinned to their own host core. ``mixed`` is a ' + 'policy with which the guest is mixing the overallocated and ' + 'pinned guest CPUs.' ), value={ 'type': str, 'description': 'The CPU policy.', 'enum': [ 'dedicated', - 'shared' + 'shared', + 'mixed', ], }, ), diff --git a/nova/exception.py b/nova/exception.py index e0e1d80f9989..e5dd12a6b87e 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2314,3 +2314,14 @@ class AcceleratorRequestOpFailed(NovaException): class InvalidLibvirtGPUConfig(NovaException): msg_fmt = _('Invalid configuration for GPU devices: %(reason)s') + + +class RequiredMixedInstancePolicy(Invalid): + msg_fmt = _("Cannot specify 'hw:cpu_dedicated_mask' without the " + "'mixed' policy.") + + +class RequiredMixedOrRealtimeCPUMask(Invalid): + msg_fmt = _("Must specify either 'hw:cpu_dedicated_mask' or " + "'hw:cpu_realtime_mask' when using 'mixed' CPU policy" + " instance.") diff --git a/nova/notifications/objects/image.py b/nova/notifications/objects/image.py index 43e6530ad401..24b63c774224 100644 --- a/nova/notifications/objects/image.py +++ b/nova/notifications/objects/image.py @@ -120,7 +120,8 @@ class ImageMetaPropsPayload(base.NotificationPayloadBase): # Version 1.1: Added 'gop', 'virtio' and 'none' to hw_video_model field # Version 1.2: Added hw_pci_numa_affinity_policy field # Version 1.3: Added hw_mem_encryption, hw_pmu and hw_time_hpet fields - VERSION = '1.3' + # Version 1.4: Added 'mixed' to hw_cpu_policy field + VERSION = '1.4' SCHEMA = { k: ('image_meta_props', k) for k in image_meta.ImageMetaProps.fields} diff --git a/nova/notifications/objects/request_spec.py b/nova/notifications/objects/request_spec.py index 7ef6be112490..dae5c5a823d6 100644 --- a/nova/notifications/objects/request_spec.py +++ b/nova/notifications/objects/request_spec.py @@ -144,7 +144,8 @@ class InstanceNUMATopologyPayload(base.NotificationPayloadBase): class InstanceNUMACellPayload(base.NotificationPayloadBase): # Version 1.0: Initial version # Version 1.1: Added pcpuset field - VERSION = '1.1' + # Version 1.2: Added 'mixed' to cpu_policy field + VERSION = '1.2' SCHEMA = { 'id': ('numa_cell', 'id'), diff --git a/nova/objects/fields.py b/nova/objects/fields.py index 946dc4e9189d..377ba010cdda 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -272,8 +272,9 @@ class CPUAllocationPolicy(BaseNovaEnum): DEDICATED = "dedicated" SHARED = "shared" + MIXED = "mixed" - ALL = (DEDICATED, SHARED) + ALL = (DEDICATED, SHARED, MIXED) class CPUThreadAllocationPolicy(BaseNovaEnum): diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py index 8e3b4cc4410c..4435e24bd760 100644 --- a/nova/objects/image_meta.py +++ b/nova/objects/image_meta.py @@ -175,14 +175,22 @@ class ImageMetaProps(base.NovaObject): # Version 1.23: Added 'hw_pmu' field # Version 1.24: Added 'hw_mem_encryption' field # Version 1.25: Added 'hw_pci_numa_affinity_policy' field + # Version 1.26: Added 'mixed' to 'hw_cpu_policy' field # NOTE(efried): When bumping this version, the version of # ImageMetaPropsPayload must also be bumped. See its docstring for details. - VERSION = '1.25' + VERSION = '1.26' def obj_make_compatible(self, primitive, target_version): super(ImageMetaProps, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 26): + policy = primitive.get('hw_cpu_policy', None) + if policy == fields.CPUAllocationPolicy.MIXED: + raise exception.ObjectActionError( + action='obj_make_compatible', + reason='hw_cpu_policy=%s not supported in version %s' % + (policy, target_version)) if target_version < (1, 25): primitive.pop('hw_pci_numa_affinity_policy', None) if target_version < (1, 24): diff --git a/nova/objects/instance_numa.py b/nova/objects/instance_numa.py index 14350a69bf53..d067ba87d34b 100644 --- a/nova/objects/instance_numa.py +++ b/nova/objects/instance_numa.py @@ -19,6 +19,7 @@ from oslo_utils import versionutils from nova.db import api as db from nova import exception +from nova.i18n import _ from nova.objects import base from nova.objects import fields as obj_fields from nova.virt import hardware @@ -34,12 +35,22 @@ class InstanceNUMACell(base.NovaEphemeralObject, # Version 1.3: Add cpu_policy and cpu_thread_policy fields # Version 1.4: Add cpuset_reserved field # Version 1.5: Add pcpuset field - VERSION = '1.5' + # Version 1.6: Add 'mixed' to cpu_policy field + VERSION = '1.6' def obj_make_compatible(self, primitive, target_version): super(InstanceNUMACell, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) + # Instance with a 'mixed' CPU policy could not provide a backward + # compatibility. + if target_version < (1, 6): + if primitive['cpu_policy'] == obj_fields.CPUAllocationPolicy.MIXED: + raise exception.ObjectActionError( + action='obj_make_compatible', + reason=_('mixed instance is not supported in version %s') % + target_version) + # NOTE(huaqiang): Since version 1.5, 'cpuset' is modified to track the # unpinned CPUs only, with pinned CPUs tracked via 'pcpuset' instead. # For a backward compatibility, move the 'dedicated' instance CPU list diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index fabdf07a33e4..580cdc88f31e 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -1249,7 +1249,7 @@ class TestInstanceNotificationSample( 'nova_object.data': {}, 'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.namespace': 'nova', - 'nova_object.version': u'1.3'}, + 'nova_object.version': u'1.4'}, 'image.size': 58145823, 'image.tags': [], 'scheduler_hints': {'_nova_check_type': ['rebuild']}, @@ -1344,7 +1344,7 @@ class TestInstanceNotificationSample( 'nova_object.data': {}, 'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.namespace': 'nova', - 'nova_object.version': u'1.3'}, + 'nova_object.version': u'1.4'}, 'image.size': 58145823, 'image.tags': [], 'scheduler_hints': {'_nova_check_type': ['rebuild']}, diff --git a/nova/tests/unit/api/validation/extra_specs/test_validators.py b/nova/tests/unit/api/validation/extra_specs/test_validators.py index 6f11673fd034..a7ab210f39e6 100644 --- a/nova/tests/unit/api/validation/extra_specs/test_validators.py +++ b/nova/tests/unit/api/validation/extra_specs/test_validators.py @@ -69,6 +69,7 @@ class TestValidators(test.NoDBTestCase): ('hw:cpu_thread_policy', 'prefer'), ('hw:emulator_threads_policy', 'isolate'), ('hw:pci_numa_affinity_policy', 'legacy'), + ('hw:cpu_policy', 'mixed'), ) for key, value in valid_specs: validators.validate(key, value) diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index 8bdd17a07f59..63931e8b6708 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -13352,10 +13352,17 @@ class CheckRequestedImageTestCase(test.TestCase): self.context, image['id'], image, self.instance_type, root_bdm) - def test_cpu_policy(self): + @mock.patch('nova.virt.hardware.get_dedicated_cpu_constraint') + def test_cpu_policy(self, dedicated_cpu_mock): image = {'id': uuids.image_id, 'status': 'active'} for v in obj_fields.CPUAllocationPolicy.ALL: image['properties'] = {'hw_cpu_policy': v} + # 'mixed' policy requires a definition of 'cpu_dedicated_mask' + if v == obj_fields.CPUAllocationPolicy.MIXED: + dedicated_cpu_mock.return_value = set([0]) + else: + dedicated_cpu_mock.return_value = None + self.compute_api._validate_flavor_image( self.context, image['id'], image, self.instance_type, None) image['properties'] = {'hw_cpu_policy': 'bar'} diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 07d3ba334933..277d9ffb4900 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -387,7 +387,7 @@ notification_object_data = { # ImageMetaProps, so when you see a fail here for that reason, you must # *also* bump the version of ImageMetaPropsPayload. See its docstring for # more information. - 'ImageMetaPropsPayload': '1.3-9c200c895932163a4e14e6bb385fa1e0', + 'ImageMetaPropsPayload': '1.4-036c794843b95a3a39ee70830f5f6557', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f', 'InstanceActionRebuildNotification': @@ -411,7 +411,7 @@ notification_object_data = { 'InstanceActionSnapshotPayload': '1.9-c3e0bbaaefafdfa2f8e6e504c2c9b12c', 'InstanceExistsNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceExistsPayload': '1.2-e082c02438ee57164829afaeee3bf7f8', - 'InstanceNUMACellPayload': '1.1-2a24ab42bf5e8dfa98291402725bf278', + 'InstanceNUMACellPayload': '1.2-a367add3378c71c21c817ab2b23db3bf', 'InstanceNUMATopologyPayload': '1.0-247361b152047c18ae9ad1da2544a3c9', 'InstancePCIRequestPayload': '1.0-12d0d61baf183daaafd93cbeeed2956f', 'InstancePCIRequestsPayload': '1.0-6751cffe0c0fabd212aad624f672429a', diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index eca06808e192..d1f47d79de88 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1077,7 +1077,7 @@ object_data = { 'HyperVLiveMigrateData': '1.4-e265780e6acfa631476c8170e8d6fce0', 'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502', 'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d', - 'ImageMetaProps': '1.25-66fc973af215eb5701ed4034bb6f0685', + 'ImageMetaProps': '1.26-b9f136cd10a2b5ffb3ae44332f2f687d', 'Instance': '2.7-d187aec68cad2e4d8b8a03a68e4739ce', 'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5', 'InstanceActionEvent': '1.4-5b1f361bd81989f8bb2c20bb7e8a4cb4', @@ -1093,7 +1093,7 @@ object_data = { 'InstanceList': '2.6-238f125650c25d6d12722340d726f723', 'InstanceMapping': '1.2-3bd375e65c8eb9c45498d2f87b882e03', 'InstanceMappingList': '1.3-d34b6ebb076d542ae0f8b440534118da', - 'InstanceNUMACell': '1.5-d6f884326eba8cae60930e06047fc7d9', + 'InstanceNUMACell': '1.6-25d9120d83a18356f4146f2a6fe2cc8d', 'InstanceNUMATopology': '1.3-ec0030cb0402a49c96da7051c037082a', 'InstancePCIRequest': '1.3-f6d324f1c337fad4f34892ed5f484c9a', 'InstancePCIRequests': '1.1-65e38083177726d806684cb1cc0136d2', diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index af46fead305e..45d5ed13b34f 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -932,10 +932,75 @@ class NUMATopologyTest(test.NoDBTestCase): ), "image": { "properties": { + "hw_cpu_policy": "shared" } }, "expect": fields.CPUAllocationPolicy.DEDICATED }, + { + "flavor": objects.Flavor( + extra_specs={ + "hw:cpu_policy": "dedicated" + } + ), + "image": { + "properties": { + } + }, + "expect": fields.CPUAllocationPolicy.DEDICATED + }, + { + + "flavor": objects.Flavor( + extra_specs={ + "hw:cpu_policy": "mixed" + } + ), + "image": { + "properties": { + "hw_cpu_policy": "dedicated" + } + }, + "expect": exception.ImageCPUPinningForbidden + }, + { + "flavor": objects.Flavor( + extra_specs={ + "hw:cpu_policy": "mixed" + } + ), + "image": { + "properties": { + "hw_cpu_policy": "mixed" + } + }, + "expect": fields.CPUAllocationPolicy.MIXED + }, + { + "flavor": objects.Flavor( + extra_specs={ + "hw:cpu_policy": "mixed" + } + ), + "image": { + "properties": { + "hw_cpu_policy": "shared" + } + }, + "expect": fields.CPUAllocationPolicy.MIXED + }, + { + "flavor": objects.Flavor( + extra_specs={ + "hw:cpu_policy": "mixed" + } + ), + "image": { + "properties": { + } + }, + "expect": fields.CPUAllocationPolicy.MIXED + }, { "flavor": objects.Flavor( extra_specs={ @@ -949,6 +1014,19 @@ class NUMATopologyTest(test.NoDBTestCase): }, "expect": exception.ImageCPUPinningForbidden }, + { + "flavor": objects.Flavor( + extra_specs={ + "hw:cpu_policy": "shared" + } + ), + "image": { + "properties": { + "hw_cpu_policy": "mixed" + } + }, + "expect": exception.ImageCPUPinningForbidden + }, { "flavor": objects.Flavor( extra_specs={ @@ -983,6 +1061,15 @@ class NUMATopologyTest(test.NoDBTestCase): }, "expect": fields.CPUAllocationPolicy.DEDICATED }, + { + "flavor": objects.Flavor(), + "image": { + "properties": { + "hw_cpu_policy": "mixed" + } + }, + "expect": fields.CPUAllocationPolicy.MIXED + }, { "flavor": objects.Flavor(), "image": { diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index 86f185d5ad4d..97db4462b677 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1505,10 +1505,17 @@ def get_cpu_policy_constraint( if flavor_policy == fields.CPUAllocationPolicy.DEDICATED: cpu_policy = flavor_policy - elif flavor_policy == fields.CPUAllocationPolicy.SHARED: + elif flavor_policy == fields.CPUAllocationPolicy.MIXED: if image_policy == fields.CPUAllocationPolicy.DEDICATED: raise exception.ImageCPUPinningForbidden() cpu_policy = flavor_policy + elif flavor_policy == fields.CPUAllocationPolicy.SHARED: + if image_policy in ( + fields.CPUAllocationPolicy.MIXED, + fields.CPUAllocationPolicy.DEDICATED, + ): + raise exception.ImageCPUPinningForbidden() + cpu_policy = flavor_policy elif image_policy in fields.CPUAllocationPolicy.ALL: cpu_policy = image_policy else: @@ -1693,6 +1700,21 @@ def _get_hyperthreading_trait( return None +# NOTE(stephenfin): This must be public as it's used elsewhere +# TODO(Huaqiang): To be filled with the logic of parsing +# 'hw:cpu_dedicated_mask' and relevant test cases in later patches once the +# code is ready to build up an instance in 'mixed' CPU allocation policy. +def get_dedicated_cpu_constraint( + flavor: 'objects.Flavor', +) -> ty.Optional[ty.Set[int]]: + """Validate and return the requested dedicated CPU mask. + + :param flavor: ``nova.objects.Flavor`` instance + :returns: The dedicated CPUs requested, else None. + """ + return None + + # NOTE(stephenfin): This must be public as it's used elsewhere def get_realtime_cpu_constraint( flavor: 'objects.Flavor', @@ -1833,12 +1855,18 @@ def numa_get_constraints(flavor, image_meta): with invalid value in image or flavor. :raises: exception.InvalidRequest if there is a conflict between explicitly and implicitly requested resources of hyperthreading traits + :raises: exception.RequiredMixedInstancePolicy if dedicated CPU mask is + provided in flavor while CPU policy is not 'mixed'. + :raises: exception.RequiredMixedOrRealtimeCPUMask the mixed policy instance + dedicated CPU mask can only be specified through either + 'hw:cpu_realtime_mask' or 'hw:cpu_dedicated_mask', not both. :returns: objects.InstanceNUMATopology, or None """ cpu_policy = get_cpu_policy_constraint(flavor, image_meta) cpu_thread_policy = get_cpu_thread_policy_constraint(flavor, image_meta) rt_mask = get_realtime_cpu_constraint(flavor, image_meta) + dedicated_cpus = get_dedicated_cpu_constraint(flavor) emu_threads_policy = get_emulator_thread_policy_constraint(flavor) # handle explicit VCPU/PCPU resource requests and the HW_CPU_HYPERTHREADING @@ -1904,13 +1932,36 @@ def numa_get_constraints(flavor, image_meta): if emu_threads_policy == fields.CPUEmulatorThreadsPolicy.ISOLATE: raise exception.BadRequirementEmulatorThreadsPolicy() + # 'hw:cpu_dedicated_mask' should not be defined in a flavor with + # 'shared' policy. + if dedicated_cpus: + raise exception.RequiredMixedInstancePolicy() + if rt_mask: raise exception.RealtimeConfigurationInvalid() + elif cpu_policy == fields.CPUAllocationPolicy.DEDICATED: + # 'hw:cpu_dedicated_mask' should not be defined in a flavor with + # 'dedicated' policy. + if dedicated_cpus: + raise exception.RequiredMixedInstancePolicy() + # But for an instance with 'dedicated' CPU allocation policy, all + # CPUs are 'dedicated' CPUs, which is 1:1 pinned to a host CPU. + dedicated_cpus = set(range(flavor.vcpus)) + else: # MIXED + # FIXME(huaqiang): So far, 'mixed' instance is not supported + # and the 'dedicated_cpus' variable is set to 'None' due to being not + # ready to parse 'hw:cpu_dedicated_mask'. + # The logic of parsing 'hw:cpu_dedicated_mask' should be added once + # the code is ready for setting up an 'mixed' instance. + if dedicated_cpus is None: + raise exception.RequiredMixedOrRealtimeCPUMask() nodes = _get_numa_node_count_constraint(flavor, image_meta) pagesize = _get_numa_pagesize_constraint(flavor, image_meta) vpmems = get_vpmems(flavor) + dedicated_cpus = dedicated_cpus or set() + # NOTE(stephenfin): There are currently four things that will configure a # NUMA topology for an instance: # @@ -1920,14 +1971,13 @@ def numa_get_constraints(flavor, image_meta): # - The use of vPMEM if nodes or pagesize or vpmems or cpu_policy in ( fields.CPUAllocationPolicy.DEDICATED, + fields.CPUAllocationPolicy.MIXED, ): # NOTE(huaqiang): Here we build the instance dedicated CPU set and the # shared CPU set, through 'pcpus' and 'vcpus' respectively, # which will be used later to calculate the per-NUMA-cell CPU set. cpus = set(range(flavor.vcpus)) - pcpus = set() - if cpu_policy == fields.CPUAllocationPolicy.DEDICATED: - pcpus = cpus + pcpus = dedicated_cpus vcpus = cpus - pcpus nodes = nodes or 1