617 lines
23 KiB
Python
617 lines
23 KiB
Python
# Copyright 2014 Violin Memory, 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.
|
|
|
|
"""
|
|
Violin Memory 6000 Series All-Flash Array Common Driver for Openstack Cinder
|
|
|
|
Provides common (ie., non-protocol specific) management functions for
|
|
V6000 series flash arrays.
|
|
|
|
Backend array communication is handled via VMEM's python library
|
|
called 'xg-tools'.
|
|
|
|
NOTE: this driver file requires the use of synchronization points for
|
|
certain types of backend operations, and as a result may not work
|
|
properly in an active-active HA configuration. See OpenStack Cinder
|
|
driver documentation for more information.
|
|
"""
|
|
|
|
import re
|
|
import time
|
|
|
|
from oslo.config import cfg
|
|
from oslo.utils import importutils
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LE, _LW, _LI
|
|
from cinder.openstack.common import log as logging
|
|
from cinder.openstack.common import loopingcall
|
|
from cinder import utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
vxg = importutils.try_import("vxg")
|
|
if vxg:
|
|
LOG.info(_LI("Running with xg-tools version: %s."), vxg.__version__)
|
|
|
|
# version vmos versions V6.3.0.4 or newer
|
|
VMOS_SUPPORTED_VERSION_PATTERNS = ['V6.3.0.[4-9]', 'V6.3.[1-9].?[0-9]?']
|
|
|
|
violin_opts = [
|
|
cfg.StrOpt('gateway_mga',
|
|
default=None,
|
|
help='IP address or hostname of mg-a'),
|
|
cfg.StrOpt('gateway_mgb',
|
|
default=None,
|
|
help='IP address or hostname of mg-b'),
|
|
cfg.BoolOpt('use_igroups',
|
|
default=False,
|
|
help='Use igroups to manage targets and initiators'),
|
|
cfg.IntOpt('request_timeout',
|
|
default=300,
|
|
help='Global backend request timeout, in seconds'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(violin_opts)
|
|
|
|
|
|
class V6000Common(object):
|
|
"""Contains common code for the Violin V6000 drivers.
|
|
|
|
Version history:
|
|
1.0 - Initial driver
|
|
"""
|
|
|
|
VERSION = '1.0'
|
|
|
|
def __init__(self, config):
|
|
self.vip = None
|
|
self.mga = None
|
|
self.mgb = None
|
|
self.container = ""
|
|
self.config = config
|
|
|
|
def do_setup(self, context):
|
|
"""Any initialization the driver does while starting."""
|
|
if not self.config.san_ip:
|
|
raise exception.InvalidInput(
|
|
reason=_('Gateway VIP option \'san_ip\' is not set'))
|
|
if not self.config.gateway_mga:
|
|
raise exception.InvalidInput(
|
|
reason=_('Gateway MG-A IP option \'gateway_mga\' is not set'))
|
|
if not self.config.gateway_mgb:
|
|
raise exception.InvalidInput(
|
|
reason=_('Gateway MG-B IP option \'gateway_mgb\' is not set'))
|
|
if self.config.request_timeout <= 0:
|
|
raise exception.InvalidInput(
|
|
reason=_('Global timeout option \'request_timeout\' must be '
|
|
'greater than 0'))
|
|
|
|
self.vip = vxg.open(self.config.san_ip, self.config.san_login,
|
|
self.config.san_password, keepalive=True)
|
|
self.mga = vxg.open(self.config.gateway_mga, self.config.san_login,
|
|
self.config.san_password, keepalive=True)
|
|
self.mgb = vxg.open(self.config.gateway_mgb, self.config.san_login,
|
|
self.config.san_password, keepalive=True)
|
|
|
|
ret_dict = self.vip.basic.get_node_values(
|
|
"/vshare/state/local/container/*")
|
|
if ret_dict:
|
|
self.container = ret_dict.items()[0][1]
|
|
|
|
def check_for_setup_error(self):
|
|
"""Returns an error if prerequisites aren't met."""
|
|
|
|
if len(self.container) == 0:
|
|
msg = _('container is missing')
|
|
raise exception.ViolinInvalidBackendConfig(reason=msg)
|
|
|
|
if not self._is_supported_vmos_version(self.vip.version):
|
|
msg = _('VMOS version is not supported')
|
|
raise exception.ViolinInvalidBackendConfig(reason=msg)
|
|
|
|
bn1 = ("/vshare/state/local/container/%s/threshold/usedspace"
|
|
"/threshold_hard_val" % self.container)
|
|
bn2 = ("/vshare/state/local/container/%s/threshold/provision"
|
|
"/threshold_hard_val" % self.container)
|
|
ret_dict = self.vip.basic.get_node_values([bn1, bn2])
|
|
|
|
for node in ret_dict:
|
|
# The infrastructure does not support space reclamation so
|
|
# ensure it is disabled. When used space exceeds the hard
|
|
# limit, snapshot space reclamation begins. Default is 0
|
|
# => no space reclamation.
|
|
#
|
|
if node.endswith('/usedspace/threshold_hard_val'):
|
|
if ret_dict[node] != 0:
|
|
msg = _('space reclamation threshold is enabled but not '
|
|
'supported by Cinder infrastructure.')
|
|
raise exception.ViolinInvalidBackendConfig(reason=msg)
|
|
|
|
# The infrastructure does not support overprovisioning so
|
|
# ensure it is disabled. When provisioned space exceeds
|
|
# the hard limit, further provisioning is stopped.
|
|
# Default is 100 => provisioned space equals usable space.
|
|
#
|
|
elif node.endswith('/provision/threshold_hard_val'):
|
|
if ret_dict[node] != 100:
|
|
msg = _('provisioned space threshold is not equal to '
|
|
'usable space.')
|
|
raise exception.ViolinInvalidBackendConfig(reason=msg)
|
|
|
|
@utils.synchronized('vmem-lun')
|
|
def _create_lun(self, volume):
|
|
"""Creates a new lun.
|
|
|
|
The equivalent CLI command is "lun create container
|
|
<container_name> name <lun_name> size <gb>"
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
"""
|
|
lun_type = '0'
|
|
|
|
LOG.debug("Creating LUN %(name)s, %(size)s GB." %
|
|
{'name': volume['name'], 'size': volume['size']})
|
|
|
|
if self.config.san_thin_provision:
|
|
lun_type = '1'
|
|
|
|
# using the defaults for fields: quantity, nozero,
|
|
# readonly, startnum, blksize, naca, alua, preferredport
|
|
#
|
|
try:
|
|
self._send_cmd(self.vip.lun.create_lun,
|
|
'LUN create: success!',
|
|
self.container, volume['id'],
|
|
volume['size'], 1, '0', lun_type, 'w',
|
|
1, 512, False, False, None)
|
|
|
|
except exception.ViolinBackendErrExists:
|
|
LOG.debug("Lun %s already exists, continuing.", volume['id'])
|
|
|
|
except Exception:
|
|
LOG.warn(_LW("Lun create for %s failed!"), volume['id'])
|
|
raise
|
|
|
|
@utils.synchronized('vmem-lun')
|
|
def _delete_lun(self, volume):
|
|
"""Deletes a lun.
|
|
|
|
The equivalent CLI command is "no lun create container
|
|
<container_name> name <lun_name>"
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
"""
|
|
success_msgs = ['lun deletion started', '']
|
|
|
|
LOG.debug("Deleting lun %s.", volume['id'])
|
|
|
|
try:
|
|
self._send_cmd(self.vip.lun.bulk_delete_luns,
|
|
success_msgs, self.container, volume['id'])
|
|
|
|
except exception.ViolinBackendErrNotFound:
|
|
LOG.debug("Lun %s already deleted, continuing.", volume['id'])
|
|
|
|
except exception.ViolinBackendErrExists:
|
|
LOG.warn(_LW("Lun %s has dependent snapshots, skipping."),
|
|
volume['id'])
|
|
raise exception.VolumeIsBusy(volume_name=volume['id'])
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("Lun delete for %s failed!"), volume['id'])
|
|
raise
|
|
|
|
@utils.synchronized('vmem-lun')
|
|
def _extend_lun(self, volume, new_size):
|
|
"""Extend an existing volume's size.
|
|
|
|
The equivalent CLI command is "lun resize container
|
|
<container_name> name <lun_name> size <gb>"
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
new_size -- new (increased) size in GB to be applied
|
|
"""
|
|
LOG.debug("Extending lun %(id)s, from %(size)s to %(new_size)s GB." %
|
|
{'id': volume['id'], 'size': volume['size'],
|
|
'new_size': new_size})
|
|
|
|
try:
|
|
self._send_cmd(self.vip.lun.resize_lun, 'Success',
|
|
self.container, volume['id'], new_size)
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("LUN extend for %s failed!"), volume['id'])
|
|
raise
|
|
|
|
@utils.synchronized('vmem-snap')
|
|
def _create_lun_snapshot(self, snapshot):
|
|
"""Creates a new snapshot for a lun.
|
|
|
|
The equivalent CLI command is "snapshot create container
|
|
<container> lun <volume_name> name <snapshot_name>"
|
|
|
|
Arguments:
|
|
snapshot -- snapshot object provided by the Manager
|
|
"""
|
|
LOG.debug("Creating snapshot %s.", snapshot['id'])
|
|
|
|
try:
|
|
self._send_cmd(self.vip.snapshot.create_lun_snapshot,
|
|
'Snapshot create: success!',
|
|
self.container, snapshot['volume_id'],
|
|
snapshot['id'])
|
|
|
|
except exception.ViolinBackendErrExists:
|
|
LOG.debug("Snapshot %s already exists, continuing.",
|
|
snapshot['id'])
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("LUN snapshot create for %s failed!"),
|
|
snapshot['id'])
|
|
raise
|
|
|
|
@utils.synchronized('vmem-snap')
|
|
def _delete_lun_snapshot(self, snapshot):
|
|
"""Deletes an existing snapshot for a lun.
|
|
|
|
The equivalent CLI command is "no snapshot create container
|
|
<container> lun <volume_name> name <snapshot_name>"
|
|
|
|
Arguments:
|
|
snapshot -- snapshot object provided by the Manager
|
|
"""
|
|
LOG.debug("Deleting snapshot %s.", snapshot['id'])
|
|
|
|
try:
|
|
self._send_cmd(self.vip.snapshot.delete_lun_snapshot,
|
|
'Snapshot delete: success!',
|
|
self.container, snapshot['volume_id'],
|
|
snapshot['id'])
|
|
|
|
except exception.ViolinBackendErrNotFound:
|
|
LOG.debug("Snapshot %s already deleted, continuing.",
|
|
snapshot['id'])
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("LUN snapshot delete for %s failed!"),
|
|
snapshot['id'])
|
|
raise
|
|
|
|
def _get_lun_id(self, volume_name):
|
|
"""Queries the gateway to find the lun id for the exported volume.
|
|
|
|
Arguments:
|
|
volume_name -- LUN to query
|
|
|
|
Returns:
|
|
LUN ID for the exported lun.
|
|
"""
|
|
lun_id = -1
|
|
|
|
prefix = "/vshare/config/export/container"
|
|
bn = "%s/%s/lun/%s/target/**" % (prefix, self.container, volume_name)
|
|
resp = self.vip.basic.get_node_values(bn)
|
|
|
|
for node in resp:
|
|
if node.endswith('/lun_id'):
|
|
lun_id = resp[node]
|
|
break
|
|
|
|
if lun_id == -1:
|
|
raise exception.ViolinBackendErrNotFound()
|
|
return lun_id
|
|
|
|
def _get_snapshot_id(self, volume_name, snapshot_name):
|
|
"""Queries the gateway to find the lun id for the exported snapshot.
|
|
|
|
Arguments:
|
|
volume_name -- LUN to query
|
|
snapshot_name -- Exported snapshot associated with LUN
|
|
|
|
Returns:
|
|
LUN ID for the exported lun
|
|
"""
|
|
lun_id = -1
|
|
|
|
prefix = "/vshare/config/export/snapshot/container"
|
|
bn = "%s/%s/lun/%s/snap/%s/target/**" \
|
|
% (prefix, self.container, volume_name, snapshot_name)
|
|
resp = self.vip.basic.get_node_values(bn)
|
|
|
|
for node in resp:
|
|
if node.endswith('/lun_id'):
|
|
lun_id = resp[node]
|
|
break
|
|
|
|
if lun_id == -1:
|
|
raise exception.ViolinBackendErrNotFound()
|
|
return lun_id
|
|
|
|
def _send_cmd(self, request_func, success_msgs, *args):
|
|
"""Run an XG request function, and retry as needed.
|
|
|
|
The request will be retried until it returns a success
|
|
message, a failure message, or the global request timeout is
|
|
hit.
|
|
|
|
This wrapper is meant to deal with backend requests that can
|
|
fail for any variety of reasons, for instance, when the system
|
|
is already busy handling other LUN requests. It is also smart
|
|
enough to give up if clustering is down (eg no HA available),
|
|
there is no space left, or other "fatal" errors are returned
|
|
(see _fatal_error_code() for a list of all known error
|
|
conditions).
|
|
|
|
Arguments:
|
|
request_func -- XG api method to call
|
|
success_msgs -- Success messages expected from the backend
|
|
*args -- argument array to be passed to the request_func
|
|
|
|
Returns:
|
|
The response dict from the last XG call.
|
|
"""
|
|
resp = {}
|
|
start = time.time()
|
|
done = False
|
|
|
|
if isinstance(success_msgs, basestring):
|
|
success_msgs = [success_msgs]
|
|
|
|
while not done:
|
|
if time.time() - start >= self.config.request_timeout:
|
|
raise exception.ViolinRequestRetryTimeout(
|
|
timeout=self.config.request_timeout)
|
|
|
|
resp = request_func(*args)
|
|
|
|
if not resp['message']:
|
|
# XG requests will return None for a message if no message
|
|
# string is passed in the raw response
|
|
resp['message'] = ''
|
|
|
|
for msg in success_msgs:
|
|
if not resp['code'] and msg in resp['message']:
|
|
done = True
|
|
break
|
|
|
|
self._fatal_error_code(resp)
|
|
|
|
return resp
|
|
|
|
def _send_cmd_and_verify(self, request_func, verify_func,
|
|
request_success_msgs, rargs=None, vargs=None):
|
|
"""Run an XG request function, retry if needed, and verify success.
|
|
|
|
If the verification fails, then retry the request/verify
|
|
cycle until both functions are successful, the request
|
|
function returns a failure message, or the global request
|
|
timeout is hit.
|
|
|
|
This wrapper is meant to deal with backend requests that can
|
|
fail for any variety of reasons, for instance, when the system
|
|
is already busy handling other LUN requests. It is also smart
|
|
enough to give up if clustering is down (eg no HA available),
|
|
there is no space left, or other "fatal" errors are returned
|
|
(see _fatal_error_code() for a list of all known error
|
|
conditions).
|
|
|
|
Arguments:
|
|
request_func -- XG api method to call
|
|
verify_func -- function to call to verify request was
|
|
completed successfully (eg for export)
|
|
request_success_msg -- Success message expected from the backend
|
|
for the request_func
|
|
rargs -- argument array to be passed to the
|
|
request_func
|
|
vargs -- argument array to be passed to the
|
|
verify_func
|
|
|
|
Returns:
|
|
The response dict from the last XG call.
|
|
"""
|
|
resp = {}
|
|
start = time.time()
|
|
request_needed = True
|
|
verify_needed = True
|
|
|
|
if isinstance(request_success_msgs, basestring):
|
|
request_success_msgs = [request_success_msgs]
|
|
|
|
rargs = rargs if rargs else []
|
|
vargs = vargs if vargs else []
|
|
|
|
while request_needed or verify_needed:
|
|
if time.time() - start >= self.config.request_timeout:
|
|
raise exception.ViolinRequestRetryTimeout(
|
|
timeout=self.config.request_timeout)
|
|
|
|
if request_needed:
|
|
resp = request_func(*rargs)
|
|
if not resp['message']:
|
|
# XG requests will return None for a message if no message
|
|
# string is passed int the raw response
|
|
resp['message'] = ''
|
|
for msg in request_success_msgs:
|
|
if not resp['code'] and msg in resp['message']:
|
|
# XG request func was completed
|
|
request_needed = False
|
|
break
|
|
self._fatal_error_code(resp)
|
|
|
|
elif verify_needed:
|
|
success = verify_func(*vargs)
|
|
if success:
|
|
# XG verify func was completed
|
|
verify_needed = False
|
|
else:
|
|
# try sending the request again
|
|
request_needed = True
|
|
|
|
return resp
|
|
|
|
def _get_igroup(self, volume, connector):
|
|
"""Gets the igroup that should be used when configuring a volume.
|
|
|
|
Arguments:
|
|
volume -- volume object used to determine the igroup name
|
|
|
|
Returns:
|
|
igroup_name -- name of igroup (for configuring targets &
|
|
initiators)
|
|
"""
|
|
# Use the connector's primary hostname and use that as the
|
|
# name of the igroup. The name must follow syntax rules
|
|
# required by the array: "must contain only alphanumeric
|
|
# characters, dashes, and underscores. The first character
|
|
# must be alphanumeric".
|
|
#
|
|
igroup_name = re.sub(r'[\W]', '_', connector['host'])
|
|
|
|
# verify that the igroup has been created on the backend, and
|
|
# if it doesn't exist, create it!
|
|
#
|
|
bn = "/vshare/config/igroup/%s" % igroup_name
|
|
resp = self.vip.basic.get_node_values(bn)
|
|
|
|
if not len(resp):
|
|
self.vip.igroup.create_igroup(igroup_name)
|
|
|
|
return igroup_name
|
|
|
|
def _wait_for_export_config(self, volume_name, snapshot_name=None,
|
|
state=False):
|
|
"""Polls backend to verify volume's export configuration.
|
|
|
|
XG sets/queries following a request to create or delete a lun
|
|
export may fail on the backend if vshared is still processing
|
|
the export action (or times out). We can check whether it is
|
|
done by polling the export binding for a lun to ensure it is
|
|
created or deleted.
|
|
|
|
This function will try to verify the creation or removal of
|
|
export state on both gateway nodes of the array every 5
|
|
seconds.
|
|
|
|
Arguments:
|
|
volume_name -- name of volume
|
|
snapshot_name -- name of volume's snapshot
|
|
state -- True to poll for existence, False for lack of
|
|
|
|
Returns:
|
|
True if the export state was correctly added or removed
|
|
(depending on 'state' param)
|
|
"""
|
|
if not snapshot_name:
|
|
bn = "/vshare/config/export/container/%s/lun/%s" \
|
|
% (self.container, volume_name)
|
|
else:
|
|
bn = "/vshare/config/export/snapshot/container/%s/lun/%s/snap/%s" \
|
|
% (self.container, volume_name, snapshot_name)
|
|
|
|
def _loop_func(state):
|
|
status = [False, False]
|
|
mg_conns = [self.mga, self.mgb]
|
|
|
|
LOG.debug("Entering _wait_for_export_config loop: state=%s.",
|
|
state)
|
|
|
|
for node_id in xrange(2):
|
|
resp = mg_conns[node_id].basic.get_node_values(bn)
|
|
if state and len(resp.keys()):
|
|
status[node_id] = True
|
|
elif (not state) and (not len(resp.keys())):
|
|
status[node_id] = True
|
|
|
|
if status[0] and status[1]:
|
|
raise loopingcall.LoopingCallDone(retvalue=True)
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_loop_func, state)
|
|
success = timer.start(interval=5).wait()
|
|
|
|
return success
|
|
|
|
def _is_supported_vmos_version(self, version_string):
|
|
"""Check that the array s/w version is supported. """
|
|
for pattern in VMOS_SUPPORTED_VERSION_PATTERNS:
|
|
if re.match(pattern, version_string):
|
|
LOG.info(_LI("Verified VMOS version %s is supported."),
|
|
version_string)
|
|
return True
|
|
return False
|
|
|
|
def _fatal_error_code(self, response):
|
|
"""Raise an exception for certain errors in a XG response.
|
|
|
|
Error codes are extracted from vdmd_mgmt.c.
|
|
|
|
Arguments:
|
|
response -- a response dict result from an XG request
|
|
"""
|
|
# known non-fatal response codes:
|
|
# 1024: 'lun deletion in progress, try again later'
|
|
# 14032: 'lc_err_lock_busy'
|
|
|
|
if response['code'] == 14000:
|
|
# lc_generic_error
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14002:
|
|
# lc_err_assertion_failed
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14004:
|
|
# lc_err_not_found
|
|
raise exception.ViolinBackendErrNotFound()
|
|
elif response['code'] == 14005:
|
|
# lc_err_exists
|
|
raise exception.ViolinBackendErrExists()
|
|
elif response['code'] == 14008:
|
|
# lc_err_unexpected_arg
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14014:
|
|
# lc_err_io_error
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14016:
|
|
# lc_err_io_closed
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14017:
|
|
# lc_err_io_timeout
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14021:
|
|
# lc_err_unexpected_case
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14025:
|
|
# lc_err_no_fs_space
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14035:
|
|
# lc_err_range
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14036:
|
|
# lc_err_invalid_param
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 14121:
|
|
# lc_err_cancelled_err
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 512:
|
|
# Not enough free space in container (vdmd bug)
|
|
raise exception.ViolinBackendErr(message=response['message'])
|
|
elif response['code'] == 1 and 'LUN ID conflict' \
|
|
in response['message']:
|
|
# lun id conflict while attempting to export
|
|
raise exception.ViolinBackendErr(message=response['message'])
|