diff --git a/nova/api/validation/extra_specs/hw.py b/nova/api/validation/extra_specs/hw.py index c50355b794..9a9e8f9964 100644 --- a/nova/api/validation/extra_specs/hw.py +++ b/nova/api/validation/extra_specs/hw.py @@ -552,6 +552,33 @@ feature_flag_validators = [ '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 = [ diff --git a/nova/compute/api.py b/nova/compute/api.py index d13a71ff8b..a291610246 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -125,6 +125,7 @@ MIN_COMPUTE_VDPA_HOTPLUG_LIVE_MIGRATION = 63 SUPPORT_SHARES = 67 MIN_COMPUTE_SOUND_MODEL_TRAITS = 69 +MIN_COMPUTE_USB_MODEL_TRAITS = 70 # 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 diff --git a/nova/objects/service.py b/nova/objects/service.py index 60863e5f3a..5903ed7ee5 100644 --- a/nova/objects/service.py +++ b/nova/objects/service.py @@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__) # 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 @@ -240,6 +240,9 @@ SERVICE_VERSION_HISTORY = ( # Version 69: Compute RPC v6.4: # Compute manager supports sound model traits {'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 diff --git a/nova/tests/unit/compute/test_flavors.py b/nova/tests/unit/compute/test_flavors.py index ba0eabc77d..fef54178ae 100644 --- a/nova/tests/unit/compute/test_flavors.py +++ b/nova/tests/unit/compute/test_flavors.py @@ -22,6 +22,7 @@ from nova import exception from nova import objects from nova.objects import base as obj_base from nova import test +from nova.virt.hardware import get_redirected_usb_ports class TestValidateExtraSpecKeys(test.NoDBTestCase): @@ -292,3 +293,73 @@ class TestCreateFlavor(test.TestCase): self.assertRaises( exception.FlavorIdExists, 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) diff --git a/nova/tests/unit/objects/test_image_meta.py b/nova/tests/unit/objects/test_image_meta.py index 508ed4d025..fab3ae81d5 100644 --- a/nova/tests/unit/objects/test_image_meta.py +++ b/nova/tests/unit/objects/test_image_meta.py @@ -569,6 +569,21 @@ class TestImageMetaProps(test.NoDBTestCase): primitive = obj.obj_to_primitive('1.27') 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): obj = objects.ImageMetaProps( hw_pci_numa_affinity_policy=fields.PCINUMAAffinityPolicy.SOCKET) diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index d35d17ff1c..7b527590a9 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -2940,3 +2940,66 @@ def get_sound_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 diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index fbb63a6e91..7b31bd2a24 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -4048,3 +4048,19 @@ class LibvirtConfigGuestSound(LibvirtConfigObject): meta = self._new_node('sound') meta.set('model', str(self.model)) 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 diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 571bcf8d53..e85a752342 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -6901,6 +6901,18 @@ class LibvirtDriver(driver.ComputeDriver): sound_device = vconfig.LibvirtConfigGuestSound(sound_model) 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): # Enable qga only if the 'hw_qemu_guest_agent' is equal to yes if image_meta.properties.get('hw_qemu_guest_agent', False): @@ -7414,7 +7426,7 @@ class LibvirtDriver(driver.ComputeDriver): 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. 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.index = 0 + # an unset model means autodetect, while 'none' means don't add a # controller (x86 gets one by default) usbhost.model = None + specified_model = hardware.get_usb_model(flavor, image_meta) if not self._guest_needs_usb(guest, image_meta): archs = ( fields.Architecture.PPC, @@ -7438,6 +7452,8 @@ class LibvirtDriver(driver.ComputeDriver): # xml, where 'none' adds it but then disables it causing # libvirt errors and the instances not being able to build usbhost.model = None + elif specified_model: + usbhost.model = specified_model else: usbhost.model = 'none' guest.add_device(usbhost) @@ -7602,7 +7618,8 @@ class LibvirtDriver(driver.ComputeDriver): if self._guest_needs_pcie(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) diff --git a/releasenotes/notes/sound-model-extra-specs-2bcbe644b889005c.yaml b/releasenotes/notes/sound-model-extra-specs-2bcbe644b889005c.yaml index 4ead886ea9..ebc0f0f0d3 100644 --- a/releasenotes/notes/sound-model-extra-specs-2bcbe644b889005c.yaml +++ b/releasenotes/notes/sound-model-extra-specs-2bcbe644b889005c.yaml @@ -1,11 +1,12 @@ --- features: - | - The `hw:sound_model` flavor extra spec and the matching `hw_sound_model` - image property were added to allow the configuration of a sound device - within an instance. This is useful with the new spice-direct console - type. The default remains no sound device, but when using the libvirt - hypervisor driver you can select from `sb16`, `es1370`, `pcspk`, `ac97`, - `ich6`, `ich9`, `usb`, and `virtio`. For most use-cases `usb` is - likely to be the best choice unless you have at least libvirt 8.2.0 and - libvirt 10.4.0. + The ``hw:sound_model`` flavor extra spec and the matching + ``hw_sound_model`` image property were added to allow the + configuration of a sound device within an instance. This is useful + with the new spice-direct console type. The default remains no sound + device, but when using the libvirt hypervisor driver you can select + from ``sb16``, ``es1370``, ``pcspk``, ``ac97``, ``ich6``, ``ich9``, + ``usb``, and ``virtio``. For most use-cases ``usb`` is likely to be + the best choice unless you have at least libvirt 8.2.0 and libvirt + 10.4.0. diff --git a/releasenotes/notes/usb-controller-extra-specs-a2209e3563d18a26.yaml b/releasenotes/notes/usb-controller-extra-specs-a2209e3563d18a26.yaml new file mode 100644 index 0000000000..f23156cfb1 --- /dev/null +++ b/releasenotes/notes/usb-controller-extra-specs-a2209e3563d18a26.yaml @@ -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``.