nova/nova/api/openstack/placement/util.py

185 lines
6.5 KiB
Python

# 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.
"""Utility methods for placement API."""
import functools
import jsonschema
from oslo_middleware import request_id
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
import webob
# NOTE(cdent): avoid cyclical import conflict between util and
# microversion
import nova.api.openstack.placement.microversion
from nova.i18n import _
# NOTE(cdent): This registers a FormatChecker on the jsonschema
# module. Do not delete this code! Although it appears that nothing
# is using the decorated method it is being used in JSON schema
# validations to check uuid formatted strings.
@jsonschema.FormatChecker.cls_checks('uuid')
def _validate_uuid_format(instance):
return uuidutils.is_uuid_like(instance)
def check_accept(*types):
"""If accept is set explicitly, try to follow it.
If there is no match for the incoming accept header
send a 406 response code.
If accept is not set send our usual content-type in
response.
"""
def decorator(f):
@functools.wraps(f)
def decorated_function(req):
if req.accept:
best_match = req.accept.best_match(types)
if not best_match:
type_string = ', '.join(types)
raise webob.exc.HTTPNotAcceptable(
_('Only %(type)s is provided') % {'type': type_string},
json_formatter=json_error_formatter)
return f(req)
return decorated_function
return decorator
def extract_json(body, schema):
"""Extract JSON from a body and validate with the provided schema."""
try:
data = jsonutils.loads(body)
except ValueError as exc:
raise webob.exc.HTTPBadRequest(
_('Malformed JSON: %(error)s') % {'error': exc},
json_formatter=json_error_formatter)
try:
jsonschema.validate(data, schema,
format_checker=jsonschema.FormatChecker())
except jsonschema.ValidationError as exc:
raise webob.exc.HTTPBadRequest(
_('JSON does not validate: %(error)s') % {'error': exc},
json_formatter=json_error_formatter)
return data
def inventory_url(environ, resource_provider, resource_class=None):
url = '%s/inventories' % resource_provider_url(environ, resource_provider)
if resource_class:
url = '%s/%s' % (url, resource_class)
return url
def json_error_formatter(body, status, title, environ):
"""A json_formatter for webob exceptions.
Follows API-WG guidelines at
http://specs.openstack.org/openstack/api-wg/guidelines/errors.html
"""
# Clear out the html that webob sneaks in.
body = webob.exc.strip_tags(body)
# Get status code out of status message. webob's error formatter
# only passes entire status string.
status_code = int(status.split(None, 1)[0])
error_dict = {
'status': status_code,
'title': title,
'detail': body
}
# If the request id middleware has had a chance to add an id,
# put it in the error response.
if request_id.ENV_REQUEST_ID in environ:
error_dict['request_id'] = environ[request_id.ENV_REQUEST_ID]
# When there is a no microversion in the environment and a 406,
# microversion parsing failed so we need to include microversion
# min and max information in the error response.
microversion = nova.api.openstack.placement.microversion
if status_code == 406 and microversion.MICROVERSION_ENVIRON not in environ:
error_dict['max_version'] = microversion.max_version_string()
error_dict['min_version'] = microversion.min_version_string()
return {'errors': [error_dict]}
def require_content(content_type):
"""Decorator to require a content type in a handler."""
def decorator(f):
@functools.wraps(f)
def decorated_function(req):
if req.content_type != content_type:
# webob's unset content_type is the empty string so
# set it the error message content to 'None' to make
# a useful message in that case. This also avoids a
# KeyError raised when webob.exc eagerly fills in a
# Template for output we will never use.
if not req.content_type:
req.content_type = 'None'
raise webob.exc.HTTPUnsupportedMediaType(
_('The media type %(bad_type)s is not supported, '
'use %(good_type)s') %
{'bad_type': req.content_type,
'good_type': content_type},
json_formatter=json_error_formatter)
else:
return f(req)
return decorated_function
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.
If SCRIPT_NAME is present, it is the mount point of the placement
WSGI app.
"""
prefix = environ.get('SCRIPT_NAME', '')
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.
Return None if the name is not present or there are no path items.
"""
# NOTE(cdent): For the time being we don't need to urldecode
# the value as the entire placement API has paths that accept no
# encoded values.
try:
return environ['wsgiorg.routing_args'][1][name]
except (KeyError, IndexError):
return None