diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index c4dc4421cd3b..a319d5ccb63c 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -54,7 +54,32 @@ class ResourceRequest(object): XS_KEYPAT = re.compile(r"^(%s)([1-9][0-9]*)?:(.*)$" % '|'.join((XS_RES_PREFIX, XS_TRAIT_PREFIX))) - def __init__(self): + def __init__(self, request_spec): + """Create a new instance of ResourceRequest from a RequestSpec. + + Examines the flavor, flavor extra specs, and (optional) image metadata + of the provided ``request_spec``. + + For extra specs, items of the following form are examined: + + - ``resources:$RESOURCE_CLASS``: $AMOUNT + - ``resources$N:$RESOURCE_CLASS``: $AMOUNT + - ``trait:$TRAIT_NAME``: "required" + - ``trait$N:$TRAIT_NAME``: "required" + + .. note:: + + This does *not* yet handle ``member_of[$N]``. + + For image metadata, traits are extracted from the ``traits_required`` + property, if present. + + For the flavor, ``VCPU``, ``MEMORY_MB`` and ``DISK_GB`` are calculated + from Flavor properties, though these are only used if they aren't + overridden by flavor extra specs. + + :param request_spec: An instance of ``objects.RequestSpec``. + """ # { ident: RequestGroup } self._rg_by_id = {} self._group_policy = None @@ -62,9 +87,75 @@ class ResourceRequest(object): # set to None to indicate "no limit". self._limit = CONF.scheduler.max_placement_results - def __str__(self): - return ', '.join(sorted( - list(str(rg) for rg in list(self._rg_by_id.values())))) + # TODO(efried): Handle member_of[$N], which will need to be reconciled + # with destination.aggregates handling in resources_from_request_spec + + # Parse the flavor extra specs + self._process_extra_specs(request_spec.flavor) + + self.numbered_groups_from_flavor = self.get_num_of_numbered_groups() + + # Now parse the (optional) image metadata + image = request_spec.image if 'image' in request_spec else None + self._process_image_meta(image) + + # Finally, parse the flavor itself, though we'll only use these fields + # if they don't conflict with something already provided by the flavor + # extra specs. These are all added to the unnumbered request group. + merged_resources = self.merged_resources() + + if orc.VCPU not in merged_resources: + self._add_resource(None, orc.VCPU, request_spec.vcpus) + + if orc.MEMORY_MB not in merged_resources: + self._add_resource(None, orc.MEMORY_MB, request_spec.memory_mb) + + if orc.DISK_GB not in merged_resources: + disk = request_spec.ephemeral_gb + disk += compute_utils.convert_mb_to_ceil_gb(request_spec.swap) + if 'is_bfv' not in request_spec or not request_spec.is_bfv: + disk += request_spec.root_gb + + if disk: + self._add_resource(None, orc.DISK_GB, disk) + + self.strip_zeros() + + def _process_extra_specs(self, flavor): + if 'extra_specs' not in flavor: + return + + for key, val in flavor.extra_specs.items(): + if key == 'group_policy': + self._add_group_policy(val) + continue + + match = self.XS_KEYPAT.match(key) + if not match: + continue + + # 'prefix' is 'resources' or 'trait' + # 'suffix' is $N or None + # 'name' is either the resource class name or the trait name. + prefix, suffix, name = match.groups() + + # Process "resources[$N]" + if prefix == self.XS_RES_PREFIX: + self._add_resource(suffix, name, val) + + # Process "trait[$N]" + elif prefix == self.XS_TRAIT_PREFIX: + self._add_trait(suffix, name, val) + + def _process_image_meta(self, image): + if not image or 'properties' not in image: + return + + for trait in image.properties.get('traits_required', []): + # required traits from the image are always added to the + # unnumbered request group, granular request groups are not + # supported in image traits + self._add_trait(None, trait, "required") @property def group_policy(self): @@ -140,81 +231,6 @@ class ResourceRequest(object): return self._group_policy = policy - @classmethod - def from_extra_specs(cls, extra_specs, req=None): - """Processes resources and traits in numbered groupings in extra_specs. - - Examines extra_specs for items of the following forms: - "resources:$RESOURCE_CLASS": $AMOUNT - "resources$N:$RESOURCE_CLASS": $AMOUNT - "trait:$TRAIT_NAME": "required" - "trait$N:$TRAIT_NAME": "required" - - Does *not* yet handle member_of[$N]. - - :param extra_specs: The flavor extra_specs dict. - :param req: the ResourceRequest object to add the requirements to or - None to create a new ResourceRequest - :return: A ResourceRequest object representing the resources and - required traits in the extra_specs. - """ - # TODO(efried): Handle member_of[$N], which will need to be reconciled - # with destination.aggregates handling in resources_from_request_spec - - if req is not None: - ret = req - else: - ret = cls() - - for key, val in extra_specs.items(): - if key == 'group_policy': - ret._add_group_policy(val) - continue - - match = cls.XS_KEYPAT.match(key) - if not match: - continue - - # 'prefix' is 'resources' or 'trait' - # 'suffix' is $N or None - # 'name' is either the resource class name or the trait name. - prefix, suffix, name = match.groups() - - # Process "resources[$N]" - if prefix == cls.XS_RES_PREFIX: - ret._add_resource(suffix, name, val) - - # Process "trait[$N]" - elif prefix == cls.XS_TRAIT_PREFIX: - ret._add_trait(suffix, name, val) - - return ret - - @classmethod - def from_image_props(cls, image_meta_props, req=None): - """Processes image properties and adds trait requirements to the - ResourceRequest - - :param image_meta_props: The ImageMetaProps object. - :param req: the ResourceRequest object to add the requirements to or - None to create a new ResourceRequest - :return: A ResourceRequest object representing the required traits on - the image. - """ - if req is not None: - ret = req - else: - ret = cls() - - if 'traits_required' in image_meta_props: - for trait in image_meta_props.traits_required: - # required traits from the image are always added to the - # unnumbered request group, granular request groups are not - # supported in image traits - ret._add_trait(None, trait, "required") - - return ret - def resource_groups(self): for rg in self._rg_by_id.values(): yield rg.resources @@ -223,33 +239,18 @@ class ResourceRequest(object): return len([ident for ident in self._rg_by_id.keys() if ident is not None]) - def merged_resources(self, flavor_resources=None): + def merged_resources(self): """Returns a merge of {resource_class: amount} for all resource groups. Amounts of the same resource class from different groups are added together. - :param flavor_resources: A flat dict of {resource_class: amount}. If - specified, the resources therein are folded - into the return dict, such that any resource - in flavor_resources is included only if that - resource class does not exist elsewhere in the - merged ResourceRequest. :return: A dict of the form {resource_class: amount} """ ret = collections.defaultdict(lambda: 0) for resource_dict in self.resource_groups(): for resource_class, amount in resource_dict.items(): ret[resource_class] += amount - if flavor_resources: - for resource_class, amount in flavor_resources.items(): - # If it's in there - even if zero - ignore the one from the - # flavor. - if resource_class not in ret: - ret[resource_class] = amount - # Now strip zeros. This has to be done after the above - we can't - # use strip_zeros :( - ret = {rc: amt for rc, amt in ret.items() if amt} return dict(ret) def _clean_empties(self): @@ -329,6 +330,10 @@ class ResourceRequest(object): traits = traits.union(rr.required_traits) return traits + def __str__(self): + return ', '.join(sorted( + list(str(rg) for rg in list(self._rg_by_id.values())))) + def build_request_spec(image, instances, instance_type=None): """Build a request_spec for the scheduler. @@ -396,25 +401,17 @@ def resources_from_flavor(instance, flavor): """ is_bfv = compute_utils.is_volume_backed_instance(instance._context, instance) - swap_in_gb = compute_utils.convert_mb_to_ceil_gb(flavor.swap) - disk = ((0 if is_bfv else flavor.root_gb) + - swap_in_gb + flavor.ephemeral_gb) + # create a fake RequestSpec as a wrapper to the caller + req_spec = objects.RequestSpec(flavor=flavor, is_bfv=is_bfv) - resources = { - orc.VCPU: flavor.vcpus, - orc.MEMORY_MB: flavor.memory_mb, - orc.DISK_GB: disk, - } - if "extra_specs" in flavor: - # TODO(efried): This method is currently only used from places that - # assume the compute node is the only resource provider. So for now, - # we just merge together all the resources specified in the flavor and - # pass them along. This will need to be adjusted when nested and/or - # shared RPs are in play. - rreq = ResourceRequest.from_extra_specs(flavor.extra_specs) - resources = rreq.merged_resources(flavor_resources=resources) + # TODO(efried): This method is currently only used from places that + # assume the compute node is the only resource provider. So for now, we + # just merge together all the resources specified in the flavor and pass + # them along. This will need to be adjusted when nested and/or shared RPs + # are in play. + res_req = ResourceRequest(req_spec) - return resources + return res_req.merged_resources() def resources_from_request_spec(ctxt, spec_obj, host_manager): @@ -428,63 +425,7 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager): :return: A ResourceRequest object. :raises NoValidHost: If the specified host/node is not found in the DB. """ - spec_resources = { - orc.VCPU: spec_obj.vcpus, - orc.MEMORY_MB: spec_obj.memory_mb, - } - - requested_disk_mb = ((1024 * spec_obj.ephemeral_gb) + - spec_obj.swap) - - if 'is_bfv' not in spec_obj or not spec_obj.is_bfv: - # Either this is not a BFV instance, or we are not sure, - # so ask for root_gb allocation - requested_disk_mb += (1024 * spec_obj.root_gb) - - # NOTE(sbauza): Disk request is expressed in MB but we count - # resources in GB. Since there could be a remainder of the division - # by 1024, we need to ceil the result to the next bigger Gb so we - # can be sure there would be enough disk space in the destination - # to sustain the request. - # FIXME(sbauza): All of that could be using math.ceil() but since - # we support both py2 and py3, let's fake it until we only support - # py3. - requested_disk_gb = requested_disk_mb // 1024 - if requested_disk_mb % 1024 != 0: - # Let's ask for a bit more space since we count in GB - requested_disk_gb += 1 - # NOTE(sbauza): Some flavors provide zero size for disk values, we need - # to avoid asking for disk usage. - if requested_disk_gb != 0: - spec_resources[orc.DISK_GB] = requested_disk_gb - - # Process extra_specs - if "extra_specs" in spec_obj.flavor: - res_req = ResourceRequest.from_extra_specs(spec_obj.flavor.extra_specs) - # If any of the three standard resources above was explicitly given in - # the extra_specs - in any group - we need to replace it, or delete it - # if it was given as zero. We'll do this by grabbing a merged version - # of the ResourceRequest resources and removing matching items from the - # spec_resources. - spec_resources = {rclass: amt for rclass, amt in spec_resources.items() - if rclass not in res_req.merged_resources()} - # Now we don't need (or want) any remaining zero entries - remove them. - res_req.strip_zeros() - - numbered_groups_from_flavor = res_req.get_num_of_numbered_groups() - else: - # Start with an empty one - res_req = ResourceRequest() - numbered_groups_from_flavor = 0 - - # Process any image properties - if 'image' in spec_obj and 'properties' in spec_obj.image: - res_req = ResourceRequest.from_image_props(spec_obj.image.properties, - req=res_req) - - # Add the (remaining) items from the spec_resources to the sharing group - for rclass, amount in spec_resources.items(): - res_req.get_request_group(None).resources[rclass] = amount + res_req = ResourceRequest(spec_obj) requested_resources = (spec_obj.requested_resources if 'requested_resources' in spec_obj and @@ -574,7 +515,7 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager): "group to be satisfied from a separate resource provider then " "use 'group_policy': 'isolate'.") - if numbered_groups_from_flavor <= 1: + if res_req.numbered_groups_from_flavor <= 1: LOG.info( "At least one numbered request group is defined outside of " "the flavor (e.g. in a port that has a QoS minimum bandwidth " diff --git a/nova/tests/functional/test_report_client.py b/nova/tests/functional/test_report_client.py index 79fdc786b019..dc8c29a4a6af 100644 --- a/nova/tests/functional/test_report_client.py +++ b/nova/tests/functional/test_report_client.py @@ -1019,9 +1019,12 @@ class SchedulerReportClientTests(SchedulerReportClientTestBase): def test_alloc_cands_smoke(self): """Simple call to get_allocation_candidates for version checking.""" + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) + req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False) with self._interceptor(): self.client.get_allocation_candidates( - self.context, utils.ResourceRequest()) + self.context, utils.ResourceRequest(req_spec)) def _set_up_provider_tree(self): r"""Create two compute nodes in placement ("this" one, and another one) @@ -1145,7 +1148,7 @@ class SchedulerReportClientTests(SchedulerReportClientTestBase): # care to check. for k, expected in pdict.items(): # For inventories, we're only validating totals - if k is 'inventory': + if k == 'inventory': self.assertEqual( set(expected), set(actual_data.inventory), "Mismatched inventory keys for provider %s" % uuid) diff --git a/nova/tests/unit/scheduler/client/test_report.py b/nova/tests/unit/scheduler/client/test_report.py index d4580f8f42aa..396a817147bf 100644 --- a/nova/tests/unit/scheduler/client/test_report.py +++ b/nova/tests/unit/scheduler/client/test_report.py @@ -2058,27 +2058,31 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'allocation_requests': mock.sentinel.alloc_reqs, 'provider_summaries': mock.sentinel.p_sums, } - resources = scheduler_utils.ResourceRequest.from_extra_specs({ - 'resources:VCPU': '1', - 'resources:MEMORY_MB': '1024', - 'trait:HW_CPU_X86_AVX': 'required', - 'trait:CUSTOM_TRAIT1': 'required', - 'trait:CUSTOM_TRAIT2': 'preferred', - 'trait:CUSTOM_TRAIT3': 'forbidden', - 'trait:CUSTOM_TRAIT4': 'forbidden', - 'resources1:DISK_GB': '30', - 'trait1:STORAGE_DISK_SSD': 'required', - 'resources2:VGPU': '2', - 'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required', - 'trait2:HW_GPU_API_VULKAN': 'required', - 'resources3:SRIOV_NET_VF': '1', - 'resources3:CUSTOM_NET_EGRESS_BYTES_SEC': '125000', - 'group_policy': 'isolate', - # These are ignored because misspelled, bad value, etc. - 'resources02:CUSTOM_WIDGET': '123', - 'trait:HW_NIC_OFFLOAD_LRO': 'preferred', - 'group_policy3': 'none', - }) + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, + extra_specs={ + 'resources:VCPU': '1', + 'resources:MEMORY_MB': '1024', + 'trait:HW_CPU_X86_AVX': 'required', + 'trait:CUSTOM_TRAIT1': 'required', + 'trait:CUSTOM_TRAIT2': 'preferred', + 'trait:CUSTOM_TRAIT3': 'forbidden', + 'trait:CUSTOM_TRAIT4': 'forbidden', + 'resources1:DISK_GB': '30', + 'trait1:STORAGE_DISK_SSD': 'required', + 'resources2:VGPU': '2', + 'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required', + 'trait2:HW_GPU_API_VULKAN': 'required', + 'resources3:SRIOV_NET_VF': '1', + 'resources3:CUSTOM_NET_EGRESS_BYTES_SEC': '125000', + 'group_policy': 'isolate', + # These are ignored because misspelled, bad value, etc. + 'resources02:CUSTOM_WIDGET': '123', + 'trait:HW_NIC_OFFLOAD_LRO': 'preferred', + 'group_policy3': 'none', + }) + req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False) + resources = scheduler_utils.ResourceRequest(req_spec) resources.get_request_group(None).aggregates = [ ['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']] expected_path = '/allocation_candidates' @@ -2123,12 +2127,16 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'allocation_requests': mock.sentinel.alloc_reqs, 'provider_summaries': mock.sentinel.p_sums, } - resources = scheduler_utils.ResourceRequest.from_extra_specs({ - 'resources:VCPU': '1', - 'resources:MEMORY_MB': '1024', - 'resources1:DISK_GB': '30', - 'group_policy': 'bogus', - }) + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, + extra_specs={ + 'resources:VCPU': '1', + 'resources:MEMORY_MB': '1024', + 'resources1:DISK_GB': '30', + 'group_policy': 'bogus', + }) + req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False) + resources = scheduler_utils.ResourceRequest(req_spec) expected_path = '/allocation_candidates' expected_query = [ ('limit', '42'), @@ -2161,14 +2169,18 @@ class TestProviderOperations(SchedulerReportClientTestCase): resp_mock = mock.Mock(status_code=404) self.ks_adap_mock.get.return_value = resp_mock expected_path = '/allocation_candidates' - expected_query = {'resources': ['MEMORY_MB:1024'], - 'limit': ['100']} + expected_query = { + 'resources': ['DISK_GB:15,MEMORY_MB:1024,VCPU:1'], + 'limit': ['100'] + } # Make sure we're also honoring the configured limit self.flags(max_placement_results=100, group='scheduler') - resources = scheduler_utils.ResourceRequest.from_extra_specs( - {'resources:MEMORY_MB': '1024'}) + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) + req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False) + resources = scheduler_utils.ResourceRequest(req_spec) res = self.client.get_allocation_candidates(self.context, resources) diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index 72cac2a7f3b9..64f003a9bb87 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -25,6 +25,19 @@ from nova.tests.unit import fake_instance from nova.tests.unit.scheduler import fakes +class FakeResourceRequest(object): + """A fake of ``nova.scheduler.utils.ResourceRequest``. + + Allows us to assert that various properties of a real ResourceRequest + object are set as we'd like them to be. + """ + + def __init__(self): + self._rg_by_id = {} + self._group_policy = None + self._limit = 1000 + + @ddt.ddt class TestUtils(test.NoDBTestCase): @@ -68,7 +81,7 @@ class TestUtils(test.NoDBTestCase): root_gb=10, ephemeral_gb=5, swap=0) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -89,7 +102,7 @@ class TestUtils(test.NoDBTestCase): extra_specs={ 'trait:CUSTOM_FLAVOR_TRAIT': 'required', 'trait:CUSTOM_IMAGE_TRAIT2': 'required'}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -116,7 +129,7 @@ class TestUtils(test.NoDBTestCase): swap=0, extra_specs={ 'trait:CUSTOM_FLAVOR_TRAIT': 'forbidden'}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -136,7 +149,7 @@ class TestUtils(test.NoDBTestCase): root_gb=0, ephemeral_gb=0, swap=0) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -153,7 +166,7 @@ class TestUtils(test.NoDBTestCase): ephemeral_gb=5, swap=0, extra_specs={"resources:CUSTOM_TEST_CLASS": 1}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -175,7 +188,7 @@ class TestUtils(test.NoDBTestCase): "resources:VCPU": 99, "resources:MEMORY_MB": 99, "resources:DISK_GB": 99}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -195,7 +208,7 @@ class TestUtils(test.NoDBTestCase): extra_specs={ "resources:VCPU": 0, "resources:DISK_GB": 0}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -213,7 +226,7 @@ class TestUtils(test.NoDBTestCase): extra_specs={ "resources:VGPU": 1, "resources:VGPU_DISPLAY_HEAD": 1}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -269,7 +282,7 @@ class TestUtils(test.NoDBTestCase): # Bogus values don't make it through 'resources1:MEMORY_MB': 'bogus', 'group_policy': 'none'}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._group_policy = 'none' expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, @@ -327,7 +340,7 @@ class TestUtils(test.NoDBTestCase): 'trait:HW_CPU_X86_SSE': 'required', 'trait:HW_CPU_X86_AVX': 'required', 'trait:HW_CPU_X86_AVX2': 'forbidden'}) - expected_resources = utils.ResourceRequest() + expected_resources = FakeResourceRequest() expected_resources._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -394,9 +407,7 @@ class TestUtils(test.NoDBTestCase): self.context, reqspec, self.mock_host_manager) self.assertEqual([], req.get_request_group(None).aggregates) - @mock.patch("nova.scheduler.utils.ResourceRequest.from_extra_specs", - return_value=utils.ResourceRequest()) - def test_process_extra_specs_granular_called(self, mock_proc): + def test_process_extra_specs_granular_called(self): flavor = objects.Flavor(vcpus=1, memory_mb=1024, root_gb=10, @@ -404,21 +415,20 @@ class TestUtils(test.NoDBTestCase): swap=0, extra_specs={"resources:CUSTOM_TEST_CLASS": 1}) fake_spec = objects.RequestSpec(flavor=flavor) + # just call this to make sure things don't explode utils.resources_from_request_spec( self.context, fake_spec, self.mock_host_manager) - mock_proc.assert_called_once() - @mock.patch("nova.scheduler.utils.ResourceRequest.from_extra_specs") - def test_process_extra_specs_granular_not_called(self, mock_proc): + def test_process_extra_specs_granular_not_called(self): flavor = objects.Flavor(vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) fake_spec = objects.RequestSpec(flavor=flavor) + # just call this to make sure things don't explode utils.resources_from_request_spec( self.context, fake_spec, self.mock_host_manager) - mock_proc.assert_not_called() def test_process_missing_extra_specs_value(self): flavor = objects.Flavor( @@ -429,6 +439,7 @@ class TestUtils(test.NoDBTestCase): swap=0, extra_specs={"resources:CUSTOM_TEST_CLASS": ""}) fake_spec = objects.RequestSpec(flavor=flavor) + # just call this to make sure things don't explode utils.resources_from_request_spec( self.context, fake_spec, self.mock_host_manager) @@ -438,7 +449,7 @@ class TestUtils(test.NoDBTestCase): root_gb=15, ephemeral_gb=0, swap=0) - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -467,7 +478,7 @@ class TestUtils(test.NoDBTestCase): ephemeral_gb=0, swap=0) fake_spec = objects.RequestSpec(flavor=flavor, force_nodes=['test']) - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -500,7 +511,7 @@ class TestUtils(test.NoDBTestCase): ephemeral_gb=0, swap=0) fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test']) - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -535,7 +546,7 @@ class TestUtils(test.NoDBTestCase): ephemeral_gb=0, swap=0) fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test']) - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -575,7 +586,7 @@ class TestUtils(test.NoDBTestCase): swap=0) fake_spec = objects.RequestSpec( flavor=flavor, requested_destination=destination) - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -654,7 +665,7 @@ class TestUtils(test.NoDBTestCase): swap=0) fake_spec = objects.RequestSpec( flavor=flavor, scheduler_hints=hints) - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ @@ -735,40 +746,59 @@ class TestUtils(test.NoDBTestCase): actual = utils.resources_from_flavor(instance, flavor) self.assertEqual(expected, actual) - def test_resource_request_from_extra_specs(self): - extra_specs = { - 'resources:VCPU': '2', - 'resources:MEMORY_MB': '2048', - 'trait:HW_CPU_X86_AVX': 'required', - # Key skipped because no colons - 'nocolons': '42', - 'trait:CUSTOM_MAGIC': 'required', - 'trait:CUSTOM_BRONZE': 'forbidden', - # Resource skipped because invalid resource class name - 'resources86:CUTSOM_MISSPELLED': '86', - 'resources1:SRIOV_NET_VF': '1', - # Resource skipped because non-int-able value - 'resources86:CUSTOM_FOO': 'seven', - # Resource skipped because negative value - 'resources86:CUSTOM_NEGATIVE': '-7', - 'resources1:IPV4_ADDRESS': '1', - # Trait skipped because unsupported value - 'trait86:CUSTOM_GOLD': 'preferred', - 'trait1:CUSTOM_PHYSNET_NET1': 'required', - 'trait1:CUSTOM_PHYSNET_NET2': 'forbidden', - 'resources2:SRIOV_NET_VF': '1', - 'resources2:IPV4_ADDRESS': '2', - 'trait2:CUSTOM_PHYSNET_NET2': 'required', - 'trait2:HW_NIC_ACCEL_SSL': 'required', - # Groupings that don't quite match the patterns are ignored - 'resources_5:SRIOV_NET_VF': '7', - 'traitFoo:HW_NIC_ACCEL_SSL': 'required', - # Solo resource, no corresponding traits - 'resources3:DISK_GB': '5', - 'group_policy': 'isolate', - } - # Build up a ResourceRequest from the inside to compare against. - expected = utils.ResourceRequest() + def test_resource_request_init(self): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) + + expected = FakeResourceRequest() + expected._rg_by_id[None] = objects.RequestGroup( + use_same_provider=False, + resources={ + 'VCPU': 1, + 'MEMORY_MB': 1024, + 'DISK_GB': 15, + }, + ) + rs = objects.RequestSpec(flavor=flavor, is_bfv=False) + rr = utils.ResourceRequest(rs) + self.assertResourceRequestsEqual(expected, rr) + + def test_resource_request_init_with_extra_specs(self): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, + extra_specs={ + 'resources:VCPU': '2', + 'resources:MEMORY_MB': '2048', + 'trait:HW_CPU_X86_AVX': 'required', + # Key skipped because no colons + 'nocolons': '42', + 'trait:CUSTOM_MAGIC': 'required', + 'trait:CUSTOM_BRONZE': 'forbidden', + # Resource skipped because invalid resource class name + 'resources86:CUTSOM_MISSPELLED': '86', + 'resources1:SRIOV_NET_VF': '1', + # Resource skipped because non-int-able value + 'resources86:CUSTOM_FOO': 'seven', + # Resource skipped because negative value + 'resources86:CUSTOM_NEGATIVE': '-7', + 'resources1:IPV4_ADDRESS': '1', + # Trait skipped because unsupported value + 'trait86:CUSTOM_GOLD': 'preferred', + 'trait1:CUSTOM_PHYSNET_NET1': 'required', + 'trait1:CUSTOM_PHYSNET_NET2': 'forbidden', + 'resources2:SRIOV_NET_VF': '1', + 'resources2:IPV4_ADDRESS': '2', + 'trait2:CUSTOM_PHYSNET_NET2': 'required', + 'trait2:HW_NIC_ACCEL_SSL': 'required', + # Groupings that don't quite match the patterns are ignored + 'resources_5:SRIOV_NET_VF': '7', + 'traitFoo:HW_NIC_ACCEL_SSL': 'required', + # Solo resource, no corresponding traits + 'resources3:DISK_GB': '5', + 'group_policy': 'isolate', + }) + + expected = FakeResourceRequest() expected._group_policy = 'isolate' expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, @@ -812,7 +842,8 @@ class TestUtils(test.NoDBTestCase): } ) - rr = utils.ResourceRequest.from_extra_specs(extra_specs) + rs = objects.RequestSpec(flavor=flavor, is_bfv=False) + rr = utils.ResourceRequest(rs) self.assertResourceRequestsEqual(expected, rr) expected_querystring = ( 'group_policy=isolate&' @@ -827,89 +858,56 @@ class TestUtils(test.NoDBTestCase): ) self.assertEqual(expected_querystring, rr.to_querystring()) - def test_resource_request_from_extra_specs_append_request(self): - extra_specs = { - 'resources:VCPU': '2', - 'resources:MEMORY_MB': '2048', - 'trait:HW_CPU_X86_AVX': 'required', - } - existing_req = utils.ResourceRequest() - existing_req._rg_by_id[None] = objects.RequestGroup( - use_same_provider=False, - required_traits={ - 'CUSTOM_MAGIC', - } - ) - # Build up a ResourceRequest from the inside to compare against. - expected = utils.ResourceRequest() + def test_resource_request_init_with_image_props(self): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) + image = objects.ImageMeta.from_dict({ + 'properties': { + 'trait:CUSTOM_TRUSTED': 'required', + }, + 'id': 'c8b1790e-a07d-4971-b137-44f2432936cd' + }) + + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ - 'VCPU': 2, - 'MEMORY_MB': 2048, + 'VCPU': 1, + 'MEMORY_MB': 1024, + 'DISK_GB': 15, }, - required_traits={ - # In addition to traits from extra spec, we get traits from a - # previous existing resource request - 'HW_CPU_X86_AVX', - 'CUSTOM_MAGIC', - } - ) - self.assertResourceRequestsEqual( - expected, utils.ResourceRequest.from_extra_specs(extra_specs, - req=existing_req)) - - def test_resource_request_from_image_props(self): - props = {'trait:CUSTOM_TRUSTED': 'required'} - image_meta_props = objects.ImageMetaProps.from_dict(props) - - # Build up a ResourceRequest from the inside to compare against. - expected = utils.ResourceRequest() - expected._rg_by_id[None] = objects.RequestGroup( - use_same_provider=False, required_traits={ 'CUSTOM_TRUSTED', } ) - self.assertResourceRequestsEqual( - expected, utils.ResourceRequest.from_image_props(image_meta_props)) + rs = objects.RequestSpec(flavor=flavor, image=image, is_bfv=False) + rr = utils.ResourceRequest(rs) + self.assertResourceRequestsEqual(expected, rr) - def test_resource_request_from_image_props_append_request(self): - props = {'trait:CUSTOM_MAGIC': 'required'} - image_meta_props = objects.ImageMetaProps.from_dict(props) + def test_resource_request_init_is_bfv(self): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=1555) - existing_req = utils.ResourceRequest() - existing_req._rg_by_id[None] = objects.RequestGroup( - use_same_provider=False, - resources={ - 'VCPU': 2, - 'MEMORY_MB': 2048, - }, - required_traits={ - 'HW_CPU_X86_AVX', - } - ) - # Build up a ResourceRequest from the inside to compare against. - expected = utils.ResourceRequest() + expected = FakeResourceRequest() expected._rg_by_id[None] = objects.RequestGroup( use_same_provider=False, resources={ - 'VCPU': 2, - 'MEMORY_MB': 2048, + 'VCPU': 1, + 'MEMORY_MB': 1024, + # this should only include the ephemeral and swap disk, and the + # latter should be converted from MB to GB and rounded up + 'DISK_GB': 7, }, - required_traits={ - # In addition to information already contained in the existing - # resource request, we add the traits from image properties - 'HW_CPU_X86_AVX', - 'CUSTOM_MAGIC', - } ) - self.assertResourceRequestsEqual( - expected, utils.ResourceRequest.from_image_props(image_meta_props, - req=existing_req)) + rs = objects.RequestSpec(flavor=flavor, is_bfv=True) + rr = utils.ResourceRequest(rs) + self.assertResourceRequestsEqual(expected, rr) def test_resource_request_add_group_inserts_the_group(self): - req = utils.ResourceRequest() + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) + rs = objects.RequestSpec(flavor=flavor, is_bfv=False) + req = utils.ResourceRequest(rs) rg1 = objects.RequestGroup() req.add_request_group(rg1) rg2 = objects.RequestGroup() @@ -918,18 +916,22 @@ class TestUtils(test.NoDBTestCase): self.assertIs(rg2, req.get_request_group(2)) def test_resource_request_add_group_inserts_the_group_implicit_group(self): - req = utils.ResourceRequest() - # this implicitly creates the unnumbered group - unnumbered_rg = req.get_request_group(None) + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) + rs = objects.RequestSpec(flavor=flavor, is_bfv=False) + req = utils.ResourceRequest(rs) + + # this implicitly creates the specified group + unnumbered_rg = req.get_request_group(42) rg1 = objects.RequestGroup() req.add_request_group(rg1) rg2 = objects.RequestGroup() req.add_request_group(rg2) - self.assertIs(rg1, req.get_request_group(1)) - self.assertIs(rg2, req.get_request_group(2)) - self.assertIs(unnumbered_rg, req.get_request_group(None)) + self.assertIs(rg1, req.get_request_group(43)) + self.assertIs(rg2, req.get_request_group(44)) + self.assertIs(unnumbered_rg, req.get_request_group(42)) def test_claim_resources_on_destination_no_source_allocations(self): """Tests the negative scenario where the instance does not have