496 lines
19 KiB
Python
496 lines
19 KiB
Python
# 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.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']
|
|
|
|
@volume_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())
|
|
|
|
@volume_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())
|
|
|
|
@volume_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())
|
|
|
|
@volume_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())
|
|
|
|
@volume_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
|
|
|
|
@volume_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)
|
|
|
|
@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)
|