This section of the document details how to create an endpoint for CloudKitty’s v2 API. The v1 API is frozen, no endpoint should be added.
In this section, we will create an example
endpoint. Create the following
files and subdirectories in cloudkitty/api/v2/
:
cloudkitty/api/v2/
└── example
├── example.py
└── __init__.py
Each v2 API endpoint is based on a Flask Blueprint and one Flask-RESTful
resource per sub-endpoint. This allows to have a logical grouping of the
resources. Let’s take the /rating/hashmap
route as an example. Each of
the hashmap module’s resources should be a Flask-RESTful resource (eg.
/rating/hashmap/service
, /rating/hashmap/field
etc…).
Note
There should be a distinction between endpoints refering to a single resource and to several ones. For example, if you want an endpoint allowing to list resources of some kind, you should implement the following:
MyResource
resource with support for GET
, POST
and PUT
HTTP methods on the /myresource/<uuid:>
route.MyResourceList
resource with support for the GET``HTTP
method on the ``/myresource
route.We’ll create an /example/
endpoint, used to manipulate fruits. We’ll create
an Example
resource, supporting GET
and POST
HTTP methods. First
of all, we’ll create a class with get
and post
methods in
cloudkitty/api/v2/example/example.py
:
import flask_restful
class Example(flask_restful.Resource):
def get(self):
pass
def post(self):
pass
A GET
request on our resource will simply return {“message”: “This is an
example endpoint”}. The add_output_schema
decorator adds voluptuous
validation to a method’s output. This allows to set defaults.
cloudkitty.api.v2.utils.
add_output_schema
(schema)[source]¶Add a voluptuous schema validation on a method’s output
Example usage:
class Example(flask_restful.Resource):
@api_utils.add_output_schema({
voluptuous.Required(
'message',
default='This is an example endpoint',
): api_utils.get_string_type(),
})
def get(self):
return {}
Parameters: | schema (dict) – Schema to apply to the method’s output |
---|
Let’s update our get
method in order to use this decorator:
import flask_restful
import voluptuous
from cloudkitty.api.v2 import utils as api_utils
class Example(flask_restful.Resource):
@api_utils.add_output_schema({
voluptuous.Required(
'message',
default='This is an example endpoint',
): api_utils.get_string_type(),
})
def get(self):
return {}
Note
In this snippet, get_string_type
returns basestring
in
python2 and str
in python3.
$ curl 'http://cloudkitty-api:8889/v2/example'
{"message": "This is an example endpoint"}
It is now time to implement the post
method. This function will take a
parameter. In order to validate it, we’ll use the add_input_schema
decorator:
cloudkitty.api.v2.utils.
add_input_schema
(location, schema)[source]¶Add a voluptuous schema validation on a method’s input
Takes a dict which can be converted to a volptuous schema as parameter,
and validates the parameters with this schema. The “location” parameter
is used to specify the parameters’ location. Note that for query
parameters, a MultiDict
is returned by Flask. Thus, each dict key will
contain a list. In order to ease interaction with unique query parameters,
the SingleQueryParam
voluptuous validator can be used:
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
To accept a list of query parameters, the following syntax can be used:
from cloudkitty.api.v2 import utils as api_utils
@api_utils.add_input_schema('query', {
voluptuous.Required('fruit'): [str],
})
def put(self, fruit=[]):
for f in fruit:
# Do something with the fruit
Parameters: |
|
---|
Arguments validated by the input schema are passed as named arguments to the decorated function. Let’s implement the post method. We’ll use Werkzeug exceptions for HTTP return codes.
@api_utils.add_input_schema('body', {
voluptuous.Required('fruit'): api_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,
}
Here, fruit
is expected to be found in the request body:
$ curl -X POST -H 'Content-Type: application/json' 'http://cloudkitty-api:8889/v2/example' -d '{"fruit": "banana"}'
{"message": "Your fruit is a banana"}
In order to retrieve fruit
from the query, the function should have been
decorated like this:
@api_utils.add_input_schema('query', {
voluptuous.Required('fruit'): api_utils.SingleQueryParam(str),
})
def post(self, fruit=None):
Note that a SingleQueryParam
is used here: given that query parameters can
be specified several times (eg xxx?groupby=a&groupby=b
), Flask provides
query parameters as lists. The SingleQueryParam
helper checks that a
parameter is provided only once, and returns it.
cloudkitty.api.v2.utils.
SingleQueryParam
(param_type)[source]¶Voluptuous validator allowing to validate unique query parameters.
This validator checks that a URL query parameter is provided only once, verifies its type and returns it directly, instead of returning a list containing a single element.
Note that this validator uses voluptuous.Coerce
internally and thus
should not be used together with api_utils.get_string_type
in python2.
Parameters: | param_type – Type of the query parameter |
---|
Warning
SingleQueryParam
uses voluptuous.Coerce
internally for
type checking. Thus, api_utils.get_string_type
cannot be used
as basestring
can’t be instantiated.
The Example
resource is still missing some authorisations. We’ll create a
policy per method, configurable via the policy.yaml
file. Create a
cloudkitty/common/policies/v2/example.py
file with the following content:
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
Add the following lines to 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(),
)
This registers two documented policies, get_example
and submit_fruit
.
They are unprotected by default, which means that everybody can access them.
However, they can be overriden in policy.yaml
. Call them the following way:
# [...]
import flask
from cloudkitty.common import policy
class Example(flask_restful.Resource):
# [...]
def get(self):
policy.authorize(flask.request.context, 'example:get_example', {})
return {}
# [...]
def post(self):
policy.authorize(flask.request.context, 'example:submit_fruit', {})
# [...]
Each endpoint should provide an init
method taking a Flask app as only
parameter. This method should call do_init
:
cloudkitty.api.v2.utils.
do_init
(app, blueprint_name, resources)[source]¶Registers a new Blueprint containing one or several resources to app.
Parameters: |
|
---|
Add the following to 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
Here, we call do_init
with the flask app passed as parameter, a blueprint
name, and a list of resources. The blueprint name will prefix the URLs of all
resources. Each resource is represented by a dict with the following
attributes:
module
: name of the python module containing the resource classresource_class
: class of the resourceurl
: url suffixIn our case, the Example
resource will be served at /example
(blueprint
name + URL suffix).
Note
In case you need to add a resource to an existing endpoint, just add it to the list.
Warning
If you created a new module, you’ll have to add it to
API_MODULES
in cloudkitty/api/v2/__init__.py
:
API_MODULES = [
'cloudkitty.api.v2.example',
]
The v2 API is documented with os_api_ref . Each v2 API endpoint must be
documented in doc/source/api-reference/v2/<endpoint_name>/
.
Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. See all OpenStack Legal Documents.