Merge "Implement USB controller extra spec for libvirt."

This commit is contained in:
Zuul
2025-08-01 00:00:03 +00:00
committed by Gerrit Code Review
10 changed files with 238 additions and 11 deletions
+27
View File
@@ -552,6 +552,33 @@ feature_flag_validators = [
'enum': fields.SoundModelType.ALL, 'enum': fields.SoundModelType.ALL,
}, },
), ),
base.ExtraSpecValidator(
name='hw:usb_model',
description=(
'The model of the attached USB controller device. '
'Only supported by the libvirt virt driver. '
'If unset, no USB controller device is attached.'
),
value={
'type': str,
'description': 'A USB controller model',
'enum': fields.USBControllerModelType.ALL,
},
),
base.ExtraSpecValidator(
name='hw:redirected_usb_ports',
description=(
'The number of redirected USB ports to add to the virtual '
'machine. Only supported by the libvirt virt driver. If unset, '
'no redirected USB ports are added. The maximum value is 15.'
),
value={
'type': int,
'description': 'The number of USB redirection devices to add',
'min': 0,
'max': 15
},
),
] ]
ephemeral_encryption_validators = [ ephemeral_encryption_validators = [
+1
View File
@@ -125,6 +125,7 @@ MIN_COMPUTE_VDPA_HOTPLUG_LIVE_MIGRATION = 63
SUPPORT_SHARES = 67 SUPPORT_SHARES = 67
MIN_COMPUTE_SOUND_MODEL_TRAITS = 69 MIN_COMPUTE_SOUND_MODEL_TRAITS = 69
MIN_COMPUTE_USB_MODEL_TRAITS = 70
# FIXME(danms): Keep a global cache of the cells we find the # FIXME(danms): Keep a global cache of the cells we find the
# first time we look. This needs to be refreshed on a timer or # first time we look. This needs to be refreshed on a timer or
+4 -1
View File
@@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter # NOTE(danms): This is the global service version counter
SERVICE_VERSION = 69 SERVICE_VERSION = 70
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any # NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@@ -240,6 +240,9 @@ SERVICE_VERSION_HISTORY = (
# Version 69: Compute RPC v6.4: # Version 69: Compute RPC v6.4:
# Compute manager supports sound model traits # Compute manager supports sound model traits
{'compute_rpc': '6.4'}, {'compute_rpc': '6.4'},
# Version 70: Compute RPC v6.4:
# Compute manager supports USB controller model traits
{'compute_rpc': '6.4'},
) )
# This is the version after which we can rely on having a persistent # This is the version after which we can rely on having a persistent
+71
View File
@@ -22,6 +22,7 @@ from nova import exception
from nova import objects from nova import objects
from nova.objects import base as obj_base from nova.objects import base as obj_base
from nova import test from nova import test
from nova.virt.hardware import get_redirected_usb_ports
class TestValidateExtraSpecKeys(test.NoDBTestCase): class TestValidateExtraSpecKeys(test.NoDBTestCase):
@@ -292,3 +293,73 @@ class TestCreateFlavor(test.TestCase):
self.assertRaises( self.assertRaises(
exception.FlavorIdExists, exception.FlavorIdExists,
flavors.create, 'flavor2', 64, 1, 120, flavorid='flavorid') flavors.create, 'flavor2', 64, 1, 120, flavorid='flavorid')
class RedirectedUSBPortsFlavorTests(test.TestCase):
def setUp(self):
super().setUp()
self.image_meta = objects.ImageMeta.from_dict(
{
'status': 'active',
'container_format': 'bare',
'min_ram': 0,
'updated_at': '2014-12-12T11:16:36.000000',
'min_disk': 0,
'owner': '2d8b9502858c406ebee60f0849486222',
'protected': 'yes',
'properties': {
'os_type': 'Linux',
'hw_video_model': 'vga',
'hw_video_ram': '512',
'hw_qemu_guest_agent': 'yes',
'hw_scsi_model': 'virtio-scsi',
},
'size': 213581824,
'name': 'f16-x86_64-openstack-sda',
'checksum': '755122332caeb9f661d5c978adb8b45f',
'created_at': '2014-12-10T16:23:14.000000',
'disk_format': 'qcow2',
'id': 'c8b1790e-a07d-4971-b137-44f2432936cd',
}
)
def test_redirected_usb_ports_flavor_specs_integer(self):
self.assertEqual(
1, get_redirected_usb_ports(
{
'extra_specs': {
'hw:redirected_usb_ports': 1
}
},
self.image_meta))
def test_redirected_usb_ports_flavor_specs_integer_as_string(self):
self.assertEqual(
1, get_redirected_usb_ports(
{
'extra_specs': {
'hw:redirected_usb_ports': '1'
}
},
self.image_meta))
def test_redirected_usb_ports_flavor_specs_integer_is_negative(self):
self.assertRaises(exception.Invalid,
get_redirected_usb_ports,
{
'extra_specs': {
'hw:redirected_usb_ports': -1
}
},
self.image_meta)
def test_redirected_usb_ports_flavor_specs_non_integer(self):
self.assertRaises(exception.Invalid,
get_redirected_usb_ports,
{
'extra_specs': {
'hw:redirected_usb_ports': 'banana'
}
},
self.image_meta)
@@ -569,6 +569,21 @@ class TestImageMetaProps(test.NoDBTestCase):
primitive = obj.obj_to_primitive('1.27') primitive = obj.obj_to_primitive('1.27')
self.assertNotIn('hw_sound_model', primitive['nova_object.data']) self.assertNotIn('hw_sound_model', primitive['nova_object.data'])
def test_obj_make_compatible_usb_model_and_redirected_ports(self):
"""Check if we pop hw_usb_model and hw_redirected_usb_ports."""
obj = objects.ImageMetaProps(
hw_usb_model='qemu-xhci',
hw_redirected_usb_ports=3
)
primitive = obj.obj_to_primitive()
self.assertIn('hw_usb_model', primitive['nova_object.data'])
self.assertIn('hw_redirected_usb_ports', primitive['nova_object.data'])
primitive = obj.obj_to_primitive('1.27')
self.assertNotIn('hw_usb_model', primitive['nova_object.data'])
self.assertNotIn(
'hw_redirected_usb_ports', primitive['nova_object.data'])
def test_obj_make_compatible_socket_policy(self): def test_obj_make_compatible_socket_policy(self):
obj = objects.ImageMetaProps( obj = objects.ImageMetaProps(
hw_pci_numa_affinity_policy=fields.PCINUMAAffinityPolicy.SOCKET) hw_pci_numa_affinity_policy=fields.PCINUMAAffinityPolicy.SOCKET)
+63
View File
@@ -2940,3 +2940,66 @@ def get_sound_model(
) )
return model return model
def get_usb_model(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[str]:
"""Get the USB controller model, if any.
:param flavor: ``nova.objects.Flavor`` instance
:param image_meta: ``nova.objects.ImageMeta`` instance
:raises: nova.exception.FlavorImageConflict if a value is specified in both
the flavor and the image, but the values do not match
:raises: nova.exception.Invalid if a value or combination of values is
invalid
:returns: A string containing the USB controller model, else None.
"""
model = _get_unique_flavor_image_meta('usb_model', flavor, image_meta)
if model and model not in fields.USBControllerModelType.ALL:
raise exception.Invalid(
'Invalid USB controller model %(model)r. '
'Allowed values: %(valid)s.'
% {
'model': model,
'valid': ', '.join(fields.USBControllerModelType.ALL)
})
return model
def get_redirected_usb_ports(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> int:
"""Get the number of redirected USB ports, if any.
:param flavor: ``nova.objects.Flavor`` instance
:param image_meta: ``nova.objects.ImageMeta`` instance
:raises: nova.exception.FlavorImageConflict if a value is specified in both
the flavor and the image, but the values do not match
:raises: nova.exception.Invalid if a value or combination of values is
invalid
:returns: An integer number of ports, else 0.
"""
count = _get_unique_flavor_image_meta(
'redirected_usb_ports', flavor, image_meta)
if not count:
count = 0
else:
try:
count = int(count)
except ValueError:
raise exception.Invalid('"%s" is not a valid integer.' % count)
if count < 0:
raise exception.Invalid(
'You cannot have a negative number of USB ports.')
elif count > 15:
# NOTE(mikal): XHCI controllers only support up to 15 ports. This isn't
# documented in the libvirt domain XML documentation at the moment, but
# is at https://www.kraxel.org/blog/2018/08/qemu-usb-tips/.
raise exception.Invalid('Nova only supports up to 15 USB ports.')
return count
+16
View File
@@ -4048,3 +4048,19 @@ class LibvirtConfigGuestSound(LibvirtConfigObject):
meta = self._new_node('sound') meta = self._new_node('sound')
meta.set('model', str(self.model)) meta.set('model', str(self.model))
return meta return meta
class LibvirtConfigGuestUSBRedirect(LibvirtConfigObject):
def __init__(self, **kwargs):
super(LibvirtConfigGuestUSBRedirect,
self).__init__(root_name='redirdev')
self.type = 'spicevmc'
self.bus = 'usb'
def format_dom(self):
meta = self._new_node('redirdev')
meta.set('type', str(self.type))
meta.set('bus', str(self.bus))
return meta
+19 -2
View File
@@ -6901,6 +6901,18 @@ class LibvirtDriver(driver.ComputeDriver):
sound_device = vconfig.LibvirtConfigGuestSound(sound_model) sound_device = vconfig.LibvirtConfigGuestSound(sound_model)
guest.add_device(sound_device) guest.add_device(sound_device)
def _add_redirected_usb_ports(
self,
guest: vconfig.LibvirtConfigGuest,
flavor: 'objects.Flavor',
instance: 'objects.Instance',
image_meta: 'objects.ImageMeta',
) -> None:
"""Add redirected USB ports, if requested."""
count = hardware.get_redirected_usb_ports(flavor, image_meta)
for i in range(count):
guest.add_device(vconfig.LibvirtConfigGuestUSBRedirect())
def _set_qemu_guest_agent(self, guest, flavor, instance, image_meta): def _set_qemu_guest_agent(self, guest, flavor, instance, image_meta):
# Enable qga only if the 'hw_qemu_guest_agent' is equal to yes # Enable qga only if the 'hw_qemu_guest_agent' is equal to yes
if image_meta.properties.get('hw_qemu_guest_agent', False): if image_meta.properties.get('hw_qemu_guest_agent', False):
@@ -7414,7 +7426,7 @@ class LibvirtDriver(driver.ComputeDriver):
return False return False
def _guest_add_usb_root_controller(self, guest, image_meta): def _guest_add_usb_root_controller(self, guest, flavor, image_meta):
"""Add USB root controller, if necessary. """Add USB root controller, if necessary.
Note that these are added by default on x86-64. We add the controller Note that these are added by default on x86-64. We add the controller
@@ -7423,9 +7435,11 @@ class LibvirtDriver(driver.ComputeDriver):
""" """
usbhost = vconfig.LibvirtConfigGuestUSBHostController() usbhost = vconfig.LibvirtConfigGuestUSBHostController()
usbhost.index = 0 usbhost.index = 0
# an unset model means autodetect, while 'none' means don't add a # an unset model means autodetect, while 'none' means don't add a
# controller (x86 gets one by default) # controller (x86 gets one by default)
usbhost.model = None usbhost.model = None
specified_model = hardware.get_usb_model(flavor, image_meta)
if not self._guest_needs_usb(guest, image_meta): if not self._guest_needs_usb(guest, image_meta):
archs = ( archs = (
fields.Architecture.PPC, fields.Architecture.PPC,
@@ -7438,6 +7452,8 @@ class LibvirtDriver(driver.ComputeDriver):
# xml, where 'none' adds it but then disables it causing # xml, where 'none' adds it but then disables it causing
# libvirt errors and the instances not being able to build # libvirt errors and the instances not being able to build
usbhost.model = None usbhost.model = None
elif specified_model:
usbhost.model = specified_model
else: else:
usbhost.model = 'none' usbhost.model = 'none'
guest.add_device(usbhost) guest.add_device(usbhost)
@@ -7602,7 +7618,8 @@ class LibvirtDriver(driver.ComputeDriver):
if self._guest_needs_pcie(guest): if self._guest_needs_pcie(guest):
self._guest_add_pcie_root_ports(guest) self._guest_add_pcie_root_ports(guest)
self._guest_add_usb_root_controller(guest, image_meta) self._guest_add_usb_root_controller(guest, flavor, image_meta)
self._add_redirected_usb_ports(guest, flavor, instance, image_meta)
self._guest_add_pci_devices(guest, instance) self._guest_add_pci_devices(guest, instance)
@@ -1,11 +1,12 @@
--- ---
features: features:
- | - |
The `hw:sound_model` flavor extra spec and the matching `hw_sound_model` The ``hw:sound_model`` flavor extra spec and the matching
image property were added to allow the configuration of a sound device ``hw_sound_model`` image property were added to allow the
within an instance. This is useful with the new spice-direct console configuration of a sound device within an instance. This is useful
type. The default remains no sound device, but when using the libvirt with the new spice-direct console type. The default remains no sound
hypervisor driver you can select from `sb16`, `es1370`, `pcspk`, `ac97`, device, but when using the libvirt hypervisor driver you can select
`ich6`, `ich9`, `usb`, and `virtio`. For most use-cases `usb` is from ``sb16``, ``es1370``, ``pcspk``, ``ac97``, ``ich6``, ``ich9``,
likely to be the best choice unless you have at least libvirt 8.2.0 and ``usb``, and ``virtio``. For most use-cases ``usb`` is likely to be
libvirt 10.4.0. the best choice unless you have at least libvirt 8.2.0 and libvirt
10.4.0.
@@ -0,0 +1,13 @@
---
features:
- |
The ``hw:usb_model`` flavor extra spec and the matching ``hw_usb_model``
image property were added to allow the configuration of a USB controller
within an instance. This is useful with the new spice-direct console
type which supports passing through USB devices from the client to the
instance, such as a smart card reader. There is also an additional
``hw:redirected_usb_ports`` / ``hw_redirected_usb_ports`` pair which
controls how many ports the USB controller has. This number will
vary based on the USB controller selected. The default remains no
USB controller, but when using the libvirt hypervisor driver you can
now also select from ``qemu_xhci`` and ``nec_xhci``.