From: Xing Yang Date: Fri, 14 Dec 2012 05:05:14 +0000 (-0500) Subject: Add EMC Volume Driver in Cinder X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=21788e4a42118d45fbcb3649110cf6c92363d4fc;p=openstack-build%2Fcinder-build.git Add EMC Volume Driver in Cinder Add support for EMC storage in the Cinder-Volume service. This driver is based on the existing ISCSIDriver, with the ability to create/delete and attach/detach volumes and create/delete snapshots, etc. The Cinder Driver executes the volume operations by communicating with the backend EMC storage. It uses a CIM client in python called PyWBEM to make CIM operations over HTTP. EMC CIM Object Manager (ECOM) is packaged with the SMI-S Provider. It is a CIM server that allows CIM clients to make CIM operations over HTTP, using SMI-S in the backend for EMC storage operations. SMI-S Provider supports the SNIA Storage Management Initiative (SMI), an ANSI standard for storage management. It supports VMAX/VMAXe and VNX storage systems. Implement bp: emc-volume-driver Change-Id: Iafce98603d31d66a7297ef11c92d5e6ac6ba3737 --- diff --git a/cinder/tests/test_emc.py b/cinder/tests/test_emc.py new file mode 100644 index 000000000..14bcc6de4 --- /dev/null +++ b/cinder/tests/test_emc.py @@ -0,0 +1,381 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 EMC Corporation, Inc. +# 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. + +from cinder.openstack.common import log as logging +from cinder import test +from cinder.volume.drivers.emc import EMCISCSIDriver + +LOG = logging.getLogger(__name__) + +storage_system = 'CLARiiON+APM00123456789' +lunmaskctrl_id = 'CLARiiON+APM00123456789+00aa11bb22cc33dd44ff55gg66hh77ii88jj' +initiator1 = 'iqn.1993-08.org.debian:01:1a2b3c4d5f6g' +stconf_service_creationclass = 'Clar_StorageConfigurationService' +ctrlconf_service_creationclass = 'Clar_ControllerConfigurationService' +rep_service_creationclass = 'Clar_ReplicationService' +vol_creationclass = 'Clar_StorageVolume' +pool_creationclass = 'Clar_UnifiedStoragePool' +lunmask_creationclass = 'Clar_LunMaskingSCSIProtocolController' +unit_creationclass = 'CIM_ProtocolControllerForUnit' +storage_type = 'gold' + +test_volume = {'name': 'vol1', + 'size': 1, + 'volume_name': 'vol1', + 'id': '1', + 'provider_auth': None, + 'project_id': 'project', + 'display_name': 'vol1', + 'display_description': 'test volume', + 'volume_type_id': None} +test_snapshot = {'name': 'snapshot1', + 'size': 1, + 'id': '4444', + 'volume_name': 'vol1', + 'volume_size': 1, + 'project_id': 'project'} +test_clone = {'name': 'clone1', + 'size': 1, + 'volume_name': 'vol1', + 'id': '2', + 'provider_auth': None, + 'project_id': 'project', + 'display_name': 'clone1', + 'display_description': 'volume created from snapshot', + 'volume_type_id': None} + + +class EMC_StorageVolume(dict): + pass + + +class FakeEcomConnection(): + + def InvokeMethod(self, MethodName, Service, ElementName=None, InPool=None, + ElementType=None, Size=None, + SyncType=None, SourceElement=None, + Operation=None, Synchronization=None, + TheElements=None, + LUNames=None, InitiatorPortIDs=None, DeviceAccesses=None, + ProtocolControllers=None, + MaskingGroup=None, Members=None): + rc = 0L + job = {'status': 'success'} + return rc, job + + def EnumerateInstanceNames(self, name): + result = None + if name == 'EMC_ReplicationService': + result = self._enum_replicationservices() + elif name == 'EMC_StorageConfigurationService': + result = self._enum_stconfsvcs() + elif name == 'EMC_ControllerConfigurationService': + result = self._enum_ctrlconfsvcs() + elif name == 'EMC_VirtualProvisioningPool': + result = self._enum_pools() + elif name == 'EMC_UnifiedStoragePool': + result = self._enum_pools() + elif name == 'EMC_StorageVolume': + result = self._enum_storagevolumes() + elif name == 'Clar_StorageVolume': + result = self._enum_storagevolumes() + elif name == 'SE_StorageSynchronized_SV_SV': + result = self._enum_syncsvsvs() + elif name == 'CIM_ProtocolControllerForUnit': + result = self._enum_unitnames() + elif name == 'EMC_LunMaskingSCSIProtocolController': + result = self._enum_lunmaskctrls() + else: + result = self._default_enum() + return result + + def GetInstance(self, objectpath, LocalOnly=False): + name = objectpath['CreationClassName'] + result = None + if name == 'Clar_StorageVolume': + result = self._getinstance_storagevolume(objectpath) + elif name == 'CIM_ProtocolControllerForUnit': + result = self._getinstance_unit(objectpath) + elif name == 'Clar_LunMaskingSCSIProtocolController': + result = self._getinstance_lunmask() + else: + result = self._default_getinstance(objectpath) + return result + + def Associators(self, objectpath, resultClass='EMC_StorageHardwareID'): + result = None + if resultClass == 'EMC_StorageHardwareID': + result = self._assoc_hdwid() + else: + result = self._default_assoc(objectpath) + return result + + def AssociatorNames(self, objectpath, + resultClass='EMC_LunMaskingSCSIProtocolController'): + result = None + if resultClass == 'EMC_LunMaskingSCSIProtocolController': + result = self._assocnames_lunmaskctrl() + else: + result = self._default_assocnames(objectpath) + return result + + def ReferenceNames(self, objectpath, + ResultClass='CIM_ProtocolControllerForUnit'): + result = None + if ResultClass == 'CIM_ProtocolControllerForUnit': + result = self._ref_unitnames() + else: + result = self._default_ref(objectpath) + return result + + def _ref_unitnames(self): + units = [] + unit = {} + + dependent = {} + dependent['CreationClassName'] = vol_creationclass + dependent['DeviceID'] = test_volume['id'] + dependent['ElementName'] = test_volume['name'] + dependent['SystemName'] = storage_system + + antecedent = {} + antecedent['CreationClassName'] = lunmask_creationclass + antecedent['DeviceID'] = lunmaskctrl_id + antecedent['SystemName'] = storage_system + + unit['Dependent'] = dependent + unit['Antecedent'] = antecedent + unit['CreationClassName'] = unit_creationclass + units.append(unit) + + return units + + def _default_ref(self, objectpath): + return objectpath + + def _assoc_hdwid(self): + assocs = [] + assoc = {} + assoc['StorageID'] = initiator1 + assocs.append(assoc) + return assocs + + def _default_assoc(self, objectpath): + return objectpath + + def _assocnames_lunmaskctrl(self): + return self._enum_lunmaskctrls() + + def _default_assocnames(self, objectpath): + return objectpath + + def _getinstance_storagevolume(self, objectpath): + instance = EMC_StorageVolume() + vols = self._enum_storagevolumes() + for vol in vols: + if vol['DeviceID'] == objectpath['DeviceID']: + instance = vol + break + return instance + + def _getinstance_lunmask(self): + lunmask = {} + lunmask['CreationClassName'] = lunmask_creationclass + lunmask['DeviceID'] = lunmaskctrl_id + lunmask['SystemName'] = storage_system + return lunmask + + def _getinstance_unit(self, objectpath): + unit = {} + + dependent = {} + dependent['CreationClassName'] = vol_creationclass + dependent['DeviceID'] = test_volume['id'] + dependent['ElementName'] = test_volume['name'] + dependent['SystemName'] = storage_system + + antecedent = {} + antecedent['CreationClassName'] = lunmask_creationclass + antecedent['DeviceID'] = lunmaskctrl_id + antecedent['SystemName'] = storage_system + + unit['Dependent'] = dependent + unit['Antecedent'] = antecedent + unit['CreationClassName'] = unit_creationclass + unit['DeviceNumber'] = '0' + + return unit + + def _default_getinstance(self, objectpath): + return objectpath + + def _enum_replicationservices(self): + rep_services = [] + rep_service = {} + rep_service['SystemName'] = storage_system + rep_service['CreationClassName'] = rep_service_creationclass + rep_services.append(rep_service) + return rep_services + + def _enum_stconfsvcs(self): + conf_services = [] + conf_service = {} + conf_service['SystemName'] = storage_system + conf_service['CreationClassName'] = stconf_service_creationclass + conf_services.append(conf_service) + return conf_services + + def _enum_ctrlconfsvcs(self): + conf_services = [] + conf_service = {} + conf_service['SystemName'] = storage_system + conf_service['CreationClassName'] = ctrlconf_service_creationclass + conf_services.append(conf_service) + return conf_services + + def _enum_pools(self): + pools = [] + pool = {} + pool['InstanceID'] = storage_system + '+U+' + storage_type + pool['CreationClassName'] = 'Clar_UnifiedStoragePool' + pools.append(pool) + return pools + + def _enum_storagevolumes(self): + vols = [] + vol = EMC_StorageVolume() + vol['CreationClassName'] = 'Clar_StorageVolume' + vol['ElementName'] = test_volume['name'] + vol['DeviceID'] = test_volume['id'] + vol['SystemName'] = storage_system + vol.path = {'DeviceID': vol['DeviceID']} + vols.append(vol) + + snap_vol = EMC_StorageVolume() + snap_vol['CreationClassName'] = 'Clar_StorageVolume' + snap_vol['ElementName'] = test_snapshot['name'] + snap_vol['DeviceID'] = test_snapshot['id'] + snap_vol['SystemName'] = storage_system + snap_vol.path = {'DeviceID': snap_vol['DeviceID']} + vols.append(snap_vol) + + clone_vol = EMC_StorageVolume() + clone_vol['CreationClassName'] = 'Clar_StorageVolume' + clone_vol['ElementName'] = test_clone['name'] + clone_vol['DeviceID'] = test_clone['id'] + clone_vol['SystemName'] = storage_system + clone_vol.path = {'DeviceID': clone_vol['DeviceID']} + vols.append(clone_vol) + + return vols + + def _enum_syncsvsvs(self): + syncs = [] + sync = {} + + vols = self._enum_storagevolumes() + objpath1 = vols[0] + objpath2 = vols[1] + sync['SyncedElement'] = objpath2 + sync['SystemElement'] = objpath1 + sync['CreationClassName'] = 'SE_StorageSynchronized_SV_SV' + syncs.append(sync) + + return syncs + + def _enum_unitnames(self): + return self._ref_unitnames() + + def _enum_lunmaskctrls(self): + ctrls = [] + ctrl = {} + ctrl['CreationClassName'] = lunmask_creationclass + ctrl['DeviceID'] = lunmaskctrl_id + ctrl['SystemName'] = storage_system + ctrls.append(ctrl) + return ctrls + + def _default_enum(self): + names = [] + name = {} + name['Name'] = 'default' + names.append(name) + return names + + +class EMCISCSIDriverTestCase(test.TestCase): + + def setUp(self): + super(EMCISCSIDriverTestCase, self).setUp() + driver = EMCISCSIDriver() + self.driver = driver + self.stubs.Set(EMCISCSIDriver, '_get_iscsi_properties', + self.fake_get_iscsi_properties) + self.stubs.Set(EMCISCSIDriver, '_get_ecom_connection', + self.fake_ecom_connection) + self.stubs.Set(EMCISCSIDriver, '_get_storage_type', + self.fake_storage_type) + + def fake_ecom_connection(self): + conn = FakeEcomConnection() + return conn + + def fake_get_iscsi_properties(self, volume): + LOG.info('Fake _get_iscsi_properties.') + properties = {} + properties['target_discovered'] = True + properties['target_portal'] = '10.10.10.10' + properties['target_iqn'] = 'iqn.1993-08.org.debian:01:a1b2c3d4e5f6' + device_number = '000008' + 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.info(_("Fake ISCSI properties: %s") % (properties)) + return properties + + def fake_storage_type(self, filename=None): + return storage_type + + def test_create_destroy(self): + self.driver.create_volume(test_volume) + self.driver.delete_volume(test_volume) + + def test_create_volume_snapshot_destroy(self): + self.driver.create_volume(test_volume) + self.driver.create_snapshot(test_snapshot) + self.driver.create_volume_from_snapshot( + test_clone, test_snapshot) + self.driver.delete_volume(test_clone) + self.driver.delete_snapshot(test_snapshot) + self.driver.delete_volume(test_volume) + + def test_map_unmap(self): + self.driver.create_volume(test_volume) + export = self.driver.create_export(None, test_volume) + test_volume['provider_location'] = export['provider_location'] + connector = {'initiator': initiator1} + connection_info = self.driver.initialize_connection(test_volume, + connector) + self.driver.terminate_connection(test_volume, connector) + self.driver.remove_export(None, test_volume) + self.driver.delete_volume(test_volume) diff --git a/cinder/volume/drivers/emc.py b/cinder/volume/drivers/emc.py new file mode 100644 index 000000000..c128364cc --- /dev/null +++ b/cinder/volume/drivers/emc.py @@ -0,0 +1,1445 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 EMC Corporation, Inc. +# 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. +""" +Drivers for EMC volumes. + +""" + +import os +import time +from xml.dom.minidom import parseString + +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__) + +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')) + +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""" + + def __init__(self, *args, **kwargs): + + super(EMCISCSIDriver, self).__init__(*args, **kwargs) + + opt = cfg.StrOpt('cinder_emc_config_file', + default=CINDER_EMC_CONFIG_FILE, + help='use this file for cinder emc plugin ' + '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. """ + + LOG.debug(_('Entering create_volume.')) + volumesize = int(volume['size']) * 1073741824 + volumename = volume['name'] + + LOG.info(_('Create Volume: %(volume)s Size: %(size)lu') + % {'volume': volumename, + 'size': volumesize}) + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Error Create Volume: %(volumename)s. " + "Cannot connect to ECOM server.") + % {'volumename': volumename}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + storage_type = self._get_storage_type() + if storage_type is None: + exception_message = (_("Error Create Volume: %(volumename)s. " + "Storage type %(storage_type)s not found.") + % {'volumename': volumename, + 'storage_type': storage_type}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug(_('Create Volume: %(volume)s ' + 'Storage type: %(storage_type)s') + % {'volume': volumename, + 'storage_type': storage_type}) + + pool, storage_system = self._find_pool(storage_type) + if pool is None: + exception_message = (_("Error Create Volume: %(volumename)s. " + "Pool %(storage_type)s not found.") + % {'volumename': volumename, + 'storage_type': storage_type}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + if storage_system is None: + exception_message = (_("Error Create Volume: %(volumename)s. " + "Storage system not found for pool " + "%(storage_type)s.") + % {'volumename': volumename, + 'storage_type': storage_type}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug(_('Create Volume: %(volume)s Pool: %(pool)s ' + 'Storage System: %(storage_system)s') + % {'volume': volumename, + 'pool': str(pool), + 'storage_system': storage_system}) + + configservice = self._find_storage_configuration_service( + storage_system) + if configservice is None: + exception_message = (_("Error Create Volume: %(volumename)s. " + "Storage Configuration Service not found for " + "pool %(storage_type)s.") + % {'volumename': volumename, + 'storage_type': storage_type}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug(_('Create Volume: %(name)s Method: ' + 'CreateOrModifyElementFromStoragePool ConfigServicie: ' + '%(service)s ElementName: %(name)s InPool: %(pool)s ' + 'ElementType: 5 Size: %(size)lu') + % {'service': str(configservice), + 'name': volumename, + 'pool': str(pool), + 'size': volumesize}) + + rc, job = conn.InvokeMethod( + 'CreateOrModifyElementFromStoragePool', + configservice, ElementName=volumename, InPool=pool, + ElementType=self._getnum(5, '16'), + Size=self._getnum(volumesize, '64')) + + LOG.debug(_('Create Volume: %(volumename)s Return code: %(rc)lu') + % {'volumename': volumename, + 'rc': rc}) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + LOG.error(_('Error Create Volume: %(volumename)s. ' + 'Return code: %(rc)lu. Error: %(error)s') + % {'volumename': volumename, + 'rc': rc, + 'error': errordesc}) + raise exception.VolumeBackendAPIException(data=errordesc) + + LOG.debug(_('Leaving create_volume: %(volumename)s ' + 'Return code: %(rc)lu') + % {'volumename': volumename, + 'rc': rc}) + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + + LOG.debug(_('Entering create_volume_from_snapshot.')) + + snapshotname = snapshot['name'] + volumename = volume['name'] + + LOG.info(_('Create Volume from Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s') + % {'volumename': volumename, + 'snapshotname': snapshotname}) + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_('Error Create Volume from Snapshot: ' + 'Volume: %(volumename)s Snapshot: ' + '%(snapshotname)s. Cannot connect to' + ' ECOM server.') + % {'volumename': volumename, + 'snapshotname': snapshotname}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + snapshot_instance = self._find_lun(snapshot) + storage_system = snapshot_instance['SystemName'] + + LOG.debug(_('Create Volume from Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s Snapshot Instance: ' + '%(snapshotinstance)s Storage System: %(storage_system)s.') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'snapshotinstance': str(snapshot_instance.path), + 'storage_system': storage_system}) + + isVMAX = storage_system.find('SYMMETRIX') + if isVMAX > -1: + exception_message = (_('Error Create Volume from Snapshot: ' + 'Volume: %(volumename)s Snapshot: ' + '%(snapshotname)s. Create Volume ' + 'from Snapshot is NOT supported on VMAX.') + % {'volumename': volumename, + 'snapshotname': snapshotname}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + repservice = self._find_replication_service(storage_system) + if repservice is None: + exception_message = (_('Error Create Volume from Snapshot: ' + 'Volume: %(volumename)s Snapshot: ' + '%(snapshotname)s. Cannot find Replication ' + 'Service to create volume from snapshot.') + % {'volumename': volumename, + 'snapshotname': snapshotname}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug(_('Create Volume from Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s Method: CreateElementReplica ' + 'ReplicationService: %(service)s ElementName: ' + '%(elementname)s SyncType: 8 SourceElement: ' + '%(sourceelement)s') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'service': str(repservice), + 'elementname': volumename, + 'sourceelement': str(snapshot_instance.path)}) + + # Create a Clone from snapshot + rc, job = conn.InvokeMethod( + 'CreateElementReplica', repservice, + ElementName=volumename, + SyncType=self._getnum(8, '16'), + SourceElement=snapshot_instance.path) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + exception_message = (_('Error Create Volume from Snapshot: ' + 'Volume: %(volumename)s Snapshot:' + '%(snapshotname)s. Return code: %(rc)lu.' + 'Error: %(error)s') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc, + 'error': errordesc}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + data=exception_message) + + LOG.debug(_('Create Volume from Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s. Successfully clone volume ' + 'from snapshot. Finding the clone relationship.') + % {'volumename': volumename, + 'snapshotname': snapshotname}) + + sync_name, storage_system = self._find_storage_sync_sv_sv( + volumename, snapshotname) + + # Remove the Clone relationshop so it can be used as a regular lun + # 8 - Detach operation + LOG.debug(_('Create Volume from Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s. Remove the clone ' + 'relationship. Method: ModifyReplicaSynchronization ' + 'ReplicationService: %(service)s Operation: 8 ' + 'Synchronization: %(sync_name)s') + % {'volumename': volumename, + 'snapshotname': snapshotname, + '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 Volume from Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s Return code: %(rc)lu') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc}) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + exception_message = (_('Error Create Volume from Snapshot: ' + 'Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s. ' + 'Return code: %(rc)lu. Error: %(error)s') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc, + 'error': errordesc}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + data=exception_message) + + LOG.debug(_('Leaving create_volume_from_snapshot: Volume: ' + '%(volumename)s Snapshot: %(snapshotname)s ' + 'Return code: %(rc)lu.') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc}) + + def delete_volume(self, volume): + """Deletes an EMC volume.""" + LOG.debug(_('Entering delete_volume.')) + volumename = volume['name'] + LOG.info(_('Delete Volume: %(volume)s') + % {'volume': volumename}) + + vol_instance = self._find_lun(volume) + if vol_instance is None: + LOG.error(_('Volume %(name)s not found on the array. ' + 'No volume to delete.') + % {'name': volumename}) + return + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Error Delete Volume: %(volumename)s. " + "Cannot connect to ECOM server.") + % {'volumename': volumename}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + storage_system = vol_instance['SystemName'] + + configservice = self._find_storage_configuration_service( + storage_system) + if configservice is None: + exception_message = (_("Error Delete Volume: %(volumename)s. " + "Storage Configuration Service not found.") + % {'volumename': volumename}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException(data=exception_message) + + device_id = vol_instance['DeviceID'] + + LOG.debug(_('Delete Volume: %(name)s DeviceID: %(deviceid)s') + % {'name': volumename, + 'deviceid': device_id}) + + LOG.debug(_('Delete Volume: %(name)s Method: EMCReturnToStoragePool ' + 'ConfigServic: %(service)s TheElement: %(vol_instance)s') + % {'service': str(configservice), + 'name': volumename, + 'vol_instance': str(vol_instance.path)}) + + rc, job = conn.InvokeMethod( + 'EMCReturnToStoragePool', + configservice, TheElements=[vol_instance.path]) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + exception_message = (_('Error Delete Volume: %(volumename)s. ' + 'Return code: %(rc)lu. Error: %(error)s') + % {'volumename': volumename, + 'rc': rc, + 'error': errordesc}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + data=exception_message) + + LOG.debug(_('Leaving delete_volume: %(volumename)s Return code: ' + '%(rc)lu') + % {'volumename': volumename, + 'rc': rc}) + + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + LOG.debug(_('Entering create_snapshot.')) + + snapshotname = snapshot['name'] + volumename = snapshot['volume_name'] + LOG.info(_('Create snapshot: %(snapshot)s: volume: %(volume)s') + % {'snapshot': snapshotname, + 'volume': volumename}) + + conn = self._get_ecom_connection() + if conn is None: + LOG.error(_('Cannot connect to ECOM server.')) + exception_message = (_("Cannot connect to ECOM server")) + raise exception.VolumeBackendAPIException(data=exception_message) + + volume = {} + volume['name'] = volumename + volume['provider_location'] = None + vol_instance = self._find_lun(volume) + device_id = vol_instance['DeviceID'] + storage_system = vol_instance['SystemName'] + LOG.debug(_('Device ID: %(deviceid)s: Storage System: ' + '%(storagesystem)s') + % {'deviceid': device_id, + 'storagesystem': storage_system}) + + repservice = self._find_replication_service(storage_system) + if repservice is None: + LOG.error(_("Cannot find Replication Service to create snapshot " + "for volume %s.") % volumename) + exception_message = (_("Cannot find Replication Service to " + "create snapshot for volume %s.") + % volumename) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug(_("Create Snapshot: Method: CreateElementReplica: " + "Target: %(snapshot)s Source: %(volume)s Replication " + "Service: %(service)s ElementName: %(elementname)s Sync " + "Type: 7 SourceElement: %(sourceelement)s.") + % {'snapshot': snapshotname, + 'volume': volumename, + 'service': str(repservice), + 'elementname': snapshotname, + 'sourceelement': str(vol_instance.path)}) + + rc, job = conn.InvokeMethod( + 'CreateElementReplica', repservice, + ElementName=snapshotname, + SyncType=self._getnum(7, '16'), + SourceElement=vol_instance.path) + + LOG.debug(_('Create Snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s Return code: %(rc)lu') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc}) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + exception_message = (_('Error Create Snapshot: (snapshot)s ' + 'Volume: %(volume)s Error: %(errordesc)s') + % {'snapshot': snapshotname, 'volume': + volumename, 'errordesc': errordesc}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + data=exception_message) + + LOG.debug(_('Leaving create_snapshot: Snapshot: %(snapshot)s ' + 'Volume: %(volume)s Return code: %(rc)lu.') % + {'snapshot': snapshotname, 'volume': volumename, 'rc': rc}) + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + LOG.debug(_('Entering delete_snapshot.')) + + snapshotname = snapshot['name'] + volumename = snapshot['volume_name'] + LOG.info(_('Delete Snapshot: %(snapshot)s: volume: %(volume)s') + % {'snapshot': snapshotname, + 'volume': volumename}) + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Cannot connect to ECOM server")) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug(_('Delete Snapshot: %(snapshot)s: volume: %(volume)s. ' + 'Finding StorageSychronization_SV_SV.') + % {'snapshot': snapshotname, + 'volume': volumename}) + + sync_name, storage_system = self._find_storage_sync_sv_sv( + snapshotname, volumename) + if sync_name is None: + LOG.error(_('Snapshot: %(snapshot)s: volume: %(volume)s ' + 'not found on the array. No snapshot to delete.') + % {'snapshot': snapshotname, + 'volume': volumename}) + return + + repservice = self._find_replication_service(storage_system) + if repservice is None: + exception_message = (_("Cannot find Replication Service to " + "create snapshot for volume %s.") + % volumename) + raise exception.VolumeBackendAPIException(data=exception_message) + + # Delete snapshot - deletes both the target element + # and the snap session + LOG.debug(_("Delete Snapshot: Target: %(snapshot)s " + "Source: %(volume)s. Method: " + "ModifyReplicaSynchronization: " + "Replication Service: %(service)s Operation: 19 " + "Synchronization: %(sync_name)s.") + % {'snapshot': snapshotname, + 'volume': volumename, + 'service': str(repservice), + 'sync_name': str(sync_name)}) + + rc, job = conn.InvokeMethod( + 'ModifyReplicaSynchronization', + repservice, + Operation=self._getnum(19, '16'), + Synchronization=sync_name) + + LOG.debug(_('Delete Snapshot: Volume: %(volumename)s Snapshot: ' + '%(snapshotname)s Return code: %(rc)lu') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc}) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + exception_message = (_('Error Delete Snapshot: Volume: ' + '%(volumename)s Snapshot: ' + '%(snapshotname)s. Return code: %(rc)lu.' + ' Error: %(error)s') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc, + 'error': errordesc}) + LOG.error(exception_message) + raise exception.VolumeBackendAPIException( + data=exception_message) + + LOG.debug(_('Leaving delete_snapshot: Volume: %(volumename)s ' + 'Snapshot: %(snapshotname)s Return code: %(rc)lu.') + % {'volumename': volumename, + 'snapshotname': snapshotname, + 'rc': rc}) + + 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.""" + vol_instance = self._find_lun(volume) + device_id = vol_instance['DeviceID'] + volumename = volume['name'] + LOG.debug(_('ensure_export: Volume: %(volume)s Device ID: ' + '%(device_id)s') + % {'volume': volumename, + 'device_id': device_id}) + + return {'provider_location': device_id} + + def create_export(self, context, volume): + """Driver entry point to get the export info for a new volume.""" + volumename = volume['name'] + LOG.info(_('Create export: %(volume)s') + % {'volume': volumename}) + vol_instance = self._find_lun(volume) + device_id = vol_instance['DeviceID'] + + LOG.debug(_('create_export: Volume: %(volume)s Device ID: ' + '%(device_id)s') + % {'volume': volumename, + 'device_id': device_id}) + + 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 + and therefore maps the volume to the host. + """ + volumename = vol_instance['ElementName'] + lun_name = vol_instance['DeviceID'] + initiator = self._find_initiator_name(connector) + storage_system = vol_instance['SystemName'] + lunmask_ctrl = self._find_lunmasking_scsi_protocol_controller( + storage_system, connector) + + LOG.debug(_('ExposePaths: %(vol)s ConfigServicie: %(service)s ' + 'LUNames: %(lun_name)s InitiatorPortIDs: %(initiator)s ' + 'DeviceAccesses: 2') + % {'vol': str(vol_instance.path), + 'service': str(configservice), + 'lun_name': lun_name, + 'initiator': initiator}) + + if lunmask_ctrl is None: + rc, controller = conn.InvokeMethod( + 'ExposePaths', + configservice, LUNames=[lun_name], + InitiatorPortIDs=[initiator], + DeviceAccesses=[self._getnum(2, '16')]) + else: + LOG.debug(_('ExposePaths parameter ' + 'LunMaskingSCSIProtocolController: ' + '%(lunmasking)s') + % {'lunmasking': str(lunmask_ctrl)}) + rc, controller = conn.InvokeMethod( + 'ExposePaths', + configservice, LUNames=[lun_name], + DeviceAccesses=[self._getnum(2, '16')], + ProtocolControllers=[lunmask_ctrl]) + + if rc != 0L: + msg = (_('Error mapping volume %s.') % volumename) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.debug(_('ExposePaths for volume %s completed successfully.') + % volumename) + + # Unmapping method for VNX + def _hide_paths(self, conn, configservice, vol_instance, connector): + """Removes a volume from the Storage Group + and therefore unmaps the volume from the host. + """ + volumename = vol_instance['ElementName'] + device_id = vol_instance['DeviceID'] + lunmask_ctrl = self._find_lunmasking_scsi_protocol_controller_for_vol( + vol_instance, connector) + + LOG.debug(_('HidePaths: %(vol)s ConfigServicie: %(service)s ' + 'LUNames: %(device_id)s LunMaskingSCSIProtocolController: ' + '%(lunmasking)s') + % {'vol': str(vol_instance.path), + 'service': str(configservice), + 'device_id': device_id, + 'lunmasking': str(lunmask_ctrl)}) + + rc, controller = conn.InvokeMethod( + 'HidePaths', configservice, + LUNames=[device_id], ProtocolControllers=[lunmask_ctrl]) + + if rc != 0L: + msg = (_('Error unmapping volume %s.') % volumename) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.debug(_('HidePaths for volume %s completed successfully.') + % volumename) + + # Mapping method for VMAX/VMAXe + def _add_members(self, conn, configservice, vol_instance, connector): + """Add volume to the Device Masking Group that belongs to + a Masking View""" + volumename = vol_instance['ElementName'] + masking_group = self._find_device_masking_group() + + LOG.debug(_('AddMembers: ConfigServicie: %(service)s MaskingGroup: ' + '%(masking_group)s Members: %(vol)s') + % {'service': str(configservice), + 'masking_group': str(masking_group), + 'vol': str(vol_instance.path)}) + + rc, job = conn.InvokeMethod( + 'AddMembers', configservice, + MaskingGroup=masking_group, Members=[vol_instance.path]) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + msg = (_('Error mapping volume %(vol)s. %(error)s') % + {'vol': volumename, 'error': errordesc}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + LOG.debug(_('AddMembers for volume %s completed successfully.') + % volumename) + + # Unmapping method for VMAX/VMAXe + def _remove_members(self, conn, configservice, vol_instance, connector): + """Removes an export for a volume.""" + volumename = vol_instance['ElementName'] + masking_group = self._find_device_masking_group() + + LOG.debug(_('RemoveMembers: ConfigServicie: %(service)s ' + 'MaskingGroup: %(masking_group)s Members: %(vol)s') + % {'service': str(configservice), + 'masking_group': str(masking_group), + 'vol': str(vol_instance.path)}) + + rc, job = conn.InvokeMethod('RemoveMembers', configservice, + MaskingGroup=masking_group, + Members=[vol_instance.path]) + + if rc != 0L: + rc, errordesc = self._wait_for_job_complete(job) + if rc != 0L: + msg = (_('Error unmapping volume %(vol)s. %(error)s') + % {'vol': volumename, 'error': errordesc}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + 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'] + LOG.info(_('Map volume: %(volume)s') + % {'volume': volumename}) + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Cannot connect to ECOM server")) + raise exception.VolumeBackendAPIException(data=exception_message) + + vol_instance = self._find_lun(volume) + storage_system = vol_instance['SystemName'] + + configservice = self._find_controller_configuration_service( + storage_system) + if configservice is None: + exception_message = (_("Cannot find Controller Configuration " + "Service for storage system %s") + % storage_system) + raise exception.VolumeBackendAPIException(data=exception_message) + + isVMAX = storage_system.find('SYMMETRIX') + if isVMAX > -1: + self._add_members(conn, configservice, vol_instance, connector) + else: + self._expose_paths(conn, configservice, vol_instance, connector) + + def _unmap_lun(self, volume, connector): + """Unmaps a volume from the host.""" + volumename = volume['name'] + LOG.info(_('Unmap volume: %(volume)s') + % {'volume': volumename}) + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Cannot connect to ECOM server")) + raise exception.VolumeBackendAPIException(data=exception_message) + + device_number = self._find_device_number(volume) + if device_number is None: + LOG.info(_("Volume %s is not mapped. No volume to unmap.") + % (volumename)) + return + + vol_instance = self._find_lun(volume) + storage_system = vol_instance['SystemName'] + + configservice = self._find_controller_configuration_service( + storage_system) + if configservice is None: + exception_message = (_("Cannot find Controller Configuration " + "Service for storage system %s") + % storage_system) + raise exception.VolumeBackendAPIException(data=exception_message) + + isVMAX = storage_system.find('SYMMETRIX') + if isVMAX > -1: + self._remove_members(conn, configservice, vol_instance, connector) + else: + 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, + } + } + + """ + volumename = volume['name'] + LOG.info(_('Initialize connection: %(volume)s') + % {'volume': volumename}) + 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'] + LOG.info(_('Terminate connection: %(volume)s') + % {'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'] + + 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.NovaException(_("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_storage_type(self, filename=None): + """Get the storage type from the config file + """ + if filename == None: + filename = FLAGS.cinder_emc_config_file + + file = open(filename, 'r') + data = file.read() + file.close() + dom = parseString(data) + storageTypes = dom.getElementsByTagName('StorageType') + if storageTypes is not None and len(storageTypes) > 0: + storageType = storageTypes[0].toxml() + storageType = storageType.replace('', '') + storageType = storageType.replace('', '') + LOG.debug(_("Found Storage Type: %s") % (storageType)) + return storageType + else: + LOG.debug(_("Storage Type not found.")) + return None + + def _get_masking_view(self, filename=None): + if filename == None: + filename = FLAGS.cinder_emc_config_file + + file = open(filename, 'r') + data = file.read() + file.close() + dom = parseString(data) + views = dom.getElementsByTagName('MaskingView') + if views is not None and len(views) > 0: + view = views[0].toxml().replace('', '') + view = view.replace('', '') + LOG.debug(_("Found Masking View: %s") % (view)) + return view + else: + LOG.debug(_("Masking View not found.")) + return None + + def _get_ecom_cred(self, filename=None): + if filename == None: + filename = FLAGS.cinder_emc_config_file + + file = open(filename, 'r') + data = file.read() + file.close() + dom = parseString(data) + ecomUsers = dom.getElementsByTagName('EcomUserName') + if ecomUsers is not None and len(ecomUsers) > 0: + ecomUser = ecomUsers[0].toxml().replace('', '') + ecomUser = ecomUser.replace('', '') + ecomPasswds = dom.getElementsByTagName('EcomPassword') + if ecomPasswds is not None and len(ecomPasswds) > 0: + ecomPasswd = ecomPasswds[0].toxml().replace('', '') + ecomPasswd = ecomPasswd.replace('', '') + if ecomUser is not None and ecomPasswd is not None: + return ecomUser, ecomPasswd + else: + LOG.debug(_("Ecom user not found.")) + return None + + def _get_ecom_server(self, filename=None): + if filename == None: + filename = FLAGS.cinder_emc_config_file + + file = open(filename, 'r') + data = file.read() + file.close() + dom = parseString(data) + ecomIps = dom.getElementsByTagName('EcomServerIp') + if ecomIps is not None and len(ecomIps) > 0: + ecomIp = ecomIps[0].toxml().replace('', '') + ecomIp = ecomIp.replace('', '') + ecomPorts = dom.getElementsByTagName('EcomServerPort') + if ecomPorts is not None and len(ecomPorts) > 0: + ecomPort = ecomPorts[0].toxml().replace('', '') + ecomPort = ecomPort.replace('', '') + if ecomIp is not None and ecomPort is not None: + LOG.debug(_("Ecom IP: %(ecomIp)s Port: %(ecomPort)s") % (locals())) + return ecomIp, ecomPort + else: + LOG.debug(_("Ecom server not found.")) + return None + + def _get_ecom_connection(self, filename=None): + ip, port = self._get_ecom_server() + user, passwd = self._get_ecom_cred() + url = 'http://' + ip + ':' + port + conn = pywbem.WBEMConnection(url, (user, passwd), + default_namespace='root/emc') + + return conn + + def _find_replication_service(self, storage_system): + foundRepService = None + conn = self._get_ecom_connection() + repservices = conn.EnumerateInstanceNames('EMC_ReplicationService') + for repservice in repservices: + if storage_system == repservice['SystemName']: + foundRepService = repservice + LOG.debug(_("Found Replication Service: %s") + % (str(repservice))) + break + + return foundRepService + + def _find_storage_configuration_service(self, storage_system): + foundConfigService = None + conn = self._get_ecom_connection() + configservices = conn.EnumerateInstanceNames( + 'EMC_StorageConfigurationService') + for configservice in configservices: + if storage_system == configservice['SystemName']: + foundConfigService = configservice + LOG.debug(_("Found Storage Configuration Service: %s") + % (str(configservice))) + break + + return foundConfigService + + def _find_controller_configuration_service(self, storage_system): + foundConfigService = None + conn = self._get_ecom_connection() + configservices = conn.EnumerateInstanceNames( + 'EMC_ControllerConfigurationService') + for configservice in configservices: + if storage_system == configservice['SystemName']: + foundConfigService = configservice + LOG.debug(_("Found Controller Configuration Service: %s") + % (str(configservice))) + break + + return foundConfigService + + # Find pool based on storage_type + def _find_pool(self, storage_type): + foundPool = None + systemname = None + conn = self._get_ecom_connection() + vpools = conn.EnumerateInstanceNames('EMC_VirtualProvisioningPool') + upools = conn.EnumerateInstanceNames('EMC_UnifiedStoragePool') + for upool in upools: + poolinstance = upool['InstanceID'] + # Example: CLARiiON+APM00115204878+U+Pool 0 + poolname, systemname = self._parse_pool_instance_id(poolinstance) + if poolname is not None and systemname is not None: + if storage_type == poolname: + foundPool = upool + break + if foundPool is not None and systemname is not None: + return foundPool, systemname + + for vpool in vpools: + poolinstance = vpool['InstanceID'] + # Example: SYMMETRIX+000195900551+TP+Sol_Innov + poolname, systemname = self._parse_pool_instance_id(poolinstance) + if poolname is not None and systemname is not None: + if storage_type == poolname: + foundPool = vpool + break + + LOG.debug(_("Pool: %(pool)s SystemName: %(systemname)s.") + % {'pool': str(foundPool), 'systemname': systemname}) + return foundPool, systemname + + def _parse_pool_instance_id(self, instanceid): + # Example of pool InstanceId: CLARiiON+APM00115204878+U+Pool 0 + poolname = None + systemname = None + endp = instanceid.rfind('+') + if endp > -1: + poolname = instanceid[endp + 1:] + + idarray = instanceid.split('+') + if len > 2: + systemname = idarray[0] + '+' + idarray[1] + + LOG.debug(_("Pool name: %(poolname)s System name: %(systemname)s.") + % {'poolname': poolname, 'systemname': systemname}) + return poolname, systemname + + def _find_lun(self, volume): + foundinstance = None + try: + device_id = volume['provider_location'] + except Exception: + device_id = None + + volumename = volume['name'] + conn = self._get_ecom_connection() + + names = conn.EnumerateInstanceNames('EMC_StorageVolume') + + for n in names: + if device_id is not None: + if n['DeviceID'] == device_id: + vol_instance = conn.GetInstance(n) + foundinstance = vol_instance + break + else: + continue + + else: + vol_instance = conn.GetInstance(n) + if vol_instance['ElementName'] == volumename: + foundinstance = vol_instance + volume['provider_location'] = foundinstance['DeviceID'] + break + + if foundinstance is None: + LOG.debug(_("Volume %(volumename)s not found on the array.") + % {'volumename': volumename}) + else: + LOG.debug(_("Volume name: %(volumename)s Volume instance: " + "%(vol_instance)s.") + % {'volumename': volumename, + 'vol_instance': str(foundinstance.path)}) + + return foundinstance + + def _find_storage_sync_sv_sv(self, snapshotname, volumename): + foundsyncname = None + storage_system = None + + LOG.debug(_("Source: %(volumename)s Target: %(snapshotname)s.") + % {'volumename': volumename, 'snapshotname': snapshotname}) + + conn = self._get_ecom_connection() + + names = conn.EnumerateInstanceNames('SE_StorageSynchronized_SV_SV') + + for n in names: + snapshot_instance = conn.GetInstance(n['SyncedElement'], + LocalOnly=False) + if snapshotname != snapshot_instance['ElementName']: + continue + + vol_instance = conn.GetInstance(n['SystemElement'], + LocalOnly=False) + if vol_instance['ElementName'] == volumename: + foundsyncname = n + storage_system = vol_instance['SystemName'] + break + + if foundsyncname is None: + LOG.debug(_("Source: %(volumename)s Target: %(snapshotname)s. " + "Storage Synchronized not found. ") + % {'volumename': volumename, + 'snapshotname': snapshotname}) + else: + LOG.debug(_("Storage system: %(storage_system)s " + "Storage Synchronized instance: %(sync)s.") + % {'storage_system': storage_system, + 'sync': str(foundsyncname)}) + return foundsyncname, storage_system + + def _find_initiator_name(self, connector): + """ Get initiator name from connector['initiator'] + """ + foundinitiatorname = None + if connector['initiator']: + foundinitiatorname = connector['initiator'] + + LOG.debug(_("Initiator name: %(initiator)s.") + % {'initiator': foundinitiatorname}) + return foundinitiatorname + + def _wait_for_job_complete(self, job): + jobinstancename = job['Job'] + + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Cannot connect to ECOM server")) + raise exception.VolumeBackendAPIException(data=exception_message) + + while True: + jobinstance = conn.GetInstance(jobinstancename, LocalOnly=False) + jobstate = jobinstance['JobState'] + # From ValueMap of JobState in CIM_ConcreteJob + # 2L=New, 3L=Starting, 4L=Running, 32767L=Queue Pending + # ValueMap("2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13..32767, + # 32768..65535"), + # Values("New, Starting, Running, Suspended, Shutting Down, + # Completed, Terminated, Killed, Exception, Service, + # Query Pending, DMTF Reserved, Vendor Reserved")] + if jobstate in [2L, 3L, 4L, 32767L]: + time.sleep(10) + else: + break + + rc = jobinstance['ErrorCode'] + errordesc = jobinstance['ErrorDescription'] + + return rc, errordesc + + # Find LunMaskingSCSIProtocolController for the local host on the + # specified storage system + def _find_lunmasking_scsi_protocol_controller(self, storage_system, + connector): + foundCtrl = None + conn = self._get_ecom_connection() + initiator = self._find_initiator_name(connector) + controllers = conn.EnumerateInstanceNames( + 'EMC_LunMaskingSCSIProtocolController') + for ctrl in controllers: + if storage_system != ctrl['SystemName']: + continue + associators = conn.Associators(ctrl, + resultClass='EMC_StorageHardwareID') + for assoc in associators: + # if EMC_StorageHardwareID matches the initiator, + # we found the existing EMC_LunMaskingSCSIProtocolController + # (Storage Group for VNX) + # we can use for masking a new LUN + if assoc['StorageID'] == initiator: + foundCtrl = ctrl + break + + if foundCtrl is not None: + break + + LOG.debug(_("LunMaskingSCSIProtocolController for storage system " + "%(storage_system)s and initiator %(initiator)s is " + "%(ctrl)s.") + % {'storage_system': storage_system, + 'initiator': initiator, + 'ctrl': str(foundCtrl)}) + return foundCtrl + + # Find LunMaskingSCSIProtocolController for the local host and the + # specified storage volume + def _find_lunmasking_scsi_protocol_controller_for_vol(self, vol_instance, + connector): + foundCtrl = None + conn = self._get_ecom_connection() + initiator = self._find_initiator_name(connector) + controllers = conn.AssociatorNames( + vol_instance.path, + resultClass='EMC_LunMaskingSCSIProtocolController') + + for ctrl in controllers: + associators = conn.Associators( + ctrl, + resultClass='EMC_StorageHardwareID') + for assoc in associators: + # if EMC_StorageHardwareID matches the initiator, + # we found the existing EMC_LunMaskingSCSIProtocolController + # (Storage Group for VNX) + # we can use for masking a new LUN + if assoc['StorageID'] == initiator: + foundCtrl = ctrl + break + + if foundCtrl is not None: + break + + LOG.debug(_("LunMaskingSCSIProtocolController for storage volume " + "%(vol)s and initiator %(initiator)s is %(ctrl)s.") + % {'vol': str(vol_instance.path), 'initiator': initiator, + 'ctrl': str(foundCtrl)}) + return foundCtrl + + # Find an available device number that a host can see + def _find_avail_device_number(self, storage_system): + out_device_number = '000000' + out_num_device_number = 0 + numlist = [] + myunitnames = [] + + conn = self._get_ecom_connection() + unitnames = conn.EnumerateInstanceNames( + 'CIM_ProtocolControllerForUnit') + for unitname in unitnames: + controller = unitname['Antecedent'] + if storage_system != controller['SystemName']: + continue + classname = controller['CreationClassName'] + index = classname.find('LunMaskingSCSIProtocolController') + if index > -1: + unitinstance = conn.GetInstance(unitname, LocalOnly=False) + numDeviceNumber = int(unitinstance['DeviceNumber']) + numlist.append(numDeviceNumber) + myunitnames.append(unitname) + + maxnum = max(numlist) + out_num_device_number = maxnum + 1 + + out_device_number = '%06d' % out_num_device_number + + LOG.debug(_("Available device number on %(storage)s: %(device)s.") + % {'storage': storage_system, 'device': out_device_number}) + return out_device_number + + # Find a device number that a host can see for a volume + def _find_device_number(self, volume): + out_num_device_number = None + + conn = self._get_ecom_connection() + volumename = volume['name'] + vol_instance = self._find_lun(volume) + + unitnames = conn.ReferenceNames( + vol_instance.path, + ResultClass='CIM_ProtocolControllerForUnit') + + for unitname in unitnames: + controller = unitname['Antecedent'] + classname = controller['CreationClassName'] + index = classname.find('LunMaskingSCSIProtocolController') + if index > -1: # VNX + # Get an instance of CIM_ProtocolControllerForUnit + unitinstance = conn.GetInstance(unitname, LocalOnly=False) + numDeviceNumber = int(unitinstance['DeviceNumber'], 16) + out_num_device_number = numDeviceNumber + break + else: + index = classname.find('Symm_LunMaskingView') + if index > -1: # VMAX/VMAXe + unitinstance = conn.GetInstance(unitname, LocalOnly=False) + numDeviceNumber = int(unitinstance['DeviceNumber'], 16) + out_num_device_number = numDeviceNumber + break + + if out_num_device_number is None: + LOG.info(_("Device number not found for volume " + "%(volumename)s %(vol_instance)s.") % + {'volumename': volumename, + 'vol_instance': str(vol_instance.path)}) + else: + LOG.debug(_("Found device number %(device)d for volume " + "%(volumename)s %(vol_instance)s.") % + {'device': out_num_device_number, + 'volumename': volumename, + 'vol_instance': str(vol_instance.path)}) + + return out_num_device_number + + def _find_device_masking_group(self): + """Finds the Device Masking Group in a masking view.""" + foundMaskingGroup = None + maskingview_name = self._get_masking_view() + conn = self._get_ecom_connection() + if conn is None: + exception_message = (_("Cannot connect to ECOM server")) + raise exception.VolumeBackendAPIException(data=exception_message) + + maskingviews = conn.EnumerateInstanceNames( + 'EMC_LunMaskingSCSIProtocolController') + for view in maskingviews: + instance = conn.GetInstance(view, LocalOnly=False) + if maskingview_name == instance['ElementName']: + foundView = view + break + + groups = conn.AssociatorNames(foundView, + ResultClass='SE_DeviceMaskingGroup') + foundMaskingGroup = groups[0] + + LOG.debug(_("Masking view: %(view)s DeviceMaskingGroup: %(masking)s.") + % {'view': maskingview_name, + 'masking': str(foundMaskingGroup)}) + + return foundMaskingGroup + + def _getnum(self, num, datatype): + try: + result = { + '8': pywbem.Uint8(num), + '16': pywbem.Uint16(num), + '32': pywbem.Uint32(num), + '64': pywbem.Uint64(num) + } + result = result.get(datatype, num) + except NameError: + result = num + + return result diff --git a/etc/cinder/cinder_emc_config.xml.sample b/etc/cinder/cinder_emc_config.xml.sample new file mode 100644 index 000000000..d67ff37df --- /dev/null +++ b/etc/cinder/cinder_emc_config.xml.sample @@ -0,0 +1,12 @@ + + + +gold + +openstack + +x.x.x.x +xxxx +xxxxxxxx +xxxxxxxx +