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 6dd047a330
commit 9c975b6bd8
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_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
},
}

View File

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

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
'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
]

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:
* 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)
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.

View File

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

View File

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

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

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