Merge "Add ceph iscsi volume driver"

This commit is contained in:
Zuul
2021-02-04 04:01:11 +00:00
committed by Gerrit Code Review
13 changed files with 915 additions and 1 deletions
+3
View File
@@ -70,6 +70,8 @@ from cinder import ssh_utils as cinder_sshutils
from cinder.transfer import api as cinder_transfer_api
from cinder.volume import api as cinder_volume_api
from cinder.volume import driver as cinder_volume_driver
from cinder.volume.drivers.ceph import rbd_iscsi as \
cinder_volume_drivers_ceph_rbdiscsi
from cinder.volume.drivers.datera import datera_iscsi as \
cinder_volume_drivers_datera_dateraiscsi
from cinder.volume.drivers.dell_emc.powerflex import driver as \
@@ -312,6 +314,7 @@ def list_opts():
cinder_volume_driver.scst_opts,
cinder_volume_driver.image_opts,
cinder_volume_driver.fqdn_opts,
cinder_volume_drivers_ceph_rbdiscsi.RBD_ISCSI_OPTS,
cinder_volume_drivers_dell_emc_powerflex_driver.
powerflex_opts,
cinder_volume_drivers_dell_emc_powermax_common.powermax_opts,
@@ -0,0 +1,25 @@
# 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.
#
"""Fake rbd-iscsi-client for testing without installing the client."""
import sys
from unittest import mock
from cinder.tests.unit.volume.drivers.ceph \
import fake_rbd_iscsi_client_exceptions as clientexceptions
rbdclient = mock.MagicMock()
rbdclient.version = "0.1.5"
rbdclient.exceptions = clientexceptions
sys.modules['rbd_iscsi_client'] = rbdclient
@@ -0,0 +1,116 @@
# 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.
#
"""Fake client exceptions to use."""
class UnsupportedVersion(Exception):
"""Unsupported version of the client."""
pass
class ClientException(Exception):
"""The base exception class for these fake exceptions."""
_error_code = None
_error_desc = None
_error_ref = None
_debug1 = None
_debug2 = None
def __init__(self, error=None):
if error:
if 'code' in error:
self._error_code = error['code']
if 'desc' in error:
self._error_desc = error['desc']
if 'ref' in error:
self._error_ref = error['ref']
if 'debug1' in error:
self._debug1 = error['debug1']
if 'debug2' in error:
self._debug2 = error['debug2']
def get_code(self):
return self._error_code
def get_description(self):
return self._error_desc
def get_ref(self):
return self._error_ref
def __str__(self):
formatted_string = self.message
if self.http_status:
formatted_string += " (HTTP %s)" % self.http_status
if self._error_code:
formatted_string += " %s" % self._error_code
if self._error_desc:
formatted_string += " - %s" % self._error_desc
if self._error_ref:
formatted_string += " - %s" % self._error_ref
if self._debug1:
formatted_string += " (1: '%s')" % self._debug1
if self._debug2:
formatted_string += " (2: '%s')" % self._debug2
return formatted_string
class HTTPConflict(ClientException):
http_status = 409
message = "Conflict"
def __init__(self, error=None):
if error:
super(HTTPConflict, self).__init__(error)
if 'message' in error:
self._error_desc = error['message']
def get_description(self):
return self._error_desc
class HTTPNotFound(ClientException):
http_status = 404
message = "Not found"
class HTTPForbidden(ClientException):
http_status = 403
message = "Forbidden"
class HTTPBadRequest(ClientException):
http_status = 400
message = "Bad request"
class HTTPUnauthorized(ClientException):
http_status = 401
message = "Unauthorized"
class HTTPServerError(ClientException):
http_status = 500
message = "Error"
def __init__(self, error=None):
if error and 'message' in error:
self._error_desc = error['message']
def get_description(self):
return self._error_desc
@@ -0,0 +1,246 @@
# Copyright 2012 Josh Durgin
# Copyright 2013 Canonical Ltd.
# 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.
from unittest import mock
import ddt
from cinder import context
from cinder import exception
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit import test
from cinder.tests.unit.volume.drivers.ceph \
import fake_rbd_iscsi_client as fake_client
import cinder.volume.drivers.ceph.rbd_iscsi as driver
# This is used to collect raised exceptions so that tests may check what was
# raised.
# NOTE: this must be initialised in test setUp().
RAISED_EXCEPTIONS = []
@ddt.ddt
class RBDISCSITestCase(test.TestCase):
def setUp(self):
global RAISED_EXCEPTIONS
RAISED_EXCEPTIONS = []
super(RBDISCSITestCase, self).setUp()
self.context = context.get_admin_context()
# bogus access to prevent pep8 violation
# from the import of fake_client.
# fake_client must be imported to create the fake
# rbd_iscsi_client system module
fake_client.rbdclient
self.fake_target_iqn = 'iqn.2019-01.com.suse.iscsi-gw:iscsi-igw'
self.fake_valid_response = {'status': '200'}
self.fake_clients = \
{'response':
{'Content-Type': 'application/json',
'Content-Length': '55',
'Server': 'Werkzeug/0.14.1 Python/2.7.15rc1',
'Date': 'Wed, 19 Jun 2019 20:13:18 GMT',
'status': '200',
'content-location': 'http://192.168.121.11:5001/api/clients/'
'XX_REPLACE_ME'},
'body':
{'clients': ['iqn.1993-08.org.debian:01:5d3b9abba13d']}}
self.volume_a = fake_volume.fake_volume_obj(
self.context,
**{'name': u'volume-0000000a',
'id': '4c39c3c7-168f-4b32-b585-77f1b3bf0a38',
'size': 10})
self.volume_b = fake_volume.fake_volume_obj(
self.context,
**{'name': u'volume-0000000b',
'id': '0c7d1f44-5a06-403f-bb82-ae7ad0d693a6',
'size': 10})
self.volume_c = fake_volume.fake_volume_obj(
self.context,
**{'name': u'volume-0000000a',
'id': '55555555-222f-4b32-b585-9991b3bf0a99',
'size': 12,
'encryption_key_id': fake.ENCRYPTION_KEY_ID})
def setup_configuration(self):
config = mock.MagicMock()
config.rbd_cluster_name = 'nondefault'
config.rbd_pool = 'rbd'
config.rbd_ceph_conf = '/etc/ceph/my_ceph.conf'
config.rbd_secret_uuid = None
config.rbd_user = 'cinder'
config.volume_backend_name = None
config.rbd_iscsi_api_user = 'fake_user'
config.rbd_iscsi_api_password = 'fake_password'
config.rbd_iscsi_api_url = 'http://fake.com:5000'
return config
@mock.patch(
'rbd_iscsi_client.client.RBDISCSIClient',
spec=True,
)
def setup_mock_client(self, _m_client, config=None, mock_conf=None):
_m_client = _m_client.return_value
# Configure the base constants, defaults etc...
if mock_conf:
_m_client.configure_mock(**mock_conf)
if config is None:
config = self.setup_configuration()
self.driver = driver.RBDISCSIDriver(configuration=config)
self.driver.set_initialized()
return _m_client
@mock.patch('rbd_iscsi_client.version', '0.1.0')
def test_unsupported_client_version(self):
self.setup_mock_client()
with mock.patch('cinder.volume.drivers.rbd.RBDDriver.do_setup'):
self.assertRaises(exception.InvalidInput,
self.driver.do_setup, None)
@ddt.data({'user': None, 'password': 'foo',
'url': 'http://fake.com:5000', 'iqn': None},
{'user': None, 'password': None,
'url': 'http://fake', 'iqn': None},
{'user': None, 'password': None,
'url': None, 'iqn': None},
{'user': 'fake', 'password': 'fake',
'url': None, 'iqn': None},
{'user': 'fake', 'password': 'fake',
'url': 'fake', 'iqn': None},
)
@ddt.unpack
def test_min_config(self, user, password, url, iqn):
config = self.setup_configuration()
config.rbd_iscsi_api_user = user
config.rbd_iscsi_api_password = password
config.rbd_iscsi_api_url = url
config.rbd_iscsi_target_iqn = iqn
self.setup_mock_client(config=config)
with mock.patch('cinder.volume.drivers.rbd.RBDDriver'
'.check_for_setup_error'):
self.assertRaises(exception.InvalidConfigurationValue,
self.driver.check_for_setup_error)
@ddt.data({'response': None},
{'response': {'nothing': 'nothing'}},
{'response': {'status': '300'}})
@ddt.unpack
def test_do_setup(self, response):
mock_conf = {
'get_api.return_value': (response, None)}
mock_client = self.setup_mock_client(mock_conf=mock_conf)
with mock.patch('cinder.volume.drivers.rbd.RBDDriver.do_setup'), \
mock.patch.object(driver.RBDISCSIDriver,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
self.assertRaises(exception.InvalidConfigurationValue,
self.driver.do_setup, None)
@mock.patch('rbd_iscsi_client.version', "0.1.4")
def test_unsupported_version(self):
self.setup_mock_client()
self.assertRaises(exception.InvalidInput,
self.driver._create_client)
@ddt.data({'status': '200',
'target_iqn': 'iqn.2019-01.com.suse.iscsi-gw:iscsi-igw',
'clients': ['foo']},
{'status': '300',
'target_iqn': 'iqn.2019-01.com.suse.iscsi-gw:iscsi-igw',
'clients': None}
)
@ddt.unpack
def test__get_clients(self, status, target_iqn, clients):
config = self.setup_configuration()
config.rbd_iscsi_target_iqn = target_iqn
response = self.fake_clients['response']
response['status'] = status
response['content-location'] = (
response['content-location'].replace('XX_REPLACE_ME', target_iqn))
body = self.fake_clients['body']
mock_conf = {
'get_clients.return_value': (response, body),
'get_api.return_value': (self.fake_valid_response, None)
}
mock_client = self.setup_mock_client(mock_conf=mock_conf,
config=config)
with mock.patch('cinder.volume.drivers.rbd.RBDDriver.do_setup'), \
mock.patch.object(driver.RBDISCSIDriver,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
self.driver.do_setup(None)
if status == '200':
actual_response = self.driver._get_clients()
self.assertEqual(actual_response, body)
else:
# we expect an exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver._get_clients)
@ddt.data({'status': '200',
'body': {'created': 'someday',
'discovery_auth': 'somecrap',
'disks': 'fakedisks',
'gateways': 'fakegws',
'targets': 'faketargets'}},
{'status': '300',
'body': None})
@ddt.unpack
def test__get_config(self, status, body):
config = self.setup_configuration()
config.rbd_iscsi_target_iqn = self.fake_target_iqn
response = self.fake_clients['response']
response['status'] = status
response['content-location'] = (
response['content-location'].replace('XX_REPLACE_ME',
self.fake_target_iqn))
mock_conf = {
'get_config.return_value': (response, body),
'get_api.return_value': (self.fake_valid_response, None)
}
mock_client = self.setup_mock_client(mock_conf=mock_conf,
config=config)
with mock.patch('cinder.volume.drivers.rbd.RBDDriver.do_setup'), \
mock.patch.object(driver.RBDISCSIDriver,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
self.driver.do_setup(None)
if status == '200':
actual_response = self.driver._get_config()
self.assertEqual(body, actual_response)
else:
# we expect an exception
self.assertRaises(exception.VolumeBackendAPIException,
self.driver._get_config)
+496
View File
@@ -0,0 +1,496 @@
# 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.
"""RADOS Block Device iSCSI Driver"""
from distutils import version
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import netutils
from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder import utils
from cinder.volume import configuration
from cinder.volume.drivers import rbd
from cinder.volume import volume_utils
try:
import rbd_iscsi_client
from rbd_iscsi_client import client
from rbd_iscsi_client import exceptions as client_exceptions
except ImportError:
rbd_iscsi_client = None
client = None
client_exceptions = None
LOG = logging.getLogger(__name__)
RBD_ISCSI_OPTS = [
cfg.StrOpt('rbd_iscsi_api_user',
default='',
help='The username for the rbd_target_api service'),
cfg.StrOpt('rbd_iscsi_api_password',
default='',
secret=True,
help='The username for the rbd_target_api service'),
cfg.StrOpt('rbd_iscsi_api_url',
default='',
help='The url to the rbd_target_api service'),
cfg.BoolOpt('rbd_iscsi_api_debug',
default=False,
help='Enable client request debugging.'),
cfg.StrOpt('rbd_iscsi_target_iqn',
default=None,
help='The preconfigured target_iqn on the iscsi gateway.'),
]
CONF = cfg.CONF
CONF.register_opts(RBD_ISCSI_OPTS, group=configuration.SHARED_CONF_GROUP)
MIN_CLIENT_VERSION = "0.1.8"
@interface.volumedriver
class RBDISCSIDriver(rbd.RBDDriver):
"""Implements RADOS block device (RBD) iSCSI volume commands."""
VERSION = '1.0.0'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Cinder_Jenkins"
SUPPORTS_ACTIVE_ACTIVE = True
STORAGE_PROTOCOL = 'iSCSI'
CHAP_LENGTH = 16
# The target IQN to use for creating all exports
# we map all the targets for OpenStack attaches to this.
target_iqn = None
def __init__(self, active_backend_id=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.configuration.append_config_values(RBD_ISCSI_OPTS)
@classmethod
def get_driver_options(cls):
additional_opts = cls._get_oslo_driver_opts(
'replication_device', 'reserved_percentage',
'max_over_subscription_ratio', 'volume_dd_blocksize',
'driver_ssl_cert_verify', 'suppress_requests_ssl_warnings')
return rbd.RBD_OPTS + RBD_ISCSI_OPTS + additional_opts
def _create_client(self):
client_version = rbd_iscsi_client.version
if (version.StrictVersion(client_version) <
version.StrictVersion(MIN_CLIENT_VERSION)):
ex_msg = (_('Invalid rbd_iscsi_client version found (%(found)s). '
'Version %(min)s or greater required. Run "pip'
' install --upgrade rbd-iscsi-client" to upgrade'
' the client.')
% {'found': client_version,
'min': MIN_CLIENT_VERSION})
LOG.error(ex_msg)
raise exception.InvalidInput(reason=ex_msg)
config = self.configuration
ssl_warn = config.safe_get('suppress_requests_ssl_warnings')
cl = client.RBDISCSIClient(
config.safe_get('rbd_iscsi_api_user'),
config.safe_get('rbd_iscsi_api_password'),
config.safe_get('rbd_iscsi_api_url'),
secure=config.safe_get('driver_ssl_cert_verify'),
suppress_ssl_warnings=ssl_warn
)
return cl
def _is_status_200(self, response):
return (response and 'status' in response and
response['status'] == '200')
def do_setup(self, context):
"""Perform initialization steps that could raise exceptions."""
super(RBDISCSIDriver, self).do_setup(context)
if client is None:
msg = _("You must install rbd-iscsi-client python package "
"before using this driver.")
raise exception.VolumeDriverException(data=msg)
# Make sure we have the basic settings we need to talk to the
# iscsi api service
config = self.configuration
self.client = self._create_client()
self.client.set_debug_flag(config.safe_get('rbd_iscsi_api_debug'))
resp, body = self.client.get_api()
if not self._is_status_200(resp):
# failed to fetch the open api url
raise exception.InvalidConfigurationValue(
option='rbd_iscsi_api_url',
value='Could not talk to the rbd-target-api')
# The admin had to have setup a target_iqn in the iscsi gateway
# already in order for the gateways to work properly
self.target_iqn = self.configuration.safe_get('rbd_iscsi_target_iqn')
LOG.info("Using target_iqn '%s'", self.target_iqn)
def check_for_setup_error(self):
"""Return an error if prerequisites aren't met."""
super(RBDISCSIDriver, self).check_for_setup_error()
required_options = ['rbd_iscsi_api_user',
'rbd_iscsi_api_password',
'rbd_iscsi_api_url',
'rbd_iscsi_target_iqn']
for attr in required_options:
val = getattr(self.configuration, attr)
if not val:
raise exception.InvalidConfigurationValue(option=attr,
value=val)
def _get_clients(self):
# make sure we have
resp, body = self.client.get_clients(self.target_iqn)
if not self._is_status_200(resp):
msg = _("Failed to get_clients() from rbd-target-api")
raise exception.VolumeBackendAPIException(data=msg)
return body
def _get_config(self):
resp, body = self.client.get_config()
if not self._is_status_200(resp):
msg = _("Failed to get_config() from rbd-target-api")
raise exception.VolumeBackendAPIException(data=msg)
return body
def _get_disks(self):
resp, disks = self.client.get_disks()
if not self._is_status_200(resp):
msg = _("Failed to get_disks() from rbd-target-api")
raise exception.VolumeBackendAPIException(data=msg)
return disks
def create_client(self, initiator_iqn):
"""Create a client iqn on the gateway if it doesn't exist."""
client = self._get_target_client(initiator_iqn)
if not client:
try:
self.client.create_client(self.target_iqn,
initiator_iqn)
except client_exceptions.ClientException as ex:
raise exception.VolumeBackendAPIException(
data=ex.get_description())
def _get_target_client(self, initiator_iqn):
"""Get the config information for a client defined to a target."""
config = self._get_config()
target_config = config['targets'][self.target_iqn]
if initiator_iqn in target_config['clients']:
return target_config['clients'][initiator_iqn]
def _get_auth_for_client(self, initiator_iqn):
initiator_config = self._get_target_client(initiator_iqn)
if initiator_config:
auth = initiator_config['auth']
return auth
def _set_chap_for_client(self, initiator_iqn, username, password):
"""Save the CHAP creds in the client on the gateway."""
# username is 8-64 chars
# Password has to be 12-16 chars
LOG.debug("Setting chap creds to %(user)s : %(pass)s",
{'user': username, 'pass': password})
try:
self.client.set_client_auth(self.target_iqn,
initiator_iqn,
username,
password)
except client_exceptions.ClientException as ex:
raise exception.VolumeBackendAPIException(
data=ex.get_description())
def _get_lun(self, iscsi_config, lun_name, initiator_iqn):
lun = None
target_info = iscsi_config['targets'][self.target_iqn]
luns = target_info['clients'][initiator_iqn]['luns']
if lun_name in luns:
lun = {'name': lun_name,
'id': luns[lun_name]['lun_id']}
return lun
def _lun_name(self, volume_name):
"""Build the iscsi gateway lun name."""
return ("%(pool)s/%(volume_name)s" %
{'pool': self.configuration.rbd_pool,
'volume_name': volume_name})
def get_existing_disks(self):
"""Get the existing list of registered volumes on the gateway."""
resp, disks = self.client.get_disks()
return disks['disks']
@utils.trace
def create_disk(self, volume_name):
"""Register the volume with the iscsi gateways.
We have to register the volume with the iscsi gateway.
Exporting the volume won't work unless the gateway knows
about it.
"""
try:
self.client.find_disk(self.configuration.rbd_pool,
volume_name)
except client_exceptions.HTTPNotFound:
try:
# disk isn't known by the gateways, so lets add it.
self.client.create_disk(self.configuration.rbd_pool,
volume_name)
except client_exceptions.ClientException as ex:
LOG.exception("Couldn't create the disk entry to "
"export the volume.")
raise exception.VolumeBackendAPIException(
data=ex.get_description())
@utils.trace
def register_disk(self, target_iqn, volume_name):
"""Register the disk with the target_iqn."""
lun_name = self._lun_name(volume_name)
try:
self.client.register_disk(target_iqn, lun_name)
except client_exceptions.HTTPBadRequest as ex:
desc = ex.get_description()
search_str = ('is already mapped on target %(target_iqn)s' %
{'target_iqn': self.target_iqn})
if desc.find(search_str):
# The volume is already registered
return
else:
LOG.error("Couldn't register the volume to the target_iqn")
raise exception.VolumeBackendAPIException(
data=ex.get_description())
except client_exceptions.ClientException as ex:
LOG.exception("Couldn't register the volume to the target_iqn",
ex)
raise exception.VolumeBackendAPIException(
data=ex.get_description())
@utils.trace
def unregister_disk(self, target_iqn, volume_name):
"""Unregister the volume from the gateway."""
lun_name = self._lun_name(volume_name)
try:
self.client.unregister_disk(target_iqn, lun_name)
except client_exceptions.ClientException as ex:
LOG.exception("Couldn't unregister the volume to the target_iqn",
ex)
raise exception.VolumeBackendAPIException(
data=ex.get_description())
@utils.trace
def export_disk(self, initiator_iqn, volume_name, iscsi_config):
"""Export a volume to an initiator."""
lun_name = self._lun_name(volume_name)
LOG.debug("Export lun %(lun)s", {'lun': lun_name})
lun = self._get_lun(iscsi_config, lun_name, initiator_iqn)
if lun:
LOG.debug("Found existing lun export.")
return lun
try:
LOG.debug("Creating new lun export for %(lun)s",
{'lun': lun_name})
self.client.export_disk(self.target_iqn, initiator_iqn,
self.configuration.rbd_pool,
volume_name)
resp, iscsi_config = self.client.get_config()
return self._get_lun(iscsi_config, lun_name, initiator_iqn)
except client_exceptions.ClientException as ex:
raise exception.VolumeBackendAPIException(
data=ex.get_description())
@utils.trace
def unexport_disk(self, initiator_iqn, volume_name, iscsi_config):
"""Remove a volume from an initiator."""
lun_name = self._lun_name(volume_name)
LOG.debug("unexport lun %(lun)s", {'lun': lun_name})
lun = self._get_lun(iscsi_config, lun_name, initiator_iqn)
if not lun:
LOG.debug("Didn't find LUN on gateway.")
return
try:
LOG.debug("unexporting %(lun)s", {'lun': lun_name})
self.client.unexport_disk(self.target_iqn, initiator_iqn,
self.configuration.rbd_pool,
volume_name)
except client_exceptions.ClientException as ex:
LOG.exception(ex)
raise exception.VolumeBackendAPIException(
data=ex.get_description())
def find_client_luns(self, target_iqn, client_iqn, iscsi_config):
"""Find luns already exported to an initiator."""
if 'targets' in iscsi_config:
if target_iqn in iscsi_config['targets']:
target_info = iscsi_config['targets'][target_iqn]
if 'clients' in target_info:
clients = target_info['clients']
client = clients[client_iqn]
luns = client['luns']
return luns
@utils.trace
def initialize_connection(self, volume, connector):
"""Export a volume to a host."""
# create client
initiator_iqn = connector['initiator']
self.create_client(initiator_iqn)
auth = self._get_auth_for_client(initiator_iqn)
username = initiator_iqn
if not auth['password']:
password = volume_utils.generate_password(length=self.CHAP_LENGTH)
self._set_chap_for_client(initiator_iqn, username, password)
else:
LOG.debug("using existing CHAP password")
password = auth['password']
# add disk for export
iscsi_config = self._get_config()
# First have to ensure that the disk is registered with
# the gateways.
self.create_disk(volume.name)
self.register_disk(self.target_iqn, volume.name)
iscsi_config = self._get_config()
# Now export the disk to the initiator
lun = self.export_disk(initiator_iqn, volume.name, iscsi_config)
# fetch the updated config so we can get the lun id
iscsi_config = self._get_config()
target_info = iscsi_config['targets'][self.target_iqn]
ips = target_info['ip_list']
target_portal = ips[0]
if netutils.is_valid_ipv6(target_portal):
target_portal = "[{}]:{}".format(
target_portal, "3260")
else:
target_portal = "{}:3260".format(target_portal)
data = {
'driver_volume_type': 'iscsi',
'data': {
'target_iqn': self.target_iqn,
'target_portal': target_portal,
'target_lun': lun['id'],
'auth_method': 'CHAP',
'auth_username': username,
'auth_password': password,
}
}
return data
def _delete_disk(self, volume):
"""Remove the defined disk from the gateway."""
# We only do this when we know it's not exported
# anywhere in the gateway
lun_name = self._lun_name(volume.name)
config = self._get_config()
# Now look for the disk on any exported target
found = False
for target_iqn in config['targets']:
# Do we have the volume we are looking for?
target = config['targets'][target_iqn]
for client_iqn in target['clients'].keys():
if lun_name in target['clients'][client_iqn]['luns']:
found = True
if not found:
# we can delete the disk definition
LOG.info("Deleteing volume definition in iscsi gateway for {}".
format(lun_name))
self.client.delete_disk(self.configuration.rbd_pool, volume.name,
preserve_image=True)
def _terminate_connection(self, volume, initiator_iqn, target_iqn,
iscsi_config):
# remove the disk from the client.
self.unexport_disk(initiator_iqn, volume.name, iscsi_config)
# Try to unregister the disk, since nobody is using it.
self.unregister_disk(self.target_iqn, volume.name)
config = self._get_config()
# If there are no more luns exported to this initiator
# then delete the initiator
luns = self.find_client_luns(target_iqn, initiator_iqn, config)
if not luns:
LOG.debug("There aren't any more LUNs attached to %(iqn)s."
"So we unregister the volume and delete "
"the client entry",
{'iqn': initiator_iqn})
try:
self.client.delete_client(target_iqn, initiator_iqn)
except client_exceptions.ClientException:
LOG.warning("Tried to delete initiator %(iqn)s, but delete "
"failed.", {'iqns': initiator_iqn})
def _terminate_all(self, volume, iscsi_config):
"""Find all exports of this volume for our target_iqn and detach."""
disks = self._get_disks()
lun_name = self._lun_name(volume.name)
if lun_name not in disks['disks']:
LOG.debug("Volume {} not attached anywhere.".format(
lun_name
))
return
for target_iqn_tmp in iscsi_config['targets']:
if self.target_iqn != target_iqn_tmp:
# We don't touch exports for targets
# we aren't configured to manage.
continue
target = iscsi_config['targets'][self.target_iqn]
for client_iqn in target['clients'].keys():
if lun_name in target['clients'][client_iqn]['luns']:
self._terminate_connection(volume, client_iqn,
self.target_iqn,
iscsi_config)
self._delete_disk(volume)
@utils.trace
def terminate_connection(self, volume, connector, **kwargs):
"""Unexport the volume from the gateway."""
iscsi_config = self._get_config()
if not connector:
# No connector was passed in, so this is a force detach
# we need to detach the volume from the configured target_iqn.
self._terminate_all(volume, iscsi_config)
initiator_iqn = connector['initiator']
self._terminate_connection(volume, initiator_iqn, self.target_iqn,
iscsi_config)
self._delete_disk(volume)
+2 -1
View File
@@ -224,6 +224,7 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
RBD_FEATURE_OBJECT_MAP = 8
RBD_FEATURE_FAST_DIFF = 16
RBD_FEATURE_JOURNALING = 64
STORAGE_PROTOCOL = 'ceph'
def __init__(self, active_backend_id=None, *args, **kwargs):
super(RBDDriver, self).__init__(*args, **kwargs)
@@ -608,7 +609,7 @@ class RBDDriver(driver.CloneableImageVD, driver.MigrateVD,
stats = {
'vendor_name': 'Open Source',
'driver_version': self.VERSION,
'storage_protocol': 'ceph',
'storage_protocol': self.STORAGE_PROTOCOL,
'total_capacity_gb': 'unknown',
'free_capacity_gb': 'unknown',
'reserved_percentage': (
+14
View File
@@ -162,6 +162,9 @@ title=Quobyte Storage Driver (quobyte)
[driver.rbd]
title=RBD (Ceph) Storage Driver (RBD)
[driver.rbd_iscsi]
title=(Ceph) iSCSI Storage Driver (iSCSI)
[driver.sandstone]
title=SandStone Storage Driver (iSCSI)
@@ -257,6 +260,7 @@ driver.pure=complete
driver.qnap=complete
driver.quobyte=complete
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=complete
driver.storpool=complete
@@ -324,6 +328,7 @@ driver.pure=complete
driver.qnap=complete
driver.quobyte=missing
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=complete
driver.storpool=complete
@@ -391,6 +396,7 @@ driver.pure=missing
driver.qnap=missing
driver.quobyte=missing
driver.rbd=missing
driver.rbd_iscsi=missing
driver.sandstone=missing
driver.seagate=missing
driver.storpool=missing
@@ -461,6 +467,7 @@ driver.pure=missing
driver.qnap=missing
driver.quobyte=missing
driver.rbd=missing
driver.rbd_iscsi=missing
driver.sandstone=complete
driver.seagate=missing
driver.storpool=missing
@@ -530,6 +537,7 @@ driver.pure=complete
driver.qnap=missing
driver.quobyte=missing
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=missing
driver.storpool=complete
@@ -600,6 +608,7 @@ driver.pure=complete
driver.qnap=missing
driver.quobyte=missing
driver.rbd=missing
driver.rbd_iscsi=missing
driver.sandstone=missing
driver.seagate=missing
driver.storpool=missing
@@ -669,6 +678,7 @@ driver.pure=complete
driver.qnap=missing
driver.quobyte=missing
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=missing
driver.storpool=complete
@@ -739,6 +749,7 @@ driver.pure=missing
driver.qnap=missing
driver.quobyte=missing
driver.rbd=missing
driver.rbd_iscsi=missing
driver.sandstone=missing
driver.seagate=missing
driver.storpool=complete
@@ -809,6 +820,7 @@ driver.pure=complete
driver.qnap=missing
driver.quobyte=missing
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=complete
driver.storpool=complete
@@ -876,6 +888,7 @@ driver.pure=complete
driver.qnap=missing
driver.quobyte=missing
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=missing
driver.storpool=missing
@@ -947,6 +960,7 @@ driver.pure=complete
driver.qnap=missing
driver.quobyte=missing
driver.rbd=complete
driver.rbd_iscsi=complete
driver.sandstone=complete
driver.seagate=missing
driver.storpool=missing
+3
View File
@@ -27,6 +27,9 @@ pyxcli>=1.1.5 # Apache-2.0
rados # LGPLv2.1
rbd # LGPLv2.1
# RBD-iSCSI
rbd-iscsi-client # Apache-2.0
# Dell EMC VNX and Unity
storops>=1.2.3 # Apache-2.0
+1
View File
@@ -106,6 +106,7 @@ python-swiftclient==3.10.1
pytz==2020.1
pyudev==0.22.0
PyYAML==5.3.1
rbd-iscsi-client==0.1.8
reno==3.2.0
repoze.lru==0.7
requests==2.23.0
@@ -0,0 +1,6 @@
---
features:
- |
Added new Ceph iSCSI driver rbd_iscsi. This new driver is derived from
the rbd driver and allows all the same features as the rbd driver.
The only difference is that volume attachments are done via iSCSI.
+3
View File
@@ -92,6 +92,7 @@ all =
storpool>=4.0.0 # Apache-2.0
storpool.spopenstack>=2.2.1 # Apache-2.0
dfs-sdk>=1.2.25 # Apache-2.0
rbd-iscsi-client>=0.1.8 # Apache-2.0
datacore =
websocket-client>=0.32.0 # LGPLv2+
powermax =
@@ -119,6 +120,8 @@ storpool =
storpool.spopenstack>=2.2.1 # Apache-2.0
datera =
dfs-sdk>=1.2.25 # Apache-2.0
rbd_iscsi =
rbd-iscsi-client>=0.1.8 # Apache-2.0
[mypy]