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_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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
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
|
# 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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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: /
|
||||||
|
@ -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']
|
||||||
|
@ -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
|
# 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
|
||||||
|
@ -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