Merge "Implement USB controller extra spec for libvirt."
This commit is contained in:
@@ -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 = [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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``.
|
||||||
Reference in New Issue
Block a user