diff --git a/nova/api/openstack/placement/handlers/resource_provider.py b/nova/api/openstack/placement/handlers/resource_provider.py index c504bc94fc72..81ecce08638a 100644 --- a/nova/api/openstack/placement/handlers/resource_provider.py +++ b/nova/api/openstack/placement/handlers/resource_provider.py @@ -41,13 +41,30 @@ POST_RESOURCE_PROVIDER_SCHEMA = { }, "required": [ "name" - ], + ], "additionalProperties": False, } # Remove uuid to create the schema for PUTting a resource provider PUT_RESOURCE_PROVIDER_SCHEMA = copy.deepcopy(POST_RESOURCE_PROVIDER_SCHEMA) PUT_RESOURCE_PROVIDER_SCHEMA['properties'].pop('uuid') +# Placement API microversion 1.14 adds an optional parent_provider_uuid field +# to the POST and PUT request schemas +POST_RP_SCHEMA_V1_14 = copy.deepcopy(POST_RESOURCE_PROVIDER_SCHEMA) +POST_RP_SCHEMA_V1_14["properties"]["parent_provider_uuid"] = { + "anyOf": [ + { + "type": "string", + "format": "uuid", + }, + { + "type": "null", + } + ] +} +PUT_RP_SCHEMA_V1_14 = copy.deepcopy(POST_RP_SCHEMA_V1_14) +PUT_RP_SCHEMA_V1_14['properties'].pop('uuid') + # Represents the allowed query string parameters to the GET /resource_providers # API call GET_RPS_SCHEMA_1_0 = { @@ -80,6 +97,17 @@ GET_RPS_SCHEMA_1_4['properties']['resources'] = { "type": "string" } +# Placement API microversion 1.14 adds support for requesting resource +# providers within a tree of providers. The 'in_tree' query string parameter +# should be the UUID of a resource provider. The result of the GET call will +# include only those resource providers in the same "provider tree" as the +# provider with the UUID represented by 'in_tree' +GET_RPS_SCHEMA_1_14 = copy.deepcopy(GET_RPS_SCHEMA_1_4) +GET_RPS_SCHEMA_1_14['properties']['in_tree'] = { + "type": "string", + "format": "uuid", +} + def _serialize_links(environ, resource_provider): url = util.resource_provider_url(environ, resource_provider) @@ -97,20 +125,23 @@ def _serialize_links(environ, resource_provider): return links -def _serialize_provider(environ, resource_provider): +def _serialize_provider(environ, resource_provider, want_version): data = { 'uuid': resource_provider.uuid, 'name': resource_provider.name, 'generation': resource_provider.generation, 'links': _serialize_links(environ, resource_provider) } + if want_version.matches((1, 14)): + data['parent_provider_uuid'] = resource_provider.parent_provider_uuid + data['root_provider_uuid'] = resource_provider.root_provider_uuid return data -def _serialize_providers(environ, resource_providers): +def _serialize_providers(environ, resource_providers, want_version): output = [] for provider in resource_providers: - provider_data = _serialize_provider(environ, provider) + provider_data = _serialize_provider(environ, provider, want_version) output.append(provider_data) return {"resource_providers": output} @@ -124,12 +155,15 @@ def create_resource_provider(req): header pointing to the newly created resource provider. """ context = req.environ['placement.context'] - data = util.extract_json(req.body, POST_RESOURCE_PROVIDER_SCHEMA) + schema = POST_RESOURCE_PROVIDER_SCHEMA + want_version = req.environ[microversion.MICROVERSION_ENVIRON] + if want_version.matches((1, 14)): + schema = POST_RP_SCHEMA_V1_14 + data = util.extract_json(req.body, schema) try: - uuid = data.get('uuid', uuidutils.generate_uuid()) - resource_provider = rp_obj.ResourceProvider( - context, name=data['name'], uuid=uuid) + uuid = data.setdefault('uuid', uuidutils.generate_uuid()) + resource_provider = rp_obj.ResourceProvider(context, **data) resource_provider.create() except db_exc.DBDuplicateEntry as exc: # Whether exc.columns has one or two entries (in the event @@ -192,8 +226,9 @@ def get_resource_provider(req): resource_provider = rp_obj.ResourceProvider.get_by_uuid( context, uuid) + want_version = req.environ[microversion.MICROVERSION_ENVIRON] req.response.body = encodeutils.to_utf8(jsonutils.dumps( - _serialize_provider(req.environ, resource_provider))) + _serialize_provider(req.environ, resource_provider, want_version))) req.response.content_type = 'application/json' return req.response @@ -210,15 +245,17 @@ def list_resource_providers(req): want_version = req.environ[microversion.MICROVERSION_ENVIRON] schema = GET_RPS_SCHEMA_1_0 - if want_version == (1, 3): - schema = GET_RPS_SCHEMA_1_3 - if want_version >= (1, 4): + if want_version.matches((1, 14)): + schema = GET_RPS_SCHEMA_1_14 + elif want_version.matches((1, 4)): schema = GET_RPS_SCHEMA_1_4 + elif want_version.matches((1, 3)): + schema = GET_RPS_SCHEMA_1_3 util.validate_query_params(req, schema) filters = {} - for attr in ['uuid', 'name', 'member_of']: + for attr in ['uuid', 'name', 'member_of', 'in_tree']: if attr in req.GET: value = req.GET[attr] # special case member_of to always make its value a @@ -250,8 +287,8 @@ def list_resource_providers(req): {'error': exc}) response = req.response - response.body = encodeutils.to_utf8( - jsonutils.dumps(_serialize_providers(req.environ, resource_providers))) + response.body = encodeutils.to_utf8(jsonutils.dumps( + _serialize_providers(req.environ, resource_providers, want_version))) response.content_type = 'application/json' return response @@ -271,9 +308,16 @@ def update_resource_provider(req): resource_provider = rp_obj.ResourceProvider.get_by_uuid( context, uuid) - data = util.extract_json(req.body, PUT_RESOURCE_PROVIDER_SCHEMA) + schema = PUT_RESOURCE_PROVIDER_SCHEMA + want_version = req.environ[microversion.MICROVERSION_ENVIRON] + if want_version.matches((1, 14)): + schema = PUT_RP_SCHEMA_V1_14 - resource_provider.name = data['name'] + data = util.extract_json(req.body, schema) + + for field in rp_obj.ResourceProvider.SETTABLE_FIELDS: + if field in data: + setattr(resource_provider, field, data[field]) try: resource_provider.save() @@ -287,7 +331,7 @@ def update_resource_provider(req): {'rp_uuid': uuid, 'error': exc}) req.response.body = encodeutils.to_utf8(jsonutils.dumps( - _serialize_provider(req.environ, resource_provider))) + _serialize_provider(req.environ, resource_provider, want_version))) req.response.status = 200 req.response.content_type = 'application/json' return req.response diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py index 323e68b4dbab..d4a6a20cfbb4 100644 --- a/nova/api/openstack/placement/microversion.py +++ b/nova/api/openstack/placement/microversion.py @@ -54,6 +54,9 @@ VERSIONS = [ # as GET. The 'allocation_requests' format in GET # /allocation_candidates is updated to be the same as well. '1.13', # Adds POST /allocations to set allocations for multiple consumers + # as GET + '1.14', # Adds parent and root provider UUID on resource provider + # representation and 'in_tree' filter 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 b4a2c1827267..5dc510a18193 100644 --- a/nova/api/openstack/placement/rest_api_version_history.rst +++ b/nova/api/openstack/placement/rest_api_version_history.rst @@ -178,3 +178,22 @@ with the new `PUT` format. Version 1.13 gives the ability to set or clear allocations for more than one consumer uuid with a request to ``POST /allocations``. + +1.14 Add nested resource providers +---------------------------------- + +The 1.14 version introduces the concept of nested resource providers. The +resource provider resource now contains two new attributes: + +* ``parent_provider_uuid`` indicates the provider's direct parent, or null if + there is no parent. This attribute can be set in the call to ``POST + /resource_providers`` and ``PUT /resource_providers/{uuid}`` if the attribute + has not already been set to a non-NULL value (i.e. we do not support + "reparenting" a provider) +* ``root_provider_uuid`` indicates the UUID of the root resource provider in + the provider's tree. This is a read-only attribute + +A new ``in_tree=`` parameter is now available in the ``GET +/resource-providers`` API call. Supplying a UUID value for the ``in_tree`` +parameter will cause all resource providers within the "provider tree" of the +provider matching ```` to be returned. diff --git a/nova/tests/functional/api/openstack/placement/fixtures.py b/nova/tests/functional/api/openstack/placement/fixtures.py index f419cd4f8d98..9e9634678673 100644 --- a/nova/tests/functional/api/openstack/placement/fixtures.py +++ b/nova/tests/functional/api/openstack/placement/fixtures.py @@ -85,6 +85,8 @@ class APIFixture(fixture.GabbiFixture): os.environ['INSTANCE_UUID'] = uuidutils.generate_uuid() os.environ['MIGRATION_UUID'] = uuidutils.generate_uuid() os.environ['CONSUMER_UUID'] = uuidutils.generate_uuid() + os.environ['PARENT_PROVIDER_UUID'] = uuidutils.generate_uuid() + os.environ['ALT_PARENT_PROVIDER_UUID'] = uuidutils.generate_uuid() def stop_fixture(self): self.api_db_fixture.cleanup() diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml index ccbdcbb8c9c6..c2e8b6deb438 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.13 +- name: latest microversion is 1.14 GET: / request_headers: openstack-api-version: placement latest response_headers: vary: /OpenStack-API-Version/ - openstack-api-version: placement 1.13 + openstack-api-version: placement 1.14 - 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 8c2ac663fa5d..c0c7f53ce482 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml @@ -6,6 +6,7 @@ defaults: request_headers: x-auth-token: admin accept: application/json + openstack-api-version: placement latest tests: @@ -80,6 +81,7 @@ tests: response_json_paths: $.uuid: $ENVIRON['RP_UUID'] $.name: $ENVIRON['RP_NAME'] + $.parent_provider_uuid: null $.generation: 0 $.links[?rel = "self"].href: /resource_providers/$ENVIRON['RP_UUID'] $.links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories @@ -107,6 +109,7 @@ tests: $.resource_providers[0].uuid: $ENVIRON['RP_UUID'] $.resource_providers[0].name: $ENVIRON['RP_NAME'] $.resource_providers[0].generation: 0 + $.resource_providers[0].parent_provider_uuid: null $.resource_providers[0].links[?rel = "self"].href: /resource_providers/$ENVIRON['RP_UUID'] $.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages @@ -174,7 +177,7 @@ tests: $.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages -- name: update a resource provider +- name: update a resource provider's name PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid'] request_headers: content-type: application/json @@ -208,6 +211,174 @@ tests: response_json_paths: $.errors[0].title: Bad Request +# This section of tests validate nested resource provider relationships and +# constraints. We attempt to set the parent provider UUID for the primary +# resource provider to a UUID value of a provider we have not yet created and +# expect a failure. We then create that parent provider record and attempt to +# set the same parent provider UUID without also setting the root provider UUID +# to the same value, with an expected failure. Finally, we set the primary +# provider's root AND parent to the new provider UUID and verify success. + +- name: test POST microversion limits nested providers + POST: /resource_providers + request_headers: + openstack-api-version: placement 1.13 + content-type: application/json + data: + name: child + parent_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 400 + response_strings: + - 'JSON does not validate' + +- name: test PUT microversion limits nested providers + PUT: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + openstack-api-version: placement 1.13 + content-type: application/json + data: + name: child + parent_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 400 + response_strings: + - 'JSON does not validate' + +- name: fail trying to set a root provider UUID + PUT: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + content-type: application/json + data: + root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 400 + response_strings: + - 'JSON does not validate' + +- name: fail trying to self-parent + POST: /resource_providers + request_headers: + content-type: application/json + data: + name: child + uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + parent_provider_uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + status: 400 + response_strings: + - 'parent provider UUID cannot be same as UUID' + +- name: update a parent provider UUID to non-existing provider + PUT: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + content-type: application/json + data: + name: parent + parent_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 400 + response_strings: + - 'parent provider UUID does not exist' + +- name: now create the parent provider + POST: /resource_providers + request_headers: + content-type: application/json + data: + name: parent + uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 201 + +- name: get provider with old microversion no root provider UUID field + GET: /resource_providers/$ENVIRON['PARENT_PROVIDER_UUID'] + request_headers: + openstack-api-version: placement 1.13 + content-type: application/json + response_json_paths: + $.`len`: 4 + name: parent + status: 200 + +- name: get provider has root provider UUID field + GET: /resource_providers/$ENVIRON['PARENT_PROVIDER_UUID'] + request_headers: + content-type: application/json + response_json_paths: + $.`len`: 6 + name: parent + root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + parent_provider_uuid: null + status: 200 + +- name: update a parent + PUT: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + content-type: application/json + data: + name: child + parent_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 200 + +- name: get provider has new parent and root provider UUID field + GET: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + content-type: application/json + response_json_paths: + name: child + root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + parent_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + status: 200 + +- name: fail trying to un-parent + PUT: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + content-type: application/json + data: + name: child + parent_provider_uuid: null + status: 400 + response_strings: + - 'un-parenting a provider is not currently allowed' + +- name: list all resource providers in a tree that does not exist + GET: /resource_providers?in_tree=$ENVIRON['ALT_PARENT_PROVIDER_UUID'] + response_json_paths: + $.resource_providers.`len`: 0 + +- name: list all resource providers in a tree with multiple providers in tree + GET: /resource_providers?in_tree=$ENVIRON['RP_UUID'] + response_json_paths: + $.resource_providers.`len`: 2 + # Verify that we have both the parent and child in the list + $.resource_providers[?uuid="$ENVIRON['PARENT_PROVIDER_UUID']"].root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + $.resource_providers[?uuid="$ENVIRON['RP_UUID']"].root_provider_uuid: $ENVIRON['PARENT_PROVIDER_UUID'] + +- name: create a new parent provider + POST: /resource_providers + request_headers: + content-type: application/json + data: + name: altwparent + uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + status: 201 + response_headers: + location: //resource_providers/[a-f0-9-]+/ + response_forbidden_headers: + - content-type + +- name: list all resource providers in a tree + GET: /resource_providers?in_tree=$ENVIRON['ALT_PARENT_PROVIDER_UUID'] + response_json_paths: + $.resource_providers.`len`: 1 + $.resource_providers[?uuid="$ENVIRON['ALT_PARENT_PROVIDER_UUID']"].root_provider_uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + +- name: fail trying to re-parent to a different provider + PUT: /resource_providers/$ENVIRON['RP_UUID'] + request_headers: + content-type: application/json + data: + name: child + parent_provider_uuid: $ENVIRON['ALT_PARENT_PROVIDER_UUID'] + status: 400 + response_strings: + - 're-parenting a provider is not currently allowed' + - name: create a new provider POST: /resource_providers request_headers: @@ -221,7 +392,7 @@ tests: request_headers: content-type: application/json data: - name: new name + name: child status: 409 response_json_paths: $.errors[0].title: Conflict diff --git a/placement-api-ref/source/create-resource_providers-request.json b/placement-api-ref/source/create-resource_providers-request.json index b6613a0a97a8..4b3dcd871c2f 100644 --- a/placement-api-ref/source/create-resource_providers-request.json +++ b/placement-api-ref/source/create-resource_providers-request.json @@ -1,4 +1,5 @@ { "name": "NFS Share", - "uuid": "7d2590ae-fb85-4080-9306-058b4c915e3f" + "uuid": "7d2590ae-fb85-4080-9306-058b4c915e3f", + "parent_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8" } diff --git a/placement-api-ref/source/get-resource_provider.json b/placement-api-ref/source/get-resource_provider.json index 7f65c1c1f77a..9a79680eb365 100644 --- a/placement-api-ref/source/get-resource_provider.json +++ b/placement-api-ref/source/get-resource_provider.json @@ -27,5 +27,7 @@ } ], "name": "Ceph Storage Pool", - "uuid": "3b4005be-d64b-456f-ba36-0ffd02718868" + "uuid": "3b4005be-d64b-456f-ba36-0ffd02718868", + "parent_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8", + "root_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8" } diff --git a/placement-api-ref/source/get-resource_providers.json b/placement-api-ref/source/get-resource_providers.json index a5a32a9466b9..33509c977df5 100644 --- a/placement-api-ref/source/get-resource_providers.json +++ b/placement-api-ref/source/get-resource_providers.json @@ -29,7 +29,9 @@ "rel": "allocations" } ], - "name": "vgr.localdomain" + "name": "vgr.localdomain", + "parent_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8", + "root_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8" }, { "generation": 2, @@ -60,7 +62,9 @@ "rel": "allocations" } ], - "name": "pony1" + "name": "pony1", + "parent_provider_uuid": null, + "root_provider_uuid": "d0b381e9-8761-42de-8e6c-bba99a96d5f5" } ] } diff --git a/placement-api-ref/source/parameters.yaml b/placement-api-ref/source/parameters.yaml index b2f49f232ef7..faca4471015f 100644 --- a/placement-api-ref/source/parameters.yaml +++ b/placement-api-ref/source/parameters.yaml @@ -58,6 +58,14 @@ resource_provider_name_query: required: false description: > The name of a resource provider to filter the list. +resource_provider_tree_query: + type: string + in: query + required: false + description: > + A UUID of a resource provider. The returned resource providers will be in + the same "provider tree" as the specified provider. + min_version: 1.14 resource_provider_uuid_query: <<: *resource_provider_uuid_path in: query @@ -290,6 +298,34 @@ resource_provider_object: required: true description: > A dictionary which contains the UUID of the resource provider. +resource_provider_parent_provider_uuid: + type: string + in: body + required: false + description: > + The UUID of the immediate parent of the resource provider. + min_version: 1.14 +resource_provider_parent_provider_uuid_required: + type: string + in: body + required: true + description: > + The UUID of the immediate parent of the resource provider. + min_version: 1.14 +resource_provider_root_provider_uuid: + type: string + in: body + required: false + description: > + Read-only UUID of the top-most provider in this provider tree. + min_version: 1.14 +resource_provider_root_provider_uuid_required: + type: string + in: body + required: true + description: > + Read-only UUID of the top-most provider in this provider tree. + min_version: 1.14 resource_provider_usages: type: object in: body diff --git a/placement-api-ref/source/resource_provider.inc b/placement-api-ref/source/resource_provider.inc index bc1dcbbd7705..21bd87d506f4 100644 --- a/placement-api-ref/source/resource_provider.inc +++ b/placement-api-ref/source/resource_provider.inc @@ -34,7 +34,8 @@ Response - uuid: resource_provider_uuid - links: resource_provider_links - name: resource_provider_name - + - parent_provider_uuid: resource_provider_parent_provider_uuid_required + - root_provider_uuid: resource_provider_root_provider_uuid_required Response Example ---------------- @@ -63,6 +64,7 @@ Request - uuid: resource_provider_uuid_path - name: resource_provider_name + - parent_provider_uuid: resource_provider_parent_provider_uuid Request example --------------- @@ -79,6 +81,8 @@ Response - uuid: resource_provider_uuid - links: resource_provider_links - name: resource_provider_name + - parent_provider_uuid: resource_provider_parent_provider_uuid_required + - root_provider_uuid: resource_provider_root_provider_uuid_required Response Example ---------------- diff --git a/placement-api-ref/source/resource_providers.inc b/placement-api-ref/source/resource_providers.inc index b0bdc5ff5c84..cffd98a7a191 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 + - in_tree: resource_provider_tree_query Response -------- @@ -39,7 +40,8 @@ Response - uuid: resource_provider_uuid - links: resource_provider_links - name: resource_provider_name - + - parent_provider_uuid: resource_provider_parent_provider_uuid + - root_provider_uuid: resource_provider_root_provider_uuid Response Example ---------------- @@ -69,6 +71,8 @@ Request - name: resource_provider_name - uuid: resource_provider_uuid_opt + - parent_provider_uuid: resource_provider_parent_provider_uuid + - root_provider_uuid: resource_provider_root_provider_uuid Request example --------------- diff --git a/placement-api-ref/source/update-resource_provider-request.json b/placement-api-ref/source/update-resource_provider-request.json index 72673db21b54..e3df4d590aa3 100644 --- a/placement-api-ref/source/update-resource_provider-request.json +++ b/placement-api-ref/source/update-resource_provider-request.json @@ -1 +1,4 @@ - {"name": "Shared storage"} + { + "name": "Shared storage", + "parent_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8" + } diff --git a/releasenotes/notes/placement-rest-api-nested-resource-providers-552a923a96d7adca.yaml b/releasenotes/notes/placement-rest-api-nested-resource-providers-552a923a96d7adca.yaml new file mode 100644 index 000000000000..548c4c93d5ac --- /dev/null +++ b/releasenotes/notes/placement-rest-api-nested-resource-providers-552a923a96d7adca.yaml @@ -0,0 +1,13 @@ +--- +features: + - New placement REST API microversion 1.14 is added to support nested + resource providers. Users of the placement REST API can now pass a + ``in_tree=`` parameter to the ``GET /resource_providers`` REST API + call. This will trigger the placement service to return all resource + provider records within the "provider tree" of the resource provider with + the supplied UUID value. The resource provider representation now includes + a ``parent_provider_uuid`` value that indicates the UUID of the immediate + parent resource provider, or ``null`` if the provider has no parent. For + convenience, the resource provider resource also contains a + ``root_provider_uuid`` field that is populated with the UUID of the + top-most resource provider in the provider tree.