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 <melwittt@gmail.com>

Blueprint: libvirt-device-bus-model-update

Change-Id: Ic8783053778cf4614742186e94059d5675121db1
This commit is contained in:
Lee Yarwood 2022-01-10 13:36:56 +00:00 committed by melanie witt
parent 7ecdfb61a9
commit 19b7cf2170
7 changed files with 732 additions and 1 deletions

View File

@ -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

View File

@ -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
========

View File

@ -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='<instance_uuid>',
help='UUID of the instance')
@args(
'property', metavar='<image_property>',
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='<instance_uuid>',
help='UUID of the instance')
@args(
'--property', metavar='<image_property>', 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,
}

View File

@ -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.")

View File

@ -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)

View File

@ -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')

View File

@ -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