Merge "libvirt: Use domain capabilities to get supported device models"

This commit is contained in:
Zuul 2020-03-26 13:22:01 +00:00 committed by Gerrit Code Review
commit 15f0215df5
6 changed files with 471 additions and 46 deletions

View File

@ -133,6 +133,9 @@ class LibvirtReportNoSevTraitsTests(LibvirtReportTraitsTestBase):
) as (mock_exists, mock_open, mock_features):
# Retrigger the detection code. In the real world this
# would be a restart of the compute service.
# As we are changing the domain caps we need to clear the
# cache in the host object.
self.compute.driver._host._domain_caps = None
self.compute.driver._host._set_amd_sev_support()
self.assertTrue(self.compute.driver._host.supports_amd_sev)
@ -142,6 +145,8 @@ class LibvirtReportNoSevTraitsTests(LibvirtReportTraitsTestBase):
# However it won't disappear in the provider tree and get synced
# back to placement until we force a reinventory:
self.compute.manager.reset()
# reset cached traits so they are recalculated.
self.compute.driver._static_traits = None
self._run_periodics()
traits = self._get_provider_traits(self.host_uuid)
@ -200,6 +205,7 @@ class LibvirtReportSevTraitsTests(LibvirtReportTraitsTestBase):
with self.patch_exists(SEV_KERNEL_PARAM_FILE, False) as mock_exists:
# Retrigger the detection code. In the real world this
# would be a restart of the compute service.
self.compute.driver._host._domain_caps = None
self.compute.driver._host._set_amd_sev_support()
self.assertFalse(self.compute.driver._host.supports_amd_sev)
@ -208,6 +214,8 @@ class LibvirtReportSevTraitsTests(LibvirtReportTraitsTestBase):
# However it won't disappear in the provider tree and get synced
# back to placement until we force a reinventory:
self.compute.manager.reset()
# reset cached traits so they are recalculated.
self.compute.driver._static_traits = None
self._run_periodics()
traits = self._get_provider_traits(self.host_uuid)

View File

@ -3808,3 +3808,132 @@ class LibvirtConfigGuestVPMEMTest(LibvirtConfigBaseTest):
</label>
</target>
</memory>""")
class LibvirtConfigVideoModelsTests(LibvirtConfigBaseTest):
def test_parse_video_model(self):
xml = """
<video supported='yes'>
<enum name='modelType'>
<value>vga</value>
<value>cirrus</value>
<value>vmvga</value>
<value>qxl</value>
<value>virtio</value>
</enum>
</video>
"""
obj = config.LibvirtConfigDomainCapsVideoModels()
obj.parse_str(xml)
expected_models = ('vga', 'cirrus', 'vmvga', 'qxl', 'virtio')
self.assertTrue(obj.supported)
for model in expected_models:
self.assertIn(model, obj.models)
self.assertNotIn('gop', obj.models)
class LibvirtConfigDiskBusesTests(LibvirtConfigBaseTest):
def test_parse_disk_buses(self):
xml = """
<disk supported='yes'>
<enum name='diskDevice'>
<value>disk</value>
<value>cdrom</value>
<value>floppy</value>
<value>lun</value>
</enum>
<enum name='bus'>
<value>ide</value>
<value>scsi</value>
<value>virtio</value>
<value>usb</value>
<value>sata</value>
</enum>
</disk>
"""
obj = config.LibvirtConfigDomainCapsDiskBuses()
obj.parse_str(xml)
expected_buses = ('ide', 'scsi', 'virtio', 'usb', 'sata')
self.assertTrue(obj.supported)
for bus in expected_buses:
self.assertIn(bus, obj.buses)
self.assertNotIn('fdc', obj.buses)
class LibvirtConfigDomainCapsDevicesTests(LibvirtConfigBaseTest):
def test_parse_domain_caps_devices(self):
xml = """
<devices>
<disk supported='yes'>
<enum name='diskDevice'>
<value>disk</value>
<value>cdrom</value>
<value>floppy</value>
<value>lun</value>
</enum>
<enum name='bus'>
<value>ide</value>
<value>fdc</value>
<value>scsi</value>
<value>virtio</value>
<value>usb</value>
<value>sata</value>
</enum>
</disk>
<graphics supported='yes'>
<enum name='type'>
<value>sdl</value>
<value>vnc</value>
<value>spice</value>
</enum>
</graphics>
<video supported='yes'>
<enum name='modelType'>
<value>vga</value>
<value>cirrus</value>
<value>vmvga</value>
<value>qxl</value>
<value>virtio</value>
</enum>
</video>
<hostdev supported='yes'>
<enum name='mode'>
<value>subsystem</value>
</enum>
<enum name='startupPolicy'>
<value>default</value>
<value>mandatory</value>
<value>requisite</value>
<value>optional</value>
</enum>
<enum name='subsysType'>
<value>usb</value>
<value>pci</value>
<value>scsi</value>
</enum>
<enum name='capsType'/>
<enum name='pciBackend'/>
</hostdev>
</devices>
"""
obj = config.LibvirtConfigDomainCapsDevices()
obj.parse_str(xml)
# we only use the video and disk devices today.
device_types = [config.LibvirtConfigDomainCapsDiskBuses,
config.LibvirtConfigDomainCapsVideoModels]
# so we assert there are only two device types parsed
self.assertEqual(2, len(obj.devices))
# we then assert that the parsed devices are of the correct type
for dev in obj.devices:
self.assertIn(type(dev), device_types)
# and that the sub-devices are accessible directly via properties.
self.assertIsInstance(
obj.disk, config.LibvirtConfigDomainCapsDiskBuses)
self.assertIsInstance(
obj.video, config.LibvirtConfigDomainCapsVideoModels)

View File

@ -120,6 +120,7 @@ from nova.virt.libvirt.storage import dmcrypt
from nova.virt.libvirt.storage import lvm
from nova.virt.libvirt.storage import rbd_utils
from nova.virt.libvirt import utils as libvirt_utils
from nova.virt.libvirt import vif as libvirt_vif
from nova.virt.libvirt.volume import volume as volume_drivers
@ -1107,6 +1108,82 @@ class LibvirtConnTestCase(test.NoDBTestCase,
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
self.assertFalse(drvr.need_legacy_block_device_info)
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_cpu_traits')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_storage_bus_traits')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_video_model_traits')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_vif_model_traits')
def test_static_traits(
self, mock_vif_traits, mock_video_traits, mock_storage_traits,
mock_cpu_traits,
):
"""Ensure driver capabilities are correctly retrieved and cached."""
# we don't mock out calls to os_traits intentionally, so we need to
# return valid traits here
mock_cpu_traits.return_value = {'HW_CPU_HYPERTHREADING': True}
mock_storage_traits.return_value = {'COMPUTE_STORAGE_BUS_VIRTIO': True}
mock_video_traits.return_value = {'COMPUTE_GRAPHICS_MODEL_VGA': True}
mock_vif_traits.return_value = {'COMPUTE_NET_VIF_MODEL_VIRTIO': True}
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
expected = {
'HW_CPU_HYPERTHREADING': True,
'COMPUTE_STORAGE_BUS_VIRTIO': True,
'COMPUTE_GRAPHICS_MODEL_VGA': True,
'COMPUTE_NET_VIF_MODEL_VIRTIO': True,
}
static_traits = drvr.static_traits
# check that results are as expected and the individual helper
# functions were called once each
self.assertEqual(expected, static_traits)
for mock_traits in (
mock_vif_traits, mock_video_traits, mock_storage_traits,
mock_cpu_traits,
):
mock_traits.assert_called_once_with()
mock_traits.reset_mock()
static_traits = drvr.static_traits
# now check that the results are still as expected but the helpers
# weren't called since the value was cached
self.assertEqual(expected, static_traits)
for mock_traits in (
mock_vif_traits, mock_video_traits, mock_storage_traits,
mock_cpu_traits,
):
mock_traits.assert_not_called()
@mock.patch.object(libvirt_driver.LOG, 'debug')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_cpu_traits')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_storage_bus_traits')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_video_model_traits')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_vif_model_traits')
def test_static_traits__invalid_trait(
self, mock_vif_traits, mock_video_traits, mock_storage_traits,
mock_cpu_traits, mock_log,
):
"""Ensure driver capabilities are correctly retrieved and cached."""
mock_cpu_traits.return_value = {'foo': True}
mock_storage_traits.return_value = {'bar': True}
mock_video_traits.return_value = {'baz': True}
mock_vif_traits.return_value = {'COMPUTE_NET_VIF_MODEL_VIRTIO': True}
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
expected = {'COMPUTE_NET_VIF_MODEL_VIRTIO': True}
static_traits = drvr.static_traits
self.assertEqual(expected, static_traits)
mock_log.assert_has_calls([
mock.call("Trait '%s' is not valid; ignoring.", "foo"),
mock.call("Trait '%s' is not valid; ignoring.", "bar"),
mock.call("Trait '%s' is not valid; ignoring.", "baz"),
],
any_order=True)
@mock.patch.object(host.Host, "has_min_version")
def test_min_version_start_ok(self, mock_version):
mock_version.return_value = True
@ -23488,16 +23565,35 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
self.assertRaises(test.TestingException,
self._test_detach_mediated_devices, exc)
def test_storage_bus_traits(self):
def test_storage_bus_traits__qemu_kvm(self):
"""Test getting storage bus traits per virt type.
This also ensures that nova and os-traits are in sync with respect to
COMPUTE_STORAGE_BUS_*.
"""
all_traits = set(ot.get_traits('COMPUTE_STORAGE_BUS_'))
self.flags(hw_machine_type='pc', group='libvirt')
for virt_type in ('qemu', 'kvm'):
self.flags(virt_type=virt_type, group='libvirt')
bus_traits = self.drvr._get_storage_bus_traits()
dom_caps = self.drvr._host.get_domain_capabilities()
buses = dom_caps['x86_64']['pc'].devices.disk.buses
for bus in buses:
name = bus.replace('-', '_').upper()
trait = f'COMPUTE_STORAGE_BUS_{name}'
self.assertIn(trait, bus_traits)
self.assertTrue(bus_traits[trait])
bus_traits.pop(trait)
self.assertTrue(all(not bus for bus in bus_traits.values()))
valid_traits = ot.check_traits(bus_traits)
self.assertEqual(len(bus_traits), len(valid_traits[0]))
self.assertEqual(0, len(valid_traits[1]))
def test_storage_bus_traits__non_qemu_kvm(self):
"""Test getting storage bus traits per virt type."""
all_traits = set(ot.get_traits('COMPUTE_STORAGE_BUS_'))
# ensure each virt type reports the correct bus types
for virt_type, buses in blockinfo.SUPPORTED_STORAGE_BUSES.items():
if virt_type in ('qemu', 'kvm'):
continue
self.flags(virt_type=virt_type, group='libvirt')
bus_traits = self.drvr._get_storage_bus_traits()
# Ensure all bus traits are accounted for
@ -23506,6 +23602,32 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
bus_from_trait = trait.rsplit('_', 1)[1].lower()
self.assertEqual(bus_from_trait in buses, bus_traits[trait])
def test_vif_model_traits(self):
"""Test getting vif model traits per virt type."""
for virt_type, models in libvirt_vif.SUPPORTED_VIF_MODELS.items():
self.flags(virt_type=virt_type, group='libvirt')
vif_models = self.drvr._get_vif_model_traits()
for model in models:
trait = 'COMPUTE_NET_VIF_MODEL_%s' % (
model.replace('-', '_').upper()
)
self.assertIn(trait, vif_models)
self.assertTrue(vif_models[trait])
vif_models.pop(trait)
self.assertTrue(all(not model for model in vif_models.values()))
def test_video_model_traits(self):
"""Test getting video model traits per virt type."""
# NOTE(sean-k-mooney): we do not have a static tables of which video
# models are supported by each virt type so just assert that traits are
# available for all models but not if the traits are mapped to true or
# false.
self.flags(virt_type='qemu', group='libvirt')
model_traits = self.drvr._get_video_model_traits()
for model in fields.VideoModel.ALL:
trait = f'COMPUTE_GRAPHICS_MODEL_{model.upper()}'
self.assertIn(trait, model_traits)
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_cpu_feature_traits',
new=mock.Mock(return_value={}))
def test_cpu_traits__sev_support(self):

View File

@ -122,6 +122,7 @@ class LibvirtConfigDomainCaps(LibvirtConfigObject):
self._features = None
self._machine = None
self._alias = None
self._devices = None
def parse_dom(self, xmldoc):
super(LibvirtConfigDomainCaps, self).parse_dom(xmldoc)
@ -133,6 +134,10 @@ class LibvirtConfigDomainCaps(LibvirtConfigObject):
self._features = features
elif c.tag == "machine":
self._machine = c.text
elif c.tag == "devices":
devices = LibvirtConfigDomainCapsDevices()
devices.parse_dom(c)
self._devices = devices
@property
def features(self):
@ -156,6 +161,79 @@ class LibvirtConfigDomainCaps(LibvirtConfigObject):
def machine_type_alias(self, alias):
self._alias = alias
@property
def devices(self):
if self._devices is None:
return []
return self._devices
class LibvirtConfigDomainCapsVideoModels(LibvirtConfigObject):
def __init__(self, **kwargs):
super().__init__(root_name='video', **kwargs)
self.supported = False
self.models = set()
def parse_dom(self, xmldoc):
super().parse_dom(xmldoc)
if xmldoc.get('supported') == 'yes':
self.supported = True
self.models = {str(node) for node in
xmldoc.xpath("//enum[@name='modelType']/value/text()")}
class LibvirtConfigDomainCapsDiskBuses(LibvirtConfigObject):
def __init__(self, **kwargs):
super().__init__(root_name='disk', **kwargs)
self.supported = False
self.buses = set()
def parse_dom(self, xmldoc):
super(LibvirtConfigDomainCapsDiskBuses, self).parse_dom(xmldoc)
if xmldoc.get('supported') == 'yes':
self.supported = True
self.buses = {str(node) for node in
xmldoc.xpath("//enum[@name='bus']/value/text()")}
class LibvirtConfigDomainCapsDevices(LibvirtConfigObject):
DEVICE_PARSERS = {
'video': LibvirtConfigDomainCapsVideoModels,
'disk': LibvirtConfigDomainCapsDiskBuses,
}
def __init__(self, **kwargs):
super().__init__(root_name='devices', **kwargs)
self.devices = set()
def parse_dom(self, xmldoc):
super().parse_dom(xmldoc)
for c in xmldoc.getchildren():
device = self.DEVICE_PARSERS.get(c.tag)
if device:
device = device()
device.parse_dom(c)
self.devices.add(device)
def _get_device(self, device_type):
for device in self.devices:
if type(device) == self.DEVICE_PARSERS.get(device_type):
return device
return None
@property
def disk(self):
return self._get_device('disk')
@property
def video(self):
return self._get_device('video')
class LibvirtConfigDomainCapsFeatures(LibvirtConfigObject):

View File

@ -415,6 +415,10 @@ class LibvirtDriver(driver.ComputeDriver):
# intended to be updatable directly
self.provider_tree = None
# driver traits will not change during the runtime of the agent
# so calcuate them once and save them
self._static_traits = None
# The CPU models in the configuration are case-insensitive, but the CPU
# model in the libvirt is case-sensitive, therefore create a mapping to
# map the lower case CPU model name to normal CPU model name.
@ -7352,15 +7356,12 @@ class LibvirtDriver(driver.ComputeDriver):
provider_tree.update_inventory(nodename, result)
provider_tree.update_resources(nodename, resources)
# _get_cpu_traits and _get_storage_bus_traits return a dict of trait
# names mapped to boolean values...
traits = self._get_cpu_traits()
traits.update(self._get_storage_bus_traits())
# ..and we add traits equal to True to provider tree while removing
# those equal to False
traits_to_add = [t for t in traits if traits[t]]
traits_to_remove = set(traits) - set(traits_to_add)
# Add supported traits i.e. those equal to True to provider tree while
# removing the unsupported ones
traits_to_add = [
t for t in self.static_traits if self.static_traits[t]
]
traits_to_remove = set(self.static_traits) - set(traits_to_add)
provider_tree.add_traits(nodename, *traits_to_add)
provider_tree.remove_traits(nodename, *traits_to_remove)
@ -7418,6 +7419,26 @@ class LibvirtDriver(driver.ComputeDriver):
else:
return db_const.MAX_INT
@property
def static_traits(self):
if self._static_traits is not None:
return self._static_traits
traits = {}
traits.update(self._get_cpu_traits())
traits.update(self._get_storage_bus_traits())
traits.update(self._get_video_model_traits())
traits.update(self._get_vif_model_traits())
_, invalid_traits = ot.check_traits(traits)
for invalid_trait in invalid_traits:
LOG.debug("Trait '%s' is not valid; ignoring.", invalid_trait)
del traits[invalid_trait]
self._static_traits = traits
return self._static_traits
@staticmethod
def _is_reshape_needed_vgpu_on_root(provider_tree, nodename):
"""Determine if root RP has VGPU inventories.
@ -10451,21 +10472,83 @@ class LibvirtDriver(driver.ComputeDriver):
nova.privsep.fs.FS_FORMAT_EXT4,
nova.privsep.fs.FS_FORMAT_XFS]
def _get_vif_model_traits(self):
"""Get vif model traits based on the currently enabled virt_type.
Not all traits generated by this function may be valid and the result
should be validated.
:return: A dict of trait names mapped to boolean values.
"""
all_models = set(itertools.chain(
*libvirt_vif.SUPPORTED_VIF_MODELS.values()
))
supported_models = libvirt_vif.SUPPORTED_VIF_MODELS.get(
CONF.libvirt.virt_type, []
)
# construct the corresponding standard trait from the VIF model name
return {
f'COMPUTE_NET_VIF_MODEL_{model.replace("-", "_").upper()}': model
in supported_models for model in all_models
}
def _get_storage_bus_traits(self):
"""Get storage bus traits based on the currently enabled virt_type.
For QEMU and KVM this function uses the information returned by the
libvirt domain capabilities API. For other virt types we generate the
traits based on the static information in the blockinfo module.
Not all traits generated by this function may be valid and the result
should be validated.
:return: A dict of trait names mapped to boolean values.
"""
all_buses = set(itertools.chain(
*blockinfo.SUPPORTED_STORAGE_BUSES.values()
))
supported_buses = blockinfo.SUPPORTED_STORAGE_BUSES.get(
CONF.libvirt.virt_type, []
)
if CONF.libvirt.virt_type in ('qemu', 'kvm'):
dom_caps = self._host.get_domain_capabilities()
supported_buses = set()
for arch_type in dom_caps:
for machine_type in dom_caps[arch_type]:
supported_buses.update(
dom_caps[arch_type][machine_type].devices.disk.buses
)
else:
supported_buses = blockinfo.SUPPORTED_STORAGE_BUSES.get(
CONF.libvirt.virt_type, []
)
# construct the corresponding standard trait from the storage bus name
return {
f'COMPUTE_STORAGE_BUS_{bus.upper()}': bus in supported_buses
for bus in all_buses
f'COMPUTE_STORAGE_BUS_{bus.replace("-", "_").upper()}': bus in
supported_buses for bus in all_buses
}
def _get_video_model_traits(self):
"""Get video model traits from libvirt.
Not all traits generated by this function may be valid and the result
should be validated.
:return: A dict of trait names mapped to boolean values.
"""
all_models = fields.VideoModel.ALL
dom_caps = self._host.get_domain_capabilities()
supported_models = set()
for arch_type in dom_caps:
for machine_type in dom_caps[arch_type]:
supported_models.update(
dom_caps[arch_type][machine_type].devices.video.models
)
# construct the corresponding standard trait from the video model name
return {
f'COMPUTE_GRAPHICS_MODEL_{model.replace("-", "_").upper()}': model
in supported_models for model in all_models
}
def _get_cpu_traits(self):

View File

@ -58,41 +58,46 @@ MIN_QEMU_INTERFACE_MTU = (2, 9, 0)
MIN_LIBVIRT_TX_QUEUE_SIZE = (3, 7, 0)
MIN_QEMU_TX_QUEUE_SIZE = (2, 10, 0)
SUPPORTED_VIF_MODELS = {
'qemu': [
network_model.VIF_MODEL_VIRTIO,
network_model.VIF_MODEL_NE2K_PCI,
network_model.VIF_MODEL_PCNET,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000,
network_model.VIF_MODEL_LAN9118,
network_model.VIF_MODEL_SPAPR_VLAN],
'kvm': [
network_model.VIF_MODEL_VIRTIO,
network_model.VIF_MODEL_NE2K_PCI,
network_model.VIF_MODEL_PCNET,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000,
network_model.VIF_MODEL_SPAPR_VLAN],
'xen': [
network_model.VIF_MODEL_NETFRONT,
network_model.VIF_MODEL_NE2K_PCI,
network_model.VIF_MODEL_PCNET,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000],
'lxc': [],
'uml': [],
'parallels': [
network_model.VIF_MODEL_VIRTIO,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000],
}
def is_vif_model_valid_for_virt(virt_type, vif_model):
valid_models = {
'qemu': [network_model.VIF_MODEL_VIRTIO,
network_model.VIF_MODEL_NE2K_PCI,
network_model.VIF_MODEL_PCNET,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000,
network_model.VIF_MODEL_LAN9118,
network_model.VIF_MODEL_SPAPR_VLAN],
'kvm': [network_model.VIF_MODEL_VIRTIO,
network_model.VIF_MODEL_NE2K_PCI,
network_model.VIF_MODEL_PCNET,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000,
network_model.VIF_MODEL_SPAPR_VLAN],
'xen': [network_model.VIF_MODEL_NETFRONT,
network_model.VIF_MODEL_NE2K_PCI,
network_model.VIF_MODEL_PCNET,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000],
'lxc': [],
'uml': [],
'parallels': [network_model.VIF_MODEL_VIRTIO,
network_model.VIF_MODEL_RTL8139,
network_model.VIF_MODEL_E1000],
}
if vif_model is None:
return True
if virt_type not in valid_models:
if virt_type not in SUPPORTED_VIF_MODELS:
raise exception.UnsupportedVirtType(virt=virt_type)
return vif_model in valid_models[virt_type]
return vif_model in SUPPORTED_VIF_MODELS[virt_type]
def set_vf_interface_vlan(pci_addr, mac_addr, vlan=0):