diff --git a/nova/api/openstack/placement/handler.py b/nova/api/openstack/placement/handler.py index 8b0da199aa00..a070f4cf70fb 100644 --- a/nova/api/openstack/placement/handler.py +++ b/nova/api/openstack/placement/handler.py @@ -34,6 +34,7 @@ from nova.api.openstack.placement.handlers import inventory from nova.api.openstack.placement.handlers import resource_class from nova.api.openstack.placement.handlers import resource_provider from nova.api.openstack.placement.handlers import root +from nova.api.openstack.placement.handlers import trait from nova.api.openstack.placement.handlers import usage from nova.api.openstack.placement import policy from nova.api.openstack.placement import util @@ -103,6 +104,19 @@ ROUTE_DECLARATIONS = { 'PUT': allocation.set_allocations, 'DELETE': allocation.delete_allocations, }, + '/traits': { + 'GET': trait.list_traits, + }, + '/traits/{name}': { + 'GET': trait.get_trait, + 'PUT': trait.put_trait, + 'DELETE': trait.delete_trait, + }, + '/resource_providers/{uuid}/traits': { + 'GET': trait.list_traits_for_resource_provider, + 'PUT': trait.update_traits_for_resource_provider, + 'DELETE': trait.delete_traits_for_resource_provider + }, } diff --git a/nova/api/openstack/placement/handlers/resource_provider.py b/nova/api/openstack/placement/handlers/resource_provider.py index 19fd8fdb2525..ffedd1446a83 100644 --- a/nova/api/openstack/placement/handlers/resource_provider.py +++ b/nova/api/openstack/placement/handlers/resource_provider.py @@ -144,7 +144,7 @@ def _normalize_resources_qs_param(qs): def _serialize_links(environ, resource_provider): url = util.resource_provider_url(environ, resource_provider) links = [{'rel': 'self', 'href': url}] - for rel in ('aggregates', 'inventories', 'usages'): + for rel in ('aggregates', 'inventories', 'usages', 'traits'): links.append({'rel': rel, 'href': '%s/%s' % (url, rel)}) return links diff --git a/nova/api/openstack/placement/handlers/trait.py b/nova/api/openstack/placement/handlers/trait.py new file mode 100644 index 000000000000..408a24f867a1 --- /dev/null +++ b/nova/api/openstack/placement/handlers/trait.py @@ -0,0 +1,265 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Traits handlers for Placement API.""" + +import copy + +import jsonschema +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +import webob + +from nova.api.openstack.placement import microversion +from nova.api.openstack.placement import util +from nova.api.openstack.placement import wsgi_wrapper +from nova import exception +from nova.i18n import _ +from nova import objects + +TRAIT = { + "type": "string", + 'minLength': 1, 'maxLength': 255, +} + +CUSTOM_TRAIT = copy.deepcopy(TRAIT) +CUSTOM_TRAIT.update({"pattern": "^CUSTOM_[A-Z0-9_]+$"}) + +PUT_TRAITS_SCHEMA = { + "type": "object", + "properties": { + "traits": { + "type": "array", + "items": CUSTOM_TRAIT, + } + }, + 'required': ['traits'], + 'additionalProperties': False +} + +SET_TRAITS_FOR_RP_SCHEMA = copy.deepcopy(PUT_TRAITS_SCHEMA) +SET_TRAITS_FOR_RP_SCHEMA['properties']['traits']['items'] = TRAIT +SET_TRAITS_FOR_RP_SCHEMA['properties'][ + 'resource_provider_generation'] = {'type': 'integer'} +SET_TRAITS_FOR_RP_SCHEMA['required'].append('resource_provider_generation') + + +LIST_TRAIT_SCHEMA = { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "associated": { + "type": "string", + } + }, + "additionalProperties": False +} + + +def _normalize_traits_qs_param(qs): + try: + op, value = qs.split(':', 1) + except ValueError: + msg = _('Badly formatted name parameter. Expected name query string ' + 'parameter in form: ' + '?name=[in|startswith]:[name1,name2|prefix]. Got: "%s"') + msg = msg % qs + raise webob.exc.HTTPBadRequest(msg) + + filters = {} + if op == 'in': + filters['name_in'] = value.split(',') + elif op == 'startswith': + filters['prefix'] = value + + return filters + + +def _serialize_traits(traits): + return {'traits': [trait.name for trait in traits]} + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +def put_trait(req): + context = req.environ['placement.context'] + name = util.wsgi_path_item(req.environ, 'name') + + try: + jsonschema.validate(name, CUSTOM_TRAIT) + except jsonschema.ValidationError: + raise webob.exc.HTTPBadRequest( + _('The trait is invalid. A valid trait must include prefix ' + '"CUSTOM_" and use following characters: "A"-"Z", "0"-"9" and ' + '"_"')) + + trait = objects.Trait(context) + trait.name = name + + try: + trait.create() + req.response.status = 201 + except exception.TraitExists: + req.response.status = 204 + + req.response.content_type = None + req.response.location = util.trait_url(req.environ, trait) + return req.response + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +def get_trait(req): + context = req.environ['placement.context'] + name = util.wsgi_path_item(req.environ, 'name') + + try: + objects.Trait.get_by_name(context, name) + except exception.TraitNotFound as ex: + raise webob.exc.HTTPNotFound( + explanation=ex.format_message()) + + req.response.status = 204 + req.response.content_type = None + return req.response + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +def delete_trait(req): + context = req.environ['placement.context'] + name = util.wsgi_path_item(req.environ, 'name') + + try: + trait = objects.Trait.get_by_name(context, name) + trait.destroy() + except exception.TraitNotFound as ex: + raise webob.exc.HTTPNotFound( + explanation=ex.format_message()) + except exception.TraitCannotDeleteStandard as ex: + raise webob.exc.HTTPBadRequest( + explanation=ex.format_message()) + except exception.TraitInUse as ex: + raise webob.exc.HTTPConflict( + explanation=ex.format_message()) + + req.response.status = 204 + req.response.content_type = None + return req.response + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +@util.check_accept('application/json') +def list_traits(req): + context = req.environ['placement.context'] + filters = {} + + try: + jsonschema.validate(dict(req.GET), LIST_TRAIT_SCHEMA, + format_checker=jsonschema.FormatChecker()) + except jsonschema.ValidationError as exc: + raise webob.exc.HTTPBadRequest( + _('Invalid query string parameters: %(exc)s') % + {'exc': exc}) + + if 'name' in req.GET: + filters = _normalize_traits_qs_param(req.GET['name']) + if 'associated' in req.GET: + if req.GET['associated'].lower() not in ['true', 'false']: + raise webob.exc.HTTPBadRequest( + explanation=_('The query parameter "associated" only accepts ' + '"true" or "false"')) + filters['associated'] = ( + True if req.GET['associated'].lower() == 'true' else False) + + traits = objects.TraitList.get_all(context, filters) + req.response.status = 200 + req.response.body = encodeutils.to_utf8( + jsonutils.dumps(_serialize_traits(traits))) + req.response.content_type = 'application/json' + return req.response + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +@util.check_accept('application/json') +def list_traits_for_resource_provider(req): + context = req.environ['placement.context'] + uuid = util.wsgi_path_item(req.environ, 'uuid') + + resource_provider = objects.ResourceProvider.get_by_uuid( + context, uuid) + + response_body = _serialize_traits(resource_provider.get_traits()) + response_body[ + "resource_provider_generation"] = resource_provider.generation + + req.response.status = 200 + req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body)) + req.response.content_type = 'application/json' + return req.response + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +@util.require_content('application/json') +def update_traits_for_resource_provider(req): + context = req.environ['placement.context'] + uuid = util.wsgi_path_item(req.environ, 'uuid') + data = util.extract_json(req.body, SET_TRAITS_FOR_RP_SCHEMA) + rp_gen = data['resource_provider_generation'] + traits = data['traits'] + resource_provider = objects.ResourceProvider.get_by_uuid( + context, uuid) + + if resource_provider.generation != rp_gen: + raise webob.exc.HTTPConflict( + _("Resource provider's generation already changed. Please update " + "the generation and try again."), + json_formatter=util.json_error_formatter) + + trait_objs = objects.TraitList.get_all( + context, filters={'name_in': traits}) + traits_name = set([obj.name for obj in trait_objs]) + non_existed_trait = set(traits) - set(traits_name) + if non_existed_trait: + raise webob.exc.HTTPBadRequest( + _("No such trait %s") % ', '.join(non_existed_trait)) + + resource_provider.set_traits(trait_objs) + + response_body = _serialize_traits(trait_objs) + response_body[ + 'resource_provider_generation'] = resource_provider.generation + req.response.status = 200 + req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body)) + req.response.content_type = 'application/json' + return req.response + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.6') +def delete_traits_for_resource_provider(req): + context = req.environ['placement.context'] + uuid = util.wsgi_path_item(req.environ, 'uuid') + + resource_provider = objects.ResourceProvider.get_by_uuid(context, uuid) + try: + resource_provider.set_traits(objects.TraitList(objects=[])) + except exception.ConcurrentUpdateDetected as e: + raise webob.exc.HTTPConflict(explanation=e.format_message()) + + req.response.status = 204 + req.response.content_type = None + return req.response diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py index 1c49e25c6e75..0771c7143683 100644 --- a/nova/api/openstack/placement/microversion.py +++ b/nova/api/openstack/placement/microversion.py @@ -41,6 +41,8 @@ VERSIONS = [ # that are members of any of the listed aggregates '1.4', # Adds resources query string parameter in GET /resource_providers '1.5', # Adds DELETE /resource_providers/{uuid}/inventories + '1.6', # Adds /traits and /resource_providers{uuid}/traits resource + # endpoints ] diff --git a/nova/api/openstack/placement/rest_api_version_history.rst b/nova/api/openstack/placement/rest_api_version_history.rst index a0d747bb9051..293ea43bc734 100644 --- a/nova/api/openstack/placement/rest_api_version_history.rst +++ b/nova/api/openstack/placement/rest_api_version_history.rst @@ -89,3 +89,27 @@ Placement API version 1.5 adds DELETE method for deleting all inventory for a resource provider. The following new method is supported: * DELETE /resource_providers/{uuid}/inventories + +1.6 Traits API +-------------- + +The 1.6 version adds basic operations allowing an admin to create, list, and +delete custom traits, also adds basic operations allowing an admin to attach +traits to a resource provider. + +The following new routes are added: + +* GET /traits: Returns all resource classes. +* PUT /traits/{name}: To insert a single custom trait. +* GET /traits/{name}: To check if a trait name exists. +* DELETE /traits/{name}: To delete the specified trait. +* GET /resource_providers/{uuid}/traits: a list of traits associated + with a specific resource provider +* PUT /resource_providers/{uuid}/traits: Set all the traits for a + specific resource provider +* DELETE /resource_providers/{uuid}/traits: Remove any existing trait + associations for a specific resource provider + +Custom traits must begin with the prefix "CUSTOM\_" and contain only +the letters A through Z, the numbers 0 through 9 and the underscore "\_" +character. diff --git a/nova/api/openstack/placement/util.py b/nova/api/openstack/placement/util.py index 71ba4aa2eee1..d716723094f4 100644 --- a/nova/api/openstack/placement/util.py +++ b/nova/api/openstack/placement/util.py @@ -160,6 +160,16 @@ def resource_provider_url(environ, resource_provider): return '%s/resource_providers/%s' % (prefix, resource_provider.uuid) +def trait_url(environ, trait): + """Produce the URL for a trait. + + If SCRIPT_NAME is present, it is the mount point of the placement + WSGI app. + """ + prefix = environ.get('SCRIPT_NAME', '') + return '%s/traits/%s' % (prefix, trait.name) + + def wsgi_path_item(environ, name): """Extract the value of a named field in a URL. diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml index 390b6571353c..2b293f5cdb21 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.5 +- name: latest microversion is 1.6 GET: / request_headers: openstack-api-version: placement latest response_headers: vary: /OpenStack-API-Version/ - openstack-api-version: placement 1.5 + openstack-api-version: placement 1.6 - 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 239efad496e6..b24dae5a44a8 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml @@ -83,6 +83,7 @@ tests: $.links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.links[?rel = "aggregates"].href: /resource_providers/$ENVIRON['RP_UUID']/aggregates $.links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages + $.links[?rel = "traits"].href: /resource_providers/$ENVIRON['RP_UUID']/traits - name: get resource provider works with no accept GET: /resource_providers/$ENVIRON['RP_UUID'] @@ -110,6 +111,7 @@ tests: $.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.resource_providers[0].links[?rel = "aggregates"].href: /resource_providers/$ENVIRON['RP_UUID']/aggregates $.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages + $.resource_providers[0].links[?rel = "traits"].href: /resource_providers/$ENVIRON['RP_UUID']/traits - name: filter out all resource providers by name GET: /resource_providers?name=flubblebubble @@ -131,6 +133,7 @@ tests: $.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.resource_providers[0].links[?rel = "aggregates"].href: /resource_providers/$ENVIRON['RP_UUID']/aggregates $.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages + $.resource_providers[0].links[?rel = "traits"].href: /resource_providers/$ENVIRON['RP_UUID']/traits - name: list resource providers filtering by invalid uuid GET: /resource_providers?uuid=spameggs @@ -158,6 +161,7 @@ tests: $.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.resource_providers[0].links[?rel = "aggregates"].href: /resource_providers/$ENVIRON['RP_UUID']/aggregates $.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages + $.resource_providers[0].links[?rel = "traits"].href: /resource_providers/$ENVIRON['RP_UUID']/traits - name: update a resource provider PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid'] diff --git a/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml b/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml new file mode 100644 index 000000000000..44a61f8104b9 --- /dev/null +++ b/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml @@ -0,0 +1,259 @@ + +fixtures: + - APIFixture + +defaults: + request_headers: + x-auth-token: admin + OpenStack-API-Version: placement latest + +tests: + +- name: create a trait without custom namespace + PUT: /traits/TRAIT_X + status: 400 + response_strings: + - 'The trait is invalid. A valid trait must include prefix "CUSTOM_" and use following characters: "A"-"Z", "0"-"9" and "_"' + +- name: create a trait with invalid characters + PUT: /traits/CUSTOM_ABC:1 + status: 400 + response_strings: + - 'The trait is invalid. A valid trait must include prefix "CUSTOM_" and use following characters: "A"-"Z", "0"-"9" and "_"' + +- name: create a trait + PUT: /traits/CUSTOM_TRAIT_1 + status: 201 + response_headers: + location: //traits/CUSTOM_TRAIT_1/ + response_forbidden_headers: + - content-type + +- name: create a trait which existed + PUT: /traits/CUSTOM_TRAIT_1 + status: 204 + response_headers: + location: //traits/CUSTOM_TRAIT_1/ + response_forbidden_headers: + - content-type + +- name: get a trait + GET: /traits/CUSTOM_TRAIT_1 + status: 204 + response_forbidden_headers: + - content-type + +- name: get a non-existed trait + GET: /traits/NON_EXISTED + status: 404 + +- name: delete a trait + DELETE: /traits/CUSTOM_TRAIT_1 + status: 204 + +- name: delete a non-existed trait + DELETE: /traits/CUSTOM_NON_EXSITED + status: 404 + +- name: create CUSTOM_TRAIT_1 + PUT: /traits/CUSTOM_TRAIT_1 + status: 201 + response_headers: + location: //traits/CUSTOM_TRAIT_1/ + response_forbidden_headers: + - content-type + +- name: create CUSTOM_TRAIT_2 + PUT: /traits/CUSTOM_TRAIT_2 + status: 201 + response_headers: + location: //traits/CUSTOM_TRAIT_2/ + response_forbidden_headers: + - content-type + +- name: list traits + GET: /traits + status: 200 + response_json_paths: + $.traits.`len`: 2 + response_strings: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + +- name: list traits with invalid format of name parameter + GET: /traits?name=in_abc + status: 400 + response_strings: + - 'Badly formatted name parameter. Expected name query string parameter in form: ?name=[in|startswith]:[name1,name2|prefix]. Got: "in_abc"' + +- name: list traits with name=in filter + GET: /traits?name=in:CUSTOM_TRAIT_1,CUSTOM_TRAIT_2 + status: 200 + response_json_paths: + $.traits.`len`: 2 + response_strings: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + +- name: create CUSTOM_ANOTHER_TRAIT + PUT: /traits/CUSTOM_ANOTHER_TRAIT + status: 201 + response_headers: + location: //traits/CUSTOM_ANOTHER_TRAIT/ + response_forbidden_headers: + - content-type + +- name: list traits with prefix + GET: /traits?name=startswith:CUSTOM_TRAIT + status: 200 + response_json_paths: + $.traits.`len`: 2 + response_strings: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + +- name: list traits with invalid parameters + GET: /traits?invalid=abc + status: 400 + response_strings: + - "Invalid query string parameters: Additional properties are not allowed" + +- name: post new resource provider + POST: /resource_providers + request_headers: + content-type: application/json + data: + name: $ENVIRON['RP_NAME'] + uuid: $ENVIRON['RP_UUID'] + status: 201 + response_headers: + location: //resource_providers/[a-f0-9-]+/ + response_forbidden_headers: + - content-type + +- name: list traits for resource provider without traits + GET: /resource_providers/$ENVIRON['RP_UUID']/traits + status: 200 + response_json_paths: + $.resource_provider_generation: 0 + $.traits.`len`: 0 + +- name: set traits for resource provider + PUT: /resource_providers/$ENVIRON['RP_UUID']/traits + request_headers: + content-type: application/json + status: 200 + data: + traits: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + resource_provider_generation: 0 + response_json_paths: + $.resource_provider_generation: 1 + $.traits.`len`: 2 + response_strings: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + +- name: get associated traits + GET: /traits?associated=true + status: 200 + response_json_paths: + $.traits.`len`: 2 + response_strings: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + +- name: get associated traits with invalid value + GET: /traits?associated=xyz + status: 400 + response_strings: + - 'The query parameter "associated" only accepts "true" or "false"' + +- name: set traits for resource provider without resource provider generation + PUT: /resource_providers/$ENVIRON['RP_UUID']/traits + request_headers: + content-type: application/json + status: 400 + data: + traits: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + response_strings: + - CUSTOM_TRAIT_1 + +- name: set traits for resource provider with conflict generation + PUT: /resource_providers/$ENVIRON['RP_UUID']/traits + request_headers: + content-type: application/json + status: 409 + data: + traits: + - CUSTOM_TRAIT_1 + resource_provider_generation: 5 + response_strings: + - Resource provider's generation already changed. Please update the generation and try again. + +- name: set non existed traits for resource provider + PUT: /resource_providers/$ENVIRON['RP_UUID']/traits + request_headers: + content-type: application/json + status: 400 + data: + traits: + - NON_EXISTED_TRAIT1 + - NON_EXISTED_TRAIT2 + - CUSTOM_TRAIT_1 + resource_provider_generation: 1 + response_strings: + - No such trait + - NON_EXISTED_TRAIT1 + - NON_EXISTED_TRAIT2 + +- name: set traits for non_existed resource provider + PUT: /resource_providers/non_existed/traits + request_headers: + content-type: application/json + data: + traits: + - CUSTOM_TRAIT_1 + resource_provider_generation: 1 + status: 404 + response_strings: + - No resource provider with uuid non_existed found + +- name: list traits for resource provider + GET: /resource_providers/$ENVIRON['RP_UUID']/traits + status: 200 + response_json_paths: + $.resource_provider_generation: 1 + $.traits.`len`: 2 + response_strings: + - CUSTOM_TRAIT_1 + - CUSTOM_TRAIT_2 + +- name: delete an in-use trait + DELETE: /traits/CUSTOM_TRAIT_1 + status: 409 + response_strings: + - The trait CUSTOM_TRAIT_1 is in use by a resource provider. + +- name: list traits for non_existed resource provider + GET: /resource_providers/non_existed/traits + request_headers: + content-type: application/json + status: 404 + response_strings: + - No resource provider with uuid non_existed found + +- name: delete traits for resource provider + DELETE: /resource_providers/$ENVIRON['RP_UUID']/traits + status: 204 + response_forbidden_headers: + - content-type + +- name: delete traits for non_existed resource provider + DELETE: /resource_providers/non_existed/traits + status: 404 + response_strings: + - No resource provider with uuid non_existed found diff --git a/nova/tests/unit/api/openstack/placement/test_microversion.py b/nova/tests/unit/api/openstack/placement/test_microversion.py index 51d43b561335..992f10837b6f 100644 --- a/nova/tests/unit/api/openstack/placement/test_microversion.py +++ b/nova/tests/unit/api/openstack/placement/test_microversion.py @@ -74,7 +74,7 @@ class TestMicroversionIntersection(test.NoDBTestCase): # if you add two different versions of method 'foobar' the # number only goes up by one if no other version foobar yet # exists. This operates as a simple sanity check. - TOTAL_VERSIONED_METHODS = 5 + TOTAL_VERSIONED_METHODS = 12 def test_methods_versioned(self): methods_data = microversion.VERSIONED_METHODS diff --git a/releasenotes/notes/placement-traits-api-efa17d46ea1b616b.yaml b/releasenotes/notes/placement-traits-api-efa17d46ea1b616b.yaml new file mode 100644 index 000000000000..484be3f51443 --- /dev/null +++ b/releasenotes/notes/placement-traits-api-efa17d46ea1b616b.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Traits are added to the placement with Microversion 1.6. + + * GET /traits: Returns all resource classes. + * PUT /traits/{name}: To insert a single custom trait. + * GET /traits/{name}: To check if a trait name exists. + * DELETE /traits/{name}: To delete the specified trait. + * GET /resource_providers/{uuid}/traits: a list of traits associated + with a specific resource provider + * PUT /resource_providers/{uuid}/traits: Set all the traits for a + specific resource provider + * DELETE /resource_providers/{uuid}/traits: Remove any existing trait + associations for a specific resource provider