placement: Add Traits API to placement service

This patch adds support for a REST API for CRUD operations on traits.

  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

Partial implement blueprint resource-provider-traits

Change-Id: Ia027895cbb4f1c71fd9470d8f9281d2bebb6d8a2
This commit is contained in:
He Jie Xu 2016-09-25 15:09:39 +08:00
parent d3bf010d3d
commit 465d17ba10
11 changed files with 597 additions and 4 deletions

View File

@ -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_class
from nova.api.openstack.placement.handlers import resource_provider from nova.api.openstack.placement.handlers import resource_provider
from nova.api.openstack.placement.handlers import root 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.handlers import usage
from nova.api.openstack.placement import policy from nova.api.openstack.placement import policy
from nova.api.openstack.placement import util from nova.api.openstack.placement import util
@ -103,6 +104,19 @@ ROUTE_DECLARATIONS = {
'PUT': allocation.set_allocations, 'PUT': allocation.set_allocations,
'DELETE': allocation.delete_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
},
} }

View File

@ -144,7 +144,7 @@ def _normalize_resources_qs_param(qs):
def _serialize_links(environ, resource_provider): def _serialize_links(environ, resource_provider):
url = util.resource_provider_url(environ, resource_provider) url = util.resource_provider_url(environ, resource_provider)
links = [{'rel': 'self', 'href': url}] 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)}) links.append({'rel': rel, 'href': '%s/%s' % (url, rel)})
return links return links

View File

@ -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

View File

@ -41,6 +41,8 @@ VERSIONS = [
# that are members of any of the listed aggregates # that are members of any of the listed aggregates
'1.4', # Adds resources query string parameter in GET /resource_providers '1.4', # Adds resources query string parameter in GET /resource_providers
'1.5', # Adds DELETE /resource_providers/{uuid}/inventories '1.5', # Adds DELETE /resource_providers/{uuid}/inventories
'1.6', # Adds /traits and /resource_providers{uuid}/traits resource
# endpoints
] ]

View File

@ -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: resource provider. The following new method is supported:
* DELETE /resource_providers/{uuid}/inventories * 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.

View File

@ -160,6 +160,16 @@ def resource_provider_url(environ, resource_provider):
return '%s/resource_providers/%s' % (prefix, resource_provider.uuid) 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): def wsgi_path_item(environ, name):
"""Extract the value of a named field in a URL. """Extract the value of a named field in a URL.

View File

@ -39,13 +39,13 @@ tests:
response_json_paths: response_json_paths:
$.errors[0].title: Not Acceptable $.errors[0].title: Not Acceptable
- name: latest microversion is 1.5 - name: latest microversion is 1.6
GET: / GET: /
request_headers: request_headers:
openstack-api-version: placement latest openstack-api-version: placement latest
response_headers: response_headers:
vary: /OpenStack-API-Version/ vary: /OpenStack-API-Version/
openstack-api-version: placement 1.5 openstack-api-version: placement 1.6
- name: other accept header bad version - name: other accept header bad version
GET: / GET: /

View File

@ -83,6 +83,7 @@ tests:
$.links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories
$.links[?rel = "aggregates"].href: /resource_providers/$ENVIRON['RP_UUID']/aggregates $.links[?rel = "aggregates"].href: /resource_providers/$ENVIRON['RP_UUID']/aggregates
$.links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages $.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 - name: get resource provider works with no accept
GET: /resource_providers/$ENVIRON['RP_UUID'] 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 = "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 = "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 = "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 - name: filter out all resource providers by name
GET: /resource_providers?name=flubblebubble 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 = "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 = "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 = "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 - name: list resource providers filtering by invalid uuid
GET: /resource_providers?uuid=spameggs 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 = "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 = "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 = "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 - name: update a resource provider
PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid'] PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid']

View File

@ -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

View File

@ -74,7 +74,7 @@ class TestMicroversionIntersection(test.NoDBTestCase):
# if you add two different versions of method 'foobar' the # if you add two different versions of method 'foobar' the
# number only goes up by one if no other version foobar yet # number only goes up by one if no other version foobar yet
# exists. This operates as a simple sanity check. # exists. This operates as a simple sanity check.
TOTAL_VERSIONED_METHODS = 5 TOTAL_VERSIONED_METHODS = 12
def test_methods_versioned(self): def test_methods_versioned(self):
methods_data = microversion.VERSIONED_METHODS methods_data = microversion.VERSIONED_METHODS

View File

@ -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