From 760a0999b45926d0af8fdd4827c31cab69af09d3 Mon Sep 17 00:00:00 2001 From: Rajesh Tailor Date: Wed, 9 Apr 2025 11:39:27 +0530 Subject: [PATCH] Fix 'nova-manage image_property set' command As of now, if operator wants to set traits using 'nova-manage image_property set' command, it fails with below error, because in ImageMetaProps traits are not stored as individual fields, but stored in 'traits_required' field which is of type list. 'Invalid image property name trait:CUSTOM_XYZ' The setting of traits are handled by _set_attr_from_trait_names method here [1]. This change handles the issue by continue the loop, if the property startswith 'traits' string. [1] https://opendev.org/openstack/nova/src/commit/725a307693806e6e32834198e23be75f771bebc1/nova/objects/image_meta.py#L708-L714 Closes-Bug: #2096341 Change-Id: Ifc20894801f723627726e3c9bed7076144542660 Signed-off-by: Rajesh Tailor (cherry picked from commit 19f206f58c7c49cb148d1801364c46401d1737fb) (cherry picked from commit f5946fca46c4b4ea7b5e5651960d2a625a4fdc1c) --- nova/cmd/manage.py | 2 ++ nova/tests/unit/cmd/test_manage.py | 54 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 36ba1f7a6ba1..a7f6c266d4e9 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -3333,6 +3333,8 @@ class ImagePropertyCommands: # fields currently listed by ImageProps. We can't use from_dict to # do this as it silently ignores invalid property keys. for image_property_name in image_properties.keys(): + if image_property_name.startswith("trait:"): + continue if image_property_name not in objects.ImageMetaProps.fields: raise exception.InvalidImagePropertyName( image_property_name=image_property_name) diff --git a/nova/tests/unit/cmd/test_manage.py b/nova/tests/unit/cmd/test_manage.py index 9dac0d6ca9b6..be2541105d0b 100644 --- a/nova/tests/unit/cmd/test_manage.py +++ b/nova/tests/unit/cmd/test_manage.py @@ -4340,6 +4340,60 @@ class ImagePropertyCommandsTestCase(test.NoDBTestCase): image_properties=['hw_cdrom_bus']) self.assertEqual(4, ret, 'return code') + @mock.patch('nova.objects.RequestSpec.save') + @mock.patch('nova.objects.RequestSpec.get_by_instance_uuid') + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.context.target_cell') + @mock.patch('nova.objects.Instance.save') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock(cell_mapping=mock.sentinel.cm)) + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_set_image_properties_with_trait_in_property_name( + self, mock_instance_save, mock_target_cell, mock_get_instance, + mock_get_request_spec, mock_request_spec_save + ): + mock_target_cell.return_value.__enter__.return_value = \ + mock.sentinel.cctxt + mock_get_instance.return_value = objects.Instance( + uuid=uuidsentinel.instance, + vm_state=obj_fields.InstanceState.STOPPED, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + }, + image_ref='' + ) + mock_get_request_spec.return_value = objects.RequestSpec() + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=["trait:CUSTOM_WINDOWS_HOST=required"]) + self.assertEqual(0, ret, 'return code') + img_props = mock_get_instance.return_value.image_meta.properties + self.assertIn('CUSTOM_WINDOWS_HOST', img_props.traits_required) + + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.context.target_cell') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock(cell_mapping=mock.sentinel.cm)) + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_set_image_properties_without_trait_in_property_name( + self, mock_target_cell, mock_get_instance + ): + mock_target_cell.return_value.__enter__.return_value = \ + mock.sentinel.cctxt + mock_get_instance.return_value = objects.Instance( + uuid=uuidsentinel.instance, + vm_state=obj_fields.InstanceState.SHELVED_OFFLOADED, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['CUSTOM_WINDOWS_HOST=required']) + self.assertEqual(5, ret, 'return code') + @mock.patch('nova.objects.Instance.get_by_uuid') @mock.patch('nova.context.target_cell') @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid',