
This change was driven out of trying to get nova functional tests working with an extracted placement, starting with getting the database fixture cleaner. Perhaps not surprisingly, trying to share the same 'cfg.CONF' between two services is rather fraught. Rather than trying to tease out all the individual issues, which is a very time consuming effort for not much gain, a different time consuming effort with great gain was tried instead. This patch removes the use of the default global cfg.CONF that oslo_config (optionally) provides and instead ensures that at the various ways in which one might enter placement: wsgi, cli, tests, the config is generated and managed in a more explicit fashion. Unfortunately this is a large change, but there's no easy way to do it in incremental chunks without getting very confused and having tests pass. There are a few classes of changes here, surrounded by various cleanups to address their addition. Quite a few holes were found in how config is managed, especially in tests where often we were getting what we wanted pretty much by accident. The big changes: * Importing placement.conf does not automatically register options with the global conf. Instead there is a now a register_opts method to which a ConfigOpts() is required. * Because of policy enforcement wanting access to conf, a convenient way of having the config pass through context.can() was needed. At the start of PlacementHandler (the main dispatch routine) the current config (provided to the PlacementHandler at application configuration time) is set as an attribute on the RequestContext. This is also used where CONF is required in the objects, such as randomizing the limited allocation canidates. * Passing in config to PlacementHandler changes the way the gabbi fixture loads the WSGI application. To work around a shortcoming in gabbi the fixture needs to CONF global. This is _not_ an oslo_config.cfg.CONF global, but something used locally in the fixture to set a different config per gabbi test suite. * The --sql command for alembic commands has been disabled. We don't really need that and it would require some messing about with config. The command lets you dump raw sql intead of migration files. * PlacementFixture, for use by nova, has been expanded to create and manage its config, database and policy requirements using non-global config. It can also accept a previously prepared config. * The Database fixtures calls 'reset()' in both setUp and cleanUp to be certain we are both starting and ending in a known state that will not disturb or be disturbed by other tests. This adds confidence (but not a guarantee) that in tests that run with eventlet (as in nova) things are in more consistent state. * Configuring the db in the Database fixture is moved into setUp where it should have been all along, but is important to be there _after_ 'reset()'. These of course cascade other changes all over the place. Especially the need to manually register_opts. There are probably refactorings that can be done or base classes that can be removed. Command line tools (e.g. status) which are mostly based on external libraries continue to use config in the pre-existing way. A lock fixture for the opportunistic migration tests has been added. There was a lock fixture previously, provided by oslo_concurrency, but it, as far as I can tell, requires global config. We don't want that. Things that will need to be changed as a result of these changes: * The goals doc at https://review.openstack.org/#/c/618811/ will need to be changed to say "keep it this way" rather than "get it this way". Change-Id: Icd629d7cd6d68ca08f9f3b4f0465c3d9a1efeb22
235 lines
9.1 KiB
Python
235 lines
9.1 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.
|
|
"""Handlers for placement API.
|
|
|
|
Individual handlers are associated with URL paths in the
|
|
ROUTE_DECLARATIONS dictionary. At the top level each key is a Routes
|
|
compliant path. The value of that key is a dictionary mapping
|
|
individual HTTP request methods to a Python function representing a
|
|
simple WSGI application for satisfying that request.
|
|
|
|
The ``make_map`` method processes ROUTE_DECLARATIONS to create a
|
|
Routes.Mapper, including automatic handlers to respond with a
|
|
405 when a request is made against a valid URL with an invalid
|
|
method.
|
|
"""
|
|
|
|
import routes
|
|
import webob
|
|
|
|
from oslo_log import log as logging
|
|
|
|
from placement import exception
|
|
from placement.handlers import aggregate
|
|
from placement.handlers import allocation
|
|
from placement.handlers import allocation_candidate
|
|
from placement.handlers import inventory
|
|
from placement.handlers import reshaper
|
|
from placement.handlers import resource_class
|
|
from placement.handlers import resource_provider
|
|
from placement.handlers import root
|
|
from placement.handlers import trait
|
|
from placement.handlers import usage
|
|
from placement.i18n import _
|
|
from placement import util
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
# URLs and Handlers
|
|
# NOTE(cdent): When adding URLs here, do not use regex patterns in
|
|
# the path parameters (e.g. {uuid:[0-9a-zA-Z-]+}) as that will lead
|
|
# to 404s that are controlled outside of the individual resources
|
|
# and thus do not include specific information on the why of the 404.
|
|
ROUTE_DECLARATIONS = {
|
|
'/': {
|
|
'GET': root.home,
|
|
},
|
|
# NOTE(cdent): This allows '/placement/' and '/placement' to
|
|
# both work as the root of the service, which we probably want
|
|
# for those situations where the service is mounted under a
|
|
# prefix (as it is in devstack). While weird, an empty string is
|
|
# a legit key in a dictionary and matches as desired in Routes.
|
|
'': {
|
|
'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
|
|
},
|
|
'/resource_providers/{uuid}': {
|
|
'GET': resource_provider.get_resource_provider,
|
|
'DELETE': resource_provider.delete_resource_provider,
|
|
'PUT': resource_provider.update_resource_provider
|
|
},
|
|
'/resource_providers/{uuid}/inventories': {
|
|
'GET': inventory.get_inventories,
|
|
'POST': inventory.create_inventory,
|
|
'PUT': inventory.set_inventories,
|
|
'DELETE': inventory.delete_inventories
|
|
},
|
|
'/resource_providers/{uuid}/inventories/{resource_class}': {
|
|
'GET': inventory.get_inventory,
|
|
'PUT': inventory.update_inventory,
|
|
'DELETE': inventory.delete_inventory
|
|
},
|
|
'/resource_providers/{uuid}/usages': {
|
|
'GET': usage.list_usages
|
|
},
|
|
'/resource_providers/{uuid}/aggregates': {
|
|
'GET': aggregate.get_aggregates,
|
|
'PUT': aggregate.set_aggregates
|
|
},
|
|
'/resource_providers/{uuid}/allocations': {
|
|
'GET': allocation.list_for_resource_provider,
|
|
},
|
|
'/allocations': {
|
|
'POST': allocation.set_allocations,
|
|
},
|
|
'/allocations/{consumer_uuid}': {
|
|
'GET': allocation.list_for_consumer,
|
|
'PUT': allocation.set_allocations_for_consumer,
|
|
'DELETE': allocation.delete_allocations,
|
|
},
|
|
'/allocation_candidates': {
|
|
'GET': allocation_candidate.list_allocation_candidates,
|
|
},
|
|
'/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
|
|
},
|
|
'/usages': {
|
|
'GET': usage.get_total_usages,
|
|
},
|
|
'/reshaper': {
|
|
'POST': reshaper.reshape,
|
|
},
|
|
}
|
|
|
|
|
|
def dispatch(environ, start_response, mapper):
|
|
"""Find a matching route for the current request.
|
|
|
|
If no match is found, raise a 404 response.
|
|
If there is a matching route, but no matching handler
|
|
for the given method, raise a 405.
|
|
"""
|
|
result = mapper.match(environ=environ)
|
|
if result is None:
|
|
raise webob.exc.HTTPNotFound(
|
|
json_formatter=util.json_error_formatter)
|
|
# We can't reach this code without action being present.
|
|
handler = result.pop('action')
|
|
environ['wsgiorg.routing_args'] = ((), result)
|
|
return handler(environ, start_response)
|
|
|
|
|
|
def handle_405(environ, start_response):
|
|
"""Return a 405 response when method is not allowed.
|
|
|
|
If _methods are in routing_args, send an allow header listing
|
|
the methods that are possible on the provided URL.
|
|
"""
|
|
_methods = util.wsgi_path_item(environ, '_methods')
|
|
headers = {}
|
|
if _methods:
|
|
# Ensure allow header is a python 2 or 3 native string (thus
|
|
# not unicode in python 2 but stay a string in python 3)
|
|
# In the process done by Routes to save the allowed methods
|
|
# to its routing table they become unicode in py2.
|
|
headers['allow'] = str(_methods)
|
|
# Use Exception class as WSGI Application. We don't want to raise here.
|
|
response = webob.exc.HTTPMethodNotAllowed(
|
|
_('The method specified is not allowed for this resource.'),
|
|
headers=headers, json_formatter=util.json_error_formatter)
|
|
return response(environ, start_response)
|
|
|
|
|
|
def make_map(declarations):
|
|
"""Process route declarations to create a Route Mapper."""
|
|
mapper = routes.Mapper()
|
|
for route, targets in declarations.items():
|
|
allowed_methods = []
|
|
for method in targets:
|
|
mapper.connect(route, action=targets[method],
|
|
conditions=dict(method=[method]))
|
|
allowed_methods.append(method)
|
|
allowed_methods = ', '.join(allowed_methods)
|
|
mapper.connect(route, action=handle_405, _methods=allowed_methods)
|
|
return mapper
|
|
|
|
|
|
class PlacementHandler(object):
|
|
"""Serve Placement API.
|
|
|
|
Dispatch to handlers defined in ROUTE_DECLARATIONS.
|
|
"""
|
|
|
|
def __init__(self, **local_config):
|
|
self._map = make_map(ROUTE_DECLARATIONS)
|
|
self.config = local_config['config']
|
|
|
|
def __call__(self, environ, start_response):
|
|
# set a reference to the oslo.config ConfigOpts on the RequestContext
|
|
context = environ['placement.context']
|
|
context.config = self.config
|
|
# Check that an incoming request with a content-length header
|
|
# that is an integer > 0 and not empty, also has a content-type
|
|
# header that is not empty. If not raise a 400.
|
|
clen = environ.get('CONTENT_LENGTH')
|
|
try:
|
|
if clen and (int(clen) > 0) and not environ.get('CONTENT_TYPE'):
|
|
raise webob.exc.HTTPBadRequest(
|
|
_('content-type header required when content-length > 0'),
|
|
json_formatter=util.json_error_formatter)
|
|
except ValueError as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
_('content-length header must be an integer'),
|
|
json_formatter=util.json_error_formatter)
|
|
try:
|
|
return dispatch(environ, start_response, self._map)
|
|
# Trap the NotFound exceptions raised by the objects used
|
|
# with the API and transform them into webob.exc.HTTPNotFound.
|
|
except exception.NotFound as exc:
|
|
raise webob.exc.HTTPNotFound(
|
|
exc, json_formatter=util.json_error_formatter)
|
|
except exception.PolicyNotAuthorized as exc:
|
|
raise webob.exc.HTTPForbidden(
|
|
exc.format_message(),
|
|
json_formatter=util.json_error_formatter)
|
|
# Remaining uncaught exceptions will rise first to the Microversion
|
|
# middleware, where any WebOb generated exceptions will be caught and
|
|
# transformed into legit HTTP error responses (with microversion
|
|
# headers added), and then to the FaultWrapper middleware which will
|
|
# catch anything else and transform them into 500 responses.
|
|
# NOTE(cdent): There should be very few uncaught exceptions which are
|
|
# not WebOb exceptions at this stage as the handlers are contained by
|
|
# the wsgify decorator which will transform those exceptions to
|
|
# responses itself.
|