resync: Hyper-V: Adds vNUMA implementation
(cherry-picked from commit 2195e4d68486ed70e55d0b5f038b13bd35e3271c) Change-Id: Ic4e03e641356ea54206ce8fdaecb2837824d2747
This commit is contained in:
parent
eeb97176c4
commit
1ea0ea8b1b
|
@ -122,18 +122,6 @@ class HostOps(object):
|
|||
LOG.debug('Windows version: %s ', version)
|
||||
return version
|
||||
|
||||
def _get_host_numa_topology(self):
|
||||
numa_nodes = self._hostutils.get_numa_nodes()
|
||||
cells = []
|
||||
for numa_node in numa_nodes:
|
||||
numa_node['pinned_cpus'] = set([])
|
||||
numa_node['mempages'] = []
|
||||
numa_node['siblings'] = []
|
||||
cell = objects.NUMACell(**numa_node)
|
||||
cells.append(cell)
|
||||
|
||||
return objects.NUMATopology(cells=cells)
|
||||
|
||||
def _get_remotefx_gpu_info(self):
|
||||
total_video_ram = 0
|
||||
available_video_ram = 0
|
||||
|
@ -150,6 +138,18 @@ class HostOps(object):
|
|||
'used_video_ram': total_video_ram - available_video_ram,
|
||||
'gpu_info': jsonutils.dumps(gpus)}
|
||||
|
||||
def _get_host_numa_topology(self):
|
||||
numa_nodes = self._hostutils.get_numa_nodes()
|
||||
cells = []
|
||||
for numa_node in numa_nodes:
|
||||
# Hyper-V does not support CPU pinning / mempages.
|
||||
# initializing the rest of the fields.
|
||||
numa_node.update(pinned_cpus=set(), mempages=[], siblings=[])
|
||||
cell = objects.NUMACell(**numa_node)
|
||||
cells.append(cell)
|
||||
|
||||
return objects.NUMATopology(cells=cells)
|
||||
|
||||
def get_available_resource(self):
|
||||
"""Retrieve resource info.
|
||||
|
||||
|
@ -192,17 +192,12 @@ class HostOps(object):
|
|||
(obj_fields.Architecture.X86_64,
|
||||
obj_fields.HVType.HYPERV,
|
||||
obj_fields.VMMode.HVM)],
|
||||
'numa_topology': self._get_host_numa_topology()._to_json(),
|
||||
}
|
||||
|
||||
gpu_info = self._get_remotefx_gpu_info()
|
||||
dic.update(gpu_info)
|
||||
|
||||
numa_topology = self._get_host_numa_topology()
|
||||
if numa_topology:
|
||||
dic['numa_topology'] = numa_topology._to_json()
|
||||
else:
|
||||
dic['numa_topology'] = None
|
||||
|
||||
return dic
|
||||
|
||||
def host_power_action(self, action):
|
||||
|
|
|
@ -367,6 +367,9 @@ class VMOps(object):
|
|||
self._get_instance_vnuma_config(instance, image_meta))
|
||||
|
||||
if memory_per_numa_node:
|
||||
LOG.debug("Instance requires vNUMA topology. Host's NUMA spanning "
|
||||
"has to be disabled in order for the instance to "
|
||||
"benefit from it.", instance=instance)
|
||||
if CONF.hyperv.dynamic_memory_ratio > 1.0:
|
||||
LOG.warning(_LW(
|
||||
"Instance vNUMA topology requested, but dynamic memory "
|
||||
|
@ -424,6 +427,48 @@ class VMOps(object):
|
|||
self._configure_secure_vm(context, instance, image_meta,
|
||||
secure_boot_enabled)
|
||||
|
||||
def _get_instance_vnuma_config(self, instance, image_meta):
|
||||
"""Returns the appropriate NUMA configuration for Hyper-V instances,
|
||||
given the desired instance NUMA topology.
|
||||
|
||||
:param instance: instance containing the flavor and it's extra_specs,
|
||||
where the NUMA topology is defined.
|
||||
:param image_meta: image's metadata, containing properties related to
|
||||
the instance's NUMA topology.
|
||||
:returns: memory amount and number of vCPUs per NUMA node or
|
||||
(None, None), if instance NUMA topology was not requested.
|
||||
:raises exception.InstanceUnacceptable:
|
||||
If the given instance NUMA topology is not possible on Hyper-V.
|
||||
"""
|
||||
image_meta = objects.ImageMeta.from_dict(image_meta)
|
||||
instance_topology = hardware.numa_get_constraints(instance.flavor,
|
||||
image_meta)
|
||||
if not instance_topology:
|
||||
# instance NUMA topology was not requested.
|
||||
return None, None
|
||||
|
||||
memory_per_numa_node = instance_topology.cells[0].memory
|
||||
cpus_per_numa_node = len(instance_topology.cells[0].cpuset)
|
||||
|
||||
# validate that the requested NUMA topology is not asymetric.
|
||||
# e.g.: it should be like: (X cpus, X cpus, Y cpus), where X == Y.
|
||||
# same with memory.
|
||||
for cell in instance_topology.cells:
|
||||
if len(cell.cpuset) != cpus_per_numa_node:
|
||||
reason = _("Hyper-V does not support NUMA topologies with "
|
||||
"uneven number of processors. (%(a)s != %(b)s)") % {
|
||||
'a': len(cell.cpuset), 'b': cpus_per_numa_node}
|
||||
raise exception.InstanceUnacceptable(reason=reason,
|
||||
instance_id=instance.uuid)
|
||||
if cell.memory != memory_per_numa_node:
|
||||
reason = _("Hyper-V does not support NUMA topologies with "
|
||||
"uneven amounts of memory. (%(a)s != %(b)s)") % {
|
||||
'a': cell.memory, 'b': memory_per_numa_node}
|
||||
raise exception.InstanceUnacceptable(reason=reason,
|
||||
instance_id=instance.uuid)
|
||||
|
||||
return memory_per_numa_node, cpus_per_numa_node
|
||||
|
||||
def _configure_remotefx(self, instance, vm_gen):
|
||||
extra_specs = instance.flavor.extra_specs
|
||||
remotefx_max_resolution = extra_specs.get(
|
||||
|
@ -621,48 +666,6 @@ class VMOps(object):
|
|||
|
||||
return configdrive_path
|
||||
|
||||
def _get_instance_vnuma_config(self, instance, image_meta):
|
||||
"""Returns the appropriate NUMA configuration for Hyper-V instances,
|
||||
given the desired instance NUMA topology.
|
||||
|
||||
:param instance: instance containing the flavor and it's extra_specs,
|
||||
where the NUMA topology is defined.
|
||||
:param image_meta: image's metadata, containing properties related to
|
||||
the instance's NUMA topology.
|
||||
:returns: memory amount and number of vCPUs per NUMA node or
|
||||
(None, None), if instance NUMA topology was not requested.
|
||||
:raises exception.InstanceUnacceptable:
|
||||
If the given instance NUMA topology is not possible on Hyper-V.
|
||||
"""
|
||||
image_meta = objects.ImageMeta.from_dict(image_meta)
|
||||
|
||||
instance_topology = hardware.numa_get_constraints(instance.flavor,
|
||||
image_meta)
|
||||
if not instance_topology:
|
||||
# instance NUMA topology was not requested.
|
||||
return None, None
|
||||
|
||||
memory_per_numa_node = instance_topology.cells[0].memory
|
||||
cpus_per_numa_node = len(instance_topology.cells[0].cpuset)
|
||||
cpus_pinned = instance_topology.cells[0].cpu_pinning is not None
|
||||
|
||||
if cpus_pinned:
|
||||
raise exception.InstanceUnacceptable(
|
||||
reason="Hyper-V cannot guarantee the CPU pinning.",
|
||||
instance_id=instance.uuid)
|
||||
|
||||
# validate that the requested NUMA topology is not asymetric.
|
||||
# e.g.: it should be like: (X cpus, X cpus, Y cpus), where X == Y.
|
||||
# same with memory.
|
||||
for cell in instance_topology.cells:
|
||||
if (len(cell.cpuset) != cpus_per_numa_node or
|
||||
cell.memory != memory_per_numa_node):
|
||||
raise exception.InstanceUnacceptable(
|
||||
reason="Hyper-V cannot guarantee the given instance NUMA "
|
||||
"topology.", instance_id=instance.uuid)
|
||||
|
||||
return memory_per_numa_node, cpus_per_numa_node
|
||||
|
||||
def attach_config_drive(self, instance, configdrive_path, vm_gen):
|
||||
configdrive_ext = configdrive_path[(configdrive_path.rfind('.') + 1):]
|
||||
# Do the attach here and if there is a certain file format that isn't
|
||||
|
|
|
@ -117,24 +117,6 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
self.assertEqual(6003, response_lower)
|
||||
self.assertEqual(10001, response_higher)
|
||||
|
||||
@mock.patch.object(hostops.objects, 'NUMACell')
|
||||
@mock.patch.object(hostops.objects, 'NUMATopology')
|
||||
def test_get_host_numa_topology(self, mock_NUMATopology, mock_NUMACell):
|
||||
numa_node = {'id': mock.sentinel.id, 'memory': mock.sentinel.memory,
|
||||
'memory_usage': mock.sentinel.memory_usage,
|
||||
'cpuset': mock.sentinel.cpuset,
|
||||
'cpu_usage': mock.sentinel.cpu_usage}
|
||||
self._hostops._hostutils.get_numa_nodes.return_value = [
|
||||
dict(numa_node)]
|
||||
|
||||
result = self._hostops._get_host_numa_topology()
|
||||
|
||||
self.assertEqual(mock_NUMATopology.return_value, result)
|
||||
mock_NUMACell.assert_called_once_with(
|
||||
pinned_cpus=set([]), mempages=[], siblings=[], **numa_node)
|
||||
mock_NUMATopology.assert_called_once_with(
|
||||
cells=[mock_NUMACell.return_value])
|
||||
|
||||
def test_get_remotefx_gpu_info(self):
|
||||
self.flags(enable_remotefx=True, group='hyperv')
|
||||
fake_gpus = [{'total_video_ram': '2048',
|
||||
|
@ -157,6 +139,24 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
self.assertEqual(0, ret_val['used_video_ram'])
|
||||
self._hostops._hostutils.get_remotefx_gpu_info.assert_not_called()
|
||||
|
||||
@mock.patch.object(hostops.objects, 'NUMACell')
|
||||
@mock.patch.object(hostops.objects, 'NUMATopology')
|
||||
def test_get_host_numa_topology(self, mock_NUMATopology, mock_NUMACell):
|
||||
numa_node = {'id': mock.sentinel.id, 'memory': mock.sentinel.memory,
|
||||
'memory_usage': mock.sentinel.memory_usage,
|
||||
'cpuset': mock.sentinel.cpuset,
|
||||
'cpu_usage': mock.sentinel.cpu_usage}
|
||||
self._hostops._hostutils.get_numa_nodes.return_value = [
|
||||
numa_node.copy()]
|
||||
|
||||
result = self._hostops._get_host_numa_topology()
|
||||
|
||||
self.assertEqual(mock_NUMATopology.return_value, result)
|
||||
mock_NUMACell.assert_called_once_with(
|
||||
pinned_cpus=set([]), mempages=[], siblings=[], **numa_node)
|
||||
mock_NUMATopology.assert_called_once_with(
|
||||
cells=[mock_NUMACell.return_value])
|
||||
|
||||
@mock.patch.object(hostops.HostOps, '_get_host_numa_topology')
|
||||
@mock.patch.object(hostops.HostOps, '_get_remotefx_gpu_info')
|
||||
@mock.patch.object(hostops.HostOps, '_get_cpu_info')
|
||||
|
@ -178,11 +178,11 @@ class HostOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
mock_cpu_info = self._get_mock_cpu_info()
|
||||
mock_get_cpu_info.return_value = mock_cpu_info
|
||||
mock_get_hypervisor_version.return_value = mock.sentinel.VERSION
|
||||
mock_get_numa_topology.return_value._to_json.return_value = (
|
||||
mock.sentinel.numa_topology_json)
|
||||
|
||||
mock_gpu_info = self._get_mock_gpu_info()
|
||||
mock_get_gpu_info.return_value = mock_gpu_info
|
||||
mock_get_numa_topology.return_value._to_json.return_value = (
|
||||
mock.sentinel.numa_topology_json)
|
||||
|
||||
self._hostops._hostutils.get_supported_vm_types.return_value = [
|
||||
constants.IMAGE_PROP_VM_GEN_1]
|
||||
|
|
|
@ -576,6 +576,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
@mock.patch('hyperv.nova.vif.get_vif_driver')
|
||||
@mock.patch.object(vmops.VMOps, '_requires_secure_boot')
|
||||
@mock.patch.object(vmops.VMOps, '_requires_certificate')
|
||||
@mock.patch.object(vmops.VMOps, '_get_instance_vnuma_config')
|
||||
@mock.patch.object(vmops.volumeops.VolumeOps, 'attach_volumes')
|
||||
@mock.patch.object(vmops.VMOps, '_set_instance_disk_qos_specs')
|
||||
@mock.patch.object(vmops.VMOps, '_attach_root_device')
|
||||
|
@ -583,21 +584,21 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
@mock.patch.object(vmops.VMOps, '_create_vm_com_port_pipes')
|
||||
@mock.patch.object(vmops.VMOps, '_attach_ephemerals')
|
||||
@mock.patch.object(vmops.VMOps, '_configure_remotefx')
|
||||
@mock.patch.object(vmops.VMOps, '_get_instance_vnuma_config')
|
||||
def _test_create_instance(self, mock_get_instance_vnuma_config,
|
||||
mock_configure_remotefx,
|
||||
def _test_create_instance(self, mock_configure_remotefx,
|
||||
mock_attach_ephemerals,
|
||||
mock_create_pipes,
|
||||
mock_get_port_settings,
|
||||
mock_attach_root_device,
|
||||
mock_set_qos_specs,
|
||||
mock_attach_volumes,
|
||||
mock_get_vnuma_config,
|
||||
mock_requires_certificate,
|
||||
mock_requires_secure_boot,
|
||||
mock_get_vif_driver,
|
||||
mock_configure_secure_vm,
|
||||
enable_instance_metrics,
|
||||
vm_gen=constants.VM_GEN_1, vnuma_enabled=False,
|
||||
vm_gen=constants.VM_GEN_1,
|
||||
vnuma_enabled=False,
|
||||
requires_sec_boot=True):
|
||||
mock_vif_driver = mock_get_vif_driver()
|
||||
self.flags(dynamic_memory_ratio=2.0, group='hyperv')
|
||||
|
@ -613,13 +614,13 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
mock_requires_secure_boot.return_value = True
|
||||
|
||||
if vnuma_enabled:
|
||||
mock_get_instance_vnuma_config.return_value = (
|
||||
mock_get_vnuma_config.return_value = (
|
||||
mock.sentinel.mem_per_numa, mock.sentinel.cpus_per_numa)
|
||||
cpus_per_numa = mock.sentinel.numa_cpus
|
||||
cpus_per_numa = mock.sentinel.cpus_per_numa
|
||||
mem_per_numa = mock.sentinel.mem_per_numa
|
||||
dynamic_memory_ratio = 1.0
|
||||
else:
|
||||
mock_get_instance_vnuma_config.return_value = (None, None)
|
||||
mock_get_vnuma_config.return_value = (None, None)
|
||||
mem_per_numa, cpus_per_numa = (None, None)
|
||||
dynamic_memory_ratio = CONF.hyperv.dynamic_memory_ratio
|
||||
|
||||
|
@ -634,6 +635,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
vm_gen=vm_gen,
|
||||
image_meta=mock.sentinel.image_meta)
|
||||
|
||||
mock_get_vnuma_config.assert_called_once_with(mock_instance,
|
||||
mock.sentinel.image_meta)
|
||||
self._vmops._vmutils.create_vm.assert_called_once_with(
|
||||
mock_instance.name, vnuma_enabled, vm_gen,
|
||||
instance_path, [mock_instance.uuid])
|
||||
|
@ -686,6 +689,61 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
self._test_create_instance(enable_instance_metrics=False,
|
||||
vm_gen=constants.VM_GEN_2)
|
||||
|
||||
def test_create_instance_vnuma_enabled(self):
|
||||
self._test_create_instance(enable_instance_metrics=False,
|
||||
vnuma_enabled=True)
|
||||
|
||||
@mock.patch.object(vmops.hardware, 'numa_get_constraints')
|
||||
@mock.patch.object(vmops.objects.ImageMeta, 'from_dict')
|
||||
def _check_get_instance_vnuma_config_exception(self, mock_from_dict,
|
||||
mock_get_numa, numa_cells):
|
||||
flavor = {'extra_specs': {}}
|
||||
mock_instance = mock.MagicMock(flavor=flavor)
|
||||
image_meta = mock.MagicMock(properties={})
|
||||
mock_get_numa.return_value.cells = numa_cells
|
||||
|
||||
self.assertRaises(exception.InstanceUnacceptable,
|
||||
self._vmops._get_instance_vnuma_config,
|
||||
mock_instance, image_meta)
|
||||
|
||||
def test_get_instance_vnuma_config_bad_cpuset(self):
|
||||
cell1 = mock.MagicMock(cpuset=set([0]), memory=1024)
|
||||
cell2 = mock.MagicMock(cpuset=set([1, 2]), memory=1024)
|
||||
self._check_get_instance_vnuma_config_exception(
|
||||
numa_cells=[cell1, cell2])
|
||||
|
||||
def test_get_instance_vnuma_config_bad_memory(self):
|
||||
cell1 = mock.MagicMock(cpuset=set([0]), memory=1024)
|
||||
cell2 = mock.MagicMock(cpuset=set([1]), memory=2048)
|
||||
self._check_get_instance_vnuma_config_exception(
|
||||
numa_cells=[cell1, cell2])
|
||||
|
||||
@mock.patch.object(vmops.hardware, 'numa_get_constraints')
|
||||
@mock.patch.object(vmops.objects.ImageMeta, 'from_dict')
|
||||
def _check_get_instance_vnuma_config(
|
||||
self, mock_from_dict, mock_get_numa, numa_topology=None,
|
||||
expected_mem_per_numa=None, expected_cpus_per_numa=None):
|
||||
mock_instance = mock.MagicMock()
|
||||
image_meta = mock.MagicMock()
|
||||
mock_get_numa.return_value = numa_topology
|
||||
|
||||
result_memory_per_numa, result_cpus_per_numa = (
|
||||
self._vmops._get_instance_vnuma_config(mock_instance, image_meta))
|
||||
|
||||
self.assertEqual(expected_cpus_per_numa, result_cpus_per_numa)
|
||||
self.assertEqual(expected_mem_per_numa, result_memory_per_numa)
|
||||
|
||||
def test_get_instance_vnuma_config(self):
|
||||
cell1 = mock.MagicMock(cpuset=set([0]), memory=2048, cpu_pinning=None)
|
||||
cell2 = mock.MagicMock(cpuset=set([1]), memory=2048, cpu_pinning=None)
|
||||
mock_topology = mock.MagicMock(cells=[cell1, cell2])
|
||||
self._check_get_instance_vnuma_config(numa_topology=mock_topology,
|
||||
expected_cpus_per_numa=1,
|
||||
expected_mem_per_numa=2048)
|
||||
|
||||
def test_get_instance_vnuma_config_no_topology(self):
|
||||
self._check_get_instance_vnuma_config()
|
||||
|
||||
@mock.patch.object(vmops.volumeops.VolumeOps, 'attach_volume')
|
||||
def test_attach_root_device_volume(self, mock_attach_volume):
|
||||
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||
|
@ -1762,62 +1820,6 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
}
|
||||
self.assertEqual(expected_specs, ret_val)
|
||||
|
||||
@mock.patch.object(vmops.hardware, 'numa_get_constraints')
|
||||
@mock.patch.object(vmops.objects.ImageMeta, 'from_dict')
|
||||
def _check_get_instance_vnuma_config_exception(self, mock_from_dict,
|
||||
mock_get_numa, numa_cells):
|
||||
flavor = {'extra_specs': {}}
|
||||
mock_instance = mock.MagicMock(flavor=flavor)
|
||||
image_meta = mock.MagicMock(properties={})
|
||||
mock_get_numa.return_value.cells = numa_cells
|
||||
|
||||
self.assertRaises(exception.InstanceUnacceptable,
|
||||
self._vmops._get_instance_vnuma_config,
|
||||
mock_instance, image_meta)
|
||||
|
||||
def test_get_instance_vnuma_config_bad_cpuset(self):
|
||||
cell1 = mock.MagicMock(cpuset=set([0]), memory=1024, cpu_pinning=None)
|
||||
cell2 = mock.MagicMock(cpuset=set([1, 2]), memory=1024,
|
||||
cpu_pinning=None)
|
||||
self._check_get_instance_vnuma_config_exception(
|
||||
numa_cells=[cell1, cell2])
|
||||
|
||||
def test_get_instance_vnuma_config_bad_memory(self):
|
||||
cell1 = mock.MagicMock(cpuset=set([0]), memory=1024, cpu_pinning=None)
|
||||
cell2 = mock.MagicMock(cpuset=set([1]), memory=2048, cpu_pinning=None)
|
||||
self._check_get_instance_vnuma_config_exception(
|
||||
numa_cells=[cell1, cell2])
|
||||
|
||||
def test_get_instance_vnuma_config_cpu_pinning_requested(self):
|
||||
cell = mock.MagicMock(cpu_pinning={})
|
||||
self._check_get_instance_vnuma_config_exception(numa_cells=[cell])
|
||||
|
||||
@mock.patch.object(vmops.hardware, 'numa_get_constraints')
|
||||
@mock.patch.object(vmops.objects.ImageMeta, 'from_dict')
|
||||
def _check_get_instance_vnuma_config(
|
||||
self, mock_from_dict, mock_get_numa, numa_topology=None,
|
||||
expected_mem_per_numa=None, expected_cpus_per_numa=None):
|
||||
mock_instance = mock.MagicMock()
|
||||
image_meta = mock.MagicMock()
|
||||
mock_get_numa.return_value = numa_topology
|
||||
|
||||
result_memory_per_numa, result_cpus_per_numa = (
|
||||
self._vmops._get_instance_vnuma_config(mock_instance, image_meta))
|
||||
|
||||
self.assertEqual(expected_cpus_per_numa, result_cpus_per_numa)
|
||||
self.assertEqual(expected_mem_per_numa, result_memory_per_numa)
|
||||
|
||||
def test_get_instance_vnuma_config(self):
|
||||
cell1 = mock.MagicMock(cpuset=set([0]), memory=2048, cpu_pinning=None)
|
||||
cell2 = mock.MagicMock(cpuset=set([1]), memory=2048, cpu_pinning=None)
|
||||
mock_topology = mock.MagicMock(cells=[cell1, cell2])
|
||||
self._check_get_instance_vnuma_config(numa_topology=mock_topology,
|
||||
expected_cpus_per_numa=1,
|
||||
expected_mem_per_numa=2048)
|
||||
|
||||
def test_get_instance_vnuma_config_no_topology(self):
|
||||
self._check_get_instance_vnuma_config()
|
||||
|
||||
@mock.patch.object(vmops.VMOps, '_get_vif_driver')
|
||||
def test_unplug_vifs(self, mock_get_vif_driver):
|
||||
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||
|
|
Loading…
Reference in New Issue