Merge "Adds RemoteFX support to the Hyper-V driver"
This commit is contained in:
@@ -411,6 +411,49 @@ Related options:
|
||||
""")
|
||||
|
||||
|
||||
enable_remotefx_opt = cfg.BoolOpt('enable_remotefx',
|
||||
default=False,
|
||||
help="""
|
||||
Enable RemoteFX feature
|
||||
|
||||
This requires at least one DirectX 11 capable graphics adapter for
|
||||
Windows / Hyper-V Server 2012 R2 or newer and RDS-Virtualization
|
||||
feature has to be enabled.
|
||||
|
||||
Possible values:
|
||||
|
||||
* False: Disables the feature (Default).
|
||||
* True: Enables the feature.
|
||||
|
||||
Instances with RemoteFX can be requested with the following flavor
|
||||
extra specs:
|
||||
|
||||
**os:resolution**. Guest VM screen resolution size. Acceptable values::
|
||||
|
||||
1024x768, 1280x1024, 1600x1200, 1920x1200, 2560x1600, 3840x2160
|
||||
|
||||
``3840x2160`` is only available on Windows / Hyper-V Server 2016.
|
||||
|
||||
**os:monitors**. Guest VM number of monitors. Acceptable values::
|
||||
|
||||
[1, 4] - Windows / Hyper-V Server 2012 R2
|
||||
[1, 8] - Windows / Hyper-V Server 2016
|
||||
|
||||
**os:vram**. Guest VM VRAM amount. Only available on
|
||||
Windows / Hyper-V Server 2016. Acceptable values::
|
||||
|
||||
64, 128, 256, 512, 1024
|
||||
|
||||
Services which consume this:
|
||||
|
||||
* nova-compute
|
||||
|
||||
Related options:
|
||||
|
||||
* None
|
||||
""")
|
||||
|
||||
|
||||
ALL_OPTS = [dynamic_memory_ratio_opt,
|
||||
enable_instance_metrics_collection_opt,
|
||||
instances_path_share_opt,
|
||||
@@ -425,7 +468,8 @@ ALL_OPTS = [dynamic_memory_ratio_opt,
|
||||
config_drive_cdrom_opt,
|
||||
config_drive_inject_password_opt,
|
||||
volume_attach_retry_count_opt,
|
||||
volume_attach_retry_interval_opt]
|
||||
volume_attach_retry_interval_opt,
|
||||
enable_remotefx_opt]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
|
||||
@@ -78,6 +78,11 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
'threads': self.FAKE_NUM_CPUS,
|
||||
'sockets': self.FAKE_NUM_CPUS}}
|
||||
|
||||
def _get_mock_gpu_info(self):
|
||||
return {'remotefx_total_video_ram': 4096,
|
||||
'remotefx_available_video_ram': 2048,
|
||||
'remotefx_gpu_info': mock.sentinel.FAKE_GPU_INFO}
|
||||
|
||||
def test_get_memory_info(self):
|
||||
self._hostops._hostutils.get_memory_info.return_value = (2 * units.Ki,
|
||||
1 * units.Ki)
|
||||
@@ -104,6 +109,29 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
self.assertEqual(6003, response_lower)
|
||||
self.assertEqual(10001, response_higher)
|
||||
|
||||
def test_get_remotefx_gpu_info(self):
|
||||
self.flags(enable_remotefx=True, group='hyperv')
|
||||
fake_gpus = [{'total_video_ram': '2048',
|
||||
'available_video_ram': '1024'},
|
||||
{'total_video_ram': '1024',
|
||||
'available_video_ram': '1024'}]
|
||||
self._hostops._hostutils.get_remotefx_gpu_info.return_value = fake_gpus
|
||||
|
||||
ret_val = self._hostops._get_remotefx_gpu_info()
|
||||
|
||||
self.assertEqual(3072, ret_val['total_video_ram'])
|
||||
self.assertEqual(1024, ret_val['used_video_ram'])
|
||||
|
||||
def test_get_remotefx_gpu_info_disabled(self):
|
||||
self.flags(enable_remotefx=False, group='hyperv')
|
||||
|
||||
ret_val = self._hostops._get_remotefx_gpu_info()
|
||||
|
||||
self.assertEqual(0, ret_val['total_video_ram'])
|
||||
self.assertEqual(0, ret_val['used_video_ram'])
|
||||
self._hostops._hostutils.get_remotefx_gpu_info.assert_not_called()
|
||||
|
||||
@mock.patch.object(hostops.HostOps, '_get_remotefx_gpu_info')
|
||||
@mock.patch.object(hostops.HostOps, '_get_cpu_info')
|
||||
@mock.patch.object(hostops.HostOps, '_get_memory_info')
|
||||
@mock.patch.object(hostops.HostOps, '_get_hypervisor_version')
|
||||
@@ -112,7 +140,8 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
def test_get_available_resource(self, mock_node,
|
||||
mock_get_local_hdd_info_gb,
|
||||
mock_get_hypervisor_version,
|
||||
mock_get_memory_info, mock_get_cpu_info):
|
||||
mock_get_memory_info, mock_get_cpu_info,
|
||||
mock_get_gpu_info):
|
||||
mock_get_local_hdd_info_gb.return_value = (mock.sentinel.LOCAL_GB,
|
||||
mock.sentinel.LOCAL_GB_FREE,
|
||||
mock.sentinel.LOCAL_GB_USED)
|
||||
@@ -123,6 +152,9 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
mock_get_cpu_info.return_value = mock_cpu_info
|
||||
mock_get_hypervisor_version.return_value = mock.sentinel.VERSION
|
||||
|
||||
mock_gpu_info = self._get_mock_gpu_info()
|
||||
mock_get_gpu_info.return_value = mock_gpu_info
|
||||
|
||||
response = self._hostops.get_available_resource()
|
||||
|
||||
mock_get_memory_info.assert_called_once_with()
|
||||
@@ -141,6 +173,9 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
'vcpus_used': 0,
|
||||
'hypervisor_type': 'hyperv',
|
||||
'numa_topology': None,
|
||||
'remotefx_available_video_ram': 2048,
|
||||
'remotefx_gpu_info': mock.sentinel.FAKE_GPU_INFO,
|
||||
'remotefx_total_video_ram': 4096,
|
||||
}
|
||||
self.assertEqual(expected, response)
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ from oslo_utils import units
|
||||
from nova.compute import vm_states
|
||||
from nova import exception
|
||||
from nova import objects
|
||||
from nova.objects import flavor as flavor_obj
|
||||
from nova.tests.unit import fake_instance
|
||||
from nova.tests.unit.objects import test_flavor
|
||||
from nova.tests.unit.objects import test_virtual_interface
|
||||
from nova.tests.unit.virt.hyperv import test_base
|
||||
from nova.virt import hardware
|
||||
@@ -413,9 +415,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
'.attach_volumes')
|
||||
@mock.patch.object(vmops.VMOps, '_attach_drive')
|
||||
@mock.patch.object(vmops.VMOps, '_create_vm_com_port_pipes')
|
||||
def _test_create_instance(self, mock_create_pipes,
|
||||
mock_attach_drive, mock_attach_volumes,
|
||||
fake_root_path, fake_ephemeral_path,
|
||||
@mock.patch.object(vmops.VMOps, '_configure_remotefx')
|
||||
def _test_create_instance(self, mock_configure_remotefx,
|
||||
mock_create_pipes, mock_attach_drive,
|
||||
mock_attach_volumes, fake_root_path,
|
||||
fake_ephemeral_path,
|
||||
enable_instance_metrics,
|
||||
vm_gen=constants.VM_GEN_1):
|
||||
mock_vif_driver = mock.MagicMock()
|
||||
@@ -427,6 +431,9 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||
instance_path = os.path.join(CONF.instances_path, mock_instance.name)
|
||||
|
||||
flavor = flavor_obj.Flavor(**test_flavor.fake_flavor)
|
||||
mock_instance.flavor = flavor
|
||||
|
||||
self._vmops.create_instance(instance=mock_instance,
|
||||
network_info=[fake_network_info],
|
||||
block_device_info=mock.sentinel.DEV_INFO,
|
||||
@@ -438,6 +445,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
mock_instance.vcpus, CONF.hyperv.limit_cpu_features,
|
||||
CONF.hyperv.dynamic_memory_ratio, vm_gen, instance_path,
|
||||
[mock_instance.uuid])
|
||||
|
||||
mock_configure_remotefx.assert_called_once_with(mock_instance, vm_gen)
|
||||
expected = []
|
||||
ctrl_type = vmops.VM_GENERATIONS_CONTROLLER_TYPES[vm_gen]
|
||||
ctrl_disk_addr = 0
|
||||
@@ -1052,6 +1061,64 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
mock.call(mock.sentinel.FAKE_DVD_PATH2,
|
||||
mock.sentinel.FAKE_DEST_PATH))
|
||||
|
||||
def _setup_remotefx_mocks(self):
|
||||
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||
mock_instance.flavor.extra_specs = {
|
||||
'os:resolution': os_win_const.REMOTEFX_MAX_RES_1920x1200,
|
||||
'os:monitors': '2',
|
||||
'os:vram': '256'}
|
||||
|
||||
return mock_instance
|
||||
|
||||
def test_configure_remotefx_not_required(self):
|
||||
self.flags(enable_remotefx=False, group='hyperv')
|
||||
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||
|
||||
self._vmops._configure_remotefx(mock_instance, mock.sentinel.VM_GEN)
|
||||
|
||||
def test_configure_remotefx_exception_enable_config(self):
|
||||
self.flags(enable_remotefx=False, group='hyperv')
|
||||
mock_instance = self._setup_remotefx_mocks()
|
||||
|
||||
self.assertRaises(exception.InstanceUnacceptable,
|
||||
self._vmops._configure_remotefx,
|
||||
mock_instance, mock.sentinel.VM_GEN)
|
||||
|
||||
def test_configure_remotefx_exception_server_feature(self):
|
||||
self.flags(enable_remotefx=True, group='hyperv')
|
||||
mock_instance = self._setup_remotefx_mocks()
|
||||
self._vmops._hostutils.check_server_feature.return_value = False
|
||||
|
||||
self.assertRaises(exception.InstanceUnacceptable,
|
||||
self._vmops._configure_remotefx,
|
||||
mock_instance, mock.sentinel.VM_GEN)
|
||||
|
||||
def test_configure_remotefx_exception_vm_gen(self):
|
||||
self.flags(enable_remotefx=True, group='hyperv')
|
||||
mock_instance = self._setup_remotefx_mocks()
|
||||
self._vmops._hostutils.check_server_feature.return_value = True
|
||||
self._vmops._vmutils.vm_gen_supports_remotefx.return_value = False
|
||||
|
||||
self.assertRaises(exception.InstanceUnacceptable,
|
||||
self._vmops._configure_remotefx,
|
||||
mock_instance, mock.sentinel.VM_GEN)
|
||||
|
||||
def test_configure_remotefx(self):
|
||||
self.flags(enable_remotefx=True, group='hyperv')
|
||||
mock_instance = self._setup_remotefx_mocks()
|
||||
self._vmops._hostutils.check_server_feature.return_value = True
|
||||
self._vmops._vmutils.vm_gen_supports_remotefx.return_value = True
|
||||
extra_specs = mock_instance.flavor.extra_specs
|
||||
|
||||
self._vmops._configure_remotefx(mock_instance,
|
||||
constants.VM_GEN_1)
|
||||
mock_enable_remotefx = (
|
||||
self._vmops._vmutils.enable_remotefx_video_adapter)
|
||||
mock_enable_remotefx.assert_called_once_with(
|
||||
mock_instance.name, int(extra_specs['os:monitors']),
|
||||
extra_specs['os:resolution'],
|
||||
int(extra_specs['os:vram']) * units.Mi)
|
||||
|
||||
@mock.patch.object(vmops.VMOps, '_get_vm_state')
|
||||
def test_check_hotplug_available_vm_disabled(self, mock_get_vm_state):
|
||||
fake_vm = fake_instance.fake_instance_obj(self.context)
|
||||
|
||||
@@ -76,3 +76,7 @@ SERIAL_PORT_TYPE_RW = 'rw'
|
||||
# The default serial console port number used for
|
||||
# logging and interactive sessions.
|
||||
DEFAULT_SERIAL_CONSOLE_PORT = 1
|
||||
|
||||
FLAVOR_ESPEC_REMOTEFX_RES = 'os:resolution'
|
||||
FLAVOR_ESPEC_REMOTEFX_MONITORS = 'os:monitors'
|
||||
FLAVOR_ESPEC_REMOTEFX_VRAM = 'os:vram'
|
||||
|
||||
@@ -105,6 +105,22 @@ class HostOps(object):
|
||||
LOG.debug('Windows version: %s ', version)
|
||||
return version
|
||||
|
||||
def _get_remotefx_gpu_info(self):
|
||||
total_video_ram = 0
|
||||
available_video_ram = 0
|
||||
|
||||
if CONF.hyperv.enable_remotefx:
|
||||
gpus = self._hostutils.get_remotefx_gpu_info()
|
||||
for gpu in gpus:
|
||||
total_video_ram += int(gpu['total_video_ram'])
|
||||
available_video_ram += int(gpu['available_video_ram'])
|
||||
else:
|
||||
gpus = []
|
||||
|
||||
return {'total_video_ram': total_video_ram,
|
||||
'used_video_ram': total_video_ram - available_video_ram,
|
||||
'gpu_info': jsonutils.dumps(gpus)}
|
||||
|
||||
def get_available_resource(self):
|
||||
"""Retrieve resource info.
|
||||
|
||||
@@ -146,6 +162,8 @@ class HostOps(object):
|
||||
'numa_topology': None,
|
||||
}
|
||||
|
||||
gpu_info = self._get_remotefx_gpu_info()
|
||||
dic.update(gpu_info)
|
||||
return dic
|
||||
|
||||
def host_power_action(self, action):
|
||||
|
||||
@@ -287,6 +287,8 @@ class VMOps(object):
|
||||
instance_path,
|
||||
[instance.uuid])
|
||||
|
||||
self._configure_remotefx(instance, vm_gen)
|
||||
|
||||
self._vmutils.create_scsi_controller(instance_name)
|
||||
controller_type = VM_GENERATIONS_CONTROLLER_TYPES[vm_gen]
|
||||
|
||||
@@ -327,6 +329,45 @@ class VMOps(object):
|
||||
if CONF.hyperv.enable_instance_metrics_collection:
|
||||
self._metricsutils.enable_vm_metrics_collection(instance_name)
|
||||
|
||||
def _configure_remotefx(self, instance, vm_gen):
|
||||
extra_specs = instance.flavor.extra_specs
|
||||
remotefx_max_resolution = extra_specs.get(
|
||||
constants.FLAVOR_ESPEC_REMOTEFX_RES)
|
||||
if not remotefx_max_resolution:
|
||||
# RemoteFX not required.
|
||||
return
|
||||
|
||||
if not CONF.hyperv.enable_remotefx:
|
||||
raise exception.InstanceUnacceptable(
|
||||
_("enable_remotefx configuration option needs to be set to "
|
||||
"True in order to use RemoteFX."))
|
||||
|
||||
if not self._hostutils.check_server_feature(
|
||||
self._hostutils.FEATURE_RDS_VIRTUALIZATION):
|
||||
raise exception.InstanceUnacceptable(
|
||||
_("The RDS-Virtualization feature must be installed in order "
|
||||
"to use RemoteFX."))
|
||||
|
||||
if not self._vmutils.vm_gen_supports_remotefx(vm_gen):
|
||||
raise exception.InstanceUnacceptable(
|
||||
_("RemoteFX is not supported on generation %s virtual "
|
||||
"machines on this version of Windows.") % vm_gen)
|
||||
|
||||
instance_name = instance.name
|
||||
LOG.debug('Configuring RemoteFX for instance: %s', instance_name)
|
||||
|
||||
remotefx_monitor_count = int(extra_specs.get(
|
||||
constants.FLAVOR_ESPEC_REMOTEFX_MONITORS) or 1)
|
||||
remotefx_vram = extra_specs.get(
|
||||
constants.FLAVOR_ESPEC_REMOTEFX_VRAM)
|
||||
vram_bytes = int(remotefx_vram) * units.Mi if remotefx_vram else None
|
||||
|
||||
self._vmutils.enable_remotefx_video_adapter(
|
||||
instance_name,
|
||||
remotefx_monitor_count,
|
||||
remotefx_max_resolution,
|
||||
vram_bytes)
|
||||
|
||||
def _attach_drive(self, instance_name, path, drive_addr, ctrl_disk_addr,
|
||||
controller_type, drive_type=constants.DISK):
|
||||
if controller_type == constants.CTRL_TYPE_SCSI:
|
||||
|
||||
62
releasenotes/notes/bp-hyper-v-remotefx-1474ef1a082ad1b0.yaml
Normal file
62
releasenotes/notes/bp-hyper-v-remotefx-1474ef1a082ad1b0.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
features: |
|
||||
- Hyper-V RemoteFX feature.
|
||||
|
||||
Microsoft RemoteFX enhances the visual experience in RDP connections,
|
||||
including providing access to virtualized instances of a physical GPU to
|
||||
multiple guests running on Hyper-V.
|
||||
|
||||
In order to use RemoteFX in Hyper-V 2012 R2, one or more DirectX 11
|
||||
capable display adapters must be present and the RDS-Virtualization
|
||||
server feature must be installed.
|
||||
|
||||
To enable this feature, the following config option must be set in
|
||||
the Hyper-V compute node's 'nova.conf' file::
|
||||
|
||||
[hyperv]
|
||||
enable_remotefx = True
|
||||
|
||||
To create instances with RemoteFX capabilities, the following flavor
|
||||
extra specs must be used:
|
||||
|
||||
**os:resolution**. Guest VM screen resolution size. Acceptable values::
|
||||
|
||||
1024x768, 1280x1024, 1600x1200, 1920x1200, 2560x1600, 3840x2160
|
||||
|
||||
'3840x2160' is only available on Windows / Hyper-V Server 2016.
|
||||
|
||||
**os:monitors**. Guest VM number of monitors. Acceptable values::
|
||||
|
||||
[1, 4] - Windows / Hyper-V Server 2012 R2
|
||||
[1, 8] - Windows / Hyper-V Server 2016
|
||||
|
||||
**os:vram**. Guest VM VRAM amount. Only available on
|
||||
Windows / Hyper-V Server 2016. Acceptable values::
|
||||
|
||||
64, 128, 256, 512, 1024
|
||||
|
||||
There are a few considerations that needs to be kept in mind:
|
||||
|
||||
* Not all guests support RemoteFX capabilities.
|
||||
* Windows / Hyper-V Server 2012 R2 does not support Generation 2 VMs
|
||||
with RemoteFX capabilities.
|
||||
* Per resolution, there is a maximum amount of monitors that can be
|
||||
added. The limits are as follows::
|
||||
|
||||
For Windows / Hyper-V Server 2012 R2::
|
||||
|
||||
1024x768: 4
|
||||
1280x1024: 4
|
||||
1600x1200: 3
|
||||
1920x1200: 2
|
||||
2560x1600: 1
|
||||
|
||||
For Windows / Hyper-V Server 2016::
|
||||
|
||||
1024x768: 8
|
||||
1280x1024: 8
|
||||
1600x1200: 4
|
||||
1920x1200: 4
|
||||
2560x1600: 2
|
||||
3840x2160: 1
|
||||
|
||||
Reference in New Issue
Block a user