From: John Griffith Date: Thu, 10 Jul 2014 00:00:31 +0000 (+0000) Subject: Add iSCSI Target objects as independent objects X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=d10fb796f491152486e394481e920479bab9e3d1;p=openstack-build%2Fcinder-build.git Add iSCSI Target objects as independent objects This patch is a step in decoupling the target methods and the Volume Driver's Control methods. This adds the targets directory and the new target objects that we use with the exception of IET (follow up for that later). TgtAdm and LIO drivers have been tested with the new LVM object. All existing drivers are still able to be specified and use the same objects and code-path they were using previously. New connector objects are only used when specifying the new driver. Next step will be mapping current ref LVM driver to the new LVM object and target model and working on the unit-tests. After that mark the "old" methods and objects as deprecated and we can then begin working on some other improvements. Partial-Bug: #1329139 Change-Id: Iaa55e31e3dadc7dcb58112302c3807a8f92bcada --- diff --git a/cinder/exception.py b/cinder/exception.py index 11f20522b..1c5ddf0c3 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -294,6 +294,18 @@ class ISCSITargetNotFoundForVolume(NotFound): message = _("No target id found for volume %(volume_id)s.") +class ISCSITargetCreateFailed(CinderException): + message = _("Failed to create iscsi target for volume %(volume_id)s.") + + +class ISCSITargetRemoveFailed(CinderException): + message = _("Failed to remove iscsi target for volume %(volume_id)s.") + + +class ISCSITargetAttachFailed(CinderException): + message = _("Failed to attach iSCSI target for volume %(volume_id)s.") + + class InvalidImageRef(Invalid): message = _("Invalid image href %(image_href)s.") diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 20d290bc0..9005564cb 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -165,13 +165,13 @@ class VolumeDriver(object): intended that these drivers ONLY implement Control Path details (create, delete, extend...), while transport or data path related implementation should be a *member object* - that we call a connector. The point here is that for example + that we call a target. The point here is that for example don't allow the LVM driver to implement iSCSI methods, instead call whatever connector it has configued via conf file (iSCSI{LIO, TGT, IET}, FC, etc). In the base class and for example the LVM driver we do this via a has-a - relationship and just provide an interface to the specific connector + relationship and just provide an interface to the specific target methods. How you do this in your own driver is of course up to you. """ diff --git a/cinder/volume/targets/__init__.py b/cinder/volume/targets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/volume/targets/driver.py b/cinder/volume/targets/driver.py new file mode 100644 index 000000000..346837a38 --- /dev/null +++ b/cinder/volume/targets/driver.py @@ -0,0 +1,55 @@ +# 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. + + +class Target(object): + """Target object for block storage devices. + + Base class for target object, where target + is data transport mechanism (target) specific calls. + This includes things like create targets, attach, detach + etc. + + Base class here does nothing more than set an executor and db as + well as force implementation of required methods. + + """ + + def __init__(self, *args, **kwargs): + self.db = kwargs.get('db') + self.configuration = kwargs.get('configuration') + self._execute = kwargs.get('executor') + + def ensure_export(self, context, volume, + iscsi_name, volume_path, + volume_group, config): + raise NotImplementedError() + + def create_export(self, context, volume): + raise NotImplementedError() + + def remove_export(self, context, volume): + raise NotImplementedError() + + def attach_volume(self, context, + volume, instance_uuid, + host_name, mountpoint): + raise NotImplementedError() + + def detach_volume(self, context, volume): + raise NotImplementedError() + + def initialize_connection(self, volume, **kwargs): + raise NotImplementedError() + + def terminate_connection(self, volume, **kwargs): + raise NotImplementedError() diff --git a/cinder/volume/targets/fake.py b/cinder/volume/targets/fake.py new file mode 100644 index 000000000..e2a318169 --- /dev/null +++ b/cinder/volume/targets/fake.py @@ -0,0 +1,43 @@ +# 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. + + +class FakeTarget(object): + VERSION = '0.1' + + def __init__(self, *args, **kwargs): + super(FakeTarget, self).__init__(*args, **kwargs) + + def ensure_export(self, context, volume, + iscsi_name, volume_path, + volume_group, config): + pass + + def create_export(self, context, volume): + pass + + def remove_export(self, context, volume): + pass + + def attach_volume(self, context, + volume, instance_uuid, + host_name, mountpoint): + pass + + def detach_volume(self, context, volume): + pass + + def initialize_connection(self, volume, **kwargs): + pass + + def terminate_connection(self, volume, **kwargs): + pass diff --git a/cinder/volume/targets/iet.py b/cinder/volume/targets/iet.py new file mode 100644 index 000000000..e7eba1fa3 --- /dev/null +++ b/cinder/volume/targets/iet.py @@ -0,0 +1,42 @@ +# 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. + + +class IetAdm(object): + VERSION = '0.1' + + def __init__(self, *args, **kwargs): + super(IetAdm, self).__init__(*args, **kwargs) + + def ensure_export(self, context, volume, + iscsi_name, volume_path, + volume_group, config): + pass + + def create_export(self, context, volume): + pass + + def remove_export(self, context, volume): + pass + + def attach_volume(self, context, volume, + instance_uuid, host_name, mountpoint): + pass + + def detach_volume(self, context, volume): + pass + + def initialize_connection(self, volume, **kwargs): + pass + + def terminate_connection(self, volume, **kwargs): + pass diff --git a/cinder/volume/targets/iscsi.py b/cinder/volume/targets/iscsi.py new file mode 100644 index 000000000..fe66def4e --- /dev/null +++ b/cinder/volume/targets/iscsi.py @@ -0,0 +1,188 @@ +# 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 cinder import exception +from cinder.openstack.common.gettextutils import _ +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils +from cinder.volume.targets import driver + +LOG = logging.getLogger(__name__) + + +class ISCSITarget(driver.Target): + """Target object for block storage devices. + + Base class for target object, where target + is data transport mechanism (target) specific calls. + This includes things like create targets, attach, detach + etc. + """ + + def __init__(self, *args, **kwargs): + super(ISCSITarget, self).__init__(*args, **kwargs) + self.iscsi_target_prefix = \ + self.configuration.safe_get('iscsi_target_prefix') + self.protocol = 'iSCSI' + + def _get_iscsi_properties(self, volume): + """Gets iscsi configuration + + We ideally get saved information in the volume entity, but fall back + to discovery if need be. Discovery may be completely removed in the + future. + + The properties are: + + :target_discovered: boolean indicating whether discovery was used + + :target_iqn: the IQN of the iSCSI target + + :target_portal: the portal of the iSCSI target + + :target_lun: the lun of the iSCSI target + + :volume_id: the uuid of the volume + + :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. + + :access_mode: the volume access mode allow client used + ('rw' or 'ro' currently supported) + """ + + properties = {} + + location = volume['provider_location'] + + if location: + # provider_location is the same format as iSCSI discovery output + properties['target_discovered'] = False + else: + location = self._do_iscsi_discovery(volume) + + if not location: + msg = (_("Could not find iSCSI export for volume %s") % + (volume['name'])) + raise exception.InvalidVolume(reason=msg) + + LOG.debug(("ISCSI 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): + # NOTE(jdg): The following is carried over from the existing + # code. The trick here is that different targets use different + # default lun numbers, the base driver with tgtadm uses 1 + # others like LIO use 0. + if (self.configuration.volume_driver in + ['cinder.volume.drivers.lvm.LVMISCSIDriver', + 'cinder.volume.drivers.lvm.ThinLVMVolumeDriver'] and + self.configuration.iscsi_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 + + geometry = volume.get('provider_geometry', None) + if geometry: + (physical_block_size, logical_block_size) = geometry.split() + properties['physical_block_size'] = physical_block_size + properties['logical_block_size'] = logical_block_size + + encryption_key_id = volume.get('encryption_key_id', None) + properties['encrypted'] = encryption_key_id is not None + + return properties + + def _iscsi_authentication(self, chap, name, password): + return "%s %s %s" % (chap, name, password) + + def _do_iscsi_discovery(self, volume): + # TODO(justinsb): Deprecate discovery and use stored info + # NOTE(justinsb): Discovery won't work with CHAP-secured targets (?) + LOG.warn(_("ISCSI provider_location not stored, using discovery")) + + volume_name = volume['name'] + + try: + # NOTE(griff) We're doing the split straight away which should be + # safe since using '@' in hostname is considered invalid + + (out, _err) = self._execute('iscsiadm', '-m', 'discovery', + '-t', 'sendtargets', '-p', + volume['host'].split('@')[0], + run_as_root=True) + except processutils.ProcessExecutionError as ex: + LOG.error(_("ISCSI discovery attempt failed for:%s") % + volume['host'].split('@')[0]) + LOG.debug(("Error from iscsiadm -m discovery: %s") % ex.stderr) + return None + + for target in out.splitlines(): + if (self.configuration.safe_get('iscsi_ip_address') in target + and volume_name in target): + return target + return None + + def detach_volume(self, context, volume): + self._get_iscsi_properties(volume) + + def initialize_connection(self, volume, **kwargs): + """Initializes the connection and returns connection info. + + The iscsi driver returns a driver_volume_type of 'iscsi'. + The format of the driver data is defined in _get_iscsi_properties. + Example return value:: + + { + 'driver_volume_type': 'iscsi' + 'data': { + 'target_discovered': True, + 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001', + 'target_portal': '127.0.0.0.1:3260', + 'volume_id': '9a0d35d0-175a-11e4-8c21-0800200c9a66', + 'access_mode': 'rw' + } + } + """ + + iscsi_properties = self._get_iscsi_properties(volume) + return { + 'driver_volume_type': 'iscsi', + 'data': iscsi_properties + } + + def validate_connector(self, connector): + # NOTE(jdg): api passes in connector which is initiator info + if 'initiator' not in connector: + err_msg = (_('The volume driver requires the iSCSI initiator ' + 'name in the connector.')) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) diff --git a/cinder/volume/targets/iser.py b/cinder/volume/targets/iser.py new file mode 100644 index 000000000..0676d6fbd --- /dev/null +++ b/cinder/volume/targets/iser.py @@ -0,0 +1,52 @@ +# 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 cinder.openstack.common import log as logging +from cinder.volume.targets.tgt import TgtAdm + + +LOG = logging.getLogger(__name__) + + +class ISERTgtAdm(TgtAdm): + VERSION = '0.2' + + VOLUME_CONF = """ + + driver iser + backing-store %s + + """ + VOLUME_CONF_WITH_CHAP_AUTH = """ + + driver iser + backing-store %s + %s + + """ + + def __init__(self, *args, **kwargs): + super(ISERTgtAdm, self).__init__(*args, **kwargs) + self.volumes_dir = self.configuration.safe_get('volumes_dir') + self.protocol = 'iSER' + + # backward compatability mess + self.configuration.num_volume_device_scan_tries = \ + self.configuration.num_iser_scan_tries + self.configuration.iscsi_num_targets = \ + self.configuration.iser_num_targets + self.configuration.iscsi_target_prefix = \ + self.configuration.iser_target_prefix + self.configuration.iscsi_ip_address = \ + self.configuration.iser_ip_address + self.configuration.iscsi_port = self.configuration.iser_port diff --git a/cinder/volume/targets/lio.py b/cinder/volume/targets/lio.py new file mode 100644 index 000000000..3ada19bf1 --- /dev/null +++ b/cinder/volume/targets/lio.py @@ -0,0 +1,183 @@ +# 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 cinder import exception +from cinder.openstack.common.gettextutils import _ +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils as putils +from cinder.volume.targets.tgt import TgtAdm + +LOG = logging.getLogger(__name__) + + +class LioAdm(TgtAdm): + """iSCSI target administration for LIO using python-rtslib.""" + def __init__(self, *args, **kwargs): + super(LioAdm, self).__init__(*args, **kwargs) + + # FIXME(jdg): modify executor to use the cinder-rtstool + self.iscsi_target_prefix =\ + self.configuration.safe_get('iscsi_target_prefix') + self.lio_initiator_iqns =\ + self.configuration.safe_get('lio_initiator_iqns') + self._verify_rtstool() + + def remove_export(self, context, volume): + try: + iscsi_target = self.db.volume_get_iscsi_target_num(context, + volume['id']) + except exception.NotFound: + LOG.info(_("Skipping remove_export. No iscsi_target " + "provisioned for volume: %s"), volume['id']) + return + + self.remove_iscsi_target(iscsi_target, 0, volume['id'], volume['name']) + + def ensure_export(self, context, volume, + iscsi_name, volume_path, + volume_group, config): + try: + volume_info = self.db.volume_get(context, volume['id']) + (auth_method, + auth_user, + auth_pass) = volume_info['provider_auth'].split(' ', 3) + chap_auth = self._iscsi_authentication(auth_method, + auth_user, + auth_pass) + except exception.NotFound: + LOG.debug(("volume_info:%s"), volume_info) + LOG.info(_("Skipping ensure_export. No iscsi_target " + "provision for volume: %s"), volume['id']) + + iscsi_target = 1 + + self.create_iscsi_target(iscsi_name, iscsi_target, 0, volume_path, + chap_auth, check_exit_code=False) + + def _verify_rtstool(self): + try: + self._execute('cinder-rtstool', 'verify') + except (OSError, putils.ProcessExecutionError): + LOG.error(_('cinder-rtstool is not installed correctly')) + raise + + def _get_target(self, iqn): + (out, err) = self._execute('cinder-rtstool', + 'get-targets', + run_as_root=True) + lines = out.split('\n') + for line in lines: + if iqn in line: + return line + + return None + + def create_iscsi_target(self, name, tid, lun, path, + chap_auth=None, **kwargs): + # tid and lun are not used + + vol_id = name.split(':')[1] + + LOG.info(_('Creating iscsi_target for volume: %s') % vol_id) + + # rtstool requires chap_auth, but unit tests don't provide it + chap_auth_userid = 'test_id' + chap_auth_password = 'test_pass' + + if chap_auth is not None: + (chap_auth_userid, chap_auth_password) = chap_auth.split(' ')[1:] + + extra_args = [] + if self.lio_initiator_iqns: + extra_args.append(self.lio_initiator_iqns) + + try: + command_args = ['cinder-rtstool', + 'create', + path, + name, + chap_auth_userid, + chap_auth_password] + if extra_args: + command_args.extend(extra_args) + self._execute(*command_args, run_as_root=True) + except putils.ProcessExecutionError as e: + LOG.error(_("Failed to create iscsi target for volume " + "id:%s.") % vol_id) + LOG.error(_("%s") % e) + + raise exception.ISCSITargetCreateFailed(volume_id=vol_id) + + iqn = '%s%s' % (self.iscsi_target_prefix, vol_id) + tid = self._get_target(iqn) + if tid is None: + LOG.error(_("Failed to create iscsi target for volume " + "id:%s.") % vol_id) + raise exception.NotFound() + + return tid + + def remove_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs): + LOG.info(_('Removing iscsi_target: %s') % vol_id) + vol_uuid_name = vol_name + iqn = '%s%s' % (self.iscsi_target_prefix, vol_uuid_name) + + try: + self._execute('cinder-rtstool', + 'delete', + iqn, + run_as_root=True) + except putils.ProcessExecutionError as e: + LOG.error(_("Failed to remove iscsi target for volume " + "id:%s.") % vol_id) + LOG.error(_("%s") % e) + raise exception.ISCSITargetRemoveFailed(volume_id=vol_id) + + 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() + + def initialize_connection(self, volume, connector): + volume_iqn = volume['provider_location'].split(' ')[1] + + (auth_method, auth_user, auth_pass) = \ + volume['provider_auth'].split(' ', 3) + + # Add initiator iqns to target ACL + try: + self._execute('cinder-rtstool', 'add-initiator', + volume_iqn, + auth_user, + auth_pass, + connector['initiator'], + run_as_root=True) + except putils.ProcessExecutionError: + LOG.error(_("Failed to add initiator iqn %s to target") % + connector['initiator']) + raise exception.ISCSITargetAttachFailed( + volume_id=volume['id']) + + iscsi_properties = self._get_iscsi_properties(volume) + + # FIXME(jdg): For LIO the target_lun is 0, other than that all data + # is the same as it is for tgtadm, just modify it here + iscsi_properties['target_lun'] = 0 + + return { + 'driver_volume_type': 'iscsi', + 'data': iscsi_properties + } diff --git a/cinder/volume/targets/tgt.py b/cinder/volume/targets/tgt.py new file mode 100644 index 000000000..351e04d43 --- /dev/null +++ b/cinder/volume/targets/tgt.py @@ -0,0 +1,371 @@ +# 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 time + +from cinder import exception +from cinder.openstack.common import fileutils +from cinder.openstack.common.gettextutils import _ +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils as putils +from cinder.volume.targets import iscsi +from cinder.volume import utils as vutils + +LOG = logging.getLogger(__name__) + + +class TgtAdm(iscsi.ISCSITarget): + """Target object for block storage devices. + + Base class for target object, where target + is data transport mechanism (target) specific calls. + This includes things like create targets, attach, detach + etc. + """ + VOLUME_CONF = """ + + backing-store %s + lld iscsi + + """ + VOLUME_CONF_WITH_CHAP_AUTH = """ + + backing-store %s + lld iscsi + %s + + """ + + def __init__(self, *args, **kwargs): + super(TgtAdm, self).__init__(*args, **kwargs) + self.volumes_dir = self.configuration.safe_get('volumes_dir') + + 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 _verify_backing_lun(self, iqn, tid): + backing_lun = True + capture = False + target_info = [] + + (out, err) = self._execute('tgt-admin', '--show', run_as_root=True) + lines = out.split('\n') + + for line in lines: + if iqn in line and "Target %s" % tid in line: + capture = True + if capture: + target_info.append(line) + if iqn not in line and 'Target ' in line: + capture = False + + if ' LUN: 1' not in target_info: + backing_lun = False + + return backing_lun + + def _recreate_backing_lun(self, iqn, tid, name, path): + LOG.warning(_('Attempting recreate of backing lun...')) + + # Since we think the most common case of this is a dev busy + # (create vol from snapshot) we're going to add a sleep here + # this will hopefully give things enough time to stabilize + # how long should we wait?? I have no idea, let's go big + # and error on the side of caution + time.sleep(10) + try: + (out, err) = self._execute('tgtadm', '--lld', 'iscsi', + '--op', 'new', '--mode', + 'logicalunit', '--tid', + tid, '--lun', '1', '-b', + path, run_as_root=True) + LOG.debug('StdOut from recreate backing lun: %s' % out) + LOG.debug('StdErr from recreate backing lun: %s' % err) + except putils.ProcessExecutionError as e: + LOG.error(_("Failed to recover attempt to create " + "iscsi backing lun for volume " + "id:%(vol_id)s: %(e)s") + % {'vol_id': name, 'e': e}) + + def _iscsi_location(self, ip, target, iqn, lun=None): + return "%s:%s,%s %s %s" % (ip, self.configuration.iscsi_port, + target, iqn, lun) + + def _get_iscsi_target(self, context, vol_id): + return 0 + + def _get_target_and_lun(self, context, volume): + lun = 1 # For tgtadm the controller is lun 0, dev starts at lun 1 + iscsi_target = 0 # NOTE(jdg): Not used by tgtadm + return iscsi_target, lun + + def _ensure_iscsi_targets(self, context, host): + """Ensure that target ids have been created in datastore.""" + # NOTE(jdg): tgtadm doesn't use the iscsi_targets table + # TODO(jdg): In the future move all of the dependent stuff into the + # cooresponding target admin class + host_iscsi_targets = self.db.iscsi_target_count_by_host(context, + host) + if host_iscsi_targets >= self.configuration.iscsi_num_targets: + return + + # NOTE(vish): Target ids start at 1, not 0. + target_end = self.configuration.iscsi_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 ensure_export(self, context, volume, + iscsi_name, volume_path, + volume_group, config): + chap_auth = None + old_name = None + + # FIXME (jdg): This appears to be broken in existing code + # we recreate the iscsi target but we pass in None + # for CHAP, so we just recreated without CHAP even if + # we had it set on initial create + + iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix, + volume['name']) + self.create_iscsi_target( + iscsi_name, + 1, 0, volume_path, + chap_auth, check_exit_code=False, + old_name=old_name) + + def create_iscsi_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(self.volumes_dir) + + vol_id = name.split(':')[1] + if chap_auth is None: + volume_conf = self.VOLUME_CONF % (name, path) + else: + volume_conf = self.VOLUME_CONF_WITH_CHAP_AUTH % (name, + path, chap_auth) + + LOG.info(_('Creating iscsi_target for: %s') % vol_id) + volumes_dir = self.volumes_dir + volume_path = os.path.join(volumes_dir, vol_id) + + f = open(volume_path, 'w+') + f.write(volume_conf) + f.close() + LOG.debug(('Created volume path %(vp)s,\n' + 'content: %(vc)s') + % {'vp': volume_path, 'vc': volume_conf}) + + 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: + # with the persistent tgts we create them + # by creating the entry in the persist file + # and then doing an update to get the target + # created. + (out, err) = self._execute('tgt-admin', '--update', name, + run_as_root=True) + LOG.debug("StdOut from tgt-admin --update: %s", out) + LOG.debug("StdErr from tgt-admin --update: %s", err) + + # Grab targets list for debug + # Consider adding a check for lun 0 and 1 for tgtadm + # before considering this as valid + (out, err) = self._execute('tgtadm', + '--lld', + 'iscsi', + '--op', + 'show', + '--mode', + 'target', + run_as_root=True) + LOG.debug("Targets after update: %s" % out) + except putils.ProcessExecutionError as e: + LOG.warning(_("Failed to create iscsi target for volume " + "id:%(vol_id)s: %(e)s") + % {'vol_id': vol_id, 'e': e}) + + #Don't forget to remove the persistent file we created + os.unlink(volume_path) + raise exception.ISCSITargetCreateFailed(volume_id=vol_id) + + iqn = '%s%s' % (self.iscsi_target_prefix, vol_id) + tid = self._get_target(iqn) + if tid is None: + LOG.error(_("Failed to create iscsi target for volume " + "id:%(vol_id)s. Please ensure your tgtd config file " + "contains 'include %(volumes_dir)s/*'") % { + 'vol_id': vol_id, + 'volumes_dir': volumes_dir, }) + raise exception.NotFound() + + # NOTE(jdg): Sometimes we have some issues with the backing lun + # not being created, believe this is due to a device busy + # or something related, so we're going to add some code + # here that verifies the backing lun (lun 1) was created + # and we'll try and recreate it if it's not there + if not self._verify_backing_lun(iqn, tid): + try: + self._recreate_backing_lun(iqn, tid, name, path) + except putils.ProcessExecutionError: + os.unlink(volume_path) + raise exception.ISCSITargetCreateFailed(volume_id=vol_id) + + # Finally check once more and if no go, fail and punt + if not self._verify_backing_lun(iqn, tid): + os.unlink(volume_path) + raise exception.ISCSITargetCreateFailed(volume_id=vol_id) + + if old_persist_file is not None and os.path.exists(old_persist_file): + os.unlink(old_persist_file) + + return tid + + def create_export(self, context, volume, volume_path): + """Creates an export for a logical volume.""" + iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix, + volume['name']) + iscsi_target, lun = self._get_target_and_lun(context, volume) + chap_username = vutils.generate_username() + chap_password = vutils.generate_password() + chap_auth = self._iscsi_authentication('IncomingUser', chap_username, + chap_password) + # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need + # should clean this all up at some point in the future + tid = self.create_iscsi_target(iscsi_name, + iscsi_target, + 0, + volume_path, + chap_auth) + data = {} + data['location'] = self._iscsi_location( + self.configuration.iscsi_ip_address, tid, iscsi_name, lun) + data['auth'] = self._iscsi_authentication( + 'CHAP', chap_username, chap_password) + return data + + def remove_export(self, context, volume): + try: + iscsi_target = self._get_iscsi_target(context, volume['id']) + except exception.NotFound: + LOG.info(_("Skipping remove_export. No iscsi_target " + "provisioned for volume: %s"), volume['id']) + return + try: + + # NOTE: provider_location may be unset if the volume hasn't + # been exported + location = volume['provider_location'].split(' ') + iqn = location[1] + + # ietadm show will exit with an error + # this export has already been removed + self.show_target(iscsi_target, iqn=iqn) + + except Exception: + LOG.info(_("Skipping remove_export. No iscsi_target " + "is presently exported for volume: %s"), volume['id']) + return + + self.remove_iscsi_target(iscsi_target, 0, volume['id'], volume['name']) + + def initialize_connection(self, volume, connector): + iscsi_properties = self._get_iscsi_properties(volume) + return { + 'driver_volume_type': 'iscsi', + 'data': iscsi_properties + } + + def remove_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs): + LOG.info(_('Removing iscsi_target for: %s') % vol_id) + vol_uuid_file = vol_name + volume_path = os.path.join(self.volumes_dir, vol_uuid_file) + if not os.path.exists(volume_path): + LOG.warning(_('Volume path %s does not exist, ' + 'nothing to remove.') % volume_path) + return + + if os.path.isfile(volume_path): + iqn = '%s%s' % (self.iscsi_target_prefix, + vol_uuid_file) + else: + raise exception.ISCSITargetRemoveFailed(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 putils.ProcessExecutionError as e: + LOG.error(_("Failed to remove iscsi target for volume " + "id:%(vol_id)s: %(e)s") + % {'vol_id': vol_id, 'e': e}) + raise exception.ISCSITargetRemoveFailed(volume_id=vol_id) + # NOTE(jdg): There's a bug in some versions of tgt that + # will sometimes fail silently when using the force flag + # https://bugs.launchpad.net/ubuntu/+source/tgt/+bug/1305343 + # For now work-around by checking if the target was deleted, + # if it wasn't, try again without the force. + + # This will NOT do any good for the case of mutliple sessions + # which the force was aded for but it will however address + # the cases pointed out in bug: + # https://bugs.launchpad.net/cinder/+bug/1304122 + if self._get_target(iqn): + try: + LOG.warning(_('Silent failure of target removal ' + 'detected, retry....')) + self._execute('tgt-admin', + '--delete', + iqn, + run_as_root=True) + except putils.ProcessExecutionError as e: + LOG.error(_("Failed to remove iscsi target for volume " + "id:%(vol_id)s: %(e)s") + % {'vol_id': vol_id, 'e': e}) + raise exception.ISCSITargetRemoveFailed(volume_id=vol_id) + + # NOTE(jdg): This *should* be there still but incase + # it's not we don't care, so just ignore it if was + # somehow deleted between entry of this method + # and here + if os.path.exists(volume_path): + os.unlink(volume_path) + else: + LOG.debug('Volume path %s not found at end, ' + 'of remove_iscsi_target.' % 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()