File: tutorial.rst

package info (click to toggle)
cloudkitty 23.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,528 kB
  • sloc: python: 21,803; sh: 528; makefile: 226; pascal: 54
file content (330 lines) | stat: -rw-r--r-- 9,806 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
====================================
 Tutorial: creating an API endpoint
====================================

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.

Setting up the layout for a new resource
========================================

In this section, we will create an ``example`` endpoint. Create the following
files and subdirectories in ``cloudkitty/api/v2/``:

.. code-block:: console

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


Creating a custom resource
==========================

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:

          * A ``MyResource`` resource with support for ``GET``, ``POST``
            and ``PUT`` HTTP methods on the ``/myresource/<uuid:>`` route.

          * A ``MyResourceList`` resource with support for the ``GET`` HTTP
            method on the ``/myresource`` route.

          * A blueprint containing these resources.


Basic resource
--------------

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``:

.. code-block:: python

   from cloudkitty.api.v2 import base


   class Example(base.BaseResource):

       def get(self):
           pass

       def post(self):
           pass


Validating a method's parameters and output
-------------------------------------------

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.

.. autofunction:: cloudkitty.api.v2.utils.add_output_schema
   :noindex:

Let's update our ``get`` method in order to use this decorator:

.. code-block:: python

   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 {}


.. note:: In this snippet, ``get_string_type`` returns ``basestring`` in
          python2 and ``str`` in python3.

.. code-block:: console

   $ 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:

.. autofunction:: cloudkitty.api.v2.utils.add_input_schema
   :noindex:

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.

.. code-block:: python

   @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,
       }


Here, ``fruit`` is expected to be found in the request body:

.. code-block:: console

   $ 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:

.. code-block:: python

   @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.

.. autoclass:: cloudkitty.api.v2.utils.SingleQueryParam
   :noindex:

.. warning:: ``SingleQueryParam`` uses ``voluptuous.Coerce`` internally for
             type checking. Thus, ``validation_utils.get_string_type`` cannot
             be used as ``basestring`` can't be instantiated.


Authorising methods
-------------------

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:

.. code-block:: python

   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``:

.. code-block:: python

   # [...]
   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:

.. code-block:: python

   # [...]
   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', {})
           # [...]


Loading drivers
---------------

Most of the time, resources need to load some drivers (storage, SQL...).
As the instantiation of these drivers can take some time, this should be done
only once.

Some drivers (like the storage driver) are loaded in ``BaseResource`` and are
thus available to all resources.

Resources requiring some additional drivers should implement the ``reload``
function:

.. code-block:: python

   class BaseResource(flask_restful.Resource):

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


Here's an example taken from ``cloudkitty.api.v2.scope.state.ScopeState``:

.. code-block:: python

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


Registering resources
=====================

Each endpoint should provide an ``init`` method taking a Flask app as only
parameter. This method should call ``do_init``:

.. autofunction:: cloudkitty.api.v2.utils.do_init
   :noindex:

Add the following to ``cloudkitty/api/v2/example/__init__.py``:

.. code-block:: python

   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 class
* ``resource_class``: class of the resource
* ``url``: url suffix

In 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``:

             .. code-block:: python

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


Documenting your endpoint
=========================

The v2 API is documented with `os_api_ref`_ . Each v2 API endpoint must be
documented in ``doc/source/api-reference/v2/<endpoint_name>/``.

.. _os_api_ref: https://docs.openstack.org/os-api-ref/latest/