From 1dd09a955bc04388bd5f14807f883db3c5ac60b9 Mon Sep 17 00:00:00 2001 From: Shlomi Sasson Date: Mon, 17 Jun 2013 20:41:36 +0300 Subject: [PATCH] Adding support for iSER transport protocol DocImpact Implements: blueprint add-iser-support-to-cinder Change-Id: I55fb7add68151141be571cb9004389951851226b --- cinder/brick/iser/__init__.py | 16 ++ cinder/brick/iser/iser.py | 231 ++++++++++++++++++++++++++ cinder/exception.py | 16 ++ cinder/tests/fake_driver.py | 19 +++ cinder/tests/fake_flags.py | 3 + cinder/tests/test_iser.py | 111 +++++++++++++ cinder/tests/test_volume.py | 31 +++- cinder/volume/driver.py | 299 +++++++++++++++++++++++++++++++++- cinder/volume/drivers/lvm.py | 191 ++++++++++++++++++++++ etc/cinder/cinder.conf.sample | 23 ++- 10 files changed, 935 insertions(+), 5 deletions(-) create mode 100644 cinder/brick/iser/__init__.py create mode 100755 cinder/brick/iser/iser.py create mode 100644 cinder/tests/test_iser.py diff --git a/cinder/brick/iser/__init__.py b/cinder/brick/iser/__init__.py new file mode 100644 index 00000000000..5e8da711fb1 --- /dev/null +++ b/cinder/brick/iser/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/cinder/brick/iser/iser.py b/cinder/brick/iser/iser.py new file mode 100755 index 00000000000..b82c2d49d74 --- /dev/null +++ b/cinder/brick/iser/iser.py @@ -0,0 +1,231 @@ +# 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 = """ + + driver iser + backing-store %s + + """ % (name, path) + else: + volume_conf = """ + + driver iser + backing-store %s + %s + + """ % (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() diff --git a/cinder/exception.py b/cinder/exception.py index c668e140407..448d8fb7152 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -298,6 +298,22 @@ class ISCSITargetRemoveFailed(CinderException): 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") diff --git a/cinder/tests/fake_driver.py b/cinder/tests/fake_driver.py index 10f791e77b8..6f352a7c773 100644 --- a/cinder/tests/fake_driver.py +++ b/cinder/tests/fake_driver.py @@ -46,6 +46,25 @@ class FakeISCSIDriver(lvm.LVMISCSIDriver): 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.""" diff --git a/cinder/tests/fake_flags.py b/cinder/tests/fake_flags.py index ef0b1a827d9..c966fc0d594 100644 --- a/cinder/tests/fake_flags.py +++ b/cinder/tests/fake_flags.py @@ -21,6 +21,7 @@ from cinder import flags 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') @@ -34,10 +35,12 @@ def set_defaults(conf): 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) diff --git a/cinder/tests/test_iser.py b/cinder/tests/test_iser.py new file mode 100644 index 00000000000..789fbe36098 --- /dev/null +++ b/cinder/tests/test_iser.py @@ -0,0 +1,111 @@ +# 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() diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index bdfe0154d58..71dab10bc85 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -29,6 +29,7 @@ import mox 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 @@ -1301,8 +1302,36 @@ class ISCSITestCase(DriverTestCase): 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): diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index a6b4455719e..cfd13403f06 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -42,19 +42,36 @@ volume_opts = [ 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'), ] @@ -62,6 +79,7 @@ volume_opts = [ 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): @@ -567,6 +585,281 @@ class FakeISCSIDriver(ISCSIDriver): 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. ':, ' + + ``provider_auth`` + if present, contains a space-separated triple: + ' '. + `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): diff --git a/cinder/volume/drivers/lvm.py b/cinder/volume/drivers/lvm.py index f4705e2b1ca..8cb87426a77 100644 --- a/cinder/volume/drivers/lvm.py +++ b/cinder/volume/drivers/lvm.py @@ -27,6 +27,7 @@ import re 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 @@ -600,6 +601,196 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): 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. ':, ' + + ``provider_auth`` + if present, contains a space-separated triple: + ' '. + `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.""" diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 871818ca383..7de087149b9 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -812,6 +812,24 @@ # 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= @@ -1339,6 +1357,9 @@ # 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 @@ -1360,6 +1381,6 @@ # # 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