placement: REST API for resource classes

This patch adds support for a REST API for CRUD operations on custom
resource classes:

    GET /resource_classes: return all resource classes
    POST /resource_classes: create a new custom resource class
    PUT /resource_classes/{name}: update name of custom resource class
    DELETE /resource_classes/{name}: deletes a custom resource class
    GET /resource_classes/{name}: get a single resource class

Change-Id: I99e7bcfe27938e5e4d50ac3005690ac1255d4c5e
blueprint: custom-resource-classes
This commit is contained in:
Jay Pipes 2016-10-31 15:33:40 -04:00 committed by Chris Dent
parent 4c5955930f
commit 1067c44663
11 changed files with 464 additions and 3 deletions

View File

@ -31,6 +31,7 @@ from oslo_log import log as logging
from nova.api.openstack.placement.handlers import aggregate
from nova.api.openstack.placement.handlers import allocation
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 usage
@ -58,6 +59,15 @@ ROUTE_DECLARATIONS = {
'': {
'GET': root.home,
},
'/resource_classes': {
'GET': resource_class.list_resource_classes,
'POST': resource_class.create_resource_class
},
'/resource_classes/{name}': {
'GET': resource_class.get_resource_class,
'PUT': resource_class.update_resource_class,
'DELETE': resource_class.delete_resource_class,
},
'/resource_providers': {
'GET': resource_provider.list_resource_providers,
'POST': resource_provider.create_resource_provider

View File

@ -0,0 +1,197 @@
# 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.
"""Placement API handlers for resource classes."""
import copy
from oslo_serialization import jsonutils
import webob
from nova.api.openstack.placement import microversion
from nova.api.openstack.placement import util
from nova import exception
from nova.i18n import _
from nova import objects
POST_RC_SCHEMA_V1_2 = {
"type": "object",
"properties": {
"name": {
"type": "string",
"pattern": "^CUSTOM\_[A-Z0-9_]+$",
},
},
"required": [
"name"
],
"additionalProperties": False,
}
PUT_RC_SCHEMA_V1_2 = copy.deepcopy(POST_RC_SCHEMA_V1_2)
def _serialize_links(environ, rc):
url = util.resource_class_url(environ, rc)
links = [{'rel': 'self', 'href': url}]
return links
def _serialize_resource_class(environ, rc):
data = {
'name': rc.name,
'links': _serialize_links(environ, rc)
}
return data
def _serialize_resource_classes(environ, rcs):
output = []
for rc in rcs:
data = _serialize_resource_class(environ, rc)
output.append(data)
return {"resource_classes": output}
@webob.dec.wsgify
@microversion.version_handler(1.2)
@util.require_content('application/json')
def create_resource_class(req):
"""POST to create a resource class.
On success return a 201 response with an empty body and a location
header pointing to the newly created resource class.
"""
context = req.environ['placement.context']
data = util.extract_json(req.body, POST_RC_SCHEMA_V1_2)
try:
rc = objects.ResourceClass(context, name=data['name'])
rc.create()
except exception.ResourceClassExists:
raise webob.exc.HTTPConflict(
_('Conflicting resource class already exists: %(name)s') %
{'name': data['name']},
json_formatter=util.json_error_formatter)
req.response.location = util.resource_class_url(req.environ, rc)
req.response.status = 201
req.response.content_type = None
return req.response
@webob.dec.wsgify
@microversion.version_handler(1.2)
def delete_resource_class(req):
"""DELETE to destroy a single resource class.
On success return a 204 and an empty body.
"""
name = util.wsgi_path_item(req.environ, 'name')
context = req.environ['placement.context']
# The containing application will catch a not found here.
rc = objects.ResourceClass.get_by_name(context, name)
try:
rc.destroy()
except exception.ResourceClassCannotDeleteStandard as exc:
raise webob.exc.HTTPBadRequest(
_('Cannot delete standard resource class %(rp_name)s: %(error)s') %
{'rp_name': name, 'error': exc},
json_formatter=util.json_error_formatter)
except exception.ResourceClassInUse as exc:
raise webob.exc.HTTPConflict(
_('Unable to delete resource class %(rp_name)s: %(error)s') %
{'rp_name': name, 'error': exc},
json_formatter=util.json_error_formatter)
req.response.status = 204
req.response.content_type = None
return req.response
@webob.dec.wsgify
@microversion.version_handler(1.2)
@util.check_accept('application/json')
def get_resource_class(req):
"""Get a single resource class.
On success return a 200 with an application/json body representing
the resource class.
"""
name = util.wsgi_path_item(req.environ, 'name')
context = req.environ['placement.context']
# The containing application will catch a not found here.
rc = objects.ResourceClass.get_by_name(context, name)
req.response.body = jsonutils.dumps(
_serialize_resource_class(req.environ, rc)
)
req.response.content_type = 'application/json'
return req.response
@webob.dec.wsgify
@microversion.version_handler(1.2)
@util.check_accept('application/json')
def list_resource_classes(req):
"""GET a list of resource classes.
On success return a 200 and an application/json body representing
a collection of resource classes.
"""
context = req.environ['placement.context']
rcs = objects.ResourceClassList.get_all(context)
response = req.response
response.body = jsonutils.dumps(
_serialize_resource_classes(req.environ, rcs)
)
response.content_type = 'application/json'
return response
@webob.dec.wsgify
@microversion.version_handler(1.2)
@util.require_content('application/json')
def update_resource_class(req):
"""PUT to update a single resource class.
On success return a 200 response with a representation of the updated
resource class.
"""
name = util.wsgi_path_item(req.environ, 'name')
context = req.environ['placement.context']
data = util.extract_json(req.body, PUT_RC_SCHEMA_V1_2)
# The containing application will catch a not found here.
rc = objects.ResourceClass.get_by_name(context, name)
rc.name = data['name']
try:
rc.save()
except exception.ResourceClassExists:
raise webob.exc.HTTPConflict(
_('Resource class already exists: %(name)s') %
{'name': name},
json_formatter=util.json_error_formatter)
except exception.ResourceClassCannotUpdateStandard:
raise webob.exc.HTTPBadRequest(
_('Cannot update standard resource class %(rp_name)s') %
{'rp_name': name},
json_formatter=util.json_error_formatter)
req.response.body = jsonutils.dumps(
_serialize_resource_class(req.environ, rc)
)
req.response.status = 200
req.response.content_type = 'application/json'
return req.response

View File

@ -36,6 +36,7 @@ VERSIONED_METHODS = collections.defaultdict(list)
VERSIONS = [
'1.0',
'1.1', # initial support for aggregate.get_aggregates and set_aggregates
'1.2', # Adds /resource-classes resource endpoint
]

View File

@ -26,3 +26,21 @@ resource providers with ``GET`` and ``PUT`` methods on one new
route:
* /resource_providers/{uuid}/aggregates
1.2 Custom resource classes
---------------------------
Placement API version 1.2 adds basic operations allowing an admin to create,
list and delete custom resource classes.
The following new routes are added:
* GET /resource_classes: return all resource classes
* POST /resource_classes: create a new custom resource class
* PUT /resource_classes/{name}: update name of custom resource class
* DELETE /resource_classes/{name}: deletes a custom resource class
* GET /resource_classes/{name}: get a single resource class
Custom resource classes 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

@ -135,6 +135,16 @@ def require_content(content_type):
return decorator
def resource_class_url(environ, resource_class):
"""Produce the URL for a resource class.
If SCRIPT_NAME is present, it is the mount point of the placement
WSGI app.
"""
prefix = environ.get('SCRIPT_NAME', '')
return '%s/resource_classes/%s' % (prefix, resource_class.name)
def resource_provider_url(environ, resource_provider):
"""Produce the URL for a resource provider.

View File

@ -68,6 +68,7 @@ class APIFixture(fixture.GabbiFixture):
os.environ['RP_UUID'] = uuidutils.generate_uuid()
os.environ['RP_NAME'] = uuidutils.generate_uuid()
os.environ['CUSTOM_RES_CLASS'] = 'CUSTOM_IRON_NFV'
def stop_fixture(self):
self.api_db_fixture.cleanup()

View File

@ -37,13 +37,13 @@ tests:
response_strings:
- "Unacceptable version header: 0.5"
- name: latest microversion is 1.1
- name: latest microversion is 1.2
GET: /
request_headers:
openstack-api-version: placement latest
response_headers:
vary: /OpenStack-API-Version/
openstack-api-version: placement 1.1
openstack-api-version: placement 1.2
- name: other accept header bad version
GET: /

View File

@ -0,0 +1,197 @@
fixtures:
- APIFixture
defaults:
request_headers:
x-auth-token: admin
OpenStack-API-Version: placement latest
tests:
- name: test microversion masks entire resource-classes endpoint with 404
GET: /resource_classes
request_headers:
OpenStack-API-Version: placement 1.1
content-type: application/json
status: 404
- name: test microversion mask when wrong content type
desc: we want to get a 404 before a 415
POST: /resource_classes
request_headers:
OpenStack-API-Version: placement 1.1
content-type: text/plain
data: data
status: 404
- name: test wrong content type
desc: we want to get a 415 when bad content type
POST: /resource_classes
request_headers:
OpenStack-API-Version: placement 1.2
content-type: text/plain
data: data
status: 415
- name: what is at resource classes
GET: /resource_classes
response_json_paths:
response_json_paths:
$.resource_classes.`len`: 10 # Number of standard resource classes
$.resource_classes[0].name: VCPU
- name: non admin forbidden
GET: /resource_classes
request_headers:
x-auth-token: user
accept: application/json
status: 403
response_json_paths:
$.errors[0].title: Forbidden
- name: non admin forbidden non json
GET: /resource_classes
request_headers:
x-auth-token: user
accept: text/plain
status: 403
response_strings:
- admin required
- name: post illegal characters in name
POST: /resource_classes
request_headers:
content-type: application/json
data:
name: CUSTOM_Illegal&@!Name?
status: 400
response_strings:
- JSON does not validate
- name: post new resource class
POST: /resource_classes
request_headers:
content-type: application/json
data:
name: $ENVIRON['CUSTOM_RES_CLASS']
status: 201
response_headers:
location: //resource_classes/$ENVIRON['CUSTOM_RES_CLASS']/
response_forbidden_headers:
- content-type
- name: try to create same again
POST: /resource_classes
request_headers:
content-type: application/json
data:
name: $ENVIRON['CUSTOM_RES_CLASS']
status: 409
response_strings:
- Conflicting resource class already exists
- name: confirm the correct post
GET: /resource_classes/$ENVIRON['CUSTOM_RES_CLASS']
request_headers:
content-type: application/json
response_json_paths:
$.name: $ENVIRON['CUSTOM_RES_CLASS']
$.links[?rel = "self"].href: /resource_classes/$ENVIRON['CUSTOM_RES_CLASS']
- name: get resource class works with no accept
GET: /resource_classes/$ENVIRON['CUSTOM_RES_CLASS']
request_headers:
content-type: application/json
response_headers:
content-type: /application/json/
response_json_paths:
$.name: $ENVIRON['CUSTOM_RES_CLASS']
- name: list resource classes after addition of custom res class
GET: /resource_classes
response_json_paths:
$.resource_classes.`len`: 11 # 10 standard plus 1 custom
$.resource_classes[10].name: $ENVIRON['CUSTOM_RES_CLASS']
$.resource_classes[10].links[?rel = "self"].href: /resource_classes/$ENVIRON['CUSTOM_RES_CLASS']
- name: update standard resource class
PUT: /resource_classes/VCPU
request_headers:
content-type: application/json
data:
name: VCPU_ALTERNATE
status: 400
response_strings:
- JSON does not validate
- name: update custom resource class to standard resource class name
PUT: /resource_classes/$ENVIRON['CUSTOM_RES_CLASS']
request_headers:
content-type: application/json
data:
name: VCPU
status: 400
response_strings:
- JSON does not validate
- name: post another custom resource class
POST: /resource_classes
request_headers:
content-type: application/json
data:
name: CUSTOM_NFV_FOO
status: 201
- name: update custom resource class to already existing custom resource class name
PUT: /resource_classes/CUSTOM_NFV_FOO
request_headers:
content-type: application/json
data:
name: $ENVIRON['CUSTOM_RES_CLASS']
status: 409
response_strings:
- Resource class already exists
- name: update custom resource class
PUT: /resource_classes/$ENVIRON['CUSTOM_RES_CLASS']
request_headers:
content-type: application/json
data:
name: CUSTOM_NFV_BAR
status: 200
response_json_paths:
$.name: CUSTOM_NFV_BAR
$.links[?rel = "self"].href: /resource_classes/CUSTOM_NFV_BAR
- name: delete standard resource class
DELETE: /resource_classes/VCPU
status: 400
response_strings:
- Cannot delete standard resource class
- name: delete custom resource class
DELETE: /resource_classes/CUSTOM_NFV_BAR
status: 204
- name: 404 on deleted resource class
DELETE: $LAST_URL
status: 404
- name: post malformed json as json
POST: /resource_classes
request_headers:
content-type: application/json
data: '{"foo": }'
status: 400
response_strings:
- 'Malformed JSON:'
- name: post bad resource class name IRON_NFV
POST: /resource_classes
request_headers:
content-type: application/json
data:
name: IRON_NFV # Doesn't start with CUSTOM_
status: 400
response_strings:
- JSON does not validate

View File

@ -57,7 +57,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 = 0
TOTAL_VERSIONED_METHODS = 5
def test_methods_versioned(self):
methods_data = microversion.VERSIONED_METHODS

View File

@ -264,6 +264,9 @@ class TestPlacementURLs(test.NoDBTestCase):
self.resource_provider = objects.ResourceProvider(
name=uuidsentinel.rp_name,
uuid=uuidsentinel.rp_uuid)
self.resource_class = objects.ResourceClass(
name='CUSTOM_BAREMETAL_GOLD',
id=1000)
def test_resource_provider_url(self):
environ = {}
@ -294,3 +297,17 @@ class TestPlacementURLs(test.NoDBTestCase):
% (uuidsentinel.rp_uuid, resource_class))
self.assertEqual(expected_url, util.inventory_url(
environ, self.resource_provider, resource_class))
def test_resource_class_url(self):
environ = {}
expected_url = '/resource_classes/CUSTOM_BAREMETAL_GOLD'
self.assertEqual(expected_url, util.resource_class_url(
environ, self.resource_class))
def test_resource_class_url_prefix(self):
# SCRIPT_NAME represents the mount point of a WSGI
# application when it is hosted at a path/prefix.
environ = {'SCRIPT_NAME': '/placement'}
expected_url = '/placement/resource_classes/CUSTOM_BAREMETAL_GOLD'
self.assertEqual(expected_url, util.resource_class_url(
environ, self.resource_class))

View File

@ -0,0 +1,10 @@
---
features:
- |
A new administrator-only resource endpoint was added to the OpenStack
Placement REST API for managing custom resource classes. Custom resource
classes are specific to a deployment and represent types of quantitative
resources that are not interoperable between OpenStack clouds. See the
`Placement REST API Version History`_ documentation for usage details.
.. _Placement REST API Version History: http://docs.openstack.org/developer/nova/placement.html#id3