diff --git a/specs/mitaka/approved/versioned-notification-api.rst b/specs/mitaka/approved/versioned-notification-api.rst new file mode 100644 index 000000000..1210949f2 --- /dev/null +++ b/specs/mitaka/approved/versioned-notification-api.rst @@ -0,0 +1,781 @@ +.. + This work is licensed under a Creative Commons Attribution 3.0 Unported + License. + + http://creativecommons.org/licenses/by/3.0/legalcode + +========================== +Versioned notification API +========================== + +https://blueprints.launchpad.net/nova/+spec/versioned-notification-api + +The notification interface of nova is not well defined and the current +notifications define a very inconsistent interface. There is no easy +way to see from the notification consumer point of view what is the format +and the content of the notification nova sends. + +Problem description +=================== + +This is the generic notification envelope format supported by 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": {} + } + +The problematic fields are: + +* priority +* event_type +* publisher_id +* payload + + +priority: Nova uses info and error priorities in the current code base except +in case of the nova.notification.notify_decorator code where the priority is +configurable with the notification_level configuration parameter. However this +decorator is only used in the monkey_patch_modules configuration default value. + + +event_type: oslo allows a raw string to be sent as event_type, nova uses the +following event_type formats today: + +* ... example: compute.instance.create.end +* .. example: aggregate.removehost.end +* . example: servergroup.create +* .. example: scheduler.select_destinations.end +* example: snapshot_instance +* . example: compute_task.build_instances + + +publisher_id: nova uses the following publisher_id formats today: + +* .controller examples: api.controller, compute.controller +* .controller example: servergroup.controller +* . example: aggregate. and + aggregate.. See: [2]. + +It seems that the content of publisher_id and event_type overlaps in some +cases. + +payload: nova does not have any restriction on the payload field which +leads to very many different formats. Sometimes it is a view of an existing +nova versioned object e.g. in case of compute.instance.update notification +nova dumps the fields of the instance object into the notification after some +filtering. In other case nova dumps the exception object or dumps the args and +kwargs of a function into the payload. This complex payload format seems to be +the biggest problem for notification consumers. + +Use Cases +--------- + +As a tool developer I want to consume nova notifications to implement my +requirements. I want to know what is the format of the notifications and I want +to have some way to detect and follow up the changes in the notification format +later on. + +Proposed change +=============== + +This spec is created to agree on the format, content and meaning of the fields +in notification sent by nova and to propose way to change the existing +notifications to the new format while giving time to the notification +consumers to adapt to the change. Also it tries to give a technical solution to +keep the notification payload more stable and versioned. + +Current notifications are un-versioned. This spec proposes to transform the +un-versioned notification to versioned notifications while keeping the +possibility to emit un-versioned notifications for limited time to help the +transition for the notification consumers. + +Versioned notifications will have a well defined format which is documented and +notification samples will be provided similarly to nova api samples. +New versions of a versioned notification will be kept backward compatible. + +To model and version the new notifications nova will use the oslo +versionedobject module. To emit such notification nova will continue to use +the notifier interface of oslo.messaging module. To convert the notification +model to the format that can be fed into the notifier interface nova will use +the existing NovaObjectSerializer. + +A single versioned notification will be modeled with a single oslo versioned +object but that object can use other new or existing versioned object as +payload field. + +However some of the today's notifications cannot be really converted to +versioned notifications. For example the notify_decorator dumps the args and +kwargs of any function into the notification payload therefore we cannot create +a single versioned model for every possible payload it generates. For these +notifications a generic, semi-managed, dict based payload can be defined +that formulates as much as possible and leaves the rest of the payload +un-managed. Adding new semi-managed notifications shall be avoided in the +future. + +We want to keep the notification envelope format defined by the notifier +interface in oslo.messaging, therefore versioned notifications will have the +same envelope on the wire as the un-versioned notifications. +Which is the following:: + + { + "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": {} + } + +The main difference between the wire format of the versioned and un-versioned +notification is the format of the payload field. The versioned notification +wire format will use the serialized format of a versioned object as payload. + +The versioned notification model will define versioned object fields for every +fields oslo.messaging notifier interface needs (priority, event_type, +publisher_id, payload) so that a single notification can be fully modeled in +nova code. However only the payload field will use the default versioned object +serialization. The other fields in the envelope will be filled with strings as +in the example above. + +The value of the event_type field of the envelope on the wire will be defined +by the name of the affected object, the name of the performed action emitting +the notification and the phase of the action. For example: instance.create.end, +aggregate.removehost.start, filterscheduler.select_destinations.end. +The notification model will do basic validation on the content of the +event_type e.g. enum for valid phases will be created. + +The value of the the priority field of the envelope on the wire can be selected +from the predefined priorities in oslo.messaging (audit, debug, info, warn, +error, critical, sample) except 'warning' (use warn instead). +The notification model will do validation of the priority by providing an enum +with the valid priorities. + +For concrete examples see the Data model impact section. + +Backward compatibility +---------------------- + +The new notification model can be used to emit the current un-versioned +notification as well to provide backward compatibility while the un-versioned +notification will be deprecated. Nova might want to restrict adding new +un-versioned notification after this spec is implemented. + +A new version of a versioned notification has to be backward compatible with +the previous version. Nova will always emit the latest version of a versioned +notification and nova will not support pinning back the notification versions. + +Backward compatibility for pre Mitaka notification consumers will be ensured +by emitting both the verisoned and the un-versioned notification format on the +wire on separate topics. The new notification model will provide +a way to emit both old and new wire format from a same notification object. +A configuration option will be provided to specify which version of the +notifications shall be emitted but asking for the old format only will be +deprecated from the beginning. Emitting the un-versioned wire format of a +versioned notification will be deprecated along with a proper deprecation +message in Mitaka and will be removed in N release. + + + +Alternatives +------------ + +Version the whole wire format instead of only the payload: + +There seems to be two main alternatives how to generate the actual notification +message on the wire from the KeyPairNotification object defined in the Data +model impact section. + +Use the current envelope structure defined by the notifier in oslo.messaging +[1] and use the versioning of the payload on the wire as proposed in the +Data model impact section. + +Pros: + +* No oslo.messaging change is required. +* Consumers only need to change the payload parsing code. +* Notification envelope in the whole OpenStack ecosystem are the same. + +Cons: + +* The envelope on the wire is not versioned just the payload field of + it. However the envelope structure is generic and well defined by + oslo.messaging. + +Or alternatively create a new envelope structure in oslo.messaging that already +a versioned object and use the serialized form of that object on the wire. +If we change oslo.messaging to provide an interface where an object inheriting +from NotificationBase object can be passed in and oslo.messaging uses the +serialized from of that object as the message directly then KeyPair +notification message on the wire would look like the following:: + + { + "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" + } + +In this case the NotificationBase classes shall be provided by the +oslo.messaging. + +Pros: + +* The whole message on the wire are versioned. + +Cons: + +* Needs extensive changes in oslo.messaging in the notification interface code + as well as in the notification drivers as today notification drivers depend + on the current envelope structure. +* It would create a circular dependency between oslo.messaging and + oslo.versionedobject +* Consumers need to adapt to the top level structure change as well. + +Use a single global notification version: + +The proposal is to use separate version number per notification. Alternatively +a single global notification version number can be defined that is bumped every +time when a single notification has been changed. + + +Data model impact +----------------- + +The following base objects will be defined: + +.. code-block:: python + + 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. """ + +Note that the publisher field of the NotificationBase will be used to fill the +publisher_id field of the envelope in the wire format by extracting the name of +the service and the host the service runs on from the Service object. + +Then here is a concrete example that uses the base object: + +.. code-block:: python + + @base.NovaObjectRegistry.register + class KeyPairNotification(notification.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'payload': fields.ObjectField('KeyPair') + } + +Where the referred KeyPair object is an already existing versioned object in +nova. Then the current keypair notification sending code can be written like: + +.. code-block:: python + + 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) + + + +When defining the payload model for a versioned notification we will try to +reuse the existing nova versioned objects like in case of the KeyPair example +above. If that is not possible a new versioned object for the payload will be +created. + +The wire format of the above KeyPair notification will look like the +followings:: + + { + "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" + } + +For an alternative wire format see the Alternatives section. + +Semi managed notification example +--------------------------------- + +The nova.exceptions.wrap_exception decorator is used to send notification in +case an exception happens during the decorated function. Today this +notification has the following structure:: + + { + event_type: , + publisher_id: , + payload: { + exception: + args: + } + timestamp: ... + message_id: ... + } + + +We can define a following semi managed notification object for it:: + + @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') + } + +Where the ArgDictField takes any python object, it uses object serialisation +when available, otherwise, a primitive->json conversion, +but if that fails, it just stringifies the object. +This field does not have a well defined wire format so this part of the +notification will not be really versioned, hence the semi versioned name. + + +send_api_fault notification example +----------------------------------- +The nova.notifications.send_api_fault function is used to send notification in +case of api faults. The current format of the notification is the following:: + + { + event_type: "api.fault", + publisher_id: "api.myhost", + payload: { + "url": , + "exception": , + "status": + } + timestamp: ... + message_id: ... + } + +We can define the following managed notification object for it:: + + @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 notification example +------------------------------------ +The nova.notifications.send_update function is used today to send notification +about the change of the instance. Here is an example of the current +notification format:: + + { + "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":{ + + }, + } + } + +We can define the following managed notification object for it:: + + @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') + } + + +No db schema changes are foreseen. + +REST API impact +--------------- +None. + +Security impact +--------------- +None. + +Notifications impact +-------------------- + +See the Proposed change and Data model section. + +Other end user impact +--------------------- + +None. + +Performance Impact +------------------ + +Sending both un-versioned and versioned wire format for a notification due to +keeping backward compatibility in Mitaka will increase the load on the message +bus. A config option will be provided to specify which version of the +notificatios shall be emited to mitigate this. Also the deployer can use NoOp +notification driver to turn the interface off. + +Other deployer impact +--------------------- + +Backward compatibility for pre Mitaka notification consumers will be ensured +by emitting both the verisoned and the un-versioned notification format on the +wire for every versioned notification using the configured driver. Emitting the +un-versioned wire format of a versioned notification will be deprecated along +with a proper deprecation message in Mitaka and will be removed in N release. + +A new config option ``notification_format`` will be introduced with three +possible values ``versioned``, ``un-versioned``, ``both`` to specify which +version of the notifications shall be emited. The ``un-versioned`` value will +be deprecated from the beginning to encourage deployers to start consuming +versioned notifications. In Mitaka the default version of this config option +will be ``both``. + +Developer impact +---------------- + +Developers shall use the notification base classes when implementing a new +notification. + + +Implementation +============== + +Assignee(s) +----------- + + +Primary assignee: + * balazs-gibizer + +Other contributors: + * belliott + * andrea-rosa-m + +Work Items +---------- + +* Create the necessary base infrastructure e.g base classes, sample generation, + basic test infrastructure, documentation +* Create a versioned notifications for an easy old style notification + (e.g. keypair notifications) to serve as an example +* Create versioned notification for instance.update notification +* Create versioned notifications for nova.notification.send_api_fault type of + notifications + + +Dependencies +============ + +None + +Testing +======= + +Functional test coverage shall be provided for versioned notifications. + + +Documentation Impact +==================== + +* Notification samples shall be generated for versioned notifications. +* A new devref shall be created that describe how to add new versioned + notifications to nova + + +References +========== + +* [1] http://docs.openstack.org/developer/oslo.messaging/notifier.html +* [2] https://github.com/openstack/nova/blob/master/nova/compute/utils.py#L320 +* [3] https://github.com/openstack/nova/blob/bc6f30de953303604625e84ad2345cfb595170d2/nova/compute/api.py#L3769 +* [4] The service status notification will be the first new notification using + a versisoned payload https://review.openstack.org/#/c/182350/ . That spec + will add only a minimal infrastructure to emit the versioned payload. + + +History +======= + +.. list-table:: Revisions + :header-rows: 1 + + * - Release Name + - Description + * - Mitaka + - Introduced