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
changes/74/307974/23
Kendall Nelson 6 years ago
parent cc47d81feb
commit c5e3d8affb
  1. 36
      os_brick/initiator/__init__.py
  2. 3279
      os_brick/initiator/connector.py
  3. 0
      os_brick/initiator/connectors/__init__.py
  4. 177
      os_brick/initiator/connectors/aoe.py
  5. 129
      os_brick/initiator/connectors/base.py
  6. 42
      os_brick/initiator/connectors/base_iscsi.py
  7. 207
      os_brick/initiator/connectors/disco.py
  8. 109
      os_brick/initiator/connectors/drbd.py
  9. 48
      os_brick/initiator/connectors/fake.py
  10. 300
      os_brick/initiator/connectors/fibre_channel.py
  11. 86
      os_brick/initiator/connectors/fibre_channel_s390x.py
  12. 182
      os_brick/initiator/connectors/hgst.py
  13. 192
      os_brick/initiator/connectors/huawei.py
  14. 832
      os_brick/initiator/connectors/iscsi.py
  15. 78
      os_brick/initiator/connectors/local.py
  16. 166
      os_brick/initiator/connectors/rbd.py
  17. 119
      os_brick/initiator/connectors/remotefs.py
  18. 491
      os_brick/initiator/connectors/scaleio.py
  19. 126
      os_brick/initiator/connectors/sheepdog.py
  20. 193
      os_brick/initiator/initiator_connector.py
  21. 9
      os_brick/initiator/windows/base.py
  22. 4
      os_brick/initiator/windows/iscsi.py
  23. 0
      os_brick/tests/initiator/connectors/__init__.py
  24. 129
      os_brick/tests/initiator/connectors/test_aoe.py
  25. 77
      os_brick/tests/initiator/connectors/test_base_iscsi.py
  26. 156
      os_brick/tests/initiator/connectors/test_disco.py
  27. 89
      os_brick/tests/initiator/connectors/test_drbd.py
  28. 396
      os_brick/tests/initiator/connectors/test_fibre_channel.py
  29. 71
      os_brick/tests/initiator/connectors/test_fibre_channel_s390x.py
  30. 219
      os_brick/tests/initiator/connectors/test_hgst.py
  31. 230
      os_brick/tests/initiator/connectors/test_huawei.py
  32. 1005
      os_brick/tests/initiator/connectors/test_iscsi.py
  33. 58
      os_brick/tests/initiator/connectors/test_local.py
  34. 126
      os_brick/tests/initiator/connectors/test_rbd.py
  35. 77
      os_brick/tests/initiator/connectors/test_remotefs.py
  36. 279
      os_brick/tests/initiator/connectors/test_scaleio.py
  37. 87
      os_brick/tests/initiator/connectors/test_sheepdog.py
  38. 2780
      os_brick/tests/initiator/test_connector.py
  39. 3
      os_brick/tests/windows/test_factory.py

@ -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

@ -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

@ -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

@ -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))]

@ -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

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -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)</