Files
compute-hyperv/hyperv/tests/unit/test_volumeops.py
Lucian Petrut cb0486cc72 Add Hyper-V storage QoS support
Hyper-V provides options to specify maximum/minimum IOPS per virtual
disk image.

By leveraging this feature, this blueprint proposes to add support
for setting QoS specs targeting instance local disks as well as
volumes exported through SMB.

Implements: bp hyperv-storage-qos
Change-Id: I4ce44c8b7ab54a0bfc8c7401e675876dbd109e2d
2015-05-13 05:39:31 -07:00

387 lines
16 KiB
Python

# Copyright 2014 Cloudbase Solutions Srl
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import mock
import os
from nova import exception
from oslo_utils import units
from hyperv.nova import pathutils
from hyperv.nova import vmutils
from hyperv.nova import volumeops
from hyperv.tests.unit import db_fakes
from hyperv.tests.unit import test_base
FAKE_TARGET_PORTAL = 'fakeportal:3260'
FAKE_VOLUME_ID = 'fake_volume_id'
class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
"""Unit tests for VolumeOps class."""
def setUp(self):
super(VolumeOpsTestCase, self).setUp()
self._volumeops = volumeops.VolumeOps()
def test_get_volume_driver_exception(self):
fake_conn_info = {'driver_volume_type': 'fake_driver'}
self.assertRaises(exception.VolumeDriverNotFound,
self._volumeops._get_volume_driver,
connection_info=fake_conn_info)
def test_fix_instance_volume_disk_paths(self):
block_device_info = db_fakes.get_fake_block_device_info(
FAKE_TARGET_PORTAL, FAKE_VOLUME_ID)
fake_vol_conn_info = (
block_device_info['block_device_mapping'][0]['connection_info'])
with contextlib.nested(
mock.patch.object(self._volumeops,
'_get_volume_driver'),
mock.patch.object(self._volumeops,
'ebs_root_in_block_devices')
) as (mock_get_volume_driver,
mock_ebs_in_block_devices):
fake_vol_driver = mock_get_volume_driver.return_value
mock_ebs_in_block_devices.return_value = False
self._volumeops.fix_instance_volume_disk_paths(
mock.sentinel.instance_name,
block_device_info)
func = fake_vol_driver.fix_instance_volume_disk_path
func.assert_called_once_with(
mock.sentinel.instance_name,
fake_vol_conn_info, 0)
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
@mock.patch.object(volumeops.VolumeOps, 'parse_disk_qos_specs')
def test_attach_volume(self, mock_parse_qos_specs,
mock_get_volume_driver):
fake_conn_info = {
'data': {'qos_specs': mock.sentinel.qos_specs}
}
mock_volume_driver = mock_get_volume_driver.return_value
mock_parse_qos_specs.return_value = [
mock.sentinel.min_iops,
mock.sentinel.max_iops
]
self._volumeops.attach_volume(fake_conn_info,
mock.sentinel.instance_name,
mock.sentinel.ebs_root)
mock_volume_driver.attach_volume.assert_called_once_with(
fake_conn_info,
mock.sentinel.instance_name,
mock.sentinel.ebs_root)
mock_parse_qos_specs.assert_called_once_with(mock.sentinel.qos_specs)
mock_volume_driver.set_disk_qos_specs.assert_called_once_with(
fake_conn_info, mock.sentinel.instance_name,
mock.sentinel.min_iops, mock.sentinel.max_iops)
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
def test_disconnect_volumes(self, mock_get_volume_driver):
block_device_info = db_fakes.get_fake_block_device_info(
FAKE_TARGET_PORTAL, FAKE_VOLUME_ID)
block_device_mapping = (
block_device_info['block_device_mapping'])
fake_volume_driver = mock_get_volume_driver.return_value
self._volumeops.disconnect_volumes(block_device_info)
fake_volume_driver.disconnect_volumes.assert_called_once_with(
block_device_mapping)
def test_parse_disk_qos_specs_using_iops(self):
fake_qos_specs = {
'total_iops_sec': 10,
'min_iops_sec': 1,
}
ret_val = self._volumeops.parse_disk_qos_specs(fake_qos_specs)
expected_qos_specs = (fake_qos_specs['min_iops_sec'],
fake_qos_specs['total_iops_sec'])
self.assertEqual(expected_qos_specs, ret_val)
def test_parse_disk_qos_specs_using_bytes_per_sec(self):
fake_qos_specs = {
'total_bytes_sec': units.Ki * 15,
'min_bytes_sec': 0,
}
ret_val = self._volumeops.parse_disk_qos_specs(fake_qos_specs)
expected_qos_specs = (0, 2) # Normalized IOPS
self.assertEqual(expected_qos_specs, ret_val)
def test_parse_disk_qos_specs_exception(self):
fake_qos_specs = {
'total_iops_sec': 1,
'min_iops_sec': 2
}
self.assertRaises(vmutils.HyperVException,
self._volumeops.parse_disk_qos_specs,
fake_qos_specs)
class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
"""Unit tests for Hyper-V ISCSIVolumeDriver class."""
def setUp(self):
super(ISCSIVolumeDriverTestCase, self).setUp()
self._volume_driver = volumeops.ISCSIVolumeDriver()
def test_get_mounted_disk_from_lun(self):
with contextlib.nested(
mock.patch.object(self._volume_driver._volutils,
'get_device_number_for_target'),
mock.patch.object(self._volume_driver._vmutils,
'get_mounted_disk_by_drive_number')
) as (mock_get_device_number_for_target,
mock_get_mounted_disk_by_drive_number):
mock_get_device_number_for_target.return_value = 0
mock_get_mounted_disk_by_drive_number.return_value = (
mock.sentinel.disk_path)
disk = self._volume_driver._get_mounted_disk_from_lun(
mock.sentinel.target_iqn,
mock.sentinel.target_lun)
self.assertEqual(disk, mock.sentinel.disk_path)
def test_fix_instace_volume_disk_path(self):
block_device_info = db_fakes.get_fake_block_device_info(
FAKE_TARGET_PORTAL, FAKE_VOLUME_ID)
fake_vol_conn_info = (
block_device_info['block_device_mapping'][0]['connection_info'])
with contextlib.nested(
mock.patch.object(self._volume_driver,
'_get_mounted_disk_from_lun'),
mock.patch.object(self._volume_driver._vmutils,
'get_vm_scsi_controller'),
mock.patch.object(self._volume_driver._vmutils,
'set_disk_host_resource')
) as (mock_get_mounted_disk_from_lun,
mock_get_vm_scsi_controller,
mock_set_disk_host_resource):
mock_get_mounted_disk_from_lun.return_value = (
mock.sentinel.mounted_path)
mock_get_vm_scsi_controller.return_value = (
mock.sentinel.controller_path)
self._volume_driver.fix_instance_volume_disk_path(
mock.sentinel.instance_name,
fake_vol_conn_info,
mock.sentinel.disk_address)
mock_get_mounted_disk_from_lun.assert_called_with(
'iqn.2010-10.org.openstack:volume-' + FAKE_VOLUME_ID,
1, True)
mock_get_vm_scsi_controller.assert_called_with(
mock.sentinel.instance_name)
mock_set_disk_host_resource.assert_called_once_with(
mock.sentinel.instance_name, mock.sentinel.controller_path,
mock.sentinel.disk_address, mock.sentinel.mounted_path)
@mock.patch('time.sleep')
def test_get_mounted_disk_from_lun_failure(self, fake_sleep):
self.flags(mounted_disk_query_retry_count=1, group='hyperv')
with mock.patch.object(self._volume_driver._volutils,
'get_device_number_for_target') as m_device_num:
m_device_num.side_effect = [None, -1]
self.assertRaises(exception.NotFound,
self._volume_driver._get_mounted_disk_from_lun,
mock.sentinel.target_iqn,
mock.sentinel.target_lun)
@mock.patch.object(volumeops.ISCSIVolumeDriver, 'logout_storage_target')
def test_disconnect_volumes(self, mock_logout_storage_target):
block_device_info = db_fakes.get_fake_block_device_info(
FAKE_TARGET_PORTAL, FAKE_VOLUME_ID)
block_device_mapping = block_device_info['block_device_mapping']
fake_target_iqn = 'iqn.2010-10.org.openstack:volume-' + FAKE_VOLUME_ID
self._volume_driver.disconnect_volumes(block_device_mapping)
mock_logout_storage_target.assert_called_once_with(fake_target_iqn, 1)
class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase):
"""Unit tests for the Hyper-V SMBFSVolumeDriver class."""
_FAKE_SHARE = '//1.2.3.4/fake_share'
_FAKE_SHARE_NORMALIZED = _FAKE_SHARE.replace('/', '\\')
_FAKE_DISK_NAME = 'fake_volume_name.vhdx'
_FAKE_USERNAME = 'fake_username'
_FAKE_PASSWORD = 'fake_password'
_FAKE_SMB_OPTIONS = '-o username=%s,password=%s' % (_FAKE_USERNAME,
_FAKE_PASSWORD)
_FAKE_CONNECTION_INFO = {'data': {'export': _FAKE_SHARE,
'name': _FAKE_DISK_NAME,
'options': _FAKE_SMB_OPTIONS}}
def setUp(self):
super(SMBFSVolumeDriverTestCase, self).setUp()
self._volume_driver = volumeops.SMBFSVolumeDriver()
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_parse_credentials')
@mock.patch.object(volumeops.SMBFSVolumeDriver, 'ensure_share_mounted')
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path')
@mock.patch.object(vmutils.VMUtils, 'get_vm_scsi_controller')
@mock.patch.object(vmutils.VMUtils, 'get_free_controller_slot')
@mock.patch.object(vmutils.VMUtils, 'attach_drive')
def _test_attach_volume(self, mock_attach_drive,
mock_get_free_controller_slot,
mock_get_vm_scsi_controller,
mock_get_disk_path,
mock_ensure_share_mounted,
mock_parse_credentials,
image_exists=True):
mock_parse_credentials.return_value = (
mock.sentinel.username, self._FAKE_PASSWORD)
mock_get_vm_scsi_controller.return_value = (
mock.sentinel.controller_path)
mock_get_free_controller_slot.return_value = (
mock.sentinel.controller_slot)
mock_get_disk_path.return_value = (
mock.sentinel.disk_path)
if image_exists:
self._volume_driver.attach_volume(
self._FAKE_CONNECTION_INFO,
mock.sentinel.instance_name)
mock_ensure_share_mounted.assert_called_with(
self._FAKE_CONNECTION_INFO)
mock_get_disk_path.assert_called_with(
self._FAKE_CONNECTION_INFO)
mock_get_vm_scsi_controller.assert_called_with(
mock.sentinel.instance_name)
mock_get_free_controller_slot.assert_called_with(
mock.sentinel.controller_path)
mock_attach_drive.assert_called_with(
mock.sentinel.instance_name, mock.sentinel.disk_path,
mock.sentinel.controller_path,
mock.sentinel.controller_slot)
else:
mock_attach_drive.side_effect = (
vmutils.HyperVException())
self.assertRaises(vmutils.HyperVException,
self._volume_driver.attach_volume,
self._FAKE_CONNECTION_INFO,
mock.sentinel.instance_name)
def test_attach_volume(self):
self._test_attach_volume()
def test_attach_non_existing_image(self):
self._test_attach_volume(image_exists=False)
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path')
@mock.patch.object(vmutils.VMUtils, 'detach_vm_disk')
@mock.patch.object(pathutils.PathUtils, 'unmount_smb_share')
def test_detach_volume(self, mock_unmount_smb_share, mock_detach_vm_disk,
mock_get_disk_path):
mock_get_disk_path.return_value = (
mock.sentinel.disk_path)
self._volume_driver.detach_volume(self._FAKE_CONNECTION_INFO,
mock.sentinel.instance_name)
mock_detach_vm_disk.assert_called_once_with(
mock.sentinel.instance_name, mock.sentinel.disk_path,
is_physical=False)
def test_parse_credentials(self):
username, password = self._volume_driver._parse_credentials(
self._FAKE_SMB_OPTIONS)
self.assertEqual(self._FAKE_USERNAME, username)
self.assertEqual(self._FAKE_PASSWORD, password)
def test_get_disk_path(self):
expected = os.path.join(self._FAKE_SHARE_NORMALIZED,
self._FAKE_DISK_NAME)
disk_path = self._volume_driver._get_disk_path(
self._FAKE_CONNECTION_INFO)
self.assertEqual(expected, disk_path)
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_parse_credentials')
@mock.patch.object(pathutils.PathUtils, 'check_smb_mapping')
@mock.patch.object(pathutils.PathUtils, 'mount_smb_share')
def _test_ensure_mounted(self, mock_mount_smb_share,
mock_check_smb_mapping, mock_parse_credentials,
is_mounted=False):
mock_check_smb_mapping.return_value = is_mounted
mock_parse_credentials.return_value = (
self._FAKE_USERNAME, self._FAKE_PASSWORD)
self._volume_driver.ensure_share_mounted(
self._FAKE_CONNECTION_INFO)
if is_mounted:
self.assertFalse(
mock_mount_smb_share.called)
else:
mock_mount_smb_share.assert_called_once_with(
self._FAKE_SHARE_NORMALIZED,
username=self._FAKE_USERNAME,
password=self._FAKE_PASSWORD)
def test_ensure_mounted_new_share(self):
self._test_ensure_mounted()
def test_ensure_already_mounted(self):
self._test_ensure_mounted(is_mounted=True)
@mock.patch.object(pathutils.PathUtils, 'unmount_smb_share')
def test_disconnect_volumes(self, mock_unmount_smb_share):
block_device_mapping = [
{'connection_info': self._FAKE_CONNECTION_INFO}]
self._volume_driver.disconnect_volumes(block_device_mapping)
mock_unmount_smb_share.assert_called_once_with(
self._FAKE_SHARE_NORMALIZED)
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path')
@mock.patch.object(vmutils.VMUtils, 'set_disk_qos_specs')
def test_set_disk_qos_specs(self, mock_set_qos_specs,
mock_get_disk_path):
self._volume_driver.set_disk_qos_specs(mock.sentinel.connection_info,
mock.sentinel.instance_name,
mock.sentinel.min_iops,
mock.sentinel.max_iops)
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.sentinel.instance_name,
mock_disk_path,
mock.sentinel.min_iops,
mock.sentinel.max_iops)