显式数据类型

https://blueprints.launchpad.net/congress/+spec/explicit-data-types

为了支持标准 Congress 策略引擎之外的更多策略引擎(例如,基于 SQL、influxDB 或 Z3 的引擎),我们需要在数据源和数据服务引擎中显式定义表列的数据类型。本规范建议以可扩展、演进和向后兼容的方式支持显式类型,包括自定义类型。

问题描述

由于每个规则引擎在表达能力和性能方面都有局限性,因此没有一个通用的规则引擎适用于所有用例。因此,Congress 从一开始就被设计为支持多种策略引擎,除了标准的 agnostic 引擎之外。

一个问题是 Congress 处理的是无类型表列,这与许多候选策略引擎的基础(例如,SQL、influxDB、Z3)冲突。

为了拥有一个开放的框架,让多个策略引擎和多个数据源能够很好地协同工作,我们需要一个显式的类型系统,该系统具有可扩展性(以适应不同数据源和不同策略引擎的特定需求)、松耦合性(在不同数据源和策略引擎之间)以及与标准 agnostic 引擎和现有数据源驱动程序向后兼容性。

作为一个可扩展性和松耦合性的示例,考虑一下基于 SMT 的 Z3 推理器需要尽可能窄的类型(例如,enum{‘ingress’, ‘egress’}),但另一个引擎不理解这种窄类型。然后,数据源驱动程序必须以一种对两种引擎都有效的方式指定其类型。

提议的变更

概述

基本思想是让每个数据源驱动程序尽可能精确和狭窄地使用基本类型和自定义类型的组合来描述其数据。然后,每个策略引擎负责使用该信息来使用其内部表示形式来表示数据。策略引擎可能无法理解数据源驱动程序使用的每个自定义类型(尤其是因为我们预计会随着时间的推移引入新的类型)。为了允许策略引擎处理它不“理解”的自定义数据类型,我们需要每个自定义数据类型都继承自另一个数据类型,所有这些最终都继承自所有策略引擎必须处理的基本类型之一。这样,当策略引擎遇到一个它不理解的自定义类型的值时,策略引擎可以回退到根据引擎理解的祖先类型来处理该值。

什么是类型?

为了在本规范中定义什么是类型,我们首先建立四个相关概念

  1. 源表示形式:用于从外部源接收的数据的表示形式。每个外部数据源都有其在 Congress 之外定义自己的源表示形式。在 IP 地址示例中,一个源可以使用 IPv4 点分十进制字符串 "1.0.0.1",而另一个源可以使用 IPv6(简短)十六进制字符串 "::ffff:100:1"

  2. 内部表示形式:由 Congress 服务(例如,策略引擎)使用的表示形式。每个 Congress 服务可以使用不同的内部表示形式。例如,IP 地址在一个引擎中可以表示为字符串,在另一个引擎中可以表示为整数,在另一个引擎中可以表示为位向量。

  3. 交换表示形式:数据在 Congress 服务之间发布和接收时使用的表示形式。在 IP 地址示例中,这可能是整数 0x01000001

数据以源表示形式的形式从外部数据源接收。数据源驱动程序在将数据发布到 Congress 服务(例如,策略引擎)之前,将数据从源表示形式转换为交换表示形式。Congress 服务(例如,策略引擎)将处理数据并使用某种形式的内部表示形式来存储数据以供将来使用。

Congress 数据类型的首要目的是成为在各种 Congress 数据源和服务之间使用的交换格式规范,每个数据源和服务都有自己的表示形式。每个 Congress 服务的开发人员可以使用 Congress 数据类型来了解预期的交换格式以及所表示的内容。因此,可以将 Congress 数据类型视为与每个数据或表列关联的标签。

作为次要便利,每个 Congress 数据类型还提供用于处理该类型值的有用方法。至少,Congress 数据类型将提供一种方法来验证值是否对此类型有效。

最后,无法预先预期整个 Congress 数据类型的集合。因此,为每个 Congress 数据使用者编程以识别和处理每个 Congress 数据类型是不切实际的。我们需要一个更有结构和可扩展的“标签”集合。我们将 Congress 数据类型组织成一个“is-a”层次结构。首先,有一组所有 Congress 数据使用者必须识别和处理的基本类型。所有其他非基本类型都从基本类型或另一个非基本类型继承,形成一个完善的层次结构。然后,每个数据使用者可以通过根据其基本类型来处理所有数据类型来处理所有数据类型。可选地,数据使用者可以为某些非基本类型添加特殊处理。

作为继承的一个例子,IP 地址类型可以从通用的字符串类型继承,以便不专门处理 IP 地址类型的策略引擎会将数据作为通用的字符串处理。

将以上三点结合起来,我们将 Congress 数据类型定义为以下 CongressDataType 抽象基类的子类。

@six.add_metaclass(abc.ABCMeta)
class CongressDataType(object):

    @classmethod
    @abc.abstractmethod
    def validate(cls, value):
        '''Validate a value as valid for this type.

        :Raises ValueError: if the value is not valid for this type
        '''
        raise NotImplementedError

    @classmethod
    def _get_parent(cls):
        congress_parents = [parent for parent in cls.__bases__
                            if issubclass(parent, CongressDataType)]
        if len(congress_parents) == 1:
            return congress_parents[0]
        elif len(congress_parents) == 0:
            raise cls.CongressDataTypeNoParent(
                'No parent type found for {0}'.format(cls))
        else:
            raise cls.CongressDataTypeHierarchyError(
                'More than one parent type found for {0}: {1}'
                    .format(cls, congress_parents))

重要

这些类预计不会被实例化为包装对象,而仅仅充当描述和操作这些值以及编码数据类型层次结构的类方法的组织单元。使用类有两个原因。

  1. Python 继承层次结构是一种方便的方式来声明和访问 Congress 数据类型之间的关系。CongressDataType 的每个非根具体子类都从另一个具体的 CongressDataType 子类继承,以编码 (A) 是数据类型层次结构中 (B) 的子类型。CongressDataType 的具体子类可以从多个父 Python 类继承,但其中父类中不应超过一个 CongressDataType 子类。

  2. Python 继承使 Congress 数据类型能够重用父 Congress 数据类型中的代码。

更多细节

数据类型规范

数据源驱动程序可以选择在其值转换器定义中指定显式数据类型。数据服务基类在每个表发布中附加数据类型信息(表模式)。为了灵活性和向后兼容性,显式类型不是必需的每个列。

标准的 agnostic 引擎处理无类型表列。其他策略引擎可能处理或不处理无类型表列。当需要类型化的策略引擎接收到无类型数据时,策略引擎可以选择拒绝该数据或施加类型化。

以下是示例转换器定义,显示了如何在 Nova 数据源驱动程序的 flavors 表中指定显式数据类型。

flavors_translator = {
    'translation-type': 'HDICT',
    'table-name': FLAVORS,
    'selector-type': 'DOT_SELECTOR',
    'field-translators':
        ({'fieldname': 'id', 'desc': 'ID of the flavor',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressStr}},
         {'fieldname': 'name', 'desc': 'Name of the flavor',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressStr}},
         {'fieldname': 'vcpus', 'desc': 'Number of vcpus',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressInt}},
         {'fieldname': 'ram', 'desc': 'Memory size in MB',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressInt}},
         {'fieldname': 'disk', 'desc': 'Disk size in GB',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressInt}},
         {'fieldname': 'ephemeral', 'desc': 'Ephemeral space size in GB',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressInt}},
         {'fieldname': 'rxtx_factor', 'desc': 'RX/TX factor',
          'translator': {'translation-type': 'VALUE',
          'data-type': CongressFloat}})
          }

类型的联合

当策略引擎从现有表派生新表时,它会创建一个包含来自多种类型的数据的表列。在这种情况下,新列应被类型化为所有组成数据类型的最低共同祖先类型。为了确保始终存在最低共同祖先类型,我们建立 Congress 字符串类型作为所有其他类型的根父类型。

类型层次结构

本规范提出一个灵活且可扩展的类型框架。类型的确切集合将在实施补丁中确定,并且会随着时间的推移而演变。作为参考,我们下面提供一个类型的示例层次结构。(括号中的项目是类型的抽象类别。)

  • string

    • [有界长度字符串]

      • 长度最多为 N 的字符串

      • [字符串枚举]

        • 布尔值

        • 网络方向(ingress 和 egress)

    • [固定长度字符串]

      • 长度为 N 的字符串

        • UUID

    • 十进制(固定精度有理数,表示为字符串)

      • integer

        • [整数枚举]

          • A 和 B 之间的整数

          • 短整数

      • 浮点数

    • IP 地址

数据类型示例代码

此处包含一些选定的数据类型的示例代码供参考。

class CongressStr(CongressDataType):
    '''Base type representing a string.

    Sample implementation only. Final implementation needs to deal with
    nullability, unicode, and other issues.'''

    @classmethod
    def validate(cls, value):
        if not isinstance(value, six.string_types):
            raise ValueError

class CongressBool(CongressStr):
    @classmethod
    def validate(cls, value):
        if not isinstance(value, bool):
            raise ValueError

class CongressIPAddress(CongressStr):
    @classmethod
    def validate(cls, value):
        try:
            ipaddress.IPv4Address(value)
        except ipaddress.AddressValueError:
            try:
                ipv6 = ipaddress.IPv6Address(value)
                if ipv6.ipv4_mapped:
                    # as a design decision in standardized format
                    # addresses in ipv4 range should be in ipv4 syntax
                    raise ValueError
            except ipaddress.AddressValueError:
                raise ValueError

数据类型的抽象类别

有时,一组类型都遵循某种模式。在这种情况下,我们可以提供一个框架来以更通用的方式创建和处理这些类型。例如,许多类型都具有固定且有限的字符串域。在这种情况下,我们可以提供一个类型工厂来创建此空间中的类型。以下是用于创建固定且有限的字符串域的类型工厂函数的示例实现。

此外,我们还可以提供一个混合抽象基类,数据使用者可以使用它以更通用的方式处理数据。在这种情况下,抽象基类 CongressTypeFiniteDomain 规定从该类继承的每个类型都必须具有一个名为 DOMAIN 的类变量,该变量是允许的类型中值的冻结集合。数据使用者可以使用此信息以通用方式处理该值,而无需识别使用的特定具体类型。

以下是包含参考的示例代码。

@six.add_metaclass(abc.ABCMeta)
class CongressTypeFiniteDomain(object):
    '''Abstract base class for a Congress type of bounded domain.

    Each type inheriting from this class must have a class variable DOMAIN
    which is a frozenset of the set of values allowed in the type.
    '''
    pass


def create_congress_str_enum_type(class_name, enum_items):
    '''Return a sub-type of CongressStr for representing a string from
    a fixed, finite domain.'''

    for item in enum_items:
        if not isinstance(item, six.string_types):
            raise ValueError

    class NewType(CongressStr, CongressTypeFiniteDomain):
        DOMAIN = frozenset(enum_items)

        @classmethod
        def validate(cls, value):
            if not value in cls.DOMAIN:
                raise ValueError

    NewType.__name__ = class_name
    return NewType

例如,处理防火墙规则的特定数据源驱动程序可以使用工厂函数创建自定义类型,如下所示。

NetworkDirection = create_congress_str_enum_type(
    'NetworkDirection', ('ingress', 'egress'))

转换为祖先类型

通常,每个后代类型的值可以直接解释为祖先类型的值。理想情况下,每个策略引擎都将识别和支持。唯一的例外是,从 CongressStr 继承的非字符串类型的值需要转换为字符串才能解释为 CongressStr 类型的值。CongressDataType 抽象基类可以包含其他辅助方法,以使解释变得容易。以下是包含其他辅助方法的扩展 CongressDataType 定义。

@six.add_metaclass(abc.ABCMeta)
class CongressDataType(object):

    @classmethod
    @abc.abstractmethod
    def validate(cls, value):
        '''Validate a value as valid for this type.

        :Raises ValueError: if the value is not valid for this type
        '''
        raise NotImplementedError

    @classmethod
    def least_ancestor(cls, target_types):
        '''Find this type's least ancestor among target_types

        This method helps a data consumer find the least common ancestor of
        this type among the types the data consumer supports.

        :param supported_types: iterable collection of types
        :returns: the subclass of CongressDataType
                  which is the least ancestor
        '''
        target_types = frozenset(target_types)
        current_class = cls
        try:
            while current_class not in target_types:
                current_class = current_class._get_parent()
            return current_class
        except cls.CongressDataTypeNoParent:
            return None

    @classmethod
    def convert_to_ancestor(cls, value, ancestor_type):
        '''Convert this type's exchange value
           to ancestor_type's exchange value

        Generally there is no actual conversion because descendant type value
        is directly interpretable as ancestor type value. The only exception
        is the conversion from non-string descendents to string. This
        conversion is needed by Agnostic engine does not support boolean.

        .. warning:: undefined behavior if ancestor_type is not
                     an ancestor of this type.
        '''
        if ancestor_type == CongressStr:
            return json.dumps(value)
        else:
            return value

    @classmethod
    def _get_parent(cls):
        congress_parents = [parent for parent in cls.__bases__
                            if issubclass(parent, CongressDataType)]
        if len(congress_parents) == 1:
            return congress_parents[0]
        elif len(congress_parents) == 0:
            raise cls.CongressDataTypeNoParent(
                'No parent type found for {0}'.format(cls))
        else:
            raise cls.CongressDataTypeHierarchyError(
                'More than one parent type found for {0}: {1}'
                    .format(cls, congress_parents))

    class CongressDataTypeNoParent(TypeError):
        pass

    class CongressDataTypeHierarchyError(TypeError):
        pass

重新施加类型

(此处包含潜在的未来工作供参考。)

策略引擎可以发布数据供其他策略引擎使用。当具有较窄类型的策略引擎(例如,Z3)发布到具有未修复类型或较宽类型的另一个策略引擎(例如,Congress agnostic)时,处理是自然的。当发生相反的情况时,接收策略引擎可能无法处理数据。

作为未来的工作,发布策略引擎可以选择在可能的情况下推断并重新施加较窄的类型。

作为未来的工作,需要较窄类型的策略引擎可以采用额外的导入构造,该构造对从其他策略引擎订阅的表施加较窄的类型。

备选方案

在本节中,我们讨论了一些替代方案可能如何偏离本规范。

  1. 要求所有策略引擎都支持无类型数据。

    此替代方案将排除许多候选策略引擎(例如,基于 SQL、influxDB、Z3 的引擎)。

  2. 使用所有数据源和所有策略引擎使用的固定类型集。

    固定类型集应该是一个首选候选者,因为它是一种最简单的方法。不幸的是,不可能预料到所有需要的类型。每当添加新的策略引擎时,以前不重要的问题变得至关重要。例如,如果没有基于 SMT 的引擎(例如 Z3),字符串枚举类型与通用字符串类型区分开并不重要。因此,当引入 Z3 引擎时,我们需要 1) 创建新类型,2) 更新数据源驱动程序以指定使用新类型,以及 3) 更新现有策略引擎以处理新类型。

    本规范中提出的解耦和可扩展的方法提供了一种更平滑的方式来演进类型。每个数据源驱动程序尽可能狭窄地指定类型,而无需考虑策略引擎想要什么。每个策略引擎然后可以使用类型信息来尽可能地将数据映射到其内部表示形式。

    因此,当引入新的类型、新的数据源驱动程序或新的策略引擎时,无需更改现有代码。

  3. 不要在数据发布者中指定类型,而只在数据使用者中施加类型。

    当无法从数据发布者处获取固定数据类型时,此替代方案很有帮助(请参阅上文中的第 5 点)。如果数据源/发布者确实具有关于固定数据类型的信息(从代码中定义的固定数据类型推断得出),那么最好避免操作者在消费端定义数据类型的必要操作负担。

  4. 与其要求子类型的每个值都能直接解释为父类型的值,不如允许子类型和父类型之间进行转换,这样可以更灵活地选择最适合特定类型的交换表示形式,而无需考虑其父类型。例如,IP 地址可以表示为单个整数值,而不是字符串。

    增加的灵活性可能很有用,但到目前为止尚未证明是必要的。

策略

不适用。

策略动作

不适用。

数据源

每个数据源驱动程序可以选择性地在其值转换器中指定数据类型。数据源驱动程序可以根据新用例和新策略引擎的需求,随时更新为显式类型。

有关更多详细信息,请参阅 数据类型规范

数据模型影响

对 congress/db/ 中定义的国会数据模型无影响。

REST API 影响

对 REST API 无影响。

安全影响

新的安全影响很小。由于新的/自定义类型包括新的/自定义数据处理方法,从理论上讲,这会增加攻击面。需要小心确保数据处理方法能够安全地处理格式错误或潜在恶意输入。有成熟的最佳实践可以最大限度地降低此类风险。

通知影响

没有影响。

其他最终用户影响

对最终用户没有直接影响。此规范可以通过实现的可能性而使更多的策略引擎间接使最终用户受益。

所有现有行为和工作流程都将保留。

性能影响

预计数据和内存影响将是最小的,因为数据以原始形式存储和传输,而无需自定义对象的开销。

另一个主要的性能影响是将数据视为祖先类型。通过追溯类型层次结构来识别最底层的祖先类型。此步骤是基于每列进行的,性能成本可以忽略不计,因为类型层次结构很浅,并且每个类型最多只有一个祖先。向上追溯的最坏情况运行时间与类型层次结构的深度成线性关系。(请参阅 转换为祖先类型 以获取示例实现。)

其他部署者影响

对部署者没有直接影响。此规范可以通过实现的可能性而使最终用户受益。

所有现有行为和工作流程都将保留。

开发者影响

对开发人员没有强制性的重大更改。策略引擎使用类型信息是可选的。在数据源驱动程序中指定类型信息也是可选的。

实现

负责人

各个任务将在 launchpad 上被选取并跟踪。主要联系人是 ekcs <ekcs.openstack@gmail.com>。

工作项

  • 最小的实现。

    • 类型的抽象基类

    • 原始类型

    • 数据服务引擎 (DSE) 更改,用于存储和传递列上的(可选)类型信息。

  • 进一步的实现。这些进一步的实现是可选的,可以根据用例的需求逐步完成。

    • 将类型信息添加到数据源驱动程序。

    • 标准策略引擎(不可知)更改,以使用类型信息。

    • 扩展类型(例如,IP 地址)

    • 类型的类别(例如,字符串的固定枚举)

    • 加载数据源驱动程序中定义的自定义类型的框架

    • 策略引擎发布表时,类型信息的推断和重新施加

    • 订阅端施加其他类型信息

依赖项

没有明显的外部依赖项。

测试

数据服务引擎中包含附加类型信息将主要通过 congress/tests/dse2/ 中现有测试风格的单元测试进行测试。

当数据源驱动程序更新为包含附加类型信息时,可以自然地将附加检查添加到现有的 tempest 测试中,该测试将独立获得的服务状态与数据源驱动程序获得的状态进行比较并转换为表。

文档影响

没有直接影响。