Merge "Revert iSCSI Target objects as independent objects"
This commit is contained in:
commit
6be45648c4
|
@ -294,18 +294,6 @@ 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.")
|
||||
|
||||
|
|
|
@ -173,13 +173,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 target. The point here is that for example
|
||||
that we call a connector. 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 target
|
||||
relationship and just provide an interface to the specific connector
|
||||
methods. How you do this in your own driver is of course up to you.
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
# 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()
|
|
@ -1,43 +0,0 @@
|
|||
# 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
|
|
@ -1,42 +0,0 @@
|
|||
# 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
|
|
@ -1,188 +0,0 @@
|
|||
# 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)
|
|
@ -1,52 +0,0 @@
|
|||
# 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 = """
|
||||
<target %s>
|
||||
driver iser
|
||||
backing-store %s
|
||||
</target>
|
||||
"""
|
||||
VOLUME_CONF_WITH_CHAP_AUTH = """
|
||||
<target %s>
|
||||
driver iser
|
||||
backing-store %s
|
||||
%s
|
||||
</target>
|
||||
"""
|
||||
|
||||
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
|
|
@ -1,183 +0,0 @@
|
|||
# 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
|
||||
}
|
|
@ -1,371 +0,0 @@
|
|||
# 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 = """
|
||||
<target %s>
|
||||
backing-store %s
|
||||
lld iscsi
|
||||
</target>
|
||||
"""
|
||||
VOLUME_CONF_WITH_CHAP_AUTH = """
|
||||
<target %s>
|
||||
backing-store %s
|
||||
lld iscsi
|
||||
%s
|
||||
</target>
|
||||
"""
|
||||
|
||||
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()
|
Loading…
Reference in New Issue