版本化通知 API

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

nova 的通知接口定义得不够完善,当前的通知定义了一个非常不一致的接口。从通知消费者角度来看,很难看出 nova 发送的通知的格式和内容是什么。

问题描述

这是 oslo.messaging 支持的通用通知信封格式 [1]

{
    "priority": "INFO",
    "event_type": "compute.instance.update",
    "timestamp": "2015-09-02 09:13:31.895554",
    "publisher_id": "api.controller",
    "message_id": "06d9290b-b9b0-4bd5-9e76-ddf8968a70b4",
    "payload": {}
}

存在问题的字段是

  • priority

  • event_type

  • publisher_id

  • payload

priority: Nova 在当前代码库中使用 info 和 error 优先级,除非在 nova.notification.notify_decorator 代码中使用,在该代码中优先级可以通过 notification_level 配置参数进行配置。但是,此装饰器仅在 monkey_patch_modules 配置的默认值中使用。

event_type: oslo 允许发送原始字符串作为 event_type,nova 今天使用以下 event_type 格式

  • <service>.<object>.<action>.<phase> 例如:compute.instance.create.end

  • <object>.<action>.<phase> 例如:aggregate.removehost.end

  • <object>.<action> 例如:servergroup.create

  • <service>.<action>.<phase> 例如:scheduler.select_destinations.end

  • <action> 例如:snapshot_instance

  • <module?>.<action> 例如:compute_task.build_instances

publisher_id: nova 今天使用以下 publisher_id 格式

  • <service>.controller 例如:api.controller, compute.controller

  • <object>.controller 例如:servergroup.controller

  • <object>.<object_id> 例如:aggregate.<aggregate.name> 和 aggregate.<aggregate_id>. 参见:[2].

在某些情况下,publisher_id 和 event_type 的内容似乎重叠了。

payload: nova 对 payload 字段没有任何限制,这导致了许多不同的格式。有时它是一个现有 nova 版本化对象的视图,例如在 compute.instance.update 通知的情况下,nova 会在经过一些过滤后将实例对象的字段转储到通知中。在其他情况下,nova 会转储异常对象或将函数的参数和关键字参数转储到 payload 中。这种复杂的 payload 格式似乎是通知消费者面临的最大问题。

用例

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

提议的变更

本规范旨在就 nova 发送的通知中字段的格式、内容和含义达成一致,并提出一种更改现有通知为新格式的方法,同时给予通知消费者适应变化的时间。它还试图提供一种技术解决方案,以保持通知 payload 的稳定性和版本化。

当前的通知是未版本化的。本规范建议将未版本化的通知转换为版本化通知,同时保留在有限的时间内发出未版本化通知的可能性,以帮助通知消费者的过渡。

版本化通知将具有明确定义的格式,该格式将记录在案,并且将提供与 nova api 示例类似的通知示例。版本化通知的新版本将保持向后兼容。

为了建模和版本化新的通知,nova 将使用 oslo versionedobject 模块。为了发出此类通知,nova 将继续使用 oslo.messaging 模块的 notifier 接口。为了将通知模型转换为可以馈送到 notifier 接口的格式,nova 将使用现有的 NovaObjectSerializer。

单个版本化通知将使用单个 oslo versioned 对象建模,但该对象可以使用其他新的或现有的版本化对象作为 payload 字段。

但是,今天的一些通知实际上无法转换为版本化通知。例如,notify_decorator 会将任何函数的参数和关键字参数转储到通知 payload 中,因此我们无法为它生成的每个可能的 payload 创建单个版本化模型。对于这些通知,可以定义一个通用的、半管理的、基于字典的 payload,该 payload 尽可能地进行格式化,并将 payload 的其余部分未管理。未来应避免添加新的半管理通知。

我们希望保持 notifier 接口在 oslo.messaging 中定义的通知信封格式,因此版本化通知将在网络上传输时与未版本化通知具有相同的信封。

{
    "priority": "INFO",
    "event_type": "compute.instance.update",
    "timestamp": "2015-09-02 09:13:31.895554",
    "publisher_id": "api.controller",
    "message_id": "06d9290b-b9b0-4bd5-9e76-ddf8968a70b4",
    "payload": {}
}

版本化和未版本化通知的线格式之间的主要区别在于 payload 字段的格式。版本化通知的线格式将使用版本化对象的序列化格式作为 payload。

版本化通知模型将为 oslo.messaging notifier 接口需要的每个字段(priority、event_type、publisher_id、payload)定义版本化对象字段,以便可以在 nova 代码中完全建模单个通知。但是,只有 payload 字段将使用默认的版本化对象序列化。信封中的其他字段将像上面的示例一样用字符串填充。

网络上信封的 event_type 字段的值将由受影响对象的名称、执行发出通知的操作的名称以及操作的阶段定义。例如:instance.create.end、aggregate.removehost.start、filterscheduler.select_destinations.end。通知模型将对 event_type 的内容进行基本验证,例如为有效的阶段创建枚举。

网络上信封的 priority 字段的值可以从 oslo.messaging 中的预定义优先级(audit、debug、info、warn、error、critical、sample)中选择,除了“warning”(使用 warn 代替)。通知模型将通过提供包含有效优先级的枚举来验证优先级。

有关具体示例,请参阅数据模型影响部分。

向后兼容性

可以使用新的通知模型来发出当前的未版本化通知,以便在未版本化通知被弃用时提供向后兼容性。Nova 可能会希望在实施本规范后限制添加新的未版本化通知。

版本化通知的新版本必须与以前的版本向后兼容。Nova 将始终发出版本化通知的最新版本,并且 Nova 不支持回退通知版本。

为了确保与 Mitaka 之前的通知消费者的向后兼容性,将通过在单独的主题上在网络上传输版本化和未版本化通知格式来实现。新的通知模型将提供一种从相同的通知对象发出旧格式和新格式的方法。将提供一个配置选项来指定应发出哪些版本的通知,但从一开始就将仅请求旧格式的值将被弃用。在 N 版本中,将弃用版本化通知的未版本化线格式,并附带适当的弃用消息。

备选方案

版本化整个线格式而不是仅 payload

似乎有两种主要的方法可以从数据模型影响部分中定义的 KeyPairNotification 对象生成实际的通知消息。

使用 notifier 在 oslo.messaging 中定义的当前信封结构 [1],并使用数据模型影响部分中提出的线上的 payload 版本化。

优点

  • 不需要对 oslo.messaging 进行更改。

  • 消费者只需要更改 payload 解析代码。

  • 整个 OpenStack 生态系统中的通知信封是相同的。

缺点

  • 线上的信封是版本化的,只有 payload 字段是版本化的。但是,信封结构是由 oslo.messaging 定义的通用结构。

或者,在 oslo.messaging 中创建一个新的信封结构,该结构已经是一个版本化对象,并使用该对象的序列化形式在网络上传输。如果我们更改 oslo.messaging 以提供一个可以传递继承自 NotificationBase 对象的接口,并且 oslo.messaging 使用该对象的序列化形式作为消息,那么 KeyPair 通知消息在网络上传输时将如下所示

{
    "nova_object.version":"1.0",
    "nova_object.name":"KeyPairNotification",
    "nova_object.data":{
        "priority":"info",
        "publisher":{
            "nova_object.version":"1.19",
            "nova_object.name":"Service",
            "nova_object.data":{
                "host":"controller",
                "binary":"api"
                ...  # a lot of other fields from the Service object here
            },
            "nova_object.namespace":"nova"
        },
        "payload":{
            "nova_object.version":"1.3",
            "nova_object.name":"KeyPair",
            "nova_object.namespace":"nova",
            "nova_object.data":{
                "id": 1,
                "user_id":"21a75a650d6d4fb28858579849a72492",
                "fingerprint": "e9:49:b2:ca:56:8c:25:77:ea:0d:d9:7c:89..."
                "public_key": "ssh-rsa AAAAB3NzaC1yc2EAA...",
                "type": "ssh",
                "name": "mykey5"
            }
        },
        "event_type":{
            "nova_object.version":"1.0",
            "nova_object.name":"EventType",
            "nova_object.data":{
                "action":"create",
                "phase":"start",
                "object":"keypair"
            },
            "nova_object.namespace":"nova"
        }
    },
    "nova_object.namespace":"nova"
}

在这种情况下,NotificationBase 类将由 oslo.messaging 提供。

优点

  • 线上的整个消息都是版本化的。

缺点

  • 需要对 oslo.messaging 中的通知接口代码以及今天的通知驱动程序进行广泛的更改,因为今天的通知驱动程序依赖于当前的信封结构。

  • 这将导致 oslo.messaging 和 oslo.versionedobject 之间的循环依赖关系

  • 消费者需要适应顶级结构的变化。

使用单个全局通知版本

建议为每个通知使用单独的版本号。或者,可以定义一个单个的全局通知版本号,每次更改单个通知时都会递增该版本号。

数据模型影响

将定义以下基本对象

class NotificationPriorityType(Enum):
    AUDIT = 'audit'
    CRITICAL = 'critical'
    DEBUG = 'debug'
    INFO = 'info'
    ERROR = 'error'
    SAMPLE = 'sample'
    WARN = 'warn'

    ALL = (AUDIT, CRITICAL, DEBUG, INFO, ERROR, SAMPLE, WARN)

    def __init__(self):
        super(NotificationPriorityType, self).__init__(
            valid_values=NotificationPriorityType.ALL)


class NotificationPriorityTypeField(BaseEnumField):
    AUTO_TYPE = NotificationPriorityType()


@base.NovaObjectRegistry.register
class EventType(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    fields = {
        'object': fields.StringField(),
        'action': fields.EventTypeActionField(),   # will be an enum
        'phase': fields.EventTypePhaseField(),     # will be an enum
    }


@base.NovaObjectRegistry.register
class NotificationBase(base.NovaObject):

    fields = {
        'priority': fields.NotificationPriorityTypeField(),
        'event_type': fields.ObjectField('EventType'),
        'publisher': fields.ObjectField('Service'),
    }

    def emit(self, context):
        """Send the notification. """

    def emit_legacy(self, context):
        """Send the legacy format of the notification. """

请注意,NotificationBase 的 publisher 字段将用于通过从 Service 对象中提取服务名称和运行服务的宿主机来填充线格式中的 publisher_id 字段。

然后,这里有一个使用基本对象的具体示例

@base.NovaObjectRegistry.register
class KeyPairNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'payload': fields.ObjectField('KeyPair')
    }

其中引用的 KeyPair 对象是 nova 中已经存在的版本化对象。然后,当前的 keypair 通知发送代码可以写成如下所示

def _notify(self, context, keypair):
    event_type = notification.EventType(
        object='keypair',
        action=obj_fields.EventTypeActionField.CREATE,
        phase=obj_fields.EventTypePhaseField.START)
    publisher = utils.get_current_service()
    keypair_obj.KeyPairNotification(
        priority=obj_fields.NotificationPriorityType.INFO,
        event_type=event_type,
        publisher=publisher,
        payload=keypair).emit(context)

在定义版本化通知的 payload 模型时,我们将尝试重用现有的 nova 版本化对象,例如在上面的 KeyPair 示例中。如果不可能,将为 payload 创建一个新的版本化对象。

上述 KeyPair 通知的线格式如下所示

{
    "priority":"INFO",
    "event_type":"keypair.create.start",
    "timestamp":"2015-10-08 11:30:09.988504",
    "publisher_id":"api:controller",
    "payload":{
        "nova_object.version":"1.3",
        "nova_object.name":"KeyPair",
        "nova_object.namespace":"nova",
        "nova_object.data":{
            "id": 1,
            "user_id":"21a75a650d6d4fb28858579849a72492",
            "fingerprint": "e9:49:b2:ca:56:8c:25:77:ea:0d:d9:7c:89:35:36"
            "public_key": "ssh-rsa AAAAB3NzaC1yc2EAA...",
            "type": "ssh",
            "name": "mykey5"
        }
    },
    "message_id":"98f1221f-ded0-4153-b92d-3d67219353ee"
}

有关替代线格式,请参阅替代方案部分。

半管理通知示例

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 expect the ones that has
               '_pass' in their names>
    }
    timestamp: ...
    message_id: ...
}

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

@base.NovaObjectRegistry.register
class Exception(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'message': fields.StringField(),
        'code': fields.IntegerField(),
    }


@base.NovaObjectRegistry.register
class ExceptionPayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'exception': fields.ObjectField('Exception'),
        'args': fields.ArgDictField(),
    }


@base.NovaObjectRegistry.register
class ExceptionNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'payload': fields.ObjectField('ExceptionPayload')
    }

其中 ArgDictField 接受任何 python 对象,它在可用时使用对象序列化,否则使用 primitive->json 转换,但如果失败,则只是将对象字符串化。此字段没有明确定义的线格式,因此通知的这部分将不会真正被版本化,因此称为半版本化。

send_api_fault 通知示例

nova.notifications.send_api_fault 函数用于在发生 api 故障时发送通知。当前通知的格式如下

{
    event_type: "api.fault",
    publisher_id: "api.myhost",
    payload: {
        "url": <the request url>,
        "exception": <the stringified exception object>,
        "status": <http status code>
    }
    timestamp: ...
    message_id: ...
}

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

@base.NovaObjectRegistry.register
class ApiFaultPayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'url': fields.UrlField(),
        'exception': fields.ObjectField('Exception'),
        'status': fields.IntegerField(),
    }


@base.NovaObjectRegistry.register
class ApiFaultNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'payload': fields.ObjectField('ApiFaultPayload')
    }

instance update 通知示例

nova.notifications.send_update 函数今天用于发送有关实例更改的通知。这是当前通知格式的示例

{
    "priority":"INFO",
    "event_type":"compute.instance.update",
    "timestamp":"2015-10-12 14:33:45.704324",
    "publisher_id":"api.controller",
    "payload":{
        "instance_id":"0ab36db7-0770-47de-b34d-45adb17248e7",
        "user_id":"21a75a650d6d4fb28858579849a72492",
        "tenant_id":"8cd4a105ae504184ade871e23a2c6d07",
        "reservation_id":"r-epzg3dq2",
        "display_name":"vm1",
        "hostname":"vm1",
        "host":null,
        "node":null,
        "architecture":null,
        "os_type":null,
        "cell_name":"",
        "availability_zone":null,

        "instance_flavor_id":"42"
        "instance_type_id":6,
        "instance_type":"m1.nano",
        "memory_mb":64,
        "vcpus":1,
        "root_gb":0,
        "disk_gb":0,
        "ephemeral_gb":0,

        "image_ref_url":"http://192.168.200.200:9292/images/34d9b758-e9c8-4162-ba15-78e6ce05a350",
        "kernel_id":"7fc91b81-2ff1-4bd2-b79b-ec218463253a",
        "ramdisk_id":"25f19ee8-a350-4d8c-bb53-12d0f834d52f",
        "image_meta":{
            "kernel_id":"7fc91b81-2ff1-4bd2-b79b-ec218463253a",
            "container_format":"ami",
            "min_ram":"0",
            "ramdisk_id":"25f19ee8-a350-4d8c-bb53-12d0f834d52f",
            "disk_format":"ami",
            "min_disk":"0",
            "base_image_ref":"34d9b758-e9c8-4162-ba15-78e6ce05a350"
        },

        "created_at":"2015-10-12 14:33:45.662955+00:00",
        "launched_at":"",
        "terminated_at":"",
        "deleted_at":"",
        "new_task_state":"scheduling",
        "state":"building",
        "state_description":"scheduling",
        "old_state":"building",
        "old_task_state":"scheduling",
        "progress":"",

        "audit_period_beginning":"2015-10-12T14:00:00.000000",
        "audit_period_ending":"2015-10-12T14:33:45.699612",

        "access_ip_v6":null,
        "access_ip_v4":null,
        "bandwidth":{

        },
        "metadata":{

        },
    }
}

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

@base.NovaObjectRegistry.register
class BwUsage(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'label': fields.StringField(),
        'bw_in': fields.IntegerField(),
        'bw_out': fields.IntegerField(),
    }


@base.NovaObjectRegistry.register
class FixedIp(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'label': fields.StringField(),
        'vif_mac': fields.StringField(),
        'meta': fields.DictOfStringsField(),
        'type': fields.StringField(),   # maybe an enum
        'version': fields.IntegerField(),  # maybe an enum
        'address': fields.IPAddress()
    }


@base.NovaObjectRegistry.register
class InstanceUpdatePayload(base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'instance_id': fields.UUIDField(),
        'user_id': fields.StringField(),
        'tenant_id': fields.StringField(),
        'reservation_id': fields.StringField(),
        'display_name': fields.StringField(),
        'host_name': fields.StringField(),
        'host': fields.StringField(),
        'node': fields.StringField(),
        'os_type': fields.StringField(),
        'architecture': fields.StringField(),
        'cell_name': fields.StringField(),
        'availability_zone': fields.StringField(),

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

        'kernel_id': fields.StringField(),
        'ramdisk_id': fields.StringField(),
        'image_meta': fields.DictOfStringField(),

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

        'new_task_state': fields.StringField(),
        'state': fields.StringField()
        'state_description': fields.StringField(),
        'old_state': fields.StringField(),
        'old_task_state': fields.StringField(),
        'progress': fields.IntegerField(),

        "audit_period_beginning": fields.DateTimeField(),
        "audit_period_ending": fields.DateTimeField(),

        'access_ip_v4': fields.IPV4AddressField(),
        'access_ip_v6': fields.IPV6AddressField(),
        'fixed_ips': fields.ListOfFixedIps(),

        'bandwidth': fields.ListOfBwUsages()

        'metadata': fields.DictOfStringField(),

    }


@base.NovaObjectRegistry.register
class InstanceUpdateNotification(notification.NotificationBase):
    # Version 1.0: Initial version
    VERSION = '1.0'
    fields = {
        'payload': fields.ObjectField('InstanceUpdatePayload')
    }

预计不会进行数据库模式更改。

REST API 影响

无。

安全影响

无。

通知影响

请参阅提出的更改和数据模型部分。

其他最终用户影响

无。

性能影响

由于保持 Mitaka 中的向后兼容性,为通知同时发送未版本化和版本化线格式会增加消息总线上的负载。将提供一个配置选项来指定应发出哪些版本的通知,以减轻此问题。此外,部署者可以使用 NoOp 通知驱动程序来关闭该接口。

其他部署者影响

通过在网络上传输每个版本化通知的已版本化和未版本化通知格式来确保与 Mitaka 之前的通知消费者的向后兼容性,使用配置的驱动程序。在 Mitaka 中,此配置选项的默认版本将为 both

将引入一个新的配置选项 notification_format,具有三个可能的值 versionedun-versionedboth,以指定应发出哪些版本的通知。从一开始就将弃用 un-versioned 值,以鼓励部署者开始消费版本化通知。

开发人员影响

开发者在实现新通知时应使用通知基本类。

实现

负责人

主要负责人
  • balazs-gibizer

其他贡献者
  • belliott

  • andrea-rosa-m

工作项

  • 创建必要的基架基础设施,例如基本类、示例生成、基本测试基础设施、文档

  • 创建一个版本化通知,用于简单的旧式通知(例如 keypair 通知),作为示例

  • 创建 instance.update 通知版本化

  • 创建 nova.notification.send_api_fault 类型通知的版本化

依赖项

测试

应为版本化通知提供功能测试覆盖。

文档影响

  • 应为版本化通知生成通知示例。

  • 将创建一个新的 devref,描述如何将新的版本化通知添加到 nova

参考资料

历史

修订版

发布名称

描述

Mitaka

引入