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