Merge "Add ceph iscsi volume driver"
This commit is contained in:
commit
876ac4e79f
|
@ -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)
|
|
@ -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)
|
|
@ -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': (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue