diff --git a/doc/api_samples/images/images-details-get-resp.json b/doc/api_samples/images/images-details-get-resp.json index dfe6ca16faae..034c35f0c081 100644 --- a/doc/api_samples/images/images-details-get-resp.json +++ b/doc/api_samples/images/images-details-get-resp.json @@ -207,6 +207,7 @@ } ], "metadata": { + "architecture": "x86_64", "kernel_id": "nokernel", "ramdisk_id": "nokernel" }, diff --git a/doc/notification_samples/common_payloads/ComputeTaskPayload.json b/doc/notification_samples/common_payloads/ComputeTaskPayload.json new file mode 100644 index 000000000000..387ff4b01e77 --- /dev/null +++ b/doc/notification_samples/common_payloads/ComputeTaskPayload.json @@ -0,0 +1,11 @@ +{ + "nova_object.version": "1.0", + "nova_object.namespace": "nova", + "nova_object.name": "ComputeTaskPayload", + "nova_object.data": { + "instance_uuid": "d5e6a7b7-80e5-4166-85a3-cd6115201082", + "reason": {"$ref": "ExceptionPayload.json#"}, + "request_spec": {"$ref": "RequestSpecPayload.json#"}, + "state": "error" + } +} diff --git a/doc/notification_samples/common_payloads/ExceptionPayload.json b/doc/notification_samples/common_payloads/ExceptionPayload.json new file mode 100644 index 000000000000..c9dd81504739 --- /dev/null +++ b/doc/notification_samples/common_payloads/ExceptionPayload.json @@ -0,0 +1,12 @@ +{ + "nova_object.version": "1.1", + "nova_object.namespace": "nova", + "nova_object.name": "ExceptionPayload", + "nova_object.data": { + "function_name": "_schedule_instances", + "module_name": "nova.conductor.manager", + "exception": "NoValidHost", + "exception_message": "No valid host was found. There are not enough hosts available.", + "traceback": "Traceback (most recent call last):\n File \"nova/conductor/manager.py\", line ..." + } +} diff --git a/doc/notification_samples/common_payloads/ImageMetaPayload.json b/doc/notification_samples/common_payloads/ImageMetaPayload.json new file mode 100644 index 000000000000..5ea3ed7f5adb --- /dev/null +++ b/doc/notification_samples/common_payloads/ImageMetaPayload.json @@ -0,0 +1,28 @@ +{ + "nova_object.namespace": "nova", + "nova_object.data": { + "checksum": null, + "container_format": "raw", + "created_at": "2011-01-01T01:02:03Z", + "direct_url": null, + "disk_format": "raw", + "id": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "min_disk": 0, + "min_ram": 0, + "name": "fakeimage123456", + "owner": null, + "properties": {"$ref":"ImageMetaPropsPayload.json#"}, + "protected": false, + "size": 25165824, + "status": "active", + "tags": [ + "tag1", + "tag2" + ], + "updated_at": "2011-01-01T01:02:03Z", + "virtual_size": null, + "visibility": "public" + }, + "nova_object.name": "ImageMetaPayload", + "nova_object.version": "1.0" +} diff --git a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json new file mode 100644 index 000000000000..e5c32fa61376 --- /dev/null +++ b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json @@ -0,0 +1,8 @@ +{ + "nova_object.namespace": "nova", + "nova_object.data": { + "hw_architecture": "x86_64" + }, + "nova_object.name": "ImageMetaPropsPayload", + "nova_object.version": "1.0" +} diff --git a/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json b/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json new file mode 100644 index 000000000000..9dcad38d08df --- /dev/null +++ b/doc/notification_samples/common_payloads/InstanceNUMACellPayload.json @@ -0,0 +1,16 @@ +{ + "nova_object.version": "1.0", + "nova_object.namespace": "nova", + "nova_object.name": "InstanceNUMACellPayload", + "nova_object.data": { + "cpu_pinning_raw": null, + "cpu_policy": null, + "cpu_thread_policy": null, + "cpu_topology": null, + "cpuset": [0], + "cpuset_reserved": null, + "id": 0, + "memory": 512, + "pagesize": null + } +} diff --git a/doc/notification_samples/common_payloads/InstanceNUMATopologyPayload.json b/doc/notification_samples/common_payloads/InstanceNUMATopologyPayload.json new file mode 100644 index 000000000000..cf28b2f43323 --- /dev/null +++ b/doc/notification_samples/common_payloads/InstanceNUMATopologyPayload.json @@ -0,0 +1,12 @@ +{ + "nova_object.version": "1.0", + "nova_object.namespace": "nova", + "nova_object.name": "InstanceNUMATopologyPayload", + "nova_object.data": { + "cells": [ + {"$ref": "InstanceNUMACellPayload.json#"} + ], + "emulator_threads_policy": null, + "instance_uuid": "75cab9f7-57e2-4bd1-984f-a0383d9ee60e" + } +} diff --git a/doc/notification_samples/common_payloads/InstancePCIRequestsPayload.json b/doc/notification_samples/common_payloads/InstancePCIRequestsPayload.json new file mode 100644 index 000000000000..3ab04139f1cb --- /dev/null +++ b/doc/notification_samples/common_payloads/InstancePCIRequestsPayload.json @@ -0,0 +1,9 @@ +{ + "nova_object.version": "1.0", + "nova_object.namespace": "nova", + "nova_object.name": "InstancePCIRequestsPayload", + "nova_object.data":{ + "instance_uuid": "d5e6a7b7-80e5-4166-85a3-cd6115201082", + "requests": [] + } +} diff --git a/doc/notification_samples/common_payloads/RequestSpecPayload.json b/doc/notification_samples/common_payloads/RequestSpecPayload.json new file mode 100644 index 000000000000..98bcb9852a31 --- /dev/null +++ b/doc/notification_samples/common_payloads/RequestSpecPayload.json @@ -0,0 +1,25 @@ +{ + "nova_object.namespace": "nova", + "nova_object.data": { + "availability_zone": null, + "flavor": { + "$ref": "FlavorPayload.json#", + "nova_object.data": { + "extra_specs": { + "hw:numa_cpus.0": "0", + "hw:numa_mem.0": "512", + "hw:numa_nodes": "1" + } + } + }, + "image": {"$ref": "ImageMetaPayload.json#"}, + "instance_uuid": "d5e6a7b7-80e5-4166-85a3-cd6115201082", + "num_instances": 1, + "numa_topology": {"$ref": "InstanceNUMATopologyPayload.json#"}, + "pci_requests": {"$ref": "InstancePCIRequestsPayload.json#"}, + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + }, + "nova_object.name": "RequestSpecPayload", + "nova_object.version": "1.0" +} diff --git a/doc/notification_samples/compute_task-build_instances-error.json b/doc/notification_samples/compute_task-build_instances-error.json new file mode 100644 index 000000000000..e904e8c1981b --- /dev/null +++ b/doc/notification_samples/compute_task-build_instances-error.json @@ -0,0 +1,6 @@ +{ + "event_type": "compute_task.build_instances.error", + "payload": {"$ref":"common_payloads/ComputeTaskPayload.json#"}, + "priority": "ERROR", + "publisher_id": "nova-conductor:fake-mini" +} diff --git a/doc/notification_samples/compute_task-migrate_server-error.json b/doc/notification_samples/compute_task-migrate_server-error.json new file mode 100644 index 000000000000..848b4da37f95 --- /dev/null +++ b/doc/notification_samples/compute_task-migrate_server-error.json @@ -0,0 +1,11 @@ +{ + "event_type": "compute_task.migrate_server.error", + "payload": { + "$ref":"common_payloads/ComputeTaskPayload.json#", + "nova_object.data":{ + "state": "active" + } + }, + "priority": "ERROR", + "publisher_id": "nova-conductor:fake-mini" +} diff --git a/doc/notification_samples/compute_task-rebuild_server-error.json b/doc/notification_samples/compute_task-rebuild_server-error.json new file mode 100644 index 000000000000..398600560b7f --- /dev/null +++ b/doc/notification_samples/compute_task-rebuild_server-error.json @@ -0,0 +1,8 @@ +{ + "event_type": "compute_task.rebuild_server.error", + "payload": { + "$ref": "common_payloads/ComputeTaskPayload.json#" + }, + "priority": "ERROR", + "publisher_id": "nova-conductor:fake-mini" +} diff --git a/nova/compute/utils.py b/nova/compute/utils.py index 813cc2e609cf..02e513d2d42a 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -36,6 +36,7 @@ from nova import exception from nova import notifications from nova.notifications.objects import aggregate as aggregate_notification from nova.notifications.objects import base as notification_base +from nova.notifications.objects import compute_task as task_notification from nova.notifications.objects import exception as notification_exception from nova.notifications.objects import flavor as flavor_notification from nova.notifications.objects import instance as instance_notification @@ -857,6 +858,41 @@ def notify_about_volume_usage(context, vol_usage, host): notification.emit(context) +@rpc.if_notifications_enabled +def notify_about_compute_task_error(context, action, instance_uuid, + request_spec, state, exception, tb): + """Send a versioned notification about compute task error. + + :param context: the request context + :param action: the name of the action + :param instance_uuid: the UUID of the instance + :param request_spec: the request spec object or + the dict includes request spec information + :param state: the vm state of the instance + :param exception: the thrown exception + :param tb: the traceback + """ + if (request_spec is not None and + not isinstance(request_spec, objects.RequestSpec)): + request_spec = objects.RequestSpec.from_primitives( + context, request_spec, {}) + + fault, _ = _get_fault_and_priority_from_exc_and_tb(exception, tb) + payload = task_notification.ComputeTaskPayload( + instance_uuid=instance_uuid, request_spec=request_spec, state=state, + reason=fault) + notification = task_notification.ComputeTaskNotification( + priority=fields.NotificationPriority.ERROR, + publisher=notification_base.NotificationPublisher( + host=CONF.host, source=fields.NotificationSource.CONDUCTOR), + event_type=notification_base.EventType( + object='compute_task', + action=action, + phase=fields.NotificationPhase.ERROR), + payload=payload) + notification.emit(context) + + def refresh_info_cache_for_instance(context, instance): """Refresh the info cache for an instance. diff --git a/nova/notifications/objects/base.py b/nova/notifications/objects/base.py index 26012dfbc898..70ce3391c950 100644 --- a/nova/notifications/objects/base.py +++ b/nova/notifications/objects/base.py @@ -67,7 +67,9 @@ class EventType(NotificationObject): # NotificationActionField enum # Version 1.16: CONNECT is added to NotificationActionField enum # Version 1.17: USAGE is added to NotificationActionField enum - VERSION = '1.17' + # Version 1.18: ComputeTask related values have been added to + # NotificationActionField enum + VERSION = '1.18' fields = { 'object': fields.StringField(nullable=False), @@ -119,7 +121,7 @@ class NotificationPayloadBase(NotificationObject): self.populated = not self.SCHEMA @rpc.if_notifications_enabled - def populate_schema(self, **kwargs): + def populate_schema(self, set_none=True, **kwargs): """Populate the object based on the SCHEMA and the source objects :param kwargs: A dict contains the source object at the key defined in @@ -137,14 +139,15 @@ class NotificationPayloadBase(NotificationObject): NotImplementedError, exception.OrphanedObjectError, ovo_exception.OrphanedObjectError): - # If it is unset or non lazy loadable in the source object - # then we cannot do anything else but try to default it in the - # payload object we are generating here. - # NOTE(gibi): This will fail if the payload field is not - # nullable, but that means that either the source object is not - # properly initialized or the payload field needs to be defined - # as nullable - setattr(self, key, None) + if set_none: + # If it is unset or non lazy loadable in the source object + # then we cannot do anything else but try to default it + # in the payload object we are generating here. + # NOTE(gibi): This will fail if the payload field is not + # nullable, but that means that either the source object + # is not properly initialized or the payload field needs + # to be defined as nullable + setattr(self, key, None) except Exception: with excutils.save_and_reraise_exception(): LOG.error('Failed trying to populate attribute "%s" ' diff --git a/nova/notifications/objects/compute_task.py b/nova/notifications/objects/compute_task.py new file mode 100644 index 000000000000..fe087dbb9396 --- /dev/null +++ b/nova/notifications/objects/compute_task.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.notifications.objects import base +from nova.notifications.objects import request_spec as reqspec_payload +from nova.objects import base as nova_base +from nova.objects import fields + + +@nova_base.NovaObjectRegistry.register_notification +class ComputeTaskPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'instance_uuid': fields.UUIDField(), + # There are some cases that request_spec is None. + # e.g. Old instances can still have no RequestSpec object + # attached to them. + 'request_spec': fields.ObjectField('RequestSpecPayload', + nullable=True), + 'state': fields.InstanceStateField(nullable=True), + 'reason': fields.ObjectField('ExceptionPayload') + } + + def __init__(self, instance_uuid, request_spec, state, reason): + super(ComputeTaskPayload, self).__init__() + self.instance_uuid = instance_uuid + self.request_spec = reqspec_payload.RequestSpecPayload( + request_spec) if request_spec is not None else None + self.state = state + self.reason = reason + + +@base.notification_sample('compute_task-build_instances-error.json') +@base.notification_sample('compute_task-migrate_server-error.json') +@base.notification_sample('compute_task-rebuild_server-error.json') +@nova_base.NovaObjectRegistry.register_notification +class ComputeTaskNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('ComputeTaskPayload') + } diff --git a/nova/notifications/objects/image.py b/nova/notifications/objects/image.py new file mode 100644 index 000000000000..d68259731c91 --- /dev/null +++ b/nova/notifications/objects/image.py @@ -0,0 +1,261 @@ +# Copyright 2018 NTT Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.notifications.objects import base +from nova.objects import base as nova_base +from nova.objects import fields + + +@nova_base.NovaObjectRegistry.register_notification +class ImageMetaPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'id': ('image_meta', 'id'), + 'name': ('image_meta', 'name'), + 'status': ('image_meta', 'status'), + 'visibility': ('image_meta', 'visibility'), + 'protected': ('image_meta', 'protected'), + 'checksum': ('image_meta', 'checksum'), + 'owner': ('image_meta', 'owner'), + 'size': ('image_meta', 'size'), + 'virtual_size': ('image_meta', 'virtual_size'), + 'container_format': ('image_meta', 'container_format'), + 'disk_format': ('image_meta', 'disk_format'), + 'created_at': ('image_meta', 'created_at'), + 'updated_at': ('image_meta', 'updated_at'), + 'tags': ('image_meta', 'tags'), + 'direct_url': ('image_meta', 'direct_url'), + 'min_ram': ('image_meta', 'min_ram'), + 'min_disk': ('image_meta', 'min_disk') + } + + # NOTE(takashin): The reason that each field is nullable is as follows. + # + # a. It is defined as "The value might be null (JSON null data type)." + # in the "Show image" API (GET /v2/images/{image_id}) + # in the glance API v2 Reference. + # (https://developer.openstack.org/api-ref/image/v2/index.html) + # + # * checksum + # * container_format + # * disk_format + # * min_disk + # * min_ram + # * name + # * owner + # * size + # * updated_at + # * virtual_size + # + # b. It is optional in the response from glance. + # * direct_url + # + # a. It is defined as nullable in the ImageMeta object. + # * created_at + # + # c. It cannot be got in the boot from volume case. + # See VIM_IMAGE_ATTRIBUTES in nova/utils.py. + # + # * id (not 'image_id') + # * visibility + # * protected + # * status + # * tags + fields = { + 'id': fields.UUIDField(nullable=True), + 'name': fields.StringField(nullable=True), + 'status': fields.StringField(nullable=True), + 'visibility': fields.StringField(nullable=True), + 'protected': fields.FlexibleBooleanField(nullable=True), + 'checksum': fields.StringField(nullable=True), + 'owner': fields.StringField(nullable=True), + 'size': fields.IntegerField(nullable=True), + 'virtual_size': fields.IntegerField(nullable=True), + 'container_format': fields.StringField(nullable=True), + 'disk_format': fields.StringField(nullable=True), + 'created_at': fields.DateTimeField(nullable=True), + 'updated_at': fields.DateTimeField(nullable=True), + 'tags': fields.ListOfStringsField(nullable=True), + 'direct_url': fields.StringField(nullable=True), + 'min_ram': fields.IntegerField(nullable=True), + 'min_disk': fields.IntegerField(nullable=True), + 'properties': fields.ObjectField('ImageMetaPropsPayload') + } + + def __init__(self, image_meta): + super(ImageMetaPayload, self).__init__() + self.properties = ImageMetaPropsPayload( + image_meta_props=image_meta.properties) + self.populate_schema(image_meta=image_meta) + + +@nova_base.NovaObjectRegistry.register_notification +class ImageMetaPropsPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'hw_architecture': ('image_meta_props', 'hw_architecture'), + 'hw_auto_disk_config': ('image_meta_props', 'hw_auto_disk_config'), + 'hw_boot_menu': ('image_meta_props', 'hw_boot_menu'), + 'hw_cdrom_bus': ('image_meta_props', 'hw_cdrom_bus'), + 'hw_cpu_cores': ('image_meta_props', 'hw_cpu_cores'), + 'hw_cpu_sockets': ('image_meta_props', 'hw_cpu_sockets'), + 'hw_cpu_max_cores': ('image_meta_props', 'hw_cpu_max_cores'), + 'hw_cpu_max_sockets': ('image_meta_props', 'hw_cpu_max_sockets'), + 'hw_cpu_max_threads': ('image_meta_props', 'hw_cpu_max_threads'), + 'hw_cpu_policy': ('image_meta_props', 'hw_cpu_policy'), + 'hw_cpu_thread_policy': ('image_meta_props', 'hw_cpu_thread_policy'), + 'hw_cpu_realtime_mask': ('image_meta_props', 'hw_cpu_realtime_mask'), + 'hw_cpu_threads': ('image_meta_props', 'hw_cpu_threads'), + 'hw_device_id': ('image_meta_props', 'hw_device_id'), + 'hw_disk_bus': ('image_meta_props', 'hw_disk_bus'), + 'hw_disk_type': ('image_meta_props', 'hw_disk_type'), + 'hw_floppy_bus': ('image_meta_props', 'hw_floppy_bus'), + 'hw_firmware_type': ('image_meta_props', 'hw_firmware_type'), + 'hw_ipxe_boot': ('image_meta_props', 'hw_ipxe_boot'), + 'hw_machine_type': ('image_meta_props', 'hw_machine_type'), + 'hw_mem_page_size': ('image_meta_props', 'hw_mem_page_size'), + 'hw_numa_nodes': ('image_meta_props', 'hw_numa_nodes'), + 'hw_numa_cpus': ('image_meta_props', 'hw_numa_cpus'), + 'hw_numa_mem': ('image_meta_props', 'hw_numa_mem'), + 'hw_pointer_model': ('image_meta_props', 'hw_pointer_model'), + 'hw_qemu_guest_agent': ('image_meta_props', 'hw_qemu_guest_agent'), + 'hw_rescue_bus': ('image_meta_props', 'hw_rescue_bus'), + 'hw_rescue_device': ('image_meta_props', 'hw_rescue_device'), + 'hw_rng_model': ('image_meta_props', 'hw_rng_model'), + 'hw_serial_port_count': ('image_meta_props', 'hw_serial_port_count'), + 'hw_scsi_model': ('image_meta_props', 'hw_scsi_model'), + 'hw_video_model': ('image_meta_props', 'hw_video_model'), + 'hw_video_ram': ('image_meta_props', 'hw_video_ram'), + 'hw_vif_model': ('image_meta_props', 'hw_vif_model'), + 'hw_vm_mode': ('image_meta_props', 'hw_vm_mode'), + 'hw_watchdog_action': ('image_meta_props', 'hw_watchdog_action'), + 'hw_vif_multiqueue_enabled': ('image_meta_props', + 'hw_vif_multiqueue_enabled'), + 'img_bittorrent': ('image_meta_props', 'img_bittorrent'), + 'img_bdm_v2': ('image_meta_props', 'img_bdm_v2'), + 'img_block_device_mapping': ('image_meta_props', + 'img_block_device_mapping'), + 'img_cache_in_nova': ('image_meta_props', 'img_cache_in_nova'), + 'img_compression_level': ('image_meta_props', 'img_compression_level'), + 'img_hv_requested_version': ('image_meta_props', + 'img_hv_requested_version'), + 'img_hv_type': ('image_meta_props', 'img_hv_type'), + 'img_config_drive': ('image_meta_props', 'img_config_drive'), + 'img_linked_clone': ('image_meta_props', 'img_linked_clone'), + 'img_mappings': ('image_meta_props', 'img_mappings'), + 'img_owner_id': ('image_meta_props', 'img_owner_id'), + 'img_root_device_name': ('image_meta_props', 'img_root_device_name'), + 'img_use_agent': ('image_meta_props', 'img_use_agent'), + 'img_version': ('image_meta_props', 'img_version'), + 'img_signature': ('image_meta_props', 'img_signature'), + 'img_signature_hash_method': ('image_meta_props', + 'img_signature_hash_method'), + 'img_signature_certificate_uuid': ('image_meta_props', + 'img_signature_certificate_uuid'), + 'img_signature_key_type': ('image_meta_props', + 'img_signature_key_type'), + 'img_hide_hypervisor_id': ('image_meta_props', + 'img_hide_hypervisor_id'), + 'os_admin_user': ('image_meta_props', 'os_admin_user'), + 'os_command_line': ('image_meta_props', 'os_command_line'), + 'os_distro': ('image_meta_props', 'os_distro'), + 'os_require_quiesce': ('image_meta_props', 'os_require_quiesce'), + 'os_secure_boot': ('image_meta_props', 'os_secure_boot'), + 'os_skip_agent_inject_files_at_boot': ( + 'image_meta_props', 'os_skip_agent_inject_files_at_boot'), + 'os_skip_agent_inject_ssh': ('image_meta_props', + 'os_skip_agent_inject_ssh'), + 'os_type': ('image_meta_props', 'os_type'), + 'traits_required': ('image_meta_props', 'traits_required') + } + + fields = { + 'hw_architecture': fields.ArchitectureField(), + 'hw_auto_disk_config': fields.StringField(), + 'hw_boot_menu': fields.FlexibleBooleanField(), + 'hw_cdrom_bus': fields.DiskBusField(), + 'hw_cpu_cores': fields.IntegerField(), + 'hw_cpu_sockets': fields.IntegerField(), + 'hw_cpu_max_cores': fields.IntegerField(), + 'hw_cpu_max_sockets': fields.IntegerField(), + 'hw_cpu_max_threads': fields.IntegerField(), + 'hw_cpu_policy': fields.CPUAllocationPolicyField(), + 'hw_cpu_thread_policy': fields.CPUThreadAllocationPolicyField(), + 'hw_cpu_realtime_mask': fields.StringField(), + 'hw_cpu_threads': fields.IntegerField(), + 'hw_device_id': fields.IntegerField(), + 'hw_disk_bus': fields.DiskBusField(), + 'hw_disk_type': fields.StringField(), + 'hw_floppy_bus': fields.DiskBusField(), + 'hw_firmware_type': fields.FirmwareTypeField(), + 'hw_ipxe_boot': fields.FlexibleBooleanField(), + 'hw_machine_type': fields.StringField(), + 'hw_mem_page_size': fields.StringField(), + 'hw_numa_nodes': fields.IntegerField(), + 'hw_numa_cpus': fields.ListOfSetsOfIntegersField(), + 'hw_numa_mem': fields.ListOfIntegersField(), + 'hw_pointer_model': fields.PointerModelField(), + 'hw_qemu_guest_agent': fields.FlexibleBooleanField(), + 'hw_rescue_bus': fields.DiskBusField(), + 'hw_rescue_device': fields.BlockDeviceTypeField(), + 'hw_rng_model': fields.RNGModelField(), + 'hw_serial_port_count': fields.IntegerField(), + 'hw_scsi_model': fields.SCSIModelField(), + 'hw_video_model': fields.VideoModelField(), + 'hw_video_ram': fields.IntegerField(), + 'hw_vif_model': fields.VIFModelField(), + 'hw_vm_mode': fields.VMModeField(), + 'hw_watchdog_action': fields.WatchdogActionField(), + 'hw_vif_multiqueue_enabled': fields.FlexibleBooleanField(), + 'img_bittorrent': fields.FlexibleBooleanField(), + 'img_bdm_v2': fields.FlexibleBooleanField(), + 'img_block_device_mapping': + fields.ListOfDictOfNullableStringsField(), + 'img_cache_in_nova': fields.FlexibleBooleanField(), + 'img_compression_level': fields.IntegerField(), + 'img_hv_requested_version': fields.VersionPredicateField(), + 'img_hv_type': fields.HVTypeField(), + 'img_config_drive': fields.ConfigDrivePolicyField(), + 'img_linked_clone': fields.FlexibleBooleanField(), + 'img_mappings': fields.ListOfDictOfNullableStringsField(), + 'img_owner_id': fields.StringField(), + 'img_root_device_name': fields.StringField(), + 'img_use_agent': fields.FlexibleBooleanField(), + 'img_version': fields.IntegerField(), + 'img_signature': fields.StringField(), + 'img_signature_hash_method': fields.ImageSignatureHashTypeField(), + 'img_signature_certificate_uuid': fields.UUIDField(), + 'img_signature_key_type': fields.ImageSignatureKeyTypeField(), + 'img_hide_hypervisor_id': fields.FlexibleBooleanField(), + 'os_admin_user': fields.StringField(), + 'os_command_line': fields.StringField(), + 'os_distro': fields.StringField(), + 'os_require_quiesce': fields.FlexibleBooleanField(), + 'os_secure_boot': fields.SecureBootField(), + 'os_skip_agent_inject_files_at_boot': fields.FlexibleBooleanField(), + 'os_skip_agent_inject_ssh': fields.FlexibleBooleanField(), + 'os_type': fields.OSTypeField(), + 'traits_required': fields.ListOfStringsField() + } + + def __init__(self, image_meta_props): + super(ImageMetaPropsPayload, self).__init__() + # NOTE(takashin): If fields are not set in the ImageMetaProps object, + # it will not set the fields in the ImageMetaPropsPayload + # in order to avoid too many fields whose values are None. + self.populate_schema(set_none=False, image_meta_props=image_meta_props) diff --git a/nova/notifications/objects/request_spec.py b/nova/notifications/objects/request_spec.py new file mode 100644 index 000000000000..f372bafcaa69 --- /dev/null +++ b/nova/notifications/objects/request_spec.py @@ -0,0 +1,226 @@ +# Copyright 2018 NTT Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.notifications.objects import base +from nova.notifications.objects import flavor as flavor_payload +from nova.notifications.objects import image as image_payload +from nova.objects import base as nova_base +from nova.objects import fields + + +@nova_base.NovaObjectRegistry.register_notification +class RequestSpecPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'instance_uuid': ('request_spec', 'instance_uuid'), + 'project_id': ('request_spec', 'project_id'), + 'user_id': ('request_spec', 'user_id'), + 'availability_zone': ('request_spec', 'availability_zone'), + 'num_instances': ('request_spec', 'num_instances') + } + + fields = { + 'instance_uuid': fields.UUIDField(), + 'project_id': fields.StringField(nullable=True), + 'user_id': fields.StringField(nullable=True), + 'availability_zone': fields.StringField(nullable=True), + 'flavor': fields.ObjectField('FlavorPayload', nullable=True), + 'image': fields.ObjectField('ImageMetaPayload', nullable=True), + 'numa_topology': fields.ObjectField('InstanceNUMATopologyPayload', + nullable=True), + 'pci_requests': fields.ObjectField('InstancePCIRequestsPayload', + nullable=True), + 'num_instances': fields.IntegerField(default=1) + } + + def __init__(self, request_spec): + super(RequestSpecPayload, self).__init__() + self.flavor = flavor_payload.FlavorPayload( + request_spec.flavor) if request_spec.obj_attr_is_set( + 'flavor') else None + self.image = image_payload.ImageMetaPayload( + request_spec.image) if request_spec.image else None + if request_spec.numa_topology is not None: + if not request_spec.numa_topology.obj_attr_is_set('instance_uuid'): + request_spec.numa_topology.instance_uuid = ( + request_spec.instance_uuid) + self.numa_topology = InstanceNUMATopologyPayload( + request_spec.numa_topology) + else: + self.numa_topology = None + if request_spec.pci_requests is not None: + if not request_spec.pci_requests.obj_attr_is_set('instance_uuid'): + request_spec.pci_requests.instance_uuid = ( + request_spec.instance_uuid) + self.pci_requests = InstancePCIRequestsPayload( + request_spec.pci_requests) + else: + self.pci_requests = None + self.populate_schema(request_spec=request_spec) + + +@nova_base.NovaObjectRegistry.register_notification +class InstanceNUMATopologyPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'instance_uuid': ('numa_topology', 'instance_uuid'), + 'emulator_threads_policy': ('numa_topology', + 'emulator_threads_policy') + } + + fields = { + 'instance_uuid': fields.UUIDField(), + 'cells': fields.ListOfObjectsField('InstanceNUMACellPayload'), + 'emulator_threads_policy': fields.CPUEmulatorThreadsPolicyField( + nullable=True) + } + + def __init__(self, numa_topology): + super(InstanceNUMATopologyPayload, self).__init__() + self.cells = InstanceNUMACellPayload.from_numa_cell_list_obj( + numa_topology.cells) + self.populate_schema(numa_topology=numa_topology) + + +@nova_base.NovaObjectRegistry.register_notification +class InstanceNUMACellPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'id': ('numa_cell', 'id'), + 'cpuset': ('numa_cell', 'cpuset'), + 'memory': ('numa_cell', 'memory'), + 'pagesize': ('numa_cell', 'pagesize'), + 'cpu_pinning_raw': ('numa_cell', 'cpu_pinning_raw'), + 'cpu_policy': ('numa_cell', 'cpu_policy'), + 'cpu_thread_policy': ('numa_cell', 'cpu_thread_policy'), + 'cpuset_reserved': ('numa_cell', 'cpuset_reserved'), + } + + fields = { + 'id': fields.IntegerField(), + 'cpuset': fields.SetOfIntegersField(), + 'memory': fields.IntegerField(), + 'pagesize': fields.IntegerField(nullable=True), + 'cpu_topology': fields.ObjectField('VirtCPUTopologyPayload', + nullable=True), + 'cpu_pinning_raw': fields.DictOfIntegersField(nullable=True), + 'cpu_policy': fields.CPUAllocationPolicyField(nullable=True), + 'cpu_thread_policy': fields.CPUThreadAllocationPolicyField( + nullable=True), + 'cpuset_reserved': fields.SetOfIntegersField(nullable=True) + } + + def __init__(self, numa_cell): + super(InstanceNUMACellPayload, self).__init__() + if (numa_cell.obj_attr_is_set('cpu_topology') and + numa_cell.cpu_topology is not None): + self.cpu_topology = VirtCPUTopologyPayload(numa_cell.cpu_topology) + else: + self.cpu_topology = None + self.populate_schema(numa_cell=numa_cell) + + @classmethod + def from_numa_cell_list_obj(cls, numa_cell_list): + """Returns a list of InstanceNUMACellPayload objects + based on the passed list of InstanceNUMACell objects. + """ + payloads = [] + for numa_cell in numa_cell_list: + payloads.append(cls(numa_cell)) + return payloads + + +@nova_base.NovaObjectRegistry.register_notification +class VirtCPUTopologyPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'sockets': ('virt_cpu_topology', 'sockets'), + 'cores': ('virt_cpu_topology', 'cores'), + 'threads': ('virt_cpu_topology', 'threads'), + } + + fields = { + 'sockets': fields.IntegerField(nullable=True, default=1), + 'cores': fields.IntegerField(nullable=True, default=1), + 'threads': fields.IntegerField(nullable=True, default=1), + } + + def __init__(self, virt_cpu_topology): + super(VirtCPUTopologyPayload, self).__init__() + self.populate_schema(virt_cpu_topology=virt_cpu_topology) + + +@nova_base.NovaObjectRegistry.register_notification +class InstancePCIRequestsPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'instance_uuid': ('pci_requests', 'instance_uuid') + } + + fields = { + 'instance_uuid': fields.UUIDField(), + 'requests': fields.ListOfObjectsField('InstancePCIRequestPayload') + } + + def __init__(self, pci_requests): + super(InstancePCIRequestsPayload, self).__init__() + self.requests = InstancePCIRequestPayload.from_pci_request_list_obj( + pci_requests.requests) + self.populate_schema(pci_requests=pci_requests) + + +@nova_base.NovaObjectRegistry.register_notification +class InstancePCIRequestPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + + SCHEMA = { + 'count': ('pci_request', 'count'), + 'spec': ('pci_request', 'spec'), + 'alias_name': ('pci_request', 'alias_name'), + 'request_id': ('pci_request', 'request_id'), + 'numa_policy': ('pci_request', 'numa_policy') + } + + fields = { + 'count': fields.IntegerField(), + 'spec': fields.ListOfDictOfNullableStringsField(), + 'alias_name': fields.StringField(nullable=True), + 'request_id': fields.UUIDField(nullable=True), + 'numa_policy': fields.PCINUMAAffinityPolicyField(nullable=True) + } + + def __init__(self, pci_request): + super(InstancePCIRequestPayload, self).__init__() + self.populate_schema(pci_request=pci_request) + + @classmethod + def from_pci_request_list_obj(cls, pci_request_list): + """Returns a list of InstancePCIRequestPayload objects + based on the passed list of InstancePCIRequest objects. + """ + payloads = [] + for pci_request in pci_request_list: + payloads.append(cls(pci_request)) + return payloads diff --git a/nova/objects/fields.py b/nova/objects/fields.py index 6a27a42902c1..f6af9b1f4e77 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -833,6 +833,9 @@ class NotificationAction(BaseNovaEnum): UPDATE_PROP = 'update_prop' CONNECT = 'connect' USAGE = 'usage' + BUILD_INSTANCES = 'build_instances' + MIGRATE_SERVER = 'migrate_server' + REBUILD_SERVER = 'rebuild_server' ALL = (UPDATE, EXCEPTION, DELETE, PAUSE, UNPAUSE, RESIZE, VOLUME_SWAP, SUSPEND, POWER_ON, REBOOT, SHUTDOWN, SNAPSHOT, INTERFACE_ATTACH, @@ -845,7 +848,7 @@ class NotificationAction(BaseNovaEnum): SOFT_DELETE, TRIGGER_CRASH_DUMP, UNRESCUE, UNSHELVE, ADD_HOST, REMOVE_HOST, ADD_MEMBER, UPDATE_METADATA, LOCK, UNLOCK, REBUILD_SCHEDULED, UPDATE_PROP, LIVE_MIGRATION_FORCE_COMPLETE, - CONNECT, USAGE) + CONNECT, USAGE, BUILD_INSTANCES, MIGRATE_SERVER, REBUILD_SERVER) # TODO(rlrossit): These should be changed over to be a StateMachine enum from diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index cc9e42913540..61d8d3230291 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -18,6 +18,7 @@ import collections import functools import re import sys +import traceback from oslo_log import log as logging import oslo_messaging as messaging @@ -644,8 +645,10 @@ def set_vm_state_and_notify(context, instance_uuid, service, method, updates, reason=ex) event_type = '%s.%s' % (service, method) - # TODO(mriedem): Send a versioned notification. notifier.error(context, event_type, payload) + compute_utils.notify_about_compute_task_error( + context, method, instance_uuid, request_spec, vm_state, ex, + traceback.format_exc()) def build_filter_properties(scheduler_hints, forced_host, diff --git a/nova/tests/functional/api_sample_tests/api_samples/images/images-details-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/images/images-details-get-resp.json.tpl index 6f311fd744bf..6b56f72139aa 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/images/images-details-get-resp.json.tpl +++ b/nova/tests/functional/api_sample_tests/api_samples/images/images-details-get-resp.json.tpl @@ -206,6 +206,7 @@ } ], "metadata": { + "architecture": "x86_64", "kernel_id": "nokernel", "ramdisk_id": "nokernel" }, diff --git a/nova/tests/functional/notification_sample_tests/notification_sample_base.py b/nova/tests/functional/notification_sample_tests/notification_sample_base.py index 0ab78bbe27dc..a1aa33bc14ed 100644 --- a/nova/tests/functional/notification_sample_tests/notification_sample_base.py +++ b/nova/tests/functional/notification_sample_tests/notification_sample_base.py @@ -157,7 +157,7 @@ class NotificationSampleTestBase(test.TestCase, self.assertJsonEqual(sample_obj, notification) def _boot_a_server(self, expected_status='ACTIVE', extra_params=None, - scheduler_hints=None): + scheduler_hints=None, additional_extra_specs=None): # We have to depend on a specific image and flavor to fix the content # of the notification that will be emitted @@ -172,6 +172,8 @@ class NotificationSampleTestBase(test.TestCase, extra_specs = { "extra_specs": { "hw:watchdog_action": "disabled"}} + if additional_extra_specs: + extra_specs['extra_specs'].update(additional_extra_specs) self.admin_api.post_extra_spec(flavor_id, extra_specs) # Ignore the create flavor notification diff --git a/nova/tests/functional/notification_sample_tests/test_compute_task.py b/nova/tests/functional/notification_sample_tests/test_compute_task.py new file mode 100644 index 000000000000..3a40bd92d954 --- /dev/null +++ b/nova/tests/functional/notification_sample_tests/test_compute_task.py @@ -0,0 +1,119 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.tests import fixtures +from nova.tests.functional.api import client as api_client +from nova.tests.functional.notification_sample_tests \ + import notification_sample_base +from nova.tests.unit import fake_notifier + + +class TestComputeTaskNotificationSample( + notification_sample_base.NotificationSampleTestBase): + + def setUp(self): + self.flags(use_neutron=True) + super(TestComputeTaskNotificationSample, self).setUp() + self.neutron = fixtures.NeutronFixture(self) + self.useFixture(self.neutron) + + def test_build_instances_fault(self): + # Force down the compute node + service_id = self.api.get_service_id('nova-compute') + self.admin_api.put_service_force_down(service_id, True) + + server = self._boot_a_server( + expected_status='ERROR', + extra_params={'networks': [{'port': self.neutron.port_1['id']}]}, + additional_extra_specs={'hw:numa_nodes': 1, + 'hw:numa_cpus.0': '0', + 'hw:numa_mem.0': 512}) + self._wait_for_notification('compute_task.build_instances.error') + self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self._verify_notification( + 'compute_task-build_instances-error', + replacements={ + 'instance_uuid': server['id'], + 'request_spec.instance_uuid': server['id'], + 'request_spec.numa_topology.instance_uuid': server['id'], + 'request_spec.pci_requests.instance_uuid': server['id'], + 'reason.function_name': self.ANY, + 'reason.module_name': self.ANY, + 'reason.traceback': self.ANY + }, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + + def test_rebuild_fault(self): + server = self._boot_a_server( + extra_params={'networks': [{'port': self.neutron.port_1['id']}]}, + additional_extra_specs={'hw:numa_nodes': 1, + 'hw:numa_cpus.0': '0', + 'hw:numa_mem.0': 512}) + self._wait_for_notification('instance.create.end') + # Force down the compute node + service_id = self.api.get_service_id('nova-compute') + self.admin_api.put_service_force_down(service_id, True) + + fake_notifier.reset() + + # NOTE(takashin): The rebuild action and the evacuate action shares + # same code path. So the 'evacuate' action is used for this test. + post = {'evacuate': {}} + + self.admin_api.post_server_action(server['id'], post) + self._wait_for_notification('compute_task.rebuild_server.error') + # 0. instance.evacuate + # 1. compute_task.rebuild_server.error + self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self._verify_notification( + 'compute_task-rebuild_server-error', + replacements={ + 'instance_uuid': server['id'], + 'request_spec.instance_uuid': server['id'], + 'request_spec.numa_topology.instance_uuid': server['id'], + 'request_spec.pci_requests.instance_uuid': server['id'], + 'reason.function_name': self.ANY, + 'reason.module_name': self.ANY, + 'reason.traceback': self.ANY + }, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) + + def test_migrate_fault(self): + server = self._boot_a_server( + extra_params={'networks': [{'port': self.neutron.port_1['id']}]}, + additional_extra_specs={'hw:numa_nodes': 1, + 'hw:numa_cpus.0': '0', + 'hw:numa_mem.0': 512}) + self._wait_for_notification('instance.create.end') + # Force down the compute node + service_id = self.api.get_service_id('nova-compute') + self.admin_api.put_service_force_down(service_id, True) + + fake_notifier.reset() + + self.assertRaises(api_client.OpenStackApiException, + self.admin_api.post_server_action, + server['id'], {'migrate': None}) + self._wait_for_notification('compute_task.migrate_server.error') + self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self._verify_notification( + 'compute_task-migrate_server-error', + replacements={ + 'instance_uuid': server['id'], + 'request_spec.instance_uuid': server['id'], + 'request_spec.numa_topology.instance_uuid': server['id'], + 'request_spec.pci_requests.instance_uuid': server['id'], + 'reason.function_name': self.ANY, + 'reason.module_name': self.ANY, + 'reason.traceback': self.ANY + }, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) diff --git a/nova/tests/functional/test_compute_mgr.py b/nova/tests/functional/test_compute_mgr.py index 84ec634a15e9..77ee20b078e7 100644 --- a/nova/tests/functional/test_compute_mgr.py +++ b/nova/tests/functional/test_compute_mgr.py @@ -67,7 +67,11 @@ class ComputeManagerTestCase(test.TestCase): # Simulate that we're on the last retry attempt filter_properties = {'retry': {'num_attempts': 3}} request_spec = objects.RequestSpec.from_primitives( - self.context, {}, filter_properties) + self.context, + {'instance_properties': {'uuid': instance.uuid}, + 'instance_type': flavor, + 'image': None}, + filter_properties) self.compute.manager.build_and_run_instance( self.context, instance, {}, request_spec, filter_properties, block_device_mapping=[]) diff --git a/nova/tests/unit/conductor/test_conductor.py b/nova/tests/unit/conductor/test_conductor.py index e967ef3f8f51..d2acb9be9f15 100644 --- a/nova/tests/unit/conductor/test_conductor.py +++ b/nova/tests/unit/conductor/test_conductor.py @@ -1818,8 +1818,10 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): self.assertEqual(2, build_and_run_instance.call_count) self.assertEqual(2, len(instance_cells)) + @mock.patch('nova.compute.utils.notify_about_compute_task_error') @mock.patch('nova.scheduler.rpcapi.SchedulerAPI.select_destinations') - def test_schedule_and_build_scheduler_failure(self, select_destinations): + def test_schedule_and_build_scheduler_failure(self, select_destinations, + mock_notify): select_destinations.side_effect = Exception self.start_service('compute', host='fake-host') self.conductor.schedule_and_build_instances(**self.params) @@ -1830,6 +1832,17 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): self.assertEqual('error', instance.vm_state) self.assertIsNone(instance.task_state) + mock_notify.assert_called_once_with( + test.MatchType(context.RequestContext), 'build_instances', + instance.uuid, test.MatchType(dict), 'error', + test.MatchType(Exception), test.MatchType(str)) + request_spec_dict = mock_notify.call_args_list[0][0][3] + for key in ('instance_type', 'num_instances', 'instance_properties', + 'image'): + self.assertIn(key, request_spec_dict) + tb = mock_notify.call_args_list[0][0][6] + self.assertIn('Traceback (most recent call last):', tb) + @mock.patch('nova.objects.TagList.destroy') @mock.patch('nova.objects.TagList.create') @mock.patch('nova.compute.utils.notify_about_instance_action') @@ -1987,10 +2000,12 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): self.assertTrue(bury.called) self.assertFalse(build_and_run.called) + @mock.patch('nova.compute.utils.notify_about_compute_task_error') @mock.patch('nova.objects.quotas.Quotas.check_deltas') @mock.patch('nova.scheduler.rpcapi.SchedulerAPI.select_destinations') def test_schedule_and_build_over_quota_during_recheck(self, mock_select, - mock_check): + mock_check, + mock_notify): mock_select.return_value = [[fake_selection1]] # Simulate a race where the first check passes and the recheck fails. # First check occurs in compute/api. @@ -2047,6 +2062,17 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): request_specs = request_spec_get_all(self.ctxt) self.assertEqual(0, len(request_specs)) + mock_notify.assert_called_once_with( + test.MatchType(context.RequestContext), 'build_instances', + instance.uuid, test.MatchType(dict), 'error', + test.MatchType(exc.TooManyInstances), test.MatchType(str)) + request_spec_dict = mock_notify.call_args_list[0][0][3] + for key in ('instance_type', 'num_instances', 'instance_properties', + 'image'): + self.assertIn(key, request_spec_dict) + tb = mock_notify.call_args_list[0][0][6] + self.assertIn('Traceback (most recent call last):', tb) + @mock.patch('nova.compute.rpcapi.ComputeAPI.build_and_run_instance') @mock.patch('nova.objects.quotas.Quotas.check_deltas') @mock.patch('nova.scheduler.rpcapi.SchedulerAPI.select_destinations') @@ -2074,7 +2100,8 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): build_requests=1) self.assertTrue(mock_cm_get.called) - def test_bury_in_cell0(self): + @mock.patch('nova.compute.utils.notify_about_compute_task_error') + def test_bury_in_cell0(self, mock_notify): bare_br = self.params['build_requests'][0] inst_br = fake_build_request.fake_req_obj(self.ctxt) @@ -2120,11 +2147,41 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): self.assertEqual(expected, inst_states) + self.assertEqual(4, mock_notify.call_count) + mock_notify.assert_has_calls([ + mock.call( + test.MatchType(context.RequestContext), 'build_instances', + bare_br.instance_uuid, test.MatchType(dict), 'error', + test.MatchType(Exception), test.MatchType(str)), + mock.call( + test.MatchType(context.RequestContext), 'build_instances', + inst_br.instance_uuid, test.MatchType(dict), 'error', + test.MatchType(Exception), test.MatchType(str)), + mock.call( + test.MatchType(context.RequestContext), 'build_instances', + deleted_br.instance_uuid, test.MatchType(dict), 'error', + test.MatchType(Exception), test.MatchType(str)), + mock.call( + test.MatchType(context.RequestContext), 'build_instances', + fast_deleted_br.instance_uuid, test.MatchType(dict), 'error', + test.MatchType(Exception), test.MatchType(str))], + any_order=True) + + for i in range(0, 3): + # traceback.format_exc() returns 'NoneType' + # because an exception is not raised in this test. + # So the argument for traceback is not checked. + request_spec_dict = mock_notify.call_args_list[i][0][3] + for key in ('instance_type', 'num_instances', + 'instance_properties', 'image'): + self.assertIn(key, request_spec_dict) + + @mock.patch('nova.compute.utils.notify_about_compute_task_error') @mock.patch.object(objects.CellMapping, 'get_by_uuid') @mock.patch.object(conductor_manager.ComputeTaskManager, '_create_block_device_mapping') def test_bury_in_cell0_with_block_device_mapping(self, mock_create_bdm, - mock_get_cell): + mock_get_cell, mock_notify): mock_get_cell.return_value = self.cell_mappings['cell0'] inst_br = fake_build_request.fake_req_obj(self.ctxt) @@ -2140,6 +2197,17 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): mock_create_bdm.assert_called_once_with( self.cell_mappings['cell0'], inst.flavor, inst.uuid, self.params['block_device_mapping']) + mock_notify.assert_called_once_with( + test.MatchType(context.RequestContext), 'build_instances', + inst.uuid, test.MatchType(dict), 'error', + test.MatchType(Exception), test.MatchType(str)) + # traceback.format_exc() returns 'NoneType' + # because an exception is not raised in this test. + # So the argument for traceback is not checked. + request_spec_dict = mock_notify.call_args_list[0][0][3] + for key in ('instance_type', 'num_instances', 'instance_properties', + 'image'): + self.assertIn(key, request_spec_dict) def test_reset(self): with mock.patch('nova.compute.rpcapi.ComputeAPI') as mock_rpc: @@ -2724,8 +2792,10 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): block_device_mapping=mock.ANY, node='node2', limits=None, host_list=[]) + @mock.patch('nova.compute.utils.notify_about_compute_task_error') @mock.patch('nova.objects.Instance.save') - def test_build_instances_max_retries_exceeded(self, mock_save): + def test_build_instances_max_retries_exceeded(self, mock_save, + mock_notify): """Tests that when populate_retry raises MaxRetriesExceeded in build_instances, we don't attempt to cleanup the build request. """ @@ -2745,8 +2815,21 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): requested_networks, mock.sentinel.secgroups) mock_save.assert_called_once_with() + mock_notify.assert_called_once_with( + self.context, 'build_instances', + instance.uuid, test.MatchType(dict), 'error', + test.MatchType(exc.MaxRetriesExceeded), test.MatchType(str)) + request_spec_dict = mock_notify.call_args_list[0][0][3] + for key in ('instance_type', 'num_instances', 'instance_properties', + 'image'): + self.assertIn(key, request_spec_dict) + tb = mock_notify.call_args_list[0][0][6] + self.assertIn('Traceback (most recent call last):', tb) + + @mock.patch('nova.compute.utils.notify_about_compute_task_error') @mock.patch('nova.objects.Instance.save') - def test_build_instances_reschedule_no_valid_host(self, mock_save): + def test_build_instances_reschedule_no_valid_host(self, mock_save, + mock_notify): """Tests that when select_destinations raises NoValidHost in build_instances, we don't attempt to cleanup the build request if we're rescheduling (num_attempts>1). @@ -2770,6 +2853,17 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase): requested_networks, mock.sentinel.secgroups) mock_save.assert_called_once_with() + mock_notify.assert_called_once_with( + self.context, 'build_instances', + instance.uuid, test.MatchType(dict), 'error', + test.MatchType(exc.NoValidHost), test.MatchType(str)) + request_spec_dict = mock_notify.call_args_list[0][0][3] + for key in ('instance_type', 'num_instances', 'instance_properties', + 'image'): + self.assertIn(key, request_spec_dict) + tb = mock_notify.call_args_list[0][0][6] + self.assertIn('Traceback (most recent call last):', tb) + def test_cleanup_allocated_networks_none_requested(self): # Tests that we don't deallocate networks if 'none' were specifically # requested. diff --git a/nova/tests/unit/image/fake.py b/nova/tests/unit/image/fake.py index 8e4cd24c2afa..212ae39c82fd 100644 --- a/nova/tests/unit/image/fake.py +++ b/nova/tests/unit/image/fake.py @@ -52,6 +52,11 @@ class _FakeImageService(object): 'container_format': 'raw', 'disk_format': 'raw', 'size': '25165824', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': ['tag1', 'tag2'], 'properties': { 'kernel_id': 'nokernel', 'ramdisk_id': 'nokernel', @@ -68,6 +73,11 @@ class _FakeImageService(object): 'container_format': 'ami', 'disk_format': 'ami', 'size': '58145823', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': [], 'properties': {'kernel_id': 'nokernel', 'ramdisk_id': 'nokernel'}} @@ -82,8 +92,15 @@ class _FakeImageService(object): 'container_format': 'bare', 'disk_format': 'raw', 'size': '83594576', - 'properties': {'kernel_id': 'nokernel', - 'ramdisk_id': 'nokernel'}} + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': ['tag3', 'tag4'], + 'properties': { + 'kernel_id': 'nokernel', + 'ramdisk_id': 'nokernel', + 'architecture': obj_fields.Architecture.X86_64}} image4 = {'id': 'cedef40a-ed67-4d10-800e-17455edce175', 'name': 'fakeimage123456', @@ -96,6 +113,11 @@ class _FakeImageService(object): 'container_format': 'ami', 'disk_format': 'ami', 'size': '84035174', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': [], 'properties': {'kernel_id': 'nokernel', 'ramdisk_id': 'nokernel'}} @@ -110,6 +132,11 @@ class _FakeImageService(object): 'container_format': 'ami', 'disk_format': 'ami', 'size': '26360814', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': [], 'properties': {'kernel_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6', 'ramdisk_id': None}} @@ -125,6 +152,11 @@ class _FakeImageService(object): 'container_format': 'ova', 'disk_format': 'vhd', 'size': '49163826', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': [], 'properties': { 'kernel_id': 'nokernel', 'ramdisk_id': 'nokernel', @@ -142,6 +174,11 @@ class _FakeImageService(object): 'container_format': 'ova', 'disk_format': 'vhd', 'size': '74185822', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': [], 'properties': { 'kernel_id': 'nokernel', 'ramdisk_id': 'nokernel', diff --git a/nova/tests/unit/image/test_fake.py b/nova/tests/unit/image/test_fake.py index cc3fb6f1f1db..a2644e8c5872 100644 --- a/nova/tests/unit/image/test_fake.py +++ b/nova/tests/unit/image/test_fake.py @@ -40,7 +40,8 @@ class FakeImageServiceTestCase(test.NoDBTestCase): 'updated_at', 'deleted_at', 'deleted', 'status', 'is_public', 'properties', 'disk_format', 'container_format', - 'size'])) + 'size', 'min_disk', 'min_ram', + 'protected', 'tags', 'visibility'])) self.assertIsInstance(image['created_at'], datetime.datetime) self.assertIsInstance(image['updated_at'], datetime.datetime) diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 8ff731e20e5a..a3ffc4e172e2 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -370,11 +370,15 @@ notification_object_data = { 'AuditPeriodPayload': '1.0-2b429dd307b8374636703b843fa3f9cb', 'BandwidthPayload': '1.0-ee2616a7690ab78406842a2b68e34130', 'BlockDevicePayload': '1.0-29751e1b6d41b1454e36768a1e764df8', - 'EventType': '1.17-242397275522a04130b3af4c0ea926e2', + 'ComputeTaskNotification': '1.0-a73147b93b520ff0061865849d3dfa56', + 'ComputeTaskPayload': '1.0-e3d34762c14d131c98337b72e8c600e1', + 'EventType': '1.18-44f33a06fd08fdba0b7dc266116c017b', 'ExceptionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ExceptionPayload': '1.1-6c43008bd81885a63bc7f7c629f0793b', 'FlavorNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'FlavorPayload': '1.4-2e7011b8b4e59167fe8b7a0a81f0d452', + 'ImageMetaPayload': '1.0-0e65beeacb3393beed564a57bc2bc989', + 'ImageMetaPropsPayload': '1.0-0665065e198b4ab1b03aa80f442d2302', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionPayload': '1.7-8c77f0c85a83d325fded152376ca809a', 'InstanceActionRebuildNotification': @@ -398,6 +402,10 @@ notification_object_data = { 'InstanceActionSnapshotPayload': '1.8-6a3a66f823b56268ea4b759c83e38c31', 'InstanceExistsNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceExistsPayload': '1.1-b7095abb18f5b75f39dc1aa59942535d', + 'InstanceNUMACellPayload': '1.0-2f13614648bc46f2e29578a206561ef6', + 'InstanceNUMATopologyPayload': '1.0-247361b152047c18ae9ad1da2544a3c9', + 'InstancePCIRequestPayload': '1.0-12d0d61baf183daaafd93cbeeed2956f', + 'InstancePCIRequestsPayload': '1.0-6751cffe0c0fabd212aad624f672429a', 'InstanceStateUpdatePayload': '1.0-07e111c0fa0f6db0f79b0726d593e3da', 'InstanceUpdateNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceUpdatePayload': '1.8-375131acb12e612a460f68211a2b3a35', @@ -410,10 +418,12 @@ notification_object_data = { 'MetricsNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'MetricsPayload': '1.0-65c69b15b4de5a8c01971cb5bb9ab650', 'NotificationPublisher': '2.2-b6ad48126247e10b46b6b0240e52e614', + 'RequestSpecPayload': '1.0-ef9936c8da44e442e397b02dec3f6914', 'ServerGroupNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServerGroupPayload': '1.1-4ded2997ea1b07038f7af33ef5c45f7f', 'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServiceStatusPayload': '1.1-7b6856bd879db7f3ecbcd0ca9f35f92f', + 'VirtCPUTopologyPayload': '1.0-1b1600fe55465209682d96bbe3209f27', 'VolumeUsageNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'VolumeUsagePayload': '1.0-5f99d8b978a32040eecac0975e5a53e9', } diff --git a/nova/tests/unit/scheduler/test_scheduler_utils.py b/nova/tests/unit/scheduler/test_scheduler_utils.py index 032d82e63d05..d1393813a00a 100644 --- a/nova/tests/unit/scheduler/test_scheduler_utils.py +++ b/nova/tests/unit/scheduler/test_scheduler_utils.py @@ -59,10 +59,12 @@ class SchedulerUtilsTestCase(test.NoDBTestCase): mock_get.assert_called_once_with() self.assertIsInstance(request_spec['instance_properties'], dict) + @mock.patch('nova.compute.utils.notify_about_compute_task_error') @mock.patch('nova.rpc.LegacyValidatingNotifier') @mock.patch.object(compute_utils, 'add_instance_fault_from_exc') @mock.patch.object(objects.Instance, 'save') def _test_set_vm_state_and_notify(self, mock_save, mock_add, mock_notifier, + mock_notify_task, request_spec, payload_request_spec): expected_uuid = uuids.instance updates = dict(vm_state='fake-vm-state') @@ -94,6 +96,10 @@ class SchedulerUtilsTestCase(test.NoDBTestCase): mock_notifier.return_value.error.assert_called_once_with(self.context, event_type, payload) + mock_notify_task.assert_called_once_with( + self.context, method, expected_uuid, + payload_request_spec, updates['vm_state'], + exc_info, test.MatchType(str)) def test_set_vm_state_and_notify_request_spec_dict(self): """Tests passing a legacy dict format request spec to