placement/placement/microversion.py
Eric Fried 7efcb2e7f1 Further simplify microversion utils
Following on from [1]:
- Since we're only ever calling _fully_qualified_name with module-level
  functions, remove the ability to call it with a class (forcing such
  usages to either use functions or modify this util).
- Consolidate a redundant invocation of _fully_qualified_name from
  version_handler.

[1] Id8f96801116818dda6c68c301619aee185e8fbff

Change-Id: I205a7fd338a0db420b891341c3c40b10f3199fd3
2019-07-23 08:57:58 -05:00

170 lines
7.4 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.
"""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 inspect
import microversion_parse
import webob
SERVICE_TYPE = 'placement'
MICROVERSION_ENVIRON = '%s.microversion' % SERVICE_TYPE
VERSIONED_METHODS = collections.defaultdict(list)
# The Canonical Version List
VERSIONS = [
'1.0',
'1.1', # initial support for aggregate.get_aggregates and set_aggregates
'1.2', # Adds /resource_classes resource endpoint
'1.3', # Adds 'member_of' query parameter to get resource providers
# 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
'1.7', # PUT /resource_classes/{name} is bodiless create or update
'1.8', # Adds 'project_id' and 'user_id' required request parameters to
# PUT /allocations
'1.9', # Adds GET /usages
'1.10', # Adds GET /allocation_candidates resource endpoint
'1.11', # Adds 'allocations' link to the GET /resource_providers response
'1.12', # Add project_id and user_id to GET /allocations/{consumer_uuid}
# and PUT to /allocations/{consumer_uuid} in the same dict form
# as GET. The 'allocation_requests' format in GET
# /allocation_candidates is updated to be the same as well.
'1.13', # Adds POST /allocations to set allocations for multiple consumers
'1.14', # Adds parent and root provider UUID on resource provider
# representation and 'in_tree' filter on GET /resource_providers
'1.15', # Include last-modified and cache-control headers
'1.16', # Add 'limit' query parameter to GET /allocation_candidates
'1.17', # Add 'required' query parameter to GET /allocation_candidates and
# return traits in the provider summary.
'1.18', # Support ?required=<traits> queryparam on GET /resource_providers
'1.19', # Include generation and conflict detection in provider aggregates
# APIs
'1.20', # Return 200 with provider payload from POST /resource_providers
'1.21', # Support ?member_of=in:<agg UUIDs> queryparam on
# GET /allocation_candidates
'1.22', # Support forbidden traits in the required parameter of
# GET /resource_providers and GET /allocation_candidates
'1.23', # Add support for error codes in error response JSON
'1.24', # Support multiple ?member_of=<agg UUIDs> queryparams on
# GET /resource_providers
'1.25', # Adds support for granular resource requests via numbered
# querystring groups in GET /allocation_candidates
'1.26', # Add ability to specify inventory with reserved value equal to
# total.
'1.27', # Include all resource class inventories in `provider_summaries`
# field in response of `GET /allocation_candidates` API even if
# the resource class is not in the requested resources.
'1.28', # Add support for consumer generation
'1.29', # Support nested providers in GET /allocation_candidates API.
'1.30', # Add POST /reshaper for atomically migrating resource provider
# inventories and allocations.
'1.31', # Add in_tree and in_tree<N> queryparam on
# `GET /allocation_candidates` API
'1.32', # Support negative member_of queryparams on
# `GET /resource_providers` and `GET /allocation_candidates`
'1.33', # Support granular resource requests with suffixes that match
# [A-Za-z0-9_-]{1,64}.
'1.34', # Include a mappings key in allocation requests that shows which
# resource providers satisfied which request group suffix.
'1.35', # Add a `root_required` queryparam on `GET /allocation_candidates`
'1.36', # Add a `same_subtree` parameter on GET /allocation_candidates
# and allow resourceless requests for groups in `same_subtree`.
]
def max_version_string():
return VERSIONS[-1]
def min_version_string():
return VERSIONS[0]
# Based on code in twisted
# https://github.com/twisted/twisted/blob/trunk/twisted/python/deprecate.py
def _fully_qualified_name(handler):
"""Return the name of a function used as an HTTP API handler, qualified by
module name.
"""
if inspect.isfunction(handler):
module_name = handler.__module__
return "%s.%s" % (module_name, handler.__name__)
# We got a class, object method, or module. This is a coding error.
raise TypeError("_fully_qualified_name received bad handler type. "
"Module-level function required.")
def _find_method(qualified_name, version, status_code):
"""Look in VERSIONED_METHODS for method with right name matching version.
If no match is found a HTTPError corresponding to status_code will
be returned.
"""
# A KeyError shouldn't be possible here, but let's be robust
# just in case.
method_list = VERSIONED_METHODS.get(qualified_name, [])
for min_version, max_version, func in method_list:
if min_version <= version <= max_version:
return func
raise webob.exc.status_map[status_code]
def version_handler(min_ver, max_ver=None, status_code=404):
"""Decorator for versioning API methods.
Add as a decorator to a placement API handler to constrain
the microversions at which it will run. Add after the
``wsgify`` decorator.
This does not check for version intersections. That's the
domain of tests.
:param min_ver: A string of two numerals, X.Y indicating the
minimum version allowed for the decorated method.
:param max_ver: A string of two numerals, X.Y, indicating the
maximum version allowed for the decorated method.
:param status_code: A status code to indicate error, 404 by default
"""
def decorator(f):
min_version = microversion_parse.parse_version_string(min_ver)
if max_ver:
max_version = microversion_parse.parse_version_string(max_ver)
else:
max_version = microversion_parse.parse_version_string(
max_version_string())
qualified_name = _fully_qualified_name(f)
VERSIONED_METHODS[qualified_name].append(
(min_version, max_version, f))
def decorated_func(req, *args, **kwargs):
version = req.environ[MICROVERSION_ENVIRON]
return _find_method(
qualified_name, version, status_code)(req, *args, **kwargs)
# Sort highest min version to beginning of list.
VERSIONED_METHODS[qualified_name].sort(key=lambda x: x[0],
reverse=True)
return decorated_func
return decorator