Add initial framing of placement API
The placement API will initially host resource provider information and then will grow to provide a full placement service. Eventually it will be fully extracted from Nova. To facilitate that extraction, this service is being developed with few ties to existing nova.wsgi structures. Instead it uses relatively plain WSGI apps that are: * uses the Routes library with declarative mapping defined in ROUTE_DECLARATIONS * basic wsgi apps, with webob and the request class, for each handler define as functions * does not use a paste-ini file to compose middleware, instead code which minimally inspects the oslo config to know how to adjust middleware (in the initial case choosing an auth_strategy) Many of these "features" will be demonstrated in commits that follow this one that enable specific behaviors for resource providers and their inventories. In order to ensure that this change can be merged in an atomic fashion it includes microversion support that was previously in its own commit. The microversion_parse library is used in a WSGI middleware to parse incoming microversion headers and place the determined value into the WSGI environment at the 'placement.microversion' key. Response headers are adjusted to add the required outgoing headers as described in: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html If a client requests an invalid microversion, they will receive a 400 response. If the microversion is of a valid form but not available, they will received a 406 response. The format of that response is dependent on the accept header of the request. If it is JSON, the min and max available microversions are presented. A request to '/' will return version discovery information. Thus far nothing else is done with the microversion information. It is there for when we need it in the future. For now everything is version 1.0. The groundwork for using gabbi to test the API is in place in nova/tests/functional/api/openstack/placement. The gabbi tests are included in the functional target. To run just the placement tests it is possible to run 'tox -efunctional placement'. Change-Id: Icb9c8f7a1fa8a9eac66c2d72f4b7e4efd4e1944f Partially-Implements: blueprint generic-resource-pools
This commit is contained in:
parent
75b9b143dd
commit
2ae10ce522
10
.testr.conf
10
.testr.conf
|
@ -6,3 +6,13 @@ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
|
|||
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
||||
# NOTE(cdent): The group_regex describes how testrepository will
|
||||
# group tests into the same process when running concurently. The
|
||||
# following insures that gabbi tests coming from the same YAML file
|
||||
# are all in the same process. This is important because each YAML
|
||||
# file represents an ordered sequence of HTTP requests. Note that
|
||||
# tests which do not match this regex will not be grouped in any
|
||||
# special way. See the following for more details.
|
||||
# http://testrepository.readthedocs.io/en/latest/MANUAL.html#grouping-tests
|
||||
# https://gabbi.readthedocs.io/en/latest/#purpose
|
||||
group_regex=(gabbi\.(?:driver|suitemaker)\.test_placement_api_([^_]+))
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
# 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.
|
||||
|
||||
|
||||
from oslo_context import context
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import request_id
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova import conf
|
||||
|
||||
|
||||
CONF = conf.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Middleware(object):
|
||||
|
||||
def __init__(self, application, **kwargs):
|
||||
self.application = application
|
||||
|
||||
|
||||
# NOTE(cdent): Only to be used in tests where auth is being faked.
|
||||
class NoAuthMiddleware(Middleware):
|
||||
"""Require a token if one isn't present."""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
if 'X-Auth-Token' not in req.headers:
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
|
||||
token = req.headers['X-Auth-Token']
|
||||
user_id, _sep, project_id = token.partition(':')
|
||||
project_id = project_id or user_id
|
||||
if user_id == 'admin':
|
||||
roles = ['admin']
|
||||
else:
|
||||
roles = []
|
||||
req.headers['X_USER_ID'] = user_id
|
||||
req.headers['X_TENANT_ID'] = project_id
|
||||
req.headers['X_ROLES'] = ','.join(roles)
|
||||
return self.application
|
||||
|
||||
|
||||
class PlacementKeystoneContext(Middleware):
|
||||
"""Make a request context from keystone headers."""
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
|
||||
|
||||
ctx = context.RequestContext.from_environ(
|
||||
req.environ, request_id=req_id)
|
||||
|
||||
if ctx.user is None:
|
||||
LOG.debug("Neither X_USER_ID nor X_USER found in request")
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
|
||||
req.environ['placement.context'] = ctx
|
||||
return self.application
|
|
@ -0,0 +1,55 @@
|
|||
# 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.
|
||||
"""Deployment handling for Placmenent API."""
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_middleware import request_id
|
||||
|
||||
from nova.api.openstack.placement import auth
|
||||
from nova.api.openstack.placement import handler
|
||||
from nova.api.openstack.placement import microversion
|
||||
|
||||
# TODO(cdent): register objects here as this is our startup place,
|
||||
# but only once we start using them.
|
||||
|
||||
# TODO(cdent): NAME points to the config project being used, so for
|
||||
# now this is "nova" but we probably want "placement" eventually.
|
||||
NAME = "nova"
|
||||
|
||||
|
||||
def deploy(conf, project_name):
|
||||
"""Assemble the middleware pipeline leading to the placement app."""
|
||||
if conf.auth_strategy == 'noauth2':
|
||||
auth_middleware = auth.NoAuthMiddleware
|
||||
else:
|
||||
# Do not provide global conf to middleware here.
|
||||
auth_middleware = auth_token.filter_factory(
|
||||
{}, olso_config_project=project_name)
|
||||
|
||||
context_middleware = auth.PlacementKeystoneContext
|
||||
req_id_middleware = request_id.RequestId
|
||||
microversion_middleware = microversion.MicroversionMiddleware
|
||||
|
||||
application = handler.PlacementHandler()
|
||||
|
||||
for middleware in (context_middleware,
|
||||
auth_middleware,
|
||||
microversion_middleware,
|
||||
req_id_middleware):
|
||||
application = middleware(application)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def loadapp(config, project_name=NAME):
|
||||
application = deploy(config, project_name)
|
||||
return application
|
|
@ -0,0 +1,114 @@
|
|||
# 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 nova.api.openstack.placement.handlers import root
|
||||
from nova.api.openstack.placement import util
|
||||
|
||||
|
||||
# URLs and Handlers
|
||||
ROUTE_DECLARATIONS = {
|
||||
'/': {
|
||||
'GET': root.home,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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 as required.
|
||||
|
||||
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:
|
||||
headers['allow'] = _methods
|
||||
raise webob.exc.HTTPMethodNotAllowed(
|
||||
'The method specified is not allowed for this resource.',
|
||||
headers=headers, json_formatter=util.json_error_formatter)
|
||||
|
||||
|
||||
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):
|
||||
# NOTE(cdent): Local config currently unused.
|
||||
self._map = make_map(ROUTE_DECLARATIONS)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# All requests but '/' require admin.
|
||||
# TODO(cdent): We'll eventually want our own auth context,
|
||||
# but using nova's is convenient for now.
|
||||
if environ['PATH_INFO'] != '/':
|
||||
context = environ['placement.context']
|
||||
# TODO(cdent): Using is_admin everywhere (except /) is
|
||||
# insufficiently flexible for future use case but is
|
||||
# convenient for initial exploration. We will need to
|
||||
# determine how to manage authorization/policy and
|
||||
# implement that, probably per handler. Also this is
|
||||
# just the wrong way to do things, but policy not
|
||||
# integrated yet.
|
||||
if 'admin' not in context.to_policy_values()['roles']:
|
||||
raise webob.exc.HTTPForbidden(
|
||||
'admin required',
|
||||
json_formatter=util.json_error_formatter)
|
||||
return dispatch(environ, start_response, self._map)
|
|
@ -0,0 +1,36 @@
|
|||
# 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.
|
||||
"""Handler for the root of the Placement API."""
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
import webob
|
||||
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
|
||||
|
||||
@webob.dec.wsgify
|
||||
def home(req):
|
||||
min_version = microversion.min_version_string()
|
||||
max_version = microversion.max_version_string()
|
||||
# NOTE(cdent): As sections of the api are added, links can be
|
||||
# added to this output to align with the guidelines at
|
||||
# http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery
|
||||
version_data = {
|
||||
'id': 'v%s' % min_version,
|
||||
'max_version': max_version,
|
||||
'min_version': min_version,
|
||||
}
|
||||
version_json = jsonutils.dumps({'versions': [version_data]})
|
||||
req.response.body = version_json
|
||||
req.response.content_type = 'application/json'
|
||||
return req.response
|
|
@ -0,0 +1,143 @@
|
|||
# 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.
|
||||
"""Microversion handling."""
|
||||
|
||||
# NOTE(cdent): This code is taken from enamel:
|
||||
# https://github.com/jaypipes/enamel and was the original source of
|
||||
# the code now used in microversion_parse library.
|
||||
|
||||
import collections
|
||||
|
||||
import microversion_parse
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import util
|
||||
|
||||
|
||||
SERVICE_TYPE = 'placement'
|
||||
MICROVERSION_ENVIRON = '%s.microversion' % SERVICE_TYPE
|
||||
|
||||
# The Canonical Version List
|
||||
VERSIONS = [
|
||||
'1.0',
|
||||
]
|
||||
|
||||
|
||||
def max_version_string():
|
||||
return VERSIONS[-1]
|
||||
|
||||
|
||||
def min_version_string():
|
||||
return VERSIONS[0]
|
||||
|
||||
|
||||
def parse_version_string(version_string):
|
||||
"""Turn a version string into a Version
|
||||
|
||||
:param version_string: A string of two numerals, X.Y, or 'latest'
|
||||
:returns: a Version
|
||||
:raises: TypeError
|
||||
"""
|
||||
if version_string == 'latest':
|
||||
version_string = max_version_string()
|
||||
try:
|
||||
# The combination of int and a limited split with the
|
||||
# named tuple means that this incantation will raise
|
||||
# ValueError or TypeError when the incoming data is
|
||||
# poorly formed but will, however, naturally adapt to
|
||||
# extraneous whitespace.
|
||||
return Version(*(int(value) for value
|
||||
in version_string.split('.', 1)))
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise TypeError('invalid version string: %s; %s' % (
|
||||
version_string, exc))
|
||||
|
||||
|
||||
class MicroversionMiddleware(object):
|
||||
"""WSGI middleware for getting microversion info."""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
try:
|
||||
req.environ[MICROVERSION_ENVIRON] = extract_version(
|
||||
req.headers)
|
||||
except ValueError as exc:
|
||||
raise webob.exc.HTTPNotAcceptable(
|
||||
'Invalid microversion: %s' % exc,
|
||||
json_formatter=util.json_error_formatter)
|
||||
except TypeError as exc:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
'Invalid microversion: %s' % exc,
|
||||
json_formatter=util.json_error_formatter)
|
||||
response = req.get_response(self.application)
|
||||
response.headers.add(Version.HEADER,
|
||||
'%s %s' % (SERVICE_TYPE,
|
||||
req.environ[MICROVERSION_ENVIRON]))
|
||||
response.headers.add('vary', Version.HEADER)
|
||||
return response
|
||||
|
||||
|
||||
class Version(collections.namedtuple('Version', 'major minor')):
|
||||
"""A namedtuple containing major and minor values.
|
||||
|
||||
Since it is a tuple is automatically comparable.
|
||||
"""
|
||||
|
||||
HEADER = 'OpenStack-API-Version'
|
||||
|
||||
MIN_VERSION = None
|
||||
MAX_VERSION = None
|
||||
|
||||
def __str__(self):
|
||||
return '%s.%s' % (self.major, self.minor)
|
||||
|
||||
@property
|
||||
def max_version(self):
|
||||
if not self.MAX_VERSION:
|
||||
self.MAX_VERSION = parse_version_string(max_version_string())
|
||||
return self.MAX_VERSION
|
||||
|
||||
@property
|
||||
def min_version(self):
|
||||
if not self.MIN_VERSION:
|
||||
self.MIN_VERSION = parse_version_string(min_version_string())
|
||||
return self.MIN_VERSION
|
||||
|
||||
def matches(self, min_version=None, max_version=None):
|
||||
if min_version is None:
|
||||
min_version = self.min_version
|
||||
if max_version is None:
|
||||
max_version = self.max_version
|
||||
return min_version <= self <= max_version
|
||||
|
||||
|
||||
def extract_version(headers):
|
||||
"""Extract the microversion from Version.HEADER
|
||||
|
||||
There may be multiple headers and some which don't match our
|
||||
service.
|
||||
"""
|
||||
found_version = microversion_parse.get_version(headers,
|
||||
service_type=SERVICE_TYPE)
|
||||
|
||||
version_string = found_version or min_version_string()
|
||||
request_version = parse_version_string(version_string)
|
||||
# We need a version that is in VERSION and within MIX and MAX.
|
||||
# This gives us the option to administratively disable a
|
||||
# version if we really need to.
|
||||
if (str(request_version) in VERSIONS and request_version.matches()):
|
||||
return request_version
|
||||
raise ValueError('Unacceptable version header: %s' % version_string)
|
|
@ -0,0 +1,27 @@
|
|||
# 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.
|
||||
"""WSGI script for Placement API
|
||||
|
||||
WSGI handler for running Placement API under Apache2, nginx, gunicorn etc.
|
||||
"""
|
||||
|
||||
from nova.api.openstack.placement import deploy
|
||||
from nova import conf
|
||||
from nova import config
|
||||
|
||||
|
||||
CONFIG_FILE = '/etc/nova/nova.conf'
|
||||
|
||||
|
||||
config.parse_args([], default_config_files=[CONFIG_FILE])
|
||||
|
||||
application = deploy.loadapp(conf.CONF)
|
|
@ -0,0 +1,65 @@
|
|||
# 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."""
|
||||
|
||||
from oslo_middleware import request_id
|
||||
import webob
|
||||
|
||||
# NOTE(cdent): avoid cyclical import conflict between util and
|
||||
# microversion
|
||||
import nova.api.openstack.placement.microversion
|
||||
|
||||
|
||||
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)
|
||||
error_dict = {
|
||||
'status': status,
|
||||
'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
|
||||
# Get status code out of status message. webob's error formatter
|
||||
# only passes entire status string.
|
||||
code = status.split(None, 1)[0]
|
||||
if code == '406' and microversion.MICROVERSION_ENVIRON not in environ:
|
||||
error_dict['max_version'] = microversion.max_version_string()
|
||||
error_dict['min_version'] = microversion.max_version_string()
|
||||
|
||||
return {'errors': [error_dict]}
|
||||
|
||||
|
||||
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
|
|
@ -0,0 +1,41 @@
|
|||
# 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.
|
||||
|
||||
from gabbi import fixture
|
||||
|
||||
from nova.api.openstack.placement import deploy
|
||||
from nova import conf
|
||||
from nova import config
|
||||
|
||||
|
||||
CONF = conf.CONF
|
||||
|
||||
|
||||
def setup_app():
|
||||
return deploy.loadapp(CONF)
|
||||
|
||||
|
||||
class APIFixture(fixture.GabbiFixture):
|
||||
"""Setup the required backend fixtures for a basic placement service."""
|
||||
|
||||
def __init__(self):
|
||||
self.conf = None
|
||||
|
||||
def start_fixture(self):
|
||||
self.conf = CONF
|
||||
config.parse_args([], default_config_files=None, configure_db=False,
|
||||
init_rpc=False)
|
||||
self.conf.set_override('auth_strategy', 'noauth2')
|
||||
|
||||
def stop_fixture(self):
|
||||
if self.conf:
|
||||
self.conf.reset()
|
|
@ -0,0 +1,39 @@
|
|||
#
|
||||
# Test the basic handling of HTTP (expected response codes and the
|
||||
# like).
|
||||
#
|
||||
|
||||
fixtures:
|
||||
- APIFixture
|
||||
|
||||
defaults:
|
||||
request_headers:
|
||||
# NOTE(cdent): Get past keystone, even though at this stage
|
||||
# we don't require auth.
|
||||
x-auth-token: admin
|
||||
accept: application/json
|
||||
|
||||
tests:
|
||||
- name: 404 at no service
|
||||
GET: /barnabas
|
||||
status: 404
|
||||
response_json_paths:
|
||||
$.errors[0].title: Not Found
|
||||
|
||||
- name: error message has request id
|
||||
GET: /barnabas
|
||||
status: 404
|
||||
response_json_paths:
|
||||
$.errors[0].request_id: /req-[a-fA-F0-9-]+/
|
||||
|
||||
- name: 405 on bad method at root
|
||||
DELETE: /
|
||||
status: 405
|
||||
response_headers:
|
||||
allow: GET
|
||||
response_json_paths:
|
||||
$.errors[0].title: Method Not Allowed
|
||||
|
||||
- name: 200 at home
|
||||
GET: /
|
||||
status: 200
|
|
@ -0,0 +1,22 @@
|
|||
#
|
||||
# Confirm that the noauth handler is causing a 401 when no fake
|
||||
# token is provided.
|
||||
#
|
||||
|
||||
fixtures:
|
||||
- APIFixture
|
||||
|
||||
defaults:
|
||||
request_headers:
|
||||
accept: application/json
|
||||
|
||||
tests:
|
||||
- name: no token gets 401
|
||||
GET: /
|
||||
status: 401
|
||||
|
||||
- name: with token 200
|
||||
GET: /
|
||||
request_headers:
|
||||
x-auth-token: admin:admin
|
||||
status: 200
|
|
@ -0,0 +1,73 @@
|
|||
# Tests to build microversion functionality behavior and confirm
|
||||
# it is present and behaving as expected.
|
||||
|
||||
fixtures:
|
||||
- APIFixture
|
||||
|
||||
defaults:
|
||||
request_headers:
|
||||
accept: application/json
|
||||
x-auth-token: user
|
||||
|
||||
tests:
|
||||
- name: root has microversion header
|
||||
GET: /
|
||||
response_headers:
|
||||
vary: /OpenStack-API-Version/
|
||||
openstack-api-version: /^placement \d+\.\d+$/
|
||||
|
||||
- name: root has microversion info
|
||||
GET: /
|
||||
response_json_paths:
|
||||
$.versions[0].max_version: /^\d+\.\d+$/
|
||||
$.versions[0].min_version: /^\d+\.\d+$/
|
||||
$.versions[0].id: v1.0
|
||||
|
||||
- name: unavailable microversion raises 406
|
||||
GET: /
|
||||
request_headers:
|
||||
openstack-api-version: placement 0.5
|
||||
status: 406
|
||||
response_headers:
|
||||
content-type: /application/json/
|
||||
response_json_paths:
|
||||
$.errors.[0].title: Not Acceptable
|
||||
$.errors.[0].max_version: /^\d+\.\d+$/
|
||||
$.errors.[0].min_version: /^\d+\.\d+$/
|
||||
response_strings:
|
||||
- "Unacceptable version header: 0.5"
|
||||
|
||||
- name: latest microversion is 1.0
|
||||
GET: /
|
||||
request_headers:
|
||||
openstack-api-version: placement latest
|
||||
response_headers:
|
||||
vary: /OpenStack-API-Version/
|
||||
openstack-api-version: placement 1.0
|
||||
|
||||
- name: other accept header bad version
|
||||
GET: /
|
||||
request_headers:
|
||||
accept: text/html
|
||||
openstack-api-version: placement 0.5
|
||||
status: 406
|
||||
response_headers:
|
||||
content-type: /text/html/
|
||||
response_strings:
|
||||
- "Unacceptable version header: 0.5"
|
||||
|
||||
- name: bad format string raises 400
|
||||
GET: /
|
||||
request_headers:
|
||||
openstack-api-version: placement pony.horse
|
||||
status: 400
|
||||
response_strings:
|
||||
- "invalid version string: pony.horse"
|
||||
|
||||
- name: bad format multidot raises 400
|
||||
GET: /
|
||||
request_headers:
|
||||
openstack-api-version: placement 1.2.3
|
||||
status: 400
|
||||
response_strings:
|
||||
- "invalid version string: 1.2.3"
|
|
@ -0,0 +1,27 @@
|
|||
# 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.
|
||||
|
||||
import os
|
||||
|
||||
from gabbi import driver
|
||||
|
||||
from nova.tests.functional.api.openstack.placement import fixtures
|
||||
|
||||
TESTS_DIR = 'gabbits'
|
||||
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
"""Provide a TestSuite to the discovery process."""
|
||||
test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR)
|
||||
return driver.build_tests(test_dir, loader, host=None,
|
||||
intercept=fixtures.setup_app,
|
||||
fixture_module=fixtures)
|
|
@ -0,0 +1,99 @@
|
|||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
"""Unit tests for the functions used by the placement API handlers."""
|
||||
|
||||
import mock
|
||||
import routes
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import handler
|
||||
from nova import test
|
||||
from nova.tests import uuidsentinel
|
||||
|
||||
|
||||
# Used in tests below
|
||||
def start_response(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def _environ(path='/moo', method='GET'):
|
||||
return {
|
||||
'PATH_INFO': path,
|
||||
'REQUEST_METHOD': method,
|
||||
'SERVER_NAME': 'example.com',
|
||||
'SERVER_PORT': '80',
|
||||
'wsgi.url_scheme': 'http',
|
||||
}
|
||||
|
||||
|
||||
class DispatchTest(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DispatchTest, self).setUp()
|
||||
self.mapper = routes.Mapper()
|
||||
self.route_handler = mock.MagicMock()
|
||||
|
||||
def test_no_match_null_map(self):
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
handler.dispatch,
|
||||
_environ(), start_response,
|
||||
self.mapper)
|
||||
|
||||
def test_no_match_with_map(self):
|
||||
self.mapper.connect('/foobar', action='hello')
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
handler.dispatch,
|
||||
_environ(), start_response,
|
||||
self.mapper)
|
||||
|
||||
def test_simple_match(self):
|
||||
self.mapper.connect('/foobar', action=self.route_handler,
|
||||
conditions=dict(method=['GET']))
|
||||
environ = _environ(path='/foobar')
|
||||
handler.dispatch(environ, start_response, self.mapper)
|
||||
self.route_handler.assert_called_with(environ, start_response)
|
||||
|
||||
def test_simple_match_routing_args(self):
|
||||
self.mapper.connect('/foobar/{id}', action=self.route_handler,
|
||||
conditions=dict(method=['GET']))
|
||||
environ = _environ(path='/foobar/%s' % uuidsentinel.foobar)
|
||||
handler.dispatch(environ, start_response, self.mapper)
|
||||
self.route_handler.assert_called_with(environ, start_response)
|
||||
self.assertEqual(uuidsentinel.foobar,
|
||||
environ['wsgiorg.routing_args'][1]['id'])
|
||||
|
||||
|
||||
class MapperTest(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(MapperTest, self).setUp()
|
||||
declarations = {
|
||||
'/hello': {'GET': 'hello'}
|
||||
}
|
||||
self.mapper = handler.make_map(declarations)
|
||||
|
||||
def test_no_match(self):
|
||||
environ = _environ(path='/cow')
|
||||
self.assertIsNone(self.mapper.match(environ=environ))
|
||||
|
||||
def test_match(self):
|
||||
environ = _environ(path='/hello')
|
||||
action = self.mapper.match(environ=environ)['action']
|
||||
self.assertEqual('hello', action)
|
||||
|
||||
def test_405(self):
|
||||
environ = _environ(path='/hello', method='POST')
|
||||
result = self.mapper.match(environ=environ)
|
||||
self.assertEqual(handler.handle_405, result['action'])
|
||||
self.assertEqual('GET', result['_methods'])
|
|
@ -24,6 +24,7 @@ testtools>=1.4.0 # MIT
|
|||
tempest-lib>=0.14.0 # Apache-2.0
|
||||
bandit>=1.0.1 # Apache-2.0
|
||||
openstackdocstheme>=1.4.0 # Apache-2.0
|
||||
gabbi>=1.24.0 # Apache-2.0
|
||||
|
||||
# vmwareapi driver specific dependencies
|
||||
oslo.vmware>=2.11.0 # Apache-2.0
|
||||
|
|
Loading…
Reference in New Issue