cinder/cinder/volume/drivers/dell_emc/unity/client.py

562 lines
21 KiB
Python

# Copyright (c) 2016 Dell Inc. or its subsidiaries.
# 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 oslo_log import log
from oslo_utils import excutils
from oslo_utils import importutils
storops = importutils.try_import('storops')
if storops:
from storops import exception as storops_ex
else:
# Set storops_ex to be None for unit test
storops_ex = None
from cinder import coordination
from cinder import exception
from cinder.i18n import _
from cinder.volume.drivers.dell_emc.unity import utils
LOG = log.getLogger(__name__)
class UnityClient(object):
def __init__(self, host, username, password, verify_cert=True):
if storops is None:
msg = _('Python package storops is not installed which '
'is required to run Unity driver.')
raise exception.VolumeBackendAPIException(data=msg)
self._system = None
self.host = host
self.username = username
self.password = password
self.verify_cert = verify_cert
self.host_cache = {}
@property
def system(self):
if self._system is None:
self._system = storops.UnitySystem(
host=self.host, username=self.username, password=self.password,
verify=self.verify_cert)
return self._system
def get_serial(self):
return self.system.serial_number
def create_lun(self, name, size, pool, description=None,
io_limit_policy=None, is_thin=None,
is_compressed=None, cg_name=None, tiering_policy=None):
"""Creates LUN on the Unity system.
:param name: lun name
:param size: lun size in GiB
:param pool: UnityPool object represent to pool to place the lun
:param description: lun description
:param io_limit_policy: io limit on the LUN
:param is_thin: if False, a thick LUN will be created
:param is_compressed: is compressed LUN enabled
:param tiering_policy: tiering policy for the LUN
:param cg_name: the name of cg to join if any
:return: UnityLun object
"""
try:
lun = pool.create_lun(lun_name=name, size_gb=size,
description=description,
io_limit_policy=io_limit_policy,
is_thin=is_thin,
is_compression=is_compressed,
tiering_policy=tiering_policy)
except storops_ex.UnityLunNameInUseError:
LOG.debug("LUN %s already exists. Return the existing one.",
name)
lun = self.system.get_lun(name=name)
return lun
def thin_clone(self, lun_or_snap, name, io_limit_policy=None,
description=None, new_size_gb=None):
try:
lun = lun_or_snap.thin_clone(
name=name, io_limit_policy=io_limit_policy,
description=description)
except storops_ex.UnityLunNameInUseError:
LOG.debug("LUN(thin clone) %s already exists. "
"Return the existing one.", name)
lun = self.system.get_lun(name=name)
if new_size_gb is not None and new_size_gb > lun.total_size_gb:
lun = self.extend_lun(lun.get_id(), new_size_gb)
return lun
def delete_lun(self, lun_id):
"""Deletes LUN on the Unity system.
:param lun_id: id of the LUN
"""
try:
lun = self.system.get_lun(_id=lun_id)
except storops_ex.UnityResourceNotFoundError:
LOG.debug("Cannot get LUN %s from unity. Do nothing.", lun_id)
return
def _delete_lun_if_exist(force_snap_delete=False):
"""Deletes LUN, skip if it doesn't exist."""
try:
lun.delete(force_snap_delete=force_snap_delete)
except storops_ex.UnityResourceNotFoundError:
LOG.debug("LUN %s doesn't exist. Deletion is not needed.",
lun_id)
try:
_delete_lun_if_exist()
except storops_ex.UnityDeleteLunInReplicationError:
LOG.info("LUN %s is participating in replication sessions. "
"Delete replication sessions first",
lun_id)
self.delete_lun_replications(lun_id)
# It could fail if not pass in force_snap_delete when
# deleting the lun immediately after
# deleting the replication sessions.
_delete_lun_if_exist(force_snap_delete=True)
def delete_lun_replications(self, lun_id):
LOG.debug("Deleting all the replication sessions which are from "
"lun %s", lun_id)
try:
rep_sessions = self.system.get_replication_session(
src_resource_id=lun_id)
except storops_ex.UnityResourceNotFoundError:
LOG.debug("No replication session found from lun %s. Do nothing.",
lun_id)
else:
for session in rep_sessions:
try:
session.delete()
except storops_ex.UnityResourceNotFoundError:
LOG.debug("Replication session %s doesn't exist. "
"Skip the deletion.", session.get_id())
def get_lun(self, lun_id=None, name=None):
"""Gets LUN on the Unity system.
:param lun_id: id of the LUN
:param name: name of the LUN
:return: `UnityLun` object
"""
lun = None
if lun_id is None and name is None:
LOG.warning(
"Both lun_id and name are None to get LUN. Return None.")
else:
try:
lun = self.system.get_lun(_id=lun_id, name=name)
except storops_ex.UnityResourceNotFoundError:
LOG.warning(
"LUN id=%(id)s, name=%(name)s doesn't exist.",
{'id': lun_id, 'name': name})
return lun
def extend_lun(self, lun_id, size_gib):
lun = self.system.get_lun(lun_id)
try:
lun.total_size_gb = size_gib
except storops_ex.UnityNothingToModifyError:
LOG.debug("LUN %s is already expanded. LUN expand is not needed.",
lun_id)
return lun
def migrate_lun(self, lun_id, dest_pool_id, dest_provision=None):
# dest_provision possible value ('thin', 'thick', 'compressed')
lun = self.system.get_lun(lun_id)
dest_pool = self.system.get_pool(dest_pool_id)
is_thin = True if dest_provision == 'thin' else None
if dest_provision == 'compressed':
# compressed needs work with thin
is_compressed = True
is_thin = True
else:
is_compressed = False
if dest_provision == 'thick':
# thick needs work with uncompressed
is_thin = False
is_compressed = False
return lun.migrate(dest_pool, is_compressed=is_compressed,
is_thin=is_thin)
def get_pools(self):
"""Gets all storage pools on the Unity system.
:return: list of UnityPool object
"""
return self.system.get_pool()
def create_snap(self, src_lun_id, name=None):
"""Creates a snapshot of LUN on the Unity system.
:param src_lun_id: the source LUN ID of the snapshot.
:param name: the name of the snapshot. The Unity system will give one
if `name` is None.
"""
try:
lun = self.get_lun(lun_id=src_lun_id)
snap = lun.create_snap(name, is_auto_delete=False)
except storops_ex.UnitySnapNameInUseError as err:
LOG.debug(
"Snap %(snap_name)s already exists on LUN %(lun_id)s. "
"Return the existing one. Message: %(err)s",
{'snap_name': name,
'lun_id': src_lun_id,
'err': err})
snap = self.get_snap(name=name)
return snap
@staticmethod
def delete_snap(snap):
if snap is None:
LOG.debug("Snap to delete is None, skipping deletion.")
return
try:
snap.delete()
except storops_ex.UnityResourceNotFoundError as err:
LOG.debug("Snap %(snap_name)s may be deleted already. "
"Message: %(err)s",
{'snap_name': snap.name,
'err': err})
except storops_ex.UnityDeleteAttachedSnapError as err:
with excutils.save_and_reraise_exception():
LOG.warning("Failed to delete snapshot %(snap_name)s "
"which is in use. Message: %(err)s",
{'snap_name': snap.name, 'err': err})
def get_snap(self, name=None):
try:
return self.system.get_snap(name=name)
except storops_ex.UnityResourceNotFoundError as err:
LOG.warning("Snapshot %(name)s doesn't exist. Message: %(err)s",
{'name': name, 'err': err})
return None
def lun_has_snapshot(self, lun):
snaps = lun.snapshots
return len(snaps) != 0
@coordination.synchronized('{self.host}-{name}')
def create_host(self, name):
return self.create_host_wo_lock(name)
def create_host_wo_lock(self, name):
"""Provides existing host if exists else create one."""
if name not in self.host_cache:
try:
host = self.system.get_host(name=name)
except storops_ex.UnityResourceNotFoundError:
LOG.debug('Host %s not found. Create a new one.',
name)
host = self.system.create_host(name=name)
self.host_cache[name] = host
else:
host = self.host_cache[name]
return host
def delete_host_wo_lock(self, host):
host.delete()
del self.host_cache[host.name]
def update_host_initiators(self, host, uids):
"""Updates host with the supplied uids."""
host_initiators_ids = self.get_host_initiator_ids(host)
un_registered = [h for h in uids if h not in host_initiators_ids]
if un_registered:
for uid in un_registered:
try:
host.add_initiator(uid, force_create=True)
except storops_ex.UnityHostInitiatorExistedError:
# This make concurrent modification of
# host initiators safe
LOG.debug(
'The uid(%s) was already in '
'%s.', uid, host.name)
host.update()
# Update host cached with new initiators.
self.host_cache[host.name] = host
return host
@staticmethod
def get_host_initiator_ids(host):
fc = host.fc_host_initiators
fc_ids = [] if fc is None else fc.initiator_id
iscsi = host.iscsi_host_initiators
iscsi_ids = [] if iscsi is None else iscsi.initiator_id
return fc_ids + iscsi_ids
@staticmethod
def attach(host, lun_or_snap):
"""Attaches a `UnityLun` or `UnitySnap` to a `UnityHost`.
:param host: `UnityHost` object
:param lun_or_snap: `UnityLun` or `UnitySnap` object
:return: hlu
"""
try:
return host.attach(lun_or_snap, skip_hlu_0=True)
except storops_ex.UnityResourceAlreadyAttachedError:
return host.get_hlu(lun_or_snap)
@staticmethod
def detach(host, lun_or_snap):
"""Detaches a `UnityLun` or `UnitySnap` from a `UnityHost`.
:param host: `UnityHost` object
:param lun_or_snap: `UnityLun` object
"""
lun_or_snap.update()
host.detach(lun_or_snap)
@staticmethod
def detach_all(lun):
"""Detaches a `UnityLun` from all hosts.
:param lun: `UnityLun` object
"""
lun.update()
lun.detach_from(host=None)
def get_ethernet_ports(self):
return self.system.get_ethernet_port()
def get_iscsi_target_info(self, allowed_ports=None):
portals = self.system.get_iscsi_portal()
portals = portals.shadow_copy(port_ids=allowed_ports)
return [{'portal': utils.convert_ip_to_portal(p.ip_address),
'iqn': p.iscsi_node.name}
for p in portals]
def get_fc_ports(self):
return self.system.get_fc_port()
def get_fc_target_info(self, host=None, logged_in_only=False,
allowed_ports=None):
"""Get the ports WWN of FC on array.
:param host: the host to which the FC port is registered.
:param logged_in_only: whether to retrieve only the logged-in port.
:return: the WWN of FC ports. For example, the FC WWN on array is like:
50:06:01:60:89:20:09:25:50:06:01:6C:09:20:09:25.
This function removes the colons and returns the last 16 bits:
5006016C09200925.
"""
wwns = set()
if logged_in_only:
for paths in filter(None, host.fc_host_initiators.paths):
paths = paths.shadow_copy(is_logged_in=True)
# `paths.fc_port` is just a list, not a UnityFcPortList,
# so use filter instead of shadow_copy here.
wwns.update(p.wwn.upper()
for p in filter(
lambda fcp: (allowed_ports is None or
fcp.get_id() in allowed_ports),
paths.fc_port))
else:
ports = self.get_fc_ports()
ports = ports.shadow_copy(port_ids=allowed_ports)
wwns.update(p.wwn.upper() for p in ports)
return [wwn.replace(':', '')[16:] for wwn in wwns]
def create_io_limit_policy(self, name, max_iops=None, max_kbps=None):
try:
limit = self.system.create_io_limit_policy(
name, max_iops=max_iops, max_kbps=max_kbps)
except storops_ex.UnityPolicyNameInUseError:
limit = self.system.get_io_limit_policy(name=name)
return limit
def get_io_limit_policy(self, qos_specs):
limit_policy = None
if qos_specs is not None:
limit_policy = self.create_io_limit_policy(
qos_specs['id'],
qos_specs.get(utils.QOS_MAX_IOPS),
qos_specs.get(utils.QOS_MAX_BWS))
return limit_policy
def get_pool_id_by_name(self, name):
pool = self.system.get_pool(name=name)
return pool.get_id()
def get_pool_name(self, lun_name):
lun = self.system.get_lun(name=lun_name)
return lun.pool_name
def restore_snapshot(self, snap_name):
snap = self.get_snap(snap_name)
return snap.restore(delete_backup=True)
def create_cg(self, name, description=None, lun_add=None):
try:
cg = self.system.create_cg(name, description=description,
lun_add=lun_add)
except storops_ex.UnityConsistencyGroupNameInUseError:
LOG.debug('CG %s already exists. Return the existing one.', name)
cg = self.system.get_cg(name=name)
return cg
def get_cg(self, name):
try:
cg = self.system.get_cg(name=name)
except storops_ex.UnityResourceNotFoundError:
LOG.info('CG %s not found.', name)
return None
else:
return cg
def delete_cg(self, name):
cg = self.get_cg(name)
if cg:
cg.delete() # Deleting cg will also delete the luns in it
def update_cg(self, name, add_lun_ids, remove_lun_ids):
cg = self.get_cg(name)
cg.update_lun(add_luns=[self.get_lun(lun_id=lun_id)
for lun_id in add_lun_ids],
remove_luns=[self.get_lun(lun_id=lun_id)
for lun_id in remove_lun_ids])
def create_cg_snap(self, cg_name, snap_name=None):
cg = self.get_cg(cg_name)
# Creating snap of cg will create corresponding snaps of luns in it
return cg.create_snap(name=snap_name, is_auto_delete=False)
def filter_snaps_in_cg_snap(self, cg_snap_id):
return self.system.get_snap(snap_group=cg_snap_id).list
def create_cg_replication(self, cg_name, pool_id,
remote_system, max_time_out_of_sync):
# Creates a new cg on remote system and sets up replication to it.
src_cg = self.get_cg(cg_name)
src_luns = src_cg.luns
return src_cg.replicate_cg_with_dst_resource_provisioning(
max_time_out_of_sync, src_luns, pool_id,
dst_cg_name=cg_name, remote_system=remote_system)
def is_cg_replicated(self, cg_name):
src_cg = self.get_cg(cg_name)
return src_cg.check_cg_is_replicated()
def delete_cg_rep_session(self, cg_name):
src_cg = self.get_cg(cg_name)
rep_sessions = self.get_replication_session(src_resource_id=src_cg.id)
for rep_session in rep_sessions:
rep_session.delete()
def failover_cg_rep_session(self, cg_name, sync):
src_cg = self.get_cg(cg_name)
rep_sessions = self.get_replication_session(src_resource_id=src_cg.id)
for rep_session in rep_sessions:
rep_session.failover(sync=sync)
def failback_cg_rep_session(self, cg_name):
cg = self.get_cg(cg_name)
# failback starts from remote replication session
rep_sessions = self.get_replication_session(dst_resource_id=cg.id)
for rep_session in rep_sessions:
rep_session.failback(force_full_copy=True)
@staticmethod
def create_replication(src_lun, max_time_out_of_sync,
dst_pool_id, remote_system):
"""Creates a new lun on remote system and sets up replication to it."""
return src_lun.replicate_with_dst_resource_provisioning(
max_time_out_of_sync, dst_pool_id, remote_system=remote_system,
dst_lun_name=src_lun.name)
def get_remote_system(self, name=None):
"""Gets remote system on the Unity system.
:param name: remote system name.
:return: remote system.
"""
try:
return self.system.get_remote_system(name=name)
except storops_ex.UnityResourceNotFoundError:
LOG.warning("Not found remote system with name %s. Return None.",
name)
return None
def get_replication_session(self, name=None,
src_resource_id=None, dst_resource_id=None):
"""Gets replication session via its name.
:param name: replication session name.
:param src_resource_id: replication session's src_resource_id.
:param dst_resource_id: replication session's dst_resource_id.
:return: replication session.
"""
try:
return self.system.get_replication_session(
name=name, src_resource_id=src_resource_id,
dst_resource_id=dst_resource_id)
except storops_ex.UnityResourceNotFoundError:
raise ClientReplicationError(
'Replication session with name %(name)s not found.' %
{'name': name})
def failover_replication(self, rep_session):
"""Fails over a replication session.
:param rep_session: replication session to fail over.
"""
name = rep_session.name
LOG.debug('Failing over replication: %s', name)
try:
# In OpenStack, only support to failover triggered from secondary
# backend because the primary could be down. Then `sync=False`
# is required here which means it won't sync from primary to
# secondary before failover.
return rep_session.failover(sync=False)
except storops_ex.UnityException as ex:
raise ClientReplicationError(
'Failover of replication: %(name)s failed, '
'error: %(err)s' % {'name': name, 'err': ex}
)
LOG.debug('Replication: %s failed over', name)
def failback_replication(self, rep_session):
"""Fails back a replication session.
:param rep_session: replication session to fail back.
"""
name = rep_session.name
LOG.debug('Failing back replication: %s', name)
try:
# If the replication was failed-over before initial copy done,
# following failback will fail without `force_full_copy` because
# the primary # and secondary data have no common base.
# `force_full_copy=True` has no effect if initial copy done.
return rep_session.failback(force_full_copy=True)
except storops_ex.UnityException as ex:
raise ClientReplicationError(
'Failback of replication: %(name)s failed, '
'error: %(err)s' % {'name': name, 'err': ex}
)
LOG.debug('Replication: %s failed back', name)
class ClientReplicationError(exception.CinderException):
pass