From 19b7cf21706c7975088dd52e02178e7c5f85666b Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Mon, 10 Jan 2022 13:36:56 +0000 Subject: [PATCH] manage: Add image_property commands This adds an image property show and image property set command to nova-manage to allow users to update image properties stored for an instance in system metadata without having to rebuild the instance. This is intended to ease migration to new machine types, as updating the machine type could potentially invalidate the existing image properties of an instance. Co-Authored-By: melanie witt Blueprint: libvirt-device-bus-model-update Change-Id: Ic8783053778cf4614742186e94059d5675121db1 --- doc/source/admin/hw-machine-type.rst | 31 +++ doc/source/cli/nova-manage.rst | 85 ++++++ nova/cmd/manage.py | 163 +++++++++++ nova/exception.py | 4 + .../libvirt/test_device_bus_migration.py | 173 ++++++++++++ nova/tests/unit/cmd/test_manage.py | 260 +++++++++++++++++- ...anage-image-property-26b2e3eaa2ef343b.yaml | 17 ++ 7 files changed, 732 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/nova-manage-image-property-26b2e3eaa2ef343b.yaml diff --git a/doc/source/admin/hw-machine-type.rst b/doc/source/admin/hw-machine-type.rst index e8a0df87e4d4..abbed019701c 100644 --- a/doc/source/admin/hw-machine-type.rst +++ b/doc/source/admin/hw-machine-type.rst @@ -25,6 +25,10 @@ hw_machine_type - Configuring and updating QEMU instance machine types Added ``nova-manage`` commands to control the machine_type of an instance. +.. versionchanged:: 25.0.0 (Yoga) + + Added ``nova-manage`` commands to set the image properties of an instance. + .. note:: The following only applies to environments using libvirt compute hosts. @@ -135,3 +139,30 @@ Once it has been verified that all instances within the environment or specific cell have had a machine type recorded then the :oslo.config:option:`libvirt.hw_machine_type` can be updated without impacting existing instances. + +Device bus and model image properties +------------------------------------- + +.. versionadded:: 25.0.0 (Yoga) + +Device bus and model types defined as image properties associated with an +instance are always used when launching instances with the libvirt driver. +Support for each device bus and model is dependent on the machine type used and +version of QEMU available on the underlying compute host. As such, any changes +to the machine type of an instance or version of QEMU on a host might suddenly +invalidate the stored device bus or model image properties. + +It is now possible to change the stored image properties of an instance without +having to rebuild the instance. + +To show the stored image properties of an instance: + +.. code-block:: shell + + $ nova-manage image_property show $instance_uuid $property + +To update the stored image properties of an instance: + +.. code-block:: shell + + $ nova-manage image_property set $instance_uuid --property $property diff --git a/doc/source/cli/nova-manage.rst b/doc/source/cli/nova-manage.rst index a13289b2e816..9d58e7ebf51b 100644 --- a/doc/source/cli/nova-manage.rst +++ b/doc/source/cli/nova-manage.rst @@ -1668,6 +1668,91 @@ within an environment. * - 3 - Instances found without ``hw_machine_type`` set +Image Property Commands +======================= + +image_property show +------------------- + +.. program:: nova-manage image_property show + +.. code-block:: shell + + nova-manage image_property show [INSTANCE_UUID] [IMAGE_PROPERTY] + +Fetch and display the recorded image property ``IMAGE_PROPERTY`` of an +instance identified by ``INSTANCE_UUID``. + +.. versionadded:: 25.0.0 (Yoga) + +.. rubric:: Return codes + +.. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Return code + - Description + * - 0 + - Successfully completed + * - 1 + - An unexpected error occurred + * - 2 + - Unable to find instance or instance mapping + * - 3 + - No image property found for instance + +image_property set +------------------ + +.. program:: nova-manage image_property set + +.. code-block:: shell + + nova-manage image_property set \ + [INSTANCE_UUID] [--property] [IMAGE_PROPERTY]=[VALUE] + +Set or update the recorded image property ``IMAGE_PROPERTY`` of instance +``INSTANCE_UUID`` to value ``VALUE``. + +The following criteria must be met when using this command: + +* The instance must have a ``vm_state`` of ``STOPPED``, ``SHELVED`` or + ``SHELVED_OFFLOADED``. + +This command is useful for operators who need to update stored instance image +properties that have become invalidated by a change of instance machine type, +for example. + +.. versionadded:: 25.0.0 (Yoga) + +.. rubric:: Options + +.. option:: --property + + Image property to set using the format name=value. For example: + ``--property hw_disk_bus=virtio --property hw_cdrom_bus=sata``. + +.. rubric:: Return codes + +.. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Return code + - Description + * - 0 + - Update completed successfully + * - 1 + - An unexpected error occurred + * - 2 + - Unable to find instance or instance mapping + * - 3 + - The instance has an invalid ``vm_state`` + * - 4 + - The provided image property name is invalid + * - 5 + - The provided image property value is invalid See Also ======== diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index fd6a499b780d..f704a42698e0 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -3192,6 +3192,168 @@ class VolumeAttachmentCommands(object): return 1 +class ImagePropertyCommands(): + + @action_description(_("Show the value of an instance image property.")) + @args( + 'instance_uuid', metavar='', + help='UUID of the instance') + @args( + 'property', metavar='', + help='Image property to show') + def show(self, instance_uuid=None, image_property=None): + """Show value of a given instance image property. + + Return codes: + * 0: Command completed successfully. + * 1: An unexpected error happened. + * 2: Instance not found. + * 3: Image property not found. + """ + try: + ctxt = context.get_admin_context() + im = objects.InstanceMapping.get_by_instance_uuid( + ctxt, instance_uuid) + with context.target_cell(ctxt, im.cell_mapping) as cctxt: + instance = objects.Instance.get_by_uuid( + cctxt, instance_uuid, expected_attrs=['system_metadata']) + image_property = instance.system_metadata.get( + f'image_{image_property}') + if image_property: + print(image_property) + return 0 + else: + print(f'Image property {image_property} not found ' + f'for instance {instance_uuid}.') + return 3 + except ( + exception.InstanceNotFound, + exception.InstanceMappingNotFound, + ) as e: + print(str(e)) + return 2 + except Exception as e: + print(f'Unexpected error, see nova-manage.log for the full ' + f'trace: {str(e)}') + LOG.exception('Unexpected error') + return 1 + + def _validate_image_properties(self, image_properties): + """Validate the provided image property names and values + + :param image_properties: List of image property names and values + """ + # Sanity check the format of the provided properties, this should be + # in the format of name=value. + if any(x for x in image_properties if '=' not in x): + raise exception.InvalidInput( + "--property should use the format key=value") + + # Transform the list of delimited properties to a dict + image_properties = dict(prop.split('=') for prop in image_properties) + + # Validate the names of each property by checking against the o.vo + # 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 not in objects.ImageMetaProps.fields: + raise exception.InvalidImagePropertyName( + image_property_name=image_property_name) + + # Validate the values by creating an object from the provided dict. + objects.ImageMetaProps.from_dict(image_properties) + + # Return the dict so we can update the instance system_metadata + return image_properties + + def _update_image_properties(self, instance, image_properties): + """Update instance image properties + + :param instance: The instance to update + :param image_properties: List of image properties and values to update + """ + # Check the state of the instance + allowed_states = [ + obj_fields.InstanceState.STOPPED, + obj_fields.InstanceState.SHELVED, + obj_fields.InstanceState.SHELVED_OFFLOADED, + ] + if instance.vm_state not in allowed_states: + raise exception.InstanceInvalidState( + instance_uuid=instance.uuid, attr='vm_state', + state=instance.vm_state, + method='image_property set (must be STOPPED, SHELVED, OR ' + 'SHELVED_OFFLOADED).') + + # Validate the property names and values + image_properties = self._validate_image_properties(image_properties) + + # Update the image properties and save the instance record + for image_property, value in image_properties.items(): + instance.system_metadata[f'image_{image_property}'] = value + + # Save and return 0 + instance.save() + return 0 + + @action_description(_( + "Set the values of instance image properties stored in the database. " + "This is only allowed for " "instances with a STOPPED, SHELVED or " + "SHELVED_OFFLOADED vm_state.")) + @args( + 'instance_uuid', metavar='', + help='UUID of the instance') + @args( + '--property', metavar='', action='append', + dest='image_properties', + help='Image property to set using the format name=value. For example: ' + '--property hw_disk_bus=virtio --property hw_cdrom_bus=sata') + def set(self, instance_uuid=None, image_properties=None): + """Set instance image property values + + Return codes: + * 0: Command completed successfully. + * 1: An unexpected error happened. + * 2: Unable to find instance. + * 3: Instance is in an invalid state. + * 4: Invalid input format. + * 5: Invalid image property name. + * 6: Invalid image property value. + """ + try: + ctxt = context.get_admin_context() + im = objects.InstanceMapping.get_by_instance_uuid( + ctxt, instance_uuid) + with context.target_cell(ctxt, im.cell_mapping) as cctxt: + instance = objects.Instance.get_by_uuid( + cctxt, instance_uuid, expected_attrs=['system_metadata']) + return self._update_image_properties( + instance, image_properties) + except ValueError as e: + print(str(e)) + return 6 + except exception.InvalidImagePropertyName as e: + print(str(e)) + return 5 + except exception.InvalidInput as e: + print(str(e)) + return 4 + except exception.InstanceInvalidState as e: + print(str(e)) + return 3 + except ( + exception.InstanceNotFound, + exception.InstanceMappingNotFound, + ) as e: + print(str(e)) + return 2 + except Exception as e: + print('Unexpected error, see nova-manage.log for the full ' + 'trace: %s ' % str(e)) + LOG.exception('Unexpected error') + return 1 + + CATEGORIES = { 'api_db': ApiDbCommands, 'cell_v2': CellV2Commands, @@ -3199,6 +3361,7 @@ CATEGORIES = { 'placement': PlacementCommands, 'libvirt': LibvirtCommands, 'volume_attachment': VolumeAttachmentCommands, + 'image_property': ImagePropertyCommands, } diff --git a/nova/exception.py b/nova/exception.py index 9a89b655b534..cab9bcd3546e 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -736,6 +736,10 @@ class InvalidImageRef(Invalid): msg_fmt = _("Invalid image href %(image_href)s.") +class InvalidImagePropertyName(Invalid): + msg_fmt = _("Invalid image property name %(image_property_name)s.") + + class AutoDiskConfigDisabledByImage(Invalid): msg_fmt = _("Requested image %(image)s " "has automatic disk resize disabled.") diff --git a/nova/tests/functional/libvirt/test_device_bus_migration.py b/nova/tests/functional/libvirt/test_device_bus_migration.py index eccb0738a9bc..82a0d4556e2e 100644 --- a/nova/tests/functional/libvirt/test_device_bus_migration.py +++ b/nova/tests/functional/libvirt/test_device_bus_migration.py @@ -13,12 +13,15 @@ import datetime from unittest import mock +import fixtures from oslo_utils.fixture import uuidsentinel as uuids +from nova.cmd import manage from nova import context as nova_context from nova import objects from nova import test from nova.tests.functional.libvirt import base +from nova.virt.libvirt import config as vconfig from nova.virt.libvirt import driver as libvirt_driver @@ -33,6 +36,7 @@ class LibvirtDeviceBusMigration(base.ServersTestBase): self.context = nova_context.get_admin_context() self.compute_hostname = self.start_compute() self.compute = self.computes[self.compute_hostname] + self.commands = manage.ImagePropertyCommands() def _unset_stashed_image_properties(self, server_id, properties): instance = objects.Instance.get_by_uuid(self.context, server_id) @@ -232,3 +236,172 @@ class LibvirtDeviceBusMigration(base.ServersTestBase): server2, default_image_properties2) self._assert_stashed_image_properties_persist( server3, default_image_properties1) + + def _assert_guest_config(self, config, image_properties): + verified_properties = set() + + # Verify the machine type matches the image property + value = image_properties.get('hw_machine_type') + if value: + self.assertEqual(value, config.os_mach_type) + verified_properties.add('hw_machine_type') + + # Look at all the devices and verify that their bus and model values + # match the desired image properties + for device in config.devices: + if isinstance(device, vconfig.LibvirtConfigGuestDisk): + if device.source_device == 'cdrom': + value = image_properties.get('hw_cdrom_bus') + if value: + self.assertEqual(value, device.target_bus) + verified_properties.add('hw_cdrom_bus') + + if device.source_device == 'disk': + value = image_properties.get('hw_disk_bus') + if value: + self.assertEqual(value, device.target_bus) + verified_properties.add('hw_disk_bus') + + if isinstance(device, vconfig.LibvirtConfigGuestInput): + value = image_properties.get('hw_input_bus') + if value: + self.assertEqual(value, device.bus) + verified_properties.add('hw_input_bus') + + if device.type == 'tablet': + value = image_properties.get('hw_pointer_model') + if value: + self.assertEqual('usbtablet', value) + verified_properties.add('hw_pointer_model') + + if isinstance(device, vconfig.LibvirtConfigGuestVideo): + value = image_properties.get('hw_video_model') + if value: + self.assertEqual(value, device.type) + verified_properties.add('hw_video_model') + + if isinstance(device, vconfig.LibvirtConfigGuestInterface): + value = image_properties.get('hw_vif_model') + if value: + self.assertEqual(value, device.model) + verified_properties.add('hw_vif_model') + + # If hw_pointer_model or hw_input_bus are in the image properties but + # we did not encounter devices for them, they should be None + for p in ['hw_pointer_model', 'hw_input_bus']: + if p in image_properties and p not in verified_properties: + self.assertIsNone(image_properties[p]) + verified_properties.add(p) + + # Assert that we verified all of the image properties + self.assertEqual( + len(image_properties), len(verified_properties), + f'image_properties: {image_properties}, ' + f'verified_properties: {verified_properties}' + ) + + def test_machine_type_and_bus_and_model_migration(self): + """Assert the behaviour of the nova-manage image_property set command + when used to migrate between machine types and associated device buses. + """ + # Create a pass-through mock around _get_guest_config to capture the + # config of an instance so we can assert things about it later. + # TODO(lyarwood): This seems like a useful thing to do in the libvirt + # func tests for all computes we start? + self.guest_configs = {} + orig_get_config = self.compute.driver._get_guest_config + + def _get_guest_config(_self, *args, **kwargs): + guest_config = orig_get_config(*args, **kwargs) + instance = args[0] + self.guest_configs[instance.uuid] = guest_config + return self.guest_configs[instance.uuid] + + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.libvirt.LibvirtDriver._get_guest_config', + _get_guest_config)) + + pc_image_properties = { + 'hw_machine_type': 'pc', + 'hw_cdrom_bus': 'ide', + 'hw_disk_bus': 'sata', + 'hw_input_bus': 'usb', + 'hw_pointer_model': 'usbtablet', + 'hw_video_model': 'cirrus', + 'hw_vif_model': 'e1000', + } + self.glance.create( + None, + { + 'id': uuids.pc_image_uuid, + 'name': 'pc_image', + 'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3), + 'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3), + 'deleted_at': None, + 'deleted': False, + 'status': 'active', + 'is_public': False, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'size': '74185822', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': [], + 'properties': pc_image_properties, + } + ) + + body = self._build_server( + image_uuid=uuids.pc_image_uuid, networks='auto') + + # Add a cdrom to be able to verify hw_cdrom_bus + body['block_device_mapping_v2'] = [{ + 'source_type': 'blank', + 'destination_type': 'local', + 'disk_bus': 'ide', + 'device_type': 'cdrom', + 'boot_index': 0, + }] + + # Create the server and verify stashed image properties + server = self.api.post_server({'server': body}) + self._wait_for_state_change(server, 'ACTIVE') + self._assert_stashed_image_properties( + server['id'], pc_image_properties) + + # Verify the guest config matches the image properties + guest_config = self.guest_configs[server['id']] + self._assert_guest_config(guest_config, pc_image_properties) + + # Set the image properties with nova-manage + self._stop_server(server) + + q35_image_properties = { + 'hw_machine_type': 'q35', + 'hw_cdrom_bus': 'sata', + 'hw_disk_bus': 'virtio', + 'hw_input_bus': 'virtio', + 'hw_pointer_model': 'usbtablet', + 'hw_video_model': 'qxl', + 'hw_vif_model': 'virtio', + } + property_list = [ + f'{p}={value}' for p, value in q35_image_properties.items() + ] + + self.commands.set( + instance_uuid=server['id'], image_properties=property_list) + + # Verify the updated stashed image properties + self._start_server(server) + self._assert_stashed_image_properties( + server['id'], q35_image_properties) + + # The guest config should reflect the new values except for the cdrom + # block device bus which is taken from the block_device_mapping record, + # not system_metadata, so it cannot be changed + q35_image_properties['hw_cdrom_bus'] = 'ide' + guest_config = self.guest_configs[server['id']] + self._assert_guest_config(guest_config, q35_image_properties) diff --git a/nova/tests/unit/cmd/test_manage.py b/nova/tests/unit/cmd/test_manage.py index 309c2fc829a8..82c3d3c84ad8 100644 --- a/nova/tests/unit/cmd/test_manage.py +++ b/nova/tests/unit/cmd/test_manage.py @@ -40,7 +40,6 @@ from nova import test from nova.tests import fixtures as nova_fixtures from nova.tests.unit import fake_requests - CONF = conf.CONF @@ -3952,3 +3951,262 @@ class LibvirtCommandsTestCase(test.NoDBTestCase): output = self.output.getvalue() self.assertEqual(3, ret) self.assertIn(uuidsentinel.instance, output) + + +class ImagePropertyCommandsTestCase(test.NoDBTestCase): + + def setUp(self): + super().setUp() + self.output = StringIO() + self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.output)) + self.commands = manage.ImagePropertyCommands() + + @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_show_image_properties( + 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.STOPPED, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + ret = self.commands.show( + instance_uuid=uuidsentinel.instance, + image_property='hw_disk_bus') + self.assertEqual(0, ret, 'return code') + self.assertIn('virtio', self.output.getvalue(), 'command output') + + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock()) + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_show_image_properties_instance_not_found( + self, + mock_get_instance + ): + mock_get_instance.side_effect = exception.InstanceNotFound( + instance_id=uuidsentinel.instance) + ret = self.commands.show( + instance_uuid=uuidsentinel.instance, + image_property='hw_disk_bus') + self.assertEqual(2, ret, 'return code') + + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid') + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_show_image_properties_instance_mapping_not_found( + self, + mock_get_instance_mapping + ): + mock_get_instance_mapping.side_effect = \ + exception.InstanceMappingNotFound( + uuid=uuidsentinel.instance) + ret = self.commands.show( + instance_uuid=uuidsentinel.instance, + image_property='hw_disk_bus') + self.assertEqual(2, 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', + 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_show_image_properties_image_property_not_found( + 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.STOPPED, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + ret = self.commands.show( + instance_uuid=uuidsentinel.instance, + image_property='foo') + self.assertEqual(3, ret, 'return code') + + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid') + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_show_image_properties_unknown_failure( + self, + mock_get_instance_mapping, + ): + mock_get_instance_mapping.side_effect = Exception() + ret = self.commands.show( + instance_uuid=uuidsentinel.instance, + image_property='hw_disk_bus') + self.assertEqual(1, ret, 'return code') + + @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( + self, mock_instance_save, mock_target_cell, mock_get_instance + ): + mock_target_cell.return_value.__enter__.return_value = \ + mock.sentinel.cctxt + instance = objects.Instance( + uuid=uuidsentinel.instance, + vm_state=obj_fields.InstanceState.STOPPED, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + mock_get_instance.return_value = instance + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['hw_cdrom_bus=sata'] + ) + self.assertEqual(0, ret, 'return code') + self.assertIn('image_hw_cdrom_bus', instance.system_metadata) + self.assertEqual( + 'sata', + instance.system_metadata.get('image_hw_cdrom_bus'), + 'image_hw_cdrom_bus' + ) + self.assertEqual( + 'virtio', + instance.system_metadata.get('image_hw_disk_bus'), + 'image_hw_disk_bus' + ) + mock_instance_save.assert_called_once() + + @mock.patch('nova.objects.Instance.get_by_uuid') + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', + new=mock.Mock()) + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_set_image_properties_instance_not_found(self, mock_get_instance): + mock_get_instance.side_effect = exception.InstanceNotFound( + instance_id=uuidsentinel.instance) + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['hw_disk_bus=virtio']) + self.assertEqual(2, ret, 'return code') + + @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid') + @mock.patch('nova.context.get_admin_context', + new=mock.Mock(return_value=mock.sentinel.ctxt)) + def test_set_image_properties_instance_mapping_not_found( + self, + mock_get_instance_mapping + ): + mock_get_instance_mapping.side_effect = \ + exception.InstanceMappingNotFound( + uuid=uuidsentinel.instance) + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['hw_disk_bus=virtio']) + self.assertEqual(2, 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', + 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_instance_invalid_state( + 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.ACTIVE, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['hw_cdrom_bus=sata'] + ) + self.assertEqual(3, 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', + 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_invalid_input( + 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, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['hw_cdrom_bus']) + self.assertEqual(4, 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', + 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_invalid_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=['foo=bar']) + 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', + 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_invalid_property_value( + 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.STOPPED, + system_metadata={ + 'image_hw_disk_bus': 'virtio', + } + ) + ret = self.commands.set( + instance_uuid=uuidsentinel.instance, + image_properties=['hw_disk_bus=bar']) + self.assertEqual(6, ret, 'return code') diff --git a/releasenotes/notes/nova-manage-image-property-26b2e3eaa2ef343b.yaml b/releasenotes/notes/nova-manage-image-property-26b2e3eaa2ef343b.yaml new file mode 100644 index 000000000000..f3de63fc2135 --- /dev/null +++ b/releasenotes/notes/nova-manage-image-property-26b2e3eaa2ef343b.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + New ``nova-manage image_property`` commands have been added to help update + instance image properties that have become invalidated by a change of + instance machine type. + + * The ``nova-manage image_property show`` command can be used to show the + current stored image property value for a given instance and property. + + * The ``nova-manage image_property set`` command can be used to update the + stored image properties stored in the database for a given instance and + image properties. + + For more detail on command usage, see the machine type documentation: + + https://docs.openstack.org/nova/latest/admin/hw-machine-type.html#device-bus-and-model-image-properties