教程:创建 API 端点

本文档的这一部分详细介绍了如何为 CloudKitty 的 v2 API 创建一个端点。v1 API 已冻结,不应添加任何端点。

设置新资源布局

在本节中,我们将创建一个 example 端点。在 cloudkitty/api/v2/ 中创建以下文件和子目录

cloudkitty/api/v2/
└── example
    ├── example.py
    └── __init__.py

创建自定义资源

每个 v2 API 端点都基于一个 Flask Blueprint 和每个子端点一个 Flask-RESTful 资源。这允许对资源进行逻辑分组。以 /rating/hashmap 路由为例。hashmap 模块的每个资源都应是一个 Flask-RESTful 资源(例如 /rating/hashmap/service/rating/hashmap/field 等)。

注意

应该区分引用单个资源和多个资源的端点。例如,如果您想要一个允许列出某种类型资源的端点,则应实现以下内容

  • 一个支持 GETPOSTPUT HTTP 方法的 MyResource 资源,路由为 /myresource/<uuid:>

  • 一个支持 GET HTTP 方法的 MyResourceList 资源,路由为 /myresource

  • 包含这些资源的蓝图。

基本资源

我们将创建一个 /example/ 端点,用于操作水果。我们将创建一个 Example 资源,支持 GETPOST HTTP 方法。首先,我们将在 cloudkitty/api/v2/example/example.py 中创建一个包含 getpost 方法的类

from cloudkitty.api.v2 import base


class Example(base.BaseResource):

    def get(self):
        pass

    def post(self):
        pass

验证方法参数和输出

对我们资源的 GET 请求将简单地返回 {“message”: “这是一个示例端点”}add_output_schema 装饰器将 voluptuous 验证添加到方法的输出。这允许设置默认值。

cloudkitty.api.v2.utils.add_output_schema(schema)[source]

为方法的输出添加 voluptuous 模式验证

示例用法

class Example(base.BaseResource):

    @api_utils.add_output_schema({
        voluptuous.Required(
            'message',
            default='This is an example endpoint',
        ): validation_utils.get_string_type(),
    })
    def get(self):
        return {}
参数:

schema (dict) – 应用于方法输出的模式

让我们更新我们的 get 方法,以便使用此装饰器

import voluptuous

from cloudkitty.api.v2 import base
from cloudkitty import validation_utils


class Example(base.BaseResource):

    @api_utils.add_output_schema({
        voluptuous.Required(
            'message',
            default='This is an example endpoint',
        ): validation_utils.get_string_type(),
    })
    def get(self):
        return {}

注意

在此片段中,get_string_type 在 python2 中返回 basestring,在 python3 中返回 str

$ curl 'http://cloudkitty-api:8889/v2/example'
{"message": "This is an example endpoint"}

现在是时候实现 post 方法了。此函数将接收一个参数。为了验证它,我们将使用 add_input_schema 装饰器

cloudkitty.api.v2.utils.add_input_schema(location, schema)[source]

为方法的输入添加 voluptuous 模式验证

接受可以转换为 voluptuous 模式的字典作为参数,并使用此模式验证参数。“location”参数用于指定参数的位置。请注意,对于查询参数,Flask 返回一个 MultiDict。因此,每个字典键都将包含一个列表。为了方便与唯一的查询参数交互,可以使用 voluptuous 验证器 SingleQueryParam

from cloudkitty.api.v2 import utils as api_utils
@api_utils.add_input_schema('query', {
    voluptuous.Required('fruit'): api_utils.SingleQueryParam(str),
})
def put(self, fruit=None):
    return fruit

要接受查询参数列表,可以使用 MultiQueryParam

from cloudkitty.api.v2 import utils as api_utils
@api_utils.add_input_schema('query', {
    voluptuous.Required('fruit'): api_utils.MultiQueryParam(str),
})
def put(self, fruit=[]):
    for f in fruit:
        # Do something with the fruit
参数:
  • location (str) – 参数的位置。必须是 [‘body’, ‘query’] 中的一个

  • schema (dict) – 应用于方法 kwargs 的模式

输入模式验证的参数作为命名参数传递给装饰的函数。让我们实现 post 方法。我们将使用 Werkzeug 异常作为 HTTP 返回代码。

@api_utils.add_input_schema('body', {
    voluptuous.Required('fruit'): validation_utils.get_string_type(),
})
def post(self, fruit=None):
    policy.authorize(flask.request.context, 'example:submit_fruit', {})
    if not fruit:
        raise http_exceptions.BadRequest(
            'You must submit a fruit',
        )
    if fruit not in ['banana', 'strawberry']:
        raise http_exceptions.Forbidden(
            'You submitted a forbidden fruit',
        )
    return {
        'message': 'Your fruit is a ' + fruit,
    }

在这里,fruit 预计可以在请求体中找到

$ curl -X POST -H 'Content-Type: application/json' 'http://cloudkitty-api:8889/v2/example' -d '{"fruit": "banana"}'
{"message": "Your fruit is a banana"}

为了从查询中检索 fruit,该函数应这样装饰

@api_utils.add_input_schema('query', {
    voluptuous.Required('fruit'): api_utils.SingleQueryParam(str),
})
def post(self, fruit=None):

请注意,这里使用了一个 SingleQueryParam:由于查询参数可以多次指定(例如 xxx?groupby=a&groupby=b),Flask 将查询参数作为列表提供。 SingleQueryParam 助手检查参数是否只提供一次,并返回它。

class cloudkitty.api.v2.utils.SingleQueryParam(param_type)[source]

Voluptuous 验证器,允许验证唯一的查询参数。

此验证器检查 URL 查询参数是否只提供一次,验证其类型并直接返回它,而不是返回包含单个元素的列表。

请注意,此验证器在内部使用 voluptuous.Coerce 进行类型检查。因此,不应将其与 cloudkitty.utils.validation.get_string_type 在 python2 中一起使用。

参数:

param_type – 查询参数的类型

警告

SingleQueryParam 在内部使用 voluptuous.Coerce 进行类型检查。因此,不能将 validation_utils.get_string_type 作为 basestring 无法实例化。

授权方法

Example 资源仍然缺少一些授权。我们将为每个方法创建一个策略,可通过 policy.yaml 文件进行配置。在 cloudkitty/common/policies/v2/example.py 中创建以下内容

from oslo_policy import policy

from cloudkitty.common.policies import base

example_policies = [
    policy.DocumentedRuleDefault(
        name='example:get_example',
        check_str=base.UNPROTECTED,
        description='Get an example message',
        operations=[{'path': '/v2/example',
                     'method': 'GET'}]),
    policy.DocumentedRuleDefault(
        name='example:submit_fruit',
        check_str=base.UNPROTECTED,
        description='Submit a fruit',
        operations=[{'path': '/v2/example',
                     'method': 'POST'}]),
]


def list_rules():
    return example_policies

将以下行添加到 cloudkitty/common/policies/__init__.py

# [...]
from cloudkitty.common.policies.v2 import example as v2_example


def list_rules():
    return itertools.chain(
        base.list_rules(),
        # [...]
        v2_example.list_rules(),
    )

这注册了两个已记录的策略,get_examplesubmit_fruit。默认情况下,它们不受保护,这意味着任何人都可以访问它们。但是,它们可以在 policy.yaml 中被覆盖。以以下方式调用它们

# [...]
import flask

from cloudkitty.common import policy
from cloudkitty.api.v2 import base

class Example(base.BaseResource):
    # [...]
    def get(self):
        policy.authorize(flask.request.context, 'example:get_example', {})
        return {}

    # [...]
    def post(self):
        policy.authorize(flask.request.context, 'example:submit_fruit', {})
        # [...]

加载驱动程序

大多数资源需要加载一些驱动程序(存储、SQL…)。由于这些驱动程序的实例化可能需要一些时间,因此应该只执行一次。

一些驱动程序(例如存储驱动程序)在 BaseResource 中加载,因此对所有资源可用。

需要一些额外驱动程序的资源应实现 reload 函数

class BaseResource(flask_restful.Resource):

    @classmethod
    def reload(cls):
        """Reloads all required drivers"""

这是一个来自 cloudkitty.api.v2.scope.state.ScopeState 的示例

@classmethod
def reload(cls):
    super(ScopeState, cls).reload()
    cls._client = messaging.get_client()
    cls._storage_state = storage_state.StateManager()

注册资源

每个端点都应提供一个接受 Flask 应用作为唯一参数的 init 方法。此方法应调用 do_init

cloudkitty.api.v2.utils.do_init(app, blueprint_name, resources)[source]

将包含一个或多个资源的蓝图注册到应用。

参数:
  • app (flask.Flask) – 要注册蓝图的 Flask 应用

  • blueprint_name (str) – 要创建的蓝图的名称

  • resources (与 cloudkitty.api.v2.RESOURCE_SCHEMA 匹配的字典列表) – 要添加到蓝图的 Api 的资源

将以下内容添加到 cloudkitty/api/v2/example/__init__.py

from cloudkitty.api.v2 import utils as api_utils


def init(app):
    api_utils.do_init(app, 'example', [
        {
            'module': __name__ + '.' + 'example',
            'resource_class': 'Example',
            'url': '',
        },
    ])
    return app

在这里,我们使用作为参数传递的 Flask 应用、蓝图名称和资源列表调用 do_init。蓝图名称将作为所有资源的 URL 的前缀。每个资源都由一个具有以下属性的字典表示

  • module:包含资源类的 python 模块的名称

  • resource_class:资源的类

  • url:URL 后缀

在我们的例子中,Example 资源将在 /example 处提供(蓝图名称 + URL 后缀)。

注意

如果您需要将资源添加到现有端点,只需将其添加到列表中即可。

警告

如果您创建了一个新模块,则必须将其添加到 cloudkitty/api/v2/__init__.py 中的 API_MODULES

API_MODULES = [
    'cloudkitty.api.v2.example',
]

记录您的端点

v2 API 使用 os_api_ref 进行文档记录。每个 v2 API 端点必须在 doc/source/api-reference/v2/<endpoint_name>/ 中记录。