keystoneauth/doc/source/using-sessions.rst

600 lines
25 KiB
ReStructuredText

==============
Using Sessions
==============
Introduction
============
The :py:class:`keystoneauth1.session.Session` class was introduced into
keystoneauth1 as an attempt to bring a unified interface to the various
OpenStack clients that share common authentication and request parameters
between a variety of services.
The model for using a Session and auth plugin as well as the general terms used
have been heavily inspired by the `requests <http://docs.python-requests.org>`_
library. However neither the Session class nor any of the authentication
plugins rely directly on those concepts from the requests library so you should
not expect a direct translation.
Features
--------
- Common client authentication
Authentication is handled by one of a variety of authentication plugins and
then this authentication information is shared between all the services that
use the same Session object.
- Security maintenance
Security code is maintained in a single place and reused between all
clients such that in the event of problems it can be fixed in a single
location.
- Standard service and version discovery
Clients are not expected to have any knowledge of an identity token or any
other form of identification credential. Service, endpoint, major version
discovery, and microversion support discovery are handled by the Session and
plugins. Discovery information is automatically cached in memory, so the user
need not worry about excessive use of discovery metadata.
- Safe logging of HTTP interactions
Clients need to be able to enable logging of the HTTP interactions, but some
things, such as the token or secrets, need to be ommitted.
Sessions for Users
==================
The Session object is the contact point to your OpenStack cloud services. It
stores the authentication credentials and connection information required to
communicate with OpenStack such that it can be reused to communicate with many
services. When creating services this Session object is passed to the client
so that it may use this information.
A Session will authenticate on demand. When a request that requires
authentication passes through the Session the authentication plugin will be
asked for a valid token. If a valid token is available it will be used
otherwise the authentication plugin may attempt to contact the authentication
service and fetch a new one.
An example using keystoneclient to wrap a Session::
>>> from keystoneauth1.identity import v3
>>> from keystoneauth1 import session
>>> from keystoneclient.v3 import client
>>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3',
... username='myuser',
... password='mypassword',
... project_name='proj',
... user_domain_id='default',
... project_domain_id='default')
>>> sess = session.Session(auth=auth,
... verify='/path/to/ca.cert')
>>> ks = client.Client(session=sess)
>>> users = ks.users.list()
As other OpenStack client libraries adopt this means of operating they will be
created in a similar fashion by passing the Session object to the client's
constructor.
Sharing Authentication Plugins
------------------------------
A Session can only contain one authentication plugin. However, there is
nothing that specifically binds the authentication plugin to that Session - a
new Session can be created that reuses the existing authentication plugin::
>>> new_sess = session.Session(auth=sess.auth,
verify='/path/to/different-cas.cert')
In this case we cannot know which Session object will be used when the plugin
performs the authentication call so the command must be able to succeed with
either.
Authentication plugins can also be provided on a per-request basis. This will
be beneficial in a situation where a single Session is juggling multiple
authentication credentials::
>>> sess.get('https://my.keystone.com:5000/v3',
auth=my_auth_plugin)
If an auth plugin is provided via parameter then it will override any auth
plugin on the Session.
Sessions for Client Developers
==============================
Sessions are intended to take away much of the hassle of dealing with
authentication data and token formats. Clients should be able to specify filter
parameters for selecting the endpoint and have the parsing of the catalog
managed for them.
Major Version Discovery and Microversion Support
------------------------------------------------
In OpenStack, the root URLs of available services are distributed to the user
in an object called the Service Catalog, which is part of the token they
receive. Clients are expected to use the URLs from the Service Catalog rather
than have them provided. The root URL of a given service is referred to as the
`endpoint` of the service. The URL of a specific version of a service is
referred to as a `versioned endpoint`. REST requests for a service are made
against a given `versioned endpoint`.
The topic of Major API versions and microversions can be confusing. As
`keystoneauth` provides facilities for discovery of versioned endpoints
associated with a Major API Version and for fetching information about
the microversions that versioned endpoint supports, it is important to be aware
of the distinction between the two.
Conceptually the most important thing to understand is that a Major API Version
describes the URL of a discrete versioned endpoint, while a given versioned
endpoint might have properties that express that it supports a range of
microversions.
When a user wants to make a REST request against a service, the user expresses
the Major API version and the type of service so that the appropriate versioned
endpoint can be found and used. For example, a user might request
version 2 of the compute service from cloud.example.com and end up with a
versioned endpoint of ``https://compute.example.com/v2``.
Each service provides a discovery document at the root of each versioned
endpoint that contains information about that versioned endpoint. Each service
also provides a document at the root of the unversioned endpoint that contains
a list of the discovery documents for all of the available versioned endpoints.
By examining these documents, it is possible to find the versioned endpoint
that corresponds with the user's desired Major API version.
Each of those documents may also indicate that the given versioned endpoint
supports microversions by listing a minimum and maximum microversion that it
understands. As a result of having found the versioned endpoint for the
requested Major API version, the user will also know which microversions,
if any, may be used in requests to that versioned endpoint.
When a client makes REST requests to the Major API version's endpoint, the
client can, optionally, on a request-by-request basis, include a header
specifying that the individual request use the behavior defined by the given
microversion. If a client does not request a microversion, the service will
behave as if the minimum supported microversion was specified.
.. note: The changes that each microversion reflects are documented elsewhere
and are not information provided by the discovery process.
The overall transaction then has three parts:
* What is the endpoint for a given Major API version of a given service?
* What are the minimum and maximum microversions supported at that endpoint?
* Which one of that range of microversions, if any, does the user want to use
for a given request?
`keystoneauth` provides facilities for discovering the endpoint for a given
Major API of a given service, as well as reporting the available microversion
ranges that endpoint supports, if any.
More information is available in the `API-WG Specs`_ on the topics of
`Microversions`_ and `Consuming the Catalog`_.
Authentication
--------------
When making a request with a Session object you can simply pass the keyword
parameter ``authenticated`` to indicate whether the argument should contain a
token, by default a token is included if an authentication plugin is available::
>>> # In keystone this route is unprotected by default
>>> resp = sess.get('https://my.keystone.com:5000/v3',
authenticated=False)
Service Discovery
-----------------
In general a client does not need to know the full URL for the server that they
are communicating with, simply that it should send a request to a path
belonging to the correct service.
This is controlled by the ``endpoint_filter`` parameter to a request which
contains all the information an authentication plugin requires to determine the
correct URL to which to send a request. When using this mode only the path for
the request needs to be specified::
>>> resp = session.get('/users',
endpoint_filter={'service_type': 'identity',
'interface': 'admin',
'region_name': 'myregion',
'min_version': '2.0',
'max_version': '3.4',
'discover_versions': False})
.. note:: The min_version and max_version arguments in this example indicate
acceptable range for finding the endpoint for the given Major API
versions. They are in the endpoint_filter, they are not requesting
the call to ``/users`` be made at a specific microversion.
`endpoint_filter` accepts a number of arguments with which it can determine an
endpoint url:
service_type
the type of service. For example ``identity``, ``compute``, ``volume`` or
many other predefined identifiers.
interface
the network exposure the interface has. Can also be a list, in which case the
first matching interface will be used. Valid values are:
- ``public``: An endpoint that is available to the wider internet or network.
- ``internal``: An endpoint that is only accessible within the private
network.
- ``admin``: An endpoint to be used for administrative tasks.
region_name
the name of the region where the endpoint resides.
version
the minimum version, restricted to a given Major API. For instance, a
`version` of ``2.2`` will match ``2.2`` and ``2.3`` but not ``2.1`` or
``3.0``. Mutually exclusive with `min_version` and `max_version`.
min_version
the minimum version of a given API, intended to be used as the lower bound of
a range with `max_version`. See `max_version` for examples. Mutually
exclusive with `version`.
max_version
the maximum version of a given API, intended to be used as the upper bound of
a range with `min_version`. For example::
'min_version': '2.2',
'max_version': '3.3'
will match ``2.2``, ``2.10``, ``3.0``, and ``3.3``, but not ``1.42``,
``2.1``, or ``3.20``. Mutually exclusive with `version`.
.. note:: version, min_version and max_version are all used to help determine
the endpoint for a given Major API version of a service.
discover_versions
whether or not version discovery should be run, even if not strictly
necessary. It is often possible to fulfill an endpoint request purely
from the catalog, meaning the version discovery API is a potentially
wasted additional call. However, it's possible that running discovery
instead of inference is desired. Defaults to ``True``.
All version arguments (`version`, `min_version` and `max_version`) can
be given as:
* string: ``'2.0'``
* int: ``2``
* float: ``2.0``
* tuple of ints: ``(2, 0)``
`version` and `max_version` can also be given the string ``latest``, which
indicates that the highest available version should be used.
The endpoint filter is a simple key-value filter and can be provided with any
number of arguments. It is then up to the auth plugin to correctly use the
parameters it understands.
If you want to further limit your service discovery by allowing experimental
APIs or disallowing deprecated APIs, you can use the ``allow`` parameter::
>>> resp = session.get('/<project-id>/volumes',
endpoint_filter={'service_type': 'volume',
'interface': 'public',
'version': 1},
allow={'allow_deprecated': False})
The discoverable types of endpoints that `allow` can recognize are:
- `allow_deprecated`: Allow deprecated version endpoints.
- `allow_experimental`: Allow experimental version endpoints.
- `allow_unknown`: Allow endpoints with an unrecognised status.
The Session object creates a valid request by determining the URL matching the
filters and appending it to the provided path. If multiple URL matches are
found then any one may be chosen.
While authentication plugins will endeavour to maintain a consistent set of
arguments for an ``endpoint_filter`` the concept of an authentication plugin is
purposefully generic. A specific mechanism may not know how to interpret
certain arguments in which case it may ignore them. For example the
:class:`keystoneauth1.token_endpoint.Token` plugin (which is used when you want
to always use a specific endpoint and token combination) will always return the
same endpoint regardless of the parameters to ``endpoint_filter`` or a custom
OpenStack authentication mechanism may not have the concept of multiple
``interface`` options and choose to ignore that parameter.
There is some expectation on the user that they understand the limitations of
the authentication system they are using.
Using Adapters
--------------
If the developer would prefer not to provide `endpoint_filter` with every API
call, a :class:`keystoneauth1.adapter.Adapter` can be created. The `Adapter`
constructor takes the same arguments as `endpoint_filter`, as well as a
`Session`. An `Adapter` behaves much like a `Session`, with the same REST
methods, but is "mounted" on the endpoint that would be found by
`endpoint_filter`.
.. code-block:: python
adapter = keystoneauth1.adapter.Adapter(
session=session,
service_type='volume',
interface='public',
version=1)
response = adapter.get('/volumes')
As with ``endpoint_filter`` on a Session, the ``version``, ``min_version``
and ``max_version`` parameters exist to help determine the appropriate
endpoint for a Major API of a service.
Endpoint Metadata
-----------------
Both :class:`keystoneauth1.adapter.Adapter` and
:class:`keystoneauth1.session.Session` have a method for getting metadata about
the endpoint found for a given service: ``get_endpoint_data``.
On the :class:`keystoneauth1.session.Session` it takes the same arguments as
`endpoint_filter`.
On the :class:`keystoneauth1.adapter.Adapter` it does not take arguments, as
it returns the information for the Endpoint the Adapter is mounted on.
``get_endpoint_data`` returns an :class:`keystoneauth1.discovery.EndpointData`
object. This object can be used to find information about the Endpoint,
including which major `api_version` was found, or which `interface` in case
of ranges, lists of input values or ``latest`` version.
It can also be used to determine the `min_microversion` and `max_microversion`
supported by the API. If an API does not support microversions, the values for
both will be ``None``. It will also contain values for `next_min_version` and
`not_before` if they exist for the endpoint, or ``None`` if they do not. The
:class:`keystoneauth1.discovery.EndpointData` object will always contain
microversion related attributes regardless of whether the REST document does
or not.
``get_endpoint_data`` makes use of the same cache as the rest of the discovery
process, so calling it should incur no undue expense. By default it will make
at least one version discovery call so that it can fetch microversion metadata.
If the user knows a service does not support microversions and is merely
curious as to which major version was discovered, ``discover_versions`` can be
set to ``False`` to prevent fetching microversion metadata.
Requesting a Microversion
-------------------------
A user who wants to specify a microversion for a given request can pass it to
the ``microversion`` parameter of the `request` method on the
:class:`keystoneauth1.session.Session` object, or the
:class:`keystoneauth1.adapter.Adapter` object. This will cause `keystoneauth`
to pass the appropriate header to the service informing the service of the
microversion the user wants.
.. code-block:: python
resp = session.get('/volumes',
microversion='3.15',
endpoint_filter={'service_type': 'volume',
'interface': 'public',
'min_version': '3',
'max_version': 'latest'})
If the user is using a :class:`keystoneauth1.adapter.Adapter`, the
`service_type`, which is a part of the data sent in the microversion header,
will be taken from the Adapter's `service_type`.
.. code-block:: python
adapter = keystoneauth1.adapter.Adapter(
session=session,
service_type='compute',
interface='public',
min_version='2.1')
response = adapter.get('/servers', microversion='2.38')
The user can also provide a ``default_microversion`` parameter to the Adapter
constructor which will be used on all requests where an explicit microversion
is not requested.
.. code-block:: python
adapter = keystoneauth1.adapter.Adapter(
session=session,
service_type='compute',
interface='public',
min_version='2.1',
default_microversion='2.38')
response = adapter.get('/servers')
If the user is using a :class:`keystoneauth1.session.Session`, the
`service_type` will be taken from the `service_type` in `endpoint_filter`.
If the `service_type` is the incorrect value to use for the microversion header
for the service in question, the parameter `microversion_service_type` can be
given. For instance, although keystoneauth already knows about Cinder, the
`service_type` for Cinder is ``block-storage`` but the microversion header
expects ``volume``.
.. code-block:: python
# Interactions with cinder do not need to explicitly override the
# microversion_service_type - it is only being used as an example for the
# use of the parameter.
resp = session.get('/volumes',
microversion='3.15',
microversion_service_type='volume',
endpoint_filter={'service_type': 'block-storage',
'interface': 'public',
'min_version': '3',
'max_version': 'latest'})
Logging
=======
The logging system uses standard `python logging`_ rooted on the
``keystoneauth`` namespace as would be expected. There are two possibilities
of where log messages about HTTP interactions will go.
By default, all messages will go to the ``keystoneauth.session`` logger.
If the ``split_loggers`` option on the :class:`keystoneauth1.session.Session`
constructor is set to ``True``, the HTTP content will be split across four
subloggers to allow for fine-grained control of what is logged and how:
keystoneauth.session.request-id
Emits a log entry at the ``DEBUG`` level for every http request
including information about the URL, ``service-type`` and ``request-id``.
keystoneauth.session.request
Emits a log entry at the ``DEBUG`` level for every http request including a
curl formatted string of the request.
keystoneauth.session.response
Emits a log entry at the ``DEBUG`` level for every http response received,
including the status code, and the headers received.
keystoneauth.session.body
Emits a log entry at the ``DEBUG`` level containing the contents of the
response body if the ``content-type`` is either ``text`` or ``json``.
Using loggers
-------------
A full description of how to consume `python logging`_ is out of scope of this
document, but a few simple examples are provided.
If you would like to configure logging to log keystoneuath at the ``INFO``
level with no ``DEBUG`` messages:
.. code-block:: python
import keystoneauth1
import logging
logger = logging.getLogger('keystoneauth')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)
If you would like to get a full HTTP debug trace including bodies:
.. code-block:: python
import keystoneauth1
import logging
logger = logging.getLogger('keystoneauth')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
If you would like to get a full HTTP debug trace bug with no bodies:
.. code-block:: python
import keystoneauth1
import keystoneauth1.session
import logging
logger = logging.getLogger('keystoneauth')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
body_logger = logging.getLogger('keystoneauth.session.body')
body_logger.setLevel(logging.WARN)
session = keystoneauth1.session.Session(split_loggers=True)
Finally, if you would like to log request-ids and response headers to one file,
request commands, response headers and response bodies to a different file,
and everything else to the console:
.. code-block:: python
import keystoneauth1
import keystoneauth1.session
import logging
# Create a handler that outputs only outputs INFO level messages to stdout
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
# Configure the default behavior of all keystoneauth logging to log at the
# INFO level.
logger = logging.getLogger('keystoneauth')
logger.setLevel(logging.INFO)
# Emit INFO messages from all keystoneauth loggers to stdout
logger.addHandler(stream_handler)
# Create an output formatter that includes logger name and timestamp.
formatter = logging.Formatter('%(asctime)s %(name)s %(message)s')
# Create a file output for request ids and response headers
request_handler = logging.FileHandler('request.log')
request_handler.setFormatter(formatter)
# Create a file output for request commands, response headers and bodies
body_handler = logging.FileHandler('response-body.log')
body_handler.setFormatter(formatter)
# Log all HTTP interactions at the DEBUG level
session_logger = logging.getLogger('keystoneauth.session')
session_logger.setLevel(logging.DEBUG)
# Emit request ids to the request log
request_id_logger = logging.getLogger('keystoneauth.session.request-id')
request_id_logger.addHandler(request_handler)
# Emit response headers to both the request log and the body log
header_logger = logging.getLogger('keystoneauth.session.response')
header_logger.addHandler(request_handler)
header_logger.addHandler(body_handler)
# Emit request commands to the body log
request_logger = logging.getLogger('keystoneauth.session.request')
request_logger.addHandler(body_handler)
# Emit bodies only to the body log
body_logger = logging.getLogger('keystoneauth.session.body')
body_logger.addHandler(body_handler)
session = keystoneauth1.session.Session(split_loggers=True)
The above will produce messages like the following in request.log:
::
2017-09-19 22:10:09,466 keystoneauth.session.request-id GET call to volumev2 for http://cloud.example.com/volume/v2/137155c35fb34172a284a3c2540c92ab/volumes/detail used request id req-f4f2058a-9308-4c4a-94e6-5ee1cd6c78bd
2017-09-19 22:10:09,751 keystoneauth.session.response [200] Date: Tue, 19 Sep 2017 22:10:09 GMT Server: Apache/2.4.18 (Ubuntu) x-compute-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Content-Type: application/json Content-Length: 15 x-openstack-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Connection: close
And content like the following into response-body.log:
::
2017-09-19 22:10:09,490 keystoneauth.session.request curl -g -i -X GET http://cloud.example.com/volume/v2/137155c35fb34172a284a3c2540c92ab/volumes/detail?marker=34cd00cf-bf67-4667-a900-5ce233e383d5 -H "User-Agent: os-client-config/1.28.0 shade/1.23.1 keystoneauth1/3.2.0 python-requests/2.18.4 CPython/2.7.12" -H "X-Auth-Token: {SHA1}a1d03d2a4cbee590a55f1786d452e1027d5fd781"
2017-09-19 22:10:09,751 keystoneauth.session.response [200] Date: Tue, 19 Sep 2017 22:10:09 GMT Server: Apache/2.4.18 (Ubuntu) x-compute-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Content-Type: application/json Content-Length: 15 x-openstack-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Connection: close
2017-09-19 22:10:09,751 keystoneauth.session.body {"volumes": []}
User Provided Loggers
---------------------
The HTTP methods (request, get, post, put, etc) on
`keystoneauth1.session.Session` and `keystoneauth1.adapter.Adapter` all support
a ``logger`` parameter. A user can provide their own `logger`_ which will
override the session loggers mentioned above. If a single logger is provided
in this manner, request, response and body content will all be logged to that
logger at the ``DEBUG`` level, and the strings ``REQ:``, ``RESP:`` and
``RESP BODY:`` will be pre-pended as appropriate.
.. _API-WG Specs: https://specs.openstack.org/openstack/api-wg/
.. _Consuming the Catalog: https://specs.openstack.org/openstack/api-wg/guidelines/consuming-catalog.html
.. _Microversions: https://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery
.. _python logging: https://docs.python.org/3/library/logging.html
.. _logger: https://docs.python.org/3/library/logging.html#logging.Logger