cinder/cinder/volume/drivers/sandstone/sds_driver.py

508 lines
20 KiB
Python

# Copyright (c) 2019 ShenZhen SandStone Data Technologies Co., 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.
"""Volume Drivers for SandStone distributed storage."""
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import units
from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.volume.drivers.sandstone.sds_client import RestCmd
LOG = logging.getLogger(__name__)
sds_opts = [
cfg.ListOpt("default_sandstone_target_ips",
default=[],
help="SandStone default target ip."),
cfg.StrOpt("sandstone_pool",
default="",
help="SandStone storage pool resource name."),
cfg.DictOpt("initiator_assign_sandstone_target_ip",
default={},
help="Support initiator assign target with assign ip.")
]
CONF = cfg.CONF
CONF.register_opts(sds_opts)
class SdsBaseDriver(driver.VolumeDriver):
"""ISCSIDriver base class."""
# ThirdPartySytems wiki page
VERSION = '1.0'
CI_WIKI_NAME = "SandStone_Storage_CI"
def __init__(self, *args, **kwargs):
"""Init configuration."""
super(SdsBaseDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(sds_opts)
self.configuration.append_config_values(san.san_opts)
def do_setup(self, context):
"""Instantiate common class and login storage system."""
if not self.configuration:
msg = _('Configuration is not found.')
raise exception.InvalidConfigurationValue(msg)
self.address = self.configuration.san_ip
self.user = self.configuration.san_login
self.password = self.configuration.san_password
self.pool = self.configuration.sandstone_pool
self.iscsi_info = (self.configuration.
initiator_assign_sandstone_target_ip)
self.default_target_ips = (self.configuration.
default_sandstone_target_ips)
self.chap_username = self.configuration.chap_username
self.chap_password = self.configuration.chap_password
self.suppress_requests_ssl_warnings = (self.configuration.
suppress_requests_ssl_warnings)
self.client = RestCmd(self.address, self.user, self.password,
self.suppress_requests_ssl_warnings)
LOG.debug("Run sandstone driver setup.")
def check_for_setup_error(self):
"""Check pool status and exist or not."""
self.client.login()
self.poolname_map_poolid = self.client.get_poolid_from_poolname()
all_pools = self.client.query_pool_info()
all_pools_name = [p['pool_name'] for p in all_pools
if p.get('pool_name')]
if self.pool not in all_pools_name:
msg = _('Storage pool %(pool)s does not exist '
'in the cluster.') % {'pool': self.pool}
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
pool_status = [p['status'] for p in all_pools
if p.get('pool_name') == self.pool]
if pool_status:
if ("health" not in pool_status[0].get('state') and
pool_status[0].get("progress", 0) != 100):
LOG.warning('Storage pool: %(poolName)s not healthy.',
{"poolName": self.pool})
if not self.poolname_map_poolid:
err_msg = _('poolname_map_poolid info is empty.')
self._raise_exception(err_msg)
self.poolid = self.poolname_map_poolid.get(self.pool)
if not self.poolid:
err_msg = _('poolid is None.')
self._raise_exception(err_msg)
def _update_volume_stats(self, pool_name):
"""Get cluster capability and capacity."""
data, pool = {}, {}
data['pools'] = []
cluster_capacity = self.client.query_capacity_info()
total_capacity_gb = (float(cluster_capacity.get("capacity_bytes", 0))
/ units.Gi)
free_capacity_gb = (float(cluster_capacity.get("free_bytes", 0))
/ units.Gi)
self._stats = pool.update(dict(
pool_name = pool_name,
vendor_name = 'SandStone USP',
driver_version = self.VERSION,
total_capacity_gb = total_capacity_gb,
free_capacity_gb = free_capacity_gb,
QoS_support=True,
thin_provisioning_support=True,
multiattach=False,
))
data['pools'].append(pool)
return data
def _raise_exception(self, msg):
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def create_volume(self, volume):
"""Create a volume."""
capacity_bytes = int(volume.size) * units.Gi
self.client.create_lun(capacity_bytes, self.poolid, volume.name)
def delete_volume(self, volume):
"""Delete a volume."""
LOG.debug("Delete volume %(volumeName)s from pool %(poolId)s",
{"volumeName": volume.name,
"poolId": self.poolid})
self.client.delete_lun(self.poolid, volume.name)
def migrate_volume(self, ctxt, volume, host, new_type=None):
"""Migrate a volume within the same array."""
return (False, None)
def create_volume_from_snapshot(self, volume, snapshot):
"""Create a volume from a snapshot.
We use LUNcopy to copy a new volume from snapshot.
The time needed increases as volume size does.
"""
if snapshot.volume:
source_vol_name = snapshot.volume.name
source_vol_size = snapshot.volume.size * units.Gi
destination_vol_name = volume.name
destination_vol_size = volume.size * units.Gi
snapshot_name = snapshot.name
self.client.create_lun_from_snapshot(snapshot_name,
source_vol_name,
self.poolid,
destination_vol_name)
if destination_vol_size > source_vol_size:
self.client.extend_lun(destination_vol_size,
self.poolid, volume.name)
else:
err_msg = _('No such snapshot volume.')
self._raise_exception(err_msg)
def create_cloned_volume(self, dst_volume, src_volume):
"""Clone a new volume from an existing volume."""
if not self._check_volume_exist(src_volume.name):
msg = (_('Source volume: %(volume_name)s does not exist.')
% {'volume_name': src_volume.name})
self._raise_exception(msg)
self.client.create_lun_from_lun(dst_volume.name, self.poolid,
src_volume.name)
dst_vol_size = dst_volume.size * units.Gi
src_vol_size = src_volume.size * units.Gi
if dst_vol_size > src_vol_size:
self.client.extend_lun(dst_vol_size, self.poolid, dst_volume.name)
def _check_volume_exist(self, volume):
return self.client.query_lun_by_name(volume, self.poolid)
def extend_volume(self, volume, new_size):
"""Extend a volume."""
old_volume = self._check_volume_exist(volume.name)
if not old_volume:
msg = (_('Not exist volume: %(volumeName)s')
% {"volumeName": volume.name})
self._raise_exception(msg)
old_size = old_volume.get("capacity_bytes")
new_size = new_size * units.Gi
if new_size == old_size:
LOG.info("New size is equal to the real size from backend "
"storage, no need to extend. "
"realsize: %(oldsize)s, newsize: %(newsize)s.",
{"oldsize": old_size,
"newsize": new_size})
return
if new_size < old_size:
msg = (_("New size should be bigger than the real size from "
"backend storage. "
"realsize: %(oldsize)s, newsize: %(newsize)s.")
% {"oldsize": old_size,
"newsize": new_size})
self._raise_exception(msg)
LOG.info(
'Extend volume: %(volumename)s, '
'oldsize: %(oldsize)s, newsize: %(newsize)s.',
{"volumename": volume.name,
"oldsize": old_size,
"newsize": new_size})
self.client.extend_lun(new_size, self.poolid, volume.name)
def create_snapshot(self, snapshot):
"""Create snapshot from volume."""
volume = snapshot.volume
if not volume:
msg = (_("Can't get volume id from snapshot, snapshot: %(id)s.")
% {"id": snapshot.id})
self._raise_exception(msg)
LOG.debug(
"create snapshot from volumeName: %(volume)s, "
"snap name: %(snapshot)s.",
{"snapshot": snapshot.name,
"volume": volume.name},)
self.client.create_snapshot(self.poolid,
volume.name,
snapshot.name)
def _check_snapshot_exist(self, snapshot):
return self.client.query_snapshot_by_name(snapshot.volume.name,
self.poolid,
snapshot.name)
def delete_snapshot(self, snapshot):
"""Delete volume's snapshot."""
snapshot_name = snapshot.name
volume_name = snapshot.volume.name
if not self._check_snapshot_exist(snapshot):
LOG.debug("not exist snapshot: %(snapshotName)s",
{"snapshotName": snapshot.name})
LOG.info(
'stop_snapshot: snapshot name: %(snapshot)s, '
'volume name: %(volume)s.',
{"snapshot": snapshot_name,
"volume": volume_name},)
self.client.delete_snapshot(self.poolid,
volume_name,
snapshot_name)
def retype(self, ctxt, volume, new_type, diff, host):
"""Convert the volume to be of the new type."""
LOG.debug("Enter retype: id=%(id)s, new_type=%(new_type)s, "
"diff=%(diff)s, host=%(host)s.", {'id': volume.id,
'new_type': new_type,
'diff': diff,
'host': host})
def create_export(self, context, volume, connector):
"""Export a volume."""
pass
def ensure_export(self, context, volume):
"""Synchronously recreate an export for a volume."""
pass
def remove_export(self, context, volume):
"""Remove an export for a volume."""
pass
def create_export_snapshot(self, context, snapshot, connector):
"""Export a snapshot."""
pass
def remove_export_snapshot(self, context, snapshot):
"""Remove an export for a snapshot."""
pass
def backup_use_temp_snapshot(self):
"""The config option has a default to be False, So just return it."""
pass
def unmanage(self, volume):
"""Export SandStone volume from Cinder."""
LOG.debug("Unmanage volume: %s.", volume.id)
def unmanage_snapshot(self, snapshot):
"""Unmanage the specified snapshot from Cinder management."""
LOG.debug("Unmanage snapshot: %s.", snapshot.id)
@interface.volumedriver
class SdsISCSIDriver(SdsBaseDriver, driver.ISCSIDriver):
"""ISCSI driver for SandStone storage arrays.
Version history:
.. code-block:: none
1.0.0 - Initial driver
Provide SandStone storage
create volume support
delete volume support
create snapshot support
delete snapshot support
extend volume support
create volume from snap support
create cloned volume support
nova volume-attach support
nova volume-detach support
"""
VERSION = "1.0.0"
def get_volume_stats(self, refresh):
"""Get volume status and capality."""
data = SdsBaseDriver.get_volume_stats(self, refresh)
backend_name = self.configuration.safe_get('volume_backend_name')
data['volume_backend_name'] = backend_name or self.__class__.__name__
data['storage_protocol'] = 'iSCSI'
data['driver_version'] = self.VERSION
data['vendor_name'] = 'SandStone USP'
return data
def _check_target_exist(self, target_name):
"""Check target name exist or not."""
return self.client.query_target_by_name(target_name)
def _check_initiator_exist(self, initiator_name):
"""Check initiator name exist or not."""
return self.client.query_initiator_by_name(initiator_name)
def _check_target_added_initiator(self, target_name, initiator_name):
return self.client.query_target_initiatoracl(target_name,
initiator_name)
def _check_target_added_lun(self, target_name, poolid, volume_name):
return self.client.query_target_lunacl(target_name, poolid,
volume_name)
def _check_target_added_chap(self, target_name, username):
return self.client.query_chapinfo_by_target(target_name, username)
def _get_target_ip(self, initiator):
ini = self.iscsi_info.get(initiator)
if ini:
target_ips = [ip.strip() for ip in ini.split(',')
if ip.strip()]
else:
target_ips = []
# If not specify target IP for some initiators, use default IP.
if not target_ips:
if self.default_target_ips:
target_ips = self.default_target_ips
else:
msg = (_(
'get_iscsi_params: Failed to get target IP '
'for initiator %(ini)s, please check config file.')
% {'ini': initiator})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return target_ips
def initialize_connection(self, volume, connector):
"""Map a volume to a host and return target iSCSI information."""
initiator_name = connector['initiator']
LOG.info(
"initiator name: %(initiator_name)s, "
"LUN ID: %(lun_id)s.",
{"initiator_name": initiator_name,
"lun_id": volume.name})
# Create target
iqn_end = initiator_name.split(':')[-1]
target_head = 'iqn.2014-10.com.szsandstone:storage:'
target_name = target_head + iqn_end
target_ips = self._get_target_ip(initiator_name)
if not self._check_target_exist(iqn_end):
targetip_to_hostid = (self.client.
query_node_by_targetips(target_ips))
self.client.create_target(iqn_end, targetip_to_hostid)
else:
# Target is exist, get target_name and nodes
LOG.info("target is exist, don't repeat to create, "
"iscsi_iqn: %(iscsi_iqn)s.",
{"iscsi_iqn": target_name})
LOG.info("initialize_connection, iscsi_iqn: %(iscsi_iqn)s, "
'target_ips: %(target_ips)s.',
{'iscsi_iqn': target_name,
'target_ips': target_ips})
# Check initiator isn't exist
if not self._check_initiator_exist(initiator_name):
# Create initiator and add in storage
self.client.create_initiator(initiator_name)
else:
LOG.info("initiator is exist, don't repeat to create "
"initiator: %(initiator_name)s.",
{"initiator_name": initiator_name})
# Check target added initiator or not
if not self._check_target_added_initiator(iqn_end,
initiator_name):
# Add initiator to target
self.client.add_initiator_to_target(iqn_end,
initiator_name)
else:
LOG.info("initiator is added to target, no action needed, "
"target: %(target_name)s, "
"initiator: %(initiator_name)s.",
{"initiator_name": initiator_name,
"target_name": target_name})
lun_id = self._check_target_added_lun(iqn_end,
self.poolid, volume.name)
if not lun_id:
# Mapping lun to target
self.client.mapping_lun(iqn_end, self.poolid,
volume.name, self.pool)
lun_id = self._check_target_added_lun(iqn_end,
self.poolid, volume.name)
else:
LOG.info("lun is added to target, don't repeat to add "
"volume: %(volume_name)s, target: %(target_name)s.",
{"volume_name": volume.name,
"target_name": target_name})
# Mapping lungroup and hostgroup to view.
LOG.info("initialize_connection, host lun id is: %(lun_id)d.",
{"lun_id": lun_id})
# Return iSCSI properties.
properties = {}
properties['target_discovered'] = True
properties['volume_id'] = volume.id
multipath = connector.get('multipath', False)
hostlun_id = lun_id
if not multipath:
properties['target_portal'] = ("%s:3260" % target_ips[0])
properties['target_iqn'] = target_name
properties['target_lun'] = hostlun_id
else:
properties['target_iqns'] = [target_name for i in
range(len(target_ips))]
properties['target_portals'] = [
"%s:3260" % ip for ip in target_ips]
properties['target_luns'] = [hostlun_id] * len(target_ips)
# If use CHAP, return CHAP info.
if self.chap_username and self.chap_password:
if not self._check_target_added_chap(iqn_end, self.chap_username):
self.client.add_chap_by_target(iqn_end, self.chap_username,
self.chap_password)
else:
LOG.info("chap username: %(chap_username)s exist, don't "
"repeat to create, iscsi_iqn: %(iscsi_iqn)s.",
{"iscsi_iqn": target_name,
"chap_username": self.chap_username})
properties['auth_method'] = 'CHAP'
properties['auth_username'] = self.chap_username
properties['auth_password'] = self.chap_password
LOG.info("initialize_connection success. Return data: %(properties)s.",
{"properties": properties})
return {'driver_volume_type': 'iscsi', 'data': properties}
def terminate_connection(self, volume, connector, **kwargs):
"""Delete map between a volume and a host."""
if not connector:
target_name = self.client.query_target_by_lun(volume.name,
self.poolid)
self.client.unmap_lun(target_name, self.poolid,
volume.name, self.pool)
return
initiator_name = connector['initiator']
# Remove lun from target force.
iqn_end = initiator_name.split(':')[-1]
target_head = 'iqn.2014-10.com.szsandstone:storage:'
target_name = target_head + iqn_end
self.client.unmap_lun(iqn_end, self.poolid, volume.name, self.pool)
LOG.info(
"terminate_connection: initiator name: %(ini)s, "
"LUN ID: %(lunid)s, "
"Target Name: %(target_name)s.",
{"ini": initiator_name,
"lunid": volume.name,
"target_name": target_name})