placement/doc/source/contributor/placement.rst
2018-02-07 02:19:41 +00:00

408 lines
20 KiB
ReStructuredText

..
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.
===============================
Placement API Developer Notes
===============================
Overview
========
The Nova project introduced the :doc:`placement service </user/placement>` as
part of the Newton release. The service provides an HTTP API to manage
inventories of different classes of resources, such as disk or virtual cpus,
made available by entities called resource providers. Information provided
through the placement API is intended to enable more effective accounting of
resources in an OpenStack deployment and better scheduling of various entities
in the cloud.
The document serves to explain the architecture of the system and to provide
some guidance on how to maintain and extend the code. For more detail on why
the system was created and how it does its job see :doc:`/user/placement`.
Big Picture
===========
The placement service is straightforward: It is a `WSGI`_ application that
sends and receives JSON, using an RDBMS (usually MySQL) for persistence.
As state is managed solely in the DB, scaling the placement service is done by
increasing the number of WSGI application instances and scaling the RDBMS using
traditional database scaling techniques.
For sake of consistency and because there was initially intent to make the
entities in the placement service available over RPC, `versioned objects`_ are
used to provide the interface between the HTTP application layer and the
SQLAlchemy-driven persistence layer. Even without RPC, these objects provide
useful structuring and separation of the code.
Though the placement service doesn't aspire to be a `microservice` it does
aspire to continue to be small and minimally complex. This means a relatively
small amount of middleware that is not configurable, and a limited number of
exposed resources where any given resource is represented by one (and only
one) URL that expresses a noun that is a member of the system. Adding
additional resources should be considered a significant change requiring robust
review from many stakeholders.
The set of HTTP resources represents a concise and constrained grammar for
expressing the management of resource providers, inventories, resource classes
and allocations. If a solution is initially designed to need more resources or
a more complex grammar that may be a sign that we need to give our goals
greater scrutiny. Is there a way to do what we want with what we have already?
Can some other service help? Is a new collaborating service required?
Minimal Framework
=================
The API is set up to use a minimal framework that tries to keep the structure
of the application as discoverable as possible and keeps the HTTP interaction
near the surface. The goal of this is to make things easy to trace when
debugging or adding functionality.
Functionality which is required for every request is handled in raw WSGI
middleware that is composed in the `nova.api.openstack.placement.deploy`
module. Dispatch or routing is handled declaratively via the
``ROUTE_DECLARATIONS`` map defined in the
`nova.api.openstack.placement.handler` module.
Mapping is by URL plus request method. The destination is a complete WSGI
application, using a subclass of the `wsgify`_ method from `WebOb`_ to provide
a `Request`_ object that provides convenience methods for accessing request
headers, bodies, and query parameters and for generating responses. In the
placement API these mini-applications are called `handlers`. The `wsgify`
subclass is provided in `nova.api.openstack.placement.wsgi_wrapper` as
`PlacementWsgify`. It is used to make sure that JSON formatted error responses
are structured according to the API-WG `errors`_ guideline.
This division between middleware, dispatch and handlers is supposed to
provide clues on where a particular behavior or functionality should be
implemented. Like most such systems, this doesn't always work but is a useful
tool.
Gotchas
=======
This section tries to shed some light on some of the differences between the
placement API and some of the nova APIs or on situations which may be
surprising or unexpected.
* The placement API is somewhat more strict about `Content-Type` and `Accept`
headers in an effort to follow the HTTP RFCs.
If a user-agent sends some JSON in a `PUT` or `POST` request without a
`Content-Type` of `application/json` the request will result in an error.
If a `GET` request is made without an `Accept` header, the response will
default to being `application/json`.
If a request is made with an explicit `Accept` header that does not include
`application/json` then there will be an error and the error will attempt to
be in the requested format (for example, `text/plain`).
* If a URL exists, but a request is made using a method that that URL does not
support, the API will respond with a `405` error. Sometimes in the nova APIs
this can be a `404` (which is wrong, but understandable given the constraints
of the code).
* Because each handler is individually wrapped by the `PlacementWsgify`
decorator any exception that is a subclass of `webob.exc.WSGIHTTPException`
that is raised from within the handler, such as `webob.exc.HTTPBadRequest`,
will be caught by WebOb and turned into a valid `Response`_ containing
headers and body set by WebOb based on the information given when the
exception was raised. It will not be seen as an exception by any of the
middleware in the placement stack.
In general this is a good thing, but it can lead to some confusion if, for
example, you are trying to add some middleware that operates on exceptions.
Other exceptions that are not from `WebOb`_ will raise outside the handlers
where they will either be caught in the `__call__` method of the
`PlacementHandler` app that is responsible for dispatch, or by the
`FaultWrap` middleware.
Microversions
=============
The placement API makes use of `microversions`_ to allow the release of new
features on an opt in basis. See :doc:`/user/placement` for an up to date
history of the available microversions.
The rules around when a microversion is needed are the same as for the
:doc:`compute API </contributor/microversions>`. When adding a new microversion
there are a few bits of required housekeeping that must be done in the code:
* Update the ``VERSIONS`` list in
``nova/api/openstack/placement/microversion.py`` to indicate the new
microversion and give a very brief summary of the added feature.
* Update ``nova/api/openstack/placement/rest_api_version_history.rst``
to add a more detailed section describing the new microversion.
* Add a `release note`_ with a ``features`` section announcing the new or
changed feature and the microversion.
* If the ``version_handler`` decorator (see below) has been used,
increment ``TOTAL_VERSIONED_METHODS`` in
``nova/tests/unit/api/openstack/placement/test_microversion.py``.
This provides a confirmatory check just to make sure you're paying
attention and as a helpful reminder to do the other things in this
list.
* Include functional gabbi tests as appropriate (see `Using Gabbi`_). At the
least, update the ``latest microversion`` test in
``nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml``.
* Update the `API Reference`_ documentation as appropriate. The source is
located under `placement-api-ref/source/`.
In the placement API, microversions only use the modern form of the
version header::
OpenStack-API-Version: placement 1.2
If a valid microversion is present in a request it will be placed,
as a ``Version`` object, into the WSGI environment with the
``placement.microversion`` key. Often, accessing this in handler
code directly (to control branching) is the most explicit and
granular way to have different behavior per microversion. A
``Version`` instance can be treated as a tuple of two ints and
compared as such or there is a ``matches`` method.
In other cases there are some helper methods in the microversion
package:
* The ``raise_http_status_code_if_not_version`` utility will raise a
http status code if the requested microversion is not within a
described version window.
* The ``version_handler`` decorator makes it possible to have
multiple different handler methods of the same (fully-qualified by
package) name, each available for a different microversion window.
If a request wants a microversion that's not available, a 404
response is returned. There is a unit test in place which will
fail if there are version intersections.
Adding a New Handler
====================
Adding a new URL or a new method (e.g, ``PATCH``) to an existing URL
requires adding a new handler function. In either case a new microversion and
release note is required. When adding an entirely new route a request for a
lower microversion should return a ``404``. When adding a new method to an
existing URL a request for a lower microversion should return a ``405``.
In either case, the ``ROUTE_DECLARATIONS`` dictionary in the
`nova.api.openstack.placement.handler` module should be updated to point to a
function within a module that contains handlers for the type of entity
identified by the URL. Collection and individual entity handlers of the same
type should be in the same module.
As mentioned above, the handler function should be decorated with
``@wsgi_wrapper.PlacementWsgify``, take a single argument ``req`` which is a
WebOb `Request`_ object, and return a WebOb `Response`_.
For ``PUT`` and ``POST`` methods, request bodies are expected to be JSON
based on a content-type of ``application/json``. This may be enforced by using
a decorator: ``@util.require_content('application/json')``. If the body is not
`JSON`, a ``415`` response status is returned.
Response bodies are usually `JSON`. A handler can check the `Accept` header
provided in a request using another decorator:
``@util.check_accept('application/json')``. If the header does not allow
`JSON`, a ``406`` response status is returned.
If a hander returns a response body, a ``Last-Modified`` header should be
included with the response. If the entity or entities in the response body
are directly associated with an object (or objects, in the case of a
collection response) that has an ``updated_at`` (or ``created_at``)
field, that field's value can be used as the value of the header (WebOb will
take care of turning the datetime object into a string timestamp). A
``util.pick_last_modified`` is available to help choose the most recent
last-modified when traversing a collection of entities.
If there is no directly associated object (for example, the output is the
composite of several objects) then the ``Last-Modified`` time should be
``timeutils.utcnow(with_timezone=True)`` (the timezone must be set in order
to be a valid HTTP timestamp). For example, the response__ to
``GET /allocation_candidates`` should have a last-modified header of now
because it is composed from queries against many different database entities,
presents a mixture of result types (allocation requests and provider
summaries), and has a view of the system that is only meaningful *now*.
__ https://developer.openstack.org/api-ref/placement/#list-allocation-candidates
If a ``Last-Modified`` header is set, then a ``Cache-Control`` header with a
value of ``no-cache`` must be set as well. This is to avoid user-agents
inadvertently caching the responses.
`JSON` sent in a request should be validated against a JSON Schema. A
``util.extract_json`` method is available. This takes a request body and a
schema. If multiple schema are used for different microversions of the same
request, the caller is responsible for selecting the right one before calling
``extract_json``.
When a handler needs to read or write the data store it should use methods on
the objects found in the `nova.objects.resource_provider` package. Doing so
requires a context which is provided to the handler method via the WSGI
environment. It can be retrieved as follows::
context = req.environ['placement.context']
.. note:: If your change requires new methods or new objects in the
`resource_provider` package, after you've made sure that you really
do need those new methods or objects (you may not!) make those
changes in a patch that is separate from and prior to the HTTP API
change.
Testing of handler code is described in the next section.
Testing
=======
Most of the handler code in the placement API is tested using `gabbi`_. Some
utility code is tested with unit tests found in
`nova/tests/unit/api/openstack/placement/`. The back-end objects are tested
with a combination of unit and functional tests found in
``nova/tests/unit/objects/test_resource_provider.py`` and
`nova/tests/functional/db`. Adding unit and non-gabbi functional tests is done
in the same way as other aspects of nova.
Using Gabbi
-----------
Gabbi was developed in the `telemetry`_ project to provide a declarative way to
test HTTP APIs that preserves visibility of both the request and response of
the HTTP interaction. Tests are written in YAML files where each file is an
ordered suite of tests. Fixtures (such as a database) are set up and torn down
at the beginning and end of each file, not each test. JSON response bodies can
be evaluated with `JSONPath`_. The placement WSGI
application is run via `wsgi-intercept`_, meaning that real HTTP requests are
being made over a file handle that appears to Python to be a socket.
In the placement API the YAML files (aka "gabbits") can be found in
``nova/tests/functional/api/openstack/placement/gabbits``. Fixture definitions are
in ``fixtures.py`` in the parent directory. Tests are currently grouped by handlers
(e.g., ``resource-provider.yaml`` and ``inventory.yaml``). This is not a
requirement and as we increase the number of tests it makes sense to have more
YAML files with fewer tests, divided up by the arc of API interaction that they
test.
The gabbi tests are integrated into the functional tox target, loaded via
``nova/tests/functional/api/openstack/placement/test_placement_api.py``. If you
want to run just the gabbi tests one way to do so is::
tox -efunctional test_placement_api
If you want to run just one yaml file (in this example ``inventory.yaml``)::
tox -efunctional placement_api.inventory
It is also possible to run just one test from within one file. When you do this
every test prior to the one you asked for will also be run. This is because
the YAML represents a sequence of dependent requests. Select the test by using
the name in the yaml file, replacing space with ``_``::
tox -efunctional placement_api.inventory_post_new_ipv4_address_inventory
.. note:: `.testr.conf` in the nova repository is configured such that each
gabbi YAML is considered a group. Thus, all tests in the file will
be run in the same process when running testr concurrently (the
default).
Writing More Gabbi Tests
------------------------
The docs for `gabbi`_ try to be complete and explain the `syntax`_ in some
depth. Where something is missing or confusing, please log a `bug`_.
While it is possible to test all aspects of a response (all the response
headers, the status code, every attribute in a JSON structure) in one single
test, doing so will likely make the test harder to read and will certainly make
debugging more challenging. If there are multiple things that need to be
asserted, making multiple requests is reasonable. Since database set up is only
happening once per file (instead of once per test) and since there's no TCP
overhead, the tests run quickly.
While `fixtures`_ can be used to establish entities that are required for
tests, creating those entities via the HTTP API results in tests which are more
descriptive. For example the ``inventory.yaml`` file creates the resource
provider to which it will then add inventory. This makes it easy to explore a
sequence of interactions and a variety of responses with the tests:
* create a resource provider
* confirm it has empty inventory
* add inventory to the resource provider (in a few different ways)
* confirm the resource provider now has inventory
* modify the inventory
* delete the inventory
* confirm the resource provider now has empty inventory
Nothing special is required to add a new set of tests: create a YAML file with
a unique name in the same directory as the others. The other files can provide
examples. Gabbi can provide a useful way of doing test driven development of a
new handler: create a YAML file that describes the desired URLs and behavior
and write the code to make it pass.
It's also possible to use gabbi against a running placement service, for
example in devstack. See `gabbi-run`_ to get started.
Futures
=======
Since before it was created there has been a long term goal for the placement
service to be extracted to its own repository and operate as its own
independent service. There are many reasons for this, but two main ones are:
* Multiple projects, not just nova, will eventually need to manage resource
providers using the placement API.
* A separate service helps to maintain and preserve a strong contract between
the placement service and the consumers of the service.
To lessen the pain of the eventual extraction of placement the service has been
developed in a way to limit dependency on the rest of the nova codebase and be
self-contained:
* Most code is in `nova/api/openstack/placement` except for oslo versioned
object code in ``nova/objects/resource_provider.py``.
* Database query code is kept within the objects.
* The methods on the objects are not remotable, as the only intended caller is
the placement API code.
There are some exceptions to the self-contained rule (which will have to be
addressed if the extraction ever happens):
* Exceptions unique to the placement API are still within the `nova.exceptions`
package.
* Code related to a resource class cache is within the `nova.db` package.
* Database models, migrations and tables use the nova api database.
* The nova `FaultWrapper` middleware is being used.
* `nova.i18n` package provides the ``_`` and related functions.
* ``nova.conf`` is used for configuration.
* Unit and functional tests depend on fixtures and other functionality in base
classes provided by nova.
When creating new code for the placement service, please be aware of the plan
for an eventual extraction and avoid creating unnecessary interdependencies.
.. _WSGI: https://www.python.org/dev/peps/pep-3333/
.. _versioned objects: http://docs.openstack.org/developer/oslo.versionedobjects/
.. _wsgify: http://docs.webob.org/en/latest/api/dec.html
.. _WebOb: http://docs.webob.org/en/latest/
.. _Request: http://docs.webob.org/en/latest/reference.html#request
.. _Response: http://docs.webob.org/en/latest/#response
.. _microversions: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html
.. _release note: https://docs.openstack.org/reno/latest/user/usage.html
.. _gabbi: https://gabbi.readthedocs.io/
.. _telemetry: http://specs.openstack.org/openstack/telemetry-specs/specs/kilo/declarative-http-tests.html
.. _wsgi-intercept: http://wsgi-intercept.readthedocs.io/
.. _syntax: https://gabbi.readthedocs.io/en/latest/format.html
.. _bug: https://github.com/cdent/gabbi/issues
.. _fixtures: http://gabbi.readthedocs.io/en/latest/fixtures.html
.. _JSONPath: http://goessner.net/articles/JsonPath/
.. _gabbi-run: http://gabbi.readthedocs.io/en/latest/runner.html
.. _errors: http://specs.openstack.org/openstack/api-wg/guidelines/errors.html
.. _API Reference: https://developer.openstack.org/api-ref/placement/