diff --git a/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json b/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json new file mode 100644 index 0000000000..1fff907888 --- /dev/null +++ b/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json @@ -0,0 +1,31 @@ +{ + "$ref": "InstanceActionPayload.json", + "nova_object.data":{ + "new_flavor": { + "nova_object.name": "FlavorPayload", + "nova_object.data": { + "description": null, + "disabled": false, + "ephemeral_gb": 0, + "extra_specs": { + "hw:watchdog_action": "reset" + }, + "flavorid": "d5a8bb54-365a-45ae-abdb-38d249df7845", + "is_public": true, + "memory_mb": 256, + "name": "other_flavor", + "projects": null, + "root_gb": 1, + "rxtx_factor": 1.0, + "swap": 0, + "vcpu_weight": 0, + "vcpus": 1 + }, + "nova_object.namespace": "nova", + "nova_object.version": "1.4" + }, + "task_state": "resize_prep" + }, + "nova_object.name": "InstanceActionResizePrepPayload", + "nova_object.version": "1.0" +} diff --git a/doc/notification_samples/instance-resize_prep-end.json b/doc/notification_samples/instance-resize_prep-end.json new file mode 100644 index 0000000000..f96df3923b --- /dev/null +++ b/doc/notification_samples/instance-resize_prep-end.json @@ -0,0 +1,6 @@ +{ + "event_type": "instance.resize_prep.end", + "payload": {"$ref":"common_payloads/InstanceActionResizePrepPayload.json#"}, + "priority": "INFO", + "publisher_id": "nova-compute:compute" +} diff --git a/doc/notification_samples/instance-resize_prep-start.json b/doc/notification_samples/instance-resize_prep-start.json new file mode 100644 index 0000000000..abe3ad2d03 --- /dev/null +++ b/doc/notification_samples/instance-resize_prep-start.json @@ -0,0 +1,6 @@ +{ + "event_type": "instance.resize_prep.start", + "payload": {"$ref":"common_payloads/InstanceActionResizePrepPayload.json#"}, + "priority": "INFO", + "publisher_id": "nova-compute:compute" +} diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 3522a7414d..293c28b180 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -4105,6 +4105,9 @@ class ComputeManager(manager.Manager): current_period=True) self._notify_about_instance_usage( context, instance, "resize.prep.start") + compute_utils.notify_about_resize_prep_instance( + context, instance, self.host, + fields.NotificationPhase.START, instance_type) try: self._prep_resize(context, image, instance, instance_type, filter_properties, @@ -4138,6 +4141,9 @@ class ComputeManager(manager.Manager): self._notify_about_instance_usage( context, instance, "resize.prep.end", extra_usage_info=extra_usage_info) + compute_utils.notify_about_resize_prep_instance( + context, instance, self.host, + fields.NotificationPhase.END, instance_type) def _reschedule_resize_or_reraise(self, context, image, instance, exc_info, instance_type, request_spec, filter_properties): diff --git a/nova/compute/utils.py b/nova/compute/utils.py index e0354f5fc0..8cecba5a53 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -35,6 +35,7 @@ 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 exception as notification_exception +from nova.notifications.objects import flavor as flavor_notification from nova.notifications.objects import instance as instance_notification from nova.notifications.objects import keypair as keypair_notification from nova.notifications.objects import server_group as sg_notification @@ -561,6 +562,36 @@ def notify_about_instance_snapshot(context, instance, host, phase, payload=payload).emit(context) +@rpc.if_notifications_enabled +def notify_about_resize_prep_instance(context, instance, host, phase, + new_flavor): + """Send versioned notification about the instance resize action + on the instance + + :param context: the request context + :param instance: the instance which the resize action performed on + :param host: the host emitting the notification + :param phase: the phase of the action + :param new_flavor: new flavor + """ + + payload = instance_notification.InstanceActionResizePrepPayload( + instance=instance, + fault=None, + new_flavor=flavor_notification.FlavorPayload(flavor=new_flavor)) + + instance_notification.InstanceActionResizePrepNotification( + context=context, + priority=fields.NotificationPriority.INFO, + publisher=notification_base.NotificationPublisher( + host=host, source=fields.NotificationSource.COMPUTE), + event_type=notification_base.EventType( + object='instance', + action=fields.NotificationAction.RESIZE_PREP, + phase=phase), + payload=payload).emit(context) + + def notify_about_server_group_update(context, event_suffix, sg_payload): """Send a notification about server group update. diff --git a/nova/notifications/objects/instance.py b/nova/notifications/objects/instance.py index 817a509d9b..9a8a154f2d 100644 --- a/nova/notifications/objects/instance.py +++ b/nova/notifications/objects/instance.py @@ -215,6 +215,23 @@ class InstanceCreatePayload(InstanceActionPayload): for instance_tag in instance.tags] +@nova_base.NovaObjectRegistry.register_notification +class InstanceActionResizePrepPayload(InstanceActionPayload): + # No SCHEMA as all the additional fields are calculated + + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'new_flavor': fields.ObjectField('FlavorPayload', nullable=True) + } + + def __init__(self, instance, fault, new_flavor): + super(InstanceActionResizePrepPayload, self).__init__( + instance=instance, + fault=fault) + self.new_flavor = new_flavor + + @nova_base.NovaObjectRegistry.register_notification class InstanceUpdatePayload(InstancePayload): # Version 1.0: Initial version @@ -457,7 +474,6 @@ class InstanceStateUpdatePayload(base.NotificationPayloadBase): @base.notification_sample('instance-interface_detach-end.json') @base.notification_sample('instance-resize_confirm-start.json') @base.notification_sample('instance-resize_confirm-end.json') -# @base.notification_sample('instance-resize_prep-start.json') @base.notification_sample('instance-resize_revert-start.json') @base.notification_sample('instance-resize_revert-end.json') @base.notification_sample('instance-shelve_offload-start.json') @@ -532,6 +548,18 @@ class InstanceCreateNotification(base.NotificationBase): } +@base.notification_sample('instance-resize_prep-start.json') +@base.notification_sample('instance-resize_prep-end.json') +@nova_base.NovaObjectRegistry.register_notification +class InstanceActionResizePrepNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('InstanceActionResizePrepPayload') + } + + @base.notification_sample('instance-snapshot-start.json') @base.notification_sample('instance-snapshot-end.json') @nova_base.NovaObjectRegistry.register_notification diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index 35516c11ea..f9f7f560ed 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -742,9 +742,12 @@ class TestInstanceNotificationSample( self.api.post_server_action(server['id'], post) self._wait_for_state_change(self.api, server, 'VERIFY_RESIZE') - self.assertEqual(4, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self.assertEqual(6, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + # This list needs to be in order. expected_notifications = [ + 'instance-resize_prep-start', + 'instance-resize_prep-end', 'instance-resize-start', 'instance-resize-end', 'instance-resize_finish-start', @@ -763,19 +766,19 @@ class TestInstanceNotificationSample( self.api.post_server_action(server['id'], post) self._wait_for_state_change(self.api, server, 'ACTIVE') - self.assertEqual(6, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self.assertEqual(8, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self._verify_notification( 'instance-resize_revert-start', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[4]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[6]) self._verify_notification( 'instance-resize_revert-end', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id']}, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[5]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[7]) @mock.patch('nova.compute.manager.ComputeManager._reschedule', return_value=True) @@ -812,15 +815,18 @@ class TestInstanceNotificationSample( mock_prep_resize.side_effect = _build_resources self.api.post_server_action(server['id'], post) self._wait_for_notification('instance.resize.error') - self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS), - 'Unexpected number of notifications: %s' % - fake_notifier.VERSIONED_NOTIFICATIONS) + # 0: instance-resize_prep-start + # 1: instance-resize-error + # 2: instance-resize_prep-end + self.assertLessEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS), + 'Unexpected number of notifications: %s' % + fake_notifier.VERSIONED_NOTIFICATIONS) self._verify_notification('instance-resize-error', replacements={ 'reservation_id': server['reservation_id'], 'uuid': server['id'] }, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) @mock.patch('nova.compute.manager.ComputeManager._reschedule') @mock.patch('nova.compute.manager.ComputeManager._prep_resize') @@ -864,10 +870,14 @@ class TestInstanceNotificationSample( self.api.post_server_action(server['id'], post) self._wait_for_state_change(self.api, server, expected_status='ERROR') self._wait_for_notification('compute.exception') - # There should be two notifications, one for the instance.resize.error - # and one for the compute.exception via the wrap_exception decorator on - # the ComputeManager.prep_resize method. - self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS), + # There should be the following four notifications. + # 0: instance-resize_prep-start + # 1: instance-resize-error + # 2: instance-resize_prep-end + # 3: compute.exception + # (via the wrap_exception decorator on + # the ComputeManager.prep_resize method.) + self.assertEqual(4, len(fake_notifier.VERSIONED_NOTIFICATIONS), 'Unexpected number of notifications: %s' % fake_notifier.VERSIONED_NOTIFICATIONS) self._verify_notification('instance-resize-error', @@ -875,7 +885,7 @@ class TestInstanceNotificationSample( 'reservation_id': server['reservation_id'], 'uuid': server['id'] }, - actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) def _test_snapshot_server(self, server): post = {'createImage': {'name': 'test-snap'}} diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index e3a6030ed2..f70b511f31 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -5255,7 +5255,8 @@ class ComputeTestCase(BaseTestCase, self.assertEqual(payload['image_ref_url'], image_ref_url) self.compute.terminate_instance(self.context, instance, [], []) - def test_resize_instance_notification(self): + @mock.patch('nova.compute.utils.notify_about_resize_prep_instance') + def test_resize_instance_notification(self, mock_notify): # Ensure notifications on instance migrate/resize. old_time = datetime.datetime(2012, 4, 1) cur_time = datetime.datetime(2012, 12, 21, 12, 21) @@ -5307,6 +5308,11 @@ class ComputeTestCase(BaseTestCase, self.context) self.assertEqual(payload['image_ref_url'], image_ref_url) self.compute.terminate_instance(self.context, instance, [], []) + mock_notify.assert_has_calls([ + mock.call(self.context, instance, 'fake-mini', 'start', + instance_type), + mock.call(self.context, instance, 'fake-mini', 'end', + instance_type)]) def test_prep_resize_instance_migration_error_on_none_host(self): """Ensure prep_resize raises a migration error if destination host is diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 0d92bac540..bf099ffa95 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -7244,6 +7244,7 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase): self.assertFalse(do_cleanup) self.assertFalse(destroy_disks) + @mock.patch('nova.compute.utils.notify_about_resize_prep_instance') @mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename') @mock.patch('nova.objects.InstanceFault.create') @mock.patch('nova.objects.Instance.save') @@ -7251,9 +7252,10 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase): @mock.patch('nova.compute.utils.notify_about_instance_usage') @mock.patch('nova.compute.utils.is_volume_backed_instance', new=lambda *a: False) - def test_prep_resize_errors_migration(self, mock_niu, mock_notify, - mock_save, - mock_if, mock_cn): + def test_prep_resize_errors_migration(self, mock_niu, + mock_notify, mock_save, + mock_if, mock_cn, + mock_notify_resize): migration = mock.MagicMock() flavor = objects.Flavor(name='flavor', id=1) cn = objects.ComputeNode(uuid=uuids.compute) @@ -7296,6 +7298,15 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase): # Make sure we only called save once (kinda obviously must be true) migration.save.assert_called_once_with() + mock_notify_resize.assert_has_calls([ + mock.call(self.context, instance, 'fake-mini', + 'start', flavor), + mock.call(self.context, instance, 'fake-mini', + 'end', flavor), + mock.call(self.context, instance, 'fake-mini', + 'start', flavor), + mock.call(self.context, instance, 'fake-mini', + 'end', flavor)]) doit() diff --git a/nova/tests/unit/compute/test_compute_utils.py b/nova/tests/unit/compute/test_compute_utils.py index ad301614ff..b8b30382df 100644 --- a/nova/tests/unit/compute/test_compute_utils.py +++ b/nova/tests/unit/compute/test_compute_utils.py @@ -790,6 +790,41 @@ class UsageInfoTestCase(test.TestCase): self.assertEqual(payload['image_uuid'], uuids.fake_image_ref) self.assertEqual(payload['rescue_image_ref'], uuids.rescue_image_ref) + def test_notify_about_resize_prep_instance(self): + instance = create_instance(self.context) + + new_flavor = flavors.get_flavor_by_name('m1.small') + + compute_utils.notify_about_resize_prep_instance( + self.context, instance, 'fake-compute', 'start', new_flavor) + + self.assertEqual(len(fake_notifier.VERSIONED_NOTIFICATIONS), 1) + notification = fake_notifier.VERSIONED_NOTIFICATIONS[0] + + self.assertEqual(notification['priority'], 'INFO') + self.assertEqual(notification['event_type'], + 'instance.resize_prep.start') + self.assertEqual(notification['publisher_id'], + 'nova-compute:fake-compute') + + payload = notification['payload']['nova_object.data'] + self.assertEqual(payload['tenant_id'], self.project_id) + self.assertEqual(payload['user_id'], self.user_id) + self.assertEqual(payload['uuid'], instance['uuid']) + + flavorid = flavors.get_flavor_by_name('m1.tiny')['flavorid'] + flavor = payload['flavor']['nova_object.data'] + self.assertEqual(str(flavor['flavorid']), flavorid) + + for attr in ('display_name', 'created_at', 'launched_at', + 'state', 'task_state', 'display_description', 'locked', + 'auto_disk_config', 'key_name'): + self.assertIn(attr, payload, "Key %s not in payload" % attr) + + self.assertEqual(payload['image_uuid'], uuids.fake_image_ref) + self.assertEqual(payload['new_flavor']['nova_object.data'][ + 'flavorid'], new_flavor.flavorid) + def test_notify_usage_exists_instance_not_found(self): # Ensure 'exists' notification generates appropriate usage data. instance = create_instance(self.context) diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 9d0fe9c37e..eafe0335c5 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -382,6 +382,9 @@ notification_object_data = { 'InstanceActionPayload': '1.5-fb2804ce9b681bfb217e729153c22611', 'InstanceActionRescueNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionRescuePayload': '1.0-a29f3339d0b8c3bcc997ab5d19d898d5', + 'InstanceActionResizePrepNotification': + '1.0-a73147b93b520ff0061865849d3dfa56', + 'InstanceActionResizePrepPayload': '1.0-3a23d3dd6516964a51c256b2f8b4646c', 'InstanceActionVolumeNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionVolumePayload': '1.3-f175b22ac6d6d0aea2bac21e12156e77', 'InstanceActionVolumeSwapNotification':