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:
parent
7ecdfb61a9
commit
19b7cf2170
@ -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
|
||||
|
@ -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
|
||||
========
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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.")
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user