resync: HyperV: use os-brick for volume related operations
Co-Authored-By: Lucian Petrut <lpetrut@cloudbasesolutions.com> (cherry-picked from commit 758a32f7cef6c675b35c04dd8d276c918be188dd) Change-Id: Ie06bfc056ef1ecf5950d780d3e02dab757a73de1
This commit is contained in:
parent
2e2795f508
commit
03e125a22e
|
@ -97,6 +97,10 @@ FLAVOR_ESPEC_REMOTEFX_VRAM = 'os:vram'
|
|||
|
||||
IOPS_BASE_SIZE = 8 * units.Ki
|
||||
|
||||
STORAGE_PROTOCOL_ISCSI = 'iscsi'
|
||||
STORAGE_PROTOCOL_FC = 'fibre_channel'
|
||||
STORAGE_PROTOCOL_SMBFS = 'smbfs'
|
||||
|
||||
MAX_CONSOLE_LOG_FILE_SIZE = units.Mi // 2
|
||||
|
||||
IMAGE_PROP_SECURE_BOOT = "os_secure_boot"
|
||||
|
|
|
@ -17,27 +17,19 @@
|
|||
"""
|
||||
Management class for Storage-related functions (attach, detach, etc).
|
||||
"""
|
||||
import abc
|
||||
import collections
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import nova.conf
|
||||
from nova import exception
|
||||
from nova import utils
|
||||
from nova.virt import driver
|
||||
from os_win import exceptions as os_win_exc
|
||||
from os_brick.initiator import connector
|
||||
from os_win import utilsfactory
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import units
|
||||
import six
|
||||
from oslo_utils import strutils
|
||||
|
||||
from hyperv.i18n import _, _LI, _LE, _LW
|
||||
from hyperv.nova import constants
|
||||
from hyperv.nova import pathutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -50,29 +42,14 @@ class VolumeOps(object):
|
|||
|
||||
_SUPPORTED_QOS_SPECS = ['total_bytes_sec', 'min_bytes_sec',
|
||||
'total_iops_sec', 'min_iops_sec']
|
||||
_IOPS_BASE_SIZE = 8 * units.Ki
|
||||
|
||||
def __init__(self):
|
||||
self._default_root_device = 'vda'
|
||||
|
||||
self._hostutils = utilsfactory.get_hostutils()
|
||||
self._vmutils = utilsfactory.get_vmutils()
|
||||
|
||||
self._verify_setup()
|
||||
self.volume_drivers = {'smbfs': SMBFSVolumeDriver(),
|
||||
'iscsi': ISCSIVolumeDriver(),
|
||||
'fibre_channel': FCVolumeDriver()}
|
||||
|
||||
def _verify_setup(self):
|
||||
if CONF.hyperv.use_multipath_io:
|
||||
mpio_enabled = self._hostutils.check_server_feature(
|
||||
self._hostutils.FEATURE_MPIO)
|
||||
if not mpio_enabled:
|
||||
err_msg = _LE(
|
||||
"Using multipath connections for iSCSI and FC disks "
|
||||
"requires the Multipath IO Windows feature to be "
|
||||
"enabled. MPIO must be configured to claim such devices.")
|
||||
raise exception.ServiceUnavailable(err_msg)
|
||||
self._default_root_device = 'vda'
|
||||
self.volume_drivers = {
|
||||
constants.STORAGE_PROTOCOL_SMBFS: SMBFSVolumeDriver(),
|
||||
constants.STORAGE_PROTOCOL_ISCSI: ISCSIVolumeDriver(),
|
||||
constants.STORAGE_PROTOCOL_FC: FCVolumeDriver()}
|
||||
|
||||
def _get_volume_driver(self, connection_info):
|
||||
driver_type = connection_info.get('driver_volume_type')
|
||||
|
@ -84,41 +61,73 @@ class VolumeOps(object):
|
|||
for vol in volumes:
|
||||
self.attach_volume(vol['connection_info'], instance_name)
|
||||
|
||||
def attach_volume(self, connection_info, instance_name,
|
||||
disk_bus=constants.CTRL_TYPE_SCSI):
|
||||
volume_driver = self._get_volume_driver(
|
||||
connection_info=connection_info)
|
||||
|
||||
volume_connected = False
|
||||
try:
|
||||
volume_driver.connect_volume(connection_info)
|
||||
volume_connected = True
|
||||
|
||||
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)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Unable to attach volume to instance %s'),
|
||||
instance_name)
|
||||
# Even if the attach failed, some cleanup may be needed. If
|
||||
# the volume could not be connected, it surely is not attached.
|
||||
if volume_connected:
|
||||
volume_driver.detach_volume(connection_info, instance_name)
|
||||
volume_driver.disconnect_volume(connection_info)
|
||||
|
||||
def disconnect_volumes(self, block_device_info):
|
||||
mapping = driver.block_device_info_get_mapping(block_device_info)
|
||||
for volume in mapping:
|
||||
connection_info = volume['connection_info']
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
volume_driver.disconnect_volume(connection_info)
|
||||
for vol in mapping:
|
||||
self.disconnect_volume(vol['connection_info'])
|
||||
|
||||
def attach_volume(self, connection_info, instance_name,
|
||||
disk_bus=constants.CTRL_TYPE_SCSI):
|
||||
tries_left = CONF.hyperv.volume_attach_retry_count + 1
|
||||
|
||||
while tries_left:
|
||||
try:
|
||||
self._attach_volume(connection_info,
|
||||
instance_name,
|
||||
disk_bus)
|
||||
break
|
||||
except Exception as ex:
|
||||
tries_left -= 1
|
||||
if not tries_left:
|
||||
LOG.exception(
|
||||
_LE("Failed to attach volume %(connection_info)s "
|
||||
"to instance %(instance_name)s. "),
|
||||
{'connection_info': strutils.mask_dict_password(
|
||||
connection_info),
|
||||
'instance_name': instance_name})
|
||||
|
||||
self.disconnect_volume(connection_info)
|
||||
raise exception.VolumeAttachFailed(
|
||||
volume_id=connection_info['serial'],
|
||||
reason=ex)
|
||||
else:
|
||||
LOG.warning(
|
||||
_LW("Failed to attach volume %(connection_info)s "
|
||||
"to instance %(instance_name)s. "
|
||||
"Tries left: %(tries_left)s."),
|
||||
{'connection_info': strutils.mask_dict_password(
|
||||
connection_info),
|
||||
'instance_name': instance_name,
|
||||
'tries_left': tries_left})
|
||||
|
||||
time.sleep(CONF.hyperv.volume_attach_retry_interval)
|
||||
|
||||
def _attach_volume(self, connection_info, instance_name,
|
||||
disk_bus=constants.CTRL_TYPE_SCSI):
|
||||
LOG.debug(
|
||||
"Attaching volume: %(connection_info)s to %(instance_name)s",
|
||||
{'connection_info': strutils.mask_dict_password(connection_info),
|
||||
'instance_name': instance_name})
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
volume_driver.attach_volume(connection_info,
|
||||
instance_name,
|
||||
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 disconnect_volume(self, connection_info):
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
volume_driver.disconnect_volume(connection_info)
|
||||
|
||||
def detach_volume(self, connection_info, instance_name):
|
||||
LOG.debug("Detaching volume: %(connection_info)s "
|
||||
"from %(instance_name)s",
|
||||
{'connection_info': strutils.mask_dict_password(
|
||||
connection_info),
|
||||
'instance_name': instance_name})
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
volume_driver.detach_volume(connection_info, instance_name)
|
||||
volume_driver.disconnect_volume(connection_info)
|
||||
|
@ -143,17 +152,15 @@ class VolumeOps(object):
|
|||
actual_disk_path)
|
||||
|
||||
def get_volume_connector(self):
|
||||
connector = {
|
||||
'host': CONF.host,
|
||||
'ip': CONF.my_block_storage_ip,
|
||||
'multipath': CONF.hyperv.use_multipath_io,
|
||||
'os_type': sys.platform,
|
||||
'platform': platform.machine(),
|
||||
}
|
||||
for volume_driver_type, volume_driver in self.volume_drivers.items():
|
||||
connector_updates = volume_driver.get_volume_connector_props()
|
||||
connector.update(connector_updates)
|
||||
return connector
|
||||
# NOTE(lpetrut): the Windows os-brick connectors
|
||||
# do not use a root helper.
|
||||
conn = connector.get_connector_properties(
|
||||
root_helper=None,
|
||||
my_ip=CONF.my_block_storage_ip,
|
||||
multipath=CONF.hyperv.use_multipath_io,
|
||||
enforce_multipath=True,
|
||||
host=CONF.host)
|
||||
return conn
|
||||
|
||||
def connect_volumes(self, block_device_info):
|
||||
mapping = driver.block_device_info_get_mapping(block_device_info)
|
||||
|
@ -162,6 +169,25 @@ class VolumeOps(object):
|
|||
volume_driver = self._get_volume_driver(connection_info)
|
||||
volume_driver.connect_volume(connection_info)
|
||||
|
||||
def get_disk_path_mapping(self, block_device_info, block_dev_only=False):
|
||||
block_mapping = driver.block_device_info_get_mapping(block_device_info)
|
||||
disk_path_mapping = {}
|
||||
for vol in block_mapping:
|
||||
connection_info = vol['connection_info']
|
||||
disk_serial = connection_info['serial']
|
||||
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
if block_dev_only and not volume_driver._is_block_dev:
|
||||
continue
|
||||
|
||||
disk_path = volume_driver.get_disk_resource_path(connection_info)
|
||||
disk_path_mapping[disk_serial] = disk_path
|
||||
return disk_path_mapping
|
||||
|
||||
def get_disk_resource_path(self, connection_info):
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
return volume_driver.get_disk_resource_path(connection_info)
|
||||
|
||||
@staticmethod
|
||||
def bytes_per_sec_to_iops(no_bytes):
|
||||
# Hyper-v uses normalized IOPS (8 KB increments)
|
||||
|
@ -182,57 +208,65 @@ class VolumeOps(object):
|
|||
'supported_qos_specs': supported_qos_specs})
|
||||
LOG.warning(msg)
|
||||
|
||||
def get_disk_path_mapping(self, block_device_info, block_dev_only=False):
|
||||
block_mapping = driver.block_device_info_get_mapping(block_device_info)
|
||||
disk_path_mapping = {}
|
||||
for vol in block_mapping:
|
||||
connection_info = vol['connection_info']
|
||||
disk_serial = connection_info['serial']
|
||||
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
if block_dev_only and not volume_driver._is_block_dev:
|
||||
continue
|
||||
|
||||
disk_path = volume_driver.get_disk_resource_path(connection_info)
|
||||
disk_path_mapping[disk_serial] = disk_path
|
||||
return disk_path_mapping
|
||||
|
||||
def get_disk_resource_path(self, connection_info):
|
||||
volume_driver = self._get_volume_driver(connection_info)
|
||||
return volume_driver.get_disk_resource_path(connection_info)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseVolumeDriver(object):
|
||||
|
||||
_is_block_dev = True
|
||||
_protocol = None
|
||||
_extra_connector_args = {}
|
||||
|
||||
def __init__(self):
|
||||
self._vmutils = utilsfactory.get_vmutils()
|
||||
self._conn = None
|
||||
self._diskutils = utilsfactory.get_diskutils()
|
||||
self._vmutils = utilsfactory.get_vmutils()
|
||||
|
||||
@property
|
||||
def _connector(self):
|
||||
if not self._conn:
|
||||
scan_attempts = CONF.hyperv.mounted_disk_query_retry_count
|
||||
scan_interval = CONF.hyperv.mounted_disk_query_retry_interval
|
||||
|
||||
self._conn = connector.InitiatorConnector.factory(
|
||||
protocol=self._protocol,
|
||||
root_helper=None,
|
||||
use_multipath=CONF.hyperv.use_multipath_io,
|
||||
device_scan_attempts=scan_attempts,
|
||||
device_scan_interval=scan_interval,
|
||||
**self._extra_connector_args)
|
||||
return self._conn
|
||||
|
||||
def connect_volume(self, connection_info):
|
||||
pass
|
||||
return self._connector.connect_volume(connection_info['data'])
|
||||
|
||||
def disconnect_volume(self, connection_info):
|
||||
pass
|
||||
self._connector.disconnect_volume(connection_info['data'])
|
||||
|
||||
def get_volume_connector_props(self):
|
||||
return {}
|
||||
def get_disk_resource_path(self, connection_info):
|
||||
disk_paths = self._connector.get_volume_paths(connection_info['data'])
|
||||
if not disk_paths:
|
||||
vol_id = connection_info['serial']
|
||||
err_msg = _("Could not find disk path. Volume id: %s")
|
||||
raise exception.DiskNotFound(err_msg % vol_id)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_disk_resource_path(connection_info):
|
||||
pass
|
||||
return self._get_disk_res_path(disk_paths[0])
|
||||
|
||||
def _get_disk_res_path(self, disk_path):
|
||||
if self._is_block_dev:
|
||||
# We need the Msvm_DiskDrive resource path as this
|
||||
# will be used when the disk is attached to an instance.
|
||||
disk_number = self._diskutils.get_device_number_from_device_name(
|
||||
disk_path)
|
||||
disk_res_path = self._vmutils.get_mounted_disk_by_drive_number(
|
||||
disk_number)
|
||||
else:
|
||||
disk_res_path = disk_path
|
||||
return disk_res_path
|
||||
|
||||
def attach_volume(self, connection_info, instance_name,
|
||||
disk_bus=constants.CTRL_TYPE_SCSI):
|
||||
"""Attach a volume to the SCSI controller or to the IDE controller if
|
||||
ebs_root is True
|
||||
"""
|
||||
serial = connection_info['serial']
|
||||
# Getting the mounted disk
|
||||
mounted_disk_path = self.get_disk_resource_path(connection_info)
|
||||
dev_info = self.connect_volume(connection_info)
|
||||
|
||||
serial = connection_info['serial']
|
||||
disk_path = self._get_disk_res_path(dev_info['path'])
|
||||
ctrller_path, slot = self._get_disk_ctrl_and_slot(instance_name,
|
||||
disk_bus)
|
||||
if self._is_block_dev:
|
||||
|
@ -242,22 +276,22 @@ class BaseVolumeDriver(object):
|
|||
self._vmutils.attach_volume_to_controller(instance_name,
|
||||
ctrller_path,
|
||||
slot,
|
||||
mounted_disk_path,
|
||||
disk_path,
|
||||
serial=serial)
|
||||
else:
|
||||
self._vmutils.attach_drive(instance_name,
|
||||
mounted_disk_path,
|
||||
disk_path,
|
||||
ctrller_path,
|
||||
slot)
|
||||
|
||||
def detach_volume(self, connection_info, instance_name):
|
||||
mounted_disk_path = self.get_disk_resource_path(connection_info)
|
||||
disk_path = self.get_disk_resource_path(connection_info)
|
||||
|
||||
LOG.debug("Detaching disk %(disk_path)s "
|
||||
"from instance: %(instance_name)s",
|
||||
dict(disk_path=mounted_disk_path,
|
||||
dict(disk_path=disk_path,
|
||||
instance_name=instance_name))
|
||||
self._vmutils.detach_vm_disk(instance_name, mounted_disk_path,
|
||||
self._vmutils.detach_vm_disk(instance_name, disk_path,
|
||||
is_physical=self._is_block_dev)
|
||||
|
||||
def _get_disk_ctrl_and_slot(self, instance_name, disk_bus):
|
||||
|
@ -267,187 +301,34 @@ class BaseVolumeDriver(object):
|
|||
instance_name, 0)
|
||||
# Attaching to the first slot
|
||||
slot = 0
|
||||
elif disk_bus == constants.CTRL_TYPE_SCSI:
|
||||
else:
|
||||
# Find the SCSI controller for the vm
|
||||
ctrller_path = self._vmutils.get_vm_scsi_controller(
|
||||
instance_name)
|
||||
slot = self._vmutils.get_free_controller_slot(ctrller_path)
|
||||
else:
|
||||
err_msg = _("Unsupported disk bus requested: %s")
|
||||
raise exception.Invalid(err_msg % disk_bus)
|
||||
return ctrller_path, slot
|
||||
|
||||
def set_disk_qos_specs(self, connection_info, min_iops, max_iops):
|
||||
volume_type = connection_info.get('driver_volume_type', '')
|
||||
LOG.warning(_LW("The %s Hyper-V volume driver does not support QoS. "
|
||||
"Ignoring QoS specs."), volume_type)
|
||||
|
||||
def _check_device_paths(self, device_paths):
|
||||
if len(device_paths) > 1:
|
||||
err_msg = _("Multiple disk paths were found: %s. This can "
|
||||
"occur if multipath is used and MPIO is not "
|
||||
"properly configured, thus not claiming the device "
|
||||
"paths. This issue must be addressed urgently as "
|
||||
"it can lead to data corruption.")
|
||||
raise exception.InvalidDevicePath(err_msg % device_paths)
|
||||
elif not device_paths:
|
||||
err_msg = _("Could not find the physical disk "
|
||||
"path for the requested volume.")
|
||||
raise exception.DiskNotFound(err_msg)
|
||||
|
||||
def _get_mounted_disk_path_by_dev_name(self, device_name):
|
||||
device_number = self._diskutils.get_device_number_from_device_name(
|
||||
device_name)
|
||||
mounted_disk_path = self._vmutils.get_mounted_disk_by_drive_number(
|
||||
device_number)
|
||||
return mounted_disk_path
|
||||
def set_disk_qos_specs(self, connection_info, disk_qos_specs):
|
||||
LOG.info(_LI("The %(protocol)s Hyper-V volume driver "
|
||||
"does not support QoS. Ignoring QoS specs."),
|
||||
dict(protocol=self._protocol))
|
||||
|
||||
|
||||
class ISCSIVolumeDriver(BaseVolumeDriver):
|
||||
def __init__(self):
|
||||
super(ISCSIVolumeDriver, self).__init__()
|
||||
self._iscsi_utils = utilsfactory.get_iscsi_initiator_utils()
|
||||
self._initiator_node_name = self._iscsi_utils.get_iscsi_initiator()
|
||||
_is_block_dev = True
|
||||
_protocol = constants.STORAGE_PROTOCOL_ISCSI
|
||||
|
||||
self.validate_initiators()
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._extra_connector_args = dict(
|
||||
initiator_list=CONF.hyperv.iscsi_initiator_list)
|
||||
|
||||
def get_volume_connector_props(self):
|
||||
props = {'initiator': self._initiator_node_name}
|
||||
return props
|
||||
|
||||
def validate_initiators(self):
|
||||
# The MS iSCSI initiator service can manage the software iSCSI
|
||||
# initiator as well as hardware initiators.
|
||||
initiator_list = CONF.hyperv.iscsi_initiator_list
|
||||
valid_initiators = True
|
||||
|
||||
if not initiator_list:
|
||||
LOG.info(_LI("No iSCSI initiator was explicitly requested. "
|
||||
"The Microsoft iSCSI initiator will choose the "
|
||||
"initiator when estabilishing sessions."))
|
||||
else:
|
||||
available_initiators = self._iscsi_utils.get_iscsi_initiators()
|
||||
for initiator in initiator_list:
|
||||
if initiator not in available_initiators:
|
||||
valid_initiators = False
|
||||
msg = _LW("The requested initiator %(req_initiator)s "
|
||||
"is not in the list of available initiators: "
|
||||
"%(avail_initiators)s.")
|
||||
LOG.warning(msg,
|
||||
dict(req_initiator=initiator,
|
||||
avail_initiators=available_initiators))
|
||||
|
||||
return valid_initiators
|
||||
|
||||
def _get_all_targets(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
'target_iqns',
|
||||
'target_luns')]):
|
||||
return zip(connection_properties['target_portals'],
|
||||
connection_properties['target_iqns'],
|
||||
connection_properties['target_luns'])
|
||||
|
||||
return [(connection_properties['target_portal'],
|
||||
connection_properties['target_iqn'],
|
||||
connection_properties.get('target_lun', 0))]
|
||||
|
||||
def _get_all_paths(self, connection_properties):
|
||||
initiator_list = CONF.hyperv.iscsi_initiator_list or [None]
|
||||
all_targets = self._get_all_targets(connection_properties)
|
||||
paths = [(initiator_name, target_portal, target_iqn, target_lun)
|
||||
for target_portal, target_iqn, target_lun in all_targets
|
||||
for initiator_name in initiator_list]
|
||||
return paths
|
||||
|
||||
def connect_volume(self, connection_info):
|
||||
connection_properties = connection_info['data']
|
||||
auth_method = connection_properties.get('auth_method')
|
||||
|
||||
if auth_method and auth_method.upper() != 'CHAP':
|
||||
LOG.error(_LE("Unsupported iSCSI authentication "
|
||||
"method: %(auth_method)s."),
|
||||
dict(auth_method=auth_method))
|
||||
raise exception.UnsupportedBDMVolumeAuthMethod(
|
||||
auth_method=auth_method)
|
||||
|
||||
volume_connected = False
|
||||
for (initiator_name,
|
||||
target_portal,
|
||||
target_iqn,
|
||||
target_lun) in self._get_all_paths(connection_properties):
|
||||
try:
|
||||
msg = _LI("Attempting to estabilish an iSCSI session to "
|
||||
"target %(target_iqn)s on portal %(target_portal)s "
|
||||
"acessing LUN %(target_lun)s using initiator "
|
||||
"%(initiator_name)s.")
|
||||
LOG.info(msg, dict(target_portal=target_portal,
|
||||
target_iqn=target_iqn,
|
||||
target_lun=target_lun,
|
||||
initiator_name=initiator_name))
|
||||
self._iscsi_utils.login_storage_target(
|
||||
target_lun=target_lun,
|
||||
target_iqn=target_iqn,
|
||||
target_portal=target_portal,
|
||||
auth_username=connection_properties.get('auth_username'),
|
||||
auth_password=connection_properties.get('auth_password'),
|
||||
mpio_enabled=CONF.hyperv.use_multipath_io,
|
||||
initiator_name=initiator_name)
|
||||
|
||||
volume_connected = True
|
||||
if not CONF.hyperv.use_multipath_io:
|
||||
break
|
||||
except os_win_exc.OSWinException:
|
||||
LOG.exception(_LE("Could not connect iSCSI target %s."),
|
||||
target_iqn)
|
||||
|
||||
if not volume_connected:
|
||||
raise exception.VolumeAttachFailed(
|
||||
_("Could not connect volume %s.") %
|
||||
connection_properties['volume_id'])
|
||||
|
||||
def disconnect_volume(self, connection_info):
|
||||
# We want to refresh the cached information first.
|
||||
self._diskutils.rescan_disks()
|
||||
|
||||
for (target_portal,
|
||||
target_iqn,
|
||||
target_lun) in self._get_all_targets(connection_info['data']):
|
||||
|
||||
luns = self._iscsi_utils.get_target_luns(target_iqn)
|
||||
# We disconnect the target only if it does not expose other
|
||||
# luns which may be in use.
|
||||
if not luns or luns == [target_lun]:
|
||||
self._iscsi_utils.logout_storage_target(target_iqn)
|
||||
|
||||
def get_disk_resource_path(self, connection_info):
|
||||
device_paths = set()
|
||||
connection_properties = connection_info['data']
|
||||
|
||||
for (target_portal,
|
||||
target_iqn,
|
||||
target_lun) in self._get_all_targets(connection_properties):
|
||||
|
||||
(device_number,
|
||||
device_path) = self._iscsi_utils.get_device_number_and_path(
|
||||
target_iqn, target_lun)
|
||||
if device_path:
|
||||
device_paths.add(device_path)
|
||||
|
||||
self._check_device_paths(device_paths)
|
||||
disk_path = list(device_paths)[0]
|
||||
return self._get_mounted_disk_path_by_dev_name(disk_path)
|
||||
super(ISCSIVolumeDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SMBFSVolumeDriver(BaseVolumeDriver):
|
||||
|
||||
_is_block_dev = False
|
||||
|
||||
def __init__(self):
|
||||
self._pathutils = pathutils.PathUtils()
|
||||
self._smbutils = utilsfactory.get_smbutils()
|
||||
self._username_regex = re.compile(r'user(?:name)?=([^, ]+)')
|
||||
self._password_regex = re.compile(r'pass(?:word)?=([^, ]+)')
|
||||
super(SMBFSVolumeDriver, self).__init__()
|
||||
_protocol = constants.STORAGE_PROTOCOL_SMBFS
|
||||
_extra_connector_args = dict(local_path_for_loopback=True)
|
||||
|
||||
def export_path_synchronized(f):
|
||||
def wrapper(inst, connection_info, *args, **kwargs):
|
||||
|
@ -459,68 +340,19 @@ class SMBFSVolumeDriver(BaseVolumeDriver):
|
|||
return inner()
|
||||
return wrapper
|
||||
|
||||
def get_disk_resource_path(self, connection_info):
|
||||
return self._get_disk_path(connection_info)
|
||||
|
||||
@export_path_synchronized
|
||||
def attach_volume(self, connection_info, instance_name,
|
||||
disk_bus=constants.CTRL_TYPE_SCSI):
|
||||
super(SMBFSVolumeDriver, self).attach_volume(
|
||||
connection_info, instance_name, disk_bus)
|
||||
|
||||
@export_path_synchronized
|
||||
def disconnect_volume(self, connection_info):
|
||||
# We synchronize share unmount and volume attach operations based on
|
||||
# the share path in order to avoid the situation when a SMB share is
|
||||
# unmounted while a volume exported by it is about to be attached to
|
||||
# an instance.
|
||||
export_path = self._get_export_path(connection_info)
|
||||
self._smbutils.unmount_smb_share(export_path)
|
||||
|
||||
def _get_export_path(self, connection_info):
|
||||
return connection_info[
|
||||
'data']['export'].replace('/', '\\').rstrip('\\')
|
||||
return connection_info['data']['export'].replace('/', '\\')
|
||||
|
||||
def _get_disk_path(self, connection_info):
|
||||
share_addr = self._get_export_path(connection_info)
|
||||
disk_dir = share_addr
|
||||
@export_path_synchronized
|
||||
def attach_volume(self, *args, **kwargs):
|
||||
super(SMBFSVolumeDriver, self).attach_volume(*args, **kwargs)
|
||||
|
||||
if self._smbutils.is_local_share(share_addr):
|
||||
share_name = share_addr.lstrip('\\').split('\\')[1]
|
||||
disk_dir = self._smbutils.get_smb_share_path(share_name)
|
||||
if not disk_dir:
|
||||
err_msg = _("Could not find the local share path for %s.")
|
||||
raise exception.DiskNotFound(err_msg % share_addr)
|
||||
|
||||
disk_name = connection_info['data']['name']
|
||||
disk_path = os.path.join(disk_dir, disk_name)
|
||||
return disk_path
|
||||
|
||||
def ensure_share_mounted(self, connection_info):
|
||||
export_path = self._get_export_path(connection_info)
|
||||
|
||||
if self._smbutils.is_local_share(export_path):
|
||||
LOG.info(_LI("Skipping mounting share %s, "
|
||||
"using local path instead."),
|
||||
export_path)
|
||||
elif not self._smbutils.check_smb_mapping(export_path):
|
||||
opts_str = connection_info['data'].get('options') or ''
|
||||
username, password = self._parse_credentials(opts_str)
|
||||
self._smbutils.mount_smb_share(export_path,
|
||||
username=username,
|
||||
password=password)
|
||||
|
||||
def _parse_credentials(self, opts_str):
|
||||
match = self._username_regex.findall(opts_str)
|
||||
username = match[0] if match and match[0] != 'guest' else None
|
||||
|
||||
match = self._password_regex.findall(opts_str)
|
||||
password = match[0] if match else None
|
||||
|
||||
return username, password
|
||||
|
||||
def connect_volume(self, connection_info):
|
||||
self.ensure_share_mounted(connection_info)
|
||||
@export_path_synchronized
|
||||
def disconnect_volume(self, *args, **kwargs):
|
||||
# We synchronize those operations based on the share path in order to
|
||||
# avoid the situation when a SMB share is unmounted while a volume
|
||||
# exported by it is about to be attached to an instance.
|
||||
super(SMBFSVolumeDriver, self).disconnect_volume(*args, **kwargs)
|
||||
|
||||
def set_disk_qos_specs(self, connection_info, qos_specs):
|
||||
supported_qos_specs = ['total_iops_sec', 'total_bytes_sec']
|
||||
|
@ -532,87 +364,10 @@ class SMBFSVolumeDriver(BaseVolumeDriver):
|
|||
total_bytes_sec))
|
||||
|
||||
if total_iops_sec:
|
||||
disk_path = self._get_disk_path(connection_info)
|
||||
disk_path = self.get_disk_resource_path(connection_info)
|
||||
self._vmutils.set_disk_qos_specs(disk_path, total_iops_sec)
|
||||
|
||||
|
||||
class FCVolumeDriver(BaseVolumeDriver):
|
||||
_MAX_RESCAN_COUNT = 10
|
||||
|
||||
def __init__(self):
|
||||
self._fc_utils = utilsfactory.get_fc_utils()
|
||||
super(FCVolumeDriver, self).__init__()
|
||||
|
||||
def get_volume_connector_props(self):
|
||||
props = {}
|
||||
|
||||
self._fc_utils.refresh_hba_configuration()
|
||||
fc_hba_ports = self._fc_utils.get_fc_hba_ports()
|
||||
|
||||
if fc_hba_ports:
|
||||
wwnns = []
|
||||
wwpns = []
|
||||
for port in fc_hba_ports:
|
||||
wwnns.append(port['node_name'])
|
||||
wwpns.append(port['port_name'])
|
||||
props['wwpns'] = wwpns
|
||||
props['wwnns'] = list(set(wwnns))
|
||||
return props
|
||||
|
||||
def connect_volume(self, connection_info):
|
||||
self.get_disk_resource_path(connection_info)
|
||||
|
||||
def get_disk_resource_path(self, connection_info):
|
||||
for attempt in range(self._MAX_RESCAN_COUNT):
|
||||
disk_paths = set()
|
||||
|
||||
self._diskutils.rescan_disks()
|
||||
volume_mappings = self._get_fc_volume_mappings(connection_info)
|
||||
|
||||
LOG.debug("Retrieved volume mappings %(vol_mappings)s "
|
||||
"for volume %(conn_info)s",
|
||||
dict(vol_mappings=volume_mappings,
|
||||
conn_info=connection_info))
|
||||
|
||||
# Because of MPIO, we may not be able to get the device name
|
||||
# from a specific mapping if the disk was accessed through
|
||||
# an other HBA at that moment. In that case, the device name
|
||||
# will show up as an empty string.
|
||||
for mapping in volume_mappings:
|
||||
device_name = mapping['device_name']
|
||||
if device_name:
|
||||
disk_paths.add(device_name)
|
||||
|
||||
if disk_paths:
|
||||
self._check_device_paths(disk_paths)
|
||||
disk_path = list(disk_paths)[0]
|
||||
return self._get_mounted_disk_path_by_dev_name(
|
||||
disk_path)
|
||||
|
||||
err_msg = _("Could not find the physical disk "
|
||||
"path for the requested volume.")
|
||||
raise exception.DiskNotFound(err_msg)
|
||||
|
||||
def _get_fc_volume_mappings(self, connection_info):
|
||||
# Note(lpetrut): All the WWNs returned by os-win are upper case.
|
||||
target_wwpns = [wwpn.upper()
|
||||
for wwpn in connection_info['data']['target_wwn']]
|
||||
target_lun = connection_info['data']['target_lun']
|
||||
|
||||
volume_mappings = []
|
||||
hba_mapping = self._get_fc_hba_mapping()
|
||||
for node_name, hba_ports in hba_mapping.items():
|
||||
target_mappings = self._fc_utils.get_fc_target_mappings(node_name)
|
||||
for mapping in target_mappings:
|
||||
if (mapping['port_name'] in target_wwpns
|
||||
and mapping['lun'] == target_lun):
|
||||
volume_mappings.append(mapping)
|
||||
|
||||
return volume_mappings
|
||||
|
||||
def _get_fc_hba_mapping(self):
|
||||
mapping = collections.defaultdict(list)
|
||||
fc_hba_ports = self._fc_utils.get_fc_hba_ports()
|
||||
for port in fc_hba_ports:
|
||||
mapping[port['node_name']].append(port['port_name'])
|
||||
return mapping
|
||||
_is_block_dev = True
|
||||
_protocol = constants.STORAGE_PROTOCOL_FC
|
||||
|
|
|
@ -138,8 +138,7 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
|||
|
||||
@mock.patch('hyperv.nova.volumeops.VolumeOps.get_disk_path_mapping')
|
||||
@mock.patch('hyperv.nova.imagecache.ImageCache.get_cached_image')
|
||||
@mock.patch('hyperv.nova.volumeops.VolumeOps'
|
||||
'.connect_volumes')
|
||||
@mock.patch('hyperv.nova.volumeops.VolumeOps.connect_volumes')
|
||||
def _test_pre_live_migration(self, mock_connect_volumes,
|
||||
mock_get_cached_image,
|
||||
mock_get_disk_path_mapping,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,7 @@
|
|||
pbr>=1.8 # Apache-2.0
|
||||
Babel>=2.3.4 # BSD
|
||||
|
||||
os-brick>=1.8.0 # Apache-2.0
|
||||
os-win>=1.1.0 # Apache-2.0
|
||||
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
|
||||
oslo.log>=3.11.0 # Apache-2.0
|
||||
|
|
Loading…
Reference in New Issue