Browse Source

Merge "Update code layout and missing Zadara features"

changes/33/781933/1
Zuul 4 months ago
committed by Gerrit Code Review
parent
commit
aeabcfc3ca
  1. 5
      cinder/opts.py
  2. 1124
      cinder/tests/unit/volume/drivers/test_zadara.py
  3. 753
      cinder/volume/drivers/zadara.py
  4. 0
      cinder/volume/drivers/zadara/__init__.py
  5. 517
      cinder/volume/drivers/zadara/common.py
  6. 53
      cinder/volume/drivers/zadara/exception.py
  7. 729
      cinder/volume/drivers/zadara/zadara.py
  8. 2
      cinder/volume/manager.py
  9. 8
      doc/source/configuration/block-storage/drivers/zadara-volume-driver.rst
  10. 2
      doc/source/reference/support-matrix.ini
  11. 14
      releasenotes/notes/Zadara-newlayout-support-features-ffa20694c008ba86.yaml

5
cinder/opts.py

@ -176,7 +176,8 @@ from cinder.volume.drivers.windows import iscsi as \
cinder_volume_drivers_windows_iscsi
from cinder.volume.drivers.windows import smbfs as \
cinder_volume_drivers_windows_smbfs
from cinder.volume.drivers import zadara as cinder_volume_drivers_zadara
from cinder.volume.drivers.zadara import zadara as \
cinder_volume_drivers_zadara_zadara
from cinder.volume import manager as cinder_volume_manager
from cinder.volume.targets import spdknvmf as cinder_volume_targets_spdknvmf
from cinder.wsgi import eventlet_server as cinder_wsgi_eventletserver
@ -392,7 +393,7 @@ def list_opts():
cinder_volume_drivers_vzstorage.vzstorage_opts,
cinder_volume_drivers_windows_iscsi.windows_opts,
cinder_volume_drivers_windows_smbfs.volume_opts,
cinder_volume_drivers_zadara.zadara_opts,
cinder_volume_drivers_zadara_zadara.common.zadara_opts,
cinder_volume_manager.volume_backend_opts,
cinder_volume_targets_spdknvmf.spdk_opts,
)),

1124
cinder/tests/unit/volume/drivers/test_zadara.py
File diff suppressed because it is too large
View File

753
cinder/volume/drivers/zadara.py

@ -1,753 +0,0 @@
# Copyright (c) 2019 Zadara Storage, Inc.
# 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 Zadara Virtual Private Storage Array (VPSA).
This driver requires VPSA with API version 15.07 or higher.
"""
from lxml import etree
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import strutils
import requests
import six
from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder.volume import configuration
from cinder.volume import driver
LOG = logging.getLogger(__name__)
zadara_opts = [
cfg.BoolOpt('zadara_use_iser',
default=True,
help='VPSA - Use ISER instead of iSCSI'),
cfg.StrOpt('zadara_vpsa_host',
default=None,
help='VPSA - Management Host name or IP address'),
cfg.PortOpt('zadara_vpsa_port',
default=None,
help='VPSA - Port number'),
cfg.BoolOpt('zadara_vpsa_use_ssl',
default=False,
help='VPSA - Use SSL connection'),
cfg.BoolOpt('zadara_ssl_cert_verify',
default=True,
help='If set to True the http client will validate the SSL '
'certificate of the VPSA endpoint.'),
cfg.StrOpt('zadara_user',
default=None,
deprecated_for_removal=True,
help='VPSA - Username'),
cfg.StrOpt('zadara_password',
default=None,
help='VPSA - Password',
deprecated_for_removal=True,
secret=True),
cfg.StrOpt('zadara_access_key',
default=None,
help='VPSA access key',
secret=True),
cfg.StrOpt('zadara_vpsa_poolname',
default=None,
help='VPSA - Storage Pool assigned for volumes'),
cfg.BoolOpt('zadara_vol_encrypt',
default=False,
help='VPSA - Default encryption policy for volumes'),
cfg.StrOpt('zadara_vol_name_template',
default='OS_%s',
help='VPSA - Default template for VPSA volume names'),
cfg.BoolOpt('zadara_default_snap_policy',
default=False,
help="VPSA - Attach snapshot policy for volumes")]
CONF = cfg.CONF
CONF.register_opts(zadara_opts, group=configuration.SHARED_CONF_GROUP)
class ZadaraServerCreateFailure(exception.VolumeDriverException):
message = _("Unable to create server object for initiator %(name)s")
class ZadaraServerNotFound(exception.NotFound):
message = _("Unable to find server object for initiator %(name)s")
class ZadaraVPSANoActiveController(exception.VolumeDriverException):
message = _("Unable to find any active VPSA controller")
class ZadaraAttachmentsNotFound(exception.NotFound):
message = _("Failed to retrieve attachments for volume %(name)s")
class ZadaraInvalidAttachmentInfo(exception.Invalid):
message = _("Invalid attachment info for volume %(name)s: %(reason)s")
class ZadaraVolumeNotFound(exception.VolumeDriverException):
message = "%(reason)s"
class ZadaraInvalidAccessKey(exception.VolumeDriverException):
message = "Invalid VPSA access key"
class ZadaraVPSAConnection(object):
"""Executes volume driver commands on VPSA."""
def __init__(self, conf):
self.conf = conf
self.access_key = conf.zadara_access_key
self.ensure_connection()
def _generate_vpsa_cmd(self, cmd, **kwargs):
"""Generate command to be sent to VPSA."""
# Dictionary of applicable VPSA commands in the following format:
# 'command': (method, API_URL, {optional parameters})
vpsa_commands = {
'login': ('POST',
'/api/users/login.xml',
{'user': self.conf.zadara_user,
'password': self.conf.zadara_password}),
# Volume operations
'create_volume': ('POST',
'/api/volumes.xml',
{'name': kwargs.get('name'),
'capacity': kwargs.get('size'),
'pool': self.conf.zadara_vpsa_poolname,
'thin': 'YES',
'crypt': 'YES'
if self.conf.zadara_vol_encrypt else 'NO',
'attachpolicies': 'NO'
if not self.conf.zadara_default_snap_policy
else 'YES'}),
'delete_volume': ('DELETE',
'/api/volumes/%s.xml' % kwargs.get('vpsa_vol'),
{'force': 'YES'}),
'expand_volume': ('POST',
'/api/volumes/%s/expand.xml'
% kwargs.get('vpsa_vol'),
{'capacity': kwargs.get('size')}),
# Snapshot operations
# Snapshot request is triggered for a single volume though the
# API call implies that snapshot is triggered for CG (legacy API).
'create_snapshot': ('POST',
'/api/consistency_groups/%s/snapshots.xml'
% kwargs.get('cg_name'),
{'display_name': kwargs.get('snap_name')}),
'delete_snapshot': ('DELETE',
'/api/snapshots/%s.xml'
% kwargs.get('snap_id'),
{}),
'create_clone_from_snap': ('POST',
'/api/consistency_groups/%s/clone.xml'
% kwargs.get('cg_name'),
{'name': kwargs.get('name'),
'snapshot': kwargs.get('snap_id')}),
'create_clone': ('POST',
'/api/consistency_groups/%s/clone.xml'
% kwargs.get('cg_name'),
{'name': kwargs.get('name')}),
# Server operations
'create_server': ('POST',
'/api/servers.xml',
{'display_name': kwargs.get('initiator'),
'iqn': kwargs.get('initiator')}),
# Attach/Detach operations
'attach_volume': ('POST',
'/api/servers/%s/volumes.xml'
% kwargs.get('vpsa_srv'),
{'volume_name[]': kwargs.get('vpsa_vol'),
'force': 'NO'}),
'detach_volume': ('POST',
'/api/volumes/%s/detach.xml'
% kwargs.get('vpsa_vol'),
{'server_name[]': kwargs.get('vpsa_srv'),
'force': 'YES'}),
# Get operations
'list_volumes': ('GET',
'/api/volumes.xml',
{}),
'list_pools': ('GET',
'/api/pools.xml',
{}),
'list_controllers': ('GET',
'/api/vcontrollers.xml',
{}),
'list_servers': ('GET',
'/api/servers.xml',
{}),
'list_vol_attachments': ('GET',
'/api/volumes/%s/servers.xml'
% kwargs.get('vpsa_vol'),
{}),
'list_vol_snapshots': ('GET',
'/api/consistency_groups/%s/snapshots.xml'
% kwargs.get('cg_name'),
{})}
try:
method, url, params = vpsa_commands[cmd]
except KeyError:
raise exception.UnknownCmd(cmd=cmd)
if method == 'GET':
params = dict(page=1, start=0, limit=0)
body = None
elif method in ['DELETE', 'POST']:
body = params
params = None
else:
msg = (_('Method %(method)s is not defined') %
{'method': method})
LOG.error(msg)
raise AssertionError(msg)
# 'access_key' was generated using username and password
# or it was taken from the input file
headers = {'X-Access-Key': self.access_key}
return method, url, params, body, headers
def ensure_connection(self, cmd=None):
"""Retrieve access key for VPSA connection."""
if self.access_key or cmd == 'login':
return
cmd = 'login'
xml_tree = self.send_cmd(cmd)
user = xml_tree.find('user')
if user is None:
raise (exception.MalformedResponse(
cmd=cmd, reason=_('no "user" field')))
access_key = user.findtext('access-key')
if access_key is None:
raise (exception.MalformedResponse(
cmd=cmd, reason=_('no "access-key" field')))
self.access_key = access_key
def send_cmd(self, cmd, **kwargs):
"""Send command to VPSA Controller."""
self.ensure_connection(cmd)
method, url, params, body, headers = self._generate_vpsa_cmd(cmd,
**kwargs)
LOG.debug('Invoking %(cmd)s using %(method)s request.',
{'cmd': cmd, 'method': method})
host = self.conf.zadara_vpsa_host
port = int(self.conf.zadara_vpsa_port)
protocol = "https" if self.conf.zadara_vpsa_use_ssl else "http"
if protocol == "https":
if not self.conf.zadara_ssl_cert_verify:
verify = False
else:
cert = ((self.conf.driver_ssl_cert_path) or None)
verify = cert if cert else True
else:
verify = False
if port:
api_url = "%s://%s:%d%s" % (protocol, host, port, url)
else:
api_url = "%s://%s%s" % (protocol, host, url)
try:
with requests.Session() as session:
session.headers.update(headers)
response = session.request(method, api_url, params=params,
data=body, headers=headers,
verify=verify)
except requests.exceptions.RequestException as e:
message = (_('Exception: %s') % six.text_type(e))
raise exception.VolumeDriverException(message=message)
if response.status_code != 200:
raise exception.BadHTTPResponseStatus(status=response.status_code)
data = response.content
xml_tree = etree.fromstring(data)
status = xml_tree.findtext('status')
if status == '5':
# Invalid Credentials
raise ZadaraInvalidAccessKey()
if status != '0':
raise exception.FailedCmdWithDump(status=status, data=data)
if method in ['POST', 'DELETE']:
LOG.debug('Operation completed with status code %(status)s',
{'status': status})
return xml_tree
@interface.volumedriver
class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
"""Zadara VPSA iSCSI/iSER volume driver.
.. code-block:: none
Version history:
15.07 - Initial driver
16.05 - Move from httplib to requests
19.08 - Add API access key authentication option
"""
VERSION = '19.08'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "ZadaraStorage_VPSA_CI"
def __init__(self, *args, **kwargs):
super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
self.vpsa = None
self.configuration.append_config_values(zadara_opts)
@staticmethod
def get_driver_options():
return zadara_opts
def do_setup(self, context):
"""Any initialization the volume driver does while starting.
Establishes initial connection with VPSA and retrieves access_key.
"""
self.vpsa = ZadaraVPSAConnection(self.configuration)
self._check_access_key_validity()
def _check_access_key_validity(self):
"""Check VPSA access key"""
self.vpsa.ensure_connection()
if not self.vpsa.access_key:
raise ZadaraInvalidAccessKey()
active_ctrl = self._get_active_controller_details()
if active_ctrl is None:
raise ZadaraInvalidAccessKey()
def check_for_setup_error(self):
"""Returns an error (exception) if prerequisites aren't met."""
self._check_access_key_validity()
def local_path(self, volume):
"""Return local path to existing local volume."""
raise NotImplementedError()
def _xml_parse_helper(self, xml_tree, first_level, search_tuple,
first=True):
"""Helper for parsing VPSA's XML output.
Returns single item if first==True or list for multiple selection.
If second argument in search_tuple is None - returns all items with
appropriate key.
"""
objects = xml_tree.find(first_level)
if objects is None:
return None
result_list = []
key, value = search_tuple
for child_object in list(objects):
found_value = child_object.findtext(key)
if found_value and (found_value == value or value is None):
if first:
return child_object
else:
result_list.append(child_object)
return result_list if result_list else None
def _get_vpsa_volume_name_and_size(self, name):
"""Return VPSA's name & size for the volume."""
xml_tree = self.vpsa.send_cmd('list_volumes')
volume = self._xml_parse_helper(xml_tree, 'volumes',
('display-name', name))
if volume is not None:
return (volume.findtext('name'),
int(volume.findtext('virtual-capacity')))
return None, None
def _get_vpsa_volume_name(self, name):
"""Return VPSA's name for the volume."""
(vol_name, size) = self._get_vpsa_volume_name_and_size(name)
return vol_name
def _get_volume_cg_name(self, name):
"""Return name of the consistency group for the volume.
cg-name is a volume uniqe identifier (legacy attribute)
and not consistency group as it may imply.
"""
xml_tree = self.vpsa.send_cmd('list_volumes')
volume = self._xml_parse_helper(xml_tree, 'volumes',
('display-name', name))
if volume is not None:
return volume.findtext('cg-name')
return None
def _get_snap_id(self, cg_name, snap_name):
"""Return snapshot ID for particular volume."""
xml_tree = self.vpsa.send_cmd('list_vol_snapshots',
cg_name=cg_name)
snap = self._xml_parse_helper(xml_tree, 'snapshots',
('display-name', snap_name))
if snap is not None:
return snap.findtext('name')
return None
def _get_pool_capacity(self, pool_name):
"""Return pool's total and available capacities."""
xml_tree = self.vpsa.send_cmd('list_pools')
pool = self._xml_parse_helper(xml_tree, 'pools',
('name', pool_name))
if pool is not None:
total = int(pool.findtext('capacity'))
free = int(float(pool.findtext('available-capacity')))
LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
{'name': pool_name, 'total': total, 'free': free})
return total, free
return 'unknown', 'unknown'
def _get_active_controller_details(self):
"""Return details of VPSA's active controller."""
xml_tree = self.vpsa.send_cmd('list_controllers')
ctrl = self._xml_parse_helper(xml_tree, 'vcontrollers',
('state', 'active'))
if ctrl is not None:
return dict(target=ctrl.findtext('target'),
ip=ctrl.findtext('iscsi-ip'),
chap_user=ctrl.findtext('vpsa-chap-user'),
chap_passwd=ctrl.findtext('vpsa-chap-secret'))
return None
def _detach_vpsa_volume(self, vpsa_vol, vpsa_srv=None):
"""Detach volume from all attached servers."""
if vpsa_srv:
list_servers_ids = [vpsa_srv]
else:
list_servers = self._get_servers_attached_to_volume(vpsa_vol)
list_servers_ids = [s.findtext('name') for s in list_servers]
for server_id in list_servers_ids:
# Detach volume from server
self.vpsa.send_cmd('detach_volume',
vpsa_srv=server_id,
vpsa_vol=vpsa_vol)
def _get_server_name(self, initiator):
"""Return VPSA's name for server object with given IQN."""
xml_tree = self.vpsa.send_cmd('list_servers')
server = self._xml_parse_helper(xml_tree, 'servers',
('iqn', initiator))
if server is not None:
return server.findtext('name')
return None
def _create_vpsa_server(self, initiator):
"""Create server object within VPSA (if doesn't exist)."""
vpsa_srv = self._get_server_name(initiator)
if not vpsa_srv:
xml_tree = self.vpsa.send_cmd('create_server', initiator=initiator)
vpsa_srv = xml_tree.findtext('server-name')
return vpsa_srv
def create_volume(self, volume):
"""Create volume."""
self.vpsa.send_cmd(
'create_volume',
name=self.configuration.zadara_vol_name_template % volume['name'],
size=volume['size'])
def delete_volume(self, volume):
"""Delete volume.
Return ok if doesn't exist. Auto detach from all servers.
"""
# Get volume name
name = self.configuration.zadara_vol_name_template % volume['name']
vpsa_vol = self._get_vpsa_volume_name(name)
if not vpsa_vol:
LOG.warning('Volume %s could not be found. '
'It might be already deleted', name)
return
self._detach_vpsa_volume(vpsa_vol=vpsa_vol)
# Delete volume
self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
LOG.debug('Create snapshot: %s', snapshot['name'])
# Retrieve the CG name for the base volume
volume_name = (self.configuration.zadara_vol_name_template
% snapshot['volume_name'])
cg_name = self._get_volume_cg_name(volume_name)
if not cg_name:
msg = _('Volume %(name)s not found') % {'name': volume_name}
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
self.vpsa.send_cmd('create_snapshot',
cg_name=cg_name,
snap_name=snapshot['name'])
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
LOG.debug('Delete snapshot: %s', snapshot['name'])
# Retrieve the CG name for the base volume
volume_name = (self.configuration.zadara_vol_name_template
% snapshot['volume_name'])
cg_name = self._get_volume_cg_name(volume_name)
if not cg_name:
# If the volume isn't present, then don't attempt to delete
LOG.warning('snapshot: original volume %s not found, '
'skipping delete operation',
volume_name)
return
snap_id = self._get_snap_id(cg_name, snapshot['name'])
if not snap_id:
# If the snapshot isn't present, then don't attempt to delete
LOG.warning('snapshot: snapshot %s not found, '
'skipping delete operation', snapshot['name'])
return
self.vpsa.send_cmd('delete_snapshot',
snap_id=snap_id)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
LOG.debug('Creating volume from snapshot: %s', snapshot['name'])
# Retrieve the CG name for the base volume
volume_name = (self.configuration.zadara_vol_name_template
% snapshot['volume_name'])
cg_name = self._get_volume_cg_name(volume_name)
if not cg_name:
LOG.error('Volume %(name)s not found', {'name': volume_name})
raise exception.VolumeNotFound(volume_id=volume['id'])
snap_id = self._get_snap_id(cg_name, snapshot['name'])
if not snap_id:
LOG.error('Snapshot %(name)s not found',
{'name': snapshot['name']})
raise exception.SnapshotNotFound(snapshot_id=snapshot['id'])
self.vpsa.send_cmd('create_clone_from_snap',
cg_name=cg_name,
name=self.configuration.zadara_vol_name_template
% volume['name'],
snap_id=snap_id)
if volume['size'] > snapshot['volume_size']:
self.extend_volume(volume, volume['size'])
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
LOG.debug('Creating clone of volume: %s', src_vref['name'])
# Retrieve the CG name for the base volume
volume_name = (self.configuration.zadara_vol_name_template
% src_vref['name'])
cg_name = self._get_volume_cg_name(volume_name)
if not cg_name:
LOG.error('Volume %(name)s not found', {'name': volume_name})
raise exception.VolumeNotFound(volume_id=volume['id'])
self.vpsa.send_cmd('create_clone',
cg_name=cg_name,
name=self.configuration.zadara_vol_name_template
% volume['name'])
if volume['size'] > src_vref['size']:
self.extend_volume(volume, volume['size'])
def extend_volume(self, volume, new_size):
"""Extend an existing volume."""
# Get volume name
name = self.configuration.zadara_vol_name_template % volume['name']
(vpsa_vol, size) = self._get_vpsa_volume_name_and_size(name)
if not vpsa_vol:
msg = (_('Volume %(name)s could not be found. '
'It might be already deleted') % {'name': name})
LOG.error(msg)
raise ZadaraVolumeNotFound(reason=msg)
if new_size < size:
raise exception.InvalidInput(
reason=_('%(new_size)s < current size %(size)s') %
{'new_size': new_size, 'size': size})
expand_size = new_size - size
self.vpsa.send_cmd('expand_volume',
vpsa_vol=vpsa_vol,
size=expand_size)
def create_export(self, context, volume, vg=None):
"""Irrelevant for VPSA volumes. Export created during attachment."""
pass
def ensure_export(self, context, volume):
"""Irrelevant for VPSA volumes. Export created during attachment."""
pass
def remove_export(self, context, volume):
"""Irrelevant for VPSA volumes. Export removed during detach."""
pass
def initialize_connection(self, volume, connector):
"""Attach volume to initiator/host.
During this call VPSA exposes volume to particular Initiator. It also
creates a 'server' entity for Initiator (if it was not created before)
All necessary connection information is returned, including auth data.
Connection data (target, LUN) is not stored in the DB.
"""
# First: Check Active controller: if not valid, raise exception
ctrl = self._get_active_controller_details()
if not ctrl:
raise ZadaraVPSANoActiveController()
# Get/Create server name for IQN
initiator_name = connector['initiator']
vpsa_srv = self._create_vpsa_server(initiator_name)
if not vpsa_srv:
raise ZadaraServerCreateFailure(name=initiator_name)
# Get volume name
name = self.configuration.zadara_vol_name_template % volume['name']
vpsa_vol = self._get_vpsa_volume_name(name)
if not vpsa_vol:
raise exception.VolumeNotFound(volume_id=volume['id'])
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
vpsa_vol=vpsa_vol)
attach = self._xml_parse_helper(xml_tree, 'servers',
('name', vpsa_srv))
# Attach volume to server
if attach is None:
self.vpsa.send_cmd('attach_volume',
vpsa_srv=vpsa_srv,
vpsa_vol=vpsa_vol)
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
vpsa_vol=vpsa_vol)
server = self._xml_parse_helper(xml_tree, 'servers',
('iqn', initiator_name))
if server is None:
raise ZadaraAttachmentsNotFound(name=name)
target = server.findtext('target')
lun = int(server.findtext('lun'))
if None in [target, lun]:
raise ZadaraInvalidAttachmentInfo(
name=name,
reason=_('target=%(target)s, lun=%(lun)s') %
{'target': target, 'lun': lun})
properties = {'target_discovered': False,
'target_portal': '%s:%s' % (ctrl['ip'], '3260'),
'target_iqn': target,
'target_lun': lun,
'volume_id': volume['id'],
'auth_method': 'CHAP',
'auth_username': ctrl['chap_user'],
'auth_password': ctrl['chap_passwd']}
LOG.debug('Attach properties: %(properties)s',
{'properties': strutils.mask_password(properties)})
return {'driver_volume_type':
('iser' if (self.configuration.safe_get('zadara_use_iser'))
else 'iscsi'), 'data': properties}
def terminate_connection(self, volume, connector, **kwargs):
"""Detach volume from the initiator."""
# Get server name for IQN
if connector is None:
# Detach volume from all servers
# Get volume name
name = self.configuration.zadara_vol_name_template % volume['name']
vpsa_vol = self._get_vpsa_volume_name(name)
if vpsa_vol:
self._detach_vpsa_volume(vpsa_vol=vpsa_vol)
return
else:
LOG.warning('Volume %s could not be found', name)
raise exception.VolumeNotFound(volume_id=volume['id'])
initiator_name = connector['initiator']
vpsa_srv = self._get_server_name(initiator_name)
if not vpsa_srv:
raise ZadaraServerNotFound(name=initiator_name)
# Get volume name
name = self.configuration.zadara_vol_name_template % volume['name']
vpsa_vol = self._get_vpsa_volume_name(name)
if not vpsa_vol:
raise exception.VolumeNotFound(volume_id=volume['id'])
# Detach volume from server
self._detach_vpsa_volume(vpsa_vol=vpsa_vol, vpsa_srv=vpsa_srv)
def _get_servers_attached_to_volume(self, vpsa_vol):
"""Return all servers attached to volume."""
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
vpsa_vol=vpsa_vol)
list_servers = self._xml_parse_helper(xml_tree, 'servers',
('iqn', None), first=False)
return list_servers or []
def _update_volume_stats(self):
"""Retrieve stats info from volume group."""
LOG.debug("Updating volume stats")
data = {}
backend_name = self.configuration.safe_get('volume_backend_name')
storage_protocol = ('iSER' if
(self.configuration.safe_get('zadara_use_iser'))
else 'iSCSI')
data["volume_backend_name"] = backend_name or self.__class__.__name__
data["vendor_name"] = 'Zadara Storage'
data["driver_version"] = self.VERSION
data["storage_protocol"] = storage_protocol
data['reserved_percentage'] = self.configuration.reserved_percentage
data['QoS_support'] = False
(total, free) = self._get_pool_capacity(self.configuration.
zadara_vpsa_poolname)
data['total_capacity_gb'] = total
data['free_capacity_gb'] = free
self._stats = data

0
cinder/volume/drivers/zadara/__init__.py

517
cinder/volume/drivers/zadara/common.py

@ -0,0 +1,517 @@
# Copyright (c) 2020 Zadara Storage, Inc.
# 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.
import json
import re
from oslo_config import cfg
from oslo_log import log as logging
import requests
LOG = logging.getLogger(__name__)
# Number of seconds the repsonse for the request sent to
# vpsa is expected. Else the request will be timed out.
# Setting it to 300 seconds initially.
vpsa_timeout = 300
# Common exception class for all the exceptions that
# are used to redirect to the driver specific exceptions.
class CommonException(Exception):
def __init__(self):
pass
class UnknownCmd(Exception):
def __init__(self, cmd):
self.cmd = cmd
class BadHTTPResponseStatus(Exception):
def __init__(self, status):
self.status = status
class FailedCmdWithDump(Exception):
def __init__(self, status, data):
self.status = status
self.data = data
class SessionRequestException(Exception):
def __init__(self, msg):
self.msg = msg
class ZadaraInvalidAccessKey(Exception):
pass
exception = CommonException()
zadara_opts = [
cfg.HostAddressOpt('zadara_vpsa_host',
default=None,
help='VPSA - Management Host name or IP address'),
cfg.PortOpt('zadara_vpsa_port',
default=None,
help='VPSA - Port number'),
cfg.BoolOpt('zadara_vpsa_use_ssl',
default=False,
help='VPSA - Use SSL connection'),
cfg.BoolOpt('zadara_ssl_cert_verify',
default=True,
help='If set to True the http client will validate the SSL '
'certificate of the VPSA endpoint.'),
cfg.StrOpt('zadara_access_key',
default=None,
help='VPSA access key',
secret=True),
cfg.StrOpt('zadara_vpsa_poolname',
default=None,
help='VPSA - Storage Pool assigned for volumes'),
cfg.BoolOpt('zadara_vol_encrypt',
default=False,
help='VPSA - Default encryption policy for volumes. '
'If the option is neither configured nor provided '
'as metadata, the VPSA will inherit the default value.'),
cfg.BoolOpt('zadara_gen3_vol_dedupe',
default=False,
help='VPSA - Enable deduplication for volumes. '
'If the option is neither configured nor provided '
'as metadata, the VPSA will inherit the default value.'),
cfg.BoolOpt('zadara_gen3_vol_compress',
default=False,
help='VPSA - Enable compression for volumes. '
'If the option is neither configured nor provided '
'as metadata, the VPSA will inherit the default value.'),
cfg.BoolOpt('zadara_default_snap_policy',
default=False,
help="VPSA - Attach snapshot policy for volumes. "
"If the option is neither configured nor provided "
"as metadata, the VPSA will inherit the default value.")]
# Class used to connect and execute the commands on
# Zadara Virtual Private Storage Array (VPSA).
class ZadaraVPSAConnection(object):
"""Executes driver commands on VPSA."""
def __init__(self, conf, driver_ssl_cert_path, block):
self.conf = conf
self.access_key = conf.zadara_access_key
if not self.access_key:
raise exception.ZadaraInvalidAccessKey()
self.driver_ssl_cert_path = driver_ssl_cert_path
# Choose the volume type of either block or file-type
# that will help to filter volumes.
self.vol_type_str = 'showonlyblock' if block else 'showonlyfile'
# Dictionary of applicable VPSA commands in the following format:
# 'command': (method, API_URL, {optional parameters})
self.vpsa_commands = {
# Volume operations
'create_volume': lambda kwargs: (
'POST',
'/api/volumes.json',
{'name': kwargs.get('name'),
'capacity': kwargs.get('size'),
'pool': self.conf.zadara_vpsa_poolname,
'block': 'YES'
if self.vol_type_str == 'showonlyblock'
else 'NO',
'thin': 'YES',
'crypt': 'YES'
if self.conf.zadara_vol_encrypt else 'NO',
'compress': 'YES'
if self.conf.zadara_gen3_vol_compress else 'NO',
'dedupe': 'YES'
if self.conf.zadara_gen3_vol_dedupe else 'NO',
'attachpolicies': 'NO'
if not self.conf.zadara_default_snap_policy
else 'YES'}),
'delete_volume': lambda kwargs: (
'DELETE',
'/api/volumes/%s.json' % kwargs.get('vpsa_vol'),
{'force': 'YES'}),
'expand_volume': lambda kwargs: (
'POST',
'/api/volumes/%s/expand.json'
% kwargs.get('vpsa_vol'),
{'capacity': kwargs.get('size')}),
'rename_volume': lambda kwargs: (
'POST',
'/api/volumes/%s/rename.json'
% kwargs.get('vpsa_vol'),
{'new_name': kwargs.get('new_name')}),
# Snapshot operations
# Snapshot request is triggered for a single volume though the
# API call implies that snapshot is triggered for CG (legacy API).
'create_snapshot': lambda kwargs: (
'POST',
'/api/consistency_groups/%s/snapshots.json'
% kwargs.get('cg_name'),
{'display_name': kwargs.get('snap_name')}),
'delete_snapshot': lambda kwargs: (
'DELETE',
'/api/snapshots/%s.json'
% kwargs.get('snap_id'),
{}),
'rename_snapshot': lambda kwargs: (
'POST',
'/api/snapshots/%s/rename.json'
% kwargs.get('snap_id'),
{'newname': kwargs.get('new_name')}),
'create_clone_from_snap': lambda kwargs: (
'POST',
'/api/consistency_groups/%s/clone.json'
% kwargs.get('cg_name'),
{'name': kwargs.get('name'),
'snapshot': kwargs.get('snap_id')}),
'create_clone': lambda kwargs: (
'POST',
'/api/consistency_groups/%s/clone.json'
% kwargs.get('cg_name'),
{'name': kwargs.get('name')}),
# Server operations
'create_server': lambda kwargs: (
'POST',
'/api/servers.json',
{'iqn': kwargs.get('iqn'),
'iscsi': kwargs.get('iscsi_ip'),
'display_name': kwargs.get('iqn')
if kwargs.get('iqn')
else kwargs.get('iscsi_ip')}),
# Attach/Detach operations
'attach_volume': lambda kwargs: (
'POST',
'/api/servers/%s/volumes.json'
% kwargs.get('vpsa_srv'),
{'volume_name[]': kwargs.get('vpsa_vol'),
'access_type': kwargs.get('share_proto'),
'readonly': kwargs.get('read_only'),
'force': 'YES'}),
'detach_volume': lambda kwargs: (
'POST',
'/api/volumes/%s/detach.json'
% kwargs.get('vpsa_vol'),
{'server_name[]': kwargs.get('vpsa_srv'),
'force': 'YES'}),
# Update volume comment
'update_volume': lambda kwargs: (
'POST',
'/api/volumes/%s/update_comment.json'
% kwargs.get('vpsa_vol'),
{'new_comment': kwargs.get('new_comment')}),
# Get operations
'list_volumes': lambda kwargs: (
'GET',
'/api/volumes.json?%s=YES' % self.vol_type_str,
{}),
'get_volume': lambda kwargs: (
'GET',
'/api/volumes/%s.json' % kwargs.get('vpsa_vol'),
{}),
'get_volume_by_name': lambda kwargs: (
'GET',
'/api/volumes.json?display_name=%s'
% kwargs.get('display_name'),
{}),
'get_pool': lambda kwargs: (
'GET',
'/api/pools/%s.json' % kwargs.get('pool_name'),
{}),
'list_controllers': lambda kwargs: (
'GET',
'/api/vcontrollers.json',
{}),
'list_servers': lambda kwargs: (
'GET',
'/api/servers.json',
{}),
'list_vol_snapshots': lambda kwargs: (
'GET',
'/api/consistency_groups/%s/snapshots.json'
% kwargs.get('cg_name'),
{}),
'list_vol_attachments': lambda kwargs: (
'GET',
'/api/volumes/%s/servers.json'
% kwargs.get('vpsa_vol'),
{}),
'list_snapshots': lambda kwargs: (
'GET',
'/api/snapshots.json',
{}),
# Put operations
'change_export_name': lambda kwargs: (
'PUT',
'/api/volumes/%s/export_name.json'
% kwargs.get('vpsa_vol'),
{'exportname': kwargs.get('exportname')})}
def _generate_vpsa_cmd(self, cmd, **kwargs):
"""Generate command to be sent to VPSA."""
try:
method, url, params = self.vpsa_commands[cmd](kwargs)
# Populate the metadata for the volume creation
metadata = kwargs.get('metadata')
if metadata:
for key, value in metadata.items():
params[key] = value
except KeyError:
raise exception.UnknownCmd(cmd=cmd)
if method == 'GET':
params = dict(page=1, start=0, limit=0)
body = None
elif method in ['DELETE', 'POST', 'PUT']:
body = params
params = None
else:
msg = ('Method %(method)s is not defined' % {'method': method})
LOG.error(msg)
raise AssertionError(msg)
# 'access_key' was generated using username and password
# or it was taken from the input file
headers = {'X-Access-Key': self.access_key}
return method, url, params, body, headers
def send_cmd(self, cmd, **kwargs):
"""Send command to VPSA Controller."""
if not self.access_key:
raise exception.ZadaraInvalidAccessKey()
method, url, params, body, headers = self._generate_vpsa_cmd(cmd,
**kwargs)
LOG.debug('Invoking %(cmd)s using %(method)s request.',
{'cmd': cmd, 'method': method})
host = self._get_target_host(self.conf.zadara_vpsa_host)
port = int(self.conf.zadara_vpsa_port)
protocol = "https" if self.conf.zadara_vpsa_use_ssl else "http"
if protocol == "https":
if not self.conf.zadara_ssl_cert_verify:
verify = False
else:
verify = (self.driver_ssl_cert_path
if self.driver_ssl_cert_path else True)
else:
verify = False
if port:
api_url = "%s://%s:%d%s" % (protocol, host, port, url)
else:
api_url = "%s://%s%s" % (protocol, host, url)
try:
with requests.Session() as session:
session.headers.update(headers)
response = session.request(method, api_url, params=params,
data=body, headers=headers,
verify=verify, timeout=vpsa_timeout)
except requests.exceptions.RequestException as e:
msg = ('Exception: %s') % e
raise exception.SessionRequestException(msg=msg)
if response.status_code != 200:
raise exception.BadHTTPResponseStatus(
status=response.status_code)
data = response.content
json_data = json.loads(data)
response = json_data['response']
status = int(response['status'])
if status == 5:
# Invalid Credentials
raise exception.ZadaraInvalidAccessKey()
if status != 0:
raise exception.FailedCmdWithDump(status=status, data=data)
LOG.debug('Operation completed with status code %(status)s',
{'status': status})
return response
def _get_target_host(self, vpsa_host):
"""Helper for target host formatting."""
ipv6_without_brackets = ':' in vpsa_host and vpsa_host[-1] != ']'
if ipv6_without_brackets:
return ('[%s]' % vpsa_host)
return ('%s' % vpsa_host)
def _get_active_controller_details(self):
"""Return details of VPSA's active controller."""
data = self.send_cmd('list_controllers')
ctrl = None
vcontrollers = data.get('vcontrollers', [])
for controller in vcontrollers:
if controller['state'] == 'active':
ctrl = controller
break
if ctrl is not None:
target_ip = (ctrl['iscsi_ipv6'] if
ctrl['iscsi_ipv6'] else
ctrl['iscsi_ip'])
return dict(target=ctrl['target'],
ip=target_ip,
chap_user=ctrl['vpsa_chap_user'],
chap_passwd=ctrl['vpsa_chap_secret'])
return None
def _check_access_key_validity(self):
"""Check VPSA access key"""
if not self.access_key:
raise exception.ZadaraInvalidAccessKey()
active_ctrl = self._get_active_controller_details()
if active_ctrl is None:
raise exception.ZadaraInvalidAccessKey()
def _get_vpsa_volume(self, name):
"""Returns a single vpsa volume based on the display name"""
volume = None
display_name = name
if re.search(r"\s", name):
display_name = re.split(r"\s", name)[0]
data = self.send_cmd('get_volume_by_name',
display_name=display_name)
if data['status'] != 0:
return None
volumes = data['volumes']
for vol in volumes:
if vol['display_name'] == name:
volume = vol
break
return volume
def _get_vpsa_volume_by_id(self, vpsa_vol):
"""Returns a single vpsa volume based on name"""
data = self.send_cmd('get_volume', vpsa_vol=vpsa_vol)
return data['volume']
def _get_volume_cg_name(self, name):
"""Return name of the consistency group for the volume.
cg-name is a volume uniqe identifier (legacy attribute)
and not consistency group as it may imply.
"""
volume = self._get_vpsa_volume(name)
if volume is not None:
return volume['cg_name']
return None
def _get_all_vpsa_snapshots(self):
"""Returns snapshots from all vpsa volumes"""
data = self.send_cmd('list_snapshots')
return data['snapshots']
def _get_all_vpsa_volumes(self):
"""Returns all vpsa block volumes from the configured pool"""
data = self.send_cmd('list_volumes')
# FIXME: Work around to filter volumes belonging to given pool
# Remove this when we have the API fixed to filter based
# on pools. This API today does not have virtual_capacity field
volumes = []
for volume in data['volumes']:
if volume['pool_name'] == self.conf.zadara_vpsa_poolname:
volumes.append(volume)
return volumes
def _get_server_name(self, initiator, share):
"""Return VPSA's name for server object.
'share' will be true to search for filesystem volumes
"""
data = self.send_cmd('list_servers')
servers = data.get('servers', [])
for server in servers:
if share:
if server['iscsi_ip'] == initiator:
return server['name']
else:
if server['iqn'] == initiator:
return server['name']
return None
def _create_vpsa_server(self, iqn=None, iscsi_ip=None):
"""Create server object within VPSA (if doesn't exist)."""
initiator = iscsi_ip if iscsi_ip else iqn
share = True if iscsi_ip else False
vpsa_srv = self._get_server_name(initiator, share)
if not vpsa_srv:
data = self.send_cmd('create_server', iqn=iqn, iscsi_ip=iscsi_ip)
if data['status'] != 0:
return None
vpsa_srv = data['server_name']
return vpsa_srv
def _get_servers_attached_to_volume(self, vpsa_vol):
"""Return all servers attached to volume."""
servers = vpsa_vol.get('server_ext_names')
list_servers = []
if servers:
list_servers = servers.split(',')
return list_servers
def _detach_vpsa_volume(self, vpsa_vol, vpsa_srv=None):
"""Detach volume from all attached servers."""
if vpsa_srv:
list_servers_ids = [vpsa_srv]
else:
list_servers_ids = self._get_servers_attached_to_volume(vpsa_vol)
for server_id in list_servers_ids:
# Detach volume from server
self.send_cmd('detach_volume', vpsa_srv=server_id,
vpsa_vol=vpsa_vol['name'])
def _get_volume_snapshots(self, cg_name):
"""Get snapshots in the consistency group"""
data = self.send_cmd('list_vol_snapshots', cg_name=cg_name)
snapshots = data.get('snapshots', [])
return snapshots
def _get_snap_id(self, cg_name, snap_name):
"""Return snapshot ID for particular volume."""
snapshots = self._get_volume_snapshots(cg_name)
for snap_vol in snapshots:
if snap_vol['display_name'] == snap_name:
return snap_vol['name']
return None
def _get_pool_capacity(self, pool_name):
"""Return pool's total and available capacities."""
data = self.send_cmd('get_pool', pool_name=pool_name)
pool = data.get('pool')
if pool is not None:
total = int(pool['capacity'])
free = int(pool['available_capacity'])
provisioned = int(pool['provisioned_capacity'])
LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free, '
'%(provisioned)sGB provisioned',
{'name': pool_name, 'total': total,
'free': free, 'provisioned': provisioned})
return total, free, provisioned
return 'unknown', 'unknown', 'unknown'

53
cinder/volume/drivers/zadara/exception.py

@ -0,0 +1,53 @@
# Copyright (c) 2020 Zadara Storage, Inc.
# 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.
"""
Zadara Cinder driver exception handling.
"""
from cinder import exception
from cinder.i18n import _
class ZadaraSessionRequestException(exception.VolumeDriverException):
message = _("%(msg)s")
class ZadaraCinderInvalidAccessKey(exception.VolumeDriverException):
message = "Invalid VPSA access key"
class ZadaraVPSANoActiveController(exception.VolumeDriverException):
message = _("Unable to find any active VPSA controller")
class ZadaraVolumeNotFound(exception.VolumeDriverException):
message = "%(reason)s"
class ZadaraServerCreateFailure(exception.VolumeDriverException):
message = _("Unable to create server object for initiator %(name)s")
class ZadaraAttachmentsNotFound(exception.VolumeDriverException):
message = _("Failed to retrieve attachments for volume %(name)s")
class ZadaraInvalidAttachmentInfo(exception.VolumeDriverException):
message = _("Invalid attachment info for volume %(name)s: %(reason)s")
class ZadaraServerNotFound(exception.VolumeDriverException):
message = _("Unable to find server object for initiator %(name)s")

729
cinder/volume/drivers/zadara/zadara.py

@ -0,0 +1,729 @@
# Copyright (c) 2019 Zadara Storage, Inc.
# 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 Zadara Virtual Private Storage Array (VPSA).
This driver requires VPSA with API version 15.07 or higher.
"""
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import strutils
import six
from cinder import exception as cinder_exception
from cinder.i18n import _
from cinder import interface
from cinder.objects import fields
from cinder.volume import configuration
from cinder.volume import driver
from cinder.volume.drivers.zadara import common
from cinder.volume.drivers.zadara import exception as zadara_exception
from cinder.volume import volume_utils
CONF = cfg.CONF
CONF.register_opts(common.zadara_opts, group=configuration.SHARED_CONF_GROUP)
LOG = logging.getLogger(__name__)
cinder_opts = [
cfg.BoolOpt('zadara_use_iser',
default=True,
help='VPSA - Use ISER instead of iSCSI'),
cfg.StrOpt('zadara_vol_name_template',
default='OS_%s',
help='VPSA - Default template for VPSA volume names')]
@interface.volumedriver
class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
"""Zadara VPSA iSCSI/iSER volume driver.
.. code-block:: none
Version history:
15.07 - Initial driver
16.05 - Move from httplib to requests
19.08 - Add API access key authentication option
20.01 - Move to json format from xml. Provide manage/unmanage
volume/snapshot feature
20.12-01 - Merging with the common code for all the openstack drivers
20.12-02 - Common code changed as part of fixing
Zadara github issue #18723
20.12-03 - Adding the metadata support while creating volume to
configure vpsa.
20.12-20 - IPv6 connectivity support for Cinder driver
20.12-24 - Optimizing get manageable volumes and snapshots
"""
VERSION = '20.12-24'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "ZadaraStorage_VPSA_CI"
def __init__(self, *args, **kwargs):
super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
self.vpsa = None
self.configuration.append_config_values(common.zadara_opts)
self.configuration.append_config_values(cinder_opts)
# The valid list of volume options that can be specified
# as the metadata while creating cinder volume
self.vol_options = ['crypt', 'compress',
'dedupe', 'attachpolicies']
@staticmethod
def get_driver_options():
driver_opts = []
driver_opts.extend(common.zadara_opts)
driver_opts.extend(cinder_opts)
return driver_opts
def _check_access_key_validity(self):
try:
self.vpsa._check_access_key_validity()
except common.exception.ZadaraInvalidAccessKey:
raise zadara_exception.ZadaraCinderInvalidAccessKey()
def do_setup(self, context):
"""Any initialization the volume driver does while starting.
Establishes initial connection with VPSA and retrieves access_key.
Need to pass driver_ssl_cert_path here (and not fetch it from the
config opts directly in common code), because this config option is
different for different drivers and so cannot be figured in the
common code.
"""
driver_ssl_cert_path = self.configuration.driver_ssl_cert_path
self.vpsa = common.ZadaraVPSAConnection(self.configuration,
driver_ssl_cert_path, True)
self._check_access_key_validity()
def check_for_setup_error(self):
"""Returns an error (exception) if prerequisites aren't met."""
self._check_access_key_validity()
def local_path(self, volume):
"""Return local path to existing local volume."""
raise NotImplementedError()
def _get_zadara_vol_template_name(self, vol_name):
return self.configuration.zadara_vol_name_template % vol_name
def _get_vpsa_volume(self, volume, raise_exception=True):
vpsa_volume = None
if volume.provider_location:
vpsa_volume = (self.vpsa._get_vpsa_volume_by_id(
volume.provider_location))
else:
vol_name = self._get_zadara_vol_template_name(volume.name)
vpsa_volume = self.vpsa._get_vpsa_volume(vol_name)
if not vpsa_volume:
vol_name = self._get_zadara_vol_template_name(volume.name)
msg = (_('Backend Volume %(name)s not found') % {'name': vol_name})
if raise_exception:
LOG.error(msg)
raise cinder_exception.VolumeDriverException(message=msg)
LOG.warning(msg)
return vpsa_volume
def vpsa_send_cmd(self, cmd, **kwargs):
try:
response = self.vpsa.send_cmd(cmd, **kwargs)
except common.exception.UnknownCmd as e:
raise cinder_exception.UnknownCmd(cmd=e.cmd)
except common.exception.SessionRequestException as e:
raise zadara_exception.ZadaraSessionRequestException(msg=e.msg)
except common.exception.BadHTTPResponseStatus as e:
raise cinder_exception.BadHTTPResponseStatus(status=e.status)
except common.exception.FailedCmdWithDump as e:
raise cinder_exception.FailedCmdWithDump(status=e.status,
data=e.data)
except common.exception.ZadaraInvalidAccessKey:
raise zadara_exception.ZadaraCinderInvalidAccessKey()
return response
def _validate_existing_ref(self, existing_ref):