From: Xing Yang Date: Thu, 17 Jan 2013 21:33:47 +0000 (-0500) Subject: Update EMC SMI-S iSCSI Driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=4c2cfaa985c57c14400d5c8604b4215c41a66f65;p=openstack-build%2Fcinder-build.git 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 --- diff --git a/cinder/tests/test_emc.py b/cinder/tests/test_emc.py index 14bcc6de4..4e431cbde 100644 --- a/cinder/tests/test_emc.py +++ b/cinder/tests/test_emc.py @@ -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) diff --git a/cinder/volume/drivers/emc/__init__.py b/cinder/volume/drivers/emc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/volume/drivers/emc.py b/cinder/volume/drivers/emc/emc_smis_common.py similarity index 86% rename from cinder/volume/drivers/emc.py rename to cinder/volume/drivers/emc/emc_smis_common.py index c7e5029db..74b75409f 100644 --- a/cinder/volume/drivers/emc.py +++ b/cinder/volume/drivers/emc/emc_smis_common.py @@ -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 EMCISCSIDriver(driver.ISCSIDriver): - """Drivers for VMAX/VMAXe and VNX""" +class EMCSMISCommon(): + """Common code used by ISCSI and FC drivers.""" - def __init__(self, *args, **kwargs): + 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} - 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 - - 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'] + def update_volume_status(self): + """Retrieve status info.""" + LOG.debug(_("Updating volume status")) - 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 + 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) - iscsi_properties, volume_path = self._attach_volume( - context, volume, connector) + 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) - with utils.temporary_chown(volume_path): - with utils.file_open(volume_path) as volume_file: - image_service.update(context, image_id, {}, volume_file) + self.stats['total_capacity_gb'] = pool['TotalManagedSpace'] + self.stats['free_capacity_gb'] = pool['RemainingManagedSpace'] - 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() diff --git a/cinder/volume/drivers/emc/emc_smis_iscsi.py b/cinder/volume/drivers/emc/emc_smis_iscsi.py new file mode 100644 index 000000000..0ffe0e42b --- /dev/null +++ b/cinder/volume/drivers/emc/emc_smis_iscsi.py @@ -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