1917 lines
84 KiB
Python
1917 lines
84 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 and Nimble AF Arrays.
|
|
|
|
"""
|
|
|
|
import abc
|
|
import functools
|
|
import json
|
|
import random
|
|
import re
|
|
import string
|
|
import sys
|
|
|
|
import eventlet
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import units
|
|
import requests
|
|
import six
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder import interface
|
|
from cinder.objects import volume
|
|
from cinder import utils
|
|
from cinder.volume import configuration
|
|
from cinder.volume import driver
|
|
from cinder.volume.drivers.san import san
|
|
from cinder.volume import volume_types
|
|
from cinder.zonemanager import utils as fczm_utils
|
|
|
|
DRIVER_VERSION = "4.0.1"
|
|
AES_256_XTS_CIPHER = 'aes_256_xts'
|
|
DEFAULT_CIPHER = 'none'
|
|
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
|
|
EXTRA_SPEC_PERF_POLICY = 'nimble:perfpol-name'
|
|
EXTRA_SPEC_MULTI_INITIATOR = 'nimble:multi-initiator'
|
|
EXTRA_SPEC_DEDUPE = 'nimble:dedupe'
|
|
EXTRA_SPEC_IOPS_LIMIT = 'nimble:iops-limit'
|
|
EXTRA_SPEC_FOLDER = 'nimble:folder'
|
|
DEFAULT_PERF_POLICY_SETTING = 'default'
|
|
DEFAULT_ENCRYPTION_SETTING = 'no'
|
|
DEFAULT_DEDUPE_SETTING = 'false'
|
|
DEFAULT_IOPS_LIMIT_SETTING = None
|
|
DEFAULT_MULTI_INITIATOR_SETTING = 'false'
|
|
DEFAULT_FOLDER_SETTING = None
|
|
DEFAULT_SNAP_QUOTA = sys.maxsize
|
|
BACKUP_VOL_PREFIX = 'backup-vol-'
|
|
AGENT_TYPE_OPENSTACK = 'openstack'
|
|
AGENT_TYPE_OPENSTACK_GST = 'openstackv2'
|
|
AGENT_TYPE_NONE = 'none'
|
|
SM_SUBNET_DATA = 'data'
|
|
SM_SUBNET_MGMT_PLUS_DATA = 'mgmt-data'
|
|
SM_STATE_MSG = "is already in requested state"
|
|
SM_OBJ_EXIST_MSG = "Object exists"
|
|
SM_OBJ_ENOENT_MSG = "No such object"
|
|
SM_OBJ_HAS_CLONE = "has a clone"
|
|
IOPS_ERR_MSG = "Please set valid IOPS limit in the range"
|
|
LUN_ID = '0'
|
|
WARN_LEVEL = 80
|
|
DEFAULT_SLEEP = 5
|
|
MIN_IOPS = 256
|
|
MAX_IOPS = 4294967294
|
|
NimbleDefaultVersion = 1
|
|
|
|
|
|
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'),
|
|
cfg.BoolOpt('nimble_verify_certificate',
|
|
default=False,
|
|
help='Whether to verify Nimble SSL Certificate'),
|
|
cfg.StrOpt('nimble_verify_cert_path',
|
|
help='Path to Nimble Array SSL certificate'), ]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(nimble_opts, group=configuration.SHARED_CONF_GROUP)
|
|
|
|
|
|
class NimbleDriverException(exception.VolumeDriverException):
|
|
message = _("Nimble Cinder Driver exception")
|
|
|
|
|
|
class NimbleAPIException(exception.VolumeBackendAPIException):
|
|
message = _("Unexpected response from Nimble API")
|
|
|
|
|
|
class NimbleVolumeBusyException(exception.VolumeIsBusy):
|
|
message = _("Nimble Cinder Driver: Volume Busy")
|
|
|
|
|
|
class NimbleBaseVolumeDriver(san.SanDriver):
|
|
"""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
|
|
3.1.0 - Fibre Channel Support
|
|
4.0.0 - Migrate from SOAP to REST API
|
|
Add support for Group Scoped Target
|
|
4.0.1 - Add QoS and dedupe support
|
|
"""
|
|
VERSION = DRIVER_VERSION
|
|
|
|
# ThirdPartySystems wiki page
|
|
CI_WIKI_NAME = "Nimble_Storage_CI"
|
|
|
|
# TODO(jsbryant) Remove driver in the 'U' release if CI is not fixed.
|
|
SUPPORTED = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(NimbleBaseVolumeDriver, self).__init__(*args, **kwargs)
|
|
self.APIExecutor = None
|
|
self.group_stats = {}
|
|
self.api_protocol = None
|
|
self._storage_protocol = None
|
|
self._group_target_enabled = False
|
|
self.configuration.append_config_values(nimble_opts)
|
|
self.verify = False
|
|
if self.configuration.nimble_verify_certificate is True:
|
|
self.verify = self.configuration.nimble_verify_cert_path or True
|
|
|
|
@staticmethod
|
|
def get_driver_options():
|
|
return 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 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,
|
|
self._storage_protocol,
|
|
self._group_target_enabled)
|
|
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
|
|
"""
|
|
vol_info = self.APIExecutor.get_vol_info(volume['name'])
|
|
LOG.debug("is_clone: %(is_clone)s base_snap_id: %(snap)s, "
|
|
"parent_vol_id: %(vol)s",
|
|
{'is_clone': vol_info['clone'],
|
|
'snap': vol_info['base_snap_id'],
|
|
'vol': vol_info['parent_vol_id']})
|
|
|
|
if vol_info['base_snap_id'] and (
|
|
vol_info['parent_vol_id'] is not None):
|
|
LOG.debug("Nimble base-snap exists for volume %(vol)s",
|
|
{'vol': volume['name']})
|
|
volume_name_prefix = volume['name'].replace(volume['id'], "")
|
|
LOG.debug("volume_name_prefix : %(prefix)s",
|
|
{'prefix': volume_name_prefix})
|
|
snap_id = self.APIExecutor.get_snap_info_by_id(
|
|
vol_info['base_snap_id'],
|
|
vol_info['parent_vol_id'])
|
|
snap_info = self.APIExecutor.get_snap_info_detail(snap_id['id'])
|
|
LOG.debug("snap_info description %(snap_info)s",
|
|
{'snap_info': snap_info['description']})
|
|
if snap_info['description'] and BACKUP_VOL_PREFIX in (
|
|
snap_info['description']):
|
|
# TODO(rkumar): get parent vol id from parent volume name
|
|
parent_vol_name = self.APIExecutor.get_volume_name(
|
|
vol_info['parent_vol_id'])
|
|
parent_vol_id = parent_vol_name. replace(
|
|
volume_name_prefix, "")
|
|
if BACKUP_VOL_PREFIX + parent_vol_id in snap_info[
|
|
'description']:
|
|
LOG.info('Nimble backup-snapshot exists name=%('
|
|
'name)s', {'name': snap_info['name']})
|
|
snap_vol_name = self.APIExecutor.get_volume_name(
|
|
snap_info['vol_id'])
|
|
LOG.debug("snap_vol_name %(snap)s",
|
|
{'snap': snap_vol_name})
|
|
return snap_info['name'], snap_vol_name
|
|
return "", ""
|
|
|
|
def delete_volume(self, volume):
|
|
"""Delete the specified volume."""
|
|
backup_snap_name, backup_vol_name = self.is_volume_backup_clone(volume)
|
|
eventlet.sleep(DEFAULT_SLEEP)
|
|
self.APIExecutor.online_vol(volume['name'], False)
|
|
LOG.debug("Deleting volume %(vol)s", {'vol': volume['name']})
|
|
|
|
@utils.retry(NimbleAPIException, retries=3)
|
|
def _retry_remove_vol(volume):
|
|
self.APIExecutor.delete_vol(volume['name'])
|
|
try:
|
|
_retry_remove_vol(volume)
|
|
except NimbleAPIException as ex:
|
|
LOG.debug("delete volume exception: %s", ex)
|
|
if SM_OBJ_HAS_CLONE in six.text_type(ex):
|
|
LOG.warning('Volume %(vol)s : %(state)s',
|
|
{'vol': volume['name'],
|
|
'state': SM_OBJ_HAS_CLONE})
|
|
# set the volume back to be online and raise busy exception
|
|
self.APIExecutor.online_vol(volume['name'], True)
|
|
raise exception.VolumeIsBusy(volume_name=volume['name'])
|
|
raise
|
|
# 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 (backup_snap_name != "" and backup_vol_name != "") and (
|
|
backup_snap_name is not None):
|
|
LOG.debug("Delete volume backup vol: %(vol)s snap: %(snap)s",
|
|
{'vol': backup_vol_name,
|
|
'snap': backup_snap_name})
|
|
self.APIExecutor.online_snap(backup_vol_name,
|
|
False,
|
|
backup_snap_name)
|
|
|
|
self.APIExecutor.delete_snap(backup_vol_name,
|
|
backup_snap_name)
|
|
|
|
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
|
|
pool_name = self.configuration.nimble_pool_name
|
|
self.APIExecutor.clone_vol(volume, snapshot, reserve,
|
|
self._group_target_enabled,
|
|
self._storage_protocol,
|
|
pool_name)
|
|
if(volume['size'] > snapshot['volume_size']):
|
|
vol_size = volume['size'] * units.Ki
|
|
reserve_size = 100 if reserve else 0
|
|
data = {"data": {'size': vol_size,
|
|
'reserve': reserve_size,
|
|
'warn_level': int(WARN_LEVEL),
|
|
'limit': 100,
|
|
'snap_limit': DEFAULT_SNAP_QUOTA}}
|
|
LOG.debug("Edit Vol request %(data)s", {'data': data})
|
|
self.APIExecutor.edit_vol(volume['name'], data)
|
|
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'])
|
|
self.APIExecutor.delete_snap(snapshot['volume_name'],
|
|
snapshot['name'])
|
|
|
|
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 _enable_group_scoped_target(self, group_info):
|
|
if 'version_current' in group_info:
|
|
current_version = group_info['version_current']
|
|
major_minor = current_version.split(".")
|
|
if len(major_minor) >= 3:
|
|
major = major_minor[0]
|
|
minor = major_minor[1]
|
|
# TODO(rkumar): Fix the major version
|
|
if int(major) >= 4 and int(minor) >= 0:
|
|
# Enforce group scoped target
|
|
if 'group_target_enabled' in group_info:
|
|
if group_info['group_target_enabled'] is False:
|
|
try:
|
|
self.APIExecutor.enable_group_scoped_target()
|
|
except Exception:
|
|
raise NimbleAPIException(_("Unable to enable"
|
|
" GST"))
|
|
self._group_target_enabled = True
|
|
LOG.info("Group Scoped Target enabled for "
|
|
"group %(group)s: %(ip)s",
|
|
{'group': group_info['name'],
|
|
'ip': self.configuration.san_ip})
|
|
elif 'group_target_enabled' not in group_info:
|
|
LOG.info("Group Scoped Target NOT "
|
|
"present for group %(group)s: "
|
|
"%(ip)s",
|
|
{'group': group_info['name'],
|
|
'ip': self.configuration.san_ip})
|
|
else:
|
|
raise NimbleAPIException(_("Unable to get current software "
|
|
"version for %s") %
|
|
self.configuration.san_ip)
|
|
|
|
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_info()
|
|
if 'usage_valid' not in group_info:
|
|
raise NimbleDriverException(_('SpaceInfo returned by '
|
|
'array is invalid'))
|
|
total_capacity = (group_info['usable_capacity_bytes'] /
|
|
float(units.Gi))
|
|
used_space = ((group_info['compressed_vol_usage_bytes'] +
|
|
group_info['compressed_snap_usage_bytes'] +
|
|
group_info['unused_reserve_bytes']) /
|
|
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': self._storage_protocol}
|
|
# 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('Entering extend_volume volume=%(vol)s '
|
|
'new_size=%(size)s',
|
|
{'vol': volume_name, 'size': new_size})
|
|
vol_size = int(new_size) * units.Ki
|
|
reserve = not self.configuration.san_thin_provision
|
|
reserve_size = 100 if reserve else 0
|
|
LOG.debug("new volume size in MB (size)s", {'size': vol_size})
|
|
data = {"data": {'size': vol_size,
|
|
'reserve': reserve_size,
|
|
'warn_level': int(WARN_LEVEL),
|
|
'limit': 100,
|
|
'snap_limit': DEFAULT_SNAP_QUOTA}}
|
|
self.APIExecutor.edit_vol(volume_name, data)
|
|
|
|
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 _get_volumetype_extraspecs_with_type(self, type_id):
|
|
specs = {}
|
|
if type_id is not None:
|
|
specs = volume_types.get_volume_type_extra_specs(type_id)
|
|
return specs
|
|
|
|
def retype(self, context, volume, new_type, diff, host):
|
|
"""Retype from one volume type to another.
|
|
|
|
At this point HPE Nimble Storage does not differentiate between
|
|
volume types on the same array. This is a no-op for us if there are
|
|
no extra specs else honor the extra-specs.
|
|
"""
|
|
if new_type is None:
|
|
return True, None
|
|
LOG.debug("retype called with volume_type %s", new_type)
|
|
|
|
volume_type_id = new_type['id']
|
|
if volume_type_id is None:
|
|
raise NimbleAPIException(_("No volume_type_id present in"
|
|
" %(type)s") % {'type': new_type})
|
|
|
|
LOG.debug("volume_type id is %s", volume_type_id)
|
|
specs_map = self._get_volumetype_extraspecs_with_type(
|
|
volume_type_id)
|
|
if specs_map is None:
|
|
# no extra specs to retype
|
|
LOG.debug("volume_type %s has no extra specs", volume_type_id)
|
|
return True, None
|
|
vol_info = self.APIExecutor.get_vol_info(volume['name'])
|
|
LOG.debug("new extra specs %s", specs_map)
|
|
data = self.APIExecutor.get_valid_nimble_extraspecs(specs_map,
|
|
vol_info)
|
|
if data is None:
|
|
# return if there is no update
|
|
LOG.debug("no data to update for %s", new_type)
|
|
return True, None
|
|
try:
|
|
# offline the volume before edit
|
|
self.APIExecutor.online_vol(volume['name'], False)
|
|
# modify the volume
|
|
LOG.debug("updated volume %s", data)
|
|
self.APIExecutor.edit_vol(volume['name'], data)
|
|
# make the volume online after changing the specs
|
|
self.APIExecutor.online_vol(volume['name'], True)
|
|
except NimbleAPIException as ex:
|
|
raise NimbleAPIException(_("Unable to retype %(vol)s to "
|
|
"%(type)s: %(err)s") %
|
|
{'vol': volume['name'],
|
|
'type': new_type,
|
|
'err': ex.message})
|
|
return True, None
|
|
|
|
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 or (
|
|
vol_info['agent_type'] == AGENT_TYPE_OPENSTACK_GST):
|
|
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']
|
|
LOG.info("Volume status before managing it : %(status)s",
|
|
{'status': vol_info['online']})
|
|
if vol_info['online'] is True:
|
|
msg = (_('Volume %s is online. Set volume to offline for '
|
|
'managing using OpenStack.') % target_vol_name)
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
# edit the volume
|
|
data = {'data': {'name': new_vol_name}}
|
|
if self._group_target_enabled is True:
|
|
# check if any ACL's are attached to this volume
|
|
if 'access_control_records' in vol_info and (
|
|
vol_info['access_control_records'] is not None):
|
|
msg = (_('Volume %s has ACL associated with it. Remove ACL '
|
|
'for managing using OpenStack') % target_vol_name)
|
|
raise exception.InvalidVolume(reason=msg)
|
|
data['data']['agent_type'] = AGENT_TYPE_OPENSTACK_GST
|
|
else:
|
|
data['data']['agent_type'] = AGENT_TYPE_OPENSTACK
|
|
|
|
LOG.debug("Data for edit %(data)s", {'data': data})
|
|
self.APIExecutor.edit_vol(target_vol_name, data)
|
|
|
|
# make the volume online after rename
|
|
self.APIExecutor.online_vol(new_vol_name, True)
|
|
|
|
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(vol_info['size'] / units.Ki)
|
|
|
|
def unmanage(self, volume):
|
|
"""Removes the specified volume from Cinder management."""
|
|
|
|
vol_name = volume['name']
|
|
LOG.debug("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 and (
|
|
vol_info['agent_type'] != AGENT_TYPE_OPENSTACK_GST):
|
|
msg = (_('Only volumes managed by OpenStack can be unmanaged.'))
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
data = {'data': {'agent_type': AGENT_TYPE_NONE}}
|
|
# update the agent-type to None
|
|
self.APIExecutor.edit_vol(vol_name, data)
|
|
|
|
# offline the volume
|
|
self.APIExecutor.online_vol(vol_name, False)
|
|
|
|
def do_setup(self, context):
|
|
"""Setup the Nimble Cinder volume driver."""
|
|
self._check_config()
|
|
# Setup API Executor
|
|
try:
|
|
self.APIExecutor = NimbleRestAPIExecutor(
|
|
username=self.configuration.san_login,
|
|
password=self.configuration.san_password,
|
|
ip=self.configuration.san_ip,
|
|
verify=self.verify)
|
|
if self._storage_protocol == "iSCSI":
|
|
group_info = self.APIExecutor.get_group_info()
|
|
self._enable_group_scoped_target(group_info)
|
|
except Exception:
|
|
LOG.error('Failed to create REST client. '
|
|
'Check san_ip, username, password'
|
|
' and make sure the array version is compatible')
|
|
raise
|
|
self._update_existing_vols_agent_type(context)
|
|
|
|
def _update_existing_vols_agent_type(self, context):
|
|
backend_name = self.configuration.safe_get('volume_backend_name')
|
|
all_vols = volume.VolumeList.get_all(
|
|
context, None, None, None, None, {'status': 'available'})
|
|
for vol in all_vols:
|
|
if backend_name in vol.host:
|
|
try:
|
|
vol_info = self.APIExecutor.get_vol_info(vol.name)
|
|
# update agent_type only if no ACL's are present
|
|
if 'access_control_records' in vol_info and (
|
|
vol_info['access_control_records'] is None):
|
|
if self._group_target_enabled:
|
|
LOG.debug("Updating %(vol)s to have agent_type :"
|
|
"%(agent)s",
|
|
{'vol': vol.name,
|
|
'agent': AGENT_TYPE_OPENSTACK_GST})
|
|
# check if this is an upgrade case from
|
|
# openstack to openstackv2
|
|
if vol_info['agent_type'] == AGENT_TYPE_NONE:
|
|
data = {'data': {'agent_type':
|
|
AGENT_TYPE_OPENSTACK_GST}}
|
|
self.APIExecutor.edit_vol(vol.name, data)
|
|
elif vol_info['agent_type'] == (
|
|
AGENT_TYPE_OPENSTACK):
|
|
# 1. update the agent type to None
|
|
data = {'data': {'agent_type':
|
|
AGENT_TYPE_NONE}}
|
|
self.APIExecutor.edit_vol(vol.name, data)
|
|
# 2. update the agent type to openstack_gst
|
|
data = {'data': {'agent_type':
|
|
AGENT_TYPE_OPENSTACK_GST}}
|
|
self.APIExecutor.edit_vol(vol.name, data)
|
|
else:
|
|
LOG.debug("Updating %(vol)s to have agent_type :"
|
|
"%(agent)s",
|
|
{'vol': vol.name,
|
|
'agent': AGENT_TYPE_OPENSTACK_GST})
|
|
if vol_info['agent_type'] == AGENT_TYPE_NONE:
|
|
data = {'data': {'agent_type':
|
|
AGENT_TYPE_OPENSTACK}}
|
|
self.APIExecutor.edit_vol(vol.name, data)
|
|
elif vol_info['agent_type'] == (
|
|
AGENT_TYPE_OPENSTACK_GST):
|
|
# 1. update the agent type to None
|
|
data = {'data': {'agent_type':
|
|
AGENT_TYPE_NONE}}
|
|
self.APIExecutor.edit_vol(vol.name, data)
|
|
# 2. update the agent type to openstack
|
|
data = {'data': {'agent_type':
|
|
AGENT_TYPE_OPENSTACK}}
|
|
self.APIExecutor.edit_vol(vol.name, data)
|
|
except NimbleAPIException:
|
|
# just log the error but don't fail driver initialization
|
|
LOG.warning('Error updating agent-type for '
|
|
'volume %s.', vol.name)
|
|
|
|
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})
|
|
|
|
@abc.abstractmethod
|
|
def _get_provider_location(self, volume_name):
|
|
"""Volume info for iSCSI and FC"""
|
|
|
|
pass
|
|
|
|
def _create_igroup_for_initiator(self, initiator_name, wwpns):
|
|
"""Creates igroup for an initiator and returns the igroup name."""
|
|
igrp_name = 'openstack-' + self._generate_random_string(12)
|
|
LOG.info('Creating initiator group %(grp)s '
|
|
'with initiator %(iname)s',
|
|
{'grp': igrp_name, 'iname': initiator_name})
|
|
if self._storage_protocol == "iSCSI":
|
|
self.APIExecutor.create_initiator_group(igrp_name)
|
|
self.APIExecutor.add_initiator_to_igroup(igrp_name, initiator_name)
|
|
elif self._storage_protocol == "FC":
|
|
self.APIExecutor.create_initiator_group_fc(igrp_name)
|
|
for wwpn in wwpns:
|
|
self.APIExecutor.add_initiator_to_igroup_fc(igrp_name, wwpn)
|
|
return igrp_name
|
|
|
|
def _get_igroupname_for_initiator_fc(self, initiator_wwpns):
|
|
initiator_groups = self.APIExecutor.get_initiator_grp_list()
|
|
for initiator_group in initiator_groups:
|
|
if 'fc_initiators' in initiator_group and initiator_group[
|
|
'fc_initiators'] is not None:
|
|
wwpns_list = []
|
|
for initiator in initiator_group['fc_initiators']:
|
|
wwpn = str(initiator['wwpn']).replace(":", "")
|
|
wwpns_list.append(wwpn)
|
|
LOG.debug("initiator_wwpns=%(initiator)s "
|
|
"wwpns_list_from_array=%(wwpns)s",
|
|
{'initiator': initiator_wwpns,
|
|
'wwpns': wwpns_list})
|
|
if set(initiator_wwpns) == set(wwpns_list):
|
|
LOG.info('igroup %(grp)s found for '
|
|
'initiator %(wwpns_list)s',
|
|
{'grp': initiator_group['name'],
|
|
'wwpns_list': wwpns_list})
|
|
return initiator_group['name']
|
|
LOG.info('No igroup found for initiators %s', initiator_wwpns)
|
|
return ''
|
|
|
|
def _get_igroupname_for_initiator(self, initiator_name):
|
|
initiator_groups = self.APIExecutor.get_initiator_grp_list()
|
|
for initiator_group in initiator_groups:
|
|
if initiator_group['iscsi_initiators'] is not None:
|
|
if (len(initiator_group['iscsi_initiators']) == 1 and
|
|
initiator_group['iscsi_initiators'][0]['iqn'] ==
|
|
initiator_name):
|
|
LOG.info('igroup %(grp)s found for '
|
|
'initiator %(iname)s',
|
|
{'grp': initiator_group['name'],
|
|
'iname': initiator_name})
|
|
return initiator_group['name']
|
|
LOG.info('No igroup found for initiator %s', initiator_name)
|
|
return ''
|
|
|
|
def get_lun_number(self, volume, initiator_group_name):
|
|
vol_info = self.APIExecutor.get_vol_info(volume['name'])
|
|
for acl in vol_info['access_control_records']:
|
|
if (initiator_group_name == acl['initiator_group_name']):
|
|
LOG.info("access_control_record =%(acl)s",
|
|
{'acl': acl})
|
|
lun = acl['lun']
|
|
LOG.info("LUN : %(lun)s", {"lun": lun})
|
|
return lun
|
|
raise NimbleAPIException(_("Lun number not found for volume %(vol)s "
|
|
"with initiator_group: %(igroup)s") %
|
|
{'vol': volume['name'],
|
|
'igroup': initiator_group_name})
|
|
|
|
|
|
@interface.volumedriver
|
|
class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver):
|
|
|
|
"""OpenStack driver to enable Nimble ISCSI Controller."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(NimbleISCSIDriver, self).__init__(*args, **kwargs)
|
|
self._storage_protocol = "iSCSI"
|
|
self._group_target_name = None
|
|
|
|
def _set_gst_for_group(self):
|
|
group_info = self.APIExecutor.get_group_info()
|
|
if 'group_target_enabled' in group_info and (
|
|
group_info['group_target_enabled']) is True and (
|
|
'group_target_name' in group_info) and (
|
|
group_info['group_target_name'] is not None):
|
|
self._group_target_name = group_info['group_target_name']
|
|
|
|
def _get_gst_for_group(self):
|
|
return self._group_target_name
|
|
|
|
def initialize_connection(self, volume, connector):
|
|
"""Driver entry point to attach a volume to an instance."""
|
|
LOG.info('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, None)
|
|
LOG.info('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)
|
|
properties = {"driver_volume_type": "iscsi",
|
|
"data": {"target_discovered": False},
|
|
}
|
|
properties['data']['volume_id'] = volume['id'] # used by xen currently
|
|
(iscsi_portal, iqn) = volume['provider_location'].split()
|
|
if self._get_gst_for_group() is not None:
|
|
lun_num = self.get_lun_number(volume, initiator_group_name)
|
|
netconfig = self.APIExecutor.get_netconfig('active')
|
|
target_portals = self._get_data_ips(netconfig)
|
|
LOG.info("target portals %(portals)s", {'portals': target_portals})
|
|
target_luns = [int(lun_num)] * len(target_portals)
|
|
target_iqns = [iqn] * len(target_portals)
|
|
LOG.debug("target iqns %(iqns)s target luns %(luns)s",
|
|
{'iqns': target_iqns, 'luns': target_luns})
|
|
if target_luns and target_iqns and target_portals:
|
|
properties["data"]["target_luns"] = target_luns
|
|
properties["data"]["target_iqns"] = target_iqns
|
|
properties["data"]["target_portals"] = target_portals
|
|
else:
|
|
# handling volume scoped target
|
|
lun_num = LUN_ID
|
|
properties['data']['target_portal'] = iscsi_portal
|
|
properties['data']['target_iqn'] = iqn
|
|
properties['data']['target_lun'] = int(lun_num)
|
|
|
|
return properties
|
|
|
|
def terminate_connection(self, volume, connector, **kwargs):
|
|
"""Driver entry point to unattach a volume from an instance."""
|
|
LOG.info('Entering terminate_connection volume=%(vol)s'
|
|
' connector=%(conn)s location=%(loc)s.',
|
|
{'vol': volume['name'],
|
|
'conn': connector,
|
|
'loc': volume['provider_location']})
|
|
|
|
if connector is None:
|
|
LOG.warning("Removing ALL host connections for volume %s",
|
|
volume)
|
|
self.APIExecutor.remove_all_acls(volume)
|
|
return
|
|
|
|
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)
|
|
eventlet.sleep(DEFAULT_SLEEP)
|
|
|
|
def _get_provider_location(self, volume_name):
|
|
"""Get volume iqn for initiator access."""
|
|
vol_info = self.APIExecutor.get_vol_info(volume_name)
|
|
netconfig = self.APIExecutor.get_netconfig('active')
|
|
self._set_gst_for_group()
|
|
if self._get_gst_for_group() is not None:
|
|
iqn = self._get_gst_for_group()
|
|
else:
|
|
iqn = vol_info['target_name']
|
|
target_ipaddr = self._get_discovery_ip(netconfig)
|
|
iscsi_portal = target_ipaddr + ':3260'
|
|
provider_location = '%s %s' % (iscsi_portal, iqn)
|
|
LOG.info('vol_name=%(name)s provider_location=%(loc)s',
|
|
{'name': volume_name, 'loc': provider_location})
|
|
return provider_location
|
|
|
|
def _get_data_ips(self, netconfig):
|
|
"""Get data ips."""
|
|
subnet_label = self.configuration.nimble_subnet_label
|
|
LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s',
|
|
{'netlabel': subnet_label, 'netconf': netconfig})
|
|
ret_data_ips = []
|
|
for subnet in netconfig['array_list'][0]['nic_list']:
|
|
LOG.info('Exploring array subnet label %s', subnet[
|
|
'subnet_label'])
|
|
if subnet['data_ip']:
|
|
if subnet_label == '*':
|
|
# if all subnets are mentioned then return all portals
|
|
# else just return specific subnet
|
|
LOG.info('Data ip %(data_ip)s is used '
|
|
'on data subnet %(net_label)s',
|
|
{'data_ip': subnet['data_ip'],
|
|
'net_label': subnet['subnet_label']})
|
|
ret_data_ips.append(str(subnet['data_ip']) + ':3260')
|
|
elif subnet_label == subnet['subnet_label']:
|
|
LOG.info('Data ip %(data_ip)s is used'
|
|
' on subnet %(net_label)s',
|
|
{'data_ip': subnet['data_ip'],
|
|
'net_label': subnet['subnet_label']})
|
|
data_ips_single_subnet = []
|
|
data_ips_single_subnet.append(str(subnet['data_ip']) +
|
|
':3260')
|
|
return data_ips_single_subnet
|
|
if ret_data_ips:
|
|
LOG.info('Data ips %s', ret_data_ips)
|
|
return ret_data_ips
|
|
else:
|
|
raise NimbleDriverException(_('No suitable data ip found'))
|
|
|
|
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('Exploring array subnet label %s', subnet['label'])
|
|
if subnet_label == '*':
|
|
# Use the first data subnet, save mgmt+data for later
|
|
if subnet['type'] == SM_SUBNET_DATA:
|
|
LOG.info('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['type'] == SM_SUBNET_MGMT_PLUS_DATA):
|
|
LOG.info('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('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('Discovery ip %s is used on mgmt+data subnet',
|
|
ret_discovery_ip)
|
|
return ret_discovery_ip
|
|
else:
|
|
raise NimbleDriverException(_('No suitable discovery ip found'))
|
|
|
|
|
|
@interface.volumedriver
|
|
class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver):
|
|
"""OpenStack driver to enable Nimble FC Driver Controller."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(NimbleFCDriver, self).__init__(*args, **kwargs)
|
|
self._storage_protocol = "FC"
|
|
self._lookup_service = fczm_utils.create_lookup_service()
|
|
|
|
def _get_provider_location(self, volume_name):
|
|
"""Get array info wwn details."""
|
|
netconfig = self.APIExecutor.get_netconfig('active')
|
|
array_name = netconfig['group_leader_array']
|
|
provider_location = '%s' % (array_name)
|
|
LOG.info('vol_name=%(name)s provider_location=%(loc)s',
|
|
{'name': volume_name, 'loc': provider_location})
|
|
return provider_location
|
|
|
|
def _build_initiator_target_map(self, target_wwns, connector):
|
|
"""Build the target_wwns and the initiator target map."""
|
|
LOG.debug("_build_initiator_target_map for %(wwns)s",
|
|
{'wwns': target_wwns})
|
|
init_targ_map = {}
|
|
|
|
if self._lookup_service:
|
|
# use FC san lookup to determine which wwpns to use
|
|
# for the new VLUN.
|
|
dev_map = self._lookup_service.get_device_mapping_from_network(
|
|
connector['wwpns'],
|
|
target_wwns)
|
|
map_fabric = dev_map
|
|
LOG.info("dev_map =%(fabric)s", {'fabric': map_fabric})
|
|
|
|
for fabric_name in dev_map:
|
|
fabric = dev_map[fabric_name]
|
|
for initiator in fabric['initiator_port_wwn_list']:
|
|
if initiator not in init_targ_map:
|
|
init_targ_map[initiator] = []
|
|
init_targ_map[initiator] += fabric['target_port_wwn_list']
|
|
init_targ_map[initiator] = list(set(
|
|
init_targ_map[initiator]))
|
|
else:
|
|
init_targ_map = dict.fromkeys(connector["wwpns"], target_wwns)
|
|
|
|
return init_targ_map
|
|
|
|
def initialize_connection(self, volume, connector):
|
|
"""Driver entry point to attach a volume to an instance."""
|
|
LOG.info('Entering initialize_connection volume=%(vol)s'
|
|
' connector=%(conn)s location=%(loc)s',
|
|
{'vol': volume,
|
|
'conn': connector,
|
|
'loc': volume['provider_location']})
|
|
wwpns = []
|
|
initiator_name = connector['initiator']
|
|
for wwpn in connector['wwpns']:
|
|
wwpns.append(wwpn)
|
|
initiator_group_name = self._get_igroupname_for_initiator_fc(wwpns)
|
|
|
|
if not initiator_group_name:
|
|
initiator_group_name = self._create_igroup_for_initiator(
|
|
initiator_name, wwpns)
|
|
|
|
LOG.info('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)
|
|
lun = self.get_lun_number(volume, initiator_group_name)
|
|
init_targ_map = {}
|
|
(array_name) = volume['provider_location'].split()
|
|
|
|
target_wwns = self.get_wwpns_from_array(array_name)
|
|
|
|
init_targ_map = self._build_initiator_target_map(target_wwns,
|
|
connector)
|
|
|
|
data = {'driver_volume_type': 'fibre_channel',
|
|
'data': {'target_lun': lun,
|
|
'target_discovered': True,
|
|
'target_wwn': target_wwns,
|
|
'initiator_target_map': init_targ_map}}
|
|
|
|
LOG.info("Return FC data for zone addition: %(data)s.",
|
|
{'data': data})
|
|
fczm_utils.add_fc_zone(data)
|
|
return data
|
|
|
|
def terminate_connection(self, volume, connector, **kwargs):
|
|
"""Driver entry point to unattach a volume from an instance."""
|
|
LOG.info('Entering terminate_connection volume=%(vol)s'
|
|
' connector=%(conn)s location=%(loc)s.',
|
|
{'vol': volume,
|
|
'conn': connector,
|
|
'loc': volume['provider_location']})
|
|
wwpns = []
|
|
if connector is None:
|
|
LOG.warning("Removing ALL host connections for volume %s",
|
|
volume)
|
|
self.APIExecutor.remove_all_acls(volume)
|
|
return
|
|
|
|
initiator_name = connector['initiator']
|
|
for wwpn in connector['wwpns']:
|
|
wwpns.append(wwpn)
|
|
(array_name) = volume['provider_location'].split()
|
|
target_wwns = self.get_wwpns_from_array(array_name)
|
|
init_targ_map = self._build_initiator_target_map(target_wwns,
|
|
connector)
|
|
initiator_group_name = self._get_igroupname_for_initiator_fc(wwpns)
|
|
if not initiator_group_name:
|
|
raise NimbleDriverException(
|
|
_('No initiator group found for initiator %s') %
|
|
initiator_name)
|
|
LOG.debug("initiator_target_map %s", init_targ_map)
|
|
self.APIExecutor.remove_acl(volume, initiator_group_name)
|
|
eventlet.sleep(DEFAULT_SLEEP)
|
|
# FIXME to check for other volumes attached to the host and then
|
|
# return the data. Bug https://bugs.launchpad.net/cinder/+bug/1617472
|
|
|
|
data = {'driver_volume_type': 'fibre_channel',
|
|
'data': {'target_wwn': target_wwns}}
|
|
|
|
# FIXME: need to optionally add the initiator_target_map here when
|
|
# there are no more volumes exported to the initiator / target pair
|
|
# otherwise the zone will never get removed.
|
|
fczm_utils.remove_fc_zone(data)
|
|
return data
|
|
|
|
def get_wwpns_from_array(self, array_name):
|
|
"""Retrieve the wwpns from the array"""
|
|
LOG.debug("get_wwpns_from_array %s", array_name)
|
|
target_wwpns = []
|
|
interface_info = self.APIExecutor.get_fc_interface_list(array_name)
|
|
LOG.info("interface_info %(interface_info)s",
|
|
{"interface_info": interface_info})
|
|
for wwpn_list in interface_info:
|
|
wwpn = wwpn_list['wwpn']
|
|
wwpn = wwpn.replace(":", "")
|
|
target_wwpns.append(wwpn)
|
|
|
|
return target_wwpns
|
|
|
|
def _convert_string_to_colon_separated_wwnn(self, wwnn):
|
|
return ':'.join(a + b for a, b in zip(wwnn[::2], wwnn[1::2]))
|
|
|
|
|
|
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 Exception as e:
|
|
if attempts < 1 and (re.search("Failed to execute",
|
|
six.text_type(e))):
|
|
LOG.info('Session might have expired.'
|
|
' Trying to relogin')
|
|
self.login()
|
|
continue
|
|
else:
|
|
LOG.error('Re-throwing Exception %s', e)
|
|
raise
|
|
return inner_connection_checker
|
|
|
|
|
|
class NimbleRestAPIExecutor(object):
|
|
|
|
"""Makes Nimble REST API calls."""
|
|
|
|
def __init__(self, api_version=NimbleDefaultVersion, *args, **kwargs):
|
|
self.token_id = None
|
|
self.ip = kwargs['ip']
|
|
self.username = kwargs['username']
|
|
self.password = kwargs['password']
|
|
self.verify = kwargs['verify']
|
|
self.api_version = api_version
|
|
self.uri = "https://%(ip)s:5392/v%(version)s/" % {
|
|
'ip': self.ip,
|
|
'version': self.api_version}
|
|
self.login()
|
|
|
|
def login(self):
|
|
data = {'data': {"username": self.username,
|
|
"password": self.password,
|
|
"app_name": "NimbleCinderDriver"}}
|
|
r = requests.post(self.uri + "tokens",
|
|
data=json.dumps(data),
|
|
verify=self.verify)
|
|
|
|
if r.status_code != 201 and r.status_code != 200:
|
|
msg = _("Failed to login for user %s"), self.username
|
|
raise NimbleAPIException(msg)
|
|
self.token_id = r.json()['data']['session_token']
|
|
self.headers = {'X-Auth-Token': self.token_id}
|
|
|
|
def get_group_id(self):
|
|
api = 'groups'
|
|
r = self.get(api)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve Group Object for : "
|
|
"%s") % self.ip)
|
|
return r.json()['data'][0]['id']
|
|
|
|
def get_group_info(self):
|
|
group_id = self.get_group_id()
|
|
api = 'groups/' + six.text_type(group_id)
|
|
r = self.get(api)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve Group info for: %s")
|
|
% group_id)
|
|
return r.json()['data']
|
|
|
|
def get_folder_id(self, folder_name):
|
|
api = 'folders'
|
|
filter = {"name": folder_name}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve information for "
|
|
"Folder: %s") % folder_name)
|
|
return r.json()['data'][0]['id']
|
|
|
|
def get_folder_info(self, folder_name):
|
|
folder_id = self.get_folder_id(folder_name)
|
|
api = "folders/" + six.text_type(folder_id)
|
|
r = self.get(api)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve Folder info for: "
|
|
"%s") % folder_id)
|
|
return r.json()['data']
|
|
|
|
def get_performance_policy_id(self, perf_policy_name):
|
|
api = 'performance_policies/'
|
|
filter = {'name': perf_policy_name}
|
|
LOG.debug("Performance policy Name %s", perf_policy_name)
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("No performance policy found for: "
|
|
"%(perf)s") % {'perf': perf_policy_name})
|
|
LOG.debug("Performance policy ID :%(perf)s",
|
|
{'perf': r.json()['data'][0]['id']})
|
|
return r.json()['data'][0]['id']
|
|
|
|
def get_netconfig(self, role):
|
|
api = "network_configs/detail"
|
|
filter = {'role': role}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("No %s network config exists") % role)
|
|
return r.json()['data'][0]
|
|
|
|
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)
|
|
iops_limit = extra_specs.get(EXTRA_SPEC_IOPS_LIMIT,
|
|
DEFAULT_IOPS_LIMIT_SETTING)
|
|
folder_name = extra_specs.get(EXTRA_SPEC_FOLDER,
|
|
DEFAULT_FOLDER_SETTING)
|
|
dedupe = extra_specs.get(EXTRA_SPEC_DEDUPE,
|
|
DEFAULT_DEDUPE_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
|
|
extra_specs_map[EXTRA_SPEC_IOPS_LIMIT] = iops_limit
|
|
extra_specs_map[EXTRA_SPEC_DEDUPE] = dedupe
|
|
extra_specs_map[EXTRA_SPEC_FOLDER] = folder_name
|
|
|
|
return extra_specs_map
|
|
|
|
def get_valid_nimble_extraspecs(self, extra_specs_map, vol_info):
|
|
|
|
extra_specs_map_updated = self._get_extra_spec_values(extra_specs_map)
|
|
data = {"data": {}}
|
|
perf_policy_name = extra_specs_map_updated[EXTRA_SPEC_PERF_POLICY]
|
|
perf_policy_id = self.get_performance_policy_id(perf_policy_name)
|
|
data['perfpolicy_id'] = perf_policy_id
|
|
|
|
encrypt = extra_specs_map_updated[EXTRA_SPEC_ENCRYPTION]
|
|
cipher = DEFAULT_CIPHER
|
|
if encrypt.lower() == 'yes':
|
|
cipher = AES_256_XTS_CIPHER
|
|
data['cipher'] = cipher
|
|
|
|
multi_initiator = extra_specs_map_updated[EXTRA_SPEC_MULTI_INITIATOR]
|
|
data['multi_initiator'] = multi_initiator
|
|
|
|
folder_name = extra_specs_map_updated[EXTRA_SPEC_FOLDER]
|
|
folder_id = None
|
|
pool_id = vol_info['pool_id']
|
|
pool_name = vol_info['pool_name']
|
|
if folder_name is not None:
|
|
# validate if folder exists in pool_name
|
|
pool_info = self.get_pool_info(pool_id)
|
|
if 'folder_list' in pool_info and (pool_info['folder_list'] is
|
|
not None):
|
|
for folder_list in pool_info['folder_list']:
|
|
LOG.debug("folder_list : %s", folder_list)
|
|
if folder_list['fqn'] == "/" + folder_name:
|
|
LOG.debug("Folder %(folder)s present in pool "
|
|
"%(pool)s",
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
folder_id = self.get_folder_id(folder_name)
|
|
if folder_id is not None:
|
|
data['data']["folder_id"] = folder_id
|
|
if folder_id is None:
|
|
raise NimbleAPIException(_("Folder '%(folder)s' not "
|
|
"present in pool '%("
|
|
"pool)s'") %
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
else:
|
|
raise NimbleAPIException(_(
|
|
"Folder '%(folder)s' not present in pool '%(pool)s'")
|
|
% {'folder': folder_name,
|
|
'pool': pool_name})
|
|
iops_limit = extra_specs_map_updated[EXTRA_SPEC_IOPS_LIMIT]
|
|
if iops_limit is not None:
|
|
if not iops_limit.isdigit() or (
|
|
int(iops_limit) < MIN_IOPS) or (int(iops_limit) > MAX_IOPS):
|
|
raise NimbleAPIException(_("%(err)s [%(min)s, %(max)s]")
|
|
% {'err': IOPS_ERR_MSG,
|
|
'min': MIN_IOPS,
|
|
'max': MAX_IOPS})
|
|
|
|
data['data']['limit_iops'] = iops_limit
|
|
|
|
dedupe = extra_specs_map_updated[EXTRA_SPEC_DEDUPE]
|
|
if dedupe.lower() == 'true':
|
|
data['data']['dedupe_enabled'] = True
|
|
|
|
return data
|
|
|
|
def create_vol(self, volume, pool_name, reserve, protocol, is_gst_enabled):
|
|
response = self._execute_create_vol(volume, pool_name, reserve,
|
|
protocol, is_gst_enabled)
|
|
LOG.info('Successfully created volume %(name)s',
|
|
{'name': response['name']})
|
|
return response['name']
|
|
|
|
def _is_ascii(self, value):
|
|
try:
|
|
return all(ord(c) < 128 for c in value)
|
|
except TypeError:
|
|
return False
|
|
|
|
def _execute_create_vol(self, volume, pool_name, reserve, protocol,
|
|
is_gst_enabled):
|
|
"""Create volume
|
|
|
|
:return: r['data']
|
|
"""
|
|
|
|
# Set volume size, display name and description
|
|
volume_size = volume['size'] * units.Ki
|
|
reserve_size = 100 if reserve else 0
|
|
# Set volume description
|
|
display_name = getattr(volume, 'display_name', '')
|
|
display_description = getattr(volume, 'display_description', '')
|
|
if self._is_ascii(display_name) and self._is_ascii(
|
|
display_description):
|
|
display_list = [getattr(volume, 'display_name', ''),
|
|
getattr(volume, 'display_description', '')]
|
|
description = ':'.join(filter(None, display_list))
|
|
elif self._is_ascii(display_name):
|
|
description = display_name
|
|
elif self._is_ascii(display_description):
|
|
description = display_description
|
|
else:
|
|
description = ""
|
|
|
|
# Limit description size to 254 characters
|
|
description = description[:254]
|
|
pool_id = self.get_pool_id(pool_name)
|
|
|
|
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]
|
|
perf_policy_id = self.get_performance_policy_id(perf_policy_name)
|
|
encrypt = extra_specs_map[EXTRA_SPEC_ENCRYPTION]
|
|
multi_initiator = extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR]
|
|
folder_name = extra_specs_map[EXTRA_SPEC_FOLDER]
|
|
iops_limit = extra_specs_map[EXTRA_SPEC_IOPS_LIMIT]
|
|
dedupe = extra_specs_map[EXTRA_SPEC_DEDUPE]
|
|
|
|
cipher = DEFAULT_CIPHER
|
|
if encrypt.lower() == 'yes':
|
|
cipher = AES_256_XTS_CIPHER
|
|
if is_gst_enabled is True:
|
|
agent_type = AGENT_TYPE_OPENSTACK_GST
|
|
else:
|
|
agent_type = AGENT_TYPE_OPENSTACK
|
|
|
|
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_size,
|
|
'pool': pool_name,
|
|
'description': description,
|
|
'perfpol-name': perf_policy_name,
|
|
'encryption': encrypt,
|
|
'cipher': cipher,
|
|
'agent-type': agent_type,
|
|
'multi-initiator': multi_initiator})
|
|
data = {"data":
|
|
{'name': volume['name'],
|
|
'description': description,
|
|
'size': volume_size,
|
|
'reserve': reserve_size,
|
|
'warn_level': int(WARN_LEVEL),
|
|
'limit': 100,
|
|
'snap_limit': DEFAULT_SNAP_QUOTA,
|
|
'online': True,
|
|
'pool_id': pool_id,
|
|
'agent_type': agent_type,
|
|
'perfpolicy_id': perf_policy_id,
|
|
'encryption_cipher': cipher}}
|
|
|
|
if protocol == "iSCSI":
|
|
data['data']['multi_initiator'] = multi_initiator
|
|
|
|
if dedupe.lower() == 'true':
|
|
data['data']['dedupe_enabled'] = True
|
|
|
|
folder_id = None
|
|
if folder_name is not None:
|
|
# validate if folder exists in pool_name
|
|
pool_info = self.get_pool_info(pool_id)
|
|
if 'folder_list' in pool_info and (pool_info['folder_list'] is
|
|
not None):
|
|
for folder_list in pool_info['folder_list']:
|
|
LOG.debug("folder_list : %s", folder_list)
|
|
if folder_list['fqn'] == "/" + folder_name:
|
|
LOG.debug("Folder %(folder)s present in pool "
|
|
"%(pool)s",
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
folder_id = self.get_folder_id(folder_name)
|
|
if folder_id is not None:
|
|
data['data']["folder_id"] = folder_id
|
|
if folder_id is None:
|
|
raise NimbleAPIException(_("Folder '%(folder)s' not "
|
|
"present in pool '%(pool)s'") %
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
else:
|
|
raise NimbleAPIException(_("Folder '%(folder)s' not present in"
|
|
" pool '%(pool)s'") %
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
|
|
if iops_limit is not None:
|
|
if not iops_limit.isdigit() or (
|
|
int(iops_limit) < MIN_IOPS) or (int(iops_limit) > MAX_IOPS):
|
|
raise NimbleAPIException(_("%(err)s [%(min)s, %(max)s]") %
|
|
{'err': IOPS_ERR_MSG,
|
|
'min': MIN_IOPS,
|
|
'max': MAX_IOPS})
|
|
|
|
data['data']['limit_iops'] = iops_limit
|
|
|
|
LOG.debug("Volume metadata :%s", volume.metadata)
|
|
for key, value in volume.metadata.items():
|
|
LOG.debug("Key %(key)s Value %(value)s",
|
|
{'key': key, 'value': value})
|
|
if key == EXTRA_SPEC_IOPS_LIMIT and value.isdigit():
|
|
if type(value) == int or int(value) < MIN_IOPS or (
|
|
int(value) > MAX_IOPS):
|
|
raise NimbleAPIException(_("%(err)s [%(min)s, %(max)s]") %
|
|
{'err': IOPS_ERR_MSG,
|
|
'min': MIN_IOPS,
|
|
'max': MAX_IOPS})
|
|
LOG.debug("IOPS Limit %s", value)
|
|
data['data']['limit_iops'] = value
|
|
LOG.debug("Data : %s", data)
|
|
|
|
api = 'volumes'
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def create_initiator_group(self, initiator_grp_name):
|
|
api = "initiator_groups"
|
|
data = {"data": {"name": initiator_grp_name,
|
|
"access_protocol": "iscsi",
|
|
}}
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def create_initiator_group_fc(self, initiator_grp_name):
|
|
api = "initiator_groups"
|
|
|
|
data = {}
|
|
data["data"] = {}
|
|
data["data"]["name"] = initiator_grp_name
|
|
data["data"]["access_protocol"] = "fc"
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def get_initiator_grp_id(self, initiator_grp_name):
|
|
api = "initiator_groups"
|
|
filter = {'name': initiator_grp_name}
|
|
r = self.get_query(api, filter)
|
|
return r.json()['data'][0]['id']
|
|
|
|
def add_initiator_to_igroup(self, initiator_grp_name, initiator_name):
|
|
initiator_group_id = self.get_initiator_grp_id(initiator_grp_name)
|
|
api = "initiators"
|
|
data = {"data": {
|
|
"access_protocol": "iscsi",
|
|
"initiator_group_id": initiator_group_id,
|
|
"label": initiator_name,
|
|
"iqn": initiator_name
|
|
}}
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def add_initiator_to_igroup_fc(self, initiator_grp_name, wwpn):
|
|
initiator_group_id = self.get_initiator_grp_id(initiator_grp_name)
|
|
api = "initiators"
|
|
data = {"data": {
|
|
"access_protocol": "fc",
|
|
"initiator_group_id": initiator_group_id,
|
|
"wwpn": self._format_to_wwpn(wwpn)
|
|
}}
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def get_pool_id(self, pool_name):
|
|
api = "pools/"
|
|
filter = {'name': pool_name}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve information for "
|
|
"pool : %(pool)s") %
|
|
{'pool': pool_name})
|
|
return r.json()['data'][0]['id']
|
|
|
|
def get_pool_info(self, pool_id):
|
|
api = 'pools/' + six.text_type(pool_id)
|
|
r = self.get(api)
|
|
return r.json()['data']
|
|
|
|
def get_initiator_grp_list(self):
|
|
api = "initiator_groups/detail"
|
|
r = self.get(api)
|
|
if 'data' not in r.json():
|
|
raise NimbleAPIException(_("Unable to retrieve initiator group "
|
|
"list"))
|
|
LOG.info('Successfully retrieved InitiatorGrpList')
|
|
return r.json()['data']
|
|
|
|
def get_initiator_grp_id_by_name(self, initiator_group_name):
|
|
api = 'initiator_groups'
|
|
filter = {"name": initiator_group_name}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve information for "
|
|
"initiator group : %s") %
|
|
initiator_group_name)
|
|
return r.json()['data'][0]['id']
|
|
|
|
def get_volume_id_by_name(self, name):
|
|
api = "volumes"
|
|
filter = {"name": name}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve information for "
|
|
"volume: %s") % name)
|
|
return r.json()['data'][0]['id']
|
|
|
|
def get_volume_name(self, volume_id):
|
|
api = "volumes/" + six.text_type(volume_id)
|
|
r = self.get(api)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve information for "
|
|
"volume: %s") % volume_id)
|
|
return r.json()['data']['name']
|
|
|
|
def add_acl(self, volume, initiator_group_name):
|
|
initiator_group_id = self.get_initiator_grp_id_by_name(
|
|
initiator_group_name)
|
|
volume_id = self.get_volume_id_by_name(volume['name'])
|
|
data = {'data': {"apply_to": 'both',
|
|
"initiator_group_id": initiator_group_id,
|
|
"vol_id": volume_id
|
|
}}
|
|
api = 'access_control_records'
|
|
try:
|
|
self.post(api, data)
|
|
except NimbleAPIException as ex:
|
|
LOG.debug("add_acl_exception: %s", ex)
|
|
if SM_OBJ_EXIST_MSG in six.text_type(ex):
|
|
LOG.warning('Volume %(vol)s : %(state)s',
|
|
{'vol': volume['name'],
|
|
'state': SM_OBJ_EXIST_MSG})
|
|
else:
|
|
msg = (_("Add access control failed with error: %s") %
|
|
six.text_type(ex))
|
|
raise NimbleAPIException(msg)
|
|
|
|
def get_acl_record(self, volume_id, initiator_group_id):
|
|
filter = {"vol_id": volume_id,
|
|
"initiator_group_id": initiator_group_id}
|
|
api = "access_control_records"
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve ACL for volume: "
|
|
"%(vol)s %(igroup)s ") %
|
|
{'vol': volume_id,
|
|
'igroup': initiator_group_id})
|
|
return r.json()['data'][0]
|
|
|
|
def get_volume_acl_records(self, volume_id):
|
|
api = "volumes/" + six.text_type(volume_id)
|
|
r = self.get(api)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve information for "
|
|
"volume: %s") % volume_id)
|
|
return r.json()['data']['access_control_records']
|
|
|
|
def remove_all_acls(self, volume):
|
|
LOG.info("removing all access control list from volume=%(vol)s",
|
|
{"vol": volume['name']})
|
|
volume_id = self.get_volume_id_by_name(volume['name'])
|
|
acl_records = self.get_volume_acl_records(volume_id)
|
|
if acl_records is not None:
|
|
for acl_record in acl_records:
|
|
LOG.info("removing acl=%(acl)s with igroup=%(igroup)s",
|
|
{"acl": acl_record['id'],
|
|
"igroup": acl_record['initiator_group_name']})
|
|
self.remove_acl(volume, acl_record['initiator_group_name'])
|
|
|
|
def remove_acl(self, volume, initiator_group_name):
|
|
LOG.info("removing ACL from volume=%(vol)s "
|
|
"and %(igroup)s",
|
|
{"vol": volume['name'],
|
|
"igroup": initiator_group_name})
|
|
initiator_group_id = self.get_initiator_grp_id_by_name(
|
|
initiator_group_name)
|
|
volume_id = self.get_volume_id_by_name(volume['name'])
|
|
|
|
try:
|
|
acl_record = self.get_acl_record(volume_id, initiator_group_id)
|
|
LOG.debug("ACL Record %(acl)s", {"acl": acl_record})
|
|
acl_id = acl_record['id']
|
|
api = 'access_control_records/' + six.text_type(acl_id)
|
|
self.delete(api)
|
|
except NimbleAPIException as ex:
|
|
LOG.debug("remove_acl_exception: %s", ex)
|
|
if SM_OBJ_ENOENT_MSG in six.text_type(ex):
|
|
LOG.warning('Volume %(vol)s : %(state)s',
|
|
{'vol': volume['name'],
|
|
'state': SM_OBJ_ENOENT_MSG})
|
|
else:
|
|
msg = (_("Remove access control failed with error: %s") %
|
|
six.text_type(ex))
|
|
raise NimbleAPIException(msg)
|
|
|
|
def get_snap_info_by_id(self, snap_id, vol_id):
|
|
filter = {"id": snap_id, "vol_id": vol_id}
|
|
api = 'snapshots'
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Unable to retrieve snapshot info for "
|
|
"snap_id: %(snap)s volume id: %(vol)s")
|
|
% {'snap': snap_id,
|
|
'vol': vol_id})
|
|
LOG.debug("SnapInfo :%s", r.json()['data'][0])
|
|
return r.json()['data'][0]
|
|
|
|
def get_snap_info(self, snap_name, vol_name):
|
|
filter = {"name": snap_name, "vol_name": vol_name}
|
|
api = 'snapshots'
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Snapshot: %(snap)s of Volume: %(vol)s "
|
|
"doesn't exist") %
|
|
{'snap': snap_name,
|
|
'vol': vol_name})
|
|
return r.json()['data'][0]
|
|
|
|
def get_snap_info_detail(self, snap_id):
|
|
api = 'snapshots/detail'
|
|
filter = {'id': snap_id}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("Snapshot: %s doesn't exist") % snap_id)
|
|
return r.json()['data'][0]
|
|
|
|
@utils.retry(NimbleAPIException, 2, 3)
|
|
def online_vol(self, volume_name, online_flag):
|
|
volume_id = self.get_volume_id_by_name(volume_name)
|
|
LOG.debug("volume_id %s", six.text_type(volume_id))
|
|
eventlet.sleep(DEFAULT_SLEEP)
|
|
api = "volumes/" + six.text_type(volume_id)
|
|
data = {'data': {"online": online_flag, 'force': True}}
|
|
try:
|
|
LOG.debug("data :%s", data)
|
|
self.put(api, data)
|
|
LOG.debug("Volume %(vol)s is in requested online state :%(flag)s",
|
|
{'vol': volume_name,
|
|
'flag': online_flag})
|
|
except Exception as ex:
|
|
msg = (_("Error %s") % ex)
|
|
LOG.debug("online_vol_exception: %s", msg)
|
|
if msg.__contains__("Object is %s" % SM_STATE_MSG):
|
|
LOG.warning('Volume %(vol)s : %(state)s',
|
|
{'vol': volume_name,
|
|
'state': SM_STATE_MSG})
|
|
# TODO(rkumar): Check if we need to ignore the connected
|
|
# initiator
|
|
elif msg.__contains__("Initiators are connected to"):
|
|
raise NimbleAPIException(msg)
|
|
else:
|
|
raise exception.InvalidVolume(reason=msg)
|
|
|
|
def online_snap(self, volume_name, online_flag, snap_name):
|
|
snap_info = self.get_snap_info(snap_name, volume_name)
|
|
api = "snapshots/" + six.text_type(snap_info['id'])
|
|
data = {'data': {"online": online_flag}}
|
|
try:
|
|
self.put(api, data)
|
|
LOG.debug("Snapshot %(snap)s is in requested online state "
|
|
":%(flag)s",
|
|
{'snap': snap_name, 'flag': online_flag})
|
|
except Exception as ex:
|
|
LOG.debug("online_snap_exception: %s", ex)
|
|
if six.text_type(ex).__contains__("Object %s" % SM_STATE_MSG):
|
|
LOG.warning('Snapshot %(snap)s :%(state)s',
|
|
{'snap': snap_name,
|
|
'state': SM_STATE_MSG})
|
|
else:
|
|
raise
|
|
|
|
@utils.retry(NimbleAPIException, 2, 3)
|
|
def get_vol_info(self, volume_name):
|
|
volume_id = self.get_volume_id_by_name(volume_name)
|
|
api = 'volumes/' + six.text_type(volume_id)
|
|
r = self.get(api)
|
|
if not r.json()['data']:
|
|
raise exception.VolumeNotFound(_("Volume: %s not found") %
|
|
volume_name)
|
|
return r.json()['data']
|
|
|
|
def delete_vol(self, volume_name):
|
|
volume_id = self.get_volume_id_by_name(volume_name)
|
|
api = "volumes/" + six.text_type(volume_id)
|
|
self.delete(api)
|
|
|
|
def snap_vol(self, snapshot):
|
|
api = "snapshots"
|
|
volume_name = snapshot['volume_name']
|
|
vol_id = self.get_volume_id_by_name(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
|
|
snap_description = snap_description[:254]
|
|
data = {"data": {"name": snap_name,
|
|
"description": snap_description,
|
|
"vol_id": vol_id
|
|
}
|
|
}
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def clone_vol(self, volume, snapshot, reserve, is_gst_enabled,
|
|
protocol, pool_name):
|
|
api = "volumes"
|
|
volume_name = snapshot['volume_name']
|
|
snap_name = snapshot['name']
|
|
snap_info = self.get_snap_info(snap_name, volume_name)
|
|
clone_name = volume['name']
|
|
snap_size = snapshot['volume_size']
|
|
reserve_size = 100 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)
|
|
perf_policy_id = self.get_performance_policy_id(perf_policy_name)
|
|
encrypt = extra_specs_map.get(EXTRA_SPEC_ENCRYPTION)
|
|
multi_initiator = extra_specs_map.get(EXTRA_SPEC_MULTI_INITIATOR)
|
|
iops_limit = extra_specs_map[EXTRA_SPEC_IOPS_LIMIT]
|
|
folder_name = extra_specs_map[EXTRA_SPEC_FOLDER]
|
|
pool_id = self.get_pool_id(pool_name)
|
|
# default value of cipher for encryption
|
|
cipher = DEFAULT_CIPHER
|
|
if encrypt.lower() == 'yes':
|
|
cipher = AES_256_XTS_CIPHER
|
|
if is_gst_enabled is True:
|
|
agent_type = AGENT_TYPE_OPENSTACK_GST
|
|
else:
|
|
agent_type = AGENT_TYPE_OPENSTACK
|
|
|
|
LOG.info('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_size,
|
|
'agent-type': agent_type,
|
|
'perfpol-name': perf_policy_name,
|
|
'encryption': encrypt,
|
|
'cipher': cipher,
|
|
'multi-initiator': multi_initiator})
|
|
|
|
data = {"data": {"name": clone_name,
|
|
"clone": 'true',
|
|
"base_snap_id": snap_info['id'],
|
|
'snap_limit': DEFAULT_SNAP_QUOTA,
|
|
'warn_level': int(WARN_LEVEL),
|
|
'limit': 100,
|
|
"online": 'true',
|
|
"reserve": reserve_size,
|
|
"agent_type": agent_type,
|
|
"perfpolicy_id": perf_policy_id,
|
|
"encryption_cipher": cipher
|
|
}
|
|
}
|
|
if protocol == "iSCSI":
|
|
data['data']['multi_initiator'] = multi_initiator
|
|
|
|
folder_id = None
|
|
if folder_name is not None:
|
|
# validate if folder exists in pool_name
|
|
pool_info = self.get_pool_info(pool_id)
|
|
if 'folder_list' in pool_info and (pool_info['folder_list'] is
|
|
not None):
|
|
for folder_list in pool_info['folder_list']:
|
|
LOG.debug("folder_list : %s", folder_list)
|
|
if folder_list['fqn'] == "/" + folder_name:
|
|
LOG.debug("Folder %(folder)s present in pool "
|
|
"%(pool)s",
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
folder_id = self.get_folder_id(folder_name)
|
|
if folder_id is not None:
|
|
data['data']["folder_id"] = folder_id
|
|
if folder_id is None:
|
|
raise NimbleAPIException(_("Folder '%(folder)s' not "
|
|
"present in pool '%(pool)s'") %
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
else:
|
|
raise NimbleAPIException(_("Folder '%(folder)s' not present in"
|
|
" pool '%(pool)s'") %
|
|
{'folder': folder_name,
|
|
'pool': pool_name})
|
|
|
|
if iops_limit is not None:
|
|
if not iops_limit.isdigit() or (
|
|
int(iops_limit) < MIN_IOPS) or (int(iops_limit) > MAX_IOPS):
|
|
raise NimbleAPIException(_("%(err)s [%(min)s, %(max)s]") %
|
|
{'err': IOPS_ERR_MSG,
|
|
'min': MIN_IOPS,
|
|
'max': MAX_IOPS})
|
|
|
|
data['data']['limit_iops'] = iops_limit
|
|
if iops_limit is not None:
|
|
if not iops_limit.isdigit() or (
|
|
int(iops_limit) < MIN_IOPS) or (int(iops_limit) > MAX_IOPS):
|
|
raise NimbleAPIException(_("Please set valid IOPS limit"
|
|
" in the range [%(min)s, %(max)s]") %
|
|
{'min': MIN_IOPS,
|
|
'max': MAX_IOPS})
|
|
data['data']['limit_iops'] = iops_limit
|
|
|
|
LOG.debug("Volume metadata :%s", volume.metadata)
|
|
for key, value in volume.metadata.items():
|
|
LOG.debug("Key %(key)s Value %(value)s",
|
|
{'key': key, 'value': value})
|
|
if key == EXTRA_SPEC_IOPS_LIMIT and value.isdigit():
|
|
if type(value) == int or int(value) < MIN_IOPS or (
|
|
int(value) > MAX_IOPS):
|
|
raise NimbleAPIException(_("Please enter valid IOPS "
|
|
"limit in the range ["
|
|
"%(min)s, %(max)s]") %
|
|
{'min': MIN_IOPS,
|
|
'max': MAX_IOPS})
|
|
LOG.debug("IOPS Limit %s", value)
|
|
data['data']['limit_iops'] = value
|
|
|
|
r = self.post(api, data)
|
|
return r['data']
|
|
|
|
def edit_vol(self, volume_name, data):
|
|
vol_id = self.get_volume_id_by_name(volume_name)
|
|
api = "volumes/" + six.text_type(vol_id)
|
|
self.put(api, data)
|
|
|
|
def delete_snap(self, volume_name, snap_name):
|
|
snap_info = self.get_snap_info(snap_name, volume_name)
|
|
api = "snapshots/" + six.text_type(snap_info['id'])
|
|
try:
|
|
self.delete(api)
|
|
except NimbleAPIException as ex:
|
|
LOG.debug("delete snapshot exception: %s", ex)
|
|
if SM_OBJ_HAS_CLONE in six.text_type(ex):
|
|
# if snap has a clone log the error and continue ahead
|
|
LOG.warning('Snapshot %(snap)s : %(state)s',
|
|
{'snap': snap_name,
|
|
'state': SM_OBJ_HAS_CLONE})
|
|
else:
|
|
raise
|
|
|
|
@_connection_checker
|
|
def get(self, api):
|
|
return self.get_query(api, None)
|
|
|
|
@_connection_checker
|
|
def get_query(self, api, query):
|
|
url = self.uri + api
|
|
return requests.get(url, headers=self.headers,
|
|
params=query, verify=self.verify)
|
|
|
|
@_connection_checker
|
|
def put(self, api, payload):
|
|
url = self.uri + api
|
|
r = requests.put(url, data=json.dumps(payload),
|
|
headers=self.headers, verify=self.verify)
|
|
if r.status_code != 201 and r.status_code != 200:
|
|
base = "Failed to execute api %(api)s : Error Code :%(code)s" % {
|
|
'api': api,
|
|
'code': r.status_code}
|
|
LOG.debug("Base error : %(base)s", {'base': base})
|
|
try:
|
|
msg = _("%(base)s Message: %(msg)s") % {
|
|
'base': base,
|
|
'msg': r.json()['messages'][1]['text']}
|
|
except IndexError:
|
|
msg = _("%(base)s Message: %(msg)s") % {
|
|
'base': base,
|
|
'msg': six.text_type(r.json())}
|
|
raise NimbleAPIException(msg)
|
|
return r.json()
|
|
|
|
@_connection_checker
|
|
def post(self, api, payload):
|
|
url = self.uri + api
|
|
r = requests.post(url, data=json.dumps(payload),
|
|
headers=self.headers, verify=self.verify)
|
|
if r.status_code != 201 and r.status_code != 200:
|
|
msg = _("Failed to execute api %(api)s : %(msg)s : %(code)s") % {
|
|
'api': api,
|
|
'msg': r.json()['messages'][1]['text'],
|
|
'code': r.status_code}
|
|
raise NimbleAPIException(msg)
|
|
return r.json()
|
|
|
|
@_connection_checker
|
|
def delete(self, api):
|
|
url = self.uri + api
|
|
r = requests.delete(url, headers=self.headers, verify=self.verify)
|
|
if r.status_code != 201 and r.status_code != 200:
|
|
base = "Failed to execute api %(api)s: Error Code: %(code)s" % {
|
|
'api': api,
|
|
'code': r.status_code}
|
|
LOG.debug("Base error : %(base)s", {'base': base})
|
|
try:
|
|
msg = _("%(base)s Message: %(msg)s") % {
|
|
'base': base,
|
|
'msg': r.json()['messages'][1]['text']}
|
|
except IndexError:
|
|
msg = _("%(base)s Message: %(msg)s") % {
|
|
'base': base,
|
|
'msg': six.text_type(r.json())}
|
|
raise NimbleAPIException(msg)
|
|
return r.json()
|
|
|
|
def _format_to_wwpn(self, string_wwpn):
|
|
return ':'.join(a + b for a, b in zip(* [iter(string_wwpn)] * 2))
|
|
|
|
def get_fc_interface_list(self, array_name):
|
|
"""getFibreChannelInterfaceList API to get FC interfaces on array."""
|
|
api = 'fibre_channel_interfaces/detail'
|
|
filter = {'array_name_or_serial': array_name}
|
|
r = self.get_query(api, filter)
|
|
if not r.json()['data']:
|
|
raise NimbleAPIException(_("No fc interfaces for array %s") %
|
|
array_name)
|
|
return r.json()['data']
|
|
|
|
def enable_group_scoped_target(self):
|
|
group_id = self.get_group_id()
|
|
api = "groups/" + six.text_type(group_id)
|
|
data = {'data': {'group_target_enabled': True}}
|
|
self.put(api, data)
|