You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
7.8 KiB
229 lines
7.8 KiB
# 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, cluster_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, cluster_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
|
|
|