Update EMC SMI-S iSCSI Driver
Refactor EMC SMI-S based iSCSI driver. There are lots of common code that can be shared between iSCSI and FC drivers using SMI-S. Break the original emc.py into two files: emc_smis_common.py and emc_smis_iscsi.py. Also added support for get_volume_stats() and create_cloned_volume(). get_volume_stats() will be revisited after winston-d's changes. copy volume<->image will be revisited after avishay-il's changes. Change-Id: I90dc3599ec2fd5bb86a3e988111a8aee5de7efea
This commit is contained in:
parent
d3324a35ad
commit
4c2cfaa985
|
@ -18,7 +18,8 @@
|
|||
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import test
|
||||
from cinder.volume.drivers.emc import EMCISCSIDriver
|
||||
from cinder.volume.drivers.emc.emc_smis_common import EMCSMISCommon
|
||||
from cinder.volume.drivers.emc.emc_smis_iscsi import EMCSMISISCSIDriver
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -58,6 +59,15 @@ test_clone = {'name': 'clone1',
|
|||
'display_name': 'clone1',
|
||||
'display_description': 'volume created from snapshot',
|
||||
'volume_type_id': None}
|
||||
test_clone3 = {'name': 'clone3',
|
||||
'size': 1,
|
||||
'volume_name': 'vol1',
|
||||
'id': '3',
|
||||
'provider_auth': None,
|
||||
'project_id': 'project',
|
||||
'display_name': 'clone3',
|
||||
'display_description': 'cloned volume',
|
||||
'volume_type_id': None}
|
||||
|
||||
|
||||
class EMC_StorageVolume(dict):
|
||||
|
@ -282,20 +292,45 @@ class FakeEcomConnection():
|
|||
clone_vol.path = {'DeviceID': clone_vol['DeviceID']}
|
||||
vols.append(clone_vol)
|
||||
|
||||
clone_vol3 = EMC_StorageVolume()
|
||||
clone_vol3['CreationClassName'] = 'Clar_StorageVolume'
|
||||
clone_vol3['ElementName'] = test_clone3['name']
|
||||
clone_vol3['DeviceID'] = test_clone3['id']
|
||||
clone_vol3['SystemName'] = storage_system
|
||||
clone_vol3.path = {'DeviceID': clone_vol3['DeviceID']}
|
||||
vols.append(clone_vol3)
|
||||
|
||||
return vols
|
||||
|
||||
def _enum_syncsvsvs(self):
|
||||
syncs = []
|
||||
sync = {}
|
||||
|
||||
vols = self._enum_storagevolumes()
|
||||
|
||||
objpath1 = vols[0]
|
||||
objpath2 = vols[1]
|
||||
sync = {}
|
||||
sync['SyncedElement'] = objpath2
|
||||
sync['SystemElement'] = objpath1
|
||||
sync['CreationClassName'] = 'SE_StorageSynchronized_SV_SV'
|
||||
syncs.append(sync)
|
||||
|
||||
objpath1 = vols[1]
|
||||
objpath2 = vols[2]
|
||||
sync2 = {}
|
||||
sync2['SyncedElement'] = objpath2
|
||||
sync2['SystemElement'] = objpath1
|
||||
sync2['CreationClassName'] = 'SE_StorageSynchronized_SV_SV'
|
||||
syncs.append(sync2)
|
||||
|
||||
objpath1 = vols[0]
|
||||
objpath2 = vols[3]
|
||||
sync3 = {}
|
||||
sync3['SyncedElement'] = objpath2
|
||||
sync3['SystemElement'] = objpath1
|
||||
sync3['CreationClassName'] = 'SE_StorageSynchronized_SV_SV'
|
||||
syncs.append(sync3)
|
||||
|
||||
return syncs
|
||||
|
||||
def _enum_unitnames(self):
|
||||
|
@ -318,17 +353,17 @@ class FakeEcomConnection():
|
|||
return names
|
||||
|
||||
|
||||
class EMCISCSIDriverTestCase(test.TestCase):
|
||||
class EMCSMISISCSIDriverTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(EMCISCSIDriverTestCase, self).setUp()
|
||||
driver = EMCISCSIDriver()
|
||||
super(EMCSMISISCSIDriverTestCase, self).setUp()
|
||||
driver = EMCSMISISCSIDriver()
|
||||
self.driver = driver
|
||||
self.stubs.Set(EMCISCSIDriver, '_get_iscsi_properties',
|
||||
self.stubs.Set(EMCSMISISCSIDriver, '_get_iscsi_properties',
|
||||
self.fake_get_iscsi_properties)
|
||||
self.stubs.Set(EMCISCSIDriver, '_get_ecom_connection',
|
||||
self.stubs.Set(EMCSMISCommon, '_get_ecom_connection',
|
||||
self.fake_ecom_connection)
|
||||
self.stubs.Set(EMCISCSIDriver, '_get_storage_type',
|
||||
self.stubs.Set(EMCSMISCommon, '_get_storage_type',
|
||||
self.fake_storage_type)
|
||||
|
||||
def fake_ecom_connection(self):
|
||||
|
@ -365,7 +400,10 @@ class EMCISCSIDriverTestCase(test.TestCase):
|
|||
self.driver.create_snapshot(test_snapshot)
|
||||
self.driver.create_volume_from_snapshot(
|
||||
test_clone, test_snapshot)
|
||||
self.driver.create_cloned_volume(
|
||||
test_clone3, test_volume)
|
||||
self.driver.delete_volume(test_clone)
|
||||
self.driver.delete_volume(test_clone3)
|
||||
self.driver.delete_snapshot(test_snapshot)
|
||||
self.driver.delete_volume(test_volume)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 EMC Corporation, Inc.
|
||||
# Copyright (c) 2012 EMC Corporation.
|
||||
# Copyright (c) 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
|
@ -16,11 +16,13 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Drivers for EMC volumes.
|
||||
Common class for SMI-S based EMC volume drivers.
|
||||
|
||||
This common class is for EMC volume drivers based on SMI-S.
|
||||
It supports ISCSI and FC protocols on VNX and VMAX/VMAXe arrays.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
|
@ -28,9 +30,6 @@ from cinder import exception
|
|||
from cinder import flags
|
||||
from cinder.openstack.common import cfg
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import utils
|
||||
from cinder.volume import driver
|
||||
from cinder.volume import volume_types
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -39,28 +38,24 @@ FLAGS = flags.FLAGS
|
|||
try:
|
||||
import pywbem
|
||||
except ImportError:
|
||||
LOG.info(_('Module PyWBEM not installed. PyWBEM can be downloaded '
|
||||
'from http://sourceforge.net/apps/mediawiki/pywbem'))
|
||||
LOG.info(_('Module PyWBEM not installed. '
|
||||
'Install PyWBEM using the python-pywbem package.'))
|
||||
|
||||
CINDER_EMC_CONFIG_FILE = '/etc/cinder/cinder_emc_config.xml'
|
||||
|
||||
|
||||
def get_iscsi_initiator():
|
||||
"""Get iscsi initiator name for this machine"""
|
||||
# NOTE openiscsi stores initiator name in a file that
|
||||
# needs root permission to read.
|
||||
contents = utils.read_file_as_root('/etc/iscsi/initiatorname.iscsi')
|
||||
for l in contents.split('\n'):
|
||||
if l.startswith('InitiatorName='):
|
||||
return l[l.index('=') + 1:].strip()
|
||||
class EMCSMISCommon():
|
||||
"""Common code used by ISCSI and FC drivers."""
|
||||
|
||||
stats = {'driver_version': '1.0',
|
||||
'free_capacity_gb': 0,
|
||||
'reserved_percentage': 0,
|
||||
'storage_protocol': None,
|
||||
'total_capacity_gb': 0,
|
||||
'vendor_name': 'EMC',
|
||||
'volume_backend_name': None}
|
||||
|
||||
class EMCISCSIDriver(driver.ISCSIDriver):
|
||||
"""Drivers for VMAX/VMAXe and VNX"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(EMCISCSIDriver, self).__init__(*args, **kwargs)
|
||||
def __init__(self):
|
||||
|
||||
opt = cfg.StrOpt('cinder_emc_config_file',
|
||||
default=CINDER_EMC_CONFIG_FILE,
|
||||
|
@ -68,9 +63,6 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
'config data')
|
||||
FLAGS.register_opt(opt)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
pass
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Creates a EMC(VMAX/VMAXe/VNX) volume. """
|
||||
|
||||
|
@ -318,6 +310,139 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
'snapshotname': snapshotname,
|
||||
'rc': rc})
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a clone of the specified volume."""
|
||||
LOG.debug(_('Entering create_cloned_volume.'))
|
||||
|
||||
srcname = src_vref['name']
|
||||
volumename = volume['name']
|
||||
|
||||
LOG.info(_('Create a Clone from Volume: Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname})
|
||||
|
||||
conn = self._get_ecom_connection()
|
||||
if conn is None:
|
||||
exception_message = (_('Error Create Cloned Volume: '
|
||||
'Volume: %(volumename)s Source Volume: '
|
||||
'%(srcname)s. Cannot connect to'
|
||||
' ECOM server.')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname})
|
||||
LOG.error(exception_message)
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
src_instance = self._find_lun(src_vref)
|
||||
storage_system = src_instance['SystemName']
|
||||
|
||||
LOG.debug(_('Create Cloned Volume: Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s Source Instance: '
|
||||
'%(src_instance)s Storage System: %(storage_system)s.')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'src_instance': str(src_instance.path),
|
||||
'storage_system': storage_system})
|
||||
|
||||
repservice = self._find_replication_service(storage_system)
|
||||
if repservice is None:
|
||||
exception_message = (_('Error Create Cloned Volume: '
|
||||
'Volume: %(volumename)s Source Volume: '
|
||||
'%(srcname)s. Cannot find Replication '
|
||||
'Service to create cloned volume.')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname})
|
||||
LOG.error(exception_message)
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
LOG.debug(_('Create Cloned Volume: Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s Method: CreateElementReplica '
|
||||
'ReplicationService: %(service)s ElementName: '
|
||||
'%(elementname)s SyncType: 8 SourceElement: '
|
||||
'%(sourceelement)s')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'service': str(repservice),
|
||||
'elementname': volumename,
|
||||
'sourceelement': str(src_instance.path)})
|
||||
|
||||
# Create a Clone from snapshot
|
||||
rc, job = conn.InvokeMethod(
|
||||
'CreateElementReplica', repservice,
|
||||
ElementName=volumename,
|
||||
SyncType=self._getnum(8, '16'),
|
||||
SourceElement=src_instance.path)
|
||||
|
||||
if rc != 0L:
|
||||
rc, errordesc = self._wait_for_job_complete(job)
|
||||
if rc != 0L:
|
||||
exception_message = (_('Error Create Cloned Volume: '
|
||||
'Volume: %(volumename)s Source Volume:'
|
||||
'%(srcname)s. Return code: %(rc)lu.'
|
||||
'Error: %(error)s')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'rc': rc,
|
||||
'error': errordesc})
|
||||
LOG.error(exception_message)
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=exception_message)
|
||||
|
||||
LOG.debug(_('Create Cloned Volume: Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s. Successfully cloned volume '
|
||||
'from source volume. Finding the clone relationship.')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname})
|
||||
|
||||
sync_name, storage_system = self._find_storage_sync_sv_sv(
|
||||
volumename, srcname)
|
||||
|
||||
# Remove the Clone relationshop so it can be used as a regular lun
|
||||
# 8 - Detach operation
|
||||
LOG.debug(_('Create Cloned Volume: Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s. Remove the clone '
|
||||
'relationship. Method: ModifyReplicaSynchronization '
|
||||
'ReplicationService: %(service)s Operation: 8 '
|
||||
'Synchronization: %(sync_name)s')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'service': str(repservice),
|
||||
'sync_name': str(sync_name)})
|
||||
|
||||
rc, job = conn.InvokeMethod(
|
||||
'ModifyReplicaSynchronization',
|
||||
repservice,
|
||||
Operation=self._getnum(8, '16'),
|
||||
Synchronization=sync_name)
|
||||
|
||||
LOG.debug(_('Create Cloned Volume: Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s Return code: %(rc)lu')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'rc': rc})
|
||||
|
||||
if rc != 0L:
|
||||
rc, errordesc = self._wait_for_job_complete(job)
|
||||
if rc != 0L:
|
||||
exception_message = (_('Error Create Cloned Volume: '
|
||||
'Volume: %(volumename)s '
|
||||
'Source Volume: %(srcname)s. '
|
||||
'Return code: %(rc)lu. Error: %(error)s')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'rc': rc,
|
||||
'error': errordesc})
|
||||
LOG.error(exception_message)
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=exception_message)
|
||||
|
||||
LOG.debug(_('Leaving create_cloned_volume: Volume: '
|
||||
'%(volumename)s Source Volume: %(srcname)s '
|
||||
'Return code: %(rc)lu.')
|
||||
% {'volumename': volumename,
|
||||
'srcname': srcname,
|
||||
'rc': rc})
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Deletes an EMC volume."""
|
||||
LOG.debug(_('Entering delete_volume.'))
|
||||
|
@ -568,14 +693,11 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
|
||||
return {'provider_location': device_id}
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Driver exntry point to remove an export for a volume.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Mapping method for VNX
|
||||
def _expose_paths(self, conn, configservice, vol_instance, connector):
|
||||
"""Adds a volume and initiator to a Storage Group
|
||||
"""This method maps a volume to a host.
|
||||
|
||||
It adds a volume and initiator to a Storage Group
|
||||
and therefore maps the volume to the host.
|
||||
"""
|
||||
volumename = vol_instance['ElementName']
|
||||
|
@ -703,10 +825,6 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
LOG.debug(_('RemoveMembers for volume %s completed successfully.')
|
||||
% volumename)
|
||||
|
||||
def check_for_export(self, context, volume_id):
|
||||
"""Make sure volume is exported."""
|
||||
pass
|
||||
|
||||
def _map_lun(self, volume, connector):
|
||||
"""Maps a volume to the host."""
|
||||
volumename = volume['name']
|
||||
|
@ -746,7 +864,7 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
exception_message = (_("Cannot connect to ECOM server"))
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
device_number = self._find_device_number(volume)
|
||||
device_number = self.find_device_number(volume)
|
||||
if device_number is None:
|
||||
LOG.info(_("Volume %s is not mapped. No volume to unmap.")
|
||||
% (volumename))
|
||||
|
@ -770,122 +888,17 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
self._hide_paths(conn, configservice, vol_instance, connector)
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Initializes the connection and returns connection info.
|
||||
|
||||
the iscsi driver returns a driver_volume_type of 'iscsi'.
|
||||
the format of the driver data is defined in _get_iscsi_properties.
|
||||
Example return value::
|
||||
|
||||
{
|
||||
'driver_volume_type': 'iscsi'
|
||||
'data': {
|
||||
'target_discovered': True,
|
||||
'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
|
||||
'target_portal': '127.0.0.0.1:3260',
|
||||
'volume_id': 1,
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
"""Initializes the connection and returns connection info."""
|
||||
volumename = volume['name']
|
||||
LOG.info(_('Initialize connection: %(volume)s')
|
||||
% {'volume': volumename})
|
||||
device_number = self._find_device_number(volume)
|
||||
device_number = self.find_device_number(volume)
|
||||
if device_number is not None:
|
||||
LOG.info(_("Volume %s is already mapped.")
|
||||
% (volumename))
|
||||
else:
|
||||
self._map_lun(volume, connector)
|
||||
|
||||
iscsi_properties = self._get_iscsi_properties(volume)
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': iscsi_properties
|
||||
}
|
||||
|
||||
def _do_iscsi_discovery(self, volume):
|
||||
|
||||
LOG.warn(_("ISCSI provider_location not stored, using discovery"))
|
||||
|
||||
(out, _err) = self._execute('iscsiadm', '-m', 'discovery',
|
||||
'-t', 'sendtargets', '-p',
|
||||
FLAGS.iscsi_ip_address,
|
||||
run_as_root=True)
|
||||
for target in out.splitlines():
|
||||
return target
|
||||
return None
|
||||
|
||||
def _get_iscsi_properties(self, volume):
|
||||
"""Gets iscsi configuration
|
||||
|
||||
We ideally get saved information in the volume entity, but fall back
|
||||
to discovery if need be. Discovery may be completely removed in future
|
||||
The properties are:
|
||||
|
||||
:target_discovered: boolean indicating whether discovery was used
|
||||
|
||||
:target_iqn: the IQN of the iSCSI target
|
||||
|
||||
:target_portal: the portal of the iSCSI target
|
||||
|
||||
:target_lun: the lun of the iSCSI target
|
||||
|
||||
:volume_id: the id of the volume (currently used by xen)
|
||||
|
||||
:auth_method:, :auth_username:, :auth_password:
|
||||
|
||||
the authentication details. Right now, either auth_method is not
|
||||
present meaning no authentication, or auth_method == `CHAP`
|
||||
meaning use CHAP with the specified credentials.
|
||||
"""
|
||||
properties = {}
|
||||
|
||||
location = self._do_iscsi_discovery(volume)
|
||||
if not location:
|
||||
raise exception.InvalidVolume(_("Could not find iSCSI export "
|
||||
" for volume %s") %
|
||||
(volume['name']))
|
||||
|
||||
LOG.debug(_("ISCSI Discovery: Found %s") % (location))
|
||||
properties['target_discovered'] = True
|
||||
|
||||
results = location.split(" ")
|
||||
properties['target_portal'] = results[0].split(",")[0]
|
||||
properties['target_iqn'] = results[1]
|
||||
|
||||
device_number = self._find_device_number(volume)
|
||||
if device_number is None:
|
||||
exception_message = (_("Cannot find device number for volume %s")
|
||||
% volume['name'])
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
properties['target_lun'] = device_number
|
||||
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
auth = volume['provider_auth']
|
||||
if auth:
|
||||
(auth_method, auth_username, auth_secret) = auth.split()
|
||||
|
||||
properties['auth_method'] = auth_method
|
||||
properties['auth_username'] = auth_username
|
||||
properties['auth_password'] = auth_secret
|
||||
|
||||
LOG.debug(_("ISCSI properties: %s") % (properties))
|
||||
|
||||
return properties
|
||||
|
||||
def _run_iscsiadm(self, iscsi_properties, iscsi_command, **kwargs):
|
||||
check_exit_code = kwargs.pop('check_exit_code', 0)
|
||||
(out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
|
||||
iscsi_properties['target_iqn'],
|
||||
'-p', iscsi_properties['target_portal'],
|
||||
*iscsi_command, run_as_root=True,
|
||||
check_exit_code=check_exit_code)
|
||||
LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
|
||||
(iscsi_command, out, err))
|
||||
return (out, err)
|
||||
|
||||
def terminate_connection(self, volume, connector):
|
||||
"""Disallow connection from connector"""
|
||||
volumename = volume['name']
|
||||
|
@ -893,78 +906,26 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
% {'volume': volumename})
|
||||
self._unmap_lun(volume, connector)
|
||||
|
||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||
"""Fetch the image from image_service and write it to the volume."""
|
||||
LOG.debug(_('copy_image_to_volume %s.') % volume['name'])
|
||||
initiator = get_iscsi_initiator()
|
||||
connector = {}
|
||||
connector['initiator'] = initiator
|
||||
def update_volume_status(self):
|
||||
"""Retrieve status info."""
|
||||
LOG.debug(_("Updating volume status"))
|
||||
|
||||
iscsi_properties, volume_path = self._attach_volume(
|
||||
context, volume, connector)
|
||||
storage_type = self._get_storage_type()
|
||||
if storage_type is None:
|
||||
exception_message = (_("Storage type name not found."))
|
||||
LOG.error(exception_message)
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
with utils.temporary_chown(volume_path):
|
||||
with utils.file_open(volume_path, "wb") as image_file:
|
||||
image_service.download(context, image_id, image_file)
|
||||
pool, storagesystem = self._find_pool(storage_type, True)
|
||||
if pool is None:
|
||||
exception_message = (_("Error finding pool details."))
|
||||
LOG.error(exception_message)
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
self.terminate_connection(volume, connector)
|
||||
self.stats['total_capacity_gb'] = pool['TotalManagedSpace']
|
||||
self.stats['free_capacity_gb'] = pool['RemainingManagedSpace']
|
||||
|
||||
def _attach_volume(self, context, volume, connector):
|
||||
"""Attach the volume."""
|
||||
iscsi_properties = None
|
||||
host_device = None
|
||||
init_conn = self.initialize_connection(volume, connector)
|
||||
iscsi_properties = init_conn['data']
|
||||
|
||||
self._run_iscsiadm(iscsi_properties, ("--login",),
|
||||
check_exit_code=[0, 255])
|
||||
|
||||
self._iscsiadm_update(iscsi_properties, "node.startup", "automatic")
|
||||
|
||||
host_device = ("/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" %
|
||||
(iscsi_properties['target_portal'],
|
||||
iscsi_properties['target_iqn'],
|
||||
iscsi_properties.get('target_lun', 0)))
|
||||
|
||||
tries = 0
|
||||
while not os.path.exists(host_device):
|
||||
if tries >= FLAGS.num_iscsi_scan_tries:
|
||||
raise exception.CinderException(
|
||||
_("iSCSI device not found at %s") % (host_device))
|
||||
|
||||
LOG.warn(_("ISCSI volume not yet found at: %(host_device)s. "
|
||||
"Will rescan & retry. Try number: %(tries)s") %
|
||||
locals())
|
||||
|
||||
# The rescan isn't documented as being necessary(?), but it helps
|
||||
self._run_iscsiadm(iscsi_properties, ("--rescan",))
|
||||
|
||||
tries = tries + 1
|
||||
if not os.path.exists(host_device):
|
||||
time.sleep(tries ** 2)
|
||||
|
||||
if tries != 0:
|
||||
LOG.debug(_("Found iSCSI node %(host_device)s "
|
||||
"(after %(tries)s rescans)") %
|
||||
locals())
|
||||
|
||||
return iscsi_properties, host_device
|
||||
|
||||
def copy_volume_to_image(self, context, volume, image_service, image_id):
|
||||
"""Copy the volume to the specified image."""
|
||||
LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
|
||||
initiator = get_iscsi_initiator()
|
||||
connector = {}
|
||||
connector['initiator'] = initiator
|
||||
|
||||
iscsi_properties, volume_path = self._attach_volume(
|
||||
context, volume, connector)
|
||||
|
||||
with utils.temporary_chown(volume_path):
|
||||
with utils.file_open(volume_path) as volume_file:
|
||||
image_service.update(context, image_id, {}, volume_file)
|
||||
|
||||
self.terminate_connection(volume, connector)
|
||||
return self.stats
|
||||
|
||||
def _get_storage_type(self, filename=None):
|
||||
"""Get the storage type from the config file
|
||||
|
@ -1100,13 +1061,20 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
|
||||
return foundConfigService
|
||||
|
||||
# Find pool based on storage_type
|
||||
def _find_pool(self, storage_type):
|
||||
# Find pool based on storage_type
|
||||
def _find_pool(self, storage_type, details=False):
|
||||
foundPool = None
|
||||
systemname = None
|
||||
conn = self._get_ecom_connection()
|
||||
vpools = conn.EnumerateInstanceNames('EMC_VirtualProvisioningPool')
|
||||
upools = conn.EnumerateInstanceNames('EMC_UnifiedStoragePool')
|
||||
# Only get instance names if details flag is False;
|
||||
# Otherwise get the whole instances
|
||||
if details is False:
|
||||
vpools = conn.EnumerateInstanceNames('EMC_VirtualProvisioningPool')
|
||||
upools = conn.EnumerateInstanceNames('EMC_UnifiedStoragePool')
|
||||
else:
|
||||
vpools = conn.EnumerateInstances('EMC_VirtualProvisioningPool')
|
||||
upools = conn.EnumerateInstances('EMC_UnifiedStoragePool')
|
||||
|
||||
for upool in upools:
|
||||
poolinstance = upool['InstanceID']
|
||||
# Example: CLARiiON+APM00115204878+U+Pool 0
|
||||
|
@ -1360,7 +1328,7 @@ class EMCISCSIDriver(driver.ISCSIDriver):
|
|||
return out_device_number
|
||||
|
||||
# Find a device number that a host can see for a volume
|
||||
def _find_device_number(self, volume):
|
||||
def find_device_number(self, volume):
|
||||
out_num_device_number = None
|
||||
|
||||
conn = self._get_ecom_connection()
|
|
@ -0,0 +1,302 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 EMC Corporation.
|
||||
# Copyright (c) 2012 OpenStack LLC.
|
||||
# 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.
|
||||
"""
|
||||
ISCSI Drivers for EMC VNX and VMAX/VMAXe arrays based on SMI-S.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import utils
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.emc import emc_smis_common
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def get_iscsi_initiator():
|
||||
"""Get iscsi initiator name for this machine."""
|
||||
# NOTE openiscsi stores initiator name in a file that
|
||||
# needs root permission to read.
|
||||
contents = utils.read_file_as_root('/etc/iscsi/initiatorname.iscsi')
|
||||
for l in contents.split('\n'):
|
||||
if l.startswith('InitiatorName='):
|
||||
return l[l.index('=') + 1:].strip()
|
||||
|
||||
|
||||
class EMCSMISISCSIDriver(driver.ISCSIDriver):
|
||||
"""EMC ISCSI Drivers for VMAX/VMAXe and VNX using SMI-S."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(EMCSMISISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self.common = emc_smis_common.EMCSMISCommon()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
pass
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Creates a EMC(VMAX/VMAXe/VNX) volume."""
|
||||
self.common.create_volume(volume)
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Creates a volume from a snapshot."""
|
||||
self.common.create_volume_from_snapshot(volume, snapshot)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a cloned volume."""
|
||||
self.common.create_cloned_volume(volume, src_vref)
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Deletes an EMC volume."""
|
||||
self.common.delete_volume(volume)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot."""
|
||||
self.common.create_snapshot(snapshot)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot."""
|
||||
self.common.delete_snapshot(snapshot)
|
||||
|
||||
def _iscsi_location(ip, target, iqn, lun=None):
|
||||
return "%s:%s,%s %s %s" % (ip, FLAGS.iscsi_port, target, iqn, lun)
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""Driver entry point to get the export info for an existing volume."""
|
||||
return self.common.ensure_export(context, volume)
|
||||
|
||||
def create_export(self, context, volume):
|
||||
"""Driver entry point to get the export info for a new volume."""
|
||||
return self.common.create_export(context, volume)
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Driver entry point to remove an export for a volume."""
|
||||
pass
|
||||
|
||||
def check_for_export(self, context, volume_id):
|
||||
"""Make sure volume is exported."""
|
||||
pass
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Initializes the connection and returns connection info.
|
||||
|
||||
the iscsi driver returns a driver_volume_type of 'iscsi'.
|
||||
the format of the driver data is defined in _get_iscsi_properties.
|
||||
Example return value::
|
||||
|
||||
{
|
||||
'driver_volume_type': 'iscsi'
|
||||
'data': {
|
||||
'target_discovered': True,
|
||||
'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
|
||||
'target_portal': '127.0.0.0.1:3260',
|
||||
'volume_id': 1,
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
self.common.initialize_connection(volume, connector)
|
||||
|
||||
iscsi_properties = self._get_iscsi_properties(volume)
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': iscsi_properties
|
||||
}
|
||||
|
||||
def _do_iscsi_discovery(self, volume):
|
||||
|
||||
LOG.warn(_("ISCSI provider_location not stored, using discovery"))
|
||||
|
||||
(out, _err) = self._execute('iscsiadm', '-m', 'discovery',
|
||||
'-t', 'sendtargets', '-p',
|
||||
FLAGS.iscsi_ip_address,
|
||||
run_as_root=True)
|
||||
for target in out.splitlines():
|
||||
return target
|
||||
return None
|
||||
|
||||
def _get_iscsi_properties(self, volume):
|
||||
"""Gets iscsi configuration
|
||||
|
||||
We ideally get saved information in the volume entity, but fall back
|
||||
to discovery if need be. Discovery may be completely removed in future
|
||||
The properties are:
|
||||
|
||||
:target_discovered: boolean indicating whether discovery was used
|
||||
|
||||
:target_iqn: the IQN of the iSCSI target
|
||||
|
||||
:target_portal: the portal of the iSCSI target
|
||||
|
||||
:target_lun: the lun of the iSCSI target
|
||||
|
||||
:volume_id: the id of the volume (currently used by xen)
|
||||
|
||||
:auth_method:, :auth_username:, :auth_password:
|
||||
|
||||
the authentication details. Right now, either auth_method is not
|
||||
present meaning no authentication, or auth_method == `CHAP`
|
||||
meaning use CHAP with the specified credentials.
|
||||
"""
|
||||
properties = {}
|
||||
|
||||
location = self._do_iscsi_discovery(volume)
|
||||
if not location:
|
||||
raise exception.InvalidVolume(_("Could not find iSCSI export "
|
||||
" for volume %s") %
|
||||
(volume['name']))
|
||||
|
||||
LOG.debug(_("ISCSI Discovery: Found %s") % (location))
|
||||
properties['target_discovered'] = True
|
||||
|
||||
results = location.split(" ")
|
||||
properties['target_portal'] = results[0].split(",")[0]
|
||||
properties['target_iqn'] = results[1]
|
||||
|
||||
device_number = self.common.find_device_number(volume)
|
||||
if device_number is None:
|
||||
exception_message = (_("Cannot find device number for volume %s")
|
||||
% volume['name'])
|
||||
raise exception.VolumeBackendAPIException(data=exception_message)
|
||||
|
||||
properties['target_lun'] = device_number
|
||||
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
auth = volume['provider_auth']
|
||||
if auth:
|
||||
(auth_method, auth_username, auth_secret) = auth.split()
|
||||
|
||||
properties['auth_method'] = auth_method
|
||||
properties['auth_username'] = auth_username
|
||||
properties['auth_password'] = auth_secret
|
||||
|
||||
LOG.debug(_("ISCSI properties: %s") % (properties))
|
||||
|
||||
return properties
|
||||
|
||||
def _run_iscsiadm(self, iscsi_properties, iscsi_command, **kwargs):
|
||||
check_exit_code = kwargs.pop('check_exit_code', 0)
|
||||
(out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
|
||||
iscsi_properties['target_iqn'],
|
||||
'-p', iscsi_properties['target_portal'],
|
||||
*iscsi_command, run_as_root=True,
|
||||
check_exit_code=check_exit_code)
|
||||
LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
|
||||
(iscsi_command, out, err))
|
||||
return (out, err)
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Disallow connection from connector"""
|
||||
self.common.terminate_connection(volume, connector)
|
||||
|
||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||
"""Fetch the image from image_service and write it to the volume."""
|
||||
LOG.debug(_('copy_image_to_volume %s.') % volume['name'])
|
||||
initiator = get_iscsi_initiator()
|
||||
connector = {}
|
||||
connector['initiator'] = initiator
|
||||
|
||||
iscsi_properties, volume_path = self._attach_volume(
|
||||
context, volume, connector)
|
||||
|
||||
with utils.temporary_chown(volume_path):
|
||||
with utils.file_open(volume_path, "wb") as image_file:
|
||||
image_service.download(context, image_id, image_file)
|
||||
|
||||
self.terminate_connection(volume, connector)
|
||||
|
||||
def _attach_volume(self, context, volume, connector):
|
||||
"""Attach the volume."""
|
||||
iscsi_properties = None
|
||||
host_device = None
|
||||
init_conn = self.initialize_connection(volume, connector)
|
||||
iscsi_properties = init_conn['data']
|
||||
|
||||
self._run_iscsiadm(iscsi_properties, ("--login",),
|
||||
check_exit_code=[0, 255])
|
||||
|
||||
self._iscsiadm_update(iscsi_properties, "node.startup", "automatic")
|
||||
|
||||
host_device = ("/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" %
|
||||
(iscsi_properties['target_portal'],
|
||||
iscsi_properties['target_iqn'],
|
||||
iscsi_properties.get('target_lun', 0)))
|
||||
|
||||
tries = 0
|
||||
while not os.path.exists(host_device):
|
||||
if tries >= FLAGS.num_iscsi_scan_tries:
|
||||
raise exception.CinderException(
|
||||
_("iSCSI device not found at %s") % (host_device))
|
||||
|
||||
LOG.warn(_("ISCSI volume not yet found at: %(host_device)s. "
|
||||
"Will rescan & retry. Try number: %(tries)s") %
|
||||
locals())
|
||||
|
||||
# The rescan isn't documented as being necessary(?), but it helps
|
||||
self._run_iscsiadm(iscsi_properties, ("--rescan",))
|
||||
|
||||
tries = tries + 1
|
||||
if not os.path.exists(host_device):
|
||||
time.sleep(tries ** 2)
|
||||
|
||||
if tries != 0:
|
||||
LOG.debug(_("Found iSCSI node %(host_device)s "
|
||||
"(after %(tries)s rescans)") %
|
||||
locals())
|
||||
|
||||
return iscsi_properties, host_device
|
||||
|
||||
def copy_volume_to_image(self, context, volume, image_service, image_id):
|
||||
"""Copy the volume to the specified image."""
|
||||
LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
|
||||
initiator = get_iscsi_initiator()
|
||||
connector = {}
|
||||
connector['initiator'] = initiator
|
||||
|
||||
iscsi_properties, volume_path = self._attach_volume(
|
||||
context, volume, connector)
|
||||
|
||||
with utils.temporary_chown(volume_path):
|
||||
with utils.file_open(volume_path) as volume_file:
|
||||
image_service.update(context, image_id, {}, volume_file)
|
||||
|
||||
self.terminate_connection(volume, connector)
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume status.
|
||||
If 'refresh' is True, run update the stats first."""
|
||||
if refresh:
|
||||
self.update_volume_status()
|
||||
|
||||
return self._stats
|
||||
|
||||
def update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = self.common.update_volume_status()
|
||||
data['volume_backend_name'] = 'EMCSMISISCSIDriver'
|
||||
data['storage_protocol'] = 'iSCSI'
|
||||
self._stats = data
|
Loading…
Reference in New Issue