使用会话

介绍

keystoneauth1 的 keystoneauth1.session.Session 类被引入 keystoneauth1,旨在为各种共享通用身份验证和请求参数的 OpenStack 客户端提供统一的接口,这些参数跨越多种服务。

使用会话和身份验证插件的模型以及使用的通用术语都深受 requests 库的启发。但是,会话类和任何身份验证插件都不直接依赖于 requests 库中的这些概念,因此您不应期望直接翻译。

特性

  • 常见的客户端身份验证

    身份验证由多种身份验证插件之一处理,然后此身份验证信息在所有使用相同会话对象的服务之间共享。

  • 安全维护

    安全代码在一个地方维护并在所有客户端之间重用,这样在出现问题时,可以在一个位置修复它。

  • 标准服务和版本发现

    客户端不需要了解身份令牌或任何其他形式的身份凭据。服务、端点、主版本发现以及微版本支持发现都由会话和插件处理。发现信息会自动缓存到内存中,因此用户无需担心过度使用发现元数据。

  • HTTP 交互的安全记录

    客户端需要能够启用 HTTP 交互的记录,但某些内容(例如令牌或密钥)需要省略。

用户会话

会话对象是与您的 OpenStack 云服务的联系点。它存储了与 OpenStack 通信所需的身份验证凭据和连接信息,以便它可以被重用于与许多服务通信。在创建服务时,会话对象会传递给客户端,以便客户端可以使用此信息。

会话将按需进行身份验证。当需要身份验证的请求通过会话时,身份验证插件将被要求提供有效的令牌。如果可用有效的令牌,将使用它,否则身份验证插件可能会尝试联系身份验证服务并获取新的令牌。

使用 keystoneclient 包装会话的示例

>>> 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()

随着其他 OpenStack 客户端库采用这种操作方式,它们将以类似的方式创建,即将会话对象传递给客户端的构造函数。

共享身份验证插件

会话只能包含一个身份验证插件。但是,没有任何内容特别将身份验证插件绑定到该会话 - 可以创建一个新的会话,该会话重用现有的身份验证插件

>>> new_sess = session.Session(auth=sess.auth,
                               verify='/path/to/different-cas.cert')

在这种情况下,我们无法知道在插件执行身份验证调用时将使用哪个会话对象,因此该命令必须能够成功执行任何一个。

身份验证插件也可以按请求提供。这在单个会话处理多个身份验证凭据的情况下会很有益

>>> sess.get('https://my.keystone.com:5000/v3',
             auth=my_auth_plugin)

如果通过参数提供了身份验证插件,它将覆盖会话上的任何身份验证插件。

客户端开发者会话

会话旨在消除处理身份验证数据和令牌格式的许多麻烦。客户端应该能够指定用于选择端点的筛选参数,并让目录的解析由会话管理。

主版本发现和微版本支持

在 OpenStack 中,可用的服务的根 URL 分发给用户在一个称为服务目录的对象中,该对象是他们收到的令牌的一部分。客户端预计会使用来自服务目录的 URL,而不是提供它们。给定服务的根 URL 被称为该服务的 端点。服务的特定版本的 URL 被称为 版本化端点。对服务的 REST 请求是在给定的 版本化端点 上发起的。

主 API 版本和微版本的主题可能令人困惑。由于 keystoneauth 提供了与主 API 版本关联的版本化端点发现以及获取该版本化端点支持的微版本信息的设施,因此了解这两者之间的区别非常重要。

从概念上讲,最重要的是要理解主 API 版本描述了离散版本化端点的 URL,而给定的版本化端点可能具有表达它支持的微版本范围的属性。

当用户想要对服务发出 REST 请求时,用户表达主 API 版本和服务类型,以便找到并使用适当的版本化端点。例如,用户可能会请求来自 cloud.example.com 的计算服务的版本 2,并最终得到一个版本化端点 https://compute.example.com/v2

每个服务在其每个版本化端点的根目录提供一个发现文档,其中包含有关该版本化端点的信息。每个服务还在未版本化端点的根目录提供一个文档,其中包含所有可用版本化端点的发现文档列表。通过检查这些文档,可以找到与用户所需的主 API 版本相对应的版本化端点。

这些文档中的每一个也可能表明给定的版本化端点通过列出它理解的最小和最大微版本来支持微版本。因此,在找到请求的主 API 版本的版本化端点之后,用户还将知道哪些微版本(如果有)可以在对该版本化端点的请求中使用。

当客户端对主 API 版本的端点发出 REST 请求时,客户端可以选择在请求的基础上包含一个标头,指定使用给定微版本定义的行为。如果客户端未请求微版本,则服务将表现得好像指定了支持的最小微版本。

总体事务然后分为三个部分

  • 给定服务的给定主 API 版本的端点是什么?

  • 该端点支持的最小和最大微版本是什么?

  • 用户希望对给定请求使用该范围内的哪个微版本(如果有)?

keystoneauth 提供了发现给定服务的主 API 的端点的设施,以及报告该端点支持的可用微版本范围(如果有)。

有关更多信息,请参阅 API-WG 规范 中关于 微版本使用目录 的主题。

身份验证

当使用会话对象发出请求时,您可以简单地传递关键字参数 authenticated 来指示参数是否应包含令牌,默认情况下,如果可用身份验证插件,则包含令牌

>>> # In keystone this route is unprotected by default
>>> resp = sess.get('https://my.keystone.com:5000/v3',
                    authenticated=False)

服务发现

通常,客户端不需要知道他们正在通信的服务器的完整 URL,只需发送到属于正确服务的路径的请求即可。

这由发送到请求的正确 URL 所需的所有信息的 endpoint_filter 参数控制。当使用此模式时,只需要指定请求的路径

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

注意

在此示例中,min_version 和 max_version 参数指示查找给定主 API 版本的端点可接受的范围。它们位于 endpoint_filter 中,它们不是请求对 /users 进行调用的特定微版本。

endpoint_filter 接受许多参数,这些参数可以用来确定端点 URL

service_type

服务的类型。例如 identitycomputevolume 或许多其他预定义的标识符。

接口

接口的网络暴露。也可以是一个列表,在这种情况下,将使用第一个匹配的接口。有效值是

  • public:一个可用于更广泛的互联网或网络的端点。

  • internal:仅可在专用网络内访问的端点。

  • admin:用于管理任务的端点。

region_name

端点所在的区域的名称。

版本

最小版本,限制于给定的主 API。例如,version2.2 将匹配 2.22.3,但不匹配 2.13.0。与 min_versionmax_version 互斥。

min_version

给定 API 的最小版本,旨在与 max_version 一起用作范围的下限。有关示例,请参阅 max_version。与 version 互斥。

max_version

给定 API 的最大版本,旨在与 min_version 一起用作范围的上限。例如

'min_version': '2.2',
'max_version': '3.3'

将匹配 2.22.103.03.3,但不匹配 1.422.13.20。与 version 互斥。

注意

version、min_version 和 max_version 都用于帮助确定给定服务的主 API 版本的端点。

discover_versions

无论是否严格必要,是否应运行版本发现。完全从目录中满足端点请求通常是可能的,这意味着版本发现 API 可能是浪费的额外调用。但是,运行发现而不是推断可能是期望的。默认为 True

所有版本参数(versionmin_versionmax_version)都可以作为以下形式给出:

  • 字符串:'2.0'

  • 整数:2

  • 浮点数:2.0

  • 整数元组:(2, 0)

versionmax_version 也可以给出字符串 latest,这表示应使用最高可用版本。

端点筛选器是一个简单的键值筛选器,可以提供任何数量的参数。然后由身份验证插件正确使用它理解的参数。

如果您想通过允许实验 API 或禁止弃用 API 来进一步限制您的服务发现,可以使用 allow 参数

>>> resp = session.get('/<project-id>/volumes',
                       endpoint_filter={'service_type': 'volume',
                                        'interface': 'public',
                                        'version': 1},
                       allow={'allow_deprecated': False})

allow 可以识别的可发现的端点类型是

  • allow_deprecated:允许弃用的版本端点。

  • allow_experimental:允许实验版本端点。

  • allow_unknown:允许具有未识别状态的端点。

会话对象通过确定与筛选器匹配的 URL 并将其附加到提供的路径来创建有效的请求。如果找到多个 URL 匹配项,则可以选择任何一个。

身份验证插件将努力维护 endpoint_filter 的一致参数集,但身份验证插件的概念是故意通用的。 某种机制可能不知道如何解释某些参数,在这种情况下,它可能会忽略它们。 例如,keystoneauth1.token_endpoint.Token 插件(当您希望始终使用特定的端点和令牌组合时使用)将始终返回相同的端点,而不管 endpoint_filter 的参数如何,或者自定义的 OpenStack 身份验证机制可能没有多个 interface 选项的概念,并选择忽略该参数。

用户有一定的期望,即他们了解正在使用的身份验证系统的局限性。

使用适配器

如果开发人员更喜欢不为每个 API 调用提供 endpoint_filter,则可以创建一个 keystoneauth1.adapter.AdapterAdapter 构造函数接受与 endpoint_filter 相同的参数,以及一个 SessionAdapter 的行为与 Session 非常相似,具有相同的 REST 方法,但“挂载”在 endpoint_filter 找到的端点上。

adapter = keystoneauth1.adapter.Adapter(
    session=session,
    service_type='volume',
    interface='public',
    version=1)
response = adapter.get('/volumes')

endpoint_filter 在 Session 上一样,versionmin_versionmax_version 参数存在于帮助确定服务的主要 API 的适当端点。

端点元数据

keystoneauth1.adapter.Adapterkeystoneauth1.session.Session 都有一个用于获取给定服务找到的端点元数据的方法:get_endpoint_data

keystoneauth1.session.Session 上,它接受与 endpoint_filter 相同的参数。

keystoneauth1.adapter.Adapter 上,它不接受任何参数,因为它返回适配器挂载的端点的相关信息。

get_endpoint_data 返回一个 keystoneauth1.discovery.EndpointData 对象。 此对象可用于查找有关端点的信息,包括找到哪个主要 api_version,或者在范围、列表或 latest 版本的情况下哪个 interface

它还可以用于确定 API 支持的 min_microversionmax_microversion。 如果 API 不支持微版本,则两者的值都将为 None。 它还将包含 next_min_versionnot_before 的值(如果端点存在),或者如果不存在则为 Nonekeystoneauth1.discovery.EndpointData 对象将始终包含与微版本相关的属性,无论 REST 文档是否包含这些属性。

get_endpoint_data 使用与发现过程的其余部分相同的缓存,因此调用它不应产生不必要的开销。 默认情况下,它将至少进行一次版本发现调用,以便可以获取微版本元数据。 如果用户知道服务不支持微版本并且只是好奇发现了哪个主要版本,则可以将 discover_versions 设置为 False 以防止获取微版本元数据。

请求一个微版本

希望为给定请求指定微版本的用户可以将它传递给 keystoneauth1.session.Session 对象或 keystoneauth1.adapter.Adapter 对象上的 microversion 参数。 这将导致 keystoneauth 将适当的标头传递给服务,告知服务用户想要使用的微版本。

resp = session.get('/volumes',
                   microversion='3.15',
                   endpoint_filter={'service_type': 'volume',
                                    'interface': 'public',
                                    'min_version': '3',
                                    'max_version': 'latest'})

如果用户使用的是 keystoneauth1.adapter.Adapter,则 service_type(这是发送到微版本标头中的数据的一部分)将取自适配器的 service_type

adapter = keystoneauth1.adapter.Adapter(
    session=session,
    service_type='compute',
    interface='public',
    min_version='2.1')
response = adapter.get('/servers', microversion='2.38')

用户还可以向 Adapter 构造函数提供 default_microversion 参数,该参数将在未请求显式微版本的所有请求中使用。

adapter = keystoneauth1.adapter.Adapter(
    session=session,
    service_type='compute',
    interface='public',
    min_version='2.1',
    default_microversion='2.38')
response = adapter.get('/servers')

如果用户使用的是 keystoneauth1.session.Session,则 service_type 将取自 endpoint_filter 中的 service_type

如果 service_type 是用于问题中服务的微版本标头的错误值,则可以提供参数 microversion_service_type。 例如,虽然 keystoneauth 已经了解 Cinder,但 Cinder 的 service_typeblock-storage,但微版本标头期望 volume

# 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'})

日志

日志系统使用标准的 python logging,根位于 keystoneauth 命名空间下,如所料。 有两种可能性,即 HTTP 交互的日志消息将去哪里。

默认情况下,所有消息都将发送到 keystoneauth.session 日志记录器。

如果 keystoneauth1.session.Session 构造函数上的 split_loggers 选项设置为 True,则 HTTP 内容将拆分为四个子日志记录器,以便可以对记录的内容和方式进行细粒度控制

keystoneauth.session.request-id

为每个 http 请求发出一个日志条目,级别为 DEBUG,包括有关 URL、service-typerequest-id 的信息。

keystoneauth.session.request

为每个 http 请求发出一个日志条目,级别为 DEBUG,包括请求的 curl 格式字符串。

keystoneauth.session.response

为收到的每个 http 响应发出一个日志条目,级别为 DEBUG,包括收到的状态码和标头。

keystoneauth.session.body

如果 content-typetextjson,则发出一个日志条目,级别为 DEBUG,其中包含响应正文的内容。

使用日志记录器

如何使用 python logging 的完整描述超出了本文档的范围,但提供了一些简单的示例。

如果您想配置日志记录以将 keystoneuath 记录在 INFO 级别,没有 DEBUG 消息

import keystoneauth1
import logging

logger = logging.getLogger('keystoneauth')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)

如果您想获得包括正文在内的完整 HTTP 调试跟踪

import keystoneauth1
import logging

logger = logging.getLogger('keystoneauth')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)

如果您想获得完整的 HTTP 调试跟踪,但没有正文

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)

最后,如果您想将 request-id 和响应标头记录到一个文件,将请求命令、响应标头和响应正文记录到另一个文件,并将所有其他内容记录到控制台

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)

上述内容将在 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

并将类似的内容记录到 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": []}

用户提供的日志记录器

keystoneauth1.session.Sessionkeystoneauth1.adapter.Adapter 上的 HTTP 方法(request、get、post、put 等)都支持一个 logger 参数。 用户可以提供他们自己的 logger,这将覆盖上述会话日志记录器。 如果以这种方式提供单个日志记录器,则请求、响应和正文内容都将以 DEBUG 级别记录到该日志记录器,并且字符串 REQ:RESP:RESP BODY: 将根据需要作为前缀添加。