diff --git a/nova/api/openstack/placement/handlers/resource_provider.py b/nova/api/openstack/placement/handlers/resource_provider.py index f108bd5a6ec9..0312fc16cb42 100644 --- a/nova/api/openstack/placement/handlers/resource_provider.py +++ b/nova/api/openstack/placement/handlers/resource_provider.py @@ -174,7 +174,9 @@ def list_resource_providers(req): want_version = req.environ[microversion.MICROVERSION_ENVIRON] schema = rp_schema.GET_RPS_SCHEMA_1_0 - if want_version.matches((1, 14)): + if want_version.matches((1, 18)): + schema = rp_schema.GET_RPS_SCHEMA_1_18 + elif want_version.matches((1, 14)): schema = rp_schema.GET_RPS_SCHEMA_1_14 elif want_version.matches((1, 4)): schema = rp_schema.GET_RPS_SCHEMA_1_4 @@ -184,7 +186,8 @@ def list_resource_providers(req): util.validate_query_params(req, schema) filters = {} - for attr in ['uuid', 'name', 'member_of', 'in_tree']: + qpkeys = ('uuid', 'name', 'member_of', 'in_tree', 'resources', 'required') + for attr in qpkeys: if attr in req.GET: value = req.GET[attr] # special case member_of to always make its value a @@ -203,10 +206,11 @@ def list_resource_providers(req): raise webob.exc.HTTPBadRequest( _('Invalid uuid value: %(uuid)s') % {'uuid': aggr_uuid}) + elif attr == 'resources': + value = util.normalize_resources_qs_param(value) + elif attr == 'required': + value = util.normalize_traits_qs_param(value) filters[attr] = value - if 'resources' in req.GET: - resources = util.normalize_resources_qs_param(req.GET['resources']) - filters['resources'] = resources try: resource_providers = rp_obj.ResourceProviderList.get_all_by_filters( context, filters) @@ -214,6 +218,10 @@ def list_resource_providers(req): raise webob.exc.HTTPBadRequest( _('Invalid resource class in resources parameter: %(error)s') % {'error': exc}) + except exception.TraitNotFound as exc: + raise webob.exc.HTTPBadRequest( + _('Invalid trait(s) in "required" parameter: %(error)s') % + {'error': exc}) response = req.response output, last_modified = _serialize_providers( diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py index 000dfcb809f3..83d18e2c8ee9 100644 --- a/nova/api/openstack/placement/microversion.py +++ b/nova/api/openstack/placement/microversion.py @@ -60,6 +60,7 @@ VERSIONS = [ '1.16', # Add 'limit' query parameter to GET /allocation_candidates '1.17', # Add 'required' query parameter to GET /allocation_candidates and # return traits in the provider summary. + '1.18', # Support ?required= queryparam on GET /resource_providers ] diff --git a/nova/api/openstack/placement/rest_api_version_history.rst b/nova/api/openstack/placement/rest_api_version_history.rst index ebb8cc295394..b6be104d0e49 100644 --- a/nova/api/openstack/placement/rest_api_version_history.rst +++ b/nova/api/openstack/placement/rest_api_version_history.rst @@ -158,7 +158,7 @@ for resources. The ``/resource_providers/{rp_uuid}/allocations`` endpoint has been available since version 1.0, but was not listed in the ``links`` section of the -``GET /resource_providers`` response. The link is included as of version 1.11. +``GET /resource_providers`` response. The link is included as of version 1.11. 1.12 PUT dict format to /allocations/{consumer_uuid} ---------------------------------------------------- @@ -221,3 +221,15 @@ value, `N`, which limits the maximum number of candidates returned. Add the `required` parameter to the `GET /allocation_candidates` API. It accepts a list of traits separated by `,`. The provider summary in the response will include the attached traits also. + +1.18 Support ?required= queryparam on GET /resource_providers +--------------------------------------------------------------------- + +Add support for the `required` query parameter to the `GET /resource_providers` +API. It accepts a comma-separated list of string trait names. When specified, +the API results will be filtered to include only resource providers marked with +all the specified traits. This is in addition to (logical AND) any filtering +based on other query parameters. + +Trait names which are empty, do not exist, or are otherwise invalid will result +in a 400 error. diff --git a/nova/api/openstack/placement/schemas/resource_provider.py b/nova/api/openstack/placement/schemas/resource_provider.py index 579791b40adc..7ca43ef69ac9 100644 --- a/nova/api/openstack/placement/schemas/resource_provider.py +++ b/nova/api/openstack/placement/schemas/resource_provider.py @@ -94,3 +94,13 @@ GET_RPS_SCHEMA_1_14['properties']['in_tree'] = { "type": "string", "format": "uuid", } + +# Microversion 1.18 adds support for the `required` query parameter to the +# `GET /resource_providers` API. It accepts a comma-separated list of string +# trait names. When specified, the API results will be filtered to include only +# resource providers marked with all the specified traits. This is in addition +# to (logical AND) any filtering based on other query parameters. +GET_RPS_SCHEMA_1_18 = copy.deepcopy(GET_RPS_SCHEMA_1_14) +GET_RPS_SCHEMA_1_18['properties']['required'] = { + "type": "string", +} diff --git a/nova/objects/resource_provider.py b/nova/objects/resource_provider.py index 7209ddad5560..a412022e710d 100644 --- a/nova/objects/resource_provider.py +++ b/nova/objects/resource_provider.py @@ -1396,6 +1396,7 @@ class ResourceProviderList(base.ObjectListBase, base.NovaObject): # 'MEMORY_MB': 1024 # }, # 'in_tree': , + # 'required': [, ...] # } if not filters: filters = {} @@ -1406,6 +1407,7 @@ class ResourceProviderList(base.ObjectListBase, base.NovaObject): name = filters.pop('name', None) uuid = filters.pop('uuid', None) member_of = filters.pop('member_of', []) + required = filters.pop('required', []) resources = filters.pop('resources', {}) # NOTE(sbauza): We want to key the dict by the resource class IDs @@ -1473,6 +1475,19 @@ class ResourceProviderList(base.ObjectListBase, base.NovaObject): [resource_provider_id]).select_from(join_statement) query = query.where(rp.c.id.in_(rps_in_aggregates)) + # If 'required' has values, add a filter to limit results to providers + # possessing *all* of the listed traits. + if required: + trait_map = _trait_ids_from_names(context, required) + if len(trait_map) != len(required): + missing = required - set(trait_map) + raise exception.TraitNotFound(names=', '.join(missing)) + rp_ids = _get_provider_ids_having_all_traits(context, trait_map) + if not rp_ids: + # If no providers have the required traits, we're done + return [] + query = query.where(rp.c.id.in_(rp_ids)) + if not resources: # Returns quickly the list in case we don't need to check the # resource usage @@ -1575,6 +1590,7 @@ class ResourceProviderList(base.ObjectListBase, base.NovaObject): :type filters: dict """ _ensure_rc_cache(context) + _ensure_trait_sync(context) resource_providers = cls._get_all_by_filters_from_db(context, filters) return base.obj_make_list(context, cls(context), ResourceProvider, resource_providers) diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml index 2a40d4f5cc17..7bafe881e47f 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml @@ -39,13 +39,13 @@ tests: response_json_paths: $.errors[0].title: Not Acceptable -- name: latest microversion is 1.17 +- name: latest microversion is 1.18 GET: / request_headers: openstack-api-version: placement latest response_headers: vary: /OpenStack-API-Version/ - openstack-api-version: placement 1.17 + openstack-api-version: placement 1.18 - name: other accept header bad version GET: / diff --git a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml index 33776f0de7b7..12305169460b 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml @@ -392,6 +392,107 @@ tests: $.resource_providers.`len`: 1 $.resource_providers[?uuid="$ENVIRON['ALT_PARENT_PROVIDER_UUID']"].root_provider_uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] +- name: filter providers by traits none of them have + GET: /resource_providers?required=HW_CPU_X86_SGX,HW_CPU_X86_SHA + response_json_paths: + $.resource_providers.`len`: 0 + +- name: add traits to a provider + PUT: /resource_providers/$ENVIRON['RP_UUID']/traits + request_headers: + content-type: application/json + data: + resource_provider_generation: 0 + traits: ['HW_CPU_X86_SGX', 'STORAGE_DISK_SSD'] + +- name: add traits to another provider + PUT: /resource_providers/$ENVIRON['ALT_PARENT_PROVIDER_UUID']/traits + request_headers: + content-type: application/json + data: + resource_provider_generation: 0 + traits: ['MISC_SHARES_VIA_AGGREGATE', 'STORAGE_DISK_SSD'] + +- name: filter providers with multiple traits where no provider has all of them + GET: /resource_providers?required=HW_CPU_X86_SGX,MISC_SHARES_VIA_AGGREGATE + response_json_paths: + $.resource_providers.`len`: 0 + +- name: filter providers with a trait some of them have + GET: /resource_providers?required=STORAGE_DISK_SSD + response_json_paths: + $.resource_providers.`len`: 2 + # Don't really care about the root UUID - just validating that the providers present are the ones we expected + $.resource_providers[?uuid="$ENVIRON['ALT_PARENT_PROVIDER_UUID']"].root_provider_uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + $.resource_providers[?uuid="$ENVIRON['RP_UUID']"].root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + +- name: list providers with 'required' parameter filters cumulatively with in_tree + GET: /resource_providers?required=STORAGE_DISK_SSD&in_tree=$ENVIRON['RP_UUID'] + response_json_paths: + $.resource_providers.`len`: 1 + # Only RP_UUID satisfies both the tree and trait constraint + $.resource_providers[?uuid="$ENVIRON['RP_UUID']"].root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + +- name: create some inventory + PUT: /resource_providers/$ENVIRON['ALT_PARENT_PROVIDER_UUID']/inventories + request_headers: + content-type: application/json + data: + resource_provider_generation: 1 + inventories: + IPV4_ADDRESS: + total: 253 + DISK_GB: + total: 1024 + status: 200 + response_json_paths: + $.resource_provider_generation: 2 + $.inventories.IPV4_ADDRESS.total: 253 + $.inventories.IPV4_ADDRESS.reserved: 0 + $.inventories.DISK_GB.total: 1024 + $.inventories.DISK_GB.allocation_ratio: 1.0 + +- name: list providers with 'required' parameter filters cumulatively with resources + GET: /resource_providers?required=STORAGE_DISK_SSD&resources=IPV4_ADDRESS:10 + response_json_paths: + $.resource_providers.`len`: 1 + # Only ALT_PARENT_PROVIDER_UUID satisfies both the tree and trait constraint + $.resource_providers[?uuid="$ENVIRON['ALT_PARENT_PROVIDER_UUID']"].root_provider_uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + +- name: invalid 'required' parameter - blank + GET: /resource_providers?required= + status: 400 + response_strings: + - "Invalid query string parameters: Expected 'required' parameter value of the form: HW_CPU_X86_VMX,CUSTOM_MAGIC." + response_json_paths: + $.errors[0].title: Bad Request + +- name: invalid 'required' parameter - contains an empty trait name + GET: /resource_providers?required=STORAGE_DISK_SSD,,MISC_SHARES_VIA_AGGREGATE + status: 400 + response_strings: + - "Invalid query string parameters: Expected 'required' parameter value of the form: HW_CPU_X86_VMX,CUSTOM_MAGIC." + response_json_paths: + $.errors[0].title: Bad Request + +- name: invalid 'required' parameter - contains a nonexistent trait + GET: /resource_providers?required=STORAGE_DISK_SSD,BOGUS_TRAIT,MISC_SHARES_VIA_AGGREGATE + status: 400 + response_strings: + - "No such trait(s): BOGUS_TRAIT." + response_json_paths: + $.errors[0].title: Bad Request + +- name: schema validation fails with 'required' parameter on old microversion + request_headers: + openstack-api-version: placement 1.17 + GET: /resource_providers?required=HW_CPU_X86_SGX,MISC_SHARES_VIA_AGGREGATE + status: 400 + response_strings: + - Additional properties are not allowed + response_json_paths: + $.errors[0].title: Bad Request + - name: fail trying to re-parent to a different provider PUT: /resource_providers/$ENVIRON['RP_UUID'] request_headers: diff --git a/placement-api-ref/source/parameters.yaml b/placement-api-ref/source/parameters.yaml index 6c1e87bdd99a..5a42e69213d7 100644 --- a/placement-api-ref/source/parameters.yaml +++ b/placement-api-ref/source/parameters.yaml @@ -85,6 +85,14 @@ resource_provider_name_query: required: false description: > The name of a resource provider to filter the list. +resource_provider_required_query: + type: string + in: query + required: false + description: > + A comma-delimited list of string trait names. Results will be filtered to + include only resource providers having all the specified traits. + min_version: 1.18 resource_provider_tree_query: type: string in: query diff --git a/placement-api-ref/source/resource_providers.inc b/placement-api-ref/source/resource_providers.inc index 20a1cf7ce69f..60bae1f5106f 100644 --- a/placement-api-ref/source/resource_providers.inc +++ b/placement-api-ref/source/resource_providers.inc @@ -28,6 +28,7 @@ of all filters are merged with a boolean `AND`. - uuid: resource_provider_uuid_query - member_of: member_of - resources: resources_query + - required: resource_provider_required_query - in_tree: resource_provider_tree_query Response diff --git a/releasenotes/notes/placement-required-traits-on-list-resource-providers-fab11cdb36cd3502.yaml b/releasenotes/notes/placement-required-traits-on-list-resource-providers-fab11cdb36cd3502.yaml new file mode 100644 index 000000000000..1fa3ecd37d93 --- /dev/null +++ b/releasenotes/notes/placement-required-traits-on-list-resource-providers-fab11cdb36cd3502.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Placement API microversion 1.18 adds support for the `required` query + parameter to the `GET /resource_providers` API. It accepts a + comma-separated list of string trait names. When specified, the API + results will be filtered to include only resource providers marked with + all the specified traits. This is in addition to (logical AND) any + filtering based on other query parameters. + + Trait names which are empty, do not exist, or are otherwise invalid will + result in a 400 error.