Merge "manage: Add image_property commands"
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user