Skip to content

Instantly share code, notes, and snippets.

@WaltHP
Created June 23, 2016 16:32
Show Gist options
  • Save WaltHP/8690005024acab59ace63644b89c61d7 to your computer and use it in GitHub Desktop.
Save WaltHP/8690005024acab59ace63644b89c61d7 to your computer and use it in GitHub Desktop.
diff --git a/os_brick/initiator/AoEConnector.py b/os_brick/initiator/AoEConnector.py
deleted file mode 100644
index 6d7acbb..0000000
--- a/os_brick/initiator/AoEConnector.py
+++ /dev/null
@@ -1,170 +0,0 @@
-# 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_concurrency import processutils as putils
-from oslo_log import log as logging
-from oslo_service import loopingcall
-
-from os_brick import exception
-
-from os_brick.i18n import _LW
-from os_brick.initiator import InitiatorConnector
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-
-class AoEConnector(InitiatorConnector):
- """Connector class to attach/detach AoE volumes."""
- def __init__(self, root_helper, driver=None,
- execute=putils.execute,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
- super(AoEConnector, self).__init__(
- root_helper,
- driver=driver,
- execute=execute,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
-
- 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
-
- @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
-
- @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
diff --git a/os_brick/initiator/DISCOConnector.py b/os_brick/initiator/DISCOConnector.py
deleted file mode 100644
index 4fbfb79..0000000
--- a/os_brick/initiator/DISCOConnector.py
+++ /dev/null
@@ -1,201 +0,0 @@
-# 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_concurrency import processutils as putils
-from oslo_log import log as logging
-import six
-
-from os_brick import exception
-
-from os_brick.i18n import _, _LI, _LE
-from os_brick.initiator import InitiatorConnector
-
-LOG = logging.getLogger(__name__)
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-synchronized = lockutils.synchronized_with_prefix('os-brick-')
-
-
-class DISCOConnector(InitiatorConnector):
- """Class implements the connector driver for DISCO."""
-
- DISCO_PREFIX = 'dms'
-
- def __init__(self, root_helper, driver=None, execute=putils.execute,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
- """Init DISCO connector."""
- super(DISCOConnector, self).__init__(
- root_helper,
- driver=driver,
- execute=execute,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs
- )
- LOG.info(_LI("Init DISCO connector"))
-
- self.server_port = None
- self.server_ip = None
-
- 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
-
- @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
-
- @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
diff --git a/os_brick/initiator/DRBDConnector.py b/os_brick/initiator/DRBDConnector.py
deleted file mode 100644
index 6d4bf3e..0000000
--- a/os_brick/initiator/DRBDConnector.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# 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 import InitiatorConnector
-
-
-class DRBDConnector(InitiatorConnector):
- """"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
-
- 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)
-
- 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
-
- 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
diff --git a/os_brick/initiator/FCConnector.py b/os_brick/initiator/FCConnector.py
deleted file mode 100644
index 7af54cb..0000000
--- a/os_brick/initiator/FCConnector.py
+++ /dev/null
@@ -1,288 +0,0 @@
-# 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 re
-
-from oslo_concurrency import lockutils
-from oslo_concurrency import processutils as putils
-from oslo_log import log as logging
-from oslo_service import loopingcall
-import six
-
-from os_brick import exception
-
-from os_brick.i18n import _LE, _LW
-from os_brick.initiator import InitiatorConnector
-from os_brick.initiator import linuxfc
-from os_brick.initiator import linuxscsi
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-synchronized = lockutils.synchronized_with_prefix('os-brick-')
-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+")
-
-LOG = logging.getLogger(__name__)
-
-
-class FibreChannelConnector(InitiatorConnector):
- """Connector class to attach/detach Fibre Channel volumes."""
-
- def __init__(self, root_helper, driver=None,
- execute=putils.execute, use_multipath=False,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
- self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute)
- 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)
-
- 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
-
- @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()
-
- @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
-
- @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
diff --git a/os_brick/initiator/FCConnectorS390X.py b/os_brick/initiator/FCConnectorS390X.py
deleted file mode 100644
index 2dff0e1..0000000
--- a/os_brick/initiator/FCConnectorS390X.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# 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.initiator import FCConnector as FibreChannelConnector
-from os_brick.initiator import linuxfc
-from os_brick.initiator import linuxscsi
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-
-class FibreChannelConnectorS390X(FibreChannelConnector):
- """Connector class to attach/detach Fibre Channel volumes on S390X arch."""
-
- def __init__(self, root_helper, driver=None,
- execute=putils.execute, use_multipath=False,
- device_scan_attempts=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._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute)
- 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)
diff --git a/os_brick/initiator/FakeConnector.py b/os_brick/initiator/FakeConnector.py
deleted file mode 100644
index c3147dd..0000000
--- a/os_brick/initiator/FakeConnector.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# 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 import InitiatorConnector
-
-
-class FakeConnector(InitiatorConnector):
-
- 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']
diff --git a/os_brick/initiator/HGSTConnector.py b/os_brick/initiator/HGSTConnector.py
deleted file mode 100644
index 5cfecc9..0000000
--- a/os_brick/initiator/HGSTConnector.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# 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.
-import os
-import socket
-
-from oslo_concurrency import processutils as putils
-from oslo_log import log as logging
-
-from os_brick import exception
-
-from os_brick.i18n import _, _LE
-from os_brick.initiator import InitiatorConnector
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-
-class HGSTConnector(InitiatorConnector):
- """Connector class to attach/detach HGST volumes."""
- VGCCLUSTER = 'vgc-cluster'
-
- def __init__(self, root_helper, driver=None,
- execute=putils.execute,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
- super(HGSTConnector, self).__init__(root_helper, driver=driver,
- execute=execute,
- device_scan_attempts=
- device_scan_attempts,
- *args, **kwargs)
- self._vgc_host = None
-
- 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]
-
- 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
-
- 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
diff --git a/os_brick/initiator/HuaweiStorHyperConnector.py b/os_brick/initiator/HuaweiStorHyperConnector.py
deleted file mode 100644
index 5a5081f..0000000
--- a/os_brick/initiator/HuaweiStorHyperConnector.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# 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_concurrency import processutils as putils
-from oslo_log import log as logging
-
-from os_brick import exception
-
-from os_brick.i18n import _, _LE
-from os_brick.initiator import InitiatorConnector
-
-LOG = logging.getLogger(__name__)
-synchronized = lockutils.synchronized_with_prefix('os-brick-')
-
-
-class HuaweiStorHyperConnector(InitiatorConnector):
- """"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, execute=putils.execute,
- *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,
- execute=execute,
- *args, **kwargs)
-
- 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']
-
- @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
-
- @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
diff --git a/os_brick/initiator/ISCSIConnector.py b/os_brick/initiator/ISCSIConnector.py
deleted file mode 100644
index 595e1fd..0000000
--- a/os_brick/initiator/ISCSIConnector.py
+++ /dev/null
@@ -1,834 +0,0 @@
-# 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 import exception
-
-from os_brick.i18n import _, _LE, _LI, _LW
-from os_brick.initiator import InitiatorConnector
-from os_brick.initiator import linuxscsi
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-synchronized = lockutils.synchronized_with_prefix('os-brick-')
-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+")
-
-LOG = logging.getLogger(__name__)
-
-
-class ISCSIConnector(InitiatorConnector):
- """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=putils.execute, use_multipath=False,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- transport='default', *args, **kwargs):
- self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute)
- 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)
-
- 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:
- raise exception.TargetPortalNotFound(
- target_portal=connection_properties['target_portal'])
-
- 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 _iterate_all_targets(self, connection_properties):
- for ip, iqn, lun in self._get_all_targets(connection_properties):
- props = copy.deepcopy(connection_properties)
- props['target_portal'] = ip
- 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))]
-
- 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)
-
- @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()
-
- @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
-
- @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' # noqa
- % 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(MULTIPATH_ERROR_REGEX, line)]
- mpath_dev = None
- mpath_map = {}
- for line in out.splitlines():
- m = MULTIPATH_DEV_CHECK_REGEX.split(line)
- if len(m) >= 2:
- mpath_dev = '/dev/mapper/' + m[0].split(" ")[0]
- continue
- m = 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])
diff --git a/os_brick/initiator/InitiatorConnector.py b/os_brick/initiator/InitiatorConnector.py
deleted file mode 100644
index 4dd4b99..0000000
--- a/os_brick/initiator/InitiatorConnector.py
+++ /dev/null
@@ -1,290 +0,0 @@
-# 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
-import glob
-import os
-
-from oslo_concurrency import processutils as putils
-from oslo_log import log as logging
-import six
-
-from os_brick import exception
-from os_brick import executor
-
-from os_brick.initiator import host_driver
-from os_brick.initiator import linuxscsi
-from os_brick.i18n import _LE, _LW
-from os_brick.initiator import utils as connector_utils
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-PLATFORM_ALL = 'ALL'
-OS_TYPE_LINUX = 'LINUX'
-
-
-@six.add_metaclass(abc.ABCMeta)
-class InitiatorConnector(executor.Executor):
-
- # This object can be used on any platform (x86, S390)
- platform = PLATFORM_ALL
-
- # This object can be used on any os type (linux, windows)
- # TODO(walter-boring) This class stil has a reliance on
- # linuxscsi object, making it specific to linux. Need to fix that.
- os_type = OS_TYPE_LINUX
-
- def __init__(self, root_helper, driver=None, execute=None,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
- super(InitiatorConnector, self).__init__(root_helper, execute=execute,
- *args, **kwargs)
- if not driver:
- driver = host_driver.HostDriver()
- self.set_driver(driver)
- self.device_scan_attempts = device_scan_attempts
- self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute)
-
- def set_driver(self, driver):
- """The driver is used to find used LUNs."""
- self.driver = driver
-
- @staticmethod
- def get_connector_properties(root_helper, *args, **kwargs):
- """The generic connector properties."""
- multipath = kwargs['multipath']
- enforce_multipath = kwargs['enforce_multipath']
- props = {}
- # TODO(walter-boring) move this into platform specific lib
- props['multipath'] = (multipath and
- linuxscsi.LinuxSCSI.is_multipath_running(
- enforce_multipath, root_helper))
-
- return props
-
- @staticmethod
- def factory(protocol, root_helper, driver=None,
- use_multipath=False,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- arch=None,
- *args, **kwargs):
- return connector_utils.connector_factory(
- protocol, root_helper, driver=driver, use_multipath=use_multipath,
- device_scan_attempts=device_scan_attempts, arch=arch, *args,
- **kwargs)
-
- 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
- """
- 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 %(info)s."),
- {"path": path, "error": e.stderr,
- "info": info})
- return False
- # If the info is none, the path does not exist.
- if info is None:
- return False
- return True
-
- 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:
- mpath_info = self._linuxscsi.find_multipath_device(
- device_name)
- 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 = self.host_device
- LOG.debug("Unable to find multipath device name for "
- "volume. Using path %(device)s for volume.",
- {'device': self.host_device})
- 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
-
- @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
-
- 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
- """
- 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 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))
diff --git a/os_brick/initiator/LocalConnector.py b/os_brick/initiator/LocalConnector.py
deleted file mode 100644
index 2e9206e..0000000
--- a/os_brick/initiator/LocalConnector.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# 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 os_brick.i18n import _
-from os_brick.initiator import InitiatorConnector
-
-
-class LocalConnector(InitiatorConnector):
- """"Connector class to attach/detach File System backed volumes."""
-
- def __init__(self, root_helper, driver=None, execute=putils.execute,
- *args, **kwargs):
- super(LocalConnector, self).__init__(root_helper, driver=driver,
- execute=execute, *args, **kwargs)
-
- 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 []
-
- 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
-
- 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
diff --git a/os_brick/initiator/RBDConnector.py b/os_brick/initiator/RBDConnector.py
deleted file mode 100644
index b026173..0000000
--- a/os_brick/initiator/RBDConnector.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# 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 import exception
-
-from os_brick.i18n import _, _LE
-from os_brick.initiator import InitiatorConnector
-from os_brick.initiator import linuxrbd
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-
-class RBDConnector(InitiatorConnector):
- """"Connector class to attach/detach RBD volumes."""
-
- def __init__(self, root_helper, driver=None,
- execute=putils.execute, use_multipath=False,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
-
- super(RBDConnector, self).__init__(root_helper, driver=driver,
- execute=execute,
- device_scan_attempts=
- device_scan_attempts,
- *args, **kwargs)
-
- def get_volume_paths(self, connection_properties):
- # TODO(walter-boring): don't know where the connector
- # looks for RBD volumes.
- 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(walter-boring): not sure what to return here for RBD
- return []
-
- def _get_rbd_handle(self, connection_properties):
- try:
- user = connection_properties['auth_username']
- pool, volume = connection_properties['name'].split('/')
- 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(rbd_volume)
- return rbd_handle
-
- 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
- """
-
- rbd_handle = self._get_rbd_handle(connection_properties)
- return {'path': rbd_handle}
-
- 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:
- 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
diff --git a/os_brick/initiator/RemoteFsConnector.py b/os_brick/initiator/RemoteFsConnector.py
deleted file mode 100644
index d6f17e8..0000000
--- a/os_brick/initiator/RemoteFsConnector.py
+++ /dev/null
@@ -1,112 +0,0 @@
-# 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 _LW
-from os_brick.initiator import InitiatorConnector
-from os_brick.remotefs import remotefs
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-
-class RemoteFsConnector(InitiatorConnector):
- """Connector class to attach/detach NFS and GlusterFS volumes."""
-
- def __init__(self, mount_type, root_helper, driver=None,
- execute=putils.execute,
- device_scan_attempts=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)
-
- 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]
-
- 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}
-
- 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
diff --git a/os_brick/initiator/ScaleIOConnector.py b/os_brick/initiator/ScaleIOConnector.py
deleted file mode 100644
index aac4eac..0000000
--- a/os_brick/initiator/ScaleIOConnector.py
+++ /dev/null
@@ -1,481 +0,0 @@
-# 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 import exception
-from os_brick import utils
-
-from os_brick.initiator import InitiatorConnector
-from os_brick.i18n import _, _LI, _LW
-
-LOG = logging.getLogger(__name__)
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-
-
-class ScaleIOConnector(InitiatorConnector):
- """Class implements the connector driver for ScaleIO."""
- OK_STATUS_CODE = 200
- VOLUME_NOT_MAPPED_ERROR = 84
- VOLUME_ALREADY_MAPPED_ERROR = 81
- GET_GUID_CMD = ['drv_cfg', '--query_guid']
-
- def __init__(self, root_helper, driver=None, execute=putils.execute,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
- super(ScaleIOConnector, self).__init__(
- root_helper,
- driver=driver,
- execute=execute,
- 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
-
- 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.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
-
- @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,
- '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._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
-
- @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._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
diff --git a/os_brick/initiator/SheepdogConnector.py b/os_brick/initiator/SheepdogConnector.py
deleted file mode 100644
index b61664c..0000000
--- a/os_brick/initiator/SheepdogConnector.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# 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 import exception
-
-from os_brick.initiator import InitiatorConnector
-from os_brick.initiator import linuxsheepdog
-from os_brick.i18n import _, _LE
-
-DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
-LOG = logging.getLogger(__name__)
-
-
-class SheepdogConnector(InitiatorConnector):
- """"Connector class to attach/detach sheepdog volumes."""
-
- def __init__(self, root_helper, driver=None,
- execute=putils.execute, use_multipath=False,
- device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
- *args, **kwargs):
-
- super(SheepdogConnector, self).__init__(root_helper, driver=driver,
- execute=execute,
- device_scan_attempts=
- device_scan_attempts,
- *args, **kwargs)
-
- 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
-
- 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}
-
- 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
diff --git a/os_brick/initiator/connector.py b/os_brick/initiator/connector.py
index 0b1b6bf..480e0b0 100644
--- a/os_brick/initiator/connector.py
+++ b/os_brick/initiator/connector.py
@@ -25,6 +25,7 @@ import re
import socket
import sys
+from os_brick.initiator import utils as connector_utils
from os_brick import utils
from oslo_concurrency import lockutils
@@ -48,19 +49,18 @@ OS_TYPE_LINUX = 'LINUX'
connector_list = [
- 'os_brick.initiator.connector.InitiatorConnector',
- 'os_brick.initiator.ISCSIConnector',
- 'os_brick.initiator.FibreChannelConnector',
- 'os_brick.initiator.FibreChannelConnectorS390X',
- 'os_brick.initiator.AoEConnector',
- 'os_brick.initiator.RemoteFsConnector',
- 'os_brick.initiator.RBDConnector',
- 'os_brick.initiator.LocalConnector',
- 'os_brick.initiator.DRBDConnector',
- 'os_brick.initiator.HuaweiStorHyperConnector',
- 'os_brick.initiator.HGSTConnector',
- 'os_brick.initiator.ScaleIOConnector',
- 'os_brick.initiator.DISCOConnector',
+ 'os_brick.initiator.connectors.iscsi.ISCSIConnector',
+ 'os_brick.initiator.connectors.fibre_channel.FibreChannelConnector',
+ 'os_brick.initiator.connectors.fibre_channel.FibreChannelConnectorS390X',
+ 'os_brick.initiator.connectors.aoe.AoEConnector',
+ 'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
+ 'os_brick.initiator.connectors.rbd.RBDConnector',
+ 'os_brick.initiator.connectors.local.LocalConnector',
+ 'os_brick.initiator.connectors.drbd.DRBDConnector',
+ 'os_brick.initiator.connectors.huawei.HuaweiStorHyperConnector',
+ 'os_brick.initiator.connectors.hgst.HGSTConnector',
+ 'os_brick.initiator.connectors.scaleio.ScaleIOConnector',
+ 'os_brick.initiator.connectors.disco.DISCOConnector',
]
@@ -95,9 +95,13 @@ def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath,
props['os_type'] = sys.platform
props['ip'] = my_ip
props['host'] = host if host else socket.gethostname()
+ import pprint
for item in connector_list:
+ pprint.pprint(item)
+
connector = importutils.import_class(item)
+ pprint.pprint(connector)
if (utils.platform_matches(props['platform'], connector.platform) and
utils.os_matches(props['os_type'], connector.os_type)):
@@ -111,3 +115,20 @@ def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath,
execute=execute))
return props
+
+
+class InitiatorConnector(object):
+ """Exists to maintain backwards compatibility."""
+
+ @staticmethod
+ def factor(protocol, root_helper, driver=None,
+ use_multipath=False,
+ device_rescan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ arch=None,
+ *args, **kwargs):
+ return connector_utils.connector_factory(
+ protocol, root_helper, driver=driver,
+ use_multipath=use_multipath,
+ device_rescan_attempts=device_rescan_attempts,
+ arch=arch,
+ *args, **kwargs)
diff --git a/os_brick/initiator/connectors/__init__.py b/os_brick/initiator/connectors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/os_brick/initiator/connectors/aoe.py b/os_brick/initiator/connectors/aoe.py
new file mode 100644
index 0000000..09724bc
--- /dev/null
+++ b/os_brick/initiator/connectors/aoe.py
@@ -0,0 +1,170 @@
+# 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_concurrency import processutils as putils
+from oslo_log import log as logging
+from oslo_service import loopingcall
+
+from os_brick import exception
+
+from os_brick.i18n import _LW
+from os_brick.initiator import initiator_connector
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+
+class AoEConnector(initiator_connector.InitiatorConnector):
+ """Connector class to attach/detach AoE volumes."""
+ def __init__(self, root_helper, driver=None,
+ execute=putils.execute,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+ super(AoEConnector, self).__init__(
+ root_helper,
+ driver=driver,
+ execute=execute,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
+
+ 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
+
+ @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
+
+ @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
diff --git a/os_brick/initiator/connectors/disco.py b/os_brick/initiator/connectors/disco.py
new file mode 100644
index 0000000..672fc4e
--- /dev/null
+++ b/os_brick/initiator/connectors/disco.py
@@ -0,0 +1,201 @@
+# 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_concurrency import processutils as putils
+from oslo_log import log as logging
+import six
+
+from os_brick import exception
+
+from os_brick.i18n import _, _LI, _LE
+from os_brick.initiator import initiator_connector
+
+LOG = logging.getLogger(__name__)
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+synchronized = lockutils.synchronized_with_prefix('os-brick-')
+
+
+class DISCOConnector(initiator_connector.InitiatorConnector):
+ """Class implements the connector driver for DISCO."""
+
+ DISCO_PREFIX = 'dms'
+
+ def __init__(self, root_helper, driver=None, execute=putils.execute,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+ """Init DISCO connector."""
+ super(DISCOConnector, self).__init__(
+ root_helper,
+ driver=driver,
+ execute=execute,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs
+ )
+ LOG.info(_LI("Init DISCO connector"))
+
+ self.server_port = None
+ self.server_ip = None
+
+ 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
+
+ @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
+
+ @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
diff --git a/os_brick/initiator/connectors/drbd.py b/os_brick/initiator/connectors/drbd.py
new file mode 100644
index 0000000..fdbd27f
--- /dev/null
+++ b/os_brick/initiator/connectors/drbd.py
@@ -0,0 +1,101 @@
+# 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 import initiator_connector
+
+
+class DRBDConnector(initiator_connector.InitiatorConnector):
+ """"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
+
+ 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)
+
+ 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
+
+ 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
diff --git a/os_brick/initiator/connectors/fake.py b/os_brick/initiator/connectors/fake.py
new file mode 100644
index 0000000..4c32e1b
--- /dev/null
+++ b/os_brick/initiator/connectors/fake.py
@@ -0,0 +1,43 @@
+# 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 import initiator_connector
+
+
+class FakeConnector(initiator_connector.InitiatorConnector):
+
+ 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']
diff --git a/os_brick/initiator/connectors/fibre_channel.py b/os_brick/initiator/connectors/fibre_channel.py
new file mode 100644
index 0000000..dae1991
--- /dev/null
+++ b/os_brick/initiator/connectors/fibre_channel.py
@@ -0,0 +1,288 @@
+# 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 re
+
+from oslo_concurrency import lockutils
+from oslo_concurrency import processutils as putils
+from oslo_log import log as logging
+from oslo_service import loopingcall
+import six
+
+from os_brick import exception
+
+from os_brick.i18n import _LE, _LW
+from os_brick.initiator import initiator_connector
+from os_brick.initiator import linuxfc
+from os_brick.initiator import linuxscsi
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+synchronized = lockutils.synchronized_with_prefix('os-brick-')
+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+")
+
+LOG = logging.getLogger(__name__)
+
+
+class FibreChannelConnector(initiator_connector.InitiatorConnector):
+ """Connector class to attach/detach Fibre Channel volumes."""
+
+ def __init__(self, root_helper, driver=None,
+ execute=putils.execute, use_multipath=False,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+ self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute)
+ 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)
+
+ 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
+
+ @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()
+
+ @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
+
+ @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
diff --git a/os_brick/initiator/connectors/fibre_channel_s390x.py b/os_brick/initiator/connectors/fibre_channel_s390x.py
new file mode 100644
index 0000000..4ae534b
--- /dev/null
+++ b/os_brick/initiator/connectors/fibre_channel_s390x.py
@@ -0,0 +1,87 @@
+# 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.initiator.connectors import fibre_channel
+from os_brick.initiator import linuxfc
+from os_brick.initiator import linuxscsi
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+
+class FibreChannelConnectorS390X(fibre_channel.FibreChannelConnector):
+ """Connector class to attach/detach Fibre Channel volumes on S390X arch."""
+
+ def __init__(self, root_helper, driver=None,
+ execute=putils.execute, use_multipath=False,
+ device_scan_attempts=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._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute)
+ 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)
diff --git a/os_brick/initiator/connectors/hgst.py b/os_brick/initiator/connectors/hgst.py
new file mode 100644
index 0000000..3d0e88a
--- /dev/null
+++ b/os_brick/initiator/connectors/hgst.py
@@ -0,0 +1,176 @@
+# 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.
+import os
+import socket
+
+from oslo_concurrency import processutils as putils
+from oslo_log import log as logging
+
+from os_brick import exception
+
+from os_brick.i18n import _, _LE
+from os_brick.initiator import initiator_connector
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+
+class HGSTConnector(initiator_connector.InitiatorConnector):
+ """Connector class to attach/detach HGST volumes."""
+ VGCCLUSTER = 'vgc-cluster'
+
+ def __init__(self, root_helper, driver=None,
+ execute=putils.execute,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+ super(HGSTConnector, self).__init__(root_helper, driver=driver,
+ execute=execute,
+ device_scan_attempts=
+ device_scan_attempts,
+ *args, **kwargs)
+ self._vgc_host = None
+
+ 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]
+
+ 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
+
+ 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
diff --git a/os_brick/initiator/connectors/huawei.py b/os_brick/initiator/connectors/huawei.py
new file mode 100644
index 0000000..8dbfe1e
--- /dev/null
+++ b/os_brick/initiator/connectors/huawei.py
@@ -0,0 +1,186 @@
+# 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_concurrency import processutils as putils
+from oslo_log import log as logging
+
+from os_brick import exception
+
+from os_brick.i18n import _, _LE
+from os_brick.initiator import initiator_connector
+
+LOG = logging.getLogger(__name__)
+synchronized = lockutils.synchronized_with_prefix('os-brick-')
+
+
+class HuaweiStorHyperConnector(initiator_connector.InitiatorConnector):
+ """"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, execute=putils.execute,
+ *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,
+ execute=execute,
+ *args, **kwargs)
+
+ 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']
+
+ @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
+
+ @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
diff --git a/os_brick/initiator/connectors/iscsi.py b/os_brick/initiator/connectors/iscsi.py
new file mode 100644
index 0000000..687a375
--- /dev/null
+++ b/os_brick/initiator/connectors/iscsi.py
@@ -0,0 +1,834 @@
+# 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 import exception
+
+from os_brick.i18n import _, _LE, _LI, _LW
+from os_brick.initiator import initiator_connector
+from os_brick.initiator import linuxscsi
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+synchronized = lockutils.synchronized_with_prefix('os-brick-')
+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+")
+
+LOG = logging.getLogger(__name__)
+
+
+class ISCSIConnector(initiator_connector.InitiatorConnector):
+ """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=putils.execute, use_multipath=False,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ transport='default', *args, **kwargs):
+ self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute)
+ 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)
+
+ 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:
+ raise exception.TargetPortalNotFound(
+ target_portal=connection_properties['target_portal'])
+
+ 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 _iterate_all_targets(self, connection_properties):
+ for ip, iqn, lun in self._get_all_targets(connection_properties):
+ props = copy.deepcopy(connection_properties)
+ props['target_portal'] = ip
+ 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))]
+
+ 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)
+
+ @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()
+
+ @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
+
+ @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' # noqa
+ % 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(MULTIPATH_ERROR_REGEX, line)]
+ mpath_dev = None
+ mpath_map = {}
+ for line in out.splitlines():
+ m = MULTIPATH_DEV_CHECK_REGEX.split(line)
+ if len(m) >= 2:
+ mpath_dev = '/dev/mapper/' + m[0].split(" ")[0]
+ continue
+ m = 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])
diff --git a/os_brick/initiator/connectors/local.py b/os_brick/initiator/connectors/local.py
new file mode 100644
index 0000000..c895c14
--- /dev/null
+++ b/os_brick/initiator/connectors/local.py
@@ -0,0 +1,72 @@
+# 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 os_brick.i18n import _
+from os_brick.initiator import initiator_connector
+
+
+class LocalConnector(initiator_connector.InitiatorConnector):
+ """"Connector class to attach/detach File System backed volumes."""
+
+ def __init__(self, root_helper, driver=None, execute=putils.execute,
+ *args, **kwargs):
+ super(LocalConnector, self).__init__(root_helper, driver=driver,
+ execute=execute, *args, **kwargs)
+
+ 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 []
+
+ 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
+
+ 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
diff --git a/os_brick/initiator/connectors/rbd.py b/os_brick/initiator/connectors/rbd.py
new file mode 100644
index 0000000..e15466f
--- /dev/null
+++ b/os_brick/initiator/connectors/rbd.py
@@ -0,0 +1,118 @@
+# 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 import exception
+
+from os_brick.i18n import _, _LE
+from os_brick.initiator import initiator_connector
+from os_brick.initiator import linuxrbd
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+
+class RBDConnector(initiator_connector.InitiatorConnector):
+ """"Connector class to attach/detach RBD volumes."""
+
+ def __init__(self, root_helper, driver=None,
+ execute=putils.execute, use_multipath=False,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+
+ super(RBDConnector, self).__init__(root_helper, driver=driver,
+ execute=execute,
+ device_scan_attempts=
+ device_scan_attempts,
+ *args, **kwargs)
+
+ def get_volume_paths(self, connection_properties):
+ # TODO(walter-boring): don't know where the connector
+ # looks for RBD volumes.
+ 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(walter-boring): not sure what to return here for RBD
+ return []
+
+ def _get_rbd_handle(self, connection_properties):
+ try:
+ user = connection_properties['auth_username']
+ pool, volume = connection_properties['name'].split('/')
+ 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(rbd_volume)
+ return rbd_handle
+
+ 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
+ """
+
+ rbd_handle = self._get_rbd_handle(connection_properties)
+ return {'path': rbd_handle}
+
+ 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:
+ 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
diff --git a/os_brick/initiator/connectors/remotefs.py b/os_brick/initiator/connectors/remotefs.py
new file mode 100644
index 0000000..3714d24
--- /dev/null
+++ b/os_brick/initiator/connectors/remotefs.py
@@ -0,0 +1,112 @@
+# 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 _LW
+from os_brick.initiator import initiator_connector
+from os_brick.remotefs import remotefs
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+
+class RemoteFsConnector(initiator_connector.InitiatorConnector):
+ """Connector class to attach/detach NFS and GlusterFS volumes."""
+
+ def __init__(self, mount_type, root_helper, driver=None,
+ execute=putils.execute,
+ device_scan_attempts=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)
+
+ 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]
+
+ 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}
+
+ 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
diff --git a/os_brick/initiator/connectors/scaleio.py b/os_brick/initiator/connectors/scaleio.py
new file mode 100644
index 0000000..4cc27cf
--- /dev/null
+++ b/os_brick/initiator/connectors/scaleio.py
@@ -0,0 +1,481 @@
+# 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 import exception
+from os_brick import utils
+
+from os_brick.initiator import initiator_connector
+from os_brick.i18n import _, _LI, _LW
+
+LOG = logging.getLogger(__name__)
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+
+
+class ScaleIOConnector(initiator_connector.InitiatorConnector):
+ """Class implements the connector driver for ScaleIO."""
+ OK_STATUS_CODE = 200
+ VOLUME_NOT_MAPPED_ERROR = 84
+ VOLUME_ALREADY_MAPPED_ERROR = 81
+ GET_GUID_CMD = ['drv_cfg', '--query_guid']
+
+ def __init__(self, root_helper, driver=None, execute=putils.execute,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+ super(ScaleIOConnector, self).__init__(
+ root_helper,
+ driver=driver,
+ execute=execute,
+ 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
+
+ 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.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
+
+ @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,
+ '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._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
+
+ @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._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
diff --git a/os_brick/initiator/connectors/sheepdog.py b/os_brick/initiator/connectors/sheepdog.py
new file mode 100644
index 0000000..f3e9808
--- /dev/null
+++ b/os_brick/initiator/connectors/sheepdog.py
@@ -0,0 +1,121 @@
+# 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 import exception
+
+from os_brick.initiator import initiator_connector
+from os_brick.initiator import linuxsheepdog
+from os_brick.i18n import _, _LE
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+
+class SheepdogConnector(initiator_connector.InitiatorConnector):
+ """"Connector class to attach/detach sheepdog volumes."""
+
+ def __init__(self, root_helper, driver=None,
+ execute=putils.execute, use_multipath=False,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+
+ super(SheepdogConnector, self).__init__(root_helper, driver=driver,
+ execute=execute,
+ device_scan_attempts=
+ device_scan_attempts,
+ *args, **kwargs)
+
+ 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
+
+ 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}
+
+ 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
diff --git a/os_brick/initiator/initiator_connector.py b/os_brick/initiator/initiator_connector.py
new file mode 100644
index 0000000..927ddd8
--- /dev/null
+++ b/os_brick/initiator/initiator_connector.py
@@ -0,0 +1,278 @@
+# 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
+import glob
+import os
+
+from oslo_concurrency import processutils as putils
+from oslo_log import log as logging
+import six
+
+from os_brick import exception
+from os_brick import executor
+
+from os_brick.initiator import host_driver
+from os_brick.initiator import linuxscsi
+from os_brick.i18n import _LE, _LW
+
+DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
+LOG = logging.getLogger(__name__)
+
+PLATFORM_ALL = 'ALL'
+OS_TYPE_LINUX = 'LINUX'
+
+
+@six.add_metaclass(abc.ABCMeta)
+class InitiatorConnector(executor.Executor):
+
+ # This object can be used on any platform (x86, S390)
+ platform = PLATFORM_ALL
+
+ # This object can be used on any os type (linux, windows)
+ # TODO(walter-boring) This class stil has a reliance on
+ # linuxscsi object, making it specific to linux. Need to fix that.
+ os_type = OS_TYPE_LINUX
+
+ def __init__(self, root_helper, driver=None, execute=None,
+ device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
+ *args, **kwargs):
+ super(InitiatorConnector, self).__init__(root_helper, execute=execute,
+ *args, **kwargs)
+ if not driver:
+ driver = host_driver.HostDriver()
+ self.set_driver(driver)
+ self.device_scan_attempts = device_scan_attempts
+ self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute)
+
+ def set_driver(self, driver):
+ """The driver is used to find used LUNs."""
+ self.driver = driver
+
+ @staticmethod
+ def get_connector_properties(root_helper, *args, **kwargs):
+ """The generic connector properties."""
+ multipath = kwargs['multipath']
+ enforce_multipath = kwargs['enforce_multipath']
+ props = {}
+ # TODO(walter-boring) move this into platform specific lib
+ props['multipath'] = (multipath and
+ linuxscsi.LinuxSCSI.is_multipath_running(
+ enforce_multipath, root_helper))
+
+ return props
+
+ 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
+ """
+ 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 %(info)s."),
+ {"path": path, "error": e.stderr,
+ "info": info})
+ return False
+ # If the info is none, the path does not exist.
+ if info is None:
+ return False
+ return True
+
+ 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:
+ mpath_info = self._linuxscsi.find_multipath_device(
+ device_name)
+ 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 = self.host_device
+ LOG.debug("Unable to find multipath device name for "
+ "volume. Using path %(device)s for volume.",
+ {'device': self.host_device})
+ 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
+
+ @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
+
+ 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
+ """
+ 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 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))
diff --git a/os_brick/initiator/utils.py b/os_brick/initiator/utils.py
index 7e700a7..0e3d17e 100644
--- a/os_brick/initiator/utils.py
+++ b/os_brick/initiator/utils.py
@@ -17,19 +17,19 @@ import platform
from oslo_log import log as logging
from os_brick.i18n import _
-from os_brick.initiator import AoEConnector
-from os_brick.initiator import DISCOConnector
-from os_brick.initiator import DRBDConnector
-from os_brick.initiator import FCConnector as FibreChannelConnector
-from os_brick.initiator import FCConnectorS390X as FibreChannelConnectorS390X
-from os_brick.initiator import HGSTConnector
-from os_brick.initiator import HuaweiStorHyperConnector
-from os_brick.initiator import ISCSIConnector
-from os_brick.initiator import LocalConnector
-from os_brick.initiator import RBDConnector
-from os_brick.initiator import RemoteFsConnector
-from os_brick.initiator import ScaleIOConnector
-from os_brick.initiator import SheepdogConnector
+from os_brick.initiator.connectors import aoe
+from os_brick.initiator.connectors import disco
+from os_brick.initiator.connectors import drbd
+from os_brick.initiator.connectors import fibre_channel
+from os_brick.initiator.connectors import fibre_channel_s390x
+from os_brick.initiator.connectors import hgst
+from os_brick.initiator.connectors import huawei
+from os_brick.initiator.connectors import iscsi
+from os_brick.initiator.connectors import local
+from os_brick.initiator.connectors import rbd
+from os_brick.initiator.connectors import remotefs
+from os_brick.initiator.connectors import scaleio
+from os_brick.initiator.connectors import sheepdog
LOG = logging.getLogger(__name__)
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
@@ -77,80 +77,79 @@ def factory(protocol, root_helper, driver=None,
# from the nova LibvirtISERVolumeDriver
if protocol == ISER:
kwargs.update({'transport': 'iser'})
- return ISCSIConnector(root_helper=root_helper,
- driver=driver,
- use_multipath=use_multipath,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return iscsi.ISCSIConnector(
+ root_helper=root_helper, driver=driver,
+ use_multipath=use_multipath,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
elif protocol == FIBRE_CHANNEL:
if arch in (S390, S390X):
- return FibreChannelConnectorS390X(
+ return fibre_channel_s390x.FibreChannelConnectorS390X(
root_helper=root_helper,
driver=driver,
use_multipath=use_multipath,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
else:
- return FibreChannelConnector(
+ return fibre_channel.FibreChannelConnector(
root_helper=root_helper,
driver=driver,
use_multipath=use_multipath,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
elif protocol == AOE:
- return AoEConnector(root_helper=root_helper,
- driver=driver,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return aoe.AoEConnector(
+ root_helper=root_helper, driver=driver,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
elif protocol in (NFS, GLUSTERFS, SCALITY, QUOBYTE, VZSTORAGE):
- return RemoteFsConnector(mount_type=protocol.lower(),
- root_helper=root_helper,
- driver=driver,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return remotefs.RemoteFsConnector(
+ mount_type=protocol.lower(), root_helper=root_helper,
+ driver=driver, device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
elif protocol == DRBD:
- return DRBDConnector(root_helper=root_helper,
- driver=driver,
- *args, **kwargs)
+ return drbd.DRBDConnector(
+ root_helper=root_helper, driver=driver,
+ *args, **kwargs)
elif protocol == LOCAL:
- return LocalConnector(root_helper=root_helper,
- driver=driver,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return local.LocalConnector(
+ root_helper=root_helper, driver=driver,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
elif protocol == HUAWEISDSHYPERVISOR:
- return HuaweiStorHyperConnector(
+ return huawei.HuaweiStorHyperConnector(
root_helper=root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
elif protocol == HGST:
- return HGSTConnector(root_helper=root_helper,
- driver=driver,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return hgst.HGSTConnector(
+ root_helper=root_helper, driver=driver,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
elif protocol == RBD:
- return RBDConnector(root_helper=root_helper,
- driver=driver,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return rbd.RBDConnector(
+ root_helper=root_helper, driver=driver,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
elif protocol == SCALEIO:
- return ScaleIOConnector(
+ return scaleio.ScaleIOConnector(
root_helper=root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
elif protocol == DISCO:
- return DISCOConnector(
+ return disco.DISCOConnector(
root_helper=root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs
)
elif protocol == SHEEPDOG:
- return SheepdogConnector(root_helper=root_helper,
- driver=driver,
- device_scan_attempts=device_scan_attempts,
- *args, **kwargs)
+ return sheepdog.SheepdogConnector(
+ root_helper=root_helper, driver=driver,
+ device_scan_attempts=device_scan_attempts,
+ *args, **kwargs)
else:
msg = (_("Invalid InitiatorConnector protocol "
"specified %(protocol)s") %
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment