1006 lines
42 KiB
Python
1006 lines
42 KiB
Python
# Nimble Storage, Inc. (c) 2013-2014
|
|
# 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 driver for Nimble Storage.
|
|
|
|
This driver supports Nimble Storage controller CS-Series.
|
|
|
|
"""
|
|
import functools
|
|
import math
|
|
import random
|
|
import re
|
|
import six
|
|
import ssl
|
|
import string
|
|
import sys
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import units
|
|
from six.moves import urllib
|
|
from suds import client
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LE, _LI, _LW
|
|
from cinder import interface
|
|
from cinder.objects import volume
|
|
from cinder.volume.drivers.san import san
|
|
from cinder.volume import volume_types
|
|
|
|
|
|
DRIVER_VERSION = '3.0.0'
|
|
AES_256_XTS_CIPHER = 2
|
|
DEFAULT_CIPHER = 3
|
|
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
|
|
EXTRA_SPEC_PERF_POLICY = 'nimble:perfpol-name'
|
|
EXTRA_SPEC_MULTI_INITIATOR = 'nimble:multi-initiator'
|
|
DEFAULT_PERF_POLICY_SETTING = 'default'
|
|
DEFAULT_ENCRYPTION_SETTING = 'no'
|
|
DEFAULT_MULTI_INITIATOR_SETTING = 'false'
|
|
DEFAULT_SNAP_QUOTA = sys.maxsize
|
|
VOL_EDIT_MASK = 4 + 16 + 32 + 64 + 256 + 512
|
|
MANAGE_EDIT_MASK = 1 + 262144
|
|
UNMANAGE_EDIT_MASK = 262144
|
|
AGENT_TYPE_OPENSTACK = 5
|
|
AGENT_TYPE_NONE = 1
|
|
SOAP_PORT = 5391
|
|
SM_ACL_APPLY_TO_BOTH = 3
|
|
SM_ACL_CHAP_USER_ANY = '*'
|
|
SM_SUBNET_DATA = 3
|
|
SM_SUBNET_MGMT_PLUS_DATA = 4
|
|
LUN_ID = '0'
|
|
WARN_LEVEL = 0.8
|
|
|
|
# Work around for ubuntu_openssl_bug_965371. Python soap client suds
|
|
# throws the error ssl-certificate-verify-failed-error, workaround to disable
|
|
# ssl check for now
|
|
if hasattr(ssl, '_create_unverified_context'):
|
|
ssl._create_default_https_context = ssl._create_unverified_context
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
nimble_opts = [
|
|
cfg.StrOpt('nimble_pool_name',
|
|
default='default',
|
|
help='Nimble Controller pool name'),
|
|
cfg.StrOpt('nimble_subnet_label',
|
|
default='*',
|
|
help='Nimble Subnet Label'), ]
|
|
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(nimble_opts)
|
|
|
|
|
|
class NimbleDriverException(exception.VolumeDriverException):
|
|
message = _("Nimble Cinder Driver exception")
|
|
|
|
|
|
class NimbleAPIException(exception.VolumeBackendAPIException):
|
|
message = _("Unexpected response from Nimble API")
|
|
|
|
|
|
@interface.volumedriver
|
|
class NimbleISCSIDriver(san.SanISCSIDriver):
|
|
|
|
"""OpenStack driver to enable Nimble Controller.
|
|
|
|
Version history:
|
|
|
|
.. code-block:: none
|
|
|
|
1.0 - Initial driver
|
|
1.1.1 - Updated VERSION to Nimble driver version
|
|
1.1.2 - Update snap-quota to unlimited
|
|
2.0.0 - Added Extra Spec Capability
|
|
Correct capacity reporting
|
|
Added Manage/Unmanage volume support
|
|
2.0.1 - Added multi-initiator support through extra-specs
|
|
2.0.2 - Fixed supporting extra specs while cloning vols
|
|
3.0.0 - Newton Support for Force Backup
|
|
"""
|
|
|
|
VERSION = DRIVER_VERSION
|
|
|
|
# ThirdPartySystems wiki page
|
|
CI_WIKI_NAME = "Nimble_Storage_CI"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(NimbleISCSIDriver, self).__init__(*args, **kwargs)
|
|
self.APIExecutor = None
|
|
self.group_stats = {}
|
|
self.configuration.append_config_values(nimble_opts)
|
|
|
|
def _check_config(self):
|
|
"""Ensure that the flags we care about are set."""
|
|
required_config = ['san_ip', 'san_login', 'san_password']
|
|
for attr in required_config:
|
|
if not getattr(self.configuration, attr, None):
|
|
raise exception.InvalidInput(reason=_('%s is not set.') %
|
|
attr)
|
|
|
|
def _get_discovery_ip(self, netconfig):
|
|
"""Get discovery ip."""
|
|
subnet_label = self.configuration.nimble_subnet_label
|
|
LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s',
|
|
{'netlabel': subnet_label, 'netconf': netconfig})
|
|
ret_discovery_ip = ''
|
|
for subnet in netconfig['subnet-list']:
|
|
LOG.info(_LI('Exploring array subnet label %s'), subnet['label'])
|
|
if subnet_label == '*':
|
|
# Use the first data subnet, save mgmt+data for later
|
|
if subnet['subnet-id']['type'] == SM_SUBNET_DATA:
|
|
LOG.info(_LI('Discovery ip %(disc_ip)s is used '
|
|
'on data subnet %(net_label)s'),
|
|
{'disc_ip': subnet['discovery-ip'],
|
|
'net_label': subnet['label']})
|
|
return subnet['discovery-ip']
|
|
elif (subnet['subnet-id']['type'] ==
|
|
SM_SUBNET_MGMT_PLUS_DATA):
|
|
LOG.info(_LI('Discovery ip %(disc_ip)s is found'
|
|
' on mgmt+data subnet %(net_label)s'),
|
|
{'disc_ip': subnet['discovery-ip'],
|
|
'net_label': subnet['label']})
|
|
ret_discovery_ip = subnet['discovery-ip']
|
|
# If subnet is specified and found, use the subnet
|
|
elif subnet_label == subnet['label']:
|
|
LOG.info(_LI('Discovery ip %(disc_ip)s is used'
|
|
' on subnet %(net_label)s'),
|
|
{'disc_ip': subnet['discovery-ip'],
|
|
'net_label': subnet['label']})
|
|
return subnet['discovery-ip']
|
|
if ret_discovery_ip:
|
|
LOG.info(_LI('Discovery ip %s is used on mgmt+data subnet'),
|
|
ret_discovery_ip)
|
|
return ret_discovery_ip
|
|
else:
|
|
raise NimbleDriverException(_('No suitable discovery ip found'))
|
|
|
|
def _update_existing_vols_agent_type(self, context):
|
|
LOG.debug("Updating existing volumes to have "
|
|
"agent_type = 'OPENSTACK'")
|
|
all_vols = volume.VolumeList.get_all_by_host(
|
|
context, self.host, {'status': 'available'})
|
|
for vol in all_vols:
|
|
try:
|
|
self.APIExecutor.edit_vol(
|
|
vol.name,
|
|
UNMANAGE_EDIT_MASK,
|
|
{'agent-type': AGENT_TYPE_OPENSTACK})
|
|
except NimbleAPIException:
|
|
LOG.warning(_LW('Error updating agent-type for '
|
|
'volume %s.'), vol.name)
|
|
|
|
def do_setup(self, context):
|
|
"""Setup the Nimble Cinder volume driver."""
|
|
self._check_config()
|
|
# Setup API Executor
|
|
try:
|
|
self.APIExecutor = NimbleAPIExecutor(
|
|
username=self.configuration.san_login,
|
|
password=self.configuration.san_password,
|
|
ip=self.configuration.san_ip)
|
|
except Exception:
|
|
LOG.error(_LE('Failed to create SOAP client.'
|
|
'Check san_ip, username, password'
|
|
' and make sure the array version is compatible'))
|
|
raise
|
|
self._update_existing_vols_agent_type(context)
|
|
|
|
def _get_provider_location(self, volume_name):
|
|
"""Get volume iqn for initiator access."""
|
|
vol_info = self.APIExecutor.get_vol_info(volume_name)
|
|
iqn = vol_info['target-name']
|
|
netconfig = self.APIExecutor.get_netconfig('active')
|
|
target_ipaddr = self._get_discovery_ip(netconfig)
|
|
iscsi_portal = target_ipaddr + ':3260'
|
|
provider_location = '%s %s %s' % (iscsi_portal, iqn, LUN_ID)
|
|
LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'),
|
|
{'name': volume_name, 'loc': provider_location})
|
|
return provider_location
|
|
|
|
def _get_model_info(self, volume_name):
|
|
"""Get model info for the volume."""
|
|
return (
|
|
{'provider_location': self._get_provider_location(volume_name),
|
|
'provider_auth': None})
|
|
|
|
def create_volume(self, volume):
|
|
"""Create a new volume."""
|
|
reserve = not self.configuration.san_thin_provision
|
|
self.APIExecutor.create_vol(
|
|
volume,
|
|
self.configuration.nimble_pool_name, reserve)
|
|
return self._get_model_info(volume['name'])
|
|
|
|
def is_volume_backup_clone(self, volume):
|
|
"""Check if the volume is created through cinder-backup workflow.
|
|
|
|
:param volume: reference to volume from delete_volume()
|
|
"""
|
|
vol_info = self.APIExecutor.get_vol_info(volume.name)
|
|
if vol_info['clone'] and vol_info['base-snap'] and vol_info[
|
|
'parent-vol']:
|
|
LOG.debug("Nimble base-snap exists for volume :%s", volume['name'])
|
|
volume_name_prefix = volume.name.replace(volume.id, "")
|
|
LOG.debug("volume_name_prefix : %s", volume_name_prefix)
|
|
snap_info = self.APIExecutor.get_snap_info(vol_info['base-snap'],
|
|
vol_info['parent-vol'])
|
|
if snap_info['description'] and "backup-vol-" in snap_info[
|
|
'description']:
|
|
parent_vol_id = vol_info['parent-vol'
|
|
].replace(volume_name_prefix, "")
|
|
if "backup-vol-" + parent_vol_id in snap_info['description']:
|
|
LOG.info(_LI("nimble backup-snapshot exists name: %s"),
|
|
snap_info['name'])
|
|
return snap_info['name'], snap_info['vol']
|
|
return "", ""
|
|
|
|
def delete_volume(self, volume):
|
|
"""Delete the specified volume."""
|
|
snap_name, vol_name = self.is_volume_backup_clone(volume)
|
|
self.APIExecutor.online_vol(volume['name'], False,
|
|
ignore_list=['SM-enoent'])
|
|
self.APIExecutor.dissociate_volcoll(volume['name'],
|
|
ignore_list=['SM-enoent'])
|
|
self.APIExecutor.delete_vol(volume['name'], ignore_list=['SM-enoent'])
|
|
|
|
# Nimble backend does not delete the snapshot from the parent volume
|
|
# if there is a dependent clone. So the deletes need to be in reverse
|
|
# order i.e.
|
|
# 1. First delete the clone volume used for backup
|
|
# 2. Delete the base snapshot used for clone from the parent volume.
|
|
# This is only done for the force backup clone operation as it is
|
|
# a temporary operation in which we are certain that the snapshot does
|
|
# not need to be preserved after the backup is completed.
|
|
|
|
if snap_name and vol_name:
|
|
self.APIExecutor.online_snap(vol_name,
|
|
False,
|
|
snap_name,
|
|
ignore_list=['SM-ealready',
|
|
'SM-enoent'])
|
|
self.APIExecutor.delete_snap(vol_name,
|
|
snap_name,
|
|
ignore_list=['SM-enoent'])
|
|
|
|
def _generate_random_string(self, length):
|
|
"""Generates random_string."""
|
|
char_set = string.ascii_lowercase
|
|
return ''.join(random.sample(char_set, length))
|
|
|
|
def _clone_volume_from_snapshot(self, volume, snapshot):
|
|
"""Clone volume from snapshot.
|
|
|
|
Extend the volume if the size of the volume is more than the snapshot.
|
|
"""
|
|
reserve = not self.configuration.san_thin_provision
|
|
self.APIExecutor.clone_vol(volume, snapshot, reserve)
|
|
if(volume['size'] > snapshot['volume_size']):
|
|
vol_size = volume['size'] * units.Gi
|
|
reserve_size = vol_size if reserve else 0
|
|
self.APIExecutor.edit_vol(
|
|
volume['name'],
|
|
VOL_EDIT_MASK, # mask for vol attributes
|
|
{'size': vol_size,
|
|
'reserve': reserve_size,
|
|
'warn-level': int(vol_size * WARN_LEVEL),
|
|
'quota': vol_size,
|
|
'snap-quota': DEFAULT_SNAP_QUOTA})
|
|
return self._get_model_info(volume['name'])
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Create a clone of the specified volume."""
|
|
snapshot_name = ('openstack-clone-' +
|
|
volume['name'] + '-' +
|
|
self._generate_random_string(12))
|
|
snapshot = {'volume_name': src_vref['name'],
|
|
'name': snapshot_name,
|
|
'volume_size': src_vref['size'],
|
|
'display_name': volume.display_name,
|
|
'display_description': ''}
|
|
self.APIExecutor.snap_vol(snapshot)
|
|
self._clone_volume_from_snapshot(volume, snapshot)
|
|
return self._get_model_info(volume['name'])
|
|
|
|
def create_export(self, context, volume, connector):
|
|
"""Driver entry point to get the export info for a new volume."""
|
|
return self._get_model_info(volume['name'])
|
|
|
|
def ensure_export(self, context, volume):
|
|
"""Driver entry point to get the export info for an existing volume."""
|
|
return self._get_model_info(volume['name'])
|
|
|
|
def create_snapshot(self, snapshot):
|
|
"""Create a snapshot."""
|
|
self.APIExecutor.snap_vol(snapshot)
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
"""Delete a snapshot."""
|
|
self.APIExecutor.online_snap(
|
|
snapshot['volume_name'],
|
|
False,
|
|
snapshot['name'],
|
|
ignore_list=['SM-ealready', 'SM-enoent'])
|
|
self.APIExecutor.delete_snap(snapshot['volume_name'],
|
|
snapshot['name'],
|
|
ignore_list=['SM-enoent'])
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Create a volume from a snapshot."""
|
|
self._clone_volume_from_snapshot(volume, snapshot)
|
|
return self._get_model_info(volume['name'])
|
|
|
|
def get_volume_stats(self, refresh=False):
|
|
"""Get volume stats. This is more of getting group stats."""
|
|
if refresh:
|
|
group_info = self.APIExecutor.get_group_config()
|
|
if not group_info['spaceInfoValid']:
|
|
raise NimbleDriverException(_('SpaceInfo returned by'
|
|
'array is invalid'))
|
|
total_capacity = (group_info['usableCapacity'] /
|
|
float(units.Gi))
|
|
used_space = ((group_info['volUsageCompressed'] +
|
|
group_info['snapUsageCompressed'] +
|
|
group_info['unusedReserve']) /
|
|
float(units.Gi))
|
|
free_space = total_capacity - used_space
|
|
LOG.debug('total_capacity=%(capacity)f '
|
|
'used_space=%(used)f free_space=%(free)f',
|
|
{'capacity': total_capacity,
|
|
'used': used_space,
|
|
'free': free_space})
|
|
backend_name = self.configuration.safe_get(
|
|
'volume_backend_name') or self.__class__.__name__
|
|
self.group_stats = {'volume_backend_name': backend_name,
|
|
'vendor_name': 'Nimble',
|
|
'driver_version': DRIVER_VERSION,
|
|
'storage_protocol': 'iSCSI'}
|
|
# Just use a single pool for now, FIXME to support multiple
|
|
# pools
|
|
single_pool = dict(
|
|
pool_name=backend_name,
|
|
total_capacity_gb=total_capacity,
|
|
free_capacity_gb=free_space,
|
|
reserved_percentage=0,
|
|
QoS_support=False)
|
|
self.group_stats['pools'] = [single_pool]
|
|
return self.group_stats
|
|
|
|
def extend_volume(self, volume, new_size):
|
|
"""Extend an existing volume."""
|
|
volume_name = volume['name']
|
|
LOG.info(_LI('Entering extend_volume volume=%(vol)s '
|
|
'new_size=%(size)s'),
|
|
{'vol': volume_name, 'size': new_size})
|
|
vol_size = int(new_size) * units.Gi
|
|
reserve = not self.configuration.san_thin_provision
|
|
reserve_size = vol_size if reserve else 0
|
|
self.APIExecutor.edit_vol(
|
|
volume_name,
|
|
VOL_EDIT_MASK, # mask for vol attributes
|
|
{'size': vol_size,
|
|
'reserve': reserve_size,
|
|
'warn-level': int(vol_size * WARN_LEVEL),
|
|
'quota': vol_size,
|
|
'snap-quota': DEFAULT_SNAP_QUOTA})
|
|
|
|
def _get_existing_volume_ref_name(self, existing_ref):
|
|
"""Returns the volume name of an existing ref"""
|
|
|
|
vol_name = None
|
|
if 'source-name' in existing_ref:
|
|
vol_name = existing_ref['source-name']
|
|
else:
|
|
reason = _("Reference must contain source-name.")
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref,
|
|
reason=reason)
|
|
|
|
return vol_name
|
|
|
|
def manage_existing(self, volume, external_ref):
|
|
"""Manage an existing nimble volume (import to cinder)"""
|
|
|
|
# Get the volume name from the external reference
|
|
target_vol_name = self._get_existing_volume_ref_name(external_ref)
|
|
LOG.debug('Entering manage_existing. '
|
|
'Target_volume_name = %s', target_vol_name)
|
|
|
|
# Get vol info from the volume name obtained from the reference
|
|
vol_info = self.APIExecutor.get_vol_info(target_vol_name)
|
|
|
|
# Check if volume is already managed by OpenStack
|
|
if vol_info['agent-type'] == AGENT_TYPE_OPENSTACK:
|
|
msg = (_('Volume %s is already managed by OpenStack.')
|
|
% target_vol_name)
|
|
raise exception.ManageExistingAlreadyManaged(
|
|
volume_ref=volume['id'])
|
|
|
|
# If agent-type is not None then raise exception
|
|
if vol_info['agent-type'] != AGENT_TYPE_NONE:
|
|
msg = (_('Volume should have agent-type set as None.'))
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
new_vol_name = volume['name']
|
|
|
|
if vol_info['online']:
|
|
msg = (_('Volume %s is online. Set volume to offline for '
|
|
'managing using OpenStack.') % target_vol_name)
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
# edit the volume
|
|
self.APIExecutor.edit_vol(target_vol_name,
|
|
MANAGE_EDIT_MASK,
|
|
{'name': new_vol_name,
|
|
'agent-type': AGENT_TYPE_OPENSTACK})
|
|
|
|
# make the volume online after rename
|
|
self.APIExecutor.online_vol(new_vol_name, True, ignore_list=[
|
|
'SM-enoent'])
|
|
|
|
return self._get_model_info(new_vol_name)
|
|
|
|
def manage_existing_get_size(self, volume, external_ref):
|
|
"""Return size of an existing volume"""
|
|
|
|
LOG.debug('Volume name : %(name)s External ref : %(ref)s',
|
|
{'name': volume['name'], 'ref': external_ref})
|
|
|
|
target_vol_name = self._get_existing_volume_ref_name(external_ref)
|
|
|
|
# get vol info
|
|
vol_info = self.APIExecutor.get_vol_info(target_vol_name)
|
|
|
|
LOG.debug('Volume size : %(size)s Volume-name : %(name)s',
|
|
{'size': vol_info['size'], 'name': vol_info['name']})
|
|
|
|
return int(math.ceil(float(vol_info['size']) / units.Gi))
|
|
|
|
def unmanage(self, volume):
|
|
"""Removes the specified volume from Cinder management."""
|
|
|
|
vol_name = volume['name']
|
|
LOG.info(_LI("Entering unmanage_volume volume = %s"), vol_name)
|
|
|
|
# check agent type
|
|
vol_info = self.APIExecutor.get_vol_info(vol_name)
|
|
if vol_info['agent-type'] != AGENT_TYPE_OPENSTACK:
|
|
msg = (_('Only volumes managed by OpenStack can be unmanaged.'))
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
# update the agent-type to None
|
|
self.APIExecutor.edit_vol(vol_name,
|
|
UNMANAGE_EDIT_MASK,
|
|
{'agent-type': AGENT_TYPE_NONE})
|
|
|
|
# offline the volume
|
|
self.APIExecutor.online_vol(vol_name, False, ignore_list=[
|
|
'SM-enoent'])
|
|
|
|
def _create_igroup_for_initiator(self, initiator_name):
|
|
"""Creates igroup for an initiator and returns the igroup name."""
|
|
igrp_name = 'openstack-' + self._generate_random_string(12)
|
|
LOG.info(_LI('Creating initiator group %(grp)s '
|
|
'with initiator %(iname)s'),
|
|
{'grp': igrp_name, 'iname': initiator_name})
|
|
self.APIExecutor.create_initiator_group(igrp_name, initiator_name)
|
|
return igrp_name
|
|
|
|
def _get_igroupname_for_initiator(self, initiator_name):
|
|
initiator_groups = self.APIExecutor.get_initiator_grp_list()
|
|
for initiator_group in initiator_groups:
|
|
if 'initiator-list' in initiator_group:
|
|
if (len(initiator_group['initiator-list']) == 1 and
|
|
initiator_group['initiator-list'][0]['name'] ==
|
|
initiator_name):
|
|
LOG.info(_LI('igroup %(grp)s found for '
|
|
'initiator %(iname)s'),
|
|
{'grp': initiator_group['name'],
|
|
'iname': initiator_name})
|
|
return initiator_group['name']
|
|
LOG.info(_LI('No igroup found for initiator %s'), initiator_name)
|
|
return ''
|
|
|
|
def initialize_connection(self, volume, connector):
|
|
"""Driver entry point to attach a volume to an instance."""
|
|
LOG.info(_LI('Entering initialize_connection volume=%(vol)s'
|
|
' connector=%(conn)s location=%(loc)s'),
|
|
{'vol': volume,
|
|
'conn': connector,
|
|
'loc': volume['provider_location']})
|
|
initiator_name = connector['initiator']
|
|
initiator_group_name = self._get_igroupname_for_initiator(
|
|
initiator_name)
|
|
if not initiator_group_name:
|
|
initiator_group_name = self._create_igroup_for_initiator(
|
|
initiator_name)
|
|
LOG.info(_LI('Initiator group name is %(grp)s for initiator '
|
|
'%(iname)s'),
|
|
{'grp': initiator_group_name, 'iname': initiator_name})
|
|
self.APIExecutor.add_acl(volume, initiator_group_name)
|
|
(iscsi_portal, iqn, lun_num) = volume['provider_location'].split()
|
|
properties = {}
|
|
properties['target_discovered'] = False # whether discovery was used
|
|
properties['target_portal'] = iscsi_portal
|
|
properties['target_iqn'] = iqn
|
|
properties['target_lun'] = int(lun_num)
|
|
properties['volume_id'] = volume['id'] # used by xen currently
|
|
return {
|
|
'driver_volume_type': 'iscsi',
|
|
'data': properties,
|
|
}
|
|
|
|
def terminate_connection(self, volume, connector, **kwargs):
|
|
"""Driver entry point to unattach a volume from an instance."""
|
|
LOG.info(_LI('Entering terminate_connection volume=%(vol)s'
|
|
' connector=%(conn)s location=%(loc)s.'),
|
|
{'vol': volume,
|
|
'conn': connector,
|
|
'loc': volume['provider_location']})
|
|
initiator_name = connector['initiator']
|
|
initiator_group_name = self._get_igroupname_for_initiator(
|
|
initiator_name)
|
|
if not initiator_group_name:
|
|
raise NimbleDriverException(
|
|
_('No initiator group found for initiator %s') %
|
|
initiator_name)
|
|
self.APIExecutor.remove_acl(volume, initiator_group_name)
|
|
|
|
|
|
def _response_checker(func):
|
|
"""Decorator function to check if the response of an API is positive."""
|
|
@functools.wraps(func)
|
|
def inner_response_checker(self, *args, **kwargs):
|
|
response = func(self, *args, **kwargs)
|
|
ignore_list = (kwargs['ignore_list']
|
|
if 'ignore_list' in kwargs else [])
|
|
for err in response['err-list']['err-list']:
|
|
err_str = self._get_err_str(err['code'])
|
|
if err_str != 'SM-ok' and err_str not in ignore_list:
|
|
msg = (_('API %(name)s failed with error string %(err)s')
|
|
% {'name': func.__name__, 'err': err_str})
|
|
LOG.error(msg)
|
|
raise NimbleAPIException(msg)
|
|
return response
|
|
return inner_response_checker
|
|
|
|
|
|
def _connection_checker(func):
|
|
"""Decorator to re-establish and re-run the api if session has expired."""
|
|
@functools.wraps(func)
|
|
def inner_connection_checker(self, *args, **kwargs):
|
|
for attempts in range(2):
|
|
try:
|
|
return func(self, *args, **kwargs)
|
|
except NimbleAPIException as e:
|
|
if attempts < 1 and (re.search('SM-eaccess',
|
|
six.text_type(e))):
|
|
LOG.info(_LI('Session might have expired.'
|
|
' Trying to relogin'))
|
|
self.login()
|
|
continue
|
|
else:
|
|
LOG.error(_LE('Re-throwing Exception %s'), e)
|
|
raise
|
|
return inner_connection_checker
|
|
|
|
|
|
class NimbleAPIExecutor(object):
|
|
|
|
"""Makes Nimble API calls."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.sid = None
|
|
self.username = kwargs['username']
|
|
self.password = kwargs['password']
|
|
|
|
wsdl_url = 'https://%s/wsdl/NsGroupManagement.wsdl' % (kwargs['ip'])
|
|
LOG.debug('Using Nimble wsdl_url: %s', wsdl_url)
|
|
self.err_string_dict = self._create_err_code_to_str_mapper(wsdl_url)
|
|
self.client = client.Client(wsdl_url,
|
|
username=self.username,
|
|
password=self.password)
|
|
soap_url = ('https://%(ip)s:%(port)s/soap' % {'ip': kwargs['ip'],
|
|
'port': SOAP_PORT})
|
|
LOG.debug('Using Nimble soap_url: %s', soap_url)
|
|
self.client.set_options(location=soap_url)
|
|
self.login()
|
|
|
|
def _create_err_code_to_str_mapper(self, wsdl_url):
|
|
f = urllib.request.urlopen(wsdl_url)
|
|
wsdl_file = f.read()
|
|
err_enums = re.findall(
|
|
r'<simpleType name="SmErrorType">(.*?)</simpleType>',
|
|
wsdl_file,
|
|
re.DOTALL)
|
|
err_enums = ''.join(err_enums).split('\n')
|
|
ret_dict = {}
|
|
for enum in err_enums:
|
|
m = re.search(r'"(.*?)"(.*?)= (\d+) ', enum)
|
|
if m:
|
|
ret_dict[int(m.group(3))] = m.group(1)
|
|
return ret_dict
|
|
|
|
def _get_err_str(self, code):
|
|
if code in self.err_string_dict:
|
|
return self.err_string_dict[code]
|
|
else:
|
|
return 'Unknown error Code: %s' % code
|
|
|
|
@_response_checker
|
|
def _execute_login(self):
|
|
return self.client.service.login(req={
|
|
'username': self.username,
|
|
'password': self.password
|
|
})
|
|
|
|
def login(self):
|
|
"""Execute Https Login API."""
|
|
response = self._execute_login()
|
|
LOG.info(_LI('Successful login by user %s'), self.username)
|
|
self.sid = response['authInfo']['sid']
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def _execute_get_netconfig(self, name):
|
|
return self.client.service.getNetConfig(request={'sid': self.sid,
|
|
'name': name})
|
|
|
|
def get_netconfig(self, name):
|
|
"""Execute getNetConfig API."""
|
|
response = self._execute_get_netconfig(name)
|
|
return response['config']
|
|
|
|
def _get_volumetype_extraspecs(self, volume):
|
|
specs = {}
|
|
|
|
type_id = volume['volume_type_id']
|
|
if type_id is not None:
|
|
specs = volume_types.get_volume_type_extra_specs(type_id)
|
|
return specs
|
|
|
|
def _get_extra_spec_values(self, extra_specs):
|
|
"""Nimble specific extra specs."""
|
|
perf_policy_name = extra_specs.get(EXTRA_SPEC_PERF_POLICY,
|
|
DEFAULT_PERF_POLICY_SETTING)
|
|
encryption = extra_specs.get(EXTRA_SPEC_ENCRYPTION,
|
|
DEFAULT_ENCRYPTION_SETTING)
|
|
multi_initiator = extra_specs.get(EXTRA_SPEC_MULTI_INITIATOR,
|
|
DEFAULT_MULTI_INITIATOR_SETTING)
|
|
extra_specs_map = {}
|
|
extra_specs_map[EXTRA_SPEC_PERF_POLICY] = perf_policy_name
|
|
extra_specs_map[EXTRA_SPEC_ENCRYPTION] = encryption
|
|
extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR] = multi_initiator
|
|
|
|
return extra_specs_map
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def _execute_create_vol(self, volume, pool_name, reserve):
|
|
# Set volume size, display name and description
|
|
volume_size = volume['size'] * units.Gi
|
|
reserve_size = volume_size if reserve else 0
|
|
# Set volume description
|
|
display_list = [getattr(volume, 'display_name', ''),
|
|
getattr(volume, 'display_description', '')]
|
|
description = ':'.join(filter(None, display_list))
|
|
# Limit description size to 254 characters
|
|
description = description[:254]
|
|
|
|
specs = self._get_volumetype_extraspecs(volume)
|
|
extra_specs_map = self._get_extra_spec_values(specs)
|
|
perf_policy_name = extra_specs_map[EXTRA_SPEC_PERF_POLICY]
|
|
encrypt = extra_specs_map[EXTRA_SPEC_ENCRYPTION]
|
|
multi_initiator = extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR]
|
|
# default value of cipher for encryption
|
|
cipher = DEFAULT_CIPHER
|
|
if encrypt.lower() == 'yes':
|
|
cipher = AES_256_XTS_CIPHER
|
|
|
|
LOG.debug('Creating a new volume=%(vol)s size=%(size)s'
|
|
' reserve=%(reserve)s in pool=%(pool)s'
|
|
' description=%(description)s with Extra Specs'
|
|
' perfpol-name=%(perfpol-name)s'
|
|
' encryption=%(encryption)s cipher=%(cipher)s'
|
|
' agent-type=%(agent-type)s'
|
|
' multi-initiator=%(multi-initiator)s',
|
|
{'vol': volume['name'],
|
|
'size': volume_size,
|
|
'reserve': reserve,
|
|
'pool': pool_name,
|
|
'description': description,
|
|
'perfpol-name': perf_policy_name,
|
|
'encryption': encrypt,
|
|
'cipher': cipher,
|
|
'agent-type': AGENT_TYPE_OPENSTACK,
|
|
'multi-initiator': multi_initiator})
|
|
|
|
return self.client.service.createVol(
|
|
request={'sid': self.sid,
|
|
'attr': {'name': volume['name'],
|
|
'description': description,
|
|
'size': volume_size,
|
|
'reserve': reserve_size,
|
|
'warn-level': int(volume_size * WARN_LEVEL),
|
|
'quota': volume_size,
|
|
'snap-quota': DEFAULT_SNAP_QUOTA,
|
|
'online': True,
|
|
'pool-name': pool_name,
|
|
'agent-type': AGENT_TYPE_OPENSTACK,
|
|
'perfpol-name': perf_policy_name,
|
|
'encryptionAttr': {'cipher': cipher},
|
|
'multi-initiator': multi_initiator}})
|
|
|
|
def create_vol(self, volume, pool_name, reserve):
|
|
"""Execute createVol API."""
|
|
response = self._execute_create_vol(volume, pool_name, reserve)
|
|
LOG.info(_LI('Successfully create volume %s'), response['name'])
|
|
return response['name']
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def _execute_get_group_config(self):
|
|
LOG.debug('Getting group config information')
|
|
return self.client.service.getGroupConfig(request={'sid': self.sid})
|
|
|
|
def get_group_config(self):
|
|
"""Execute getGroupConfig API."""
|
|
response = self._execute_get_group_config()
|
|
LOG.debug('Successfully retrieved group config information')
|
|
return response['info']
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def add_acl(self, volume, initiator_group_name):
|
|
"""Execute addAcl API."""
|
|
LOG.info(_LI('Adding ACL to volume=%(vol)s with'
|
|
' initiator group name %(igrp)s'),
|
|
{'vol': volume['name'],
|
|
'igrp': initiator_group_name})
|
|
return self.client.service.addVolAcl(
|
|
request={'sid': self.sid,
|
|
'volname': volume['name'],
|
|
'apply-to': SM_ACL_APPLY_TO_BOTH,
|
|
'chapuser': SM_ACL_CHAP_USER_ANY,
|
|
'initiatorgrp': initiator_group_name})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def remove_acl(self, volume, initiator_group_name):
|
|
"""Execute removeVolAcl API."""
|
|
LOG.info(_LI('Removing ACL from volume=%(vol)s'
|
|
' for initiator group %(igrp)s'),
|
|
{'vol': volume['name'],
|
|
'igrp': initiator_group_name})
|
|
return self.client.service.removeVolAcl(
|
|
request={'sid': self.sid,
|
|
'volname': volume['name'],
|
|
'apply-to': SM_ACL_APPLY_TO_BOTH,
|
|
'chapuser': SM_ACL_CHAP_USER_ANY,
|
|
'initiatorgrp': initiator_group_name})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def _execute_get_vol_info(self, vol_name):
|
|
LOG.info(_LI('Getting volume information '
|
|
'for vol_name=%s'), vol_name)
|
|
return self.client.service.getVolInfo(request={'sid': self.sid,
|
|
'name': vol_name})
|
|
|
|
def get_vol_info(self, vol_name):
|
|
"""Execute getVolInfo API."""
|
|
response = self._execute_get_vol_info(vol_name)
|
|
LOG.info(_LI('Successfully got volume information for volume %s'),
|
|
vol_name)
|
|
return response['vol']
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def _execute_get_snap_info(self, snap_name, vol_name):
|
|
LOG.info(_LI('Getting snapshot information for %(vol_name)s '
|
|
'%(snap_name)s'), {'vol_name': vol_name,
|
|
'snap_name': snap_name})
|
|
return self.client.service.getSnapInfo(request={'sid': self.sid,
|
|
'vol': vol_name,
|
|
'name': snap_name})
|
|
|
|
def get_snap_info(self, snap_name, vol_name):
|
|
"""Get snapshot information.
|
|
|
|
:param snap_name: snapshot name
|
|
:param vol_name: volume name
|
|
:return: response object
|
|
"""
|
|
|
|
response = self._execute_get_snap_info(snap_name, vol_name)
|
|
LOG.info(_LI('Successfully got snapshot information for snapshot '
|
|
'%(snap)s and %(volume)s'),
|
|
{'snap': snap_name,
|
|
'volume': vol_name})
|
|
return response['snap']
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def online_vol(self, vol_name, online_flag, *args, **kwargs):
|
|
"""Execute onlineVol API."""
|
|
LOG.info(_LI('Setting volume %(vol)s to online_flag %(flag)s'),
|
|
{'vol': vol_name, 'flag': online_flag})
|
|
return self.client.service.onlineVol(request={'sid': self.sid,
|
|
'name': vol_name,
|
|
'online': online_flag})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def online_snap(self, vol_name, online_flag, snap_name, *args, **kwargs):
|
|
"""Execute onlineSnap API."""
|
|
LOG.info(_LI('Setting snapshot %(snap)s to online_flag %(flag)s'),
|
|
{'snap': snap_name, 'flag': online_flag})
|
|
return self.client.service.onlineSnap(request={'sid': self.sid,
|
|
'vol': vol_name,
|
|
'name': snap_name,
|
|
'online': online_flag})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def dissociate_volcoll(self, vol_name, *args, **kwargs):
|
|
"""Execute dissocProtPol API."""
|
|
LOG.info(_LI('Dissociating volume %s '), vol_name)
|
|
return self.client.service.dissocProtPol(
|
|
request={'sid': self.sid,
|
|
'vol-name': vol_name})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def delete_vol(self, vol_name, *args, **kwargs):
|
|
"""Execute deleteVol API."""
|
|
LOG.info(_LI('Deleting volume %s '), vol_name)
|
|
return self.client.service.deleteVol(request={'sid': self.sid,
|
|
'name': vol_name})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def snap_vol(self, snapshot):
|
|
"""Execute snapVol API."""
|
|
volume_name = snapshot['volume_name']
|
|
snap_name = snapshot['name']
|
|
# Set snapshot description
|
|
display_list = [getattr(snapshot, 'display_name', snapshot[
|
|
'display_name']),
|
|
getattr(snapshot, 'display_description', '')]
|
|
snap_description = ':'.join(filter(None, display_list))
|
|
# Limit to 254 characters
|
|
LOG.debug("snap_description %s", snap_description)
|
|
snap_description = snap_description[:254]
|
|
LOG.info(_LI('Creating snapshot for volume_name=%(vol)s'
|
|
' snap_name=%(name)s snap_description=%(desc)s'),
|
|
{'vol': volume_name,
|
|
'name': snap_name,
|
|
'desc': snap_description})
|
|
return self.client.service.snapVol(
|
|
request={'sid': self.sid,
|
|
'vol': volume_name,
|
|
'snapAttr': {'name': snap_name,
|
|
'description': snap_description}})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def delete_snap(self, vol_name, snap_name, *args, **kwargs):
|
|
"""Execute deleteSnap API."""
|
|
LOG.info(_LI('Deleting snapshot %s '), snap_name)
|
|
return self.client.service.deleteSnap(request={'sid': self.sid,
|
|
'vol': vol_name,
|
|
'name': snap_name})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def clone_vol(self, volume, snapshot, reserve):
|
|
"""Execute cloneVol API."""
|
|
volume_name = snapshot['volume_name']
|
|
snap_name = snapshot['name']
|
|
clone_name = volume['name']
|
|
snap_size = snapshot['volume_size']
|
|
reserve_size = snap_size * units.Gi if reserve else 0
|
|
|
|
specs = self._get_volumetype_extraspecs(volume)
|
|
extra_specs_map = self._get_extra_spec_values(specs)
|
|
perf_policy_name = extra_specs_map.get(EXTRA_SPEC_PERF_POLICY)
|
|
encrypt = extra_specs_map.get(EXTRA_SPEC_ENCRYPTION)
|
|
multi_initiator = extra_specs_map.get(EXTRA_SPEC_MULTI_INITIATOR)
|
|
# default value of cipher for encryption
|
|
cipher = DEFAULT_CIPHER
|
|
if encrypt.lower() == 'yes':
|
|
cipher = AES_256_XTS_CIPHER
|
|
|
|
LOG.info(_LI('Cloning volume from snapshot volume=%(vol)s '
|
|
'snapshot=%(snap)s clone=%(clone)s snap_size=%(size)s '
|
|
'reserve=%(reserve)s' 'agent-type=%(agent-type)s '
|
|
'perfpol-name=%(perfpol-name)s '
|
|
'encryption=%(encryption)s cipher=%(cipher)s '
|
|
'multi-initiator=%(multi-initiator)s'),
|
|
{'vol': volume_name,
|
|
'snap': snap_name,
|
|
'clone': clone_name,
|
|
'size': snap_size,
|
|
'reserve': reserve,
|
|
'agent-type': AGENT_TYPE_OPENSTACK,
|
|
'perfpol-name': perf_policy_name,
|
|
'encryption': encrypt,
|
|
'cipher': cipher,
|
|
'multi-initiator': multi_initiator})
|
|
clone_size = snap_size * units.Gi
|
|
return self.client.service.cloneVol(
|
|
request={'sid': self.sid,
|
|
'name': volume_name,
|
|
'attr': {'name': clone_name,
|
|
'reserve': reserve_size,
|
|
'warn-level': int(clone_size * WARN_LEVEL),
|
|
'quota': clone_size,
|
|
'snap-quota': DEFAULT_SNAP_QUOTA,
|
|
'online': True,
|
|
'agent-type': AGENT_TYPE_OPENSTACK,
|
|
'perfpol-name': perf_policy_name,
|
|
'encryptionAttr': {'cipher': cipher},
|
|
'multi-initiator': multi_initiator},
|
|
'snap-name': snap_name})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def edit_vol(self, vol_name, mask, attr):
|
|
"""Execute editVol API."""
|
|
LOG.info(_LI('Editing Volume %(vol)s with mask %(mask)s'),
|
|
{'vol': vol_name, 'mask': str(mask)})
|
|
return self.client.service.editVol(request={'sid': self.sid,
|
|
'name': vol_name,
|
|
'mask': mask,
|
|
'attr': attr})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def _execute_get_initiator_grp_list(self):
|
|
LOG.info(_LI('Getting getInitiatorGrpList'))
|
|
return (self.client.service.getInitiatorGrpList(
|
|
request={'sid': self.sid}))
|
|
|
|
def get_initiator_grp_list(self):
|
|
"""Execute getInitiatorGrpList API."""
|
|
response = self._execute_get_initiator_grp_list()
|
|
LOG.info(_LI('Successfully retrieved InitiatorGrpList'))
|
|
return (response['initiatorgrp-list']
|
|
if 'initiatorgrp-list' in response else [])
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def create_initiator_group(self, initiator_group_name, initiator_name):
|
|
"""Execute createInitiatorGrp API."""
|
|
LOG.info(_LI('Creating initiator group %(igrp)s'
|
|
' with one initiator %(iname)s'),
|
|
{'igrp': initiator_group_name, 'iname': initiator_name})
|
|
return self.client.service.createInitiatorGrp(
|
|
request={'sid': self.sid,
|
|
'attr': {'name': initiator_group_name,
|
|
'initiator-list': [{'label': initiator_name,
|
|
'name': initiator_name}]}})
|
|
|
|
@_connection_checker
|
|
@_response_checker
|
|
def delete_initiator_group(self, initiator_group_name, *args, **kwargs):
|
|
"""Execute deleteInitiatorGrp API."""
|
|
LOG.info(_LI('Deleting deleteInitiatorGrp %s '), initiator_group_name)
|
|
return self.client.service.deleteInitiatorGrp(
|
|
request={'sid': self.sid,
|
|
'name': initiator_group_name})
|