diff --git a/doc/notification_samples/flavor-delete.json b/doc/notification_samples/flavor-delete.json index 5b7b3e7cafcd..1cad9c1f004e 100644 --- a/doc/notification_samples/flavor-delete.json +++ b/doc/notification_samples/flavor-delete.json @@ -17,7 +17,7 @@ "vcpu_weight": 0, "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3", "extra_specs": null, - "projects": [] + "projects": null } }, "event_type": "flavor.delete", diff --git a/nova/objects/flavor.py b/nova/objects/flavor.py index 1d95948cf085..f8c1bb570b12 100644 --- a/nova/objects/flavor.py +++ b/nova/objects/flavor.py @@ -614,7 +614,12 @@ class Flavor(base.NovaPersistentObject, base.NovaObject, # lazy-load projects (which is a problem for instance-bound # flavors and compute-cell operations), just load them here. if 'projects' not in self: - self._load_projects() + # If the flavor is deleted we can't lazy-load projects. + # FlavorPayload will orphan the flavor which will make the + # NotificationPayloadBase set projects=None in the notification + # payload. + if action != fields.NotificationAction.DELETE: + self._load_projects() notification_type = flavor_notification.FlavorNotification payload_type = flavor_notification.FlavorPayload diff --git a/nova/tests/unit/notifications/objects/test_flavor.py b/nova/tests/unit/notifications/objects/test_flavor.py index 7fbaaa4106f0..95ee1b4a8063 100644 --- a/nova/tests/unit/notifications/objects/test_flavor.py +++ b/nova/tests/unit/notifications/objects/test_flavor.py @@ -21,6 +21,8 @@ from nova.objects import fields from nova import test from nova.tests.unit.objects.test_flavor import fake_flavor +PROJECTS_SENTINEL = object() + class TestFlavorNotification(test.TestCase): def setUp(self): @@ -29,7 +31,8 @@ class TestFlavorNotification(test.TestCase): @mock.patch('nova.notifications.objects.flavor.FlavorNotification') def _verify_notification(self, flavor_obj, flavor, action, - mock_notification, project_id=None): + mock_notification, project_id=None, + expected_projects=PROJECTS_SENTINEL): notification = mock_notification if action == "CREATE": flavor_obj.create() @@ -61,8 +64,12 @@ class TestFlavorNotification(test.TestCase): schema = flavor_notification.FlavorPayload.SCHEMA for field in schema: - if field in flavor: - self.assertEqual(flavor[field], getattr(payload, field)) + if field == 'projects' and expected_projects != PROJECTS_SENTINEL: + self.assertEqual(expected_projects, getattr(payload, field)) + elif field in flavor_obj: + self.assertEqual(flavor_obj[field], getattr(payload, field)) + else: + self.fail('Missing check for field %s in flavor_obj.' % field) @mock.patch('nova.objects.Flavor._flavor_create') def test_flavor_create_with_notification(self, mock_create): @@ -117,6 +124,26 @@ class TestFlavorNotification(test.TestCase): mock_destroy.return_value = flavor flavor_obj = objects.Flavor(context=self.ctxt, **flavor) flavor_obj.obj_reset_changes() + self.assertNotIn('projects', flavor_obj) + # We specifically expect there to not be any projects as we don't want + # to try and lazy-load them from the main database and end up with []. + self._verify_notification(flavor_obj, flavor, "DELETE", + expected_projects=None) + + @mock.patch('nova.objects.Flavor._flavor_destroy') + def test_flavor_destroy_with_notification_and_projects(self, mock_destroy): + """Tests the flavor-delete notification with flavor.projects loaded.""" + flavor = copy.deepcopy(fake_flavor) + flavorid = '1' + flavor['flavorid'] = flavorid + flavor['id'] = flavorid + mock_destroy.return_value = flavor + flavor_obj = objects.Flavor( + context=self.ctxt, projects=['foo'], **flavor) + flavor_obj.obj_reset_changes() + self.assertIn('projects', flavor_obj) + self.assertEqual(['foo'], flavor_obj.projects) + # Since projects is loaded we shouldn't try to lazy-load it. self._verify_notification(flavor_obj, flavor, "DELETE") def test_obj_make_compatible(self):