版本化通知转换

https://blueprints.launchpad.net/nova/+spec/versioned-notification-transformation-newton

在 Mitaka 中,版本化通知的基础设施已经合并 [1]。现在是时候开始将现有的遗留通知转换为新格式了。本规范提出了最初的几次转换。

问题描述

nova 的遗留通知接口定义不明确,并且遗留通知定义了一个非常不一致的接口。通知消费者无法轻松地查看 nova 将发送的格式和内容。

用例

作为工具开发者,我想消费 nova 通知以实现我的需求。我想知道通知的格式,并且希望有一种方法来检测和跟踪通知格式的变化。

提议的变更

让我们首先转换以下通知,因为它们是 nova 代码库中最常见的通知

  • instance.update 是 nova 中 payload 大小最大的通知,这将为我们提供有关新版本化通知基础设施实用性的良好反馈

  • instance.delete.* 是 nova 中常见通知模式的一个实例。存在具有 instance.* event_type 的类似通知。它们都通过相同的代码路径,并带有不同的额外 payload 部分。因此,将定义一个通用的 instance action payload,如果 event_type 没有额外的 payload 字段,可以直接使用,或者可以轻松地子类化以添加额外的 payload 字段。此外,有一种通用的模式,即对于给定的 instance action,具有 action.start action.end 和 action.error 通知。新的通知将在这些 event_type 之间尽可能多地共享 payload 类。

    instance.delete.end – 与 identity.user.deleted – 类似,由操作员用于在系统释放资源时触发清理和计费活动。因此,它很重要。此外,nova 想要添加具有类似类型的通知 [3],[4],因此创建一个示例也将有助于这些工作。

  • nova.exceptions.wrap_exception 装饰器会发出具有可变 payload 的遗留通知。通知的 ‘args’ 字段填充了被装饰函数的调用参数,这些参数由 nova.safe_utils.getcallargs 收集。因此,我们无法为每个被装饰函数制定一个完全版本化的通知,因为这是不可行的,所以我们将只发出信息的静态部分,例如模块名称、函数名称、异常类、异常消息。

在转换过程中,我们将为这些通知定义一个对象模型,有关详细信息,请参阅数据模型影响部分。新的通知对象将支持同时发出遗留格式和新的版本化格式,因此所提出的更改是向后兼容的。

备选方案

我们可以以不同的顺序开始转换遗留通知。

数据模型影响

数据库模式不受影响。

通知对象的分离命名空间

目前,我们拥有的每个对象都是 Nova 的私有/内部对象。为通知 payload 定义的对象模型是 Nova 公共接口的一部分。因此,需要将通知模型与现有的对象模型分离,以便开发者在定义在 nova 外部使用的内容时能够清楚地了解,并保证我们不会意外地将内部对象作为公共通知的一部分暴露出来。

为了实现必要的隔离,我们将

  • 我们将已经创建的与通知相关的对象移动到 nova/notifications/objects/ 下的单独目录中,并将新提出的对象也添加到那里。

  • NotificationBase 和 NotificationPayloadBase 将 OBJ_PROJECT_NAMESPACE 设置为 ‘nova-notification’,因此所有与通知相关的对象都将属于一个单独的 ovo 命名空间。

  • 继续使用 NovaObject 作为通知对象的基类,以保持线格式,但不要将通知对象注册到 NovaObjectRegistry,以避免将 nova 内部对象与通知对象混合。

  • 分离单元测试,以便我们可以测试未注册的对象哈希以维护版本控制。

instance.update 和 instance.delete

instance.delete 和 instance.update 通知具有部分公共 payload,因此我们可以创建一些基类,然后根据需要将它们混合在一起。

以下 InstancePayload 类持有公共部分

@base.NovaObjectRegistry.register_if(False)
class InstancePayload(notification.NotificationPayloadBase):
    SCHEMA = {
        'uuid': ('instance', 'uuid'),
        'user_id': ('instance', 'user_id'),
        'tenant_id': ('instance', 'project_id'),
        'reservation_id': ('instance', 'reservation_id'),
        'display_name': ('instance', 'display_name'),
        'host_name': ('instance', 'hostname'),
        'host': ('instance', 'host'),
        'node': ('instance', 'node'),
        'os_type': ('instance', 'os_type'),
        'architecture': ('instance', 'architecture'),
        'cell_name': ('instance', 'cell_name'),
        'availability_zone': ('instance', 'availability_zone'),

        'instance_type_id': ('instance', 'instance_type_id'),
        'memory_mb': ('instance', 'memory_mb'),
        'vcpus': ('instance', 'vcpus'),
        'root_gb': ('instance', 'root_gb'),
        'ephemeral_gb': ('instance', 'ephemeral_gb'),

        'kernel_id': ('instance', 'kernel_id'),
        'ramdisk_id': ('instance', 'ramdisk_id'),

        'created_at': ('instance', 'created_at'),
        'launched_at': ('instance', 'launched_at'),
        'terminated_at': ('instance', 'terminated_at'),
        'deleted_at': ('instance', 'deleted_at'),

        'state': ('instance', 'terminated_at'),
        'state_description': ('instance', 'task_state'),
        'progress': ('instance', 'progress'),

        'metadata': ('instance', 'metadata'),
    }
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'uuid': fields.UUIDField(),
        'user_id': fields.StringField(nullable=True),
        'tenant_id': fields.StringField(nullable=True),
        'reservation_id': fields.StringField(nullable=True),
        'display_name': fields.StringField(nullable=True),
        'host_name': fields.StringField(nullable=True),
        'host': fields.StringField(nullable=True),
        'node': fields.StringField(nullable=True),
        'os_type': fields.StringField(nullable=True),
        'architecture': fields.StringField(nullable=True),
        'cell_name': fields.StringField(nullable=True),
        'availability_zone': fields.StringField(nullable=True),

        'instance_flavor_id': fields.StringField(nullable=True),
        'instance_type_id': fields.IntegerField(nullable=True),
        'instance_type': fields.StringField(nullable=True),
        'memory_mb': fields.IntegerField(nullable=True),
        'vcpus': fields.IntegerField(nullable=True),
        'root_gb': fields.IntegerField(nullable=True),
        'disk_gb': fields.IntegerField(nullable=True),
        'ephemeral_gb': fields.IntegerField(nullable=True),
        'image_ref_url': fields.StringField(nullable=True),

        'kernel_id': fields.StringField(nullable=True),
        'ramdisk_id': fields.StringField(nullable=True),
        'image_meta': fields.DictOfStringsField(nullable=True),

        'created_at': fields.DateTimeField(nullable=True),
        'launched_at': fields.DateTimeField(nullable=True),
        'terminated_at': fields.DateTimeField(nullable=True),
        'deleted_at': fields.DateTimeField(nullable=True),

        'state': fields.StringField(nullable=True),
        'state_description': fields.StringField(nullable=True),
        'progress': fields.IntegerField(nullable=True),

        'ip_addresses': fields.ListOfObjectsField('IpPayload'),

        'metadata': fields.DictOfStringsField(),
    }

    def __init__(self, instance):
        super(InstancePayload, self).__init__()
        self.populate_schema(instance=instance)

然后这里是 InstanceUpdatePayload,它添加了 instance.update 通知独有的额外字段

@base.NovaObjectRegistry.register_if(False)
class InstanceUpdatePayload(InstancePayload):
    # No SCHEMA as all the additional fields are calculated

    VERSION = '1.0'
    fields = {
        'state_update': fields.ObjectField('InstanceStateUpdatePayload'),
        'audit_period': fields.ObjectField('AuditPeriodPayload'),
        'bandwidth': fields.ListOfObjectsField('BandwidthPayload'),
        'old_display_name': fields.StringField(nullable=True)
    }

    def __init__(self, instance):
        super(InstanceUpdatePayload, self).__init__(instance)

然后这里是 InstanceActionPayload,它添加了所有 instance.<action> 通知通用的额外 fault 字段

@base.NovaObjectRegistry.register_if(False)
class InstanceActionPayload(InstancePayload):
    # No SCHEMA as all the additional fields are calculated

    VERSION = '1.0'
    fields = {
        'fault': fields.ObjectField('ExceptionPayload', nullable=True),
    }

    def __init__(self, instance):
        super(InstanceActionPayload, self).__init__(instance)

此外,我们的 payload 引用了几个额外的类

@base.NovaObjectRegistry.register_if(False)
class BandwidthPayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'network_name': fields.StringField(),
        'in_bytes': fields.IntegerField(),
        'out_bytes': fields.IntegerField(),
    }


@base.NovaObjectRegistry.register_if(False)
class IpPayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'label': fields.StringField(),
        'vif_mac': fields.StringField(),
        'meta': fields.DictOfStringsField(),
        'port_uuid': fields.UUIDField(nullable=True),
        'version': fields.IntegerField(),
        'address': fields.IPAddressField(),
    }

@base.NovaObjectRegistry.register_if(False)
class AuditPeriodPayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'audit_period_beginning': fields.DateTimeField(nullable=True),
        'audit_period_ending': fields.DateTimeField(nullable=True),
    }


@base.NovaObjectRegistry.register_if(False)
class InstanceStateUpdatePayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'old_state': fields.StringField(nullable=True),
        'state': fields.StringField(nullable=True),
        'old_task_state': fields.StringField(nullable=True),
        'new_task_state': fields.StringField(nullable=True),
    }

现在我们可以定义 instance.update 通知类

@notification.notification_sample('instance-update.json')
@base.NovaObjectRegistry.register_if(False)
class InstanceUpdateNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'

    fields = {
        'payload': fields.ObjectField('InstanceUpdatePayload')
    }

然后我们可以定义三个 instance.delete.* 通知

@notification.notification_sample('instance-action.json')
@base.NovaObjectRegistry.register_if(False)
class InstanceActionNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'

    fields = {
        'payload': fields.ObjectField('InstanceActionPayload')
    }

请注意,instance.delete.start 和 instance.delete.end 和 instance.delete.error 的 payload 具有相同的结构,因此可以在模型中使用相同的通用 InstanceActionPayload。这允许从相同的 InstanceActionNotification 类创建这两个通知。

该模型旨在保存与现有遗留通知相同的信息,但是需要进行一些更改

  • 遗留通知中的 ‘progress’ 字段在通知中要么是一个整数,要么是一个空字符串。无法在模型中保留此行为,因此在版本化通知中,‘progress’ 是一个可为空的整数。

  • 在现有的通知中,‘bandwidth’ 字段是一个字典,其中键是网络标签,值是具有两个键值对的字典,用于输入和输出带宽。新模型将其简化为字典列表,其中每个字典具有三个键值对,一个用于标签,两个用于带宽。

  • 审核周期字段位于遗留 instance.update 通知 payload 的根级别,现在它被移动到子对象。

所提出的 IpPayload、InstanceStateUpdatePayload 和 AuditPeriodPayload 类与现有的 nova.object 类是单独的定义。现有的类用于 nova 内部使用,而新的类用于通知 payload 使用。我们不能使用相同的名称来命名这些对象,因为 ovos 仅使用类的不合格名称来验证字段的内容。

nova.exception.wrap_exception

nova.exceptions.wrap_exception 装饰器用于在被装饰函数发生异常时发送通知。今天,此通知具有以下结构

{
    event_type: <the named of the decorated function>,
    publisher_id: <needs to be provided to the decorator via the notifier>,
    payload: {
        exception: <the exception object>
        args: <dict of the call args of the decorated function as gathered
               by nova.safe_utils.getcallargs except the ones that has
               '_pass' in their names>
    }
    timestamp: ...
    message_id: ...
}

可变的 event_type 使消费这些通知变得非常困难,因此在版本化格式中,我们将定义一个单一的 event_type ‘compute.exception’,并将函数名称添加到 payload 中。

我们可以为它定义以下通知对象

@base.NovaObjectRegistry.register_if(False)
class ExceptionPayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'module_name': fields.StringField(),
        'function_name': fields.StringField(),
        'exception': fields.StringField(),
        'exception_message': fields.StringField()
    }


@notification.notification_sample('compute-exception.json')
@base.NovaObjectRegistry.register_if(False)
class ExceptionNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'payload': fields.ObjectField('ExceptionPayload')
    }

此外,NotificationPayloadBase 类将扩展两个新的可为空字段 instance_uuid 和 request_id,因为这些是几乎所有 nova 通知(包括 instance action 通知)的通用信息。

REST API 影响

安全影响

通知影响

转换后的通知将在 ‘notification_format’ 配置选项设置为 ‘both’ 或 ‘versioned’ 时发出新的版本化通知格式。如果配置设置为 ‘unversioned’ 或 ‘both’,则遗留通知将保持不变地发出。

如 versioned-notification-api bp 中实现的那样,版本化通知始终发送到名为 ‘versioned_notifications’ 的不同的 amqp 主题,因此消费者可以通过主题来区分遗留格式和新格式。

其他最终用户影响

性能影响

如果 ‘notification_format’ 设置为 ‘both’,则将以不同的格式发出相同通知的两个实例。

其他部署者影响

开发人员影响

添加新通知发出代码的开发者需要调用新对象模型提供的新的接口。

实现

负责人

主要负责人

balazs-gibizer

工作项

对于每个转换后的通知

  • 将现有的通知相关对象移动到单独的命名空间

  • 添加新的对象模型

  • 添加使用新的 Notification 类发出遗留格式的可能性

  • 更改 nova 代码库以调用新的 Notification 类

  • 为新的版本化格式添加通知示例

依赖项

测试

将提供功能测试来执行发出新的版本化通知,并且测试将断言存储的通知示例的有效性。

文档影响

将提供通知示例文件。notification.rst [2] 中的版本化通知表会自动更新。

参考资料

历史

修订版

发布名称

描述

Newton

引入