Merge "Add Hyper-V storage QoS support"

This commit is contained in:
Jenkins 2016-09-26 23:31:52 +00:00 committed by Gerrit Code Review
commit 5158ca7dcf
6 changed files with 226 additions and 1 deletions

View File

@ -34,6 +34,7 @@ from nova.tests.unit.virt.hyperv import test_base
from nova.virt import hardware
from nova.virt.hyperv import constants
from nova.virt.hyperv import vmops
from nova.virt.hyperv import volumeops
CONF = cfg.CONF
@ -475,6 +476,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps'
'.attach_volumes')
@mock.patch.object(vmops.VMOps, '_set_instance_disk_qos_specs')
@mock.patch.object(vmops.VMOps, '_create_vm_com_port_pipes')
@mock.patch.object(vmops.VMOps, '_attach_ephemerals')
@mock.patch.object(vmops.VMOps, '_attach_root_device')
@ -483,6 +485,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
mock_attach_root_device,
mock_attach_ephemerals,
mock_create_pipes,
mock_set_qos_specs,
mock_attach_volumes,
enable_instance_metrics,
vm_gen=constants.VM_GEN_1):
@ -529,6 +532,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
mock_enable = self._vmops._metricsutils.enable_vm_metrics_collection
if enable_instance_metrics:
mock_enable.assert_called_once_with(mock_instance.name)
mock_set_qos_specs.assert_called_once_with(mock_instance)
def test_create_instance(self):
self._test_create_instance(enable_instance_metrics=True)
@ -1444,3 +1448,64 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
self.assertRaises(exception.InstanceNotRescuable,
self._vmops.unrescue_instance,
mock_instance)
@mock.patch.object(volumeops.VolumeOps, 'bytes_per_sec_to_iops')
@mock.patch.object(vmops.VMOps, '_get_scoped_flavor_extra_specs')
@mock.patch.object(vmops.VMOps, '_get_instance_local_disks')
def test_set_instance_disk_qos_specs(self, mock_get_local_disks,
mock_get_scoped_specs,
mock_bytes_per_sec_to_iops):
fake_total_bytes_sec = 8
fake_total_iops_sec = 1
mock_instance = fake_instance.fake_instance_obj(self.context)
mock_local_disks = [mock.sentinel.root_vhd_path,
mock.sentinel.eph_vhd_path]
mock_get_local_disks.return_value = mock_local_disks
mock_set_qos_specs = self._vmops._vmutils.set_disk_qos_specs
mock_get_scoped_specs.return_value = dict(
disk_total_bytes_sec=fake_total_bytes_sec)
mock_bytes_per_sec_to_iops.return_value = fake_total_iops_sec
self._vmops._set_instance_disk_qos_specs(mock_instance)
mock_bytes_per_sec_to_iops.assert_called_once_with(
fake_total_bytes_sec)
mock_get_local_disks.assert_called_once_with(mock_instance.name)
expected_calls = [mock.call(disk_path, fake_total_iops_sec)
for disk_path in mock_local_disks]
mock_set_qos_specs.assert_has_calls(expected_calls)
def test_get_instance_local_disks(self):
fake_instance_dir = 'fake_instance_dir'
fake_local_disks = [os.path.join(fake_instance_dir, disk_name)
for disk_name in ['root.vhd', 'configdrive.iso']]
fake_instance_disks = ['fake_remote_disk'] + fake_local_disks
mock_get_storage_paths = self._vmops._vmutils.get_vm_storage_paths
mock_get_storage_paths.return_value = [fake_instance_disks, []]
mock_get_instance_dir = self._vmops._pathutils.get_instance_dir
mock_get_instance_dir.return_value = fake_instance_dir
ret_val = self._vmops._get_instance_local_disks(
mock.sentinel.instance_name)
self.assertEqual(fake_local_disks, ret_val)
def test_get_scoped_flavor_extra_specs(self):
# The flavor extra spect dict contains only string values.
fake_total_bytes_sec = '8'
mock_instance = fake_instance.fake_instance_obj(self.context)
mock_instance.flavor.extra_specs = {
'spec_key': 'spec_value',
'quota:total_bytes_sec': fake_total_bytes_sec}
ret_val = self._vmops._get_scoped_flavor_extra_specs(
mock_instance, scope='quota')
expected_specs = {
'total_bytes_sec': fake_total_bytes_sec
}
self.assertEqual(expected_specs, ret_val)

View File

@ -19,6 +19,7 @@ import os
import mock
from os_win import exceptions as os_win_exc
from oslo_config import cfg
from oslo_utils import units
from nova import exception
from nova import test
@ -145,6 +146,25 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
fake_volume_driver.disconnect_volumes.assert_called_once_with(
block_device_mapping)
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
def test_attach_volume(self, mock_get_volume_driver):
fake_conn_info = {
'data': {'qos_specs': mock.sentinel.qos_specs}
}
mock_volume_driver = mock_get_volume_driver.return_value
self._volumeops.attach_volume(fake_conn_info,
mock.sentinel.instance_name,
disk_bus=mock.sentinel.disk_bus)
mock_volume_driver.attach_volume.assert_called_once_with(
fake_conn_info,
mock.sentinel.instance_name,
disk_bus=mock.sentinel.disk_bus)
mock_volume_driver.set_disk_qos_specs.assert_called_once_with(
fake_conn_info, mock.sentinel.qos_specs)
def test_get_volume_connector(self):
mock_instance = mock.DEFAULT
initiator = self._volumeops._volutils.get_iscsi_initiator.return_value
@ -213,6 +233,23 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
self.assertEqual(get_mounted_disk.return_value,
resulted_disk_path)
def test_bytes_per_sec_to_iops(self):
no_bytes = 15 * units.Ki
expected_iops = 2
resulted_iops = self._volumeops.bytes_per_sec_to_iops(no_bytes)
self.assertEqual(expected_iops, resulted_iops)
@mock.patch.object(volumeops.LOG, 'warning')
def test_validate_qos_specs(self, mock_warning):
supported_qos_specs = [mock.sentinel.spec1, mock.sentinel.spec2]
requested_qos_specs = {mock.sentinel.spec1: mock.sentinel.val,
mock.sentinel.spec3: mock.sentinel.val2}
self._volumeops.validate_qos_specs(requested_qos_specs,
supported_qos_specs)
self.assertTrue(mock_warning.called)
class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
"""Unit tests for Hyper-V ISCSIVolumeDriver class."""
@ -601,3 +638,31 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase):
mock_unmount_share = self._volume_driver._smbutils.unmount_smb_share
mock_unmount_share.assert_called_once_with(
self._FAKE_SHARE_NORMALIZED)
@mock.patch.object(volumeops.VolumeOps, 'bytes_per_sec_to_iops')
@mock.patch.object(volumeops.VolumeOps, 'validate_qos_specs')
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path')
def test_set_disk_qos_specs(self, mock_get_disk_path,
mock_validate_qos_specs,
mock_bytes_per_sec_to_iops):
fake_total_bytes_sec = 8
fake_total_iops_sec = 1
storage_qos_specs = {'total_bytes_sec': fake_total_bytes_sec}
expected_supported_specs = ['total_iops_sec', 'total_bytes_sec']
mock_set_qos_specs = self._volume_driver._vmutils.set_disk_qos_specs
mock_bytes_per_sec_to_iops.return_value = fake_total_iops_sec
self._volume_driver.set_disk_qos_specs(mock.sentinel.connection_info,
storage_qos_specs)
mock_validate_qos_specs.assert_called_once_with(
storage_qos_specs, expected_supported_specs)
mock_bytes_per_sec_to_iops.assert_called_once_with(
fake_total_bytes_sec)
mock_disk_path = mock_get_disk_path.return_value
mock_get_disk_path.assert_called_once_with(
mock.sentinel.connection_info)
mock_set_qos_specs.assert_called_once_with(
mock_disk_path,
fake_total_iops_sec)

View File

@ -83,3 +83,5 @@ DEFAULT_SERIAL_CONSOLE_PORT = 1
FLAVOR_ESPEC_REMOTEFX_RES = 'os:resolution'
FLAVOR_ESPEC_REMOTEFX_MONITORS = 'os:monitors'
FLAVOR_ESPEC_REMOTEFX_VRAM = 'os:vram'
IOPS_BASE_SIZE = 8 * units.Ki

View File

@ -350,6 +350,8 @@ class VMOps(object):
if CONF.hyperv.enable_instance_metrics_collection:
self._metricsutils.enable_vm_metrics_collection(instance_name)
self._set_instance_disk_qos_specs(instance)
def _configure_remotefx(self, instance, vm_gen):
extra_specs = instance.flavor.extra_specs
remotefx_max_resolution = extra_specs.get(
@ -869,3 +871,35 @@ class VMOps(object):
self.attach_config_drive(instance, configdrive_path, vm_gen)
self.power_on(instance)
def _set_instance_disk_qos_specs(self, instance):
quota_specs = self._get_scoped_flavor_extra_specs(instance, 'quota')
disk_total_bytes_sec = int(
quota_specs.get('disk_total_bytes_sec') or 0)
disk_total_iops_sec = int(
quota_specs.get('disk_total_iops_sec') or
self._volumeops.bytes_per_sec_to_iops(disk_total_bytes_sec))
if disk_total_iops_sec:
local_disks = self._get_instance_local_disks(instance.name)
for disk_path in local_disks:
self._vmutils.set_disk_qos_specs(disk_path,
disk_total_iops_sec)
def _get_instance_local_disks(self, instance_name):
instance_path = self._pathutils.get_instance_dir(instance_name)
instance_disks = self._vmutils.get_vm_storage_paths(instance_name)[0]
local_disks = [disk_path for disk_path in instance_disks
if instance_path in disk_path]
return local_disks
def _get_scoped_flavor_extra_specs(self, instance, scope):
extra_specs = instance.flavor.extra_specs or {}
filtered_specs = {}
for spec, value in extra_specs.items():
if ':' in spec:
_scope, key = spec.split(':')
if _scope == scope:
filtered_specs[key] = value
return filtered_specs

View File

@ -30,7 +30,7 @@ from six.moves import range
import nova.conf
from nova import exception
from nova.i18n import _, _LE, _LW
from nova.i18n import _, _LE, _LI, _LW
from nova import utils
from nova.virt import driver
from nova.virt.hyperv import constants
@ -78,6 +78,11 @@ class VolumeOps(object):
volume_driver.attach_volume(connection_info, instance_name,
disk_bus=disk_bus)
qos_specs = connection_info['data'].get('qos_specs') or {}
if qos_specs:
volume_driver.set_disk_qos_specs(connection_info,
qos_specs)
def detach_volume(self, connection_info, instance_name):
volume_driver = self._get_volume_driver(
connection_info=connection_info)
@ -147,6 +152,26 @@ class VolumeOps(object):
return volume_driver.get_mounted_disk_path_from_volume(
connection_info)
@staticmethod
def bytes_per_sec_to_iops(no_bytes):
# Hyper-v uses normalized IOPS (8 KB increments)
# as IOPS allocation units.
return (
(no_bytes + constants.IOPS_BASE_SIZE - 1) //
constants.IOPS_BASE_SIZE)
@staticmethod
def validate_qos_specs(qos_specs, supported_qos_specs):
unsupported_specs = set(qos_specs.keys()).difference(
supported_qos_specs)
if unsupported_specs:
msg = (_LW('Got unsupported QoS specs: '
'%(unsupported_specs)s. '
'Supported qos specs: %(supported_qos_specs)s') %
{'unsupported_specs': unsupported_specs,
'supported_qos_specs': supported_qos_specs})
LOG.warning(msg)
class ISCSIVolumeDriver(object):
def __init__(self):
@ -325,6 +350,10 @@ class ISCSIVolumeDriver(object):
def initialize_volume_connection(self, connection_info):
self.login_storage_target(connection_info)
def set_disk_qos_specs(self, connection_info, disk_qos_specs):
LOG.info(_LI("The iSCSI Hyper-V volume driver does not support QoS. "
"Ignoring QoS specs."))
def export_path_synchronized(f):
def wrapper(inst, connection_info, *args, **kwargs):
@ -439,3 +468,16 @@ class SMBFSVolumeDriver(object):
def unmount_synchronized():
self._smbutils.unmount_smb_share(export_path)
unmount_synchronized()
def set_disk_qos_specs(self, connection_info, qos_specs):
supported_qos_specs = ['total_iops_sec', 'total_bytes_sec']
VolumeOps.validate_qos_specs(qos_specs, supported_qos_specs)
total_bytes_sec = int(qos_specs.get('total_bytes_sec') or 0)
total_iops_sec = int(qos_specs.get('total_iops_sec') or
VolumeOps.bytes_per_sec_to_iops(
total_bytes_sec))
if total_iops_sec:
disk_path = self._get_disk_path(connection_info)
self._vmutils.set_disk_qos_specs(disk_path, total_iops_sec)

View File

@ -0,0 +1,17 @@
---
features:
- |
The Hyper-V driver now supports the following quota flavor extra
specs, allowing to specify IO limits applied for each of the
instance local disks, individually.
- quota:disk_total_bytes_sec
- quota:disk_total_iops_sec - those are normalized IOPS, thus each
IO request is accounted for as 1 normalized IO if the size of the
request is less than or equal to a predefined base size (8KB).
Also, the following Cinder front-end QoS specs are now supported
for SMB Cinder backends:
- total_bytes_sec
- total_iops_sec - normalized IOPS