Splitting Out Connectors from connector.py

This is a larger refactor of the connector.py file. The goal is to
simplfy the file by moving the vendor connector classes to their own
files, and keep only the InitiatorConnector in the connector.py file.
The vendor specific connector tests are also split out into their own
files.

Change-Id: I020e75ca8cd8bec2ad1b38f3ade5cc1f63a4fee5
Implements: bp connector-refactor
This commit is contained in:
Kendall Nelson 2016-07-11 11:39:25 -05:00
parent cc47d81feb
commit c5e3d8affb
39 changed files with 6645 additions and 5942 deletions

View File

@ -19,3 +19,39 @@ The initator module contains the capabilities for discovering the initiator
information as well as discovering and removing volumes from a host.
"""
import re
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$")
MULTIPATH_DEV_CHECK_REGEX = re.compile("\s+dm-\d+\s+")
MULTIPATH_PATH_CHECK_REGEX = re.compile("\s+\d+:\d+:\d+:\d+\s+")
PLATFORM_ALL = 'ALL'
PLATFORM_x86 = 'X86'
PLATFORM_S390 = 'S390'
OS_TYPE_ALL = 'ALL'
OS_TYPE_LINUX = 'LINUX'
OS_TYPE_WINDOWS = 'WIN'
S390X = "s390x"
S390 = "s390"
ISCSI = "ISCSI"
ISER = "ISER"
FIBRE_CHANNEL = "FIBRE_CHANNEL"
AOE = "AOE"
DRBD = "DRBD"
NFS = "NFS"
GLUSTERFS = "GLUSTERFS"
LOCAL = "LOCAL"
HUAWEISDSHYPERVISOR = "HUAWEISDSHYPERVISOR"
HGST = "HGST"
RBD = "RBD"
SCALEIO = "SCALEIO"
SCALITY = "SCALITY"
QUOBYTE = "QUOBYTE"
DISCO = "DISCO"
VZSTORAGE = "VZSTORAGE"
SHEEPDOG = "SHEEPDOG"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,177 @@
# 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 os
from oslo_concurrency import lockutils
from oslo_log import log as logging
from oslo_service import loopingcall
from os_brick import exception
from os_brick import initiator
from os_brick.i18n import _LW
from os_brick.initiator.connectors import base
from os_brick import utils
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
LOG = logging.getLogger(__name__)
class AoEConnector(base.BaseLinuxConnector):
"""Connector class to attach/detach AoE volumes."""
def __init__(self, root_helper, driver=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(AoEConnector, self).__init__(
root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The AoE connector properties."""
return {}
def get_search_path(self):
return '/dev/etherd'
def get_volume_paths(self, connection_properties):
aoe_device, aoe_path = self._get_aoe_info(connection_properties)
volume_paths = []
if os.path.exists(aoe_path):
volume_paths.append(aoe_path)
return volume_paths
def _get_aoe_info(self, connection_properties):
shelf = connection_properties['target_shelf']
lun = connection_properties['target_lun']
aoe_device = 'e%(shelf)s.%(lun)s' % {'shelf': shelf,
'lun': lun}
path = self.get_search_path()
aoe_path = '%(path)s/%(device)s' % {'path': path,
'device': aoe_device}
return aoe_device, aoe_path
@utils.trace
@lockutils.synchronized('aoe_control', 'aoe-')
def connect_volume(self, connection_properties):
"""Discover and attach the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
connection_properties for AoE must include:
target_shelf - shelf id of volume
target_lun - lun id of volume
"""
aoe_device, aoe_path = self._get_aoe_info(connection_properties)
device_info = {
'type': 'block',
'device': aoe_device,
'path': aoe_path,
}
if os.path.exists(aoe_path):
self._aoe_revalidate(aoe_device)
else:
self._aoe_discover()
waiting_status = {'tries': 0}
# NOTE(jbr_): Device path is not always present immediately
def _wait_for_discovery(aoe_path):
if os.path.exists(aoe_path):
raise loopingcall.LoopingCallDone
if waiting_status['tries'] >= self.device_scan_attempts:
raise exception.VolumeDeviceNotFound(device=aoe_path)
LOG.warning(_LW("AoE volume not yet found at: %(path)s. "
"Try number: %(tries)s"),
{'path': aoe_device,
'tries': waiting_status['tries']})
self._aoe_discover()
waiting_status['tries'] += 1
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_discovery,
aoe_path)
timer.start(interval=2).wait()
if waiting_status['tries']:
LOG.debug("Found AoE device %(path)s "
"(after %(tries)s rediscover)",
{'path': aoe_path,
'tries': waiting_status['tries']})
return device_info
@utils.trace
@lockutils.synchronized('aoe_control', 'aoe-')
def disconnect_volume(self, connection_properties, device_info):
"""Detach and flush the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
connection_properties for AoE must include:
target_shelf - shelf id of volume
target_lun - lun id of volume
"""
aoe_device, aoe_path = self._get_aoe_info(connection_properties)
if os.path.exists(aoe_path):
self._aoe_flush(aoe_device)
def _aoe_discover(self):
(out, err) = self._execute('aoe-discover',
run_as_root=True,
root_helper=self._root_helper,
check_exit_code=0)
LOG.debug('aoe-discover: stdout=%(out)s stderr%(err)s',
{'out': out, 'err': err})
def _aoe_revalidate(self, aoe_device):
(out, err) = self._execute('aoe-revalidate',
aoe_device,
run_as_root=True,
root_helper=self._root_helper,
check_exit_code=0)
LOG.debug('aoe-revalidate %(dev)s: stdout=%(out)s stderr%(err)s',
{'dev': aoe_device, 'out': out, 'err': err})
def _aoe_flush(self, aoe_device):
(out, err) = self._execute('aoe-flush',
aoe_device,
run_as_root=True,
root_helper=self._root_helper,
check_exit_code=0)
LOG.debug('aoe-flush %(dev)s: stdout=%(out)s stderr%(err)s',
{'dev': aoe_device, 'out': out, 'err': err})
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,129 @@
# 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 glob
import os
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from os_brick import exception
from os_brick import initiator
from os_brick.i18n import _LE, _LW
from os_brick.initiator import host_driver
from os_brick.initiator import initiator_connector
from os_brick.initiator import linuxscsi
LOG = logging.getLogger(__name__)
class BaseLinuxConnector(initiator_connector.InitiatorConnector):
os_type = initiator.OS_TYPE_LINUX
def __init__(self, root_helper, driver=None, execute=None,
*args, **kwargs):
self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute)
if not driver:
driver = host_driver.HostDriver()
self.set_driver(driver)
super(BaseLinuxConnector, self).__init__(root_helper, execute=execute,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The generic connector properties."""
multipath = kwargs['multipath']
enforce_multipath = kwargs['enforce_multipath']
props = {}
props['multipath'] = (multipath and
linuxscsi.LinuxSCSI.is_multipath_running(
enforce_multipath, root_helper,
execute=kwargs.get('execute')))
return props
def check_valid_device(self, path, run_as_root=True):
cmd = ('dd', 'if=%(path)s' % {"path": path},
'of=/dev/null', 'count=1')
out, info = None, None
try:
out, info = self._execute(*cmd, run_as_root=run_as_root,
root_helper=self._root_helper)
except putils.ProcessExecutionError as e:
LOG.error(_LE("Failed to access the device on the path "
"%(path)s: %(error)s."),
{"path": path, "error": e.stderr})
return False
# If the info is none, the path does not exist.
if info is None:
return False
return True
def get_all_available_volumes(self, connection_properties=None):
volumes = []
path = self.get_search_path()
if path:
# now find all entries in the search path
if os.path.isdir(path):
path_items = [path, '/*']
file_filter = ''.join(path_items)
volumes = glob.glob(file_filter)
return volumes
def _discover_mpath_device(self, device_wwn, connection_properties,
device_name):
"""This method discovers a multipath device.
Discover a multipath device based on a defined connection_property
and a device_wwn and return the multipath_id and path of the multipath
enabled device if there is one.
"""
path = self._linuxscsi.find_multipath_device_path(device_wwn)
device_path = None
multipath_id = None
if path is None:
# find_multipath_device only accept realpath not symbolic path
device_realpath = os.path.realpath(device_name)
mpath_info = self._linuxscsi.find_multipath_device(
device_realpath)
if mpath_info:
device_path = mpath_info['device']
multipath_id = device_wwn
else:
# we didn't find a multipath device.
# so we assume the kernel only sees 1 device
device_path = device_name
LOG.debug("Unable to find multipath device name for "
"volume. Using path %(device)s for volume.",
{'device': device_path})
else:
device_path = path
multipath_id = device_wwn
if connection_properties.get('access_mode', '') != 'ro':
try:
# Sometimes the multipath devices will show up as read only
# initially and need additional time/rescans to get to RW.
self._linuxscsi.wait_for_rw(device_wwn, device_path)
except exception.BlockDeviceReadOnly:
LOG.warning(_LW('Block device %s is still read-only. '
'Continuing anyway.'), device_path)
return device_path, multipath_id

View File

@ -0,0 +1,42 @@
# 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 copy
from os_brick.initiator import initiator_connector
class BaseISCSIConnector(initiator_connector.InitiatorConnector):
def _iterate_all_targets(self, connection_properties):
for portal, iqn, lun in self._get_all_targets(connection_properties):
props = copy.deepcopy(connection_properties)
props['target_portal'] = portal
props['target_iqn'] = iqn
props['target_lun'] = lun
for key in ('target_portals', 'target_iqns', 'target_luns'):
props.pop(key, None)
yield props
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))]

View File

@ -0,0 +1,207 @@
# 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 glob
import os
import socket
import struct
from oslo_concurrency import lockutils
from oslo_log import log as logging
import six
from os_brick.i18n import _, _LI, _LE
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick import utils
LOG = logging.getLogger(__name__)
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
synchronized = lockutils.synchronized_with_prefix('os-brick-')
class DISCOConnector(base.BaseLinuxConnector):
"""Class implements the connector driver for DISCO."""
DISCO_PREFIX = 'dms'
def __init__(self, root_helper, driver=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
"""Init DISCO connector."""
super(DISCOConnector, self).__init__(
root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs
)
LOG.info(_LI("Init DISCO connector"))
self.server_port = None
self.server_ip = None
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The DISCO connector properties."""
return {}
def get_search_path(self):
"""Get directory path where to get DISCO volumes."""
return "/dev"
def get_volume_paths(self, connection_properties):
"""Get config for DISCO volume driver."""
self.get_config(connection_properties)
volume_paths = []
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
device_paths = [disco_dev]
for path in device_paths:
if os.path.exists(path):
volume_paths.append(path)
return volume_paths
def get_all_available_volumes(self, connection_properties=None):
"""Return all DISCO volumes that exist in the search directory."""
path = self.get_search_path()
if os.path.isdir(path):
path_items = [path, '/', self.DISCO_PREFIX, '*']
file_filter = ''.join(path_items)
return glob.glob(file_filter)
else:
return []
def get_config(self, connection_properties):
"""Get config for DISCO volume driver."""
self.server_port = (
six.text_type(connection_properties['conf']['server_port']))
self.server_ip = (
six.text_type(connection_properties['conf']['server_ip']))
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
device_info = {'type': 'block',
'path': disco_dev}
return device_info
@utils.trace
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Connect the volume. Returns xml for libvirt."""
LOG.debug("Enter in DISCO connect_volume")
device_info = self.get_config(connection_properties)
LOG.debug("Device info : %s.", device_info)
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
LOG.debug("Attaching %s", disco_dev)
self._mount_disco_volume(disco_dev, disco_id)
return device_info
@utils.trace
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume from instance."""
disco_id = connection_properties['disco_id']
disco_dev = '/dev/dms%s' % (disco_id)
LOG.debug("detaching %s", disco_dev)
if os.path.exists(disco_dev):
ret = self._send_disco_vol_cmd(self.server_ip,
self.server_port,
2,
disco_id)
if ret is not None:
msg = _("Detach volume failed")
raise exception.BrickException(message=msg)
else:
LOG.info(_LI("Volume already detached from host"))
def _mount_disco_volume(self, path, volume_id):
"""Send request to mount volume on physical host."""
LOG.debug("Enter in mount disco volume %(port)s "
"and %(ip)s.",
{'port': self.server_port,
'ip': self.server_ip})
if not os.path.exists(path):
ret = self._send_disco_vol_cmd(self.server_ip,
self.server_port,
1,
volume_id)
if ret is not None:
msg = _("Attach volume failed")
raise exception.BrickException(message=msg)
else:
LOG.info(_LI("Volume already attached to host"))
def _connect_tcp_socket(self, client_ip, client_port):
"""Connect to TCP socket."""
sock = None
for res in socket.getaddrinfo(client_ip,
client_port,
socket.AF_UNSPEC,
socket.SOCK_STREAM):
aff, socktype, proto, canonname, saa = res
try:
sock = socket.socket(aff, socktype, proto)
except socket.error:
sock = None
continue
try:
sock.connect(saa)
except socket.error:
sock.close()
sock = None
continue
break
if sock is None:
LOG.error(_LE("Cannot connect TCP socket"))
return sock
def _send_disco_vol_cmd(self, client_ip, client_port, op_code, vol_id):
"""Send DISCO client socket command."""
s = self._connect_tcp_socket(client_ip, int(client_port))
if s is not None:
inst_id = 'DEFAULT-INSTID'
pktlen = 2 + 8 + len(inst_id)
LOG.debug("pktlen=%(plen)s op=%(op)s "
"vol_id=%(vol_id)s, inst_id=%(inst_id)s",
{'plen': pktlen, 'op': op_code,
'vol_id': vol_id, 'inst_id': inst_id})
data = struct.pack("!HHQ14s",
pktlen,
op_code,
int(vol_id),
inst_id)
s.sendall(data)
ret = s.recv(4)
s.close()
LOG.debug("Received ret len=%(lenR)d, ret=%(ret)s",
{'lenR': len(repr(ret)), 'ret': repr(ret)})
ret_val = "".join("%02x" % ord(c) for c in ret)
if ret_val != '00000000':
return 'ERROR'
return None
def extend_volume(self, connection_properties):
raise NotImplementedError

View File

@ -0,0 +1,109 @@
# 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 os
import tempfile
from oslo_concurrency import processutils as putils
from os_brick.initiator.connectors import base
from os_brick import utils
class DRBDConnector(base.BaseLinuxConnector):
""""Connector class to attach/detach DRBD resources."""
def __init__(self, root_helper, driver=None,
execute=putils.execute, *args, **kwargs):
super(DRBDConnector, self).__init__(root_helper, driver=driver,
execute=execute, *args, **kwargs)
self._execute = execute
self._root_helper = root_helper
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The DRBD connector properties."""
return {}
def check_valid_device(self, path, run_as_root=True):
"""Verify an existing volume."""
# TODO(linbit): check via drbdsetup first, to avoid blocking/hanging
# in case of network problems?
return super(DRBDConnector, self).check_valid_device(path, run_as_root)
def get_all_available_volumes(self, connection_properties=None):
base = "/dev/"
blkdev_list = []
for e in os.listdir(base):
path = base + e
if os.path.isblk(path):
blkdev_list.append(path)
return blkdev_list
def _drbdadm_command(self, cmd, data_dict, sh_secret):
# TODO(linbit): Write that resource file to a permanent location?
tmp = tempfile.NamedTemporaryFile(suffix="res", delete=False, mode="w")
try:
kv = {'shared-secret': sh_secret}
tmp.write(data_dict['config'] % kv)
tmp.close()
(out, err) = self._execute('drbdadm', cmd,
"-c", tmp.name,
data_dict['name'],
run_as_root=True,
root_helper=self._root_helper)
finally:
os.unlink(tmp.name)
return (out, err)
@utils.trace
def connect_volume(self, connection_properties):
"""Attach the volume."""
self._drbdadm_command("adjust", connection_properties,
connection_properties['provider_auth'])
device_info = {
'type': 'block',
'path': connection_properties['device'],
}
return device_info
@utils.trace
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume."""
self._drbdadm_command("down", connection_properties,
connection_properties['provider_auth'])
def get_volume_paths(self, connection_properties):
path = connection_properties['device']
return [path]
def get_search_path(self):
# TODO(linbit): is it allowed to return "/dev", or is that too broad?
return None
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,48 @@
# Copyright 2013 OpenStack Foundation.
# 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.
from os_brick.initiator.connectors import base
from os_brick.initiator.connectors import base_iscsi
class FakeConnector(base.BaseLinuxConnector):
fake_path = '/dev/vdFAKE'
def connect_volume(self, connection_properties):
fake_device_info = {'type': 'fake',
'path': self.fake_path}
return fake_device_info
def disconnect_volume(self, connection_properties, device_info):
pass
def get_volume_paths(self, connection_properties):
return [self.fake_path]
def get_search_path(self):
return '/dev/disk/by-path'
def extend_volume(self, connection_properties):
return None
def get_all_available_volumes(self, connection_properties=None):
return ['/dev/disk/by-path/fake-volume-1',
'/dev/disk/by-path/fake-volume-X']
class FakeBaseISCSIConnector(FakeConnector, base_iscsi.BaseISCSIConnector):
pass

View File

@ -0,0 +1,300 @@
# 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 os
from oslo_concurrency import lockutils
from oslo_log import log as logging
from oslo_service import loopingcall
import six
from os_brick.i18n import _LE, _LW
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.initiator import linuxfc
from os_brick import utils
synchronized = lockutils.synchronized_with_prefix('os-brick-')
LOG = logging.getLogger(__name__)
class FibreChannelConnector(base.BaseLinuxConnector):
"""Connector class to attach/detach Fibre Channel volumes."""
def __init__(self, root_helper, driver=None,
execute=None, use_multipath=False,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
self._linuxfc = linuxfc.LinuxFibreChannel(root_helper, execute)
super(FibreChannelConnector, self).__init__(
root_helper, driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
self.use_multipath = use_multipath
def set_execute(self, execute):
super(FibreChannelConnector, self).set_execute(execute)
self._linuxscsi.set_execute(execute)
self._linuxfc.set_execute(execute)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The Fibre Channel connector properties."""
props = {}
fc = linuxfc.LinuxFibreChannel(root_helper,
execute=kwargs.get('execute'))
wwpns = fc.get_fc_wwpns()
if wwpns:
props['wwpns'] = wwpns
wwnns = fc.get_fc_wwnns()
if wwnns:
props['wwnns'] = wwnns
return props
def get_search_path(self):
"""Where do we look for FC based volumes."""
return '/dev/disk/by-path'
def _get_possible_volume_paths(self, connection_properties, hbas):
ports = connection_properties['target_wwn']
possible_devs = self._get_possible_devices(hbas, ports)
lun = connection_properties.get('target_lun', 0)
host_paths = self._get_host_devices(possible_devs, lun)
return host_paths
def get_volume_paths(self, connection_properties):
volume_paths = []
# first fetch all of the potential paths that might exist
# how the FC fabric is zoned may alter the actual list
# that shows up on the system. So, we verify each path.
hbas = self._linuxfc.get_fc_hbas_info()
device_paths = self._get_possible_volume_paths(
connection_properties, hbas)
for path in device_paths:
if os.path.exists(path):
volume_paths.append(path)
return volume_paths
@utils.trace
@synchronized('extend_volume')
def extend_volume(self, connection_properties):
"""Update the local kernel's size information.
Try and update the local kernel's size information
for an FC volume.
"""
volume_paths = self.get_volume_paths(connection_properties)
if volume_paths:
return self._linuxscsi.extend_volume(volume_paths[0])
else:
LOG.warning(_LW("Couldn't find any volume paths on the host to "
"extend volume for %(props)s"),
{'props': connection_properties})
raise exception.VolumePathsNotFound()
@utils.trace
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Attach the volume to instance_name.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
connection_properties for Fibre Channel must include:
target_wwn - World Wide Name
target_lun - LUN id of the volume
"""
LOG.debug("execute = %s", self._execute)
device_info = {'type': 'block'}
hbas = self._linuxfc.get_fc_hbas_info()
host_devices = self._get_possible_volume_paths(
connection_properties, hbas)
if len(host_devices) == 0:
# this is empty because we don't have any FC HBAs
LOG.warning(
_LW("We are unable to locate any Fibre Channel devices"))
raise exception.NoFibreChannelHostsFound()
# The /dev/disk/by-path/... node is not always present immediately
# We only need to find the first device. Once we see the first device
# multipath will have any others.
def _wait_for_device_discovery(host_devices):
tries = self.tries
for device in host_devices:
LOG.debug("Looking for Fibre Channel dev %(device)s",
{'device': device})
if os.path.exists(device):
self.host_device = device
# get the /dev/sdX device. This is used
# to find the multipath device.
self.device_name = os.path.realpath(device)
raise loopingcall.LoopingCallDone()
if self.tries >= self.device_scan_attempts:
LOG.error(_LE("Fibre Channel volume device not found."))
raise exception.NoFibreChannelVolumeDeviceFound()
LOG.warning(_LW("Fibre Channel volume device not yet found. "
"Will rescan & retry. Try number: %(tries)s."),
{'tries': tries})
self._linuxfc.rescan_hosts(hbas)
self.tries = self.tries + 1
self.host_device = None
self.device_name = None
self.tries = 0
timer = loopingcall.FixedIntervalLoopingCall(
_wait_for_device_discovery, host_devices)
timer.start(interval=2).wait()
tries = self.tries
if self.host_device is not None and self.device_name is not None:
LOG.debug("Found Fibre Channel volume %(name)s "
"(after %(tries)s rescans)",
{'name': self.device_name, 'tries': tries})
# find out the WWN of the device
device_wwn = self._linuxscsi.get_scsi_wwn(self.host_device)
LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn})
device_info['scsi_wwn'] = device_wwn
# see if the new drive is part of a multipath
# device. If so, we'll use the multipath device.
if self.use_multipath:
(device_path, multipath_id) = (super(
FibreChannelConnector, self)._discover_mpath_device(
device_wwn, connection_properties, self.device_name))
if multipath_id:
# only set the multipath_id if we found one
device_info['multipath_id'] = multipath_id
else:
device_path = self.host_device
device_info['path'] = device_path
LOG.debug("connect_volume returning %s", device_info)
return device_info
def _get_host_devices(self, possible_devs, lun):
host_devices = []
for pci_num, target_wwn in possible_devs:
host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % (
pci_num,
target_wwn,
self._linuxscsi.process_lun_id(lun))
host_devices.append(host_device)
return host_devices
def _get_possible_devices(self, hbas, wwnports):
"""Compute the possible fibre channel device options.
:param hbas: available hba devices.
:param wwnports: possible wwn addresses. Can either be string
or list of strings.
:returns: list of (pci_id, wwn) tuples
Given one or more wwn (mac addresses for fibre channel) ports
do the matrix math to figure out a set of pci device, wwn
tuples that are potentially valid (they won't all be). This
provides a search space for the device connection.
"""
# the wwn (think mac addresses for fiber channel devices) can
# either be a single value or a list. Normalize it to a list
# for further operations.
wwns = []
if isinstance(wwnports, list):
for wwn in wwnports:
wwns.append(str(wwn))
elif isinstance(wwnports, six.string_types):
wwns.append(str(wwnports))
raw_devices = []
for hba in hbas:
pci_num = self._get_pci_num(hba)
if pci_num is not None:
for wwn in wwns:
target_wwn = "0x%s" % wwn.lower()
raw_devices.append((pci_num, target_wwn))
return raw_devices
@utils.trace
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume from instance_name.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
connection_properties for Fibre Channel must include:
target_wwn - World Wide Name
target_lun - LUN id of the volume
"""
devices = []
volume_paths = self.get_volume_paths(connection_properties)
wwn = None
for path in volume_paths:
real_path = self._linuxscsi.get_name_from_path(path)
if not wwn:
wwn = self._linuxscsi.get_scsi_wwn(path)
device_info = self._linuxscsi.get_device_info(real_path)
devices.append(device_info)
LOG.debug("devices to remove = %s", devices)
self._remove_devices(connection_properties, devices)
if self.use_multipath:
# There is a bug in multipath where the flushing
# doesn't remove the entry if friendly names are on
# we'll try anyway.
self._linuxscsi.flush_multipath_device(wwn)
def _remove_devices(self, connection_properties, devices):
# There may have been more than 1 device mounted
# by the kernel for this volume. We have to remove
# all of them
for device in devices:
self._linuxscsi.remove_scsi_device(device["device"])
def _get_pci_num(self, hba):
# NOTE(walter-boring)
# device path is in format of (FC and FCoE) :
# /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2
# /sys/devices/pci0000:20/0000:20:03.0/0000:21:00.2/net/ens2f2/ctlr_2
# /host3/fc_host/host3
# we always want the value prior to the host or net value
if hba is not None:
if "device_path" in hba:
device_path = hba['device_path'].split('/')
for index, value in enumerate(device_path):
if value.startswith('net') or value.startswith('host'):
return device_path[index - 1]
return None

View File

@ -0,0 +1,86 @@
# 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.
from oslo_log import log as logging
from os_brick import initiator
from os_brick.initiator.connectors import fibre_channel
from os_brick.initiator import linuxfc
LOG = logging.getLogger(__name__)
class FibreChannelConnectorS390X(fibre_channel.FibreChannelConnector):
"""Connector class to attach/detach Fibre Channel volumes on S390X arch."""
platform = initiator.PLATFORM_S390
def __init__(self, root_helper, driver=None,
execute=None, use_multipath=False,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(FibreChannelConnectorS390X, self).__init__(
root_helper,
driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
LOG.debug("Initializing Fibre Channel connector for S390")
self._linuxfc = linuxfc.LinuxFibreChannelS390X(root_helper, execute)
self.use_multipath = use_multipath
def set_execute(self, execute):
super(FibreChannelConnectorS390X, self).set_execute(execute)
self._linuxscsi.set_execute(execute)
self._linuxfc.set_execute(execute)
def _get_host_devices(self, possible_devs, lun):
host_devices = []
for pci_num, target_wwn in possible_devs:
target_lun = self._get_lun_string(lun)
host_device = self._get_device_file_path(
pci_num,
target_wwn,
target_lun)
self._linuxfc.configure_scsi_device(pci_num, target_wwn,
target_lun)
host_devices.append(host_device)
return host_devices
def _get_lun_string(self, lun):
target_lun = 0
if lun <= 0xffff:
target_lun = "0x%04x000000000000" % lun
elif lun <= 0xffffffff:
target_lun = "0x%08x00000000" % lun
return target_lun
def _get_device_file_path(self, pci_num, target_wwn, target_lun):
host_device = "/dev/disk/by-path/ccw-%s-zfcp-%s:%s" % (
pci_num,
target_wwn,
target_lun)
return host_device
def _remove_devices(self, connection_properties, devices):
hbas = self._linuxfc.get_fc_hbas_info()
ports = connection_properties['target_wwn']
possible_devs = self._get_possible_devices(hbas, ports)
lun = connection_properties.get('target_lun', 0)
target_lun = self._get_lun_string(lun)
for pci_num, target_wwn in possible_devs:
self._linuxfc.deconfigure_scsi_device(pci_num,
target_wwn,
target_lun)

View File

@ -0,0 +1,182 @@
# 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 os
import socket
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from os_brick.i18n import _, _LE
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick import utils
LOG = logging.getLogger(__name__)
class HGSTConnector(base.BaseLinuxConnector):
"""Connector class to attach/detach HGST volumes."""
VGCCLUSTER = 'vgc-cluster'
def __init__(self, root_helper, driver=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(HGSTConnector, self).__init__(root_helper, driver=driver,
device_scan_attempts=
device_scan_attempts,
*args, **kwargs)
self._vgc_host = None
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The HGST connector properties."""
return {}
def _log_cli_err(self, err):
"""Dumps the full command output to a logfile in error cases."""
LOG.error(_LE("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n"
"err: %(stderr)s"),
{'cmd': err.cmd, 'code': err.exit_code,
'stdout': err.stdout, 'stderr': err.stderr})
def _find_vgc_host(self):
"""Finds vgc-cluster hostname for this box."""
params = [self.VGCCLUSTER, "domain-list", "-1"]
try:
out, unused = self._execute(*params, run_as_root=True,
root_helper=self._root_helper)
except putils.ProcessExecutionError as err:
self._log_cli_err(err)
msg = _("Unable to get list of domain members, check that "
"the cluster is running.")
raise exception.BrickException(message=msg)
domain = out.splitlines()
params = ["ip", "addr", "list"]
try:
out, unused = self._execute(*params, run_as_root=False)
except putils.ProcessExecutionError as err:
self._log_cli_err(err)
msg = _("Unable to get list of IP addresses on this host, "
"check permissions and networking.")
raise exception.BrickException(message=msg)
nets = out.splitlines()
for host in domain:
try:
ip = socket.gethostbyname(host)
for l in nets:
x = l.strip()
if x.startswith("inet %s/" % ip):
return host
except socket.error:
pass
msg = _("Current host isn't part of HGST domain.")
raise exception.BrickException(message=msg)
def _hostname(self):
"""Returns hostname to use for cluster operations on this box."""
if self._vgc_host is None:
self._vgc_host = self._find_vgc_host()
return self._vgc_host
def get_search_path(self):
return "/dev"
def get_volume_paths(self, connection_properties):
path = ("%(path)s/%(name)s" %
{'path': self.get_search_path(),
'name': connection_properties['name']})
volume_path = None
if os.path.exists(path):
volume_path = path
return [volume_path]
@utils.trace
def connect_volume(self, connection_properties):
"""Attach a Space volume to running host.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
connection_properties for HGST must include:
name - Name of space to attach
:type connection_properties: dict
:returns: dict
"""
if connection_properties is None:
msg = _("Connection properties passed in as None.")
raise exception.BrickException(message=msg)
if 'name' not in connection_properties:
msg = _("Connection properties missing 'name' field.")
raise exception.BrickException(message=msg)
device_info = {
'type': 'block',
'device': connection_properties['name'],
'path': '/dev/' + connection_properties['name']
}
volname = device_info['device']
params = [self.VGCCLUSTER, 'space-set-apphosts']
params += ['-n', volname]
params += ['-A', self._hostname()]
params += ['--action', 'ADD']
try:
self._execute(*params, run_as_root=True,
root_helper=self._root_helper)
except putils.ProcessExecutionError as err:
self._log_cli_err(err)
msg = (_("Unable to set apphost for space %s") % volname)
raise exception.BrickException(message=msg)
return device_info
@utils.trace
def disconnect_volume(self, connection_properties, device_info):
"""Detach and flush the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
For HGST must include:
name - Name of space to detach
noremovehost - Host which should never be removed
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
if connection_properties is None:
msg = _("Connection properties passed in as None.")
raise exception.BrickException(message=msg)
if 'name' not in connection_properties:
msg = _("Connection properties missing 'name' field.")
raise exception.BrickException(message=msg)
if 'noremovehost' not in connection_properties:
msg = _("Connection properties missing 'noremovehost' field.")
raise exception.BrickException(message=msg)
if connection_properties['noremovehost'] != self._hostname():
params = [self.VGCCLUSTER, 'space-set-apphosts']
params += ['-n', connection_properties['name']]
params += ['-A', self._hostname()]
params += ['--action', 'DELETE']
try:
self._execute(*params, run_as_root=True,
root_helper=self._root_helper)
except putils.ProcessExecutionError as err:
self._log_cli_err(err)
msg = (_("Unable to set apphost for space %s") %
connection_properties['name'])
raise exception.BrickException(message=msg)
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,192 @@
# 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 os
from oslo_concurrency import lockutils
from oslo_log import log as logging
from os_brick.i18n import _, _LE
from os_brick import exception
from os_brick.initiator.connectors import base
from os_brick import utils
LOG = logging.getLogger(__name__)
synchronized = lockutils.synchronized_with_prefix('os-brick-')
class HuaweiStorHyperConnector(base.BaseLinuxConnector):
""""Connector class to attach/detach SDSHypervisor volumes."""
attached_success_code = 0
has_been_attached_code = 50151401
attach_mnid_done_code = 50151405
vbs_unnormal_code = 50151209
not_mount_node_code = 50155007
iscliexist = True
def __init__(self, root_helper, driver=None,
*args, **kwargs):
self.cli_path = os.getenv('HUAWEISDSHYPERVISORCLI_PATH')
if not self.cli_path:
self.cli_path = '/usr/local/bin/sds/sds_cli'
LOG.debug("CLI path is not configured, using default %s.",
self.cli_path)
if not os.path.isfile(self.cli_path):
self.iscliexist = False
LOG.error(_LE('SDS CLI file not found, '
'HuaweiStorHyperConnector init failed.'))
super(HuaweiStorHyperConnector, self).__init__(root_helper,
driver=driver,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The HuaweiStor connector properties."""
return {}
def get_search_path(self):
# TODO(walter-boring): Where is the location on the filesystem to
# look for Huawei volumes to show up?
return None
def get_all_available_volumes(self, connection_properties=None):
# TODO(walter-boring): what to return here for all Huawei volumes ?
return []
def get_volume_paths(self, connection_properties):
volume_path = None
try:
volume_path = self._get_volume_path(connection_properties)
except Exception:
msg = _("Couldn't find a volume.")
LOG.warning(msg)
raise exception.BrickException(message=msg)
return [volume_path]
def _get_volume_path(self, connection_properties):
out = self._query_attached_volume(
connection_properties['volume_id'])
if not out or int(out['ret_code']) != 0:
msg = _("Couldn't find attached volume.")
LOG.error(msg)
raise exception.BrickException(message=msg)
return out['dev_addr']
@utils.trace
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Connect to a volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
"""
LOG.debug("Connect_volume connection properties: %s.",
connection_properties)
out = self._attach_volume(connection_properties['volume_id'])
if not out or int(out['ret_code']) not in (self.attached_success_code,
self.has_been_attached_code,
self.attach_mnid_done_code):
msg = (_("Attach volume failed, "
"error code is %s") % out['ret_code'])
raise exception.BrickException(message=msg)
try:
volume_path = self._get_volume_path(connection_properties)
except Exception:
msg = _("query attached volume failed or volume not attached.")
LOG.error(msg)
raise exception.BrickException(message=msg)
device_info = {'type': 'block',
'path': volume_path}
return device_info
@utils.trace
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect a volume from the local host.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
LOG.debug("Disconnect_volume: %s.", connection_properties)
out = self._detach_volume(connection_properties['volume_id'])
if not out or int(out['ret_code']) not in (self.attached_success_code,
self.vbs_unnormal_code,
self.not_mount_node_code):
msg = (_("Disconnect_volume failed, "
"error code is %s") % out['ret_code'])
raise exception.BrickException(message=msg)
def is_volume_connected(self, volume_name):
"""Check if volume already connected to host"""
LOG.debug('Check if volume %s already connected to a host.',
volume_name)
out = self._query_attached_volume(volume_name)
if out:
return int(out['ret_code']) == 0
return False
def _attach_volume(self, volume_name):
return self._cli_cmd('attach', volume_name)
def _detach_volume(self, volume_name):
return self._cli_cmd('detach', volume_name)
def _query_attached_volume(self, volume_name):
return self._cli_cmd('querydev', volume_name)
def _cli_cmd(self, method, volume_name):
LOG.debug("Enter into _cli_cmd.")
if not self.iscliexist:
msg = _("SDS command line doesn't exist, "
"can't execute SDS command.")
raise exception.BrickException(message=msg)
if not method or volume_name is None:
return
cmd = [self.cli_path, '-c', method, '-v', volume_name]
out, clilog = self._execute(*cmd, run_as_root=False,
root_helper=self._root_helper)
analyse_result = self._analyze_output(out)
LOG.debug('%(method)s volume returns %(analyse_result)s.',
{'method': method, 'analyse_result': analyse_result})
if clilog:
LOG.error(_LE("SDS CLI output some log: %s."), clilog)
return analyse_result
def _analyze_output(self, out):
LOG.debug("Enter into _analyze_output.")
if out:
analyse_result = {}
out_temp = out.split('\n')
for line in out_temp:
LOG.debug("Line is %s.", line)
if line.find('=') != -1:
key, val = line.split('=', 1)
LOG.debug("%(key)s = %(val)s", {'key': key, 'val': val})
if key in ['ret_code', 'ret_desc', 'dev_addr']:
analyse_result[key] = val
return analyse_result
else:
return None
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,832 @@
# 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 copy
import glob
import os
import re
import time
from oslo_concurrency import lockutils
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from oslo_utils import strutils
from os_brick.i18n import _, _LE, _LI, _LW
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.initiator.connectors import base_iscsi
from os_brick import utils
synchronized = lockutils.synchronized_with_prefix('os-brick-')
LOG = logging.getLogger(__name__)
class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector):
"""Connector class to attach/detach iSCSI volumes."""
supported_transports = ['be2iscsi', 'bnx2i', 'cxgb3i', 'default',
'cxgb4i', 'qla4xxx', 'ocs', 'iser']
def __init__(self, root_helper, driver=None,
execute=None, use_multipath=False,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
transport='default', *args, **kwargs):
super(ISCSIConnector, self).__init__(
root_helper, driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
transport=transport, *args, **kwargs)
self.use_multipath = use_multipath
self.transport = self._validate_iface_transport(transport)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The iSCSI connector properties."""
props = {}
iscsi = ISCSIConnector(root_helper=root_helper,
execute=kwargs.get('execute'))
initiator = iscsi.get_initiator()
if initiator:
props['initiator'] = initiator
return props
def get_search_path(self):
"""Where do we look for iSCSI based volumes."""
return '/dev/disk/by-path'
def get_volume_paths(self, connection_properties):
"""Get the list of existing paths for a volume.
This method's job is to simply report what might/should
already exist for a volume. We aren't trying to attach/discover
a new volume, but find any existing paths for a volume we
think is already attached.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
"""
volume_paths = []
# if there are no sessions, then target_portal won't exist
if (('target_portal' not in connection_properties) and
('target_portals' not in connection_properties)):
return volume_paths
# Don't try and connect to the portals in the list as
# this can create empty iSCSI sessions to hosts if they
# didn't exist previously.
# We are simply trying to find any existing volumes with
# already connected sessions.
host_devices, target_props = self._get_potential_volume_paths(
connection_properties,
connect_to_portal=False,
use_rescan=False)
for path in host_devices:
if os.path.exists(path):
volume_paths.append(path)
return volume_paths
def _get_iscsi_sessions(self):
out, err = self._run_iscsi_session()
iscsi_sessions = []
if err:
LOG.warning(_LW("Couldn't find iscsi sessions because "
"iscsiadm err: %s"),
err)
else:
# parse the output from iscsiadm
# lines are in the format of
# tcp: [1] 192.168.121.250:3260,1 iqn.2010-10.org.openstack:volume-
lines = out.split('\n')
for line in lines:
if line:
entries = line.split()
portal = entries[2].split(',')
iscsi_sessions.append(portal[0])
return iscsi_sessions
def _get_potential_volume_paths(self, connection_properties,
connect_to_portal=True,
use_rescan=True):
"""Build a list of potential volume paths that exist.
Given a list of target_portals in the connection_properties,
a list of paths might exist on the system during discovery.
This method's job is to build that list of potential paths
for a volume that might show up.
This is used during connect_volume time, in which case we want
to connect to the iSCSI target portal.
During get_volume_paths time, we are looking to
find a list of existing volume paths for the connection_properties.
In this case, we don't want to connect to the portal. If we
blindly try and connect to a portal, it could create a new iSCSI
session that didn't exist previously, and then leave it stale.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param connect_to_portal: Do we want to try a new connection to the
target portal(s)? Set this to False if you
want to search for existing volumes, not
discover new volumes.
:param connect_to_portal: bool
:param use_rescan: Issue iSCSI rescan during discovery?
:type use_rescan: bool
:returns: dict
"""
target_props = None
connected_to_portal = False
if self.use_multipath:
LOG.info(_LI("Multipath discovery for iSCSI enabled"))
# Multipath installed, discovering other targets if available
try:
ips_iqns = self._discover_iscsi_portals(connection_properties)
except Exception:
if 'target_portals' in connection_properties:
raise exception.TargetPortalsNotFound(
target_portal=connection_properties['target_portals'])
elif 'target_portal' in connection_properties:
raise exception.TargetPortalNotFound(
target_portal=connection_properties['target_portal'])
else:
raise
if not connection_properties.get('target_iqns'):
# There are two types of iSCSI multipath devices. One which
# shares the same iqn between multiple portals, and the other
# which use different iqns on different portals.
# Try to identify the type by checking the iscsiadm output
# if the iqn is used by multiple portals. If it is, it's
# the former, so use the supplied iqn. Otherwise, it's the
# latter, so try the ip,iqn combinations to find the targets
# which constitutes the multipath device.
main_iqn = connection_properties['target_iqn']
all_portals = set([ip for ip, iqn in ips_iqns])
match_portals = set([ip for ip, iqn in ips_iqns
if iqn == main_iqn])
if len(all_portals) == len(match_portals):
ips_iqns = zip(all_portals, [main_iqn] * len(all_portals))
for ip, iqn in ips_iqns:
props = copy.deepcopy(connection_properties)
props['target_portal'] = ip
props['target_iqn'] = iqn
if connect_to_portal:
if self._connect_to_iscsi_portal(props):
connected_to_portal = True
if use_rescan:
self._rescan_iscsi()
host_devices = self._get_device_path(connection_properties)
else:
LOG.info(_LI("Multipath discovery for iSCSI not enabled."))
iscsi_sessions = []
if not connect_to_portal:
iscsi_sessions = self._get_iscsi_sessions()
host_devices = []
target_props = connection_properties
for props in self._iterate_all_targets(connection_properties):
if connect_to_portal:
if self._connect_to_iscsi_portal(props):
target_props = props
connected_to_portal = True
host_devices = self._get_device_path(props)
break
else:
LOG.warning(_LW(
'Failed to connect to iSCSI portal %(portal)s.'),
{'portal': props['target_portal']})
else:
# If we aren't trying to connect to the portal, we
# want to find ALL possible paths from all of the
# alternate portals
if props['target_portal'] in iscsi_sessions:
paths = self._get_device_path(props)
host_devices = list(set(paths + host_devices))
if connect_to_portal and not connected_to_portal:
msg = _("Could not login to any iSCSI portal.")
LOG.error(msg)
raise exception.FailedISCSITargetPortalLogin(message=msg)
return host_devices, target_props
def set_execute(self, execute):
super(ISCSIConnector, self).set_execute(execute)
self._linuxscsi.set_execute(execute)
def _validate_iface_transport(self, transport_iface):
"""Check that given iscsi_iface uses only supported transports
Accepted transport names for provided iface param are
be2iscsi, bnx2i, cxgb3i, cxgb4i, default, qla4xxx, ocs or iser.
Note the difference between transport and iface;
unlike default(iscsi_tcp)/iser, this is not one and the same for
offloaded transports, where the default format is
transport_name.hwaddress
:param transport_iface: The iscsi transport type.
:type transport_iface: str
:returns: str
"""
# Note that default(iscsi_tcp) and iser do not require a separate
# iface file, just the transport is enough and do not need to be
# validated. This is not the case for the other entries in
# supported_transports array.
if transport_iface in ['default', 'iser']:
return transport_iface
# Will return (6) if iscsi_iface file was not found, or (2) if iscsid
# could not be contacted
out = self._run_iscsiadm_bare(['-m',
'iface',
'-I',
transport_iface],
check_exit_code=[0, 2, 6])[0] or ""
LOG.debug("iscsiadm %(iface)s configuration: stdout=%(out)s.",
{'iface': transport_iface, 'out': out})
for data in [line.split() for line in out.splitlines()]:
if data[0] == 'iface.transport_name':
if data[2] in self.supported_transports:
return transport_iface
LOG.warning(_LW("No useable transport found for iscsi iface %s. "
"Falling back to default transport."),
transport_iface)
return 'default'
def _get_transport(self):
return self.transport
def _discover_iscsi_portals(self, connection_properties):
if all([key in connection_properties for key in ('target_portals',
'target_iqns')]):
# Use targets specified by connection_properties
return zip(connection_properties['target_portals'],
connection_properties['target_iqns'])
out = None
if connection_properties.get('discovery_auth_method'):
try:
self._run_iscsiadm_update_discoverydb(connection_properties)
except putils.ProcessExecutionError as exception:
# iscsiadm returns 6 for "db record not found"
if exception.exit_code == 6:
# Create a new record for this target and update the db
self._run_iscsiadm_bare(
['-m', 'discoverydb',
'-t', 'sendtargets',
'-p', connection_properties['target_portal'],
'--op', 'new'],
check_exit_code=[0, 255])
self._run_iscsiadm_update_discoverydb(
connection_properties
)
else:
LOG.error(_LE("Unable to find target portal: "
"%(target_portal)s."),
{'target_portal': connection_properties[
'target_portal']})
raise
out = self._run_iscsiadm_bare(
['-m', 'discoverydb',
'-t', 'sendtargets',
'-p', connection_properties['target_portal'],
'--discover'],
check_exit_code=[0, 255])[0] or ""
else:
out = self._run_iscsiadm_bare(
['-m', 'discovery',
'-t', 'sendtargets',
'-p', connection_properties['target_portal']],
check_exit_code=[0, 255])[0] or ""
return self._get_target_portals_from_iscsiadm_output(out)
def _run_iscsiadm_update_discoverydb(self, connection_properties):
return self._execute(
'iscsiadm',
'-m', 'discoverydb',
'-t', 'sendtargets',
'-p', connection_properties['target_portal'],
'--op', 'update',
'-n', "discovery.sendtargets.auth.authmethod",
'-v', connection_properties['discovery_auth_method'],
'-n', "discovery.sendtargets.auth.username",
'-v', connection_properties['discovery_auth_username'],
'-n', "discovery.sendtargets.auth.password",
'-v', connection_properties['discovery_auth_password'],
run_as_root=True,
root_helper=self._root_helper)
@utils.trace
@synchronized('extend_volume')
def extend_volume(self, connection_properties):
"""Update the local kernel's size information.
Try and update the local kernel's size information
for an iSCSI volume.
"""
LOG.info(_LI("Extend volume for %s"), connection_properties)
volume_paths = self.get_volume_paths(connection_properties)
LOG.info(_LI("Found paths for volume %s"), volume_paths)
if volume_paths:
return self._linuxscsi.extend_volume(volume_paths[0])
else:
LOG.warning(_LW("Couldn't find any volume paths on the host to "
"extend volume for %(props)s"),
{'props': connection_properties})
raise exception.VolumePathsNotFound()
@utils.trace
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Attach the volume to instance_name.
:param connection_properties: The valid dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
connection_properties for iSCSI must include:
target_portal(s) - ip and optional port
target_iqn(s) - iSCSI Qualified Name
target_lun(s) - LUN id of the volume
Note that plural keys may be used when use_multipath=True
"""
device_info = {'type': 'block'}
host_devices, target_props = self._get_potential_volume_paths(
connection_properties)
# The /dev/disk/by-path/... node is not always present immediately
# TODO(justinsb): This retry-with-delay is a pattern, move to utils?
tries = 0
# Loop until at least 1 path becomes available
while all(map(lambda x: not os.path.exists(x), host_devices)):
if tries >= self.device_scan_attempts:
raise exception.VolumeDeviceNotFound(device=host_devices)
LOG.warning(_LW("ISCSI volume not yet found at: %(host_devices)s. "
"Will rescan & retry. Try number: %(tries)s."),
{'host_devices': host_devices,
'tries': tries})
# The rescan isn't documented as being necessary(?), but it helps
if self.use_multipath:
self._rescan_iscsi()
else:
if (tries):
host_devices = self._get_device_path(target_props)
self._run_iscsiadm(target_props, ("--rescan",))
tries = tries + 1
if all(map(lambda x: not os.path.exists(x), host_devices)):
time.sleep(tries ** 2)
else:
break
if tries != 0:
LOG.debug("Found iSCSI node %(host_devices)s "
"(after %(tries)s rescans)",
{'host_devices': host_devices, 'tries': tries})
# Choose an accessible host device
host_device = next(dev for dev in host_devices if os.path.exists(dev))
# find out the WWN of the device
device_wwn = self._linuxscsi.get_scsi_wwn(host_device)
LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn})
device_info['scsi_wwn'] = device_wwn
if self.use_multipath:
(host_device, multipath_id) = (super(
ISCSIConnector, self)._discover_mpath_device(
device_wwn, connection_properties, host_device))
if multipath_id:
device_info['multipath_id'] = multipath_id
device_info['path'] = host_device
LOG.debug("connect_volume returning %s", device_info)
return device_info
@utils.trace
@synchronized('connect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Detach the volume from instance_name.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
connection_properties for iSCSI must include:
target_portal(s) - IP and optional port
target_iqn(s) - iSCSI Qualified Name
target_lun(s) - LUN id of the volume
"""
if self.use_multipath:
self._rescan_multipath()
host_device = multipath_device = None
host_devices = self._get_device_path(connection_properties)
# Choose an accessible host device
for dev in host_devices:
if os.path.exists(dev):
host_device = dev
device_wwn = self._linuxscsi.get_scsi_wwn(dev)
(multipath_device, multipath_id) = (super(
ISCSIConnector, self)._discover_mpath_device(
device_wwn, connection_properties, dev))
if multipath_device:
break
if not host_device:
LOG.error(_LE("No accessible volume device: %(host_devices)s"),
{'host_devices': host_devices})
raise exception.VolumeDeviceNotFound(device=host_devices)
if multipath_device:
device_realpath = os.path.realpath(host_device)
self._linuxscsi.remove_multipath_device(device_realpath)
return self._disconnect_volume_multipath_iscsi(
connection_properties, multipath_device)
# When multiple portals/iqns/luns are specified, we need to remove
# unused devices created by logging into other LUNs' session.
for props in self._iterate_all_targets(connection_properties):
self._disconnect_volume_iscsi(props)
def _disconnect_volume_iscsi(self, connection_properties):
# remove the device from the scsi subsystem
# this eliminates any stale entries until logout
host_devices = self._get_device_path(connection_properties)
if host_devices:
host_device = host_devices[0]
else:
return
dev_name = self._linuxscsi.get_name_from_path(host_device)
if dev_name:
self._linuxscsi.remove_scsi_device(dev_name)
# NOTE(jdg): On busy systems we can have a race here
# where remove_iscsi_device is called before the device file
# has actually been removed. The result is an orphaned
# iscsi session that never gets logged out. The following
# call to wait addresses that issue.
self._linuxscsi.wait_for_volume_removal(host_device)
# NOTE(vish): Only disconnect from the target if no luns from the
# target are in use.
device_byname = ("ip-%(portal)s-iscsi-%(iqn)s-lun-" %
{'portal': connection_properties['target_portal'],
'iqn': connection_properties['target_iqn']})
devices = self.driver.get_all_block_devices()
devices = [dev for dev in devices if (device_byname in dev
and
dev.startswith(
'/dev/disk/by-path/'))
and os.path.exists(dev)]
if not devices:
self._disconnect_from_iscsi_portal(connection_properties)
def _munge_portal(self, target):
"""Remove brackets from portal.
In case IPv6 address was used the udev path should not contain any
brackets. Udev code specifically forbids that.
"""
portal, iqn, lun = target
return (portal.replace('[', '').replace(']', ''), iqn,
self._linuxscsi.process_lun_id(lun))
def _get_device_path(self, connection_properties):
if self._get_transport() == "default":
return ["/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" %
self._munge_portal(x) for x in
self._get_all_targets(connection_properties)]
else:
# we are looking for paths in the format :
# /dev/disk/by-path/
# pci-XXXX:XX:XX.X-ip-PORTAL:PORT-iscsi-IQN-lun-LUN_ID
device_list = []
for x in self._get_all_targets(connection_properties):
look_for_device = glob.glob(
'/dev/disk/by-path/*ip-%s-iscsi-%s-lun-%s' %
self._munge_portal(x))
if look_for_device:
device_list.extend(look_for_device)
return device_list
def get_initiator(self):
"""Secure helper to read file as root."""
file_path = '/etc/iscsi/initiatorname.iscsi'
try:
lines, _err = self._execute('cat', file_path, run_as_root=True,
root_helper=self._root_helper)
for l in lines.split('\n'):
if l.startswith('InitiatorName='):
return l[l.index('=') + 1:].strip()
except putils.ProcessExecutionError:
LOG.warning(_LW("Could not find the iSCSI Initiator File %s"),
file_path)
return None
def _run_iscsiadm(self, connection_properties, iscsi_command, **kwargs):
check_exit_code = kwargs.pop('check_exit_code', 0)
attempts = kwargs.pop('attempts', 1)
delay_on_retry = kwargs.pop('delay_on_retry', True)
(out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
connection_properties['target_iqn'],
'-p',
connection_properties['target_portal'],
*iscsi_command, run_as_root=True,
root_helper=self._root_helper,
check_exit_code=check_exit_code,
attempts=attempts,
delay_on_retry=delay_on_retry)
msg = ("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s" %
{'iscsi_command': iscsi_command, 'out': out, 'err': err})
# don't let passwords be shown in log output
LOG.debug(strutils.mask_password(msg))
return (out, err)
def _iscsiadm_update(self, connection_properties, property_key,
property_value, **kwargs):
iscsi_command = ('--op', 'update', '-n', property_key,
'-v', property_value)
return self._run_iscsiadm(connection_properties, iscsi_command,
**kwargs)
def _get_target_portals_from_iscsiadm_output(self, output):
# return both portals and iqns
#
# as we are parsing a command line utility, allow for the
# possibility that additional debug data is spewed in the
# stream, and only grab actual ip / iqn lines.
targets = []
for data in [line.split() for line in output.splitlines()]:
if len(data) == 2 and data[1].startswith('iqn.'):
targets.append(data)
return targets
def _disconnect_volume_multipath_iscsi(self, connection_properties,
multipath_name):
"""This removes a multipath device and it's LUNs."""
LOG.debug("Disconnect multipath device %s", multipath_name)
mpath_map = self._get_multipath_device_map()
block_devices = self.driver.get_all_block_devices()
devices = []
for dev in block_devices:
if os.path.exists(dev):
if "/mapper/" in dev:
devices.append(dev)
else:
mpdev = mpath_map.get(dev)
if mpdev:
devices.append(mpdev)
# Do a discovery to find all targets.
# Targets for multiple paths for the same multipath device
# may not be the same.
all_ips_iqns = self._discover_iscsi_portals(connection_properties)
# As discovery result may contain other targets' iqns, extract targets
# to be disconnected whose block devices are already deleted here.
ips_iqns = []
entries = [device.lstrip('ip-').split('-lun-')[0]
for device in self._get_iscsi_devices()]
for ip, iqn in all_ips_iqns:
ip_iqn = "%s-iscsi-%s" % (ip.split(",")[0], iqn)
if ip_iqn not in entries:
ips_iqns.append([ip, iqn])
if not devices:
# disconnect if no other multipath devices
self._disconnect_mpath(connection_properties, ips_iqns)
return
# Get a target for all other multipath devices
other_iqns = self._get_multipath_iqns(devices, mpath_map)
# Get all the targets for the current multipath device
current_iqns = [iqn for ip, iqn in ips_iqns]
in_use = False
for current in current_iqns:
if current in other_iqns:
in_use = True
break
# If no other multipath device attached has the same iqn
# as the current device
if not in_use:
# disconnect if no other multipath devices with same iqn
self._disconnect_mpath(connection_properties, ips_iqns)
return
# else do not disconnect iscsi portals,
# as they are used for other luns
return
def _connect_to_iscsi_portal(self, connection_properties):
# NOTE(vish): If we are on the same host as nova volume, the
# discovery makes the target so we don't need to
# run --op new. Therefore, we check to see if the
# target exists, and if we get 255 (Not Found), then
# we run --op new. This will also happen if another
# volume is using the same target.
LOG.info(_LI("Trying to connect to iSCSI portal %(portal)s"),
{"portal": connection_properties['target_portal']})
try:
self._run_iscsiadm(connection_properties, ())
except putils.ProcessExecutionError as exc:
# iscsiadm returns 21 for "No records found" after version 2.0-871
if exc.exit_code in [21, 255]:
self._run_iscsiadm(connection_properties,
('--interface', self._get_transport(),
'--op', 'new'))
else:
raise
if connection_properties.get('auth_method'):
self._iscsiadm_update(connection_properties,
"node.session.auth.authmethod",
connection_properties['auth_method'])
self._iscsiadm_update(connection_properties,
"node.session.auth.username",
connection_properties['auth_username'])
self._iscsiadm_update(connection_properties,
"node.session.auth.password",
connection_properties['auth_password'])
# Duplicate logins crash iscsiadm after load,
# so we scan active sessions to see if the node is logged in.
out = self._run_iscsiadm_bare(["-m", "session"],
run_as_root=True,
check_exit_code=[0, 1, 21])[0] or ""
portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]}
for p in out.splitlines() if p.startswith("tcp:")]
stripped_portal = connection_properties['target_portal'].split(",")[0]
if len(portals) == 0 or len([s for s in portals
if stripped_portal ==
s['portal'].split(",")[0]
and
s['iqn'] ==
connection_properties['target_iqn']]
) == 0:
try:
self._run_iscsiadm(connection_properties,
("--login",),
check_exit_code=[0, 255])
except putils.ProcessExecutionError as err:
# exit_code=15 means the session already exists, so it should
# be regarded as successful login.
if err.exit_code not in [15]:
LOG.warning(_LW('Failed to login iSCSI target %(iqn)s '
'on portal %(portal)s (exit code '
'%(err)s).'),
{'iqn': connection_properties['target_iqn'],
'portal': connection_properties[
'target_portal'],
'err': err.exit_code})
return False
self._iscsiadm_update(connection_properties,
"node.startup",
"automatic")
return True
def _disconnect_from_iscsi_portal(self, connection_properties):
self._iscsiadm_update(connection_properties, "node.startup", "manual",
check_exit_code=[0, 21, 255])
self._run_iscsiadm(connection_properties, ("--logout",),
check_exit_code=[0, 21, 255])
self._run_iscsiadm(connection_properties, ('--op', 'delete'),
check_exit_code=[0, 21, 255],
attempts=5,
delay_on_retry=True)
def _get_iscsi_devices(self):
try:
devices = list(os.walk('/dev/disk/by-path'))[0][-1]
except IndexError:
return []
# For iSCSI HBAs, look at an offset of len('pci-0000:00:00.0')
return [entry for entry in devices if (entry.startswith("ip-")
or (entry.startswith("pci-")
and
entry.find("ip-", 16, 21)
>= 16))]
def _disconnect_mpath(self, connection_properties, ips_iqns):
for ip, iqn in ips_iqns:
props = copy.deepcopy(connection_properties)
props['target_portal'] = ip
props['target_iqn'] = iqn
self._disconnect_from_iscsi_portal(props)
self._rescan_multipath()
def _get_multipath_iqns(self, multipath_devices, mpath_map):
entries = self._get_iscsi_devices()
iqns = []
for entry in entries:
entry_real_path = os.path.realpath("/dev/disk/by-path/%s" % entry)
entry_multipath = mpath_map.get(entry_real_path)
if entry_multipath and entry_multipath in multipath_devices:
iqns.append(entry.split("iscsi-")[1].split("-lun")[0])
return iqns
def _get_multipath_device_map(self):
out = self._run_multipath(['-ll'], check_exit_code=[0, 1])[0]
mpath_line = [line for line in out.splitlines()
if not re.match(initiator.MULTIPATH_ERROR_REGEX, line)]
mpath_dev = None
mpath_map = {}
for line in out.splitlines():
m = initiator.MULTIPATH_DEV_CHECK_REGEX.split(line)
if len(m) >= 2:
mpath_dev = '/dev/mapper/' + m[0].split(" ")[0]
continue
m = initiator.MULTIPATH_PATH_CHECK_REGEX.split(line)
if len(m) >= 2:
mpath_map['/dev/' + m[1].split(" ")[0]] = mpath_dev
if mpath_line and not mpath_map:
LOG.warning(_LW("Failed to parse the output of multipath -ll. "
"stdout: %s"), out)
return mpath_map
def _run_iscsi_session(self):
(out, err) = self._run_iscsiadm_bare(('-m', 'session'),
check_exit_code=[0, 1, 21, 255])
LOG.debug("iscsi session list stdout=%(out)s stderr=%(err)s",
{'out': out, 'err': err})
return (out, err)
def _run_iscsiadm_bare(self, iscsi_command, **kwargs):
check_exit_code = kwargs.pop('check_exit_code', 0)
(out, err) = self._execute('iscsiadm',
*iscsi_command,
run_as_root=True,
root_helper=self._root_helper,
check_exit_code=check_exit_code)
LOG.debug("iscsiadm %(iscsi_command)s: stdout=%(out)s stderr=%(err)s",
{'iscsi_command': iscsi_command, 'out': out, 'err': err})
return (out, err)
def _run_multipath(self, multipath_command, **kwargs):
check_exit_code = kwargs.pop('check_exit_code', 0)
(out, err) = self._execute('multipath',
*multipath_command,
run_as_root=True,
root_helper=self._root_helper,
check_exit_code=check_exit_code)
LOG.debug("multipath %(multipath_command)s: "
"stdout=%(out)s stderr=%(err)s",
{'multipath_command': multipath_command,
'out': out, 'err': err})
return (out, err)
def _rescan_iscsi(self):
self._run_iscsiadm_bare(('-m', 'node', '--rescan'),
check_exit_code=[0, 1, 21, 255])
self._run_iscsiadm_bare(('-m', 'session', '--rescan'),
check_exit_code=[0, 1, 21, 255])
def _rescan_multipath(self):
self._run_multipath(['-r'], check_exit_code=[0, 1, 21])

View File

@ -0,0 +1,78 @@
# 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.
from os_brick.i18n import _
from os_brick.initiator.connectors import base
from os_brick import utils
class LocalConnector(base.BaseLinuxConnector):
""""Connector class to attach/detach File System backed volumes."""
def __init__(self, root_helper, driver=None,
*args, **kwargs):
super(LocalConnector, self).__init__(root_helper, driver=driver,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The Local connector properties."""
return {}
def get_volume_paths(self, connection_properties):
path = connection_properties['device_path']
return [path]
def get_search_path(self):
return None
def get_all_available_volumes(self, connection_properties=None):
# TODO(walter-boring): not sure what to return here.
return []
@utils.trace
def connect_volume(self, connection_properties):
"""Connect to a volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
connection_properties must include:
device_path - path to the volume to be connected
:type connection_properties: dict
:returns: dict
"""
if 'device_path' not in connection_properties:
msg = (_("Invalid connection_properties specified "
"no device_path attribute"))
raise ValueError(msg)
device_info = {'type': 'local',
'path': connection_properties['device_path']}
return device_info
@utils.trace
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect a volume from the local host.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
pass
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,166 @@
# 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.
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from os_brick.i18n import _, _LE
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.initiator import linuxrbd
from os_brick import utils
LOG = logging.getLogger(__name__)
class RBDConnector(base.BaseLinuxConnector):
""""Connector class to attach/detach RBD volumes."""
def __init__(self, root_helper, driver=None, use_multipath=False,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(RBDConnector, self).__init__(root_helper, driver=driver,
device_scan_attempts=
device_scan_attempts,
*args, **kwargs)
self.do_local_attach = kwargs.get('do_local_attach', False)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The RBD connector properties."""
return {'do_local_attach': kwargs.get('do_local_attach', False)}
def get_volume_paths(self, connection_properties):
# TODO(e0ne): Implement this for local volume.
return []
def get_search_path(self):
# TODO(walter-boring): don't know where the connector
# looks for RBD volumes.
return None
def get_all_available_volumes(self, connection_properties=None):
# TODO(e0ne): Implement this for local volume.
return []
def _get_rbd_handle(self, connection_properties):
try:
user = connection_properties['auth_username']
pool, volume = connection_properties['name'].split('/')
conf = connection_properties.get('conffile')
except IndexError:
msg = _("Connect volume failed, malformed connection properties")
raise exception.BrickException(msg=msg)
rbd_client = linuxrbd.RBDClient(user, pool)
rbd_volume = linuxrbd.RBDVolume(rbd_client, volume)
rbd_handle = linuxrbd.RBDVolumeIOWrapper(
linuxrbd.RBDImageMetadata(rbd_volume, pool, user, conf))
return rbd_handle
@staticmethod
def get_rbd_device_name(pool, volume):
"""Return device name which will be generated by RBD kernel module.
:param pool: RBD pool name.
:type pool: string
:param volume: RBD image name.
:type volume: string
"""
return '/dev/rbd/{pool}/{volume}'.format(pool=pool, volume=volume)
@utils.trace
def connect_volume(self, connection_properties):
"""Connect to a volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
"""
do_local_attach = connection_properties.get('do_local_attach',
self.do_local_attach)
if do_local_attach:
# NOTE(e0ne): sanity check if ceph-common is installed.
cmd = ['which', 'rbd']
try:
self._execute(*cmd)
except putils.ProcessExecutionError:
msg = _("ceph-common package is not installed.")
LOG.error(msg)
raise exception.BrickException(message=msg)
# NOTE(e0ne): map volume to a block device
# via the rbd kernel module.
pool, volume = connection_properties['name'].split('/')
cmd = ['rbd', 'map', volume, '--pool', pool]
self._execute(*cmd, root_helper=self._root_helper,
run_as_root=True)
return {'path': RBDConnector.get_rbd_device_name(pool, volume),
'type': 'block'}
rbd_handle = self._get_rbd_handle(connection_properties)
return {'path': rbd_handle}
@utils.trace
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect a volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
do_local_attach = connection_properties.get('do_local_attach',
self.do_local_attach)
if do_local_attach:
pool, volume = connection_properties['name'].split('/')
dev_name = RBDConnector.get_rbd_device_name(pool, volume)
cmd = ['rbd', 'unmap', dev_name]
self._execute(*cmd, root_helper=self._root_helper,
run_as_root=True)
else:
if device_info:
rbd_handle = device_info.get('path', None)
if rbd_handle is not None:
rbd_handle.close()
def check_valid_device(self, path, run_as_root=True):
"""Verify an existing RBD handle is connected and valid."""
rbd_handle = path
if rbd_handle is None:
return False
original_offset = rbd_handle.tell()
try:
rbd_handle.read(4096)
except Exception as e:
LOG.error(_LE("Failed to access RBD device handle: %(error)s"),
{"error": e})
return False
finally:
rbd_handle.seek(original_offset, 0)
return True
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,119 @@
# 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.
from oslo_log import log as logging
from os_brick.i18n import _LW
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.remotefs import remotefs
from os_brick import utils
LOG = logging.getLogger(__name__)
class RemoteFsConnector(base.BaseLinuxConnector):
"""Connector class to attach/detach NFS and GlusterFS volumes."""
def __init__(self, mount_type, root_helper, driver=None,
execute=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
kwargs = kwargs or {}
conn = kwargs.get('conn')
mount_type_lower = mount_type.lower()
if conn:
mount_point_base = conn.get('mount_point_base')
if mount_type_lower in ('nfs', 'glusterfs', 'scality',
'quobyte', 'vzstorage'):
kwargs[mount_type_lower + '_mount_point_base'] = (
kwargs.get(mount_type_lower + '_mount_point_base') or
mount_point_base)
else:
LOG.warning(_LW("Connection details not present."
" RemoteFsClient may not initialize properly."))
if mount_type_lower == 'scality':
cls = remotefs.ScalityRemoteFsClient
else:
cls = remotefs.RemoteFsClient
self._remotefsclient = cls(mount_type, root_helper, execute=execute,
*args, **kwargs)
super(RemoteFsConnector, self).__init__(
root_helper, driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The RemoteFS connector properties."""
return {}
def set_execute(self, execute):
super(RemoteFsConnector, self).set_execute(execute)
self._remotefsclient.set_execute(execute)
def get_search_path(self):
return self._remotefsclient.get_mount_base()
def _get_volume_path(self, connection_properties):
mnt_flags = []
if connection_properties.get('options'):
mnt_flags = connection_properties['options'].split()
nfs_share = connection_properties['export']
self._remotefsclient.mount(nfs_share, mnt_flags)
mount_point = self._remotefsclient.get_mount_point(nfs_share)
path = mount_point + '/' + connection_properties['name']
return path
def get_volume_paths(self, connection_properties):
path = self._get_volume_path(connection_properties)
return [path]
@utils.trace
def connect_volume(self, connection_properties):
"""Ensure that the filesystem containing the volume is mounted.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
connection_properties must include:
export - remote filesystem device (e.g. '172.18.194.100:/var/nfs')
name - file name within the filesystem
:type connection_properties: dict
:returns: dict
connection_properties may optionally include:
options - options to pass to mount
"""
path = self._get_volume_path(connection_properties)
return {'path': path}
@utils.trace
def disconnect_volume(self, connection_properties, device_info):
"""No need to do anything to disconnect a volume in a filesystem.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,491 @@
# 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 json
import os
import requests
from six.moves import urllib
from oslo_concurrency import lockutils
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from os_brick.i18n import _, _LI, _LW
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick import utils
LOG = logging.getLogger(__name__)
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
synchronized = lockutils.synchronized_with_prefix('os-brick-')
class ScaleIOConnector(base.BaseLinuxConnector):
"""Class implements the connector driver for ScaleIO."""
OK_STATUS_CODE = 200
VOLUME_NOT_MAPPED_ERROR = 84
VOLUME_ALREADY_MAPPED_ERROR = 81
GET_GUID_CMD = ['/opt/emc/scaleio/sdc/bin/drv_cfg', '--query_guid']
def __init__(self, root_helper, driver=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(ScaleIOConnector, self).__init__(
root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs
)
self.local_sdc_ip = None
self.server_ip = None
self.server_port = None
self.server_username = None
self.server_password = None
self.server_token = None
self.volume_id = None
self.volume_name = None
self.volume_path = None
self.iops_limit = None
self.bandwidth_limit = None
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The ScaleIO connector properties."""
return {}
def get_search_path(self):
return "/dev/disk/by-id"
def get_volume_paths(self, connection_properties):
self.get_config(connection_properties)
volume_paths = []
device_paths = [self._find_volume_path()]
for path in device_paths:
if os.path.exists(path):
volume_paths.append(path)
return volume_paths
def _find_volume_path(self):
LOG.info(_LI(
"Looking for volume %(volume_id)s, maximum tries: %(tries)s"),
{'volume_id': self.volume_id, 'tries': self.device_scan_attempts}
)
# look for the volume in /dev/disk/by-id directory
by_id_path = self.get_search_path()
disk_filename = self._wait_for_volume_path(by_id_path)
full_disk_name = ("%(path)s/%(filename)s" %
{'path': by_id_path, 'filename': disk_filename})
LOG.info(_LI("Full disk name is %(full_path)s"),
{'full_path': full_disk_name})
return full_disk_name
# NOTE: Usually 3 retries is enough to find the volume.
# If there are network issues, it could take much longer. Set
# the max retries to 15 to make sure we can find the volume.
@utils.retry(exceptions=exception.BrickException,
retries=15,
backoff_rate=1)
def _wait_for_volume_path(self, path):
if not os.path.isdir(path):
msg = (
_("ScaleIO volume %(volume_id)s not found at "
"expected path.") % {'volume_id': self.volume_id}
)
LOG.debug(msg)
raise exception.BrickException(message=msg)
disk_filename = None
filenames = os.listdir(path)
LOG.info(_LI(
"Files found in %(path)s path: %(files)s "),
{'path': path, 'files': filenames}
)
for filename in filenames:
if (filename.startswith("emc-vol") and
filename.endswith(self.volume_id)):
disk_filename = filename
break
if not disk_filename:
msg = (_("ScaleIO volume %(volume_id)s not found.") %
{'volume_id': self.volume_id})
LOG.debug(msg)
raise exception.BrickException(message=msg)
return disk_filename
def _get_client_id(self):
request = (
"https://%(server_ip)s:%(server_port)s/"
"api/types/Client/instances/getByIp::%(sdc_ip)s/" %
{
'server_ip': self.server_ip,
'server_port': self.server_port,
'sdc_ip': self.local_sdc_ip
}
)
LOG.info(_LI("ScaleIO get client id by ip request: %(request)s"),
{'request': request})
r = requests.get(
request,
auth=(self.server_username, self.server_token),
verify=False
)
r = self._check_response(r, request)
sdc_id = r.json()
if not sdc_id:
msg = (_("Client with ip %(sdc_ip)s was not found.") %
{'sdc_ip': self.local_sdc_ip})
raise exception.BrickException(message=msg)
if r.status_code != 200 and "errorCode" in sdc_id:
msg = (_("Error getting sdc id from ip %(sdc_ip)s: %(err)s") %
{'sdc_ip': self.local_sdc_ip, 'err': sdc_id['message']})
LOG.error(msg)
raise exception.BrickException(message=msg)
LOG.info(_LI("ScaleIO sdc id is %(sdc_id)s."),
{'sdc_id': sdc_id})
return sdc_id
def _get_volume_id(self):
volname_encoded = urllib.parse.quote(self.volume_name, '')
volname_double_encoded = urllib.parse.quote(volname_encoded, '')
LOG.debug(_(
"Volume name after double encoding is %(volume_name)s."),
{'volume_name': volname_double_encoded}
)
request = (
"https://%(server_ip)s:%(server_port)s/api/types/Volume/instances"
"/getByName::%(encoded_volume_name)s" %
{
'server_ip': self.server_ip,
'server_port': self.server_port,
'encoded_volume_name': volname_double_encoded
}
)
LOG.info(
_LI("ScaleIO get volume id by name request: %(request)s"),
{'request': request}
)
r = requests.get(request,
auth=(self.server_username, self.server_token),
verify=False)
r = self._check_response(r, request)
volume_id = r.json()
if not volume_id:
msg = (_("Volume with name %(volume_name)s wasn't found.") %
{'volume_name': self.volume_name})
LOG.error(msg)
raise exception.BrickException(message=msg)
if r.status_code != self.OK_STATUS_CODE and "errorCode" in volume_id:
msg = (
_("Error getting volume id from name %(volume_name)s: "
"%(err)s") %
{'volume_name': self.volume_name, 'err': volume_id['message']}
)
LOG.error(msg)
raise exception.BrickException(message=msg)
LOG.info(_LI("ScaleIO volume id is %(volume_id)s."),
{'volume_id': volume_id})
return volume_id
def _check_response(self, response, request, is_get_request=True,
params=None):
if response.status_code == 401 or response.status_code == 403:
LOG.info(_LI("Token is invalid, "
"going to re-login to get a new one"))
login_request = (
"https://%(server_ip)s:%(server_port)s/api/login" %
{'server_ip': self.server_ip, 'server_port': self.server_port}
)
r = requests.get(
login_request,
auth=(self.server_username, self.server_password),
verify=False
)
token = r.json()
# repeat request with valid token
LOG.debug(_("Going to perform request %(request)s again "
"with valid token"), {'request': request})
if is_get_request:
res = requests.get(request,
auth=(self.server_username, token),
verify=False)
else:
headers = {'content-type': 'application/json'}
res = requests.post(
request,
data=json.dumps(params),
headers=headers,
auth=(self.server_username, token),
verify=False
)
self.server_token = token
return res
return response
def get_config(self, connection_properties):
self.local_sdc_ip = connection_properties['hostIP']
self.volume_name = connection_properties['scaleIO_volname']
self.volume_id = connection_properties['scaleIO_volume_id']
self.server_ip = connection_properties['serverIP']
self.server_port = connection_properties['serverPort']
self.server_username = connection_properties['serverUsername']
self.server_password = connection_properties['serverPassword']
self.server_token = connection_properties['serverToken']
self.iops_limit = connection_properties['iopsLimit']
self.bandwidth_limit = connection_properties['bandwidthLimit']
device_info = {'type': 'block',
'path': self.volume_path}
return device_info
@utils.trace
@lockutils.synchronized('scaleio', 'scaleio-')
def connect_volume(self, connection_properties):
"""Connect the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
"""
device_info = self.get_config(connection_properties)
LOG.debug(
_(
"scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, "
"REST Server IP: %(server_ip)s, "
"REST Server username: %(username)s, "
"iops limit:%(iops_limit)s, "
"bandwidth limit: %(bandwidth_limit)s."
), {
'volume_name': self.volume_name,
'volume_id': self.volume_id,
'sdc_ip': self.local_sdc_ip,
'server_ip': self.server_ip,
'username': self.server_username,
'iops_limit': self.iops_limit,
'bandwidth_limit': self.bandwidth_limit
}
)
LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"),
{'cmd': self.GET_GUID_CMD})
try:
(out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True,
root_helper=self._root_helper)
LOG.info(_LI("Map volume %(cmd)s: stdout=%(out)s "
"stderr=%(err)s"),
{'cmd': self.GET_GUID_CMD, 'out': out, 'err': err})
except putils.ProcessExecutionError as e:
msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr})
LOG.error(msg)
raise exception.BrickException(message=msg)
guid = out
LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid})
params = {'guid': guid, 'allowMultipleMappings': 'TRUE'}
self.volume_id = self.volume_id or self._get_volume_id()
headers = {'content-type': 'application/json'}
request = (
"https://%(server_ip)s:%(server_port)s/api/instances/"
"Volume::%(volume_id)s/action/addMappedSdc" %
{'server_ip': self.server_ip, 'server_port': self.server_port,
'volume_id': self.volume_id}
)
LOG.info(_LI("map volume request: %(request)s"), {'request': request})
r = requests.post(
request,
data=json.dumps(params),
headers=headers,
auth=(self.server_username, self.server_token),
verify=False
)
r = self._check_response(r, request, False, params)
if r.status_code != self.OK_STATUS_CODE:
response = r.json()
error_code = response['errorCode']
if error_code == self.VOLUME_ALREADY_MAPPED_ERROR:
LOG.warning(_LW(
"Ignoring error mapping volume %(volume_name)s: "
"volume already mapped."),
{'volume_name': self.volume_name}
)
else:
msg = (
_("Error mapping volume %(volume_name)s: %(err)s") %
{'volume_name': self.volume_name,
'err': response['message']}
)
LOG.error(msg)
raise exception.BrickException(message=msg)
self.volume_path = self._find_volume_path()
device_info['path'] = self.volume_path
# Set QoS settings after map was performed
if self.iops_limit is not None or self.bandwidth_limit is not None:
params = {'guid': guid}
if self.bandwidth_limit is not None:
params['bandwidthLimitInKbps'] = self.bandwidth_limit
if self.iops_limit is not None:
params['iopsLimit'] = self.iops_limit
request = (
"https://%(server_ip)s:%(server_port)s/api/instances/"
"Volume::%(volume_id)s/action/setMappedSdcLimits" %
{'server_ip': self.server_ip, 'server_port': self.server_port,
'volume_id': self.volume_id}
)
LOG.info(_LI("Set client limit request: %(request)s"),
{'request': request})
r = requests.post(
request,
data=json.dumps(params),
headers=headers,
auth=(self.server_username, self.server_token),
verify=False
)
r = self._check_response(r, request, False, params)
if r.status_code != self.OK_STATUS_CODE:
response = r.json()
LOG.info(_LI("Set client limit response: %(response)s"),
{'response': response})
msg = (
_("Error setting client limits for volume "
"%(volume_name)s: %(err)s") %
{'volume_name': self.volume_name,
'err': response['message']}
)
LOG.error(msg)
return device_info
@utils.trace
@lockutils.synchronized('scaleio', 'scaleio-')
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect the ScaleIO volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
self.get_config(connection_properties)
self.volume_id = self.volume_id or self._get_volume_id()
LOG.info(_LI(
"ScaleIO disconnect volume in ScaleIO brick volume driver."
))
LOG.debug(
_("ScaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, "
"REST Server IP: %(server_ip)s"),
{'volume_name': self.volume_name, 'sdc_ip': self.local_sdc_ip,
'server_ip': self.server_ip}
)
LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"),
{'cmd': self.GET_GUID_CMD})
try:
(out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True,
root_helper=self._root_helper)
LOG.info(
_LI("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s"),
{'cmd': self.GET_GUID_CMD, 'out': out, 'err': err}
)
except putils.ProcessExecutionError as e:
msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr}
LOG.error(msg)
raise exception.BrickException(message=msg)
guid = out
LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid})
params = {'guid': guid}
headers = {'content-type': 'application/json'}
request = (
"https://%(server_ip)s:%(server_port)s/api/instances/"
"Volume::%(volume_id)s/action/removeMappedSdc" %
{'server_ip': self.server_ip, 'server_port': self.server_port,
'volume_id': self.volume_id}
)
LOG.info(_LI("Unmap volume request: %(request)s"),
{'request': request})
r = requests.post(
request,
data=json.dumps(params),
headers=headers,
auth=(self.server_username, self.server_token),
verify=False
)
r = self._check_response(r, request, False, params)
if r.status_code != self.OK_STATUS_CODE:
response = r.json()
error_code = response['errorCode']
if error_code == self.VOLUME_NOT_MAPPED_ERROR:
LOG.warning(_LW(
"Ignoring error unmapping volume %(volume_id)s: "
"volume not mapped."), {'volume_id': self.volume_name}
)
else:
msg = (_("Error unmapping volume %(volume_id)s: %(err)s") %
{'volume_id': self.volume_name,
'err': response['message']})
LOG.error(msg)
raise exception.BrickException(message=msg)
def extend_volume(self, connection_properties):
# TODO(walter-boring): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,126 @@
# 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.
from oslo_log import log as logging
from os_brick.i18n import _, _LE
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.initiator import linuxsheepdog
from os_brick import utils
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
LOG = logging.getLogger(__name__)
class SheepdogConnector(base.BaseLinuxConnector):
""""Connector class to attach/detach sheepdog volumes."""
def __init__(self, root_helper, driver=None, use_multipath=False,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(SheepdogConnector, self).__init__(root_helper, driver=driver,
device_scan_attempts=
device_scan_attempts,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The Sheepdog connector properties."""
return {}
def get_volume_paths(self, connection_properties):
# TODO(lixiaoy1): don't know where the connector
# looks for sheepdog volumes.
return []
def get_search_path(self):
# TODO(lixiaoy1): don't know where the connector
# looks for sheepdog volumes.
return None
def get_all_available_volumes(self, connection_properties=None):
# TODO(lixiaoy1): not sure what to return here for sheepdog
return []
def _get_sheepdog_handle(self, connection_properties):
try:
host = connection_properties['hosts'][0]
name = connection_properties['name']
port = connection_properties['ports'][0]
except IndexError:
msg = _("Connect volume failed, malformed connection properties")
raise exception.BrickException(msg=msg)
sheepdog_handle = linuxsheepdog.SheepdogVolumeIOWrapper(
host, port, name)
return sheepdog_handle
@utils.trace
def connect_volume(self, connection_properties):
"""Connect to a volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
"""
sheepdog_handle = self._get_sheepdog_handle(connection_properties)
return {'path': sheepdog_handle}
@utils.trace
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect a volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
if device_info:
sheepdog_handle = device_info.get('path', None)
self.check_IO_handle_valid(sheepdog_handle,
linuxsheepdog.SheepdogVolumeIOWrapper,
'Sheepdog')
if sheepdog_handle is not None:
sheepdog_handle.close()
def check_valid_device(self, path, run_as_root=True):
"""Verify an existing sheepdog handle is connected and valid."""
sheepdog_handle = path
if sheepdog_handle is None:
return False
original_offset = sheepdog_handle.tell()
try:
sheepdog_handle.read(4096)
except Exception as e:
LOG.error(_LE("Failed to access sheepdog device "
"handle: %(error)s"),
{"error": e})
return False
finally:
sheepdog_handle.seek(original_offset, 0)
return True
def extend_volume(self, connection_properties):
# TODO(lixiaoy1): is this possible?
raise NotImplementedError

View File

@ -0,0 +1,193 @@
# 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 abc
from oslo_log import log as logging
import six
from os_brick import exception
from os_brick import executor
from os_brick import initiator
LOG = logging.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class InitiatorConnector(executor.Executor):
# This object can be used on any platform (x86, S390)
platform = initiator.PLATFORM_ALL
# This object can be used on any os type (linux, windows)
os_type = initiator.OS_TYPE_ALL
def __init__(self, root_helper, driver=None, execute=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(InitiatorConnector, self).__init__(root_helper, execute=execute,
*args, **kwargs)
self.device_scan_attempts = device_scan_attempts
def set_driver(self, driver):
"""The driver is used to find used LUNs."""
self.driver = driver
@abc.abstractmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The generic connector properties."""
pass
@abc.abstractmethod
def check_valid_device(self, path, run_as_root=True):
"""Test to see if the device path is a real device.
:param path: The file system path for the device.
:type path: str
:param run_as_root: run the tests as root user?
:type run_as_root: bool
:returns: bool
"""
pass
@abc.abstractmethod
def connect_volume(self, connection_properties):
"""Connect to a volume.
The connection_properties describes the information needed by
the specific protocol to use to make the connection.
The connection_properties is a dictionary that describes the target
volume. It varies slightly by protocol type (iscsi, fibre_channel),
but the structure is usually the same.
An example for iSCSI:
{'driver_volume_type': 'iscsi',
'data': {
'target_luns': [0, 2],
'target_iqns': ['iqn.2000-05.com.3pardata:20810002ac00383d',
'iqn.2000-05.com.3pardata:21810002ac00383d'],
'target_discovered': True,
'encrypted': False,
'qos_specs': None,
'target_portals': ['10.52.1.11:3260', '10.52.2.11:3260'],
'access_mode': 'rw',
}}
An example for fibre_channel:
{'driver_volume_type': 'fibre_channel',
'data': {
'initiator_target_map': {'100010604b010459': ['21230002AC00383D'],
'100010604b01045d': ['21230002AC00383D']
},
'target_discovered': True,
'encrypted': False,
'qos_specs': None,
'target_lun': 1,
'access_mode': 'rw',
'target_wwn': [
'20210002AC00383D',
'20220002AC00383D',
],
}}
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:returns: dict
"""
pass
@abc.abstractmethod
def disconnect_volume(self, connection_properties, device_info):
"""Disconnect a volume from the local host.
The connection_properties are the same as from connect_volume.
The device_info is returned from connect_volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
pass
@abc.abstractmethod
def get_volume_paths(self, connection_properties):
"""Return the list of existing paths for a volume.
The job of this method is to find out what paths in
the system are associated with a volume as described
by the connection_properties.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
"""
pass
@abc.abstractmethod
def get_search_path(self):
"""Return the directory where a Connector looks for volumes.
Some Connectors need the information in the
connection_properties to determine the search path.
"""
pass
@abc.abstractmethod
def extend_volume(self, connection_properties):
"""Update the attached volume's size.
This method will attempt to update the local hosts's
volume after the volume has been extended on the remote
system. The new volume size in bytes will be returned.
If there is a failure to update, then None will be returned.
:param connection_properties: The volume connection properties.
:returns: new size of the volume.
"""
pass
@abc.abstractmethod
def get_all_available_volumes(self, connection_properties=None):
"""Return all volumes that exist in the search directory.
At connect_volume time, a Connector looks in a specific
directory to discover a volume's paths showing up.
This method's job is to return all paths in the directory
that connect_volume uses to find a volume.
This method is used in coordination with get_volume_paths()
to verify that volumes have gone away after disconnect_volume
has been called.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
"""
pass
def check_IO_handle_valid(self, handle, data_type, protocol):
"""Check IO handle has correct data type."""
if (handle and not isinstance(handle, data_type)):
raise exception.InvalidIOHandleObject(
protocol=protocol,
actual_type=type(handle))

View File

@ -17,16 +17,17 @@ from os_win import utilsfactory
from oslo_log import log as logging
from os_brick import exception
from os_brick import initiator
from os_brick.i18n import _, _LE
from os_brick.initiator import connector
from os_brick.initiator import initiator_connector
from os_brick import utils
LOG = logging.getLogger(__name__)
class BaseWindowsConnector(connector.InitiatorConnector):
platform = connector.PLATFORM_ALL
os_type = connector.OS_TYPE_WINDOWS
class BaseWindowsConnector(initiator_connector.InitiatorConnector):
platform = initiator.PLATFORM_ALL
os_type = initiator.OS_TYPE_WINDOWS
def __init__(self, root_helper=None, *args, **kwargs):
super(BaseWindowsConnector, self).__init__(root_helper,

View File

@ -19,7 +19,7 @@ from oslo_log import log as logging
from os_brick import exception
from os_brick.i18n import _, _LE, _LI, _LW
from os_brick.initiator import connector
from os_brick.initiator.connectors import base_iscsi
from os_brick.initiator.windows import base as win_conn_base
from os_brick import utils
@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__)
class WindowsISCSIConnector(win_conn_base.BaseWindowsConnector,
connector.BaseISCSIConnector):
base_iscsi.BaseISCSIConnector):
def __init__(self, *args, **kwargs):
super(WindowsISCSIConnector, self).__init__(*args, **kwargs)
self.use_multipath = kwargs.pop('use_multipath', False)

View File

@ -0,0 +1,129 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
import os
from oslo_service import loopingcall
from os_brick import exception
from os_brick.initiator.connectors import aoe
from os_brick.tests.initiator import test_connector
class FakeFixedIntervalLoopingCall(object):
def __init__(self, f=None, *args, **kw):
self.args = args
self.kw = kw
self.f = f
self._stop = False
def stop(self):
self._stop = True
def wait(self):
return self
def start(self, interval, initial_delay=None):
while not self._stop:
try:
self.f(*self.args, **self.kw)
except loopingcall.LoopingCallDone:
return self
except Exception:
raise
class AoEConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for AoE initiator class."""
def setUp(self):
super(AoEConnectorTestCase, self).setUp()
self.connector = aoe.AoEConnector('sudo')
self.connection_properties = {'target_shelf': 'fake_shelf',
'target_lun': 'fake_lun'}
mock.patch.object(loopingcall, 'FixedIntervalLoopingCall',
FakeFixedIntervalLoopingCall).start()
self.addCleanup(mock.patch.stopall)
def test_get_search_path(self):
expected = "/dev/etherd"
actual_path = self.connector.get_search_path()
self.assertEqual(expected, actual_path)
@mock.patch.object(os.path, 'exists', return_value=True)
def test_get_volume_paths(self, mock_exists):
expected = ["/dev/etherd/efake_shelf.fake_lun"]
paths = self.connector.get_volume_paths(self.connection_properties)
self.assertEqual(expected, paths)
def test_get_connector_properties(self):
props = aoe.AoEConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
@mock.patch.object(os.path, 'exists', side_effect=[True, True])
def test_connect_volume(self, exists_mock):
"""Ensure that if path exist aoe-revalidate was called."""
aoe_device, aoe_path = self.connector._get_aoe_info(
self.connection_properties)
with mock.patch.object(self.connector, '_execute',
return_value=["", ""]):
self.connector.connect_volume(self.connection_properties)
@mock.patch.object(os.path, 'exists', side_effect=[False, True])
def test_connect_volume_without_path(self, exists_mock):
"""Ensure that if path doesn't exist aoe-discovery was called."""
aoe_device, aoe_path = self.connector._get_aoe_info(
self.connection_properties)
expected_info = {
'type': 'block',
'device': aoe_device,
'path': aoe_path,
}
with mock.patch.object(self.connector, '_execute',
return_value=["", ""]):
volume_info = self.connector.connect_volume(
self.connection_properties)
self.assertDictMatch(volume_info, expected_info)
@mock.patch.object(os.path, 'exists', return_value=False)
def test_connect_volume_could_not_discover_path(self, exists_mock):
_aoe_device, aoe_path = self.connector._get_aoe_info(
self.connection_properties)
with mock.patch.object(self.connector, '_execute',
return_value=["", ""]):
self.assertRaises(exception.VolumeDeviceNotFound,
self.connector.connect_volume,
self.connection_properties)
@mock.patch.object(os.path, 'exists', return_value=True)
def test_disconnect_volume(self, mock_exists):
"""Ensure that if path exist aoe-revaliadte was called."""
aoe_device, aoe_path = self.connector._get_aoe_info(
self.connection_properties)
with mock.patch.object(self.connector, '_execute',
return_value=["", ""]):
self.connector.disconnect_volume(self.connection_properties, {})
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)

View File

@ -0,0 +1,77 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
from os_brick.initiator.connectors import base_iscsi
from os_brick.initiator.connectors import fake
from os_brick.tests import base as test_base
class BaseISCSIConnectorTestCase(test_base.TestCase):
def setUp(self):
super(BaseISCSIConnectorTestCase, self).setUp()
self.connector = fake.FakeBaseISCSIConnector(None)
@mock.patch.object(base_iscsi.BaseISCSIConnector, '_get_all_targets')
def test_iterate_all_targets(self, mock_get_all_targets):
# extra_property cannot be a sentinel, a copied sentinel will not
# identical to the original one.
connection_properties = {
'target_portals': mock.sentinel.target_portals,
'target_iqns': mock.sentinel.target_iqns,
'target_luns': mock.sentinel.target_luns,
'extra_property': 'extra_property'}
mock_get_all_targets.return_value = [(
mock.sentinel.portal, mock.sentinel.iqn, mock.sentinel.lun)]
# method is a generator, and it yields dictionaries. list() will
# iterate over all of the method's items.
list_props = list(
self.connector._iterate_all_targets(connection_properties))
mock_get_all_targets.assert_called_once_with(connection_properties)
self.assertEqual(1, len(list_props))
expected_props = {'target_portal': mock.sentinel.portal,
'target_iqn': mock.sentinel.iqn,
'target_lun': mock.sentinel.lun,
'extra_property': 'extra_property'}
self.assertDictEqual(expected_props, list_props[0])
def test_get_all_targets(self):
connection_properties = {
'target_portals': [mock.sentinel.target_portals],
'target_iqns': [mock.sentinel.target_iqns],
'target_luns': [mock.sentinel.target_luns]}
all_targets = self.connector._get_all_targets(connection_properties)
expected_targets = zip([mock.sentinel.target_portals],
[mock.sentinel.target_iqns],
[mock.sentinel.target_luns])
self.assertEqual(list(expected_targets), list(all_targets))
def test_get_all_targets_single_target(self):
connection_properties = {
'target_portal': mock.sentinel.target_portal,
'target_iqn': mock.sentinel.target_iqn,
'target_lun': mock.sentinel.target_lun}
all_targets = self.connector._get_all_targets(connection_properties)
expected_target = (mock.sentinel.target_portal,
mock.sentinel.target_iqn,
mock.sentinel.target_lun)
self.assertEqual([expected_target], all_targets)

View File

@ -0,0 +1,156 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 glob
import mock
import os
from os_brick import exception
from os_brick.initiator.connectors import disco
from os_brick.tests.initiator import test_connector
class DISCOConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for DISCO connector."""
# Fake volume information
volume = {
'name': 'a-disco-volume',
'disco_id': '1234567'
}
# Conf for test
conf = {
'ip': test_connector.MY_IP,
'port': 9898
}
def setUp(self):
super(DISCOConnectorTestCase, self).setUp()
self.fake_connection_properties = {
'name': self.volume['name'],
'disco_id': self.volume['disco_id'],
'conf': {
'server_ip': self.conf['ip'],
'server_port': self.conf['port']}
}
self.fake_volume_status = {'attached': True,
'detached': False}
self.fake_request_status = {'success': None,
'fail': 'ERROR'}
self.volume_status = 'detached'
self.request_status = 'success'
# Patch the request and os calls to fake versions
mock.patch.object(disco.DISCOConnector,
'_send_disco_vol_cmd',
self.perform_disco_request).start()
mock.patch.object(os.path,
'exists', self.is_volume_attached).start()
mock.patch.object(glob,
'glob', self.list_disco_volume).start()
self.addCleanup(mock.patch.stopall)
# The actual DISCO connector
self.connector = disco.DISCOConnector(
'sudo', execute=self.fake_execute)
def perform_disco_request(self, *cmd, **kwargs):
"""Fake the socket call."""
return self.fake_request_status[self.request_status]
def is_volume_attached(self, *cmd, **kwargs):
"""Fake volume detection check."""
return self.fake_volume_status[self.volume_status]
def list_disco_volume(self, *cmd, **kwargs):
"""Fake the glob call."""
path_dir = self.connector.get_search_path()
volume_id = self.volume['disco_id']
volume_items = [path_dir, '/', self.connector.DISCO_PREFIX, volume_id]
volume_path = ''.join(volume_items)
return [volume_path]
def test_get_connector_properties(self):
props = disco.DISCOConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_get_search_path(self):
"""DISCO volumes should be under /dev."""
expected = "/dev"
actual = self.connector.get_search_path()
self.assertEqual(expected, actual)
def test_get_volume_paths(self):
"""Test to get all the path for a specific volume."""
expected = ['/dev/dms1234567']
self.volume_status = 'attached'
actual = self.connector.get_volume_paths(
self.fake_connection_properties)
self.assertEqual(expected, actual)
def test_connect_volume(self):
"""Attach a volume."""
self.connector.connect_volume(self.fake_connection_properties)
def test_connect_volume_already_attached(self):
"""Make sure that we don't issue the request."""
self.request_status = 'fail'
self.volume_status = 'attached'
self.test_connect_volume()
def test_connect_volume_request_fail(self):
"""Fail the attach request."""
self.volume_status = 'detached'
self.request_status = 'fail'
self.assertRaises(exception.BrickException,
self.test_connect_volume)
def test_disconnect_volume(self):
"""Detach a volume."""
self.connector.disconnect_volume(self.fake_connection_properties, None)
def test_disconnect_volume_attached(self):
"""Detach a volume attached."""
self.request_status = 'success'
self.volume_status = 'attached'
self.test_disconnect_volume()
def test_disconnect_volume_already_detached(self):
"""Ensure that we don't issue the request."""
self.request_status = 'fail'
self.volume_status = 'detached'
self.test_disconnect_volume()
def test_disconnect_volume_request_fail(self):
"""Fail the detach request."""
self.volume_status = 'attached'
self.request_status = 'fail'
self.assertRaises(exception.BrickException,
self.test_disconnect_volume)
def test_get_all_available_volumes(self):
"""Test to get all the available DISCO volumes."""
expected = ['/dev/dms1234567']
actual = self.connector.get_all_available_volumes(None)
self.assertItemsEqual(expected, actual)
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.fake_connection_properties)

View File

@ -0,0 +1,89 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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.
from os_brick.initiator.connectors import drbd
from os_brick.tests.initiator import test_connector
class DRBDConnectorTestCase(test_connector.ConnectorTestCase):
RESOURCE_TEMPLATE = '''
resource r0 {
on host1 {
}
net {
shared-secret "%(shared-secret)s";
}
}
'''
def setUp(self):
super(DRBDConnectorTestCase, self).setUp()
self.connector = drbd.DRBDConnector(
None, execute=self._fake_exec)
self.execs = []
def _fake_exec(self, *cmd, **kwargs):
self.execs.append(cmd)
# out, err
return ('', '')
def test_get_connector_properties(self):
props = drbd.DRBDConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_connect_volume(self):
"""Test connect_volume."""
cprop = {
'provider_auth': 'my-secret',
'config': self.RESOURCE_TEMPLATE,
'name': 'my-precious',
'device': '/dev/drbd951722',
'data': {},
}
res = self.connector.connect_volume(cprop)
self.assertEqual(cprop['device'], res['path'])
self.assertEqual('adjust', self.execs[0][1])
self.assertEqual(cprop['name'], self.execs[0][4])
def test_disconnect_volume(self):
"""Test the disconnect volume case."""
cprop = {
'provider_auth': 'my-secret',
'config': self.RESOURCE_TEMPLATE,
'name': 'my-precious',
'device': '/dev/drbd951722',
'data': {},
}
dev_info = {}
self.connector.disconnect_volume(cprop, dev_info)
self.assertEqual('down', self.execs[0][1])
def test_extend_volume(self):
cprop = {'name': 'something'}
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
cprop)

View File

@ -0,0 +1,396 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
import os
import six
from os_brick import exception
from os_brick.initiator.connectors import base
from os_brick.initiator.connectors import fibre_channel
from os_brick.initiator import linuxfc
from os_brick.initiator import linuxscsi
from os_brick.tests.initiator import test_connector
class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
def setUp(self):
super(FibreChannelConnectorTestCase, self).setUp()
self.connector = fibre_channel.FibreChannelConnector(
None, execute=self.fake_execute, use_multipath=False)
self.assertIsNotNone(self.connector)
self.assertIsNotNone(self.connector._linuxfc)
self.assertIsNotNone(self.connector._linuxscsi)
def fake_get_fc_hbas(self):
return [{'ClassDevice': 'host1',
'ClassDevicePath': '/sys/devices/pci0000:00/0000:00:03.0'
'/0000:05:00.2/host1/fc_host/host1',
'dev_loss_tmo': '30',
'fabric_name': '0x1000000533f55566',
'issue_lip': '<store method only>',
'max_npiv_vports': '255',
'maxframe_size': '2048 bytes',
'node_name': '0x200010604b019419',
'npiv_vports_inuse': '0',
'port_id': '0x680409',
'port_name': '0x100010604b019419',
'port_state': 'Online',
'port_type': 'NPort (fabric via point-to-point)',
'speed': '10 Gbit',
'supported_classes': 'Class 3',
'supported_speeds': '10 Gbit',
'symbolic_name': 'Emulex 554M FV4.0.493.0 DV8.3.27',
'tgtid_bind_type': 'wwpn (World Wide Port Name)',
'uevent': None,
'vport_create': '<store method only>',
'vport_delete': '<store method only>'}]
def fake_get_fc_hbas_info(self):
hbas = self.fake_get_fc_hbas()
info = [{'port_name': hbas[0]['port_name'].replace('0x', ''),
'node_name': hbas[0]['node_name'].replace('0x', ''),
'host_device': hbas[0]['ClassDevice'],
'device_path': hbas[0]['ClassDevicePath']}]
return info
def fibrechan_connection(self, volume, location, wwn):
return {'driver_volume_type': 'fibrechan',
'data': {
'volume_id': volume['id'],
'target_portal': location,
'target_wwn': wwn,
'target_lun': 1,
}}
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
def test_get_connector_properties(self, mock_hbas):
mock_hbas.return_value = self.fake_get_fc_hbas()
multipath = True
enforce_multipath = True
props = fibre_channel.FibreChannelConnector.get_connector_properties(
'sudo', multipath=multipath,
enforce_multipath=enforce_multipath)
hbas = self.fake_get_fc_hbas()
expected_props = {'wwpns': [hbas[0]['port_name'].replace('0x', '')],
'wwnns': [hbas[0]['node_name'].replace('0x', '')]}
self.assertEqual(expected_props, props)
def test_get_search_path(self):
search_path = self.connector.get_search_path()
expected = "/dev/disk/by-path"
self.assertEqual(expected, search_path)
def test_get_pci_num(self):
hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0"
"/0000:05:00.3/host2/fc_host/host2"}
pci_num = self.connector._get_pci_num(hba)
self.assertEqual("0000:05:00.3", pci_num)
hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0"
"/0000:05:00.3/0000:06:00.6/host2/fc_host/host2"}
pci_num = self.connector._get_pci_num(hba)
self.assertEqual("0000:06:00.6", pci_num)
hba = {'device_path': "/sys/devices/pci0000:20/0000:20:03.0"
"/0000:21:00.2/net/ens2f2/ctlr_2/host3"
"/fc_host/host3"}
pci_num = self.connector._get_pci_num(hba)
self.assertEqual("0000:21:00.2", pci_num)
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
def test_get_volume_paths(self, fake_fc_hbas_info,
fake_fc_hbas, fake_exists):
fake_fc_hbas.side_effect = self.fake_get_fc_hbas
fake_fc_hbas_info.side_effect = self.fake_get_fc_hbas_info
name = 'volume-00000001'
vol = {'id': 1, 'name': name}
location = '10.0.2.15:3260'
wwn = '1234567890123456'
connection_info = self.fibrechan_connection(vol, location, wwn)
volume_paths = self.connector.get_volume_paths(
connection_info['data'])
expected = ['/dev/disk/by-path/pci-0000:05:00.2'
'-fc-0x1234567890123456-lun-1']
self.assertEqual(expected, volume_paths)
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_connect_volume(self, get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock):
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
wwn = '1234567890'
multipath_devname = '/dev/md-1'
devices = {"device": multipath_devname,
"id": wwn,
"devices": [{'device': '/dev/sdb',
'address': '1:0:0:1',
'host': 1, 'channel': 0,
'id': 0, 'lun': 1}]}
get_device_info_mock.return_value = devices['devices'][0]
get_scsi_wwn_mock.return_value = wwn
location = '10.0.2.15:3260'
name = 'volume-00000001'
vol = {'id': 1, 'name': name}
# Should work for string, unicode, and list
wwns = ['1234567890123456', six.text_type('1234567890123456'),
['1234567890123456', '1234567890123457']]
for wwn in wwns:
connection_info = self.fibrechan_connection(vol, location, wwn)
dev_info = self.connector.connect_volume(connection_info['data'])
exp_wwn = wwn[0] if isinstance(wwn, list) else wwn
dev_str = ('/dev/disk/by-path/pci-0000:05:00.2-fc-0x%s-lun-1' %
exp_wwn)
self.assertEqual(dev_info['type'], 'block')
self.assertEqual(dev_info['path'], dev_str)
self.assertTrue('multipath_id' not in dev_info)
self.assertTrue('devices' not in dev_info)
self.connector.disconnect_volume(connection_info['data'], dev_info)
expected_commands = []
self.assertEqual(expected_commands, self.cmds)
# Should not work for anything other than string, unicode, and list
connection_info = self.fibrechan_connection(vol, location, 123)
self.assertRaises(exception.NoFibreChannelHostsFound,
self.connector.connect_volume,
connection_info['data'])
get_fc_hbas_mock.side_effect = [[]]
get_fc_hbas_info_mock.side_effect = [[]]
self.assertRaises(exception.NoFibreChannelHostsFound,
self.connector.connect_volume,
connection_info['data'])
def _test_connect_volume_multipath(self, get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock,
access_mode,
should_wait_for_rw):
self.connector.use_multipath = True
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
wwn = '1234567890'
multipath_devname = '/dev/md-1'
devices = {"device": multipath_devname,
"id": wwn,
"devices": [{'device': '/dev/sdb',
'address': '1:0:0:1',
'host': 1, 'channel': 0,
'id': 0, 'lun': 1}]}
get_device_info_mock.return_value = devices['devices'][0]
get_scsi_wwn_mock.return_value = wwn
location = '10.0.2.15:3260'
name = 'volume-00000001'
vol = {'id': 1, 'name': name}
initiator_wwn = ['1234567890123456', '1234567890123457']
find_mp_dev_mock.return_value = '/dev/disk/by-id/dm-uuid-mpath-' + wwn
connection_info = self.fibrechan_connection(vol, location,
initiator_wwn)
connection_info['data']['access_mode'] = access_mode
self.connector.connect_volume(connection_info['data'])
self.assertEqual(should_wait_for_rw, wait_for_rw_mock.called)
return connection_info
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_connect_volume_multipath_rw(self, get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock):
self._test_connect_volume_multipath(get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock,
'rw',
True)
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_connect_volume_multipath_no_access_mode(self,
get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock):
self._test_connect_volume_multipath(get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock,
None,
True)
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_connect_volume_multipath_ro(self, get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock):
self._test_connect_volume_multipath(get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock,
'ro',
False)
@mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
def test_connect_volume_multipath_not_found(self,
get_device_info_mock,
get_scsi_wwn_mock,
remove_device_mock,
get_fc_hbas_info_mock,
get_fc_hbas_mock,
realpath_mock,
exists_mock,
wait_for_rw_mock,
find_mp_dev_mock,
discover_mp_dev_mock):
discover_mp_dev_mock.return_value = ("/dev/disk/by-path/something",
None)
connection_info = self._test_connect_volume_multipath(
get_device_info_mock, get_scsi_wwn_mock, remove_device_mock,
get_fc_hbas_info_mock, get_fc_hbas_mock, realpath_mock,
exists_mock, wait_for_rw_mock, find_mp_dev_mock,
'rw', False)
self.assertNotIn('multipathd_id', connection_info['data'])
@mock.patch.object(fibre_channel.FibreChannelConnector, 'get_volume_paths')
def test_extend_volume_no_path(self, mock_volume_paths):
mock_volume_paths.return_value = []
volume = {'id': 'fake_uuid'}
wwn = '1234567890123456'
connection_info = self.fibrechan_connection(volume,
"10.0.2.15:3260",
wwn)
self.assertRaises(exception.VolumePathsNotFound,
self.connector.extend_volume,
connection_info['data'])
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
@mock.patch.object(fibre_channel.FibreChannelConnector, 'get_volume_paths')
def test_extend_volume(self, mock_volume_paths, mock_scsi_extend):
fake_new_size = 1024
mock_volume_paths.return_value = ['/dev/vdx']
mock_scsi_extend.return_value = fake_new_size
volume = {'id': 'fake_uuid'}
wwn = '1234567890123456'
connection_info = self.fibrechan_connection(volume,
"10.0.2.15:3260",
wwn)
new_size = self.connector.extend_volume(connection_info['data'])
self.assertEqual(fake_new_size, new_size)
@mock.patch.object(os.path, 'isdir')
def test_get_all_available_volumes_path_not_dir(self, mock_isdir):
mock_isdir.return_value = False
expected = []
actual = self.connector.get_all_available_volumes()
self.assertItemsEqual(expected, actual)

View File

@ -0,0 +1,71 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
from os_brick.initiator.connectors import fibre_channel_s390x
from os_brick.initiator import linuxfc
from os_brick.tests.initiator import test_connector
class FibreChannelConnectorS390XTestCase(test_connector.ConnectorTestCase):
def setUp(self):
super(FibreChannelConnectorS390XTestCase, self).setUp()
self.connector = fibre_channel_s390x.FibreChannelConnectorS390X(
None, execute=self.fake_execute, use_multipath=False)
self.assertIsNotNone(self.connector)
self.assertIsNotNone(self.connector._linuxfc)
self.assertEqual(self.connector._linuxfc.__class__.__name__,
"LinuxFibreChannelS390X")
self.assertIsNotNone(self.connector._linuxscsi)
@mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'configure_scsi_device')
def test_get_host_devices(self, mock_configure_scsi_device):
lun = 2
possible_devs = [(3, 5), ]
devices = self.connector._get_host_devices(possible_devs, lun)
mock_configure_scsi_device.assert_called_with(3, 5,
"0x0002000000000000")
self.assertEqual(1, len(devices))
device_path = "/dev/disk/by-path/ccw-3-zfcp-5:0x0002000000000000"
self.assertEqual(devices[0], device_path)
def test_get_lun_string(self):
lun = 1
lunstring = self.connector._get_lun_string(lun)
self.assertEqual(lunstring, "0x0001000000000000")
lun = 0xff
lunstring = self.connector._get_lun_string(lun)
self.assertEqual(lunstring, "0x00ff000000000000")
lun = 0x101
lunstring = self.connector._get_lun_string(lun)
self.assertEqual(lunstring, "0x0101000000000000")
lun = 0x4020400a
lunstring = self.connector._get_lun_string(lun)
self.assertEqual(lunstring, "0x4020400a00000000")
@mock.patch.object(fibre_channel_s390x.FibreChannelConnectorS390X,
'_get_possible_devices', return_value=[(3, 5), ])
@mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'get_fc_hbas_info',
return_value=[])
@mock.patch.object(linuxfc.LinuxFibreChannelS390X,
'deconfigure_scsi_device')
def test_remove_devices(self, mock_deconfigure_scsi_device,
mock_get_fc_hbas_info, mock_get_possible_devices):
connection_properties = {'target_wwn': 5, 'target_lun': 2}
self.connector._remove_devices(connection_properties, devices=None)
mock_deconfigure_scsi_device.assert_called_with(3, 5,
"0x0002000000000000")
mock_get_fc_hbas_info.assert_called_once_with()
mock_get_possible_devices.assert_called_once_with([], 5)

View File

@ -0,0 +1,219 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
import os
from oslo_concurrency import processutils as putils
from os_brick import exception
from os_brick.initiator import connector
from os_brick.initiator.connectors import hgst
from os_brick.tests.initiator import test_connector
class HGSTConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for HGST initiator class."""
IP_OUTPUT = """
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet 169.254.169.254/32 scope link lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master
link/ether 00:25:90:d9:18:08 brd ff:ff:ff:ff:ff:ff
inet6 fe80::225:90ff:fed9:1808/64 scope link
valid_lft forever preferred_lft forever
3: em2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state
link/ether 00:25:90:d9:18:09 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.23/24 brd 192.168.0.255 scope global em2
valid_lft forever preferred_lft forever
inet6 fe80::225:90ff:fed9:1809/64 scope link
valid_lft forever preferred_lft forever
"""
DOMAIN_OUTPUT = """localhost"""
DOMAIN_FAILED = """this.better.not.resolve.to.a.name.or.else"""
SET_APPHOST_OUTPUT = """
VLVM_SET_APPHOSTS0000000395
Request Succeeded
"""
def setUp(self):
super(HGSTConnectorTestCase, self).setUp()
self.connector = hgst.HGSTConnector(
None, execute=self._fake_exec)
self._fail_set_apphosts = False
self._fail_ip = False
self._fail_domain_list = False
def _fake_exec_set_apphosts(self, *cmd):
if self._fail_set_apphosts:
raise putils.ProcessExecutionError(None, None, 1)
else:
return self.SET_APPHOST_OUTPUT, ''
def _fake_exec_ip(self, *cmd):
if self._fail_ip:
# Remove localhost so there is no IP match
return self.IP_OUTPUT.replace("127.0.0.1", "x.x.x.x"), ''
else:
return self.IP_OUTPUT, ''
def _fake_exec_domain_list(self, *cmd):
if self._fail_domain_list:
return self.DOMAIN_FAILED, ''
else:
return self.DOMAIN_OUTPUT, ''
def _fake_exec(self, *cmd, **kwargs):
self.cmdline = " ".join(cmd)
if cmd[0] == "ip":
return self._fake_exec_ip(*cmd)
elif cmd[0] == "vgc-cluster":
if cmd[1] == "domain-list":
return self._fake_exec_domain_list(*cmd)
elif cmd[1] == "space-set-apphosts":
return self._fake_exec_set_apphosts(*cmd)
else:
return '', ''
def test_factory(self):
"""Can we instantiate a HGSTConnector of the right kind?"""
obj = connector.InitiatorConnector.factory('HGST', None)
self.assertEqual("HGSTConnector", obj.__class__.__name__)
def test_get_search_path(self):
expected = "/dev"
actual = self.connector.get_search_path()
self.assertEqual(expected, actual)
@mock.patch.object(os.path, 'exists', return_value=True)
def test_get_volume_paths(self, mock_exists):
cprops = {'name': 'space', 'noremovehost': 'stor1'}
path = "/dev/%s" % cprops['name']
expected = [path]
actual = self.connector.get_volume_paths(cprops)
self.assertEqual(expected, actual)
def test_connect_volume(self):
"""Tests that a simple connection succeeds"""
self._fail_set_apphosts = False
self._fail_ip = False
self._fail_domain_list = False
cprops = {'name': 'space', 'noremovehost': 'stor1'}
dev_info = self.connector.connect_volume(cprops)
self.assertEqual('block', dev_info['type'])
self.assertEqual('space', dev_info['device'])
self.assertEqual('/dev/space', dev_info['path'])
def test_get_connector_properties(self):
props = hgst.HGSTConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_connect_volume_nohost_fail(self):
"""This host should not be found, connect should fail."""
self._fail_set_apphosts = False
self._fail_ip = True
self._fail_domain_list = False
cprops = {'name': 'space', 'noremovehost': 'stor1'}
self.assertRaises(exception.BrickException,
self.connector.connect_volume,
cprops)
def test_connect_volume_nospace_fail(self):
"""The space command will fail, exception to be thrown"""
self._fail_set_apphosts = True
self._fail_ip = False
self._fail_domain_list = False
cprops = {'name': 'space', 'noremovehost': 'stor1'}
self.assertRaises(exception.BrickException,
self.connector.connect_volume,
cprops)
def test_disconnect_volume(self):
"""Simple disconnection should pass and disconnect me"""
self._fail_set_apphosts = False
self._fail_ip = False
self._fail_domain_list = False
self._cmdline = ""
cprops = {'name': 'space', 'noremovehost': 'stor1'}
self.connector.disconnect_volume(cprops, None)
exp_cli = ("vgc-cluster space-set-apphosts -n space "
"-A localhost --action DELETE")
self.assertEqual(exp_cli, self.cmdline)
def test_disconnect_volume_nohost(self):
"""Should not run a setapphosts because localhost will"""
"""be the noremotehost"""
self._fail_set_apphosts = False
self._fail_ip = False
self._fail_domain_list = False
self._cmdline = ""
cprops = {'name': 'space', 'noremovehost': 'localhost'}
self.connector.disconnect_volume(cprops, None)
# The last command should be the IP listing, not set apphosts
exp_cli = ("ip addr list")
self.assertEqual(exp_cli, self.cmdline)
def test_disconnect_volume_fails(self):
"""The set-apphosts should fail, exception to be thrown"""
self._fail_set_apphosts = True
self._fail_ip = False
self._fail_domain_list = False
self._cmdline = ""
cprops = {'name': 'space', 'noremovehost': 'stor1'}
self.assertRaises(exception.BrickException,
self.connector.disconnect_volume,
cprops, None)
def test_bad_connection_properties(self):
"""Send in connection_properties missing required fields"""
# Invalid connection_properties
self.assertRaises(exception.BrickException,
self.connector.connect_volume,
None)
# Name required for connect_volume
cprops = {'noremovehost': 'stor1'}
self.assertRaises(exception.BrickException,
self.connector.connect_volume,
cprops)
# Invalid connection_properties
self.assertRaises(exception.BrickException,
self.connector.disconnect_volume,
None, None)
# Name and noremovehost needed for disconnect_volume
cprops = {'noremovehost': 'stor1'}
self.assertRaises(exception.BrickException,
self.connector.disconnect_volume,
cprops, None)
cprops = {'name': 'space'}
self.assertRaises(exception.BrickException,
self.connector.disconnect_volume,
cprops, None)
def test_extend_volume(self):
cprops = {'name': 'space', 'noremovehost': 'stor1'}
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
cprops)

View File

@ -0,0 +1,230 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
import os
import tempfile
from os_brick import exception
from os_brick.initiator.connectors import huawei
from os_brick.tests.initiator import test_connector
class HuaweiStorHyperConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for StorHyper initiator class."""
attached = False
def setUp(self):
super(HuaweiStorHyperConnectorTestCase, self).setUp()
self.fake_sdscli_file = tempfile.mktemp()
self.addCleanup(os.remove, self.fake_sdscli_file)
newefile = open(self.fake_sdscli_file, 'w')
newefile.write('test')
newefile.close()
self.connector = huawei.HuaweiStorHyperConnector(
None, execute=self.fake_execute)
self.connector.cli_path = self.fake_sdscli_file
self.connector.iscliexist = True
self.connector_fail = huawei.HuaweiStorHyperConnector(
None, execute=self.fake_execute_fail)
self.connector_fail.cli_path = self.fake_sdscli_file
self.connector_fail.iscliexist = True
self.connector_nocli = huawei.HuaweiStorHyperConnector(
None, execute=self.fake_execute_fail)
self.connector_nocli.cli_path = self.fake_sdscli_file
self.connector_nocli.iscliexist = False
self.connection_properties = {
'access_mode': 'rw',
'qos_specs': None,
'volume_id': 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f'
}
self.device_info = {'type': 'block',
'path': '/dev/vdxxx'}
HuaweiStorHyperConnectorTestCase.attached = False
def fake_execute(self, *cmd, **kwargs):
method = cmd[2]
self.cmds.append(" ".join(cmd))
if 'attach' == method:
HuaweiStorHyperConnectorTestCase.attached = True
return 'ret_code=0', None
if 'querydev' == method:
if HuaweiStorHyperConnectorTestCase.attached:
return 'ret_code=0\ndev_addr=/dev/vdxxx', None
else:
return 'ret_code=1\ndev_addr=/dev/vdxxx', None
if 'detach' == method:
HuaweiStorHyperConnectorTestCase.attached = False
return 'ret_code=0', None
def fake_execute_fail(self, *cmd, **kwargs):
method = cmd[2]
self.cmds.append(" ".join(cmd))
if 'attach' == method:
HuaweiStorHyperConnectorTestCase.attached = False
return 'ret_code=330151401', None
if 'querydev' == method:
if HuaweiStorHyperConnectorTestCase.attached:
return 'ret_code=0\ndev_addr=/dev/vdxxx', None
else:
return 'ret_code=1\ndev_addr=/dev/vdxxx', None
if 'detach' == method:
HuaweiStorHyperConnectorTestCase.attached = True
return 'ret_code=330155007', None
def test_get_connector_properties(self):
props = huawei.HuaweiStorHyperConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_get_search_path(self):
actual = self.connector.get_search_path()
self.assertIsNone(actual)
@mock.patch.object(huawei.HuaweiStorHyperConnector,
'_query_attached_volume')
def test_get_volume_paths(self, mock_query_attached):
path = self.device_info['path']
mock_query_attached.return_value = {'ret_code': 0,
'dev_addr': path}
expected = [path]
actual = self.connector.get_volume_paths(self.connection_properties)
self.assertEqual(expected, actual)
def test_connect_volume(self):
"""Test the basic connect volume case."""
retval = self.connector.connect_volume(self.connection_properties)
self.assertEqual(self.device_info, retval)
expected_commands = [self.fake_sdscli_file + ' -c attach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
self.assertEqual(expected_commands, self.cmds)
def test_disconnect_volume(self):
"""Test the basic disconnect volume case."""
self.connector.connect_volume(self.connection_properties)
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
self.connector.disconnect_volume(self.connection_properties,
self.device_info)
self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached)
expected_commands = [self.fake_sdscli_file + ' -c attach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c detach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
self.assertEqual(expected_commands, self.cmds)
def test_is_volume_connected(self):
"""Test if volume connected to host case."""
self.connector.connect_volume(self.connection_properties)
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
is_connected = self.connector.is_volume_connected(
'volume-b2911673-863c-4380-a5f2-e1729eecfe3f')
self.assertEqual(HuaweiStorHyperConnectorTestCase.attached,
is_connected)
self.connector.disconnect_volume(self.connection_properties,
self.device_info)
self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached)
is_connected = self.connector.is_volume_connected(
'volume-b2911673-863c-4380-a5f2-e1729eecfe3f')
self.assertEqual(HuaweiStorHyperConnectorTestCase.attached,
is_connected)
expected_commands = [self.fake_sdscli_file + ' -c attach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c detach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
self.assertEqual(expected_commands, self.cmds)
def test__analyze_output(self):
cliout = 'ret_code=0\ndev_addr=/dev/vdxxx\nret_desc="success"'
analyze_result = {'dev_addr': '/dev/vdxxx',
'ret_desc': '"success"',
'ret_code': '0'}
result = self.connector._analyze_output(cliout)
self.assertEqual(analyze_result, result)
def test_connect_volume_fail(self):
"""Test the fail connect volume case."""
self.assertRaises(exception.BrickException,
self.connector_fail.connect_volume,
self.connection_properties)
expected_commands = [self.fake_sdscli_file + ' -c attach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
self.assertEqual(expected_commands, self.cmds)
def test_disconnect_volume_fail(self):
"""Test the fail disconnect volume case."""
self.connector.connect_volume(self.connection_properties)
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
self.assertRaises(exception.BrickException,
self.connector_fail.disconnect_volume,
self.connection_properties,
self.device_info)
expected_commands = [self.fake_sdscli_file + ' -c attach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c detach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
self.assertEqual(expected_commands, self.cmds)
def test_connect_volume_nocli(self):
"""Test the fail connect volume case."""
self.assertRaises(exception.BrickException,
self.connector_nocli.connect_volume,
self.connection_properties)
def test_disconnect_volume_nocli(self):
"""Test the fail disconnect volume case."""
self.connector.connect_volume(self.connection_properties)
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
self.assertRaises(exception.BrickException,
self.connector_nocli.disconnect_volume,
self.connection_properties,
self.device_info)
expected_commands = [self.fake_sdscli_file + ' -c attach'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
self.fake_sdscli_file + ' -c querydev'
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
self.assertEqual(expected_commands, self.cmds)
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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.
from os_brick.initiator.connectors import local
from os_brick.tests.initiator import test_connector
class LocalConnectorTestCase(test_connector.ConnectorTestCase):
def setUp(self):
super(LocalConnectorTestCase, self).setUp()
self.connection_properties = {'name': 'foo',
'device_path': '/tmp/bar'}
self.connector = local.LocalConnector(None)
def test_get_connector_properties(self):
props = local.LocalConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_get_search_path(self):
actual = self.connector.get_search_path()
self.assertIsNone(actual)
def test_get_volume_paths(self):
expected = [self.connection_properties['device_path']]
actual = self.connector.get_volume_paths(
self.connection_properties)
self.assertEqual(expected, actual)
def test_connect_volume(self):
cprops = self.connection_properties
dev_info = self.connector.connect_volume(cprops)
self.assertEqual(dev_info['type'], 'local')
self.assertEqual(dev_info['path'], cprops['device_path'])
def test_connect_volume_with_invalid_connection_data(self):
cprops = {}
self.assertRaises(ValueError,
self.connector.connect_volume, cprops)
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)

View File

@ -0,0 +1,126 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
from oslo_utils import encodeutils
from os_brick.initiator.connectors import rbd
from os_brick.initiator import linuxrbd
from os_brick.privileged import rootwrap as priv_rootwrap
from os_brick.tests.initiator import test_connector
class RBDConnectorTestCase(test_connector.ConnectorTestCase):
def setUp(self):
super(RBDConnectorTestCase, self).setUp()
self.user = 'fake_user'
self.pool = 'fake_pool'
self.volume = 'fake_volume'
self.connection_properties = {
'auth_username': self.user,
'name': '%s/%s' % (self.pool, self.volume),
}
def test_get_search_path(self):
rbd_connector = rbd.RBDConnector(None)
path = rbd_connector.get_search_path()
self.assertIsNone(path)
@mock.patch('os_brick.initiator.linuxrbd.rbd')
@mock.patch('os_brick.initiator.linuxrbd.rados')
def test_get_volume_paths(self, mock_rados, mock_rbd):
rbd_connector = rbd.RBDConnector(None)
expected = []
actual = rbd_connector.get_volume_paths(self.connection_properties)
self.assertEqual(expected, actual)
def test_get_connector_properties(self):
props = rbd.RBDConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {'do_local_attach': False}
self.assertEqual(expected_props, props)
@mock.patch('os_brick.initiator.linuxrbd.rbd')
@mock.patch('os_brick.initiator.linuxrbd.rados')
def test_connect_volume(self, mock_rados, mock_rbd):
"""Test the connect volume case."""
rbd_connector = rbd.RBDConnector(None)
device_info = rbd_connector.connect_volume(self.connection_properties)
# Ensure rados is instantiated correctly
mock_rados.Rados.assert_called_once_with(
clustername='ceph',
rados_id=encodeutils.safe_encode(self.user),
conffile='/etc/ceph/ceph.conf')
# Ensure correct calls to connect to cluster
self.assertEqual(1, mock_rados.Rados.return_value.connect.call_count)
mock_rados.Rados.return_value.open_ioctx.assert_called_once_with(
encodeutils.safe_encode(self.pool))
# Ensure rbd image is instantiated correctly
mock_rbd.Image.assert_called_once_with(
mock_rados.Rados.return_value.open_ioctx.return_value,
encodeutils.safe_encode(self.volume), read_only=False,
snapshot=None)
# Ensure expected object is returned correctly
self.assertTrue(isinstance(device_info['path'],
linuxrbd.RBDVolumeIOWrapper))
@mock.patch.object(priv_rootwrap, 'execute')
def test_connect_local_volume(self, mock_execute):
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
conn = {'name': 'pool/image'}
device_info = rbd_connector.connect_volume(conn)
execute_call1 = mock.call('which', 'rbd')
cmd = ['rbd', 'map', 'image', '--pool', 'pool']
execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
mock_execute.assert_has_calls([execute_call1, execute_call2])
expected_info = {'path': '/dev/rbd/pool/image',
'type': 'block'}
self.assertEqual(expected_info, device_info)
@mock.patch('os_brick.initiator.linuxrbd.rbd')
@mock.patch('os_brick.initiator.linuxrbd.rados')
@mock.patch.object(linuxrbd.RBDVolumeIOWrapper, 'close')
def test_disconnect_volume(self, volume_close, mock_rados, mock_rbd):
"""Test the disconnect volume case."""
rbd_connector = rbd.RBDConnector(None)
device_info = rbd_connector.connect_volume(self.connection_properties)
rbd_connector.disconnect_volume(
self.connection_properties, device_info)
self.assertEqual(1, volume_close.call_count)
@mock.patch.object(priv_rootwrap, 'execute')
def test_disconnect_local_volume(self, mock_execute):
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
conn = {'name': 'pool/image'}
rbd_connector.disconnect_volume(conn, None)
dev_name = '/dev/rbd/pool/image'
cmd = ['rbd', 'unmap', dev_name]
mock_execute.assert_called_once_with(*cmd, root_helper=None,
run_as_root=True)
def test_extend_volume(self):
rbd_connector = rbd.RBDConnector(None)
self.assertRaises(NotImplementedError,
rbd_connector.extend_volume,
self.connection_properties)

View File

@ -0,0 +1,77 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
from os_brick.initiator.connectors import remotefs
from os_brick.remotefs import remotefs as remotefs_client
from os_brick.tests.initiator import test_connector
class RemoteFsConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for Remote FS initiator class."""
TEST_DEV = '172.18.194.100:/var/nfs'
TEST_PATH = '/mnt/test/df0808229363aad55c27da50c38d6328'
TEST_BASE = '/mnt/test'
TEST_NAME = '9c592d52-ce47-4263-8c21-4ecf3c029cdb'
def setUp(self):
super(RemoteFsConnectorTestCase, self).setUp()
self.connection_properties = {
'export': self.TEST_DEV,
'name': self.TEST_NAME}
self.connector = remotefs.RemoteFsConnector(
'nfs', root_helper='sudo',
nfs_mount_point_base=self.TEST_BASE,
nfs_mount_options='vers=3')
@mock.patch('os_brick.remotefs.remotefs.ScalityRemoteFsClient')
def test_init_with_scality(self, mock_scality_remotefs_client):
remotefs.RemoteFsConnector('scality', root_helper='sudo')
self.assertEqual(1, mock_scality_remotefs_client.call_count)
def test_get_connector_properties(self):
props = remotefs.RemoteFsConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_get_search_path(self):
expected = self.TEST_BASE
actual = self.connector.get_search_path()
self.assertEqual(expected, actual)
@mock.patch.object(remotefs_client.RemoteFsClient, 'mount')
def test_get_volume_paths(self, mock_mount):
path = ("%(path)s/%(name)s" % {'path': self.TEST_PATH,
'name': self.TEST_NAME})
expected = [path]
actual = self.connector.get_volume_paths(self.connection_properties)
self.assertEqual(expected, actual)
@mock.patch.object(remotefs_client.RemoteFsClient, 'mount')
@mock.patch.object(remotefs_client.RemoteFsClient, 'get_mount_point',
return_value="something")
def test_connect_volume(self, mount_point_mock, mount_mock):
"""Test the basic connect volume case."""
self.connector.connect_volume(self.connection_properties)
def test_disconnect_volume(self):
"""Nothing should happen here -- make sure it doesn't blow up."""
self.connector.disconnect_volume(self.connection_properties, {})
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.connection_properties)

View File

@ -0,0 +1,279 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 json
import mock
import os
import requests
import six
from oslo_concurrency import processutils as putils
from os_brick import exception
from os_brick.initiator.connectors import scaleio
from os_brick.tests.initiator import test_connector
class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for ScaleIO connector."""
# Fake volume information
vol = {
'id': 'vol1',
'name': 'test_volume',
'provider_id': 'vol1'
}
# Fake SDC GUID
fake_guid = 'FAKE_GUID'
def setUp(self):
super(ScaleIOConnectorTestCase, self).setUp()
self.fake_connection_properties = {
'hostIP': test_connector.MY_IP,
'serverIP': test_connector.MY_IP,
'scaleIO_volname': self.vol['name'],
'scaleIO_volume_id': self.vol['provider_id'],
'serverPort': 443,
'serverUsername': 'test',
'serverPassword': 'fake',
'serverToken': 'fake_token',
'iopsLimit': None,
'bandwidthLimit': None
}
# Formatting string for REST API calls
self.action_format = "instances/Volume::{}/action/{{}}".format(
self.vol['id'])
self.get_volume_api = 'types/Volume/instances/getByName::{}'.format(
self.vol['name'])
# Map of REST API calls to responses
self.mock_calls = {
self.get_volume_api:
self.MockHTTPSResponse(json.dumps(self.vol['id'])),
self.action_format.format('addMappedSdc'):
self.MockHTTPSResponse(''),
self.action_format.format('setMappedSdcLimits'):
self.MockHTTPSResponse(''),
self.action_format.format('removeMappedSdc'):
self.MockHTTPSResponse(''),
}
# Default error REST response
self.error_404 = self.MockHTTPSResponse(content=dict(
errorCode=0,
message='HTTP 404',
), status_code=404)
# Patch the request and os calls to fake versions
mock.patch.object(
requests, 'get', self.handle_scaleio_request).start()
mock.patch.object(
requests, 'post', self.handle_scaleio_request).start()
mock.patch.object(os.path, 'isdir', return_value=True).start()
mock.patch.object(
os, 'listdir', return_value=["emc-vol-{}".format(self.vol['id'])]
).start()
self.addCleanup(mock.patch.stopall)
# The actual ScaleIO connector
self.connector = scaleio.ScaleIOConnector(
'sudo', execute=self.fake_execute)
class MockHTTPSResponse(requests.Response):
"""Mock HTTP Response
Defines the https replies from the mocked calls to do_request()
"""
def __init__(self, content, status_code=200):
super(ScaleIOConnectorTestCase.MockHTTPSResponse,
self).__init__()
self._content = content
self.encoding = 'UTF-8'
self.status_code = status_code
def json(self, **kwargs):
if isinstance(self._content, six.string_types):
return super(ScaleIOConnectorTestCase.MockHTTPSResponse,
self).json(**kwargs)
return self._content
@property
def text(self):
if not isinstance(self._content, six.string_types):
return json.dumps(self._content)
self._content = self._content.encode('utf-8')
return super(ScaleIOConnectorTestCase.MockHTTPSResponse,
self).text
def fake_execute(self, *cmd, **kwargs):
"""Fakes the rootwrap call"""
return self.fake_guid, None
def fake_missing_execute(self, *cmd, **kwargs):
"""Error when trying to call rootwrap drv_cfg"""
raise putils.ProcessExecutionError("Test missing drv_cfg.")
def handle_scaleio_request(self, url, *args, **kwargs):
"""Fake REST server"""
api_call = url.split(':', 2)[2].split('/', 1)[1].replace('api/', '')
if 'setMappedSdcLimits' in api_call:
self.assertNotIn("iops_limit", kwargs['data'])
if "iopsLimit" not in kwargs['data']:
self.assertIn("bandwidthLimitInKbps",
kwargs['data'])
elif "bandwidthLimitInKbps" not in kwargs['data']:
self.assertIn("iopsLimit", kwargs['data'])
else:
self.assertIn("bandwidthLimitInKbps",
kwargs['data'])
self.assertIn("iopsLimit", kwargs['data'])
try:
return self.mock_calls[api_call]
except KeyError:
return self.error_404
def test_get_search_path(self):
expected = "/dev/disk/by-id"
actual = self.connector.get_search_path()
self.assertEqual(expected, actual)
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch.object(scaleio.ScaleIOConnector, '_wait_for_volume_path')
def test_get_volume_paths(self, mock_wait_for_path, mock_exists):
mock_wait_for_path.return_value = "emc-vol-vol1"
expected = ['/dev/disk/by-id/emc-vol-vol1']
actual = self.connector.get_volume_paths(
self.fake_connection_properties)
self.assertEqual(expected, actual)
def test_get_connector_properties(self):
props = scaleio.ScaleIOConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_connect_volume(self):
"""Successful connect to volume"""
self.connector.connect_volume(self.fake_connection_properties)
def test_connect_with_bandwidth_limit(self):
"""Successful connect to volume with bandwidth limit"""
self.fake_connection_properties['bandwidthLimit'] = '500'
self.test_connect_volume()
def test_connect_with_iops_limit(self):
"""Successful connect to volume with iops limit"""
self.fake_connection_properties['iopsLimit'] = '80'
self.test_connect_volume()
def test_connect_with_iops_and_bandwidth_limits(self):
"""Successful connect with iops and bandwidth limits"""
self.fake_connection_properties['bandwidthLimit'] = '500'
self.fake_connection_properties['iopsLimit'] = '80'
self.test_connect_volume()
def test_disconnect_volume(self):
"""Successful disconnect from volume"""
self.connector.disconnect_volume(self.fake_connection_properties, None)
def test_error_id(self):
"""Fail to connect with bad volume name"""
self.fake_connection_properties['scaleIO_volume_id'] = 'bad_id'
self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse(
dict(errorCode='404', message='Test volume not found'), 404)
self.assertRaises(exception.BrickException, self.test_connect_volume)
def test_error_no_volume_id(self):
"""Faile to connect with no volume id"""
self.fake_connection_properties['scaleIO_volume_id'] = None
self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse(
'null', 200)
self.assertRaises(exception.BrickException, self.test_connect_volume)
def test_error_bad_login(self):
"""Fail to connect with bad authentication"""
self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse(
'null', 401)
self.mock_calls['login'] = self.MockHTTPSResponse('null', 401)
self.mock_calls[self.action_format.format(
'addMappedSdc')] = self.MockHTTPSResponse(
dict(errorCode=401, message='bad login'), 401)
self.assertRaises(exception.BrickException, self.test_connect_volume)
def test_error_bad_drv_cfg(self):
"""Fail to connect with missing rootwrap executable"""
self.connector.set_execute(self.fake_missing_execute)
self.assertRaises(exception.BrickException, self.test_connect_volume)
def test_error_map_volume(self):
"""Fail to connect with REST API failure"""
self.mock_calls[self.action_format.format(
'addMappedSdc')] = self.MockHTTPSResponse(
dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR,
message='Test error map volume'), 500)
self.assertRaises(exception.BrickException, self.test_connect_volume)
@mock.patch('time.sleep')
def test_error_path_not_found(self, sleep_mock):
"""Timeout waiting for volume to map to local file system"""
mock.patch.object(
os, 'listdir', return_value=["emc-vol-no-volume"]
).start()
self.assertRaises(exception.BrickException, self.test_connect_volume)
self.assertTrue(sleep_mock.called)
def test_map_volume_already_mapped(self):
"""Ignore REST API failure for volume already mapped"""
self.mock_calls[self.action_format.format(
'addMappedSdc')] = self.MockHTTPSResponse(
dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR,
message='Test error map volume'), 500)
self.test_connect_volume()
def test_error_disconnect_volume(self):
"""Fail to disconnect with REST API failure"""
self.mock_calls[self.action_format.format(
'removeMappedSdc')] = self.MockHTTPSResponse(
dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR,
message='Test error map volume'), 500)
self.assertRaises(exception.BrickException,
self.test_disconnect_volume)
def test_disconnect_volume_not_mapped(self):
"""Ignore REST API failure for volume not mapped"""
self.mock_calls[self.action_format.format(
'removeMappedSdc')] = self.MockHTTPSResponse(
dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR,
message='Test error map volume'), 500)
self.test_disconnect_volume()
def test_extend_volume(self):
self.assertRaises(NotImplementedError,
self.connector.extend_volume,
self.fake_connection_properties)

View File

@ -0,0 +1,87 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# 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 mock
from os_brick import exception
from os_brick.initiator.connectors import sheepdog
from os_brick.initiator import linuxsheepdog
from os_brick.tests.initiator import test_connector
class SheepdogConnectorTestCase(test_connector.ConnectorTestCase):
def setUp(self):
super(SheepdogConnectorTestCase, self).setUp()
self.hosts = ['fake_hosts']
self.ports = ['fake_ports']
self.volume = 'fake_volume'
self.connection_properties = {
'hosts': self.hosts,
'name': self.volume,
'ports': self.ports,
}
def test_get_connector_properties(self):
props = sheepdog.SheepdogConnector.get_connector_properties(
'sudo', multipath=True, enforce_multipath=True)
expected_props = {}
self.assertEqual(expected_props, props)
def test_get_search_path(self):
sd_connector = sheepdog.SheepdogConnector(None)
path = sd_connector.get_search_path()
self.assertIsNone(path)
def test_get_volume_paths(self):
sd_connector = sheepdog.SheepdogConnector(None)
expected = []
actual = sd_connector.get_volume_paths(self.connection_properties)
self.assertEqual(expected, actual)
def test_connect_volume(self):
"""Test the connect volume case."""
sd_connector = sheepdog.SheepdogConnector(None)
device_info = sd_connector.connect_volume(self.connection_properties)
# Ensure expected object is returned correctly
self.assertTrue(isinstance(device_info['path'],
linuxsheepdog.SheepdogVolumeIOWrapper))
@mock.patch.object(linuxsheepdog.SheepdogVolumeIOWrapper, 'close')
def test_disconnect_volume(self, volume_close):
"""Test the disconnect volume case."""
sd_connector = sheepdog.SheepdogConnector(None)
device_info = sd_connector.connect_volume(self.connection_properties)
sd_connector.disconnect_volume(self.connection_properties, device_info)
self.assertEqual(1, volume_close.call_count)
def test_disconnect_volume_with_invalid_handle(self):
"""Test the disconnect volume case with invalid handle."""
sd_connector = sheepdog.SheepdogConnector(None)
device_info = {'path': 'fake_handle'}
self.assertRaises(exception.InvalidIOHandleObject,
sd_connector.disconnect_volume,
self.connection_properties,
device_info)
def test_extend_volume(self):
sd_connector = sheepdog.SheepdogConnector(None)
self.assertRaises(NotImplementedError,
sd_connector.extend_volume,
self.connection_properties)

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
import ddt
import mock
from os_brick import initiator
from os_brick.initiator import connector
from os_brick.initiator.windows import iscsi
from os_brick.tests.windows import test_base
@ -23,7 +24,7 @@ from os_brick.tests.windows import test_base
@ddt.ddt
class WindowsConnectorFactoryTestCase(test_base.WindowsConnectorTestBase):
@ddt.data({'proto': connector.ISCSI,
@ddt.data({'proto': initiator.ISCSI,
'expected_cls': iscsi.WindowsISCSIConnector})
@ddt.unpack
@mock.patch('sys.platform', 'win32')