Merge "Adding support for iSER transport protocol"
This commit is contained in:
commit
ed69d6e98c
|
@ -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.
|
|
@ -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 = """
|
||||
<target %s>
|
||||
driver iser
|
||||
backing-store %s
|
||||
</target>
|
||||
""" % (name, path)
|
||||
else:
|
||||
volume_conf = """
|
||||
<target %s>
|
||||
driver iser
|
||||
backing-store %s
|
||||
%s
|
||||
</target>
|
||||
""" % (name, path, chap_auth)
|
||||
|
||||
LOG.info(_('Creating iser_target for: %s') % vol_id)
|
||||
volumes_dir = CONF.volumes_dir
|
||||
volume_path = os.path.join(volumes_dir, vol_id)
|
||||
|
||||
f = open(volume_path, 'w+')
|
||||
f.write(volume_conf)
|
||||
f.close()
|
||||
|
||||
old_persist_file = None
|
||||
old_name = kwargs.get('old_name', None)
|
||||
if old_name is not None:
|
||||
old_persist_file = os.path.join(volumes_dir, old_name)
|
||||
|
||||
try:
|
||||
(out, err) = self._execute('tgt-admin',
|
||||
'--update',
|
||||
name,
|
||||
run_as_root=True)
|
||||
except exception.ProcessExecutionError as e:
|
||||
LOG.error(_("Failed to create iser target for volume "
|
||||
"id:%(vol_id)s: %(e)s")
|
||||
% {'vol_id': vol_id, 'e': str(e)})
|
||||
|
||||
#Don't forget to remove the persistent file we created
|
||||
os.unlink(volume_path)
|
||||
raise exception.ISERTargetCreateFailed(volume_id=vol_id)
|
||||
|
||||
iqn = '%s%s' % (CONF.iser_target_prefix, vol_id)
|
||||
tid = self._get_target(iqn)
|
||||
if tid is None:
|
||||
LOG.error(_("Failed to create iser target for volume "
|
||||
"id:%(vol_id)s. Please ensure your tgtd config file "
|
||||
"contains 'include %(volumes_dir)s/*'") % locals())
|
||||
raise exception.NotFound()
|
||||
|
||||
if old_persist_file is not None and os.path.exists(old_persist_file):
|
||||
os.unlink(old_persist_file)
|
||||
|
||||
return tid
|
||||
|
||||
def remove_iser_target(self, tid, lun, vol_id, **kwargs):
|
||||
LOG.info(_('Removing iser_target for: %s') % vol_id)
|
||||
vol_uuid_file = CONF.volume_name_template % vol_id
|
||||
volume_path = os.path.join(CONF.volumes_dir, vol_uuid_file)
|
||||
if os.path.isfile(volume_path):
|
||||
iqn = '%s%s' % (CONF.iser_target_prefix,
|
||||
vol_uuid_file)
|
||||
else:
|
||||
raise exception.ISERTargetRemoveFailed(volume_id=vol_id)
|
||||
try:
|
||||
# NOTE(vish): --force is a workaround for bug:
|
||||
# https://bugs.launchpad.net/cinder/+bug/1159948
|
||||
self._execute('tgt-admin',
|
||||
'--force',
|
||||
'--delete',
|
||||
iqn,
|
||||
run_as_root=True)
|
||||
except exception.ProcessExecutionError as e:
|
||||
LOG.error(_("Failed to remove iser target for volume "
|
||||
"id:%(vol_id)s: %(e)s")
|
||||
% {'vol_id': vol_id, 'e': str(e)})
|
||||
raise exception.ISERTargetRemoveFailed(volume_id=vol_id)
|
||||
|
||||
os.unlink(volume_path)
|
||||
|
||||
def show_target(self, tid, iqn=None, **kwargs):
|
||||
if iqn is None:
|
||||
raise exception.InvalidParameterValue(
|
||||
err=_('valid iqn needed for show_target'))
|
||||
|
||||
tid = self._get_target(iqn)
|
||||
if tid is None:
|
||||
raise exception.NotFound()
|
||||
|
||||
|
||||
class FakeIserHelper(object):
|
||||
|
||||
def __init__(self):
|
||||
self.tid = 1
|
||||
|
||||
def set_execute(self, execute):
|
||||
self._execute = execute
|
||||
|
||||
def create_iser_target(self, *args, **kwargs):
|
||||
self.tid += 1
|
||||
return self.tid
|
||||
|
||||
|
||||
def get_target_admin():
|
||||
if CONF.iser_helper == 'fake':
|
||||
return FakeIserHelper()
|
||||
else:
|
||||
return TgtAdm()
|
|
@ -310,6 +310,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")
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
@ -33,10 +34,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)
|
||||
|
|
|
@ -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()
|
|
@ -32,6 +32,7 @@ from oslo.config import cfg
|
|||
|
||||
from cinder.brick.initiator import connector as brick_conn
|
||||
from cinder.brick.iscsi import iscsi
|
||||
from cinder.brick.iser import iser
|
||||
from cinder import context
|
||||
from cinder import db
|
||||
from cinder import exception
|
||||
|
@ -1814,8 +1815,36 @@ class ISCSITestCase(DriverTestCase):
|
|||
iscsi_driver.validate_connector, connector)
|
||||
|
||||
|
||||
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):
|
||||
|
|
|
@ -46,19 +46,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'),
|
||||
|
@ -70,6 +87,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):
|
||||
|
@ -612,6 +630,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. '<ip>:<port>,<portal> <target IQN>'
|
||||
|
||||
``provider_auth``
|
||||
if present, contains a space-separated triple:
|
||||
'<auth method> <auth username> <auth password>'.
|
||||
`CHAP` is the only auth_method in use at the moment.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ISERDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def _do_iser_discovery(self, volume):
|
||||
LOG.warn(_("ISER provider_location not stored, using discovery"))
|
||||
|
||||
volume_name = volume['name']
|
||||
|
||||
(out, _err) = self._execute('iscsiadm', '-m', 'discovery',
|
||||
'-t', 'sendtargets', '-p', volume['host'],
|
||||
run_as_root=True)
|
||||
for target in out.splitlines():
|
||||
if (self.configuration.iser_ip_address in target
|
||||
and volume_name in target):
|
||||
return target
|
||||
return None
|
||||
|
||||
def _get_iser_properties(self, volume):
|
||||
"""Gets iser configuration
|
||||
|
||||
We ideally get saved information in the volume entity, but fall back
|
||||
to discovery if need be. Discovery may be completely removed in future
|
||||
The properties are:
|
||||
|
||||
:target_discovered: boolean indicating whether discovery was used
|
||||
|
||||
:target_iqn: the IQN of the iSER target
|
||||
|
||||
:target_portal: the portal of the iSER target
|
||||
|
||||
:target_lun: the lun of the iSER target
|
||||
|
||||
:volume_id: the id of the volume (currently used by xen)
|
||||
|
||||
:auth_method:, :auth_username:, :auth_password:
|
||||
|
||||
the authentication details. Right now, either auth_method is not
|
||||
present meaning no authentication, or auth_method == `CHAP`
|
||||
meaning use CHAP with the specified credentials.
|
||||
"""
|
||||
|
||||
properties = {}
|
||||
|
||||
location = volume['provider_location']
|
||||
|
||||
if location:
|
||||
# provider_location is the same format as iSER discovery output
|
||||
properties['target_discovered'] = False
|
||||
else:
|
||||
location = self._do_iser_discovery(volume)
|
||||
|
||||
if not location:
|
||||
msg = (_("Could not find iSER export for volume %s") %
|
||||
(volume['name']))
|
||||
raise exception.InvalidVolume(reason=msg)
|
||||
|
||||
LOG.debug(_("ISER Discovery: Found %s") % (location))
|
||||
properties['target_discovered'] = True
|
||||
|
||||
results = location.split(" ")
|
||||
properties['target_portal'] = results[0].split(",")[0]
|
||||
properties['target_iqn'] = results[1]
|
||||
try:
|
||||
properties['target_lun'] = int(results[2])
|
||||
except (IndexError, ValueError):
|
||||
if (self.configuration.volume_driver in
|
||||
['cinder.volume.drivers.lvm.LVMISERDriver',
|
||||
'cinder.volume.drivers.lvm.ThinLVMVolumeDriver'] and
|
||||
self.configuration.iser_helper == 'tgtadm'):
|
||||
properties['target_lun'] = 1
|
||||
else:
|
||||
properties['target_lun'] = 0
|
||||
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
auth = volume['provider_auth']
|
||||
if auth:
|
||||
(auth_method, auth_username, auth_secret) = auth.split()
|
||||
|
||||
properties['auth_method'] = auth_method
|
||||
properties['auth_username'] = auth_username
|
||||
properties['auth_password'] = auth_secret
|
||||
|
||||
return properties
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Initializes the connection and returns connection info.
|
||||
|
||||
The iser driver returns a driver_volume_type of 'iser'.
|
||||
The format of the driver data is defined in _get_iser_properties.
|
||||
Example return value::
|
||||
|
||||
{
|
||||
'driver_volume_type': 'iser'
|
||||
'data': {
|
||||
'target_discovered': True,
|
||||
'target_iqn':
|
||||
'iqn.2010-10.org.iser.openstack:volume-00000001',
|
||||
'target_portal': '127.0.0.0.1:3260',
|
||||
'volume_id': 1,
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
iser_properties = self._get_iser_properties(volume)
|
||||
return {
|
||||
'driver_volume_type': 'iser',
|
||||
'data': iser_properties
|
||||
}
|
||||
|
||||
def _check_valid_device(self, path):
|
||||
cmd = ('dd', 'if=%(path)s' % {"path": path},
|
||||
'of=/dev/null', 'count=1')
|
||||
out, info = None, None
|
||||
try:
|
||||
out, info = self._execute(*cmd, run_as_root=True)
|
||||
except exception.ProcessExecutionError as e:
|
||||
LOG.error(_("Failed to access the device on the path "
|
||||
"%(path)s: %(error)s.") %
|
||||
{"path": path, "error": e.stderr})
|
||||
return False
|
||||
# If the info is none, the path does not exist.
|
||||
if info is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _attach_volume(self, context, volume, connector):
|
||||
"""Attach the volume."""
|
||||
iser_properties = None
|
||||
host_device = None
|
||||
init_conn = self.initialize_connection(volume, connector)
|
||||
iser_properties = init_conn['data']
|
||||
|
||||
# code "inspired by" nova/virt/libvirt/volume.py
|
||||
try:
|
||||
self._run_iscsiadm(iser_properties, ())
|
||||
except exception.ProcessExecutionError as exc:
|
||||
# iscsiadm returns 21 for "No records found" after version 2.0-871
|
||||
if exc.exit_code in [21, 255]:
|
||||
self._run_iscsiadm(iser_properties, ('--op', 'new'))
|
||||
else:
|
||||
raise
|
||||
|
||||
if iser_properties.get('auth_method'):
|
||||
self._iscsiadm_update(iser_properties,
|
||||
"node.session.auth.authmethod",
|
||||
iser_properties['auth_method'])
|
||||
self._iscsiadm_update(iser_properties,
|
||||
"node.session.auth.username",
|
||||
iser_properties['auth_username'])
|
||||
self._iscsiadm_update(iser_properties,
|
||||
"node.session.auth.password",
|
||||
iser_properties['auth_password'])
|
||||
|
||||
host_device = ("/dev/disk/by-path/ip-%s-iser-%s-lun-%s" %
|
||||
(iser_properties['target_portal'],
|
||||
iser_properties['target_iqn'],
|
||||
iser_properties.get('target_lun', 0)))
|
||||
|
||||
out = self._run_iscsiadm_bare(["-m", "session"],
|
||||
run_as_root=True,
|
||||
check_exit_code=[0, 1, 21])[0] or ""
|
||||
|
||||
portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]}
|
||||
for p in out.splitlines() if p.startswith("iser:")]
|
||||
|
||||
stripped_portal = iser_properties['target_portal'].split(",")[0]
|
||||
length_iqn = [s for s in portals
|
||||
if stripped_portal ==
|
||||
s['portal'].split(",")[0] and
|
||||
s['iqn'] == iser_properties['target_iqn']]
|
||||
if len(portals) == 0 or len(length_iqn) == 0:
|
||||
try:
|
||||
self._run_iscsiadm(iser_properties, ("--login",),
|
||||
check_exit_code=[0, 255])
|
||||
except exception.ProcessExecutionError as err:
|
||||
if err.exit_code in [15]:
|
||||
self._iscsiadm_update(iser_properties,
|
||||
"node.startup",
|
||||
"automatic")
|
||||
return iser_properties, host_device
|
||||
else:
|
||||
raise
|
||||
|
||||
self._iscsiadm_update(iser_properties,
|
||||
"node.startup", "automatic")
|
||||
|
||||
tries = 0
|
||||
while not os.path.exists(host_device):
|
||||
if tries >= self.configuration.num_iser_scan_tries:
|
||||
raise exception.CinderException(_("iSER device "
|
||||
"not found "
|
||||
"at %s") % (host_device))
|
||||
|
||||
LOG.warn(_("ISER volume not yet found at: %(host_device)s. "
|
||||
"Will rescan & retry. Try number: %(tries)s.") %
|
||||
{'host_device': host_device, 'tries': tries})
|
||||
|
||||
# The rescan isn't documented as being necessary(?),
|
||||
# but it helps
|
||||
self._run_iscsiadm(iser_properties, ("--rescan",))
|
||||
|
||||
tries = tries + 1
|
||||
if not os.path.exists(host_device):
|
||||
time.sleep(tries ** 2)
|
||||
|
||||
if tries != 0:
|
||||
LOG.debug(_("Found iSER node %(host_device)s "
|
||||
"(after %(tries)s rescans).") %
|
||||
{'host_device': host_device,
|
||||
'tries': tries})
|
||||
|
||||
if not self._check_valid_device(host_device):
|
||||
raise exception.DeviceUnavailable(path=host_device,
|
||||
reason=(_("Unable to access "
|
||||
"the backend storage "
|
||||
"via the path "
|
||||
"%(path)s.") %
|
||||
{'path': host_device}))
|
||||
return iser_properties, host_device
|
||||
|
||||
def _update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = {}
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
data["volume_backend_name"] = backend_name or 'Generic_iSER'
|
||||
data["vendor_name"] = 'Open Source'
|
||||
data["driver_version"] = '1.0'
|
||||
data["storage_protocol"] = 'iSER'
|
||||
|
||||
data['total_capacity_gb'] = 'infinite'
|
||||
data['free_capacity_gb'] = 'infinite'
|
||||
data['reserved_percentage'] = 100
|
||||
data['QoS_support'] = False
|
||||
self._stats = data
|
||||
|
||||
|
||||
class FakeISERDriver(FakeISCSIDriver):
|
||||
"""Logs calls instead of executing."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FakeISERDriver, self).__init__(execute=self.fake_execute,
|
||||
*args, **kwargs)
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
return {
|
||||
'driver_volume_type': 'iser',
|
||||
'data': {}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fake_execute(cmd, *_args, **_kwargs):
|
||||
"""Execute that simply logs the command."""
|
||||
LOG.debug(_("FAKE ISER: %s"), cmd)
|
||||
return (None, None)
|
||||
|
||||
|
||||
class FibreChannelDriver(VolumeDriver):
|
||||
"""Executes commands relating to Fibre Channel volumes."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
|
@ -28,6 +28,7 @@ import socket
|
|||
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
|
||||
|
@ -626,6 +627,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. '<ip>:<port>,<portal> <target IQN>'
|
||||
|
||||
``provider_auth``
|
||||
if present, contains a space-separated triple:
|
||||
'<auth method> <auth username> <auth password>'.
|
||||
`CHAP` is the only auth_method in use at the moment.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tgtadm = iser.get_target_admin()
|
||||
LVMVolumeDriver.__init__(self, *args, **kwargs)
|
||||
|
||||
def set_execute(self, execute):
|
||||
LVMVolumeDriver.set_execute(self, execute)
|
||||
self.tgtadm.set_execute(execute)
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""Synchronously recreates an export for a logical volume."""
|
||||
|
||||
if not isinstance(self.tgtadm, iser.TgtAdm):
|
||||
try:
|
||||
iser_target = self.db.volume_get_iscsi_target_num(
|
||||
context,
|
||||
volume['id'])
|
||||
except exception.NotFound:
|
||||
LOG.info(_("Skipping ensure_export. No iser_target "
|
||||
"provisioned for volume: %s"), volume['id'])
|
||||
return
|
||||
else:
|
||||
iser_target = 1 # dummy value when using TgtAdm
|
||||
|
||||
chap_auth = None
|
||||
|
||||
# Check for https://bugs.launchpad.net/cinder/+bug/1065702
|
||||
old_name = None
|
||||
volume_name = volume['name']
|
||||
if (volume['provider_location'] is not None and
|
||||
volume['name'] not in volume['provider_location']):
|
||||
|
||||
msg = _('Detected inconsistency in provider_location id')
|
||||
LOG.debug(msg)
|
||||
old_name = self._fix_id_migration(context, volume)
|
||||
if 'in-use' in volume['status']:
|
||||
volume_name = old_name
|
||||
old_name = None
|
||||
|
||||
iser_name = "%s%s" % (self.configuration.iser_target_prefix,
|
||||
volume_name)
|
||||
volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
|
||||
volume_name)
|
||||
|
||||
self.tgtadm.create_iser_target(iser_name, iser_target,
|
||||
0, volume_path, chap_auth,
|
||||
check_exit_code=False,
|
||||
old_name=old_name)
|
||||
|
||||
def _ensure_iser_targets(self, context, host):
|
||||
"""Ensure that target ids have been created in datastore."""
|
||||
if not isinstance(self.tgtadm, iser.TgtAdm):
|
||||
host_iser_targets = self.db.iscsi_target_count_by_host(context,
|
||||
host)
|
||||
if host_iser_targets >= self.configuration.iser_num_targets:
|
||||
return
|
||||
|
||||
# NOTE(vish): Target ids start at 1, not 0.
|
||||
target_end = self.configuration.iser_num_targets + 1
|
||||
for target_num in xrange(1, target_end):
|
||||
target = {'host': host, 'target_num': target_num}
|
||||
self.db.iscsi_target_create_safe(context, target)
|
||||
|
||||
def create_export(self, context, volume):
|
||||
"""Creates an export for a logical volume."""
|
||||
|
||||
iser_name = "%s%s" % (self.configuration.iser_target_prefix,
|
||||
volume['name'])
|
||||
volume_path = "/dev/%s/%s" % (self.configuration.volume_group,
|
||||
volume['name'])
|
||||
model_update = {}
|
||||
|
||||
# TODO(jdg): In the future move all of the dependent stuff into the
|
||||
# cooresponding target admin class
|
||||
if not isinstance(self.tgtadm, iser.TgtAdm):
|
||||
lun = 0
|
||||
self._ensure_iser_targets(context, volume['host'])
|
||||
iser_target = self.db.volume_allocate_iscsi_target(context,
|
||||
volume['id'],
|
||||
volume['host'])
|
||||
else:
|
||||
lun = 1 # For tgtadm the controller is lun 0, dev starts at lun 1
|
||||
iser_target = 0
|
||||
|
||||
# Use the same method to generate the username and the password.
|
||||
chap_username = utils.generate_username()
|
||||
chap_password = utils.generate_password()
|
||||
chap_auth = self._iser_authentication('IncomingUser', chap_username,
|
||||
chap_password)
|
||||
tid = self.tgtadm.create_iser_target(iser_name,
|
||||
iser_target,
|
||||
0,
|
||||
volume_path,
|
||||
chap_auth)
|
||||
model_update['provider_location'] = self._iser_location(
|
||||
self.configuration.iser_ip_address, tid, iser_name, lun)
|
||||
model_update['provider_auth'] = self._iser_authentication(
|
||||
'CHAP', chap_username, chap_password)
|
||||
return model_update
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Removes an export for a logical volume."""
|
||||
|
||||
if not isinstance(self.tgtadm, iser.TgtAdm):
|
||||
try:
|
||||
iser_target = self.db.volume_get_iscsi_target_num(
|
||||
context,
|
||||
volume['id'])
|
||||
except exception.NotFound:
|
||||
LOG.info(_("Skipping remove_export. No iser_target "
|
||||
"provisioned for volume: %s"), volume['id'])
|
||||
return
|
||||
else:
|
||||
iser_target = 0
|
||||
|
||||
try:
|
||||
|
||||
# NOTE: provider_location may be unset if the volume hasn't
|
||||
# been exported
|
||||
location = volume['provider_location'].split(' ')
|
||||
iqn = location[1]
|
||||
|
||||
self.tgtadm.show_target(iser_target, iqn=iqn)
|
||||
|
||||
except Exception as e:
|
||||
LOG.info(_("Skipping remove_export. No iser_target "
|
||||
"is presently exported for volume: %s"), volume['id'])
|
||||
return
|
||||
|
||||
self.tgtadm.remove_iser_target(iser_target, 0, volume['id'])
|
||||
|
||||
def _update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = {}
|
||||
|
||||
# Note(zhiteng): These information are driver/backend specific,
|
||||
# each driver may define these values in its own config options
|
||||
# or fetch from driver specific configuration file.
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
data["volume_backend_name"] = backend_name or 'LVM_iSER'
|
||||
data["vendor_name"] = 'Open Source'
|
||||
data["driver_version"] = self.VERSION
|
||||
data["storage_protocol"] = 'iSER'
|
||||
|
||||
data['total_capacity_gb'] = 0
|
||||
data['free_capacity_gb'] = 0
|
||||
data['reserved_percentage'] = self.configuration.reserved_percentage
|
||||
data['QoS_support'] = False
|
||||
|
||||
try:
|
||||
out, err = self._execute('vgs', '--noheadings', '--nosuffix',
|
||||
'--unit=G', '-o', 'name,size,free',
|
||||
self.configuration.volume_group,
|
||||
run_as_root=True)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error retrieving volume status: %s"), exc.stderr)
|
||||
out = False
|
||||
|
||||
if out:
|
||||
volume = out.split()
|
||||
data['total_capacity_gb'] = float(volume[1].replace(',', '.'))
|
||||
data['free_capacity_gb'] = float(volume[2].replace(',', '.'))
|
||||
|
||||
self._stats = data
|
||||
|
||||
def _iser_location(self, ip, target, iqn, lun=None):
|
||||
return "%s:%s,%s %s %s" % (ip, self.configuration.iser_port,
|
||||
target, iqn, lun)
|
||||
|
||||
def _iser_authentication(self, chap, name, password):
|
||||
return "%s %s %s" % (chap, name, password)
|
||||
|
||||
|
||||
class ThinLVMVolumeDriver(LVMISCSIDriver):
|
||||
"""Subclass for thin provisioned LVM's."""
|
||||
|
||||
|
|
|
@ -851,6 +851,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=<None>
|
||||
|
@ -1405,6 +1423,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
|
||||
|
||||
|
@ -1426,7 +1447,7 @@
|
|||
#
|
||||
|
||||
# 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
|
||||
|
||||
#
|
||||
# Options defined in cinder.volume.drivers.gpfs
|
||||
|
|
Loading…
Reference in New Issue