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:
parent
6dd047a330
commit
9c975b6bd8
@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
265
nova/api/openstack/placement/handlers/trait.py
Normal file
265
nova/api/openstack/placement/handlers/trait.py
Normal 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
|
@ -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
|
||||
]
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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: /
|
||||
|
@ -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']
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user