magnum/magnum/api/controllers/base.py

230 lines
7.8 KiB
Python

# 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.
import datetime
import operator
import six
from magnum.api.controllers import versions
from magnum.api import versioned_method
from magnum.common import exception
from magnum.i18n import _
from pecan import rest
from webob import exc
import wsme
from wsme import types as wtypes
# name of attribute to keep version method information
VER_METHOD_ATTR = 'versioned_methods'
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is created"""
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is updated"""
def as_dict(self):
"""Render this object as a dict of its fields."""
return {k: getattr(self, k)
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset}
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.
:param except_list: A list of fields that won't be touched.
"""
if except_list is None:
except_list = []
for k in self.as_dict():
if k not in except_list:
setattr(self, k, wsme.Unset)
class ControllerMetaclass(type):
"""Controller metaclass.
This metaclass automates the task of assembling a dictionary
mapping action keys to method names.
"""
def __new__(mcs, name, bases, cls_dict):
"""Adds version function dictionary to the class."""
versioned_methods = None
for base in bases:
if base.__name__ == "Controller":
# NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute
# between API controller class creations. This allows us
# to use a class decorator on the API methods that doesn't
# require naming explicitly what method is being versioned as
# it can be implicit based on the method decorated. It is a bit
# ugly.
if VER_METHOD_ATTR in base.__dict__:
versioned_methods = getattr(base, VER_METHOD_ATTR)
delattr(base, VER_METHOD_ATTR)
if versioned_methods:
cls_dict[VER_METHOD_ATTR] = versioned_methods
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
cls_dict)
@six.add_metaclass(ControllerMetaclass)
class Controller(rest.RestController):
"""Base Rest Controller"""
def __getattribute__(self, key):
def version_select():
"""Select the correct method based on version
@return: Returns the correct versioned method
@raises: HTTPNotAcceptable if there is no method which
matches the name and version constraints
"""
from pecan import request
ver = request.version
func_list = self.versioned_methods[key]
for func in func_list:
if ver.matches(func.start_version, func.end_version):
return func.func
raise exc.HTTPNotAcceptable(_(
"Version %(ver)s was requested but the requested API %(api)s "
"is not supported for this version.") % {'ver': ver,
'api': key})
try:
version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR)
except AttributeError:
# No versioning on this class
return object.__getattribute__(self, key)
if version_meth_dict and key in version_meth_dict:
return version_select().__get__(self, self.__class__)
return object.__getattribute__(self, key)
# NOTE: This decorator MUST appear first (the outermost
# decorator) on an API method for it to work correctly
@classmethod
def api_version(cls, min_ver, max_ver=None):
"""Decorator for versioning api methods.
Add the decorator to any pecan method that has been exposed.
This decorator will store the method, min version, and max
version in a list for each api. It will check that there is no
overlap between versions and methods. When the api is called the
controller will use the list for each api to determine which
method to call.
Example:
@base.Controller.api_version("1.1", "1.2")
@expose.expose(Cluster, types.uuid_or_name)
def get_one(self, bay_ident):
{...code for versions 1.1 to 1.2...}
@base.Controller.api_version("1.3")
@expose.expose(Cluster, types.uuid_or_name)
def get_one(self, bay_ident):
{...code for versions 1.3 to latest}
@min_ver: string representing minimum version
@max_ver: optional string representing maximum version
@raises: ApiVersionsIntersect if an version overlap is found between
method versions.
"""
def decorator(f):
obj_min_ver = versions.Version('', '', '', min_ver)
if max_ver:
obj_max_ver = versions.Version('', '', '', max_ver)
else:
obj_max_ver = versions.Version('', '', '',
versions.CURRENT_MAX_VER)
# Add to list of versioned methods registered
func_name = f.__name__
new_func = versioned_method.VersionedMethod(
func_name, obj_min_ver, obj_max_ver, f)
func_dict = getattr(cls, VER_METHOD_ATTR, {})
if not func_dict:
setattr(cls, VER_METHOD_ATTR, func_dict)
func_list = func_dict.get(func_name, [])
if not func_list:
func_dict[func_name] = func_list
func_list.append(new_func)
is_intersect = Controller.check_for_versions_intersection(
func_list)
if is_intersect:
raise exception.ApiVersionsIntersect(
name=new_func.name,
min_ver=new_func.start_version,
max_ver=new_func.end_version
)
# Ensure the list is sorted by minimum version (reversed)
# so later when we work through the list in order we find
# the method which has the latest version which supports
# the version requested.
func_list.sort(key=lambda f: f.start_version, reverse=True)
return f
return decorator
@staticmethod
def check_for_versions_intersection(func_list):
"""Determines whether function list intersections
General algorithm:
https://en.wikipedia.org/wiki/Intersection_algorithm
:param func_list: list of VersionedMethod objects
:return: boolean
"""
pairs = []
counter = 0
for f in func_list:
pairs.append((f.start_version, 1))
pairs.append((f.end_version, -1))
pairs.sort(key=operator.itemgetter(1), reverse=True)
pairs.sort(key=operator.itemgetter(0))
for p in pairs:
counter += p[1]
if counter > 1:
return True
return False