599 lines
21 KiB
Python
599 lines
21 KiB
Python
# Copyright 2013 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 iSCSI Driver for Openstack Cinder
|
|
|
|
Provides iSCSI specific LUN services for V6000 series flash arrays.
|
|
|
|
This driver requires VMOS v6.3.0.4 or newer software on the array.
|
|
|
|
You will need to install the python xg-tools client:
|
|
sudo pip install xg-tools
|
|
|
|
Set the following in the cinder.conf file to enable the VMEM V6000
|
|
ISCSI Driver along with the required flags:
|
|
|
|
volume_driver=cinder.volume.drivers.violin.v6000_iscsi.V6000ISCSIDriver
|
|
|
|
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 random
|
|
|
|
from oslo.utils import units
|
|
|
|
from cinder import context
|
|
from cinder.db.sqlalchemy import models
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LE, _LI, _LW
|
|
from cinder.openstack.common import log as logging
|
|
from cinder.openstack.common import loopingcall
|
|
from cinder import utils
|
|
from cinder.volume import driver
|
|
from cinder.volume.drivers.san import san
|
|
from cinder.volume.drivers.violin import v6000_common
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class V6000ISCSIDriver(driver.ISCSIDriver):
|
|
"""Executes commands relating to iSCSI-based Violin Memory Arrays.
|
|
|
|
Version history:
|
|
1.0 - Initial driver
|
|
"""
|
|
|
|
VERSION = '1.0'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(V6000ISCSIDriver, self).__init__(*args, **kwargs)
|
|
self.array_info = []
|
|
self.gateway_iscsi_ip_addresses_mga = []
|
|
self.gateway_iscsi_ip_addresses_mgb = []
|
|
self.stats = {}
|
|
self.configuration.append_config_values(v6000_common.violin_opts)
|
|
self.configuration.append_config_values(san.san_opts)
|
|
self.common = v6000_common.V6000Common(self.configuration)
|
|
|
|
LOG.info(_LI("Initialized driver %(name)s version: %(vers)s.") %
|
|
{'name': self.__class__.__name__, 'vers': self.VERSION})
|
|
|
|
def do_setup(self, context):
|
|
"""Any initialization the driver does while starting."""
|
|
super(V6000ISCSIDriver, self).do_setup(context)
|
|
self.common.do_setup(context)
|
|
|
|
self.gateway_iscsi_ip_addresses_mga = self._get_active_iscsi_ips(
|
|
self.common.mga)
|
|
for ip in self.gateway_iscsi_ip_addresses_mga:
|
|
self.array_info.append({"node": self._get_hostname('mga'),
|
|
"addr": ip,
|
|
"conn": self.common.mga})
|
|
self.gateway_iscsi_ip_addresses_mgb = self._get_active_iscsi_ips(
|
|
self.common.mgb)
|
|
for ip in self.gateway_iscsi_ip_addresses_mgb:
|
|
self.array_info.append({"node": self._get_hostname('mgb'),
|
|
"addr": ip,
|
|
"conn": self.common.mgb})
|
|
|
|
def check_for_setup_error(self):
|
|
"""Returns an error if prerequisites aren't met."""
|
|
self.common.check_for_setup_error()
|
|
|
|
bn = "/vshare/config/iscsi/enable"
|
|
resp = self.common.vip.basic.get_node_values(bn)
|
|
if resp[bn] is not True:
|
|
raise exception.ViolinInvalidBackendConfig(
|
|
reason=_('iSCSI is not enabled'))
|
|
if len(self.gateway_iscsi_ip_addresses_mga) == 0:
|
|
raise exception.ViolinInvalidBackendConfig(
|
|
reason=_('no available iSCSI IPs on mga'))
|
|
if len(self.gateway_iscsi_ip_addresses_mgb) == 0:
|
|
raise exception.ViolinInvalidBackendConfig(
|
|
reason=_('no available iSCSI IPs on mgb'))
|
|
|
|
def create_volume(self, volume):
|
|
"""Creates a volume."""
|
|
self.common._create_lun(volume)
|
|
|
|
def delete_volume(self, volume):
|
|
"""Deletes a volume."""
|
|
self.common._delete_lun(volume)
|
|
|
|
def extend_volume(self, volume, new_size):
|
|
"""Deletes a volume."""
|
|
self.common._extend_lun(volume, new_size)
|
|
|
|
def create_snapshot(self, snapshot):
|
|
"""Creates a snapshot from an existing volume."""
|
|
self.common._create_lun_snapshot(snapshot)
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
"""Deletes a snapshot."""
|
|
self.common._delete_lun_snapshot(snapshot)
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Creates a volume from a snapshot."""
|
|
ctxt = context.get_admin_context()
|
|
snapshot['size'] = snapshot['volume']['size']
|
|
self.common._create_lun(volume)
|
|
self.copy_volume_data(ctxt, snapshot, volume)
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Creates a full clone of the specified volume."""
|
|
ctxt = context.get_admin_context()
|
|
self.common._create_lun(volume)
|
|
self.copy_volume_data(ctxt, src_vref, volume)
|
|
|
|
def ensure_export(self, context, volume):
|
|
"""Synchronously checks and re-exports volumes at cinder start time."""
|
|
pass
|
|
|
|
def create_export(self, context, volume):
|
|
"""Exports the volume."""
|
|
pass
|
|
|
|
def remove_export(self, context, volume):
|
|
"""Removes an export for a logical volume."""
|
|
pass
|
|
|
|
def initialize_connection(self, volume, connector):
|
|
"""Initializes the connection (target<-->initiator)."""
|
|
igroup = None
|
|
|
|
if self.configuration.use_igroups:
|
|
#
|
|
# Most drivers don't use igroups, because there are a
|
|
# number of issues with multipathing and iscsi/fcp where
|
|
# lun devices either aren't cleaned up properly or are
|
|
# stale (from previous scans).
|
|
#
|
|
|
|
# If the customer really wants igroups for whatever
|
|
# reason, we create a new igroup for each host/hypervisor.
|
|
# Every lun that is exported to the particular
|
|
# hypervisor/host will be contained in this igroup. This
|
|
# should prevent other hosts from seeing luns they aren't
|
|
# using when they perform scans.
|
|
#
|
|
igroup = self.common._get_igroup(volume, connector)
|
|
self._add_igroup_member(connector, igroup)
|
|
|
|
vol = self._get_short_name(volume['id'])
|
|
tgt = self._create_iscsi_target(volume)
|
|
if isinstance(volume, models.Volume):
|
|
lun = self._export_lun(volume, connector, igroup)
|
|
else:
|
|
lun = self._export_snapshot(volume, connector, igroup)
|
|
|
|
iqn = "%s%s:%s" % (self.configuration.iscsi_target_prefix,
|
|
tgt['node'], vol)
|
|
self.common.vip.basic.save_config()
|
|
|
|
properties = {}
|
|
properties['target_discovered'] = False
|
|
properties['target_portal'] = '%s:%d' \
|
|
% (tgt['addr'], self.configuration.iscsi_port)
|
|
properties['target_iqn'] = iqn
|
|
properties['target_lun'] = lun
|
|
properties['volume_id'] = volume['id']
|
|
properties['auth_method'] = 'CHAP'
|
|
properties['auth_username'] = ''
|
|
properties['auth_password'] = ''
|
|
|
|
return {'driver_volume_type': 'iscsi', 'data': properties}
|
|
|
|
def terminate_connection(self, volume, connector, force=False, **kwargs):
|
|
"""Terminates the connection (target<-->initiator)."""
|
|
if isinstance(volume, models.Volume):
|
|
self._unexport_lun(volume)
|
|
else:
|
|
self._unexport_snapshot(volume)
|
|
self._delete_iscsi_target(volume)
|
|
self.common.vip.basic.save_config()
|
|
|
|
def get_volume_stats(self, refresh=False):
|
|
"""Get volume stats."""
|
|
if refresh or not self.stats:
|
|
self._update_stats()
|
|
return self.stats
|
|
|
|
@utils.synchronized('vmem-export')
|
|
def _create_iscsi_target(self, volume):
|
|
"""Creates a new target for use in exporting a lun.
|
|
|
|
Openstack does not yet support multipathing. We still create
|
|
HA targets but we pick a single random target for the
|
|
Openstack infrastructure to use. This at least allows us to
|
|
evenly distribute LUN connections across the storage cluster.
|
|
The equivalent CLI commands are "iscsi target create
|
|
<target_name>" and "iscsi target bind <target_name> to
|
|
<ip_of_mg_eth_intf>".
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
|
|
Returns:
|
|
reference to randomly selected target object
|
|
"""
|
|
v = self.common.vip
|
|
target_name = self._get_short_name(volume['id'])
|
|
|
|
LOG.debug("Creating iscsi target %s.", target_name)
|
|
|
|
try:
|
|
self.common._send_cmd_and_verify(v.iscsi.create_iscsi_target,
|
|
self._wait_for_targetstate,
|
|
'', [target_name], [target_name])
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("Failed to create iscsi target!"))
|
|
raise
|
|
|
|
try:
|
|
self.common._send_cmd(self.common.mga.iscsi.bind_ip_to_target,
|
|
'', target_name,
|
|
self.gateway_iscsi_ip_addresses_mga)
|
|
self.common._send_cmd(self.common.mgb.iscsi.bind_ip_to_target,
|
|
'', target_name,
|
|
self.gateway_iscsi_ip_addresses_mgb)
|
|
except Exception:
|
|
LOG.exception(_LE("Failed to bind iSCSI targets!"))
|
|
raise
|
|
|
|
return self.array_info[random.randint(0, len(self.array_info) - 1)]
|
|
|
|
@utils.synchronized('vmem-export')
|
|
def _delete_iscsi_target(self, volume):
|
|
"""Deletes the iscsi target for a lun.
|
|
|
|
The CLI equivalent is "no iscsi target create <target_name>".
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
"""
|
|
v = self.common.vip
|
|
success_msgs = ['', 'Invalid target']
|
|
target_name = self._get_short_name(volume['id'])
|
|
|
|
LOG.debug("Deleting iscsi target for %s.", target_name)
|
|
|
|
try:
|
|
self.common._send_cmd(v.iscsi.delete_iscsi_target,
|
|
success_msgs, target_name)
|
|
except Exception:
|
|
LOG.exception(_LE("Failed to delete iSCSI target!"))
|
|
raise
|
|
|
|
@utils.synchronized('vmem-export')
|
|
def _export_lun(self, volume, connector=None, igroup=None):
|
|
"""Generates the export configuration for the given volume.
|
|
|
|
The equivalent CLI command is "lun export container
|
|
<container_name> name <lun_name>"
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
connector -- connector object provided by the Manager
|
|
igroup -- name of igroup to use for exporting
|
|
|
|
Returns:
|
|
lun_id -- the LUN ID assigned by the backend
|
|
"""
|
|
lun_id = -1
|
|
export_to = ''
|
|
v = self.common.vip
|
|
|
|
if igroup:
|
|
export_to = igroup
|
|
elif connector:
|
|
export_to = connector['initiator']
|
|
else:
|
|
raise exception.Error(_("No initiators found, cannot proceed"))
|
|
|
|
target_name = self._get_short_name(volume['id'])
|
|
|
|
LOG.debug("Exporting lun %s." % volume['id'])
|
|
|
|
try:
|
|
self.common._send_cmd_and_verify(
|
|
v.lun.export_lun, self.common._wait_for_export_config, '',
|
|
[self.common.container, volume['id'], target_name,
|
|
export_to, 'auto'], [volume['id'], 'state=True'])
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("LUN export for %s failed!"), volume['id'])
|
|
raise
|
|
|
|
lun_id = self.common._get_lun_id(volume['id'])
|
|
|
|
return lun_id
|
|
|
|
@utils.synchronized('vmem-export')
|
|
def _unexport_lun(self, volume):
|
|
"""Removes the export configuration for the given volume.
|
|
|
|
The equivalent CLI command is "no lun export container
|
|
<container_name> name <lun_name>"
|
|
|
|
Arguments:
|
|
volume -- volume object provided by the Manager
|
|
"""
|
|
v = self.common.vip
|
|
|
|
LOG.debug("Unexporting lun %s.", volume['id'])
|
|
|
|
try:
|
|
self.common._send_cmd_and_verify(
|
|
v.lun.unexport_lun, self.common._wait_for_export_config, '',
|
|
[self.common.container, volume['id'], 'all', 'all', 'auto'],
|
|
[volume['id'], 'state=False'])
|
|
|
|
except exception.ViolinBackendErrNotFound:
|
|
LOG.debug("Lun %s already unexported, continuing.", volume['id'])
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("LUN unexport for %s failed!"), volume['id'])
|
|
raise
|
|
|
|
@utils.synchronized('vmem-export')
|
|
def _export_snapshot(self, snapshot, connector=None, igroup=None):
|
|
"""Generates the export configuration for the given snapshot.
|
|
|
|
The equivalent CLI command is "snapshot export container
|
|
PROD08 lun <snapshot_name> name <volume_name>"
|
|
|
|
Arguments:
|
|
snapshot -- snapshot object provided by the Manager
|
|
connector -- connector object provided by the Manager
|
|
igroup -- name of igroup to use for exporting
|
|
|
|
Returns:
|
|
lun_id -- the LUN ID assigned by the backend
|
|
"""
|
|
lun_id = -1
|
|
export_to = ''
|
|
v = self.common.vip
|
|
|
|
target_name = self._get_short_name(snapshot['id'])
|
|
|
|
LOG.debug("Exporting snapshot %s.", snapshot['id'])
|
|
|
|
if igroup:
|
|
export_to = igroup
|
|
elif connector:
|
|
export_to = connector['initiator']
|
|
else:
|
|
raise exception.Error(_("No initiators found, cannot proceed"))
|
|
|
|
try:
|
|
self.common._send_cmd(v.snapshot.export_lun_snapshot, '',
|
|
self.common.container, snapshot['volume_id'],
|
|
snapshot['id'], export_to, target_name,
|
|
'auto')
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("Snapshot export for %s failed!"),
|
|
snapshot['id'])
|
|
raise
|
|
|
|
else:
|
|
self.common._wait_for_export_config(snapshot['volume_id'],
|
|
snapshot['id'], state=True)
|
|
lun_id = self.common._get_snapshot_id(snapshot['volume_id'],
|
|
snapshot['id'])
|
|
|
|
return lun_id
|
|
|
|
@utils.synchronized('vmem-export')
|
|
def _unexport_snapshot(self, snapshot):
|
|
"""Removes the export configuration for the given snapshot.
|
|
|
|
The equivalent CLI command is "no snapshot export container
|
|
PROD08 lun <snapshot_name> name <volume_name>"
|
|
|
|
Arguments:
|
|
snapshot -- snapshot object provided by the Manager
|
|
"""
|
|
v = self.common.vip
|
|
|
|
LOG.debug("Unexporting snapshot %s.", snapshot['id'])
|
|
|
|
try:
|
|
self.common._send_cmd(v.snapshot.unexport_lun_snapshot, '',
|
|
self.common.container, snapshot['volume_id'],
|
|
snapshot['id'], 'all', 'all', 'auto', False)
|
|
|
|
except Exception:
|
|
LOG.exception(_LE("Snapshot unexport for %s failed!"),
|
|
snapshot['id'])
|
|
raise
|
|
|
|
else:
|
|
self.common._wait_for_export_config(snapshot['volume_id'],
|
|
snapshot['id'], state=False)
|
|
|
|
def _add_igroup_member(self, connector, igroup):
|
|
"""Add an initiator to an igroup so it can see exports.
|
|
|
|
The equivalent CLI command is "igroup addto name <igroup_name>
|
|
initiators <initiator_name>"
|
|
|
|
Arguments:
|
|
connector -- connector object provided by the Manager
|
|
"""
|
|
v = self.common.vip
|
|
|
|
LOG.debug("Adding initiator %s to igroup.", connector['initiator'])
|
|
|
|
resp = v.igroup.add_initiators(igroup, connector['initiator'])
|
|
|
|
if resp['code'] != 0:
|
|
raise exception.Error(
|
|
_('Failed to add igroup member: %(code)d, %(message)s') % resp)
|
|
|
|
def _update_stats(self):
|
|
"""Gathers array stats from the backend and converts them to GB values.
|
|
"""
|
|
data = {}
|
|
total_gb = 0
|
|
free_gb = 0
|
|
v = self.common.vip
|
|
|
|
master_cluster_id = v.basic.get_node_values(
|
|
'/cluster/state/master_id').values()[0]
|
|
|
|
bn1 = "/vshare/state/global/%s/container/%s/total_bytes" \
|
|
% (master_cluster_id, self.common.container)
|
|
bn2 = "/vshare/state/global/%s/container/%s/free_bytes" \
|
|
% (master_cluster_id, self.common.container)
|
|
resp = v.basic.get_node_values([bn1, bn2])
|
|
|
|
if bn1 in resp:
|
|
total_gb = resp[bn1] / units.Gi
|
|
else:
|
|
LOG.warn(_LW("Failed to receive update for total_gb stat!"))
|
|
|
|
if bn2 in resp:
|
|
free_gb = resp[bn2] / units.Gi
|
|
else:
|
|
LOG.warn(_LW("Failed to receive update for free_gb stat!"))
|
|
|
|
backend_name = self.configuration.volume_backend_name
|
|
data['volume_backend_name'] = backend_name or self.__class__.__name__
|
|
data['vendor_name'] = 'Violin Memory, Inc.'
|
|
data['driver_version'] = self.VERSION
|
|
data['storage_protocol'] = 'iSCSI'
|
|
data['reserved_percentage'] = 0
|
|
data['QoS_support'] = False
|
|
data['total_capacity_gb'] = total_gb
|
|
data['free_capacity_gb'] = free_gb
|
|
|
|
for i in data:
|
|
LOG.debug("stat update: %(name)s=%(data)s." %
|
|
{'name': i, 'data': data[i]})
|
|
|
|
self.stats = data
|
|
|
|
def _get_short_name(self, volume_name):
|
|
"""Creates a vSHARE-compatible iSCSI target name.
|
|
|
|
The Folsom-style volume names are prefix(7) + uuid(36), which
|
|
is too long for vSHARE for target names. To keep things
|
|
simple we can just truncate the name to 32 chars.
|
|
|
|
Arguments:
|
|
volume_name -- name of volume/lun
|
|
|
|
Returns:
|
|
Shortened volume name as a string.
|
|
"""
|
|
return volume_name[:32]
|
|
|
|
def _get_active_iscsi_ips(self, mg_conn):
|
|
"""Get a list of gateway IP addresses that can be used for iSCSI.
|
|
|
|
Arguments:
|
|
mg_conn -- active XG connection to one of the gateways
|
|
|
|
Returns:
|
|
active_gw_iscsi_ips -- list of IP addresses
|
|
"""
|
|
active_gw_iscsi_ips = []
|
|
interfaces_to_skip = ['lo', 'vlan10', 'eth1', 'eth2', 'eth3']
|
|
|
|
bn = "/net/interface/config/*"
|
|
intf_list = mg_conn.basic.get_node_values(bn)
|
|
|
|
for i in intf_list:
|
|
if intf_list[i] in interfaces_to_skip:
|
|
continue
|
|
|
|
bn1 = "/net/interface/state/%s/addr/ipv4/1/ip" % intf_list[i]
|
|
bn2 = "/net/interface/state/%s/flags/link_up" % intf_list[i]
|
|
resp = mg_conn.basic.get_node_values([bn1, bn2])
|
|
|
|
if len(resp.keys()) == 2 and resp[bn2] is True:
|
|
active_gw_iscsi_ips.append(resp[bn1])
|
|
|
|
return active_gw_iscsi_ips
|
|
|
|
def _get_hostname(self, mg_to_query=None):
|
|
"""Get the hostname of one of the mgs (hostname is used in IQN).
|
|
|
|
If the remote query fails then fall back to using the hostname
|
|
provided in the cinder configuration file.
|
|
|
|
Arguments:
|
|
mg_to_query -- name of gateway to query 'mga' or 'mgb'
|
|
|
|
Returns: hostname -- hostname as a string
|
|
"""
|
|
hostname = self.configuration.san_ip
|
|
conn = self.common.vip
|
|
|
|
if mg_to_query == "mga":
|
|
hostname = self.configuration.gateway_mga
|
|
conn = self.common.mga
|
|
elif mg_to_query == "mgb":
|
|
hostname = self.configuration.gateway_mgb
|
|
conn = self.common.mgb
|
|
|
|
ret_dict = conn.basic.get_node_values("/system/hostname")
|
|
if ret_dict:
|
|
hostname = ret_dict.items()[0][1]
|
|
else:
|
|
LOG.debug("Unable to fetch gateway hostname for %s." % mg_to_query)
|
|
|
|
return hostname
|
|
|
|
def _wait_for_targetstate(self, target_name):
|
|
"""Polls backend to verify an iscsi target configuration.
|
|
|
|
This function will try to verify the creation of an iscsi
|
|
target on both gateway nodes of the array every 5 seconds.
|
|
|
|
Arguments:
|
|
target_name -- name of iscsi target to be polled
|
|
|
|
Returns:
|
|
True if the export state was correctly added
|
|
"""
|
|
bn = "/vshare/config/iscsi/target/%s" % (target_name)
|
|
|
|
def _loop_func():
|
|
status = [False, False]
|
|
mg_conns = [self.common.mga, self.common.mgb]
|
|
|
|
LOG.debug("Entering _wait_for_targetstate loop: target=%s.",
|
|
target_name)
|
|
|
|
for node_id in xrange(2):
|
|
resp = mg_conns[node_id].basic.get_node_values(bn)
|
|
if len(resp.keys()):
|
|
status[node_id] = True
|
|
|
|
if status[0] and status[1]:
|
|
raise loopingcall.LoopingCallDone(retvalue=True)
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_loop_func)
|
|
success = timer.start(interval=5).wait()
|
|
|
|
return success
|