--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 Mellanox Technologies. 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.
+"""
+Helper code for the iSER volume driver.
+
+"""
+
+
+import os
+import re
+
+from oslo.config import cfg
+
+from cinder import exception
+from cinder.openstack.common import fileutils
+from cinder.openstack.common import log as logging
+from cinder import utils
+from cinder.volume import utils as volume_utils
+
+
+LOG = logging.getLogger(__name__)
+
+iser_helper_opt = [cfg.StrOpt('iser_helper',
+ default='tgtadm',
+ help='iser target user-land tool to use'),
+ cfg.StrOpt('volumes_dir',
+ default='$state_path/volumes',
+ help='Volume configuration file storage '
+ 'directory'
+ )
+ ]
+
+CONF = cfg.CONF
+CONF.register_opts(iser_helper_opt)
+CONF.import_opt('volume_name_template', 'cinder.db')
+
+
+class TargetAdmin(object):
+ """iSER target administration.
+
+ Base class for iSER target admin helpers.
+ """
+
+ def __init__(self, cmd, execute):
+ self._cmd = cmd
+ self.set_execute(execute)
+
+ def set_execute(self, execute):
+ """Set the function to be used to execute commands."""
+ self._execute = execute
+
+ def _run(self, *args, **kwargs):
+ self._execute(self._cmd, *args, run_as_root=True, **kwargs)
+
+ def create_iser_target(self, name, tid, lun, path,
+ chap_auth=None, **kwargs):
+ """Create a iSER target and logical unit."""
+ raise NotImplementedError()
+
+ def remove_iser_target(self, tid, lun, vol_id, **kwargs):
+ """Remove a iSER target and logical unit."""
+ raise NotImplementedError()
+
+ def _new_target(self, name, tid, **kwargs):
+ """Create a new iSER target."""
+ raise NotImplementedError()
+
+ def _delete_target(self, tid, **kwargs):
+ """Delete a target."""
+ raise NotImplementedError()
+
+ def show_target(self, tid, iqn=None, **kwargs):
+ """Query the given target ID."""
+ raise NotImplementedError()
+
+ def _new_logicalunit(self, tid, lun, path, **kwargs):
+ """Create a new LUN on a target using the supplied path."""
+ raise NotImplementedError()
+
+ def _delete_logicalunit(self, tid, lun, **kwargs):
+ """Delete a logical unit from a target."""
+ raise NotImplementedError()
+
+
+class TgtAdm(TargetAdmin):
+ """iSER target administration using tgtadm."""
+
+ def __init__(self, execute=utils.execute):
+ super(TgtAdm, self).__init__('tgtadm', execute)
+
+ def _get_target(self, iqn):
+ (out, err) = self._execute('tgt-admin', '--show', run_as_root=True)
+ lines = out.split('\n')
+ for line in lines:
+ if iqn in line:
+ parsed = line.split()
+ tid = parsed[1]
+ return tid[:-1]
+
+ return None
+
+ def create_iser_target(self, name, tid, lun, path,
+ chap_auth=None, **kwargs):
+ # Note(jdg) tid and lun aren't used by TgtAdm but remain for
+ # compatibility
+
+ fileutils.ensure_tree(CONF.volumes_dir)
+
+ vol_id = name.split(':')[1]
+ if chap_auth is None:
+ volume_conf = """
+ <target %s>
+ driver iser
+ backing-store %s
+ </target>
+ """ % (name, path)
+ else:
+ volume_conf = """
+ <target %s>
+ driver iser
+ backing-store %s
+ %s
+ </target>
+ """ % (name, path, chap_auth)
+
+ LOG.info(_('Creating iser_target for: %s') % vol_id)
+ volumes_dir = CONF.volumes_dir
+ volume_path = os.path.join(volumes_dir, vol_id)
+
+ f = open(volume_path, 'w+')
+ f.write(volume_conf)
+ f.close()
+
+ old_persist_file = None
+ old_name = kwargs.get('old_name', None)
+ if old_name is not None:
+ old_persist_file = os.path.join(volumes_dir, old_name)
+
+ try:
+ (out, err) = self._execute('tgt-admin',
+ '--update',
+ name,
+ run_as_root=True)
+ except exception.ProcessExecutionError as e:
+ LOG.error(_("Failed to create iser target for volume "
+ "id:%(vol_id)s: %(e)s")
+ % {'vol_id': vol_id, 'e': str(e)})
+
+ #Don't forget to remove the persistent file we created
+ os.unlink(volume_path)
+ raise exception.ISERTargetCreateFailed(volume_id=vol_id)
+
+ iqn = '%s%s' % (CONF.iser_target_prefix, vol_id)
+ tid = self._get_target(iqn)
+ if tid is None:
+ LOG.error(_("Failed to create iser target for volume "
+ "id:%(vol_id)s. Please ensure your tgtd config file "
+ "contains 'include %(volumes_dir)s/*'") % locals())
+ raise exception.NotFound()
+
+ if old_persist_file is not None and os.path.exists(old_persist_file):
+ os.unlink(old_persist_file)
+
+ return tid
+
+ def remove_iser_target(self, tid, lun, vol_id, **kwargs):
+ LOG.info(_('Removing iser_target for: %s') % vol_id)
+ vol_uuid_file = CONF.volume_name_template % vol_id
+ volume_path = os.path.join(CONF.volumes_dir, vol_uuid_file)
+ if os.path.isfile(volume_path):
+ iqn = '%s%s' % (CONF.iser_target_prefix,
+ vol_uuid_file)
+ else:
+ raise exception.ISERTargetRemoveFailed(volume_id=vol_id)
+ try:
+ # NOTE(vish): --force is a workaround for bug:
+ # https://bugs.launchpad.net/cinder/+bug/1159948
+ self._execute('tgt-admin',
+ '--force',
+ '--delete',
+ iqn,
+ run_as_root=True)
+ except exception.ProcessExecutionError as e:
+ LOG.error(_("Failed to remove iser target for volume "
+ "id:%(vol_id)s: %(e)s")
+ % {'vol_id': vol_id, 'e': str(e)})
+ raise exception.ISERTargetRemoveFailed(volume_id=vol_id)
+
+ os.unlink(volume_path)
+
+ def show_target(self, tid, iqn=None, **kwargs):
+ if iqn is None:
+ raise exception.InvalidParameterValue(
+ err=_('valid iqn needed for show_target'))
+
+ tid = self._get_target(iqn)
+ if tid is None:
+ raise exception.NotFound()
+
+
+class FakeIserHelper(object):
+
+ def __init__(self):
+ self.tid = 1
+
+ def set_execute(self, execute):
+ self._execute = execute
+
+ def create_iser_target(self, *args, **kwargs):
+ self.tid += 1
+ return self.tid
+
+
+def get_target_admin():
+ if CONF.iser_helper == 'fake':
+ return FakeIserHelper()
+ else:
+ return TgtAdm()
message = _("Failed to remove iscsi target for volume %(volume_id)s.")
+class ISERTargetNotFoundForVolume(NotFound):
+ message = _("No target id found for volume %(volume_id)s.")
+
+
+class ISERTargetCreateFailed(CinderException):
+ message = _("Failed to create iser target for volume %(volume_id)s.")
+
+
+class ISERTargetAttachFailed(CinderException):
+ message = _("Failed to attach iser target for volume %(volume_id)s.")
+
+
+class ISERTargetRemoveFailed(CinderException):
+ message = _("Failed to remove iser target for volume %(volume_id)s.")
+
+
class DiskNotFound(NotFound):
message = _("No disk at %(location)s")
return (None, None)
+class FakeISERDriver(FakeISCSIDriver):
+ """Logs calls instead of executing."""
+ def __init__(self, *args, **kwargs):
+ super(FakeISERDriver, self).__init__(execute=self.fake_execute,
+ *args, **kwargs)
+
+ def initialize_connection(self, volume, connector):
+ return {
+ 'driver_volume_type': 'iser',
+ 'data': {}
+ }
+
+ @staticmethod
+ def fake_execute(cmd, *_args, **_kwargs):
+ """Execute that simply logs the command."""
+ LOG.debug(_("FAKE ISER: %s"), cmd)
+ return (None, None)
+
+
class LoggingVolumeDriver(driver.VolumeDriver):
"""Logs and records calls, for unit tests."""
FLAGS = flags.FLAGS
flags.DECLARE('iscsi_num_targets', 'cinder.volume.drivers.lvm')
+flags.DECLARE('iser_num_targets', 'cinder.volume.drivers.lvm')
flags.DECLARE('policy_file', 'cinder.policy')
flags.DECLARE('volume_driver', 'cinder.volume.manager')
flags.DECLARE('xiv_proxy', 'cinder.volume.drivers.xiv')
conf.set_default('volume_driver',
'cinder.tests.fake_driver.FakeISCSIDriver')
conf.set_default('iscsi_helper', 'fake')
+ conf.set_default('iser_helper', 'fake')
conf.set_default('connection_type', 'fake')
conf.set_default('fake_rabbit', True)
conf.set_default('rpc_backend', 'cinder.openstack.common.rpc.impl_fake')
conf.set_default('iscsi_num_targets', 8)
+ conf.set_default('iser_num_targets', 8)
conf.set_default('verbose', True)
conf.set_default('connection', 'sqlite://', group='database')
conf.set_default('sqlite_synchronous', False)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 Mellanox Technologies. 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.path
+import shutil
+import string
+import tempfile
+
+from cinder.brick.iser import iser
+from cinder import test
+from cinder.volume import utils as volume_utils
+
+
+class TargetAdminTestCase(object):
+
+ def setUp(self):
+ self.cmds = []
+
+ self.tid = 1
+ self.target_name = 'iqn.2011-09.org.foo.bar:blaa'
+ self.lun = 10
+ self.path = '/foo'
+ self.vol_id = 'blaa'
+
+ self.script_template = None
+ self.stubs.Set(os.path, 'isfile', lambda _: True)
+ self.stubs.Set(os, 'unlink', lambda _: '')
+ self.stubs.Set(iser.TgtAdm, '_get_target', self.fake_get_target)
+
+ def fake_init(obj):
+ return
+
+ def fake_get_target(obj, iqn):
+ return 1
+
+ def get_script_params(self):
+ return {'tid': self.tid,
+ 'target_name': self.target_name,
+ 'lun': self.lun,
+ 'path': self.path}
+
+ def get_script(self):
+ return self.script_template % self.get_script_params()
+
+ def fake_execute(self, *cmd, **kwargs):
+ self.cmds.append(string.join(cmd))
+ return "", None
+
+ def clear_cmds(self):
+ self.cmds = []
+
+ def verify_cmds(self, cmds):
+ self.assertEqual(len(cmds), len(self.cmds))
+ for a, b in zip(cmds, self.cmds):
+ self.assertEqual(a, b)
+
+ def verify(self):
+ script = self.get_script()
+ cmds = []
+ for line in script.split('\n'):
+ if not line.strip():
+ continue
+ cmds.append(line)
+ self.verify_cmds(cmds)
+
+ def run_commands(self):
+ tgtadm = iser.get_target_admin()
+ tgtadm.set_execute(self.fake_execute)
+ tgtadm.create_iser_target(self.target_name, self.tid,
+ self.lun, self.path)
+ tgtadm.show_target(self.tid, iqn=self.target_name)
+ tgtadm.remove_iser_target(self.tid, self.lun, self.vol_id)
+
+ def test_target_admin(self):
+ self.clear_cmds()
+ self.run_commands()
+ self.verify()
+
+
+class TgtAdmTestCase(test.TestCase, TargetAdminTestCase):
+
+ def setUp(self):
+ super(TgtAdmTestCase, self).setUp()
+ TargetAdminTestCase.setUp(self)
+ self.persist_tempdir = tempfile.mkdtemp()
+ self.flags(iser_helper='tgtadm')
+ self.flags(volumes_dir=self.persist_tempdir)
+ self.script_template = "\n".join([
+ 'tgt-admin --update iqn.2011-09.org.foo.bar:blaa',
+ 'tgt-admin --force '
+ '--delete iqn.2010-10.org.iser.openstack:volume-blaa'])
+
+ def tearDown(self):
+ try:
+ shutil.rmtree(self.persist_tempdir)
+ except OSError:
+ pass
+ super(TgtAdmTestCase, self).tearDown()
from oslo.config import cfg
from cinder.brick.iscsi import iscsi
+from cinder.brick.iser import iser
from cinder import context
from cinder import db
from cinder import exception
self.assertEquals(stats['free_capacity_gb'], float('0.52'))
+class ISERTestCase(ISCSITestCase):
+ """Test Case for ISERDriver."""
+ driver_name = "cinder.volume.drivers.lvm.LVMISERDriver"
+
+ def test_do_iscsi_discovery(self):
+ configuration = mox.MockObject(conf.Configuration)
+ configuration.iser_ip_address = '0.0.0.0'
+ configuration.append_config_values(mox.IgnoreArg())
+
+ iser_driver = driver.ISERDriver(configuration=configuration)
+ iser_driver._execute = lambda *a, **kw: \
+ ("%s dummy" % CONF.iser_ip_address, '')
+ volume = {"name": "dummy",
+ "host": "0.0.0.0"}
+ iser_driver._do_iser_discovery(volume)
+
+ def test_get_iscsi_properties(self):
+ volume = {"provider_location": '',
+ "id": "0",
+ "provider_auth": "a b c"}
+ iser_driver = driver.ISERDriver()
+ iser_driver._do_iser_discovery = lambda v: "0.0.0.0:0000,0 iqn:iqn 0"
+ result = iser_driver._get_iser_properties(volume)
+ self.assertEquals(result["target_portal"], "0.0.0.0:0000")
+ self.assertEquals(result["target_iqn"], "iqn:iqn")
+ self.assertEquals(result["target_lun"], 0)
+
+
class FibreChannelTestCase(DriverTestCase):
- """Test Case for FibreChannelDriver"""
+ """Test Case for FibreChannelDriver."""
driver_name = "cinder.volume.driver.FibreChannelDriver"
def test_initialize_connection(self):
help='The percentage of backend capacity is reserved'),
cfg.IntOpt('num_iscsi_scan_tries',
default=3,
- help='number of times to rescan iSCSI target to find volume'),
+ help='The maximum number of times to rescan iSCSI target'
+ 'to find volume'),
cfg.IntOpt('iscsi_num_targets',
default=100,
- help='Number of iscsi target ids per host'),
+ help='The maximum number of iscsi target ids per host'),
cfg.StrOpt('iscsi_target_prefix',
default='iqn.2010-10.org.openstack:',
help='prefix for iscsi volumes'),
cfg.StrOpt('iscsi_ip_address',
default='$my_ip',
- help='The port that the iSCSI daemon is listening on'),
+ help='The IP address that the iSCSI daemon is listening on'),
cfg.IntOpt('iscsi_port',
default=3260,
help='The port that the iSCSI daemon is listening on'),
+ cfg.IntOpt('num_iser_scan_tries',
+ default=3,
+ help='The maximum number of times to rescan iSER target'
+ 'to find volume'),
+ cfg.IntOpt('iser_num_targets',
+ default=100,
+ help='The maximum number of iser target ids per host'),
+ cfg.StrOpt('iser_target_prefix',
+ default='iqn.2010-10.org.iser.openstack:',
+ help='prefix for iser volumes'),
+ cfg.StrOpt('iser_ip_address',
+ default='$my_ip',
+ help='The IP address that the iSER daemon is listening on'),
+ cfg.IntOpt('iser_port',
+ default=3260,
+ help='The port that the iSER daemon is listening on'),
cfg.StrOpt('volume_backend_name',
default=None,
help='The backend name for a given driver implementation'), ]
CONF = cfg.CONF
CONF.register_opts(volume_opts)
CONF.import_opt('iscsi_helper', 'cinder.brick.iscsi.iscsi')
+CONF.import_opt('iser_helper', 'cinder.brick.iser.iser')
class VolumeDriver(object):
return (None, None)
+class ISERDriver(ISCSIDriver):
+ """Executes commands relating to ISER volumes.
+
+ We make use of model provider properties as follows:
+
+ ``provider_location``
+ if present, contains the iSER target information in the same
+ format as an ietadm discovery
+ i.e. '<ip>:<port>,<portal> <target IQN>'
+
+ ``provider_auth``
+ if present, contains a space-separated triple:
+ '<auth method> <auth username> <auth password>'.
+ `CHAP` is the only auth_method in use at the moment.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(ISERDriver, self).__init__(*args, **kwargs)
+
+ def _do_iser_discovery(self, volume):
+ LOG.warn(_("ISER provider_location not stored, using discovery"))
+
+ volume_name = volume['name']
+
+ (out, _err) = self._execute('iscsiadm', '-m', 'discovery',
+ '-t', 'sendtargets', '-p', volume['host'],
+ run_as_root=True)
+ for target in out.splitlines():
+ if (self.configuration.iser_ip_address in target
+ and volume_name in target):
+ return target
+ return None
+
+ def _get_iser_properties(self, volume):
+ """Gets iser configuration
+
+ We ideally get saved information in the volume entity, but fall back
+ to discovery if need be. Discovery may be completely removed in future
+ The properties are:
+
+ :target_discovered: boolean indicating whether discovery was used
+
+ :target_iqn: the IQN of the iSER target
+
+ :target_portal: the portal of the iSER target
+
+ :target_lun: the lun of the iSER target
+
+ :volume_id: the id of the volume (currently used by xen)
+
+ :auth_method:, :auth_username:, :auth_password:
+
+ the authentication details. Right now, either auth_method is not
+ present meaning no authentication, or auth_method == `CHAP`
+ meaning use CHAP with the specified credentials.
+ """
+
+ properties = {}
+
+ location = volume['provider_location']
+
+ if location:
+ # provider_location is the same format as iSER discovery output
+ properties['target_discovered'] = False
+ else:
+ location = self._do_iser_discovery(volume)
+
+ if not location:
+ msg = (_("Could not find iSER export for volume %s") %
+ (volume['name']))
+ raise exception.InvalidVolume(reason=msg)
+
+ LOG.debug(_("ISER Discovery: Found %s") % (location))
+ properties['target_discovered'] = True
+
+ results = location.split(" ")
+ properties['target_portal'] = results[0].split(",")[0]
+ properties['target_iqn'] = results[1]
+ try:
+ properties['target_lun'] = int(results[2])
+ except (IndexError, ValueError):
+ if (self.configuration.volume_driver in
+ ['cinder.volume.drivers.lvm.LVMISERDriver',
+ 'cinder.volume.drivers.lvm.ThinLVMVolumeDriver'] and
+ self.configuration.iser_helper == 'tgtadm'):
+ properties['target_lun'] = 1
+ else:
+ properties['target_lun'] = 0
+
+ properties['volume_id'] = volume['id']
+
+ auth = volume['provider_auth']
+ if auth:
+ (auth_method, auth_username, auth_secret) = auth.split()
+
+ properties['auth_method'] = auth_method
+ properties['auth_username'] = auth_username
+ properties['auth_password'] = auth_secret
+
+ return properties
+
+ def initialize_connection(self, volume, connector):
+ """Initializes the connection and returns connection info.
+
+ The iser driver returns a driver_volume_type of 'iser'.
+ The format of the driver data is defined in _get_iser_properties.
+ Example return value::
+
+ {
+ 'driver_volume_type': 'iser'
+ 'data': {
+ 'target_discovered': True,
+ 'target_iqn':
+ 'iqn.2010-10.org.iser.openstack:volume-00000001',
+ 'target_portal': '127.0.0.0.1:3260',
+ 'volume_id': 1,
+ }
+ }
+
+ """
+
+ iser_properties = self._get_iser_properties(volume)
+ return {
+ 'driver_volume_type': 'iser',
+ 'data': iser_properties
+ }
+
+ def _check_valid_device(self, path):
+ 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=True)
+ except exception.ProcessExecutionError as e:
+ LOG.error(_("Failed to access the device on the path "
+ "%(path)s: %(error)s.") %
+ {"path": path, "error": e.stderr})
+ return False
+ # If the info is none, the path does not exist.
+ if info is None:
+ return False
+ return True
+
+ def _attach_volume(self, context, volume, connector):
+ """Attach the volume."""
+ iser_properties = None
+ host_device = None
+ init_conn = self.initialize_connection(volume, connector)
+ iser_properties = init_conn['data']
+
+ # code "inspired by" nova/virt/libvirt/volume.py
+ try:
+ self._run_iscsiadm(iser_properties, ())
+ except exception.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(iser_properties, ('--op', 'new'))
+ else:
+ raise
+
+ if iser_properties.get('auth_method'):
+ self._iscsiadm_update(iser_properties,
+ "node.session.auth.authmethod",
+ iser_properties['auth_method'])
+ self._iscsiadm_update(iser_properties,
+ "node.session.auth.username",
+ iser_properties['auth_username'])
+ self._iscsiadm_update(iser_properties,
+ "node.session.auth.password",
+ iser_properties['auth_password'])
+
+ host_device = ("/dev/disk/by-path/ip-%s-iser-%s-lun-%s" %
+ (iser_properties['target_portal'],
+ iser_properties['target_iqn'],
+ iser_properties.get('target_lun', 0)))
+
+ 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("iser:")]
+
+ stripped_portal = iser_properties['target_portal'].split(",")[0]
+ length_iqn = [s for s in portals
+ if stripped_portal ==
+ s['portal'].split(",")[0] and
+ s['iqn'] == iser_properties['target_iqn']]
+ if len(portals) == 0 or len(length_iqn) == 0:
+ try:
+ self._run_iscsiadm(iser_properties, ("--login",),
+ check_exit_code=[0, 255])
+ except exception.ProcessExecutionError as err:
+ if err.exit_code in [15]:
+ self._iscsiadm_update(iser_properties,
+ "node.startup",
+ "automatic")
+ return iser_properties, host_device
+ else:
+ raise
+
+ self._iscsiadm_update(iser_properties,
+ "node.startup", "automatic")
+
+ tries = 0
+ while not os.path.exists(host_device):
+ if tries >= self.configuration.num_iser_scan_tries:
+ raise exception.CinderException(_("iSER device "
+ "not found "
+ "at %s") % (host_device))
+
+ LOG.warn(_("ISER volume not yet found at: %(host_device)s. "
+ "Will rescan & retry. Try number: %(tries)s.") %
+ {'host_device': host_device, 'tries': tries})
+
+ # The rescan isn't documented as being necessary(?),
+ # but it helps
+ self._run_iscsiadm(iser_properties, ("--rescan",))
+
+ tries = tries + 1
+ if not os.path.exists(host_device):
+ time.sleep(tries ** 2)
+
+ if tries != 0:
+ LOG.debug(_("Found iSER node %(host_device)s "
+ "(after %(tries)s rescans).") %
+ {'host_device': host_device,
+ 'tries': tries})
+
+ if not self._check_valid_device(host_device):
+ raise exception.DeviceUnavailable(path=host_device,
+ reason=(_("Unable to access "
+ "the backend storage "
+ "via the path "
+ "%(path)s.") %
+ {'path': host_device}))
+ return iser_properties, host_device
+
+ def _update_volume_status(self):
+ """Retrieve status info from volume group."""
+
+ LOG.debug(_("Updating volume status"))
+ data = {}
+ backend_name = self.configuration.safe_get('volume_backend_name')
+ data["volume_backend_name"] = backend_name or 'Generic_iSER'
+ data["vendor_name"] = 'Open Source'
+ data["driver_version"] = '1.0'
+ data["storage_protocol"] = 'iSER'
+
+ data['total_capacity_gb'] = 'infinite'
+ data['free_capacity_gb'] = 'infinite'
+ data['reserved_percentage'] = 100
+ data['QoS_support'] = False
+ self._stats = data
+
+
+class FakeISERDriver(FakeISCSIDriver):
+ """Logs calls instead of executing."""
+ def __init__(self, *args, **kwargs):
+ super(FakeISERDriver, self).__init__(execute=self.fake_execute,
+ *args, **kwargs)
+
+ def initialize_connection(self, volume, connector):
+ return {
+ 'driver_volume_type': 'iser',
+ 'data': {}
+ }
+
+ @staticmethod
+ def fake_execute(cmd, *_args, **_kwargs):
+ """Execute that simply logs the command."""
+ LOG.debug(_("FAKE ISER: %s"), cmd)
+ return (None, None)
+
+
class FibreChannelDriver(VolumeDriver):
"""Executes commands relating to Fibre Channel volumes."""
def __init__(self, *args, **kwargs):
from oslo.config import cfg
from cinder.brick.iscsi import iscsi
+from cinder.brick.iser import iser
from cinder import exception
from cinder.image import image_utils
from cinder.openstack.common import fileutils
return "%s %s %s" % (chap, name, password)
+class LVMISERDriver(LVMISCSIDriver, driver.ISERDriver):
+ """Executes commands relating to ISER volumes.
+
+ We make use of model provider properties as follows:
+
+ ``provider_location``
+ if present, contains the iSER target information in the same
+ format as an ietadm discovery
+ i.e. '<ip>:<port>,<portal> <target IQN>'
+
+ ``provider_auth``
+ if present, contains a space-separated triple:
+ '<auth method> <auth username> <auth password>'.
+ `CHAP` is the only auth_method in use at the moment.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.tgtadm = iser.get_target_admin()
+ LVMVolumeDriver.__init__(self, *args, **kwargs)
+
+ def set_execute(self, execute):
+ LVMVolumeDriver.set_execute(self, execute)
+ self.tgtadm.set_execute(execute)
+
+ def ensure_export(self, context, volume):
+ """Synchronously recreates an export for a logical volume."""
+
+ if not isinstance(self.tgtadm, iser.TgtAdm):
+ try:
+ iser_target = self.db.volume_get_iscsi_target_num(
+ context,
+ volume['id'])
+ except exception.NotFound:
+ LOG.info(_("Skipping ensure_export. No iser_target "
+ "provisioned for volume: %s"), volume['id'])
+ return
+ else:
+ iser_target = 1 # dummy value when using TgtAdm
+
+ chap_auth = None
+
+ # Check for https://bugs.launchpad.net/cinder/+bug/1065702
+ old_name = None
+ volume_name = volume['name']
+ if (volume['provider_location'] is not None and
+ volume['name'] not in volume['provider_location']):
+
+ msg = _('Detected inconsistency in provider_location id')
+ LOG.debug(msg)
+ old_name = self._fix_id_migration(context, volume)
+ if 'in-use' in volume['status']:
+ volume_name = old_name
+ old_name = None
+
+ iser_name = "%s%s" % (self.configuration.iser_target_prefix,
+ volume_name)
+ volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
+ volume_name)
+
+ self.tgtadm.create_iser_target(iser_name, iser_target,
+ 0, volume_path, chap_auth,
+ check_exit_code=False,
+ old_name=old_name)
+
+ def _ensure_iser_targets(self, context, host):
+ """Ensure that target ids have been created in datastore."""
+ if not isinstance(self.tgtadm, iser.TgtAdm):
+ host_iser_targets = self.db.iscsi_target_count_by_host(context,
+ host)
+ if host_iser_targets >= self.configuration.iser_num_targets:
+ return
+
+ # NOTE(vish): Target ids start at 1, not 0.
+ target_end = self.configuration.iser_num_targets + 1
+ for target_num in xrange(1, target_end):
+ target = {'host': host, 'target_num': target_num}
+ self.db.iscsi_target_create_safe(context, target)
+
+ def create_export(self, context, volume):
+ """Creates an export for a logical volume."""
+
+ iser_name = "%s%s" % (self.configuration.iser_target_prefix,
+ volume['name'])
+ volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
+ volume['name'])
+ model_update = {}
+
+ # TODO(jdg): In the future move all of the dependent stuff into the
+ # cooresponding target admin class
+ if not isinstance(self.tgtadm, iser.TgtAdm):
+ lun = 0
+ self._ensure_iser_targets(context, volume['host'])
+ iser_target = self.db.volume_allocate_iscsi_target(context,
+ volume['id'],
+ volume['host'])
+ else:
+ lun = 1 # For tgtadm the controller is lun 0, dev starts at lun 1
+ iser_target = 0
+
+ # Use the same method to generate the username and the password.
+ chap_username = utils.generate_username()
+ chap_password = utils.generate_password()
+ chap_auth = self._iser_authentication('IncomingUser', chap_username,
+ chap_password)
+ tid = self.tgtadm.create_iser_target(iser_name,
+ iser_target,
+ 0,
+ volume_path,
+ chap_auth)
+ model_update['provider_location'] = self._iser_location(
+ self.configuration.iser_ip_address, tid, iser_name, lun)
+ model_update['provider_auth'] = self._iser_authentication(
+ 'CHAP', chap_username, chap_password)
+ return model_update
+
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume."""
+
+ if not isinstance(self.tgtadm, iser.TgtAdm):
+ try:
+ iser_target = self.db.volume_get_iscsi_target_num(
+ context,
+ volume['id'])
+ except exception.NotFound:
+ LOG.info(_("Skipping remove_export. No iser_target "
+ "provisioned for volume: %s"), volume['id'])
+ return
+ else:
+ iser_target = 0
+
+ try:
+
+ # NOTE: provider_location may be unset if the volume hasn't
+ # been exported
+ location = volume['provider_location'].split(' ')
+ iqn = location[1]
+
+ self.tgtadm.show_target(iser_target, iqn=iqn)
+
+ except Exception as e:
+ LOG.info(_("Skipping remove_export. No iser_target "
+ "is presently exported for volume: %s"), volume['id'])
+ return
+
+ self.tgtadm.remove_iser_target(iser_target, 0, volume['id'])
+
+ def _update_volume_status(self):
+ """Retrieve status info from volume group."""
+
+ LOG.debug(_("Updating volume status"))
+ data = {}
+
+ # Note(zhiteng): These information are driver/backend specific,
+ # each driver may define these values in its own config options
+ # or fetch from driver specific configuration file.
+ backend_name = self.configuration.safe_get('volume_backend_name')
+ data["volume_backend_name"] = backend_name or 'LVM_iSER'
+ data["vendor_name"] = 'Open Source'
+ data["driver_version"] = self.VERSION
+ data["storage_protocol"] = 'iSER'
+
+ data['total_capacity_gb'] = 0
+ data['free_capacity_gb'] = 0
+ data['reserved_percentage'] = self.configuration.reserved_percentage
+ data['QoS_support'] = False
+
+ try:
+ out, err = self._execute('vgs', '--noheadings', '--nosuffix',
+ '--unit=G', '-o', 'name,size,free',
+ self.configuration.volume_group,
+ run_as_root=True)
+ except exception.ProcessExecutionError as exc:
+ LOG.error(_("Error retrieving volume status: %s"), exc.stderr)
+ out = False
+
+ if out:
+ volume = out.split()
+ data['total_capacity_gb'] = float(volume[1].replace(',', '.'))
+ data['free_capacity_gb'] = float(volume[2].replace(',', '.'))
+
+ self._stats = data
+
+ def _iser_location(self, ip, target, iqn, lun=None):
+ return "%s:%s,%s %s %s" % (ip, self.configuration.iser_port,
+ target, iqn, lun)
+
+ def _iser_authentication(self, chap, name, password):
+ return "%s %s %s" % (chap, name, password)
+
+
class ThinLVMVolumeDriver(LVMISCSIDriver):
"""Subclass for thin provisioned LVM's."""
# value)
#iscsi_port=3260
+# number of times to rescan iSER target to find volume
+# (integer value)
+#num_iser_scan_tries=3
+
+# Number of iser target ids per host (integer value)
+#iser_num_targets=100
+
+# prefix for iser volumes (string value)
+#iser_target_prefix=iqn.2010-10.org.iser.openstack:
+
+# The port that the iSER daemon is listening on (string
+# value)
+#iser_ip_address=$my_ip
+
+# The port that the iSER daemon is listening on (integer
+# value)
+#iser_port=3260
+
# The backend name for a given driver implementation (string
# value)
#volume_backend_name=<None>
# iscsi target user-land tool to use (string value)
#iscsi_helper=tgtadm
+# iser target user-land tool to use (string value)
+#iser_helper=tgtadm
+
# Volume configuration file storage directory (string value)
#volumes_dir=$state_path/volumes
#
# Driver to use for volume creation (string value)
-#volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver
+#volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver,cinder.volume.drivers.lvm.LVMISERDriver
# Total option count: 300