3ebefda515
Add 'access_key' API authentication login method given as input and move access_key to HTTP request header. Fix unittest accordingly. Change-Id: I11df21a7a213a0700d277d57ceee40f636955c50
765 lines
29 KiB
Python
765 lines
29 KiB
Python
# 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 defusedxml import lxml
|
|
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 = lxml.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 objects.getchildren():
|
|
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_volume_stats(self, refresh=False):
|
|
"""Get volume stats.
|
|
|
|
If 'refresh' is True, run update the stats first.
|
|
"""
|
|
|
|
if refresh:
|
|
self._update_volume_stats()
|
|
|
|
return self._stats
|
|
|
|
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
|