diff --git a/doc/source/user/flavors.rst b/doc/source/user/flavors.rst index 3052b7d5ba6c..89aa538b15be 100644 --- a/doc/source/user/flavors.rst +++ b/doc/source/user/flavors.rst @@ -632,9 +632,26 @@ Required traits The scheduler will pass required traits to the ``GET /allocation_candidates`` endpoint in the Placement API to include - only resource providers that can satisfy the required traits. Currently - the only valid value is ``required``. Any other value will be considered + only resource providers that can satisfy the required traits. In 17.0.0 + the only valid value is ``required``. In 18.0.0 ``forbidden`` is added (see + below). Any other value will be considered invalid. The FilterScheduler is currently the only scheduler driver that supports this feature. + +Forbidden traits + Added in the 18.0.0 Rocky release. + + Forbidden traits are similar to required traits, described above, but + instead of specifying the set of traits that must be satisfied by a compute + node, forbidden traits must **not** be present. + + The syntax of the extra spec is ``trait:=forbidden``, for + example: + + - trait:HW_CPU_X86_AVX2=forbidden + - trait:STORAGE_DISK_SSD=forbidden + + The FilterScheduler is currently the only scheduler driver that supports + this feature. diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index af3c6e4d5795..f0e33552fd17 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -339,6 +339,7 @@ class SchedulerReportClient(object): # and traits in the query string (via a new method on ResourceRequest). res = resources.get_request_group(None).resources required_traits = resources.get_request_group(None).required_traits + forbidden_traits = resources.get_request_group(None).forbidden_traits aggregates = resources.get_request_group(None).member_of resource_query = ",".join( @@ -350,6 +351,14 @@ class SchedulerReportClient(object): } if required_traits: qs_params['required'] = ",".join(required_traits) + if forbidden_traits: + # Sorted to make testing easier to manage and for + # predictability. + forbiddens = ',!'.join(sorted(forbidden_traits)) + if qs_params['required']: + qs_params['required'] += ',!' + forbiddens + else: + qs_params['required'] = '!' + forbiddens if aggregates: # NOTE(danms): In 1.21, placement cannot take an AND'd # set of aggregates, only an OR'd set. Thus, if we have diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 6e5d0b9d17bd..602e3310bc42 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -85,17 +85,20 @@ class ResourceRequest(object): self.get_request_group(groupid).resources[rclass] = amount def _add_trait(self, groupid, trait_name, trait_type): - # Currently the only valid value for a trait entry is 'required'. - trait_vals = ('required',) - # Ensure the value is supported. - if trait_type not in trait_vals: + # Currently the only valid values for a trait entry are 'required' + # and 'forbidden' + trait_vals = ('required', 'forbidden') + if trait_type == 'required': + self.get_request_group(groupid).required_traits.add(trait_name) + elif trait_type == 'forbidden': + self.get_request_group(groupid).forbidden_traits.add(trait_name) + else: LOG.warning( "Only (%(tvals)s) traits are supported. Received '%(val)s' " "for key trait%(groupid)s.", {"tvals": ', '.join(trait_vals), "groupid": groupid, "val": trait_type}) - return - self.get_request_group(groupid).required_traits.add(trait_name) + return @classmethod def from_extra_specs(cls, extra_specs): diff --git a/nova/tests/unit/scheduler/client/test_report.py b/nova/tests/unit/scheduler/client/test_report.py index c809dff09862..8f0ec5b23a37 100644 --- a/nova/tests/unit/scheduler/client/test_report.py +++ b/nova/tests/unit/scheduler/client/test_report.py @@ -1421,14 +1421,18 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'resources1:DISK_GB': '30', 'trait:CUSTOM_TRAIT1': 'required', 'trait:CUSTOM_TRAIT2': 'preferred', + 'trait:CUSTOM_TRAIT3': 'forbidden', + 'trait:CUSTOM_TRAIT4': 'forbidden', }) resources.get_request_group(None).member_of = [ ('agg1', 'agg2', 'agg3'), ('agg1', 'agg2')] expected_path = '/allocation_candidates' - expected_query = {'resources': ['MEMORY_MB:1024,VCPU:1'], - 'required': ['CUSTOM_TRAIT1'], - 'member_of': ['in:agg1,agg2'], - 'limit': ['1000']} + expected_query = { + 'resources': ['MEMORY_MB:1024,VCPU:1'], + 'required': ['CUSTOM_TRAIT1,!CUSTOM_TRAIT3,!CUSTOM_TRAIT4'], + 'member_of': ['in:agg1,agg2'], + 'limit': ['1000'] + } resp_mock.json.return_value = json_data self.ks_adap_mock.get.return_value = resp_mock diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index 39117ff48d03..ff3b425fca92 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -373,6 +373,7 @@ class TestUtils(test.NoDBTestCase): # 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', @@ -384,6 +385,7 @@ class TestUtils(test.NoDBTestCase): # 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', @@ -405,7 +407,10 @@ class TestUtils(test.NoDBTestCase): required_traits={ 'HW_CPU_X86_AVX', 'CUSTOM_MAGIC', - } + }, + forbidden_traits={ + 'CUSTOM_BRONZE', + }, ) expected._rg_by_id['1'] = plib.RequestGroup( resources={ @@ -414,7 +419,10 @@ class TestUtils(test.NoDBTestCase): }, required_traits={ 'CUSTOM_PHYSNET_NET1', - } + }, + forbidden_traits={ + 'CUSTOM_PHYSNET_NET2', + }, ) expected._rg_by_id['2'] = plib.RequestGroup( resources={ diff --git a/releasenotes/notes/forbidden-traits-in-nova-478f1884a06e50e7.yaml b/releasenotes/notes/forbidden-traits-in-nova-478f1884a06e50e7.yaml new file mode 100644 index 000000000000..4f7968d3db6a --- /dev/null +++ b/releasenotes/notes/forbidden-traits-in-nova-478f1884a06e50e7.yaml @@ -0,0 +1,21 @@ +--- +features: + - | + Added support for forbidden traits to the scheduler. A flavor extra spec + is extended to support specifying the forbidden traits. The syntax of + extra spec is ``trait:=forbidden``, for example: + + - trait:HW_CPU_X86_AVX2=forbidden + - trait:STORAGE_DISK_SSD=forbidden + + The scheduler will pass the forbidden traits to the + ``GET /allocation_candidates`` endpoint in the Placement API to include + only resource providers that do not include the forbidden traits. Currently + the only valid values are ``required`` and ``forbidden``. Any other values + will be considered invalid. + + This requires that the Placement API version 1.22 is available before + the ``nova-scheduler`` service can use this feature. + + The FilterScheduler is currently the only scheduler driver that supports + this feature.