cinder/cinder/volume/drivers/violin/v6000_common.py

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'])