nova-manage: Add libvirt get_machine_type command

This change introduces the first machine_type command to nova-manage to
fetch and display the current machine type if set in the system metadata
of the instance.

blueprint: libvirt-default-machine-type
Change-Id: Idc035671892e4668141a93763f8f2bed7a630812
This commit is contained in:
Lee Yarwood 2021-01-05 15:20:44 +00:00
parent 5b66caab87
commit 20692c245c
8 changed files with 297 additions and 7 deletions

View File

@ -708,6 +708,28 @@ Placement
* - 127
- Invalid input
libvirt
~~~~~~~
``nova-manage libvirt get_machine_type [instance-uuid]``
Fetch and display the recorded machine type of a libvirt instance.
**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 machine type found for instance
See Also
========

View File

@ -5,6 +5,7 @@ nova/scheduler/request_filter.py
nova/scheduler/utils.py
nova/virt/driver.py
nova/virt/hardware.py
nova/virt/libvirt/machine_type_utils.py
nova/virt/libvirt/__init__.py
nova/virt/libvirt/driver.py
nova/virt/libvirt/event.py

View File

@ -67,6 +67,7 @@ from nova import rpc
from nova.scheduler.client import report
from nova.scheduler import utils as scheduler_utils
from nova import version
from nova.virt.libvirt import machine_type_utils
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
@ -2620,11 +2621,49 @@ class PlacementCommands(object):
return 0
class LibvirtCommands(object):
"""Commands for managing libvirt instances"""
@action_description(
_("Fetch the stored machine type of the instance from the database."))
@args('instance_uuid', metavar='<instance_uuid>',
help='UUID of instance to fetch the machine type for')
def get_machine_type(self, instance_uuid=None):
"""Fetch the stored machine type of the instance from the database.
Return codes:
* 0: Command completed successfully.
* 1: An unexpected error happened.
* 2: Unable to find instance or instance mapping.
* 3: No machine type found for the instance.
"""
try:
ctxt = context.get_admin_context()
mtype = machine_type_utils.get_machine_type(ctxt, instance_uuid)
if mtype:
print(mtype)
return 0
else:
print(_('No machine type registered for instance %s' %
instance_uuid))
return 3
except (exception.InstanceNotFound,
exception.InstanceMappingNotFound) as e:
print(str(e))
return 2
except Exception:
LOG.exception('Unexpected error')
return 1
CATEGORIES = {
'api_db': ApiDbCommands,
'cell_v2': CellV2Commands,
'db': DbCommands,
'placement': PlacementCommands
'placement': PlacementCommands,
'libvirt': LibvirtCommands,
}

View File

@ -18,6 +18,7 @@ from oslo_utils.fixture import uuidsentinel
from nova import context as nova_context
from nova import objects
from nova.tests.functional.libvirt import base
from nova.virt.libvirt import machine_type_utils
class LibvirtMachineTypeTest(base.ServersTestBase):
@ -78,6 +79,14 @@ class LibvirtMachineTypeTest(base.ServersTestBase):
self.guest_configs[server_id].os_mach_type
)
def _unset_machine_type(self, server_id):
instance = objects.Instance.get_by_uuid(
self.context,
server_id,
)
instance.system_metadata.pop('image_hw_machine_type')
instance.save()
def test_init_host_register_machine_type(self):
"""Assert that the machine type of an instance is recorded during
init_host if not already captured by an image prop.
@ -91,12 +100,7 @@ class LibvirtMachineTypeTest(base.ServersTestBase):
# Stop n-cpu and clear the recorded machine type from server_without to
# allow init_host to register the machine type.
self.computes['compute1'].stop()
instance_without = objects.Instance.get_by_uuid(
self.context,
server_without['id'],
)
instance_without.system_metadata.pop('image_hw_machine_type')
instance_without.save()
self._unset_machine_type(server_without['id'])
self.flags(hw_machine_type='x86_64=pc-q35-1.2.3', group='libvirt')
@ -178,3 +182,20 @@ class LibvirtMachineTypeTest(base.ServersTestBase):
def test_machine_type_after_server_hard_reboot(self):
self._test_machine_type_after_server_reboot(hard=True)
def test_machine_type_get(self):
self.flags(hw_machine_type='x86_64=pc', group='libvirt')
server_with, server_without = self._create_servers()
self.assertEqual(
'q35',
machine_type_utils.get_machine_type(
self.context, server_with['id']
)
)
self.assertEqual(
'pc',
machine_type_utils.get_machine_type(
self.context, server_without['id']
)
)

View File

@ -3019,3 +3019,80 @@ class TestNovaManageMain(test.NoDBTestCase):
mock_conf.post_mortem = True
self.assertEqual(255, manage.main())
self.assertTrue(mock_pm.called)
class LibvirtCommandsTestCase(test.NoDBTestCase):
def setUp(self):
super().setUp()
self.output = StringIO()
self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.output))
self.commands = manage.LibvirtCommands()
@mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type')
@mock.patch('nova.context.get_admin_context')
def test_get(self, mock_get_context, mock_get_machine_type):
mock_get_context.return_value = mock.sentinel.admin_context
mock_get_machine_type.return_value = 'pc'
ret = self.commands.get_machine_type(
instance_uuid=uuidsentinel.instance
)
mock_get_machine_type.assert_called_once_with(
mock.sentinel.admin_context,
uuidsentinel.instance
)
output = self.output.getvalue()
self.assertEqual(0, ret)
self.assertIn('pc', output)
@mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type')
@mock.patch('nova.context.get_admin_context')
def test_get_unknown_failure(
self, mock_get_context, mock_get_machine_type
):
mock_get_machine_type.side_effect = Exception()
ret = self.commands.get_machine_type(
instance_uuid=uuidsentinel.instance
)
self.assertEqual(1, ret)
@mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type')
@mock.patch('nova.context.get_admin_context', new=mock.Mock())
def test_get_unable_to_find_instance_mapping(self, mock_get_machine_type):
mock_get_machine_type.side_effect = exception.InstanceMappingNotFound(
uuid=uuidsentinel.instance)
ret = self.commands.get_machine_type(
instance_uuid=uuidsentinel.instance
)
output = self.output.getvalue()
self.assertEqual(2, ret)
self.assertIn(
f"Instance {uuidsentinel.instance} has no mapping to a cell.",
output)
@mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type')
@mock.patch('nova.context.get_admin_context', new=mock.Mock())
def test_get_machine_type_unable_to_find_instance(
self, mock_get_machine_type
):
mock_get_machine_type.side_effect = exception.InstanceNotFound(
instance_id=uuidsentinel.instance)
ret = self.commands.get_machine_type(
instance_uuid=uuidsentinel.instance)
output = self.output.getvalue()
self.assertEqual(2, ret)
self.assertIn(
f"Instance {uuidsentinel.instance} could not be found.",
output)
@mock.patch('nova.virt.libvirt.machine_type_utils.get_machine_type',
new=mock.Mock(return_value=None))
@mock.patch('nova.context.get_admin_context', new=mock.Mock())
def test_get_none_found(self):
ret = self.commands.get_machine_type(
instance_uuid=uuidsentinel.instance
)
output = self.output.getvalue()
self.assertEqual(3, ret)
self.assertIn("No machine type registered for instance "
f"{uuidsentinel.instance}", output)

View File

@ -0,0 +1,84 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo_utils.fixture import uuidsentinel
from nova.compute import vm_states
from nova import objects
from nova import test
from nova.virt.libvirt import machine_type_utils
class TestMachineTypeUtils(test.NoDBTestCase):
def _create_test_instance_obj(
self,
vm_state=vm_states.STOPPED,
mtype=None
):
instance = objects.Instance(
uuid=uuidsentinel.instance, host='fake', node='fake',
task_state=None, flavor=objects.Flavor(),
project_id='fake-project', user_id='fake-user',
vm_state=vm_state, system_metadata={}
)
if mtype:
instance.system_metadata = {
'image_hw_machine_type': mtype,
}
return instance
@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.cell_mapping))
def test_get_machine_type(self, mock_target_cell, mock_get_instance):
mock_target_cell.return_value.__enter__.return_value = (
mock.sentinel.cell_context)
mock_get_instance.return_value = self._create_test_instance_obj(
mtype='pc'
)
self.assertEqual(
'pc',
machine_type_utils.get_machine_type(
mock.sentinel.context,
instance_uuid=uuidsentinel.instance
)
)
mock_get_instance.assert_called_once_with(
mock.sentinel.cell_context,
uuidsentinel.instance,
expected_attrs=['system_metadata']
)
@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.cell_mapping))
def test_get_machine_type_none_found(
self, mock_target_cell, mock_get_instance
):
mock_target_cell.return_value.__enter__.return_value = (
mock.sentinel.cell_context)
mock_get_instance.return_value = self._create_test_instance_obj()
self.assertIsNone(
machine_type_utils.get_machine_type(
mock.sentinel.context,
instance_uuid=uuidsentinel.instance
)
)
mock_get_instance.assert_called_once_with(
mock.sentinel.cell_context,
uuidsentinel.instance,
expected_attrs=['system_metadata']
)

View File

@ -0,0 +1,38 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import typing as ty
from nova import context as nova_context
from nova import objects
def get_machine_type(
context: 'nova_context.RequestContext',
instance_uuid: str,
) -> ty.Optional[str]:
"""Get the registered machine type of an instance
:param context: Request context.
:param instance_uuid: Instance UUID to check.
:returns: The machine type or None.
:raises: exception.InstanceNotFound, exception.InstanceMappingNotFound
"""
im = objects.InstanceMapping.get_by_instance_uuid(context, instance_uuid)
with nova_context.target_cell(context, im.cell_mapping) as cctxt:
# NOTE(lyarwood): While we are after the value of 'hw_machine_type'
# stored as an image metadata property this actually comes from the
# system metadata of the instance so we need to add
# expected_attrs=['system_metadata'] here.
instance = objects.instance.Instance.get_by_uuid(
cctxt, instance_uuid, expected_attrs=['system_metadata'])
return instance.image_meta.properties.get('hw_machine_type')

View File

@ -8,3 +8,11 @@ upgrade:
This machine type will then be used when the instance is restarted or
migrated as it will now appear as an image metadata property associated
with the instance.
The following new ``nova-manage`` commands have been introduced to help
operators manage the ``hw_machine_type`` image property:
``nova-manage libvirt get_machine_type``
This command will print the current machine type if set in the image
metadata of the instance.