From 5bc5af7a94a99f6457f5f553e14bcdc6d04381b1 Mon Sep 17 00:00:00 2001 From: tpsilva Date: Wed, 12 Dec 2018 10:53:58 -0200 Subject: [PATCH] Remove support for NetApp E-Series systems The deprecation notice [1] for this driver in Cinder was issued in Rocky, so now it is time to remove them from the tree. [1] http://lists.openstack.org/pipermail/openstack-operators/2018-July/015521.html Change-Id: I82cfbacdf572d68dc4e6c21a3d3005bc92344a38 --- cinder/opts.py | 1 - .../volume/drivers/netapp/eseries/__init__.py | 0 .../volume/drivers/netapp/eseries/fakes.py | 1469 ---------- .../drivers/netapp/eseries/test_client.py | 1216 -------- .../drivers/netapp/eseries/test_driver.py | 543 ---- .../drivers/netapp/eseries/test_fc_driver.py | 35 - .../netapp/eseries/test_host_mapper.py | 662 ----- .../netapp/eseries/test_iscsi_driver.py | 33 - .../drivers/netapp/eseries/test_library.py | 2570 ----------------- .../drivers/netapp/eseries/test_utils.py | 35 - cinder/volume/drivers/netapp/common.py | 6 - .../volume/drivers/netapp/eseries/__init__.py | 0 .../volume/drivers/netapp/eseries/client.py | 1055 ------- .../drivers/netapp/eseries/exception.py | 33 - .../drivers/netapp/eseries/fc_driver.py | 132 - .../drivers/netapp/eseries/host_mapper.py | 250 -- .../drivers/netapp/eseries/iscsi_driver.py | 129 - .../volume/drivers/netapp/eseries/library.py | 2147 -------------- cinder/volume/drivers/netapp/eseries/utils.py | 63 - cinder/volume/drivers/netapp/options.py | 39 +- doc/source/admin/blockstorage-groups.rst | 2 +- .../drivers/netapp-volume-driver.rst | 111 +- .../tables/cinder-netapp_cdot_iscsi.inc | 4 +- .../tables/cinder-netapp_cdot_nfs.inc | 4 +- .../tables/cinder-netapp_eseries_iscsi.inc | 48 - doc/source/contributor/groups.rst | 2 +- doc/source/reference/support-matrix.ini | 12 - .../remove_eseries-bb1bc134645aee50.yaml | 5 + 28 files changed, 19 insertions(+), 10587 deletions(-) delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/__init__.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_iscsi_driver.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py delete mode 100644 cinder/tests/unit/volume/drivers/netapp/eseries/test_utils.py delete mode 100644 cinder/volume/drivers/netapp/eseries/__init__.py delete mode 100644 cinder/volume/drivers/netapp/eseries/client.py delete mode 100644 cinder/volume/drivers/netapp/eseries/exception.py delete mode 100644 cinder/volume/drivers/netapp/eseries/fc_driver.py delete mode 100644 cinder/volume/drivers/netapp/eseries/host_mapper.py delete mode 100644 cinder/volume/drivers/netapp/eseries/iscsi_driver.py delete mode 100644 cinder/volume/drivers/netapp/eseries/library.py delete mode 100644 cinder/volume/drivers/netapp/eseries/utils.py delete mode 100644 doc/source/configuration/tables/cinder-netapp_eseries_iscsi.inc create mode 100644 releasenotes/notes/remove_eseries-bb1bc134645aee50.yaml diff --git a/cinder/opts.py b/cinder/opts.py index cf25d618c84..b8856189b0c 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -318,7 +318,6 @@ def list_opts(): cinder_volume_drivers_netapp_options.netapp_cluster_opts, cinder_volume_drivers_netapp_options.netapp_provisioning_opts, cinder_volume_drivers_netapp_options.netapp_img_cache_opts, - cinder_volume_drivers_netapp_options.netapp_eseries_opts, cinder_volume_drivers_netapp_options.netapp_nfs_extra_opts, cinder_volume_drivers_netapp_options.netapp_san_opts, cinder_volume_drivers_netapp_options.netapp_replication_opts, diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/__init__.py b/cinder/tests/unit/volume/drivers/netapp/eseries/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py deleted file mode 100644 index 2653b0e8c24..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py +++ /dev/null @@ -1,1469 +0,0 @@ -# Copyright (c) - 2015, Alex Meade -# Copyright (c) - 2015, Yogesh Kshirsagar -# Copyright (c) - 2015, Michael Price -# 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. - - -import copy -import json - -import mock - -from cinder.objects import fields -from cinder.tests.unit import fake_constants as fake -from cinder.volume import configuration as conf -from cinder.volume.drivers.netapp.eseries import utils -import cinder.volume.drivers.netapp.options as na_opts -import cinder.volume.drivers.netapp.utils as na_utils - -FAKE_CINDER_VOLUME = { - 'id': fake.VOLUME_ID, - 'size': 1, - 'volume_name': 'lun1', - 'host': 'hostname@backend#DDP', - 'os_type': 'linux', - 'provider_location': 'lun1', - 'name_id': fake.VOLUME2_ID, - 'provider_auth': 'provider a b', - 'project_id': fake.PROJECT_ID, - 'display_name': None, - 'display_description': 'lun1', - 'volume_type_id': None, - 'migration_status': None, - 'attach_status': fields.VolumeAttachStatus.DETACHED -} - -FAKE_CINDER_SNAPSHOT = { - 'id': fake.SNAPSHOT_ID, - 'volume': FAKE_CINDER_VOLUME, - 'provider_id': '3400000060080E500023BB3400631F335294A5A8', -} - -FAKE_CINDER_CG = { - 'id': fake.CONSISTENCY_GROUP_ID, -} - -FAKE_CINDER_CG_SNAPSHOT = { - 'id': fake.CGSNAPSHOT_ID, - 'consistencygroup_id': FAKE_CINDER_CG['id'], -} - -MULTIATTACH_HOST_GROUP = { - 'clusterRef': '8500000060080E500023C7340036035F515B78FC', - 'label': utils.MULTI_ATTACH_HOST_GROUP_NAME, -} - -FOREIGN_HOST_GROUP = { - 'clusterRef': '8500000060080E500023C7340036035F515B78FD', - 'label': 'FOREIGN HOST GROUP', -} - -HOST_GROUPS = [MULTIATTACH_HOST_GROUP, FOREIGN_HOST_GROUP] - -SSC_POOLS = [ - { - "poolId": "0400000060080E5000290D8000009C9955828DD2", - "name": "DDP", - "pool": { - "sequenceNum": 2, - "offline": False, - "raidLevel": "raidDiskPool", - "worldWideName": "60080E5000290D8000009C9955828DD2", - "volumeGroupRef": "0400000060080E5000290D8000009C9955828DD2", - "reserved1": "000000000000000000000000", - "reserved2": "", - "trayLossProtection": False, - "label": "DDP", - "state": "complete", - "spindleSpeedMatch": True, - "spindleSpeed": 7200, - "isInaccessible": False, - "securityType": "none", - "drawerLossProtection": True, - "protectionInformationCapable": False, - "protectionInformationCapabilities": { - "protectionInformationCapable": True, - "protectionType": "type2Protection" - }, - "volumeGroupData": { - "type": "diskPool", - "diskPoolData": { - "reconstructionReservedDriveCount": 1, - "reconstructionReservedAmt": "2992518463488", - "reconstructionReservedDriveCountCurrent": 1, - "poolUtilizationWarningThreshold": 100, - "poolUtilizationCriticalThreshold": 100, - "poolUtilizationState": "utilizationOptimal", - "unusableCapacity": "0", - "degradedReconstructPriority": "high", - "criticalReconstructPriority": "highest", - "backgroundOperationPriority": "low", - "allocGranularity": "4294967296" - } - }, - "usage": "standard", - "driveBlockFormat": "allNative", - "reservedSpaceAllocated": True, - "usedSpace": "13653701033984", - "totalRaidedSpace": "23459111370752", - "extents": [ - { - "sectorOffset": "0", - "rawCapacity": "9805410336768", - "raidLevel": "raidDiskPool", - "volumeGroupRef": - "0400000060080E5000290D8000009C9955828DD2", - "freeExtentRef": - "0301000060080E5000290D8000009C9955828DD2", - "reserved1": "000000000000000000000000", - "reserved2": "" - } - ], - "largestFreeExtentSize": "9805410336768", - "raidStatus": "optimal", - "freeSpace": "9805410336768", - "drivePhysicalType": "sas", - "driveMediaType": "hdd", - "normalizedSpindleSpeed": "spindleSpeed7200", - "id": "0400000060080E5000290D8000009C9955828DD2", - "diskPool": True, - "name": "DDP" - }, - "flashCacheCapable": True, - "dataAssuranceCapable": True, - "encrypted": False, - "thinProvisioningCapable": True, - "spindleSpeed": "spindleSpeed7200", - "raidLevel": "raidDiskPool", - "availableFreeExtentCapacities": [ - "9805410336768" - ] - }, - { - "poolId": "0400000060080E5000290D8000009CBA55828E96", - "name": "pool_raid1", - "pool": { - "sequenceNum": 6, - "offline": False, - "raidLevel": "raid1", - "worldWideName": "60080E5000290D8000009CBA55828E96", - "volumeGroupRef": "0400000060080E5000290D8000009CBA55828E96", - "reserved1": "000000000000000000000000", - "reserved2": "", - "trayLossProtection": False, - "label": "pool_raid1", - "state": "complete", - "spindleSpeedMatch": True, - "spindleSpeed": 10000, - "isInaccessible": False, - "securityType": "none", - "drawerLossProtection": True, - "protectionInformationCapable": False, - "protectionInformationCapabilities": { - "protectionInformationCapable": True, - "protectionType": "type2Protection" - }, - "volumeGroupData": { - "type": "unknown", - "diskPoolData": None - }, - "usage": "standard", - "driveBlockFormat": "allNative", - "reservedSpaceAllocated": True, - "usedSpace": "2978559819776", - "totalRaidedSpace": "6662444097536", - "extents": [ - { - "sectorOffset": "387891200", - "rawCapacity": "3683884277760", - "raidLevel": "raid1", - "volumeGroupRef": - "0400000060080E5000290D8000009CBA55828E96", - "freeExtentRef": - "030000B360080E5000290D8000009CBA55828E96", - "reserved1": "000000000000000000000000", - "reserved2": "" - } - ], - "largestFreeExtentSize": "3683884277760", - "raidStatus": "optimal", - "freeSpace": "3683884277760", - "drivePhysicalType": "sas", - "driveMediaType": "hdd", - "normalizedSpindleSpeed": "spindleSpeed10k", - "id": "0400000060080E5000290D8000009CBA55828E96", - "diskPool": False, - "name": "pool_raid1" - }, - "flashCacheCapable": False, - "dataAssuranceCapable": True, - "encrypted": False, - "thinProvisioningCapable": False, - "spindleSpeed": "spindleSpeed10k", - "raidLevel": "raid1", - "availableFreeExtentCapacities": [ - "3683884277760" - ] - }, - { - "poolId": "0400000060080E5000290D8000009CAB55828E51", - "name": "pool_raid6", - "pool": { - "sequenceNum": 3, - "offline": False, - "raidLevel": "raid6", - "worldWideName": "60080E5000290D8000009CAB55828E51", - "volumeGroupRef": "0400000060080E5000290D8000009CAB55828E51", - "reserved1": "000000000000000000000000", - "reserved2": "", - "trayLossProtection": False, - "label": "pool_raid6", - "state": "complete", - "spindleSpeedMatch": True, - "spindleSpeed": 15000, - "isInaccessible": False, - "securityType": "enabled", - "drawerLossProtection": False, - "protectionInformationCapable": False, - "protectionInformationCapabilities": { - "protectionInformationCapable": True, - "protectionType": "type2Protection" - }, - "volumeGroupData": { - "type": "unknown", - "diskPoolData": None - }, - "usage": "standard", - "driveBlockFormat": "allNative", - "reservedSpaceAllocated": True, - "usedSpace": "16413217521664", - "totalRaidedSpace": "16637410312192", - "extents": [ - { - "sectorOffset": "1144950784", - "rawCapacity": "224192790528", - "raidLevel": "raid6", - "volumeGroupRef": - "0400000060080E5000290D8000009CAB55828E51", - "freeExtentRef": - "0300005960080E5000290D8000009CAB55828E51", - "reserved1": "000000000000000000000000", - "reserved2": "" - } - ], - "largestFreeExtentSize": "224192790528", - "raidStatus": "optimal", - "freeSpace": "224192790528", - "drivePhysicalType": "sas", - "driveMediaType": "hdd", - "normalizedSpindleSpeed": "spindleSpeed15k", - "id": "0400000060080E5000290D8000009CAB55828E51", - "diskPool": False, - "name": "pool_raid6" - }, - "flashCacheCapable": False, - "dataAssuranceCapable": True, - "encrypted": True, - "thinProvisioningCapable": False, - "spindleSpeed": "spindleSpeed15k", - "raidLevel": "raid6", - "availableFreeExtentCapacities": [ - "224192790528" - ] - } -] - -STORAGE_POOLS = [ssc_pool['pool'] for ssc_pool in SSC_POOLS] - -VOLUMES = [ - { - "offline": False, - "extremeProtection": False, - "volumeHandle": 2, - "raidLevel": "raid0", - "sectorOffset": "0", - "worldWideName": "60080E50002998A00000945355C37C19", - "label": "1", - "blkSize": 512, - "capacity": "10737418240", - "reconPriority": 1, - "segmentSize": 131072, - "action": "initializing", - "cache": { - "cwob": False, - "enterpriseCacheDump": False, - "mirrorActive": True, - "mirrorEnable": True, - "readCacheActive": True, - "readCacheEnable": True, - "writeCacheActive": True, - "writeCacheEnable": True, - "cacheFlushModifier": "flush10Sec", - "readAheadMultiplier": 1 - }, - "mediaScan": { - "enable": False, - "parityValidationEnable": False - }, - "volumeRef": "0200000060080E50002998A00000945355C37C19", - "status": "optimal", - "volumeGroupRef": "fakevolgroupref", - "currentManager": "070000000000000000000001", - "preferredManager": "070000000000000000000001", - "perms": { - "mapToLUN": True, - "snapShot": True, - "format": True, - "reconfigure": True, - "mirrorPrimary": True, - "mirrorSecondary": True, - "copySource": True, - "copyTarget": True, - "readable": True, - "writable": True, - "rollback": True, - "mirrorSync": True, - "newImage": True, - "allowDVE": True, - "allowDSS": True, - "concatVolumeMember": True, - "flashReadCache": True, - "asyncMirrorPrimary": True, - "asyncMirrorSecondary": True, - "pitGroup": True, - "cacheParametersChangeable": True, - "allowThinManualExpansion": False, - "allowThinGrowthParametersChange": False, - "allowVaulting": False, - "allowRestore": False - }, - "mgmtClientAttribute": 0, - "dssPreallocEnabled": True, - "dssMaxSegmentSize": 2097152, - "preReadRedundancyCheckEnabled": False, - "protectionInformationCapable": False, - "protectionType": "type1Protection", - "applicationTagOwned": False, - "untrustworthy": 0, - "volumeUse": "standardVolume", - "volumeFull": False, - "volumeCopyTarget": False, - "volumeCopySource": False, - "pitBaseVolume": False, - "asyncMirrorTarget": False, - "asyncMirrorSource": False, - "remoteMirrorSource": False, - "remoteMirrorTarget": False, - "diskPool": False, - "flashCached": False, - "increasingBy": "0", - "metadata": [], - "dataAssurance": True, - "name": "1", - "id": "0200000060080E50002998A00000945355C37C19", - "wwn": "60080E50002998A00000945355C37C19", - "objectType": "volume", - "mapped": False, - "preferredControllerId": "070000000000000000000001", - "totalSizeInBytes": "10737418240", - "onlineVolumeCopy": False, - "listOfMappings": [], - "currentControllerId": "070000000000000000000001", - "cacheSettings": { - "cwob": False, - "enterpriseCacheDump": False, - "mirrorActive": True, - "mirrorEnable": True, - "readCacheActive": True, - "readCacheEnable": True, - "writeCacheActive": True, - "writeCacheEnable": True, - "cacheFlushModifier": "flush10Sec", - "readAheadMultiplier": 1 - }, - "thinProvisioned": False - }, - { - "volumeHandle": 16385, - "worldWideName": "60080E500029347000001D7B55C3791E", - "label": "2", - "allocationGranularity": 128, - "capacity": "53687091200", - "reconPriority": 1, - "volumeRef": "3A00000060080E500029347000001D7B55C3791E", - "status": "optimal", - "repositoryRef": "3600000060080E500029347000001D7955C3791D", - "currentManager": "070000000000000000000002", - "preferredManager": "070000000000000000000002", - "perms": { - "mapToLUN": True, - "snapShot": False, - "format": True, - "reconfigure": False, - "mirrorPrimary": False, - "mirrorSecondary": False, - "copySource": True, - "copyTarget": False, - "readable": True, - "writable": True, - "rollback": True, - "mirrorSync": True, - "newImage": True, - "allowDVE": True, - "allowDSS": True, - "concatVolumeMember": False, - "flashReadCache": True, - "asyncMirrorPrimary": True, - "asyncMirrorSecondary": True, - "pitGroup": True, - "cacheParametersChangeable": True, - "allowThinManualExpansion": False, - "allowThinGrowthParametersChange": False, - "allowVaulting": False, - "allowRestore": False - }, - "mgmtClientAttribute": 0, - "preReadRedundancyCheckEnabled": False, - "protectionType": "type0Protection", - "applicationTagOwned": True, - "maxVirtualCapacity": "69269232549888", - "initialProvisionedCapacity": "4294967296", - "currentProvisionedCapacity": "4294967296", - "provisionedCapacityQuota": "55834574848", - "growthAlertThreshold": 85, - "expansionPolicy": "automatic", - "volumeCache": { - "cwob": False, - "enterpriseCacheDump": False, - "mirrorActive": True, - "mirrorEnable": True, - "readCacheActive": True, - "readCacheEnable": True, - "writeCacheActive": True, - "writeCacheEnable": True, - "cacheFlushModifier": "flush10Sec", - "readAheadMultiplier": 0 - }, - "offline": False, - "volumeFull": False, - "volumeGroupRef": "0400000060080E50002998A00000945155C37C08", - "blkSize": 512, - "storageVolumeRef": "0200000060080E500029347000001D7855C3791D", - "volumeCopyTarget": False, - "volumeCopySource": False, - "pitBaseVolume": False, - "asyncMirrorTarget": False, - "asyncMirrorSource": False, - "remoteMirrorSource": False, - "remoteMirrorTarget": False, - "flashCached": False, - "mediaScan": { - "enable": False, - "parityValidationEnable": False - }, - "metadata": [], - "dataAssurance": False, - "name": "2", - "id": "3A00000060080E500029347000001D7B55C3791E", - "wwn": "60080E500029347000001D7B55C3791E", - "objectType": "thinVolume", - "mapped": False, - "diskPool": True, - "preferredControllerId": "070000000000000000000002", - "totalSizeInBytes": "53687091200", - "onlineVolumeCopy": False, - "listOfMappings": [], - "currentControllerId": "070000000000000000000002", - "segmentSize": 131072, - "cacheSettings": { - "cwob": False, - "enterpriseCacheDump": False, - "mirrorActive": True, - "mirrorEnable": True, - "readCacheActive": True, - "readCacheEnable": True, - "writeCacheActive": True, - "writeCacheEnable": True, - "cacheFlushModifier": "flush10Sec", - "readAheadMultiplier": 0 - }, - "thinProvisioned": True - } -] - -VOLUME = VOLUMES[0] - -STORAGE_POOL = { - 'label': 'DDP', - 'id': 'fakevolgroupref', - 'volumeGroupRef': 'fakevolgroupref', - 'raidLevel': 'raidDiskPool', - 'usedSpace': '16413217521664', - 'totalRaidedSpace': '16637410312192', -} - -INITIATOR_NAME = 'iqn.1998-01.com.vmware:localhost-28a58148' -INITIATOR_NAME_2 = 'iqn.1998-01.com.vmware:localhost-28a58149' -INITIATOR_NAME_3 = 'iqn.1998-01.com.vmware:localhost-28a58150' -WWPN = '20130080E5322230' -WWPN_2 = '20230080E5322230' - -FC_TARGET_WWPNS = [ - '500a098280feeba5', - '500a098290feeba5', - '500a098190feeba5', - '500a098180feeba5' -] - -FC_I_T_MAP = { - '20230080E5322230': [ - '500a098280feeba5', - '500a098290feeba5' - ], - '20130080E5322230': [ - '500a098190feeba5', - '500a098180feeba5' - ] -} - -FC_FABRIC_MAP = { - 'fabricB': { - 'target_port_wwn_list': [ - '500a098190feeba5', - '500a098180feeba5' - ], - 'initiator_port_wwn_list': [ - '20130080E5322230' - ] - }, - 'fabricA': { - 'target_port_wwn_list': [ - '500a098290feeba5', - '500a098280feeba5' - ], - 'initiator_port_wwn_list': [ - '20230080E5322230' - ] - } -} - -HOST = { - 'isSAControlled': False, - 'confirmLUNMappingCreation': False, - 'label': 'stlrx300s7-55', - 'isLargeBlockFormatHost': False, - 'clusterRef': '8500000060080E500023C7340036035F515B78FC', - 'protectionInformationCapableAccessMethod': False, - 'ports': [], - 'hostRef': '8400000060080E500023C73400300381515BFBA3', - 'hostTypeIndex': 6, - 'hostSidePorts': [{ - 'label': 'NewStore', - 'type': 'iscsi', - 'address': INITIATOR_NAME}] -} -HOST_2 = { - 'isSAControlled': False, - 'confirmLUNMappingCreation': False, - 'label': 'stlrx300s7-55', - 'isLargeBlockFormatHost': False, - 'clusterRef': utils.NULL_REF, - 'protectionInformationCapableAccessMethod': False, - 'ports': [], - 'hostRef': '8400000060080E500023C73400300381515BFBA5', - 'hostTypeIndex': 6, - 'hostSidePorts': [{ - 'label': 'NewStore', 'type': 'iscsi', - 'address': INITIATOR_NAME_2}] -} -# HOST_3 has all lun_ids in use. -HOST_3 = { - 'isSAControlled': False, - 'confirmLUNMappingCreation': False, - 'label': 'stlrx300s7-55', - 'isLargeBlockFormatHost': False, - 'clusterRef': '8500000060080E500023C73400360351515B78FC', - 'protectionInformationCapableAccessMethod': False, - 'ports': [], - 'hostRef': '8400000060080E501023C73400800381515BFBA5', - 'hostTypeIndex': 6, - 'hostSidePorts': [{ - 'label': 'NewStore', 'type': 'iscsi', - 'address': INITIATOR_NAME_3}], -} - - -VOLUME_MAPPING = { - 'lunMappingRef': '8800000000000000000000000000000000000000', - 'lun': 1, - 'ssid': 16384, - 'perms': 15, - 'volumeRef': VOLUME['volumeRef'], - 'type': 'all', - 'mapRef': HOST['hostRef'] -} -# VOLUME_MAPPING_3 corresponding to HOST_3 has all lun_ids in use. -VOLUME_MAPPING_3 = { - 'lunMappingRef': '8800000000000000000000000000000000000000', - 'lun': range(255), - 'ssid': 16384, - 'perms': 15, - 'volumeRef': VOLUME['volumeRef'], - 'type': 'all', - 'mapRef': HOST_3['hostRef'], -} - -VOLUME_MAPPING_TO_MULTIATTACH_GROUP = copy.deepcopy(VOLUME_MAPPING) -VOLUME_MAPPING_TO_MULTIATTACH_GROUP.update( - {'mapRef': MULTIATTACH_HOST_GROUP['clusterRef']} -) - -STORAGE_SYSTEM = { - 'chassisSerialNumber': 1, - 'fwVersion': '08.10.15.00', - 'freePoolSpace': 11142431623168, - 'driveCount': 24, - 'hostSparesUsed': 0, 'id': - '1fa6efb5-f07b-4de4-9f0e-52e5f7ff5d1b', - 'hotSpareSizeAsString': '0', 'wwn': - '60080E500023C73400000000515AF323', - 'passwordStatus': 'valid', - 'parameters': { - 'minVolSize': 1048576, 'maxSnapshotsPerBase': 16, - 'maxDrives': 192, - 'maxVolumes': 512, - 'maxVolumesPerGroup': 256, - 'maxMirrors': 0, - 'maxMappingsPerVolume': 1, - 'maxMappableLuns': 256, - 'maxVolCopys': 511, - 'maxSnapshots': 256 - }, 'hotSpareCount': 0, - 'hostSpareCountInStandby': 0, - 'status': 'needsattn', - 'trayCount': 1, - 'usedPoolSpaceAsString': '5313000380416', - 'ip2': '10.63.165.216', - 'ip1': '10.63.165.215', - 'freePoolSpaceAsString': '11142431623168', - 'types': 'SAS', - 'name': 'stle2600-7_8', - 'hotSpareSize': 0, - 'usedPoolSpace': 5313000380416, - 'driveTypes': ['sas'], - 'unconfiguredSpaceByDriveType': {}, - 'unconfiguredSpaceAsStrings': '0', - 'model': '2650', - 'unconfiguredSpace': 0 -} - -SNAPSHOT_GROUP = { - 'id': '3300000060080E500023C7340000098D5294AC9A', - 'status': 'optimal', - 'autoDeleteLimit': 0, - 'maxRepositoryCapacity': '-65536', - 'rollbackStatus': 'none', - 'unusableRepositoryCapacity': '0', - 'pitGroupRef': '3300000060080E500023C7340000098D5294AC9A', - 'clusterSize': 65536, - 'label': 'C6JICISVHNG2TFZX4XB5ZWL7F', - 'maxBaseCapacity': '476187142128128', - 'repositoryVolume': '3600000060080E500023BB3400001FA952CEF12C', - 'fullWarnThreshold': 99, - 'repFullPolicy': 'purgepit', - 'action': 'none', - 'rollbackPriority': 'medium', - 'creationPendingStatus': 'none', - 'consistencyGroupRef': '0000000000000000000000000000000000000000', - 'volumeHandle': 49153, - 'consistencyGroup': False, - 'baseVolume': '0200000060080E500023C734000009825294A534', - 'snapshotCount': 32 -} - -SNAPSHOT_IMAGE = { - 'id': fake.SNAPSHOT_ID, - 'baseVol': '0200000060080E500023C734000009825294A534', - 'status': 'optimal', - 'pitCapacity': '2147483648', - 'pitTimestamp': '1389315375', - 'pitGroupRef': '3300000060080E500023C7340000098D5294AC9A', - 'creationMethod': 'user', - 'repositoryCapacityUtilization': '2818048', - 'activeCOW': True, - 'isRollbackSource': False, - 'pitRef': '3400000060080E500023BB3400631F335294A5A8', - 'pitSequenceNumber': '19', - 'consistencyGroupId': '0000000000000000000000000000000000000000', -} - -SNAPSHOT_VOLUME = { - 'id': '35000000600A0980006077F80000F8BF566581AA', - 'viewRef': '35000000600A0980006077F80000F8BF566581AA', - 'worldWideName': '600A0980006077F80000F8BF566581AA', - 'baseVol': '02000000600A0980006077F80000F89B56657E26', - 'basePIT': '0000000000000000000000000000000000000000', - 'boundToPIT': False, - 'accessMode': 'readOnly', - 'label': 'UZJ45SLUKNGWRF3QZHBTOG4C4E_DEL', - 'status': 'stopped', - 'currentManager': '070000000000000000000001', - 'preferredManager': '070000000000000000000001', - 'repositoryVolume': '0000000000000000000000000000000000000000', - 'fullWarnThreshold': 0, - 'viewTime': '1449453419', - 'viewSequenceNumber': '2104', - 'volumeHandle': 16510, - 'clusterSize': 0, - 'maxRepositoryCapacity': '0', - 'unusableRepositoryCapacity': '0', - 'membership': { - 'viewType': 'individual', - 'cgViewRef': None - }, - 'mgmtClientAttribute': 0, - 'offline': False, - 'volumeFull': False, - 'repositoryCapacity': '0', - 'baseVolumeCapacity': '1073741824', - 'totalSizeInBytes': '0', - 'consistencyGroupId': None, - 'volumeCopyTarget': False, - 'cloneCopy': False, - 'volumeCopySource': False, - 'pitBaseVolume': False, - 'asyncMirrorTarget': False, - 'asyncMirrorSource': False, - 'protectionType': 'type0Protection', - 'remoteMirrorSource': False, - 'remoteMirrorTarget': False, - 'wwn': '600A0980006077F80000F8BF566581AA', - 'listOfMappings': [], - 'mapped': False, - 'currentControllerId': '070000000000000000000001', - 'preferredControllerId': '070000000000000000000001', - 'onlineVolumeCopy': False, - 'objectType': 'pitView', - 'name': 'UZJ45SLUKNGWRF3QZHBTOG4C4E', -} - -FAKE_BACKEND_STORE = { - 'key': 'cinder-snapshots', - 'value': '{"3300000060080E50003416400000E90D56B047E5":"2"}' -} - -HARDWARE_INVENTORY_SINGLE_CONTROLLER = { - 'controllers': [ - { - 'modelName': '2752', - 'serialNumber': '021436001321' - } - ] -} - -HARDWARE_INVENTORY = { - 'controllers': [ - { - 'modelName': '2752', - 'serialNumber': '021436000943' - }, - { - 'modelName': '2752', - 'serialNumber': '021436001321' - } - ], - 'iscsiPorts': [ - { - 'controllerId': - '070000000000000000000002', - 'ipv4Enabled': True, - 'ipv4Data': { - 'ipv4Address': '0.0.0.0', - 'ipv4AddressConfigMethod': - 'configStatic', - 'ipv4VlanId': { - 'isEnabled': False, - 'value': 0 - }, - 'ipv4AddressData': { - 'ipv4Address': '172.20.123.66', - 'ipv4SubnetMask': '255.255.255.0', - 'configState': 'configured', - 'ipv4GatewayAddress': '0.0.0.0' - } - }, - 'tcpListenPort': 3260, - 'interfaceRef': '2202040000000000000000000000000000000000', - 'iqn': 'iqn.1992-01.com.lsi:2365.60080e500023c73400000000515af323' - } - ], - 'fibrePorts': [ - { - "channel": 1, - "loopID": 126, - "speed": 800, - "hardAddress": 6, - "nodeName": "20020080E5322230", - "portName": "20130080E5322230", - "portId": "011700", - "topology": "fabric", - "part": "PM8032 ", - "revision": 8, - "chanMiswire": False, - "esmMiswire": False, - "linkStatus": "up", - "isDegraded": False, - "speedControl": "auto", - "maxSpeed": 800, - "speedNegError": False, - "reserved1": "000000000000000000000000", - "reserved2": "", - "ddsChannelState": 0, - "ddsStateReason": 0, - "ddsStateWho": 0, - "isLocal": True, - "channelPorts": [], - "currentInterfaceSpeed": "speed8gig", - "maximumInterfaceSpeed": "speed8gig", - "interfaceRef": "2202020000000000000000000000000000000000", - "physicalLocation": { - "trayRef": "0000000000000000000000000000000000000000", - "slot": 0, - "locationParent": { - "refType": "generic", - "controllerRef": None, - "symbolRef": "0000000000000000000000000000000000000000", - "typedReference": None - }, - "locationPosition": 0 - }, - "isTrunkCapable": False, - "trunkMiswire": False, - "protectionInformationCapable": True, - "controllerId": "070000000000000000000002", - "interfaceId": "2202020000000000000000000000000000000000", - "addressId": "20130080E5322230", - "niceAddressId": "20:13:00:80:E5:32:22:30" - }, - { - "channel": 2, - "loopID": 126, - "speed": 800, - "hardAddress": 7, - "nodeName": "20020080E5322230", - "portName": "20230080E5322230", - "portId": "011700", - "topology": "fabric", - "part": "PM8032 ", - "revision": 8, - "chanMiswire": False, - "esmMiswire": False, - "linkStatus": "up", - "isDegraded": False, - "speedControl": "auto", - "maxSpeed": 800, - "speedNegError": False, - "reserved1": "000000000000000000000000", - "reserved2": "", - "ddsChannelState": 0, - "ddsStateReason": 0, - "ddsStateWho": 0, - "isLocal": True, - "channelPorts": [], - "currentInterfaceSpeed": "speed8gig", - "maximumInterfaceSpeed": "speed8gig", - "interfaceRef": "2202030000000000000000000000000000000000", - "physicalLocation": { - "trayRef": "0000000000000000000000000000000000000000", - "slot": 0, - "locationParent": { - "refType": "generic", - "controllerRef": None, - "symbolRef": "0000000000000000000000000000000000000000", - "typedReference": None - }, - "locationPosition": 0 - }, - "isTrunkCapable": False, - "trunkMiswire": False, - "protectionInformationCapable": True, - "controllerId": "070000000000000000000002", - "interfaceId": "2202030000000000000000000000000000000000", - "addressId": "20230080E5322230", - "niceAddressId": "20:23:00:80:E5:32:22:30" - }, - ] -} - -FAKE_POOL_ACTION_PROGRESS = [ - { - "volumeRef": "0200000060080E50002998A00000945355C37C19", - "progressPercentage": 55, - "estimatedTimeToCompletion": 1, - "currentAction": "initializing" - }, - { - "volumeRef": "0200000060080E50002998A00000945355C37C18", - "progressPercentage": 0, - "estimatedTimeToCompletion": 0, - "currentAction": "progressDve" - }, -] - -FAKE_CHAP_SECRET = 'password123' -FAKE_RESOURCE_URL = '/devmgr/v2/devmgr/utils/about' -FAKE_APP_VERSION = '2015.2|2015.2.dev59|vendor|Linux-3.13.0-24-generic' -FAKE_BACKEND = 'eseriesiSCSI' -FAKE_CINDER_HOST = 'ubuntu-1404' -FAKE_SERIAL_NUMBERS = ['021436000943', '021436001321'] -FAKE_SERIAL_NUMBER = ['021436001321'] -FAKE_DEFAULT_SERIAL_NUMBER = ['unknown', 'unknown'] -FAKE_DEFAULT_MODEL = 'unknown' -FAKE_ABOUT_RESPONSE = { - 'runningAsProxy': True, - 'version': '01.53.9010.0005', - 'systemId': 'a89355ab-692c-4d4a-9383-e249095c3c0', -} - -FAKE_TARGET_IQN = 'iqn.1992-01.com.lsi:2365.60080e500023c73400000000515af323' - -FAKE_CHAP_USERNAME = 'eserieschapuser' - -FAKE_CHAP_PARAMETERS = { - 'ChapAuthentication': True, - 'iqn': FAKE_TARGET_IQN, - 'chapSecret': FAKE_CHAP_SECRET, - 'authMethod': 'CHAP', -} - -FAKE_CLIENT_CHAP_PARAMETERS = ( - FAKE_TARGET_IQN, - FAKE_CHAP_USERNAME, - FAKE_CHAP_SECRET, -) - -FAKE_TARGET_DICT = { - 'data': { - 'auth_method': 'CHAP', - 'auth_password': FAKE_CHAP_SECRET, - 'auth_username': FAKE_CHAP_USERNAME, - 'discovery_auth_method': 'CHAP', - 'discovery_auth_password': FAKE_CHAP_SECRET, - 'discovery_auth_username': FAKE_CHAP_USERNAME, - 'target_discovered': False, - 'target_iqn': FAKE_TARGET_IQN, - 'target_lun': 1, - 'target_portal': '172.20.123.66:3260', - 'volume_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', - }, - 'driver_volume_type': 'iscsi', -} - -FAKE_CHAP_POST_DATA = { - 'enableChapAuthentication': True, - 'alias': FAKE_CHAP_USERNAME, - 'iqn': FAKE_TARGET_IQN, - 'chapSecret': FAKE_CHAP_SECRET, - 'authMethod': 'CHAP', -} - - -FAKE_CONTROLLERS = [ - {'serialNumber': FAKE_SERIAL_NUMBERS[0], 'modelName': '2752'}, - {'serialNumber': FAKE_SERIAL_NUMBERS[1], 'modelName': '2752'}] - -FAKE_SINGLE_CONTROLLER = [{'serialNumber': FAKE_SERIAL_NUMBERS[1]}] - -FAKE_KEY = ('openstack-%s-%s-%s' % (FAKE_CINDER_HOST, FAKE_SERIAL_NUMBERS[0], - FAKE_SERIAL_NUMBERS[1])) - -FAKE_ASUP_DATA = { - 'category': 'provisioning', - 'app-version': FAKE_APP_VERSION, - 'event-source': 'Cinder driver NetApp_iSCSI_ESeries', - 'event-description': 'OpenStack Cinder connected to E-Series proxy', - 'system-version': '08.10.15.00', - 'computer-name': FAKE_CINDER_HOST, - 'model': FAKE_CONTROLLERS[0]['modelName'], - 'controller2-serial': FAKE_CONTROLLERS[1]['serialNumber'], - 'controller1-serial': FAKE_CONTROLLERS[0]['serialNumber'], - 'chassis-serial-number': FAKE_SERIAL_NUMBER[0], - 'operating-mode': 'proxy', -} - -GET_ASUP_RETURN = { - 'model': FAKE_CONTROLLERS[0]['modelName'], - 'serial_numbers': FAKE_SERIAL_NUMBERS, - 'firmware_version': FAKE_ASUP_DATA['system-version'], - 'chassis_sn': FAKE_ASUP_DATA['chassis-serial-number'], -} - -FAKE_POST_INVOKE_DATA = ('POST', '/key-values/%s' % FAKE_KEY, - json.dumps(FAKE_ASUP_DATA)) - -VOLUME_COPY_JOB = { - "status": "complete", - "cloneCopy": True, - "pgRef": "3300000060080E500023C73400000ACA52D29454", - "volcopyHandle": 49160, - "idleTargetWriteProt": True, - "copyPriority": "priority2", - "volcopyRef": "1800000060080E500023C73400000ACF52D29466", - "worldWideName": "60080E500023C73400000ACF52D29466", - "copyCompleteTime": "0", - "sourceVolume": "3500000060080E500023C73400000ACE52D29462", - "currentManager": "070000000000000000000002", - "copyStartTime": "1389551671", - "reserved1": "00000000", - "targetVolume": "0200000060080E500023C73400000A8C52D10675", -} - -FAKE_ENDPOINT_HTTP = 'http://host:80/endpoint' - -FAKE_ENDPOINT_HTTPS = 'https://host:8443/endpoint' - -FAKE_INVOC_MSG = 'success' - -FAKE_CLIENT_PARAMS = { - 'scheme': 'http', - 'host': '127.0.0.1', - 'port': 8080, - 'service_path': '/devmgr/vn', - 'username': 'rw', - 'password': 'rw', -} - -FAKE_CONSISTENCY_GROUP = { - 'cgRef': '2A000000600A0980006077F8008702F45480F41A', - 'label': '5BO5GPO4PFGRPMQWEXGTILSAUI', - 'repFullPolicy': 'failbasewrites', - 'fullWarnThreshold': 75, - 'autoDeleteLimit': 0, - 'rollbackPriority': 'medium', - 'uniqueSequenceNumber': [8940, 8941, 8942], - 'creationPendingStatus': 'none', - 'name': '5BO5GPO4PFGRPMQWEXGTILSAUI', - 'id': '2A000000600A0980006077F8008702F45480F41A' -} - -FAKE_CONSISTENCY_GROUP_MEMBER = { - 'consistencyGroupId': '2A000000600A0980006077F8008702F45480F41A', - 'volumeId': '02000000600A0980006077F8000002F55480F421', - 'volumeWwn': '600A0980006077F8000002F55480F421', - 'baseVolumeName': 'I5BHHNILUJGZHEUD4S36GCOQYA', - 'clusterSize': 65536, - 'totalRepositoryVolumes': 1, - 'totalRepositoryCapacity': '4294967296', - 'usedRepositoryCapacity': '5636096', - 'fullWarnThreshold': 75, - 'totalSnapshotImages': 3, - 'totalSnapshotVolumes': 2, - 'autoDeleteSnapshots': False, - 'autoDeleteLimit': 0, - 'pitGroupId': '33000000600A0980006077F8000002F85480F435', - 'repositoryVolume': '36000000600A0980006077F8000002F75480F435' -} -FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME = { - 'id': '2C00000060080E500034194F002C96A256BD50F9', - 'name': '6TRZHKDG75DVLBC2JU5J647RME', - 'cgViewRef': '2C00000060080E500034194F002C96A256BD50F9', - 'groupRef': '2A00000060080E500034194F0087969856BD2D67', - 'label': '6TRZHKDG75DVLBC2JU5J647RME', - 'viewTime': '1455221060', - 'viewSequenceNumber': '10', -} - - -def list_snapshot_groups(numGroups): - snapshots = [] - for n in range(0, numGroups): - s = copy.deepcopy(SNAPSHOT_GROUP) - s['label'] = s['label'][:-1] + str(n) - snapshots.append(s) - return snapshots - - -def create_configuration_eseries(): - config = conf.Configuration(None) - config.append_config_values(na_opts.netapp_connection_opts) - config.append_config_values(na_opts.netapp_transport_opts) - config.append_config_values(na_opts.netapp_basicauth_opts) - config.append_config_values(na_opts.netapp_provisioning_opts) - config.append_config_values(na_opts.netapp_eseries_opts) - config.netapp_storage_protocol = 'iscsi' - config.netapp_login = 'rw' - config.netapp_password = 'rw' - config.netapp_server_hostname = '127.0.0.1' - config.netapp_transport_type = 'http' - config.netapp_server_port = '8080' - config.netapp_storage_pools = 'DDP' - config.netapp_storage_family = 'eseries' - config.netapp_sa_password = 'saPass' - config.netapp_controller_ips = '10.11.12.13,10.11.12.14' - config.netapp_webservice_path = '/devmgr/v2' - config.netapp_enable_multiattach = False - config.use_chap_auth = False - return config - - -def deepcopy_return_value_method_decorator(fn): - """Returns a deepcopy of the returned value of the wrapped function.""" - def decorator(*args, **kwargs): - return copy.deepcopy(fn(*args, **kwargs)) - - return decorator - - -def deepcopy_return_value_class_decorator(cls): - """Wraps 'non-protected' methods of a class with decorator. - - Wraps all 'non-protected' methods of a class with the - deepcopy_return_value_method_decorator decorator. - """ - class NewClass(cls): - def __getattribute__(self, attr_name): - obj = super(NewClass, self).__getattribute__(attr_name) - if (hasattr(obj, '__call__') and not attr_name.startswith('_') - and not isinstance(obj, mock.Mock)): - return deepcopy_return_value_method_decorator(obj) - return obj - - return NewClass - - -@deepcopy_return_value_class_decorator -class FakeEseriesClient(object): - features = na_utils.Features() - - def __init__(self, *args, **kwargs): - self.features.add_feature('AUTOSUPPORT') - self.features.add_feature('SSC_API_V2') - self.features.add_feature('REST_1_3_RELEASE') - self.features.add_feature('REST_1_4_RELEASE') - - def list_storage_pools(self): - return STORAGE_POOLS - - def register_storage_system(self, *args, **kwargs): - return { - 'freePoolSpace': '17055871480319', - 'driveCount': 24, - 'wwn': '60080E500023C73400000000515AF323', - 'id': '1', - 'hotSpareSizeAsString': '0', - 'hostSparesUsed': 0, - 'types': '', - 'hostSpareCountInStandby': 0, - 'status': 'optimal', - 'trayCount': 1, - 'usedPoolSpaceAsString': '37452115456', - 'ip2': '10.63.165.216', - 'ip1': '10.63.165.215', - 'freePoolSpaceAsString': '17055871480319', - 'hotSpareCount': 0, - 'hotSpareSize': '0', - 'name': 'stle2600-7_8', - 'usedPoolSpace': '37452115456', - 'driveTypes': ['sas'], - 'unconfiguredSpaceByDriveType': {}, - 'unconfiguredSpaceAsStrings': '0', - 'model': '2650', - 'unconfiguredSpace': '0' - } - - def list_volume(self, volume_id): - return VOLUME - - def list_volumes(self): - return [VOLUME] - - def delete_volume(self, vol): - pass - - def create_host_group(self, name): - return MULTIATTACH_HOST_GROUP - - def get_host_group(self, ref): - return MULTIATTACH_HOST_GROUP - - def list_host_groups(self): - return [MULTIATTACH_HOST_GROUP, FOREIGN_HOST_GROUP] - - def get_host_group_by_name(self, name, *args, **kwargs): - host_groups = self.list_host_groups() - return [host_group for host_group in host_groups - if host_group['label'] == name][0] - - def set_host_group_for_host(self, *args, **kwargs): - pass - - def create_host_with_ports(self, *args, **kwargs): - return HOST - - def list_hosts(self): - return [HOST, HOST_2] - - def get_host(self, *args, **kwargs): - return HOST - - def create_volume(self, *args, **kwargs): - return VOLUME - - def create_volume_mapping(self, *args, **kwargs): - return VOLUME_MAPPING - - def get_volume_mappings(self): - return [VOLUME_MAPPING] - - def get_volume_mappings_for_volume(self, volume): - return [VOLUME_MAPPING] - - def get_volume_mappings_for_host(self, host_ref): - return [VOLUME_MAPPING] - - def get_volume_mappings_for_host_group(self, hg_ref): - return [VOLUME_MAPPING] - - def delete_volume_mapping(self): - return - - def move_volume_mapping_via_symbol(self, map_ref, to_ref, lun_id): - return {'lun': lun_id} - - def list_storage_system(self): - return STORAGE_SYSTEM - - def list_storage_systems(self): - return [STORAGE_SYSTEM] - - def list_snapshot_groups(self): - return [SNAPSHOT_GROUP] - - def list_snapshot_images(self): - return [SNAPSHOT_IMAGE] - - def list_snapshot_image(self, *args, **kwargs): - return SNAPSHOT_IMAGE - - def create_cg_snapshot_view(self, *args, **kwargs): - return SNAPSHOT_VOLUME - - def list_host_types(self): - return [ - { - 'name': 'FactoryDefault', - 'index': 0, - 'code': 'FactoryDefault', - }, - { - 'name': 'Windows 2000/Server 2003/Server 2008 Non-Clustered', - 'index': 1, - 'code': 'W2KNETNCL', - }, - { - 'name': 'Solaris', - 'index': 2, - 'code': 'SOL', - }, - { - 'name': 'ONTAP_RDAC', - 'index': 4, - 'code': 'ONTAP_RDAC', - }, - { - 'name': 'AVT_4M', - 'index': 5, - 'code': 'AVT_4M', - }, - { - 'name': 'Linux', - 'index': 6, - 'code': 'LNX', - }, - { - 'name': 'LnxALUA', - 'index': 7, - 'code': 'LnxALUA', - }, - { - 'name': 'Windows 2000/Server 2003/Server 2008 Clustered', - 'index': 8, - 'code': 'W2KNETCL', - }, - { - 'name': 'AIX MPIO', - 'index': 9, - 'code': 'AIX MPIO', - }, - { - 'name': 'VmwTPGSALUA', - 'index': 10, - 'code': 'VmwTPGSALUA', - }, - { - 'name': 'HP-UX TPGS', - 'index': 15, - 'code': 'HPXTPGS', - }, - { - 'name': 'SolTPGSALUA', - 'index': 17, - 'code': 'SolTPGSALUA', - }, - { - 'name': 'SVC', - 'index': 18, - 'code': 'SVC', - }, - { - 'name': 'MacTPGSALUA', - 'index': 22, - 'code': 'MacTPGSALUA', - }, - { - 'name': 'WinTPGSALUA', - 'index': 23, - 'code': 'WinTPGSALUA', - }, - { - 'name': 'LnxTPGSALUA', - 'index': 24, - 'code': 'LnxTPGSALUA', - }, - { - 'name': 'LnxTPGSALUA_PM', - 'index': 25, - 'code': 'LnxTPGSALUA_PM', - }, - { - 'name': 'ONTAP_ALUA', - 'index': 26, - 'code': 'ONTAP_ALUA', - }, - { - 'name': 'LnxTPGSALUA_SF', - 'index': 27, - 'code': 'LnxTPGSALUA_SF', - } - ] - - def update_host_type(self, *args, **kwargs): - pass - - def list_hardware_inventory(self): - return HARDWARE_INVENTORY - - def get_eseries_api_info(self, verify=False): - return 'Proxy', '1.53.9010.0005' - - def set_counter(self, key, value): - pass - - def add_autosupport_data(self, *args): - pass - - def set_chap_authentication(self, *args, **kwargs): - return FAKE_CHAP_PARAMETERS - - def get_serial_numbers(self): - return FAKE_ASUP_DATA.get('controller1-serial'), FAKE_ASUP_DATA.get( - 'controller2-serial') - - def get_model_name(self): - pass - - def api_operating_mode(self): - pass - - def get_firmware_version(self): - return FAKE_ASUP_DATA['system-version'] - - def create_volume_copy_job(self, *args, **kwargs): - return VOLUME_COPY_JOB - - def list_vol_copy_job(self, *args, **kwargs): - return VOLUME_COPY_JOB - - def delete_vol_copy_job(self, *args, **kwargs): - pass - - def create_snapshot_image(self, *args, **kwargs): - return SNAPSHOT_IMAGE - - def create_snapshot_volume(self, *args, **kwargs): - return SNAPSHOT_VOLUME - - def list_snapshot_volumes(self, *args, **kwargs): - return [SNAPSHOT_VOLUME] - - def list_snapshot_volume(self, *args, **kwargs): - return SNAPSHOT_IMAGE - - def create_snapshot_group(self, *args, **kwargs): - return SNAPSHOT_GROUP - - def list_snapshot_group(self, *args, **kwargs): - return SNAPSHOT_GROUP - - def delete_snapshot_volume(self, *args, **kwargs): - pass - - def list_target_wwpns(self, *args, **kwargs): - return [WWPN_2] - - def update_stored_system_password(self, *args, **kwargs): - pass - - def update_snapshot_volume(self, *args, **kwargs): - return SNAPSHOT_VOLUME - - def delete_snapshot_image(self, *args, **kwargs): - pass - - def delete_snapshot_group(self, *args, **kwargs): - pass - - def restart_snapshot_volume(self, *args, **kwargs): - pass - - def create_consistency_group(self, *args, **kwargs): - return FAKE_CONSISTENCY_GROUP - - def delete_consistency_group(self, *args, **kwargs): - pass - - def list_consistency_groups(self, *args, **kwargs): - return [FAKE_CONSISTENCY_GROUP] - - def remove_consistency_group_member(self, *args, **kwargs): - pass - - def add_consistency_group_member(self, *args, **kwargs): - pass - - def list_backend_store(self, key): - return {} - - def save_backend_store(self, key, val): - pass - - def create_consistency_group_snapshot(self, *args, **kwargs): - return [SNAPSHOT_IMAGE] - - def get_consistency_group_snapshots(self, *args, **kwargs): - return [SNAPSHOT_IMAGE] - - def delete_consistency_group_snapshot(self, *args, **kwargs): - pass diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py deleted file mode 100644 index 30c007c4efc..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py +++ /dev/null @@ -1,1216 +0,0 @@ -# Copyright (c) 2014 Alex Meade -# Copyright (c) 2015 Yogesh Kshirsagar -# Copyright (c) 2015 Michael Price -# 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. - -import copy -import json - -import ddt -import mock -from six.moves import http_client - -from cinder import exception -from cinder import test -from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \ - eseries_fake -from cinder.volume.drivers.netapp.eseries import client -from cinder.volume.drivers.netapp.eseries import exception as es_exception -from cinder.volume.drivers.netapp import utils as na_utils - - -@ddt.ddt -class NetAppEseriesClientDriverTestCase(test.TestCase): - """Test case for NetApp e-series client.""" - - def setUp(self): - super(NetAppEseriesClientDriverTestCase, self).setUp() - self.mock_log = mock.Mock() - self.mock_object(client, 'LOG', self.mock_log) - self.fake_password = 'mysecret' - - self.my_client = client.RestClient('http', 'host', '80', '/test', - 'user', self.fake_password, - system_id='fake_sys_id') - self.my_client._endpoint = eseries_fake.FAKE_ENDPOINT_HTTP - - fake_response = mock.Mock() - fake_response.status_code = http_client.OK - self.my_client.invoke_service = mock.Mock(return_value=fake_response) - self.my_client.api_version = '01.52.9000.1' - - @ddt.data(http_client.OK, http_client.CREATED, - http_client.NON_AUTHORITATIVE_INFORMATION, - http_client.NO_CONTENT) - def test_eval_response_success(self, status_code): - fake_resp = mock.Mock() - fake_resp.status_code = status_code - - self.assertIsNone(self.my_client._eval_response(fake_resp)) - - @ddt.data(300, 400, 404, 500) - def test_eval_response_failure(self, status_code): - fake_resp = mock.Mock() - fake_resp.status_code = status_code - expected_msg = "Response error code - %s." % status_code - - with self.assertRaisesRegex(es_exception.WebServiceException, - expected_msg) as exc: - self.my_client._eval_response(fake_resp) - - self.assertEqual(status_code, exc.status_code) - - @ddt.data(('30', 'storage array password.*?incorrect'), - ('authFailPassword', 'storage array password.*?incorrect'), - ('unknown', None)) - @ddt.unpack - def test_eval_response_422(self, ret_code, exc_regex): - status_code = http_client.UNPROCESSABLE_ENTITY - fake_resp = mock.Mock() - fake_resp.text = "fakeError" - fake_resp.json = mock.Mock(return_value={'retcode': ret_code}) - fake_resp.status_code = status_code - exc_regex = exc_regex if exc_regex is not None else fake_resp.text - - with self.assertRaisesRegex(es_exception.WebServiceException, - exc_regex) as exc: - self.my_client._eval_response(fake_resp) - self.assertEqual(status_code, exc.status_code) - - def test_eval_response_424(self): - status_code = http_client.FAILED_DEPENDENCY - fake_resp = mock.Mock() - fake_resp.status_code = status_code - fake_resp.text = "Fake Error Message" - - with self.assertRaisesRegex(es_exception.WebServiceException, - "The storage-system is offline") as exc: - self.my_client._eval_response(fake_resp) - - self.assertEqual(status_code, exc.status_code) - - def test_register_storage_system_does_not_log_password(self): - self.my_client._eval_response = mock.Mock() - self.my_client.register_storage_system([], password=self.fake_password) - for call in self.mock_log.debug.mock_calls: - __, args, __ = call - self.assertNotIn(self.fake_password, args[0]) - - def test_update_stored_system_password_does_not_log_password(self): - self.my_client._eval_response = mock.Mock() - self.my_client.update_stored_system_password( - password=self.fake_password) - for call in self.mock_log.debug.mock_calls: - __, args, __ = call - self.assertNotIn(self.fake_password, args[0]) - - def test_list_target_wwpns(self): - fake_hardware_inventory = copy.deepcopy( - eseries_fake.HARDWARE_INVENTORY) - - mock_hardware_inventory = mock.Mock( - return_value=fake_hardware_inventory) - self.mock_object(self.my_client, 'list_hardware_inventory', - mock_hardware_inventory) - expected_wwpns = [eseries_fake.WWPN, eseries_fake.WWPN_2] - - actual_wwpns = self.my_client.list_target_wwpns() - - self.assertEqual(expected_wwpns, actual_wwpns) - - def test_list_target_wwpns_single_wwpn(self): - fake_hardware_inventory = copy.deepcopy( - eseries_fake.HARDWARE_INVENTORY) - - fake_hardware_inventory['fibrePorts'] = [ - fake_hardware_inventory['fibrePorts'][0] - ] - mock_hardware_inventory = mock.Mock( - return_value=fake_hardware_inventory) - self.mock_object(self.my_client, 'list_hardware_inventory', - mock_hardware_inventory) - expected_wwpns = [eseries_fake.WWPN] - - actual_wwpns = self.my_client.list_target_wwpns() - - self.assertEqual(expected_wwpns, actual_wwpns) - - def test_list_target_wwpns_no_wwpn(self): - fake_hardware_inventory = copy.deepcopy( - eseries_fake.HARDWARE_INVENTORY) - - fake_hardware_inventory['fibrePorts'] = [] - mock_hardware_inventory = mock.Mock( - return_value=fake_hardware_inventory) - self.mock_object(self.my_client, 'list_hardware_inventory', - mock_hardware_inventory) - expected_wwpns = [] - - actual_wwpns = self.my_client.list_target_wwpns() - - self.assertEqual(expected_wwpns, actual_wwpns) - - def test_get_host_group_by_name(self): - groups = copy.deepcopy(eseries_fake.HOST_GROUPS) - group = groups[0] - self.mock_object(self.my_client, 'list_host_groups', - return_value=groups) - - result = self.my_client.get_host_group_by_name(group['label']) - - self.assertEqual(group, result) - - def test_move_volume_mapping_via_symbol(self): - invoke = self.mock_object(self.my_client, '_invoke', return_value='ok') - host_ref = 'host' - cluster_ref = 'cluster' - lun_id = 10 - expected_data = {'lunMappingRef': host_ref, 'lun': lun_id, - 'mapRef': cluster_ref} - - result = self.my_client.move_volume_mapping_via_symbol(host_ref, - cluster_ref, - lun_id) - - invoke.assert_called_once_with('POST', '/storage-systems/{system-id}/' - 'symbol/moveLUNMapping', - expected_data) - - self.assertEqual({'lun': lun_id}, result) - - def test_move_volume_mapping_via_symbol_fail(self): - self.mock_object(self.my_client, '_invoke', return_value='failure') - - self.assertRaises( - exception.NetAppDriverException, - self.my_client.move_volume_mapping_via_symbol, '1', '2', 10) - - def test_create_host_from_ports_fc(self): - label = 'fake_host' - host_type = 'linux' - port_type = 'fc' - port_ids = [eseries_fake.WWPN, eseries_fake.WWPN_2] - expected_ports = [ - {'type': port_type, 'port': eseries_fake.WWPN, 'label': mock.ANY}, - {'type': port_type, 'port': eseries_fake.WWPN_2, - 'label': mock.ANY}] - mock_create_host = self.mock_object(self.my_client, 'create_host') - - self.my_client.create_host_with_ports(label, host_type, port_ids, - port_type) - - mock_create_host.assert_called_once_with(label, host_type, - expected_ports, None) - - def test_host_from_ports_with_no_ports_provided_fc(self): - label = 'fake_host' - host_type = 'linux' - port_type = 'fc' - port_ids = [] - expected_ports = [] - mock_create_host = self.mock_object(self.my_client, 'create_host') - - self.my_client.create_host_with_ports(label, host_type, port_ids, - port_type) - - mock_create_host.assert_called_once_with(label, host_type, - expected_ports, None) - - def test_create_host_from_ports_iscsi(self): - label = 'fake_host' - host_type = 'linux' - port_type = 'iscsi' - port_ids = [eseries_fake.INITIATOR_NAME, - eseries_fake.INITIATOR_NAME_2] - expected_ports = [ - {'type': port_type, 'port': eseries_fake.INITIATOR_NAME, - 'label': mock.ANY}, - {'type': port_type, 'port': eseries_fake.INITIATOR_NAME_2, - 'label': mock.ANY}] - mock_create_host = self.mock_object(self.my_client, 'create_host') - - self.my_client.create_host_with_ports(label, host_type, port_ids, - port_type) - - mock_create_host.assert_called_once_with(label, host_type, - expected_ports, None) - - def test_get_volume_mappings_for_volume(self): - volume_mapping_1 = copy.deepcopy(eseries_fake.VOLUME_MAPPING) - volume_mapping_2 = copy.deepcopy(eseries_fake.VOLUME_MAPPING) - volume_mapping_2['volumeRef'] = '2' - self.mock_object(self.my_client, 'get_volume_mappings', - return_value=[volume_mapping_1, volume_mapping_2]) - - mappings = self.my_client.get_volume_mappings_for_volume( - eseries_fake.VOLUME) - - self.assertEqual([volume_mapping_1], mappings) - - def test_get_volume_mappings_for_host(self): - volume_mapping_1 = copy.deepcopy( - eseries_fake.VOLUME_MAPPING) - volume_mapping_2 = copy.deepcopy(eseries_fake.VOLUME_MAPPING) - volume_mapping_2['volumeRef'] = '2' - volume_mapping_2['mapRef'] = 'hostRef' - self.mock_object(self.my_client, 'get_volume_mappings', - return_value=[volume_mapping_1, volume_mapping_2]) - - mappings = self.my_client.get_volume_mappings_for_host( - 'hostRef') - - self.assertEqual([volume_mapping_2], mappings) - - def test_get_volume_mappings_for_hostgroup(self): - volume_mapping_1 = copy.deepcopy( - eseries_fake.VOLUME_MAPPING) - volume_mapping_2 = copy.deepcopy(eseries_fake.VOLUME_MAPPING) - volume_mapping_2['volumeRef'] = '2' - volume_mapping_2['mapRef'] = 'hostGroupRef' - self.mock_object(self.my_client, 'get_volume_mappings', - return_value=[volume_mapping_1, volume_mapping_2]) - - mappings = self.my_client.get_volume_mappings_for_host_group( - 'hostGroupRef') - - self.assertEqual([volume_mapping_2], mappings) - - def test_to_pretty_dict_string(self): - dict = { - 'foo': 'bar', - 'fu': { - 'nested': 'boo' - } - } - expected_dict_string = ("""{ - "foo": "bar", - "fu": { - "nested": "boo" - } -}""") - - dict_string = self.my_client._to_pretty_dict_string(dict) - - self.assertEqual(expected_dict_string, dict_string) - - def test_log_http_request(self): - mock_log = self.mock_object(client, 'LOG') - verb = "POST" - url = "/v2/test/me" - headers = {"Content-Type": "application/json"} - headers_string = """{ - "Content-Type": "application/json" -}""" - body = {} - body_string = "{}" - - self.my_client._log_http_request(verb, url, headers, body) - - args = mock_log.debug.call_args - log_message, log_params = args[0] - final_msg = log_message % log_params - self.assertIn(verb, final_msg) - self.assertIn(url, final_msg) - self.assertIn(headers_string, final_msg) - self.assertIn(body_string, final_msg) - - def test_log_http_request_no_body(self): - mock_log = self.mock_object(client, 'LOG') - verb = "POST" - url = "/v2/test/me" - headers = {"Content-Type": "application/json"} - headers_string = """{ - "Content-Type": "application/json" -}""" - body = None - body_string = "" - - self.my_client._log_http_request(verb, url, headers, body) - - args = mock_log.debug.call_args - log_message, log_params = args[0] - final_msg = log_message % log_params - self.assertIn(verb, final_msg) - self.assertIn(url, final_msg) - self.assertIn(headers_string, final_msg) - self.assertIn(body_string, final_msg) - - def test_log_http_response(self): - mock_log = self.mock_object(client, 'LOG') - status = "200" - headers = {"Content-Type": "application/json"} - headers_string = """{ - "Content-Type": "application/json" -}""" - body = {} - body_string = "{}" - - self.my_client._log_http_response(status, headers, body) - - args = mock_log.debug.call_args - log_message, log_params = args[0] - final_msg = log_message % log_params - self.assertIn(status, final_msg) - self.assertIn(headers_string, final_msg) - self.assertIn(body_string, final_msg) - - def test_log_http_response_no_body(self): - mock_log = self.mock_object(client, 'LOG') - status = "200" - headers = {"Content-Type": "application/json"} - headers_string = """{ - "Content-Type": "application/json" -}""" - body = None - body_string = "" - - self.my_client._log_http_response(status, headers, body) - - args = mock_log.debug.call_args - log_message, log_params = args[0] - final_msg = log_message % log_params - self.assertIn(status, final_msg) - self.assertIn(headers_string, final_msg) - self.assertIn(body_string, final_msg) - - def test_add_autosupport_data(self): - self.mock_object( - client.RestClient, 'get_eseries_api_info', - return_value=(eseries_fake.FAKE_ASUP_DATA['operating-mode'], - eseries_fake.FAKE_ABOUT_RESPONSE['version'])) - self.mock_object( - self.my_client, 'get_asup_info', - return_value=eseries_fake.GET_ASUP_RETURN) - self.mock_object( - self.my_client, 'set_counter', return_value={'value': 1}) - mock_invoke = self.mock_object( - self.my_client, '_invoke', - return_value=eseries_fake.FAKE_ASUP_DATA) - - client.RestClient.add_autosupport_data( - self.my_client, - eseries_fake.FAKE_KEY, - eseries_fake.FAKE_ASUP_DATA - ) - - mock_invoke.assert_called_with(*eseries_fake.FAKE_POST_INVOKE_DATA) - - @ddt.data((eseries_fake.FAKE_SERIAL_NUMBERS, - eseries_fake.HARDWARE_INVENTORY), - (eseries_fake.FAKE_DEFAULT_SERIAL_NUMBER, {}), - (eseries_fake.FAKE_SERIAL_NUMBER, - eseries_fake.HARDWARE_INVENTORY_SINGLE_CONTROLLER)) - @ddt.unpack - def test_get_asup_info_serial_numbers(self, expected_serial_numbers, - controllers): - self.mock_object( - client.RestClient, 'list_hardware_inventory', - return_value=controllers) - self.mock_object( - client.RestClient, 'list_storage_system', return_value={}) - - sn = client.RestClient.get_asup_info(self.my_client)['serial_numbers'] - - self.assertEqual(expected_serial_numbers, sn) - - def test_get_asup_info_model_name(self): - self.mock_object( - client.RestClient, 'list_hardware_inventory', - return_value=eseries_fake.HARDWARE_INVENTORY) - self.mock_object( - client.RestClient, 'list_storage_system', - return_value=eseries_fake.STORAGE_SYSTEM) - - model_name = client.RestClient.get_asup_info(self.my_client)['model'] - - self.assertEqual(eseries_fake.HARDWARE_INVENTORY['controllers'][0] - ['modelName'], model_name) - - def test_get_asup_info_model_name_empty_controllers_list(self): - self.mock_object( - client.RestClient, 'list_hardware_inventory', return_value={}) - self.mock_object( - client.RestClient, 'list_storage_system', return_value={}) - - model_name = client.RestClient.get_asup_info(self.my_client)['model'] - - self.assertEqual(eseries_fake.FAKE_DEFAULT_MODEL, model_name) - - def test_get_eseries_api_info(self): - fake_invoke_service = mock.Mock() - fake_invoke_service.json = mock.Mock( - return_value=eseries_fake.FAKE_ABOUT_RESPONSE) - self.mock_object( - client.RestClient, '_get_resource_url', - return_value=eseries_fake.FAKE_RESOURCE_URL) - self.mock_object( - self.my_client, 'invoke_service', return_value=fake_invoke_service) - - eseries_info = client.RestClient.get_eseries_api_info( - self.my_client, verify=False) - - self.assertEqual((eseries_fake.FAKE_ASUP_DATA['operating-mode'], - eseries_fake.FAKE_ABOUT_RESPONSE['version']), - eseries_info) - - def test_list_ssc_storage_pools(self): - self.my_client.features = mock.Mock() - self.my_client._invoke = mock.Mock( - return_value=eseries_fake.SSC_POOLS) - - pools = client.RestClient.list_ssc_storage_pools(self.my_client) - - self.assertEqual(eseries_fake.SSC_POOLS, pools) - - def test_get_ssc_storage_pool(self): - fake_pool = eseries_fake.SSC_POOLS[0] - self.my_client.features = mock.Mock() - self.my_client._invoke = mock.Mock( - return_value=fake_pool) - - pool = client.RestClient.get_ssc_storage_pool(self.my_client, - fake_pool['poolId']) - - self.assertEqual(fake_pool, pool) - - @ddt.data(('volumes', True), ('volumes', False), - ('volume', True), ('volume', False)) - @ddt.unpack - def test_get_volume_api_path(self, path_key, ssc_available): - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=ssc_available) - expected_key = 'ssc_' + path_key if ssc_available else path_key - expected = self.my_client.RESOURCE_PATHS.get(expected_key) - - actual = self.my_client._get_volume_api_path(path_key) - - self.assertEqual(expected, actual) - - @ddt.data(True, False) - def test_get_volume_api_path_invalid(self, ssc_available): - key = 'invalidKey' - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=ssc_available) - - self.assertRaises(KeyError, self.my_client._get_volume_api_path, key) - - def test_list_volumes(self): - url = client.RestClient.RESOURCE_PATHS['ssc_volumes'] - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - self.my_client._invoke = mock.Mock( - return_value=eseries_fake.VOLUMES) - - volumes = client.RestClient.list_volumes(self.my_client) - - self.assertEqual(eseries_fake.VOLUMES, volumes) - self.my_client._invoke.assert_called_once_with('GET', url) - - @ddt.data(client.RestClient.ID, client.RestClient.WWN, - client.RestClient.NAME) - def test_list_volume_v1(self, uid_field_name): - url = client.RestClient.RESOURCE_PATHS['volumes'] - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=False) - fake_volume = copy.deepcopy(eseries_fake.VOLUME) - self.my_client._invoke = mock.Mock( - return_value=eseries_fake.VOLUMES) - - volume = client.RestClient.list_volume(self.my_client, - fake_volume[uid_field_name]) - - self.my_client._invoke.assert_called_once_with('GET', url) - self.assertEqual(fake_volume, volume) - - def test_list_volume_v1_not_found(self): - url = client.RestClient.RESOURCE_PATHS['volumes'] - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=False) - self.my_client._invoke = mock.Mock( - return_value=eseries_fake.VOLUMES) - - self.assertRaises(exception.VolumeNotFound, - client.RestClient.list_volume, - self.my_client, 'fakeId') - self.my_client._invoke.assert_called_once_with('GET', url) - - def test_list_volume_v2(self): - url = client.RestClient.RESOURCE_PATHS['ssc_volume'] - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - fake_volume = copy.deepcopy(eseries_fake.VOLUME) - self.my_client._invoke = mock.Mock(return_value=fake_volume) - - volume = client.RestClient.list_volume(self.my_client, - fake_volume['id']) - - self.my_client._invoke.assert_called_once_with('GET', url, - **{'object-id': - mock.ANY}) - self.assertEqual(fake_volume, volume) - - def test_list_volume_v2_not_found(self): - status_code = http_client.NOT_FOUND - url = client.RestClient.RESOURCE_PATHS['ssc_volume'] - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - msg = "Response error code - %s." % status_code - self.my_client._invoke = mock.Mock( - side_effect=es_exception.WebServiceException(message=msg, - status_code= - status_code)) - - self.assertRaises(exception.VolumeNotFound, - client.RestClient.list_volume, - self.my_client, 'fakeId') - self.my_client._invoke.assert_called_once_with('GET', url, - **{'object-id': - mock.ANY}) - - def test_list_volume_v2_failure(self): - status_code = http_client.UNPROCESSABLE_ENTITY - url = client.RestClient.RESOURCE_PATHS['ssc_volume'] - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - msg = "Response error code - %s." % status_code - self.my_client._invoke = mock.Mock( - side_effect=es_exception.WebServiceException(message=msg, - status_code= - status_code)) - - self.assertRaises(es_exception.WebServiceException, - client.RestClient.list_volume, self.my_client, - 'fakeId') - self.my_client._invoke.assert_called_once_with('GET', url, - **{'object-id': - mock.ANY}) - - def test_create_volume_V1(self): - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=False) - create_volume = self.my_client._invoke = mock.Mock( - return_value=eseries_fake.VOLUME) - - volume = client.RestClient.create_volume(self.my_client, - 'fakePool', '1', 1) - - args, kwargs = create_volume.call_args - verb, url, body = args - # Ensure the correct API was used - self.assertEqual('/storage-systems/{system-id}/volumes', url) - self.assertEqual(eseries_fake.VOLUME, volume) - - def test_create_volume_V2(self): - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - create_volume = self.my_client._invoke = mock.Mock( - return_value=eseries_fake.VOLUME) - - volume = client.RestClient.create_volume(self.my_client, - 'fakePool', '1', 1) - - args, kwargs = create_volume.call_args - verb, url, body = args - # Ensure the correct API was used - self.assertIn('/storage-systems/{system-id}/ssc/volumes', url, - 'The legacy API was used!') - self.assertEqual(eseries_fake.VOLUME, volume) - - def test_create_volume_unsupported_specs(self): - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=False) - self.my_client.api_version = '01.52.9000.1' - - self.assertRaises(exception.NetAppDriverException, - client.RestClient.create_volume, self.my_client, - '1', 'label', 1, read_cache=True) - - @ddt.data(True, False) - def test_update_volume(self, ssc_api_enabled): - label = 'updatedName' - fake_volume = copy.deepcopy(eseries_fake.VOLUME) - expected_volume = copy.deepcopy(fake_volume) - expected_volume['name'] = label - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=ssc_api_enabled) - self.my_client._invoke = mock.Mock(return_value=expected_volume) - - updated_volume = self.my_client.update_volume(fake_volume['id'], - label) - - if ssc_api_enabled: - url = self.my_client.RESOURCE_PATHS.get('ssc_volume') - else: - url = self.my_client.RESOURCE_PATHS.get('volume') - - self.my_client._invoke.assert_called_once_with('POST', url, - {'name': label}, - **{'object-id': - fake_volume['id']} - ) - self.assertDictEqual(expected_volume, updated_volume) - - def test_get_pool_operation_progress(self): - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - fake_response = copy.deepcopy(eseries_fake.FAKE_POOL_ACTION_PROGRESS) - self.my_client._invoke = mock.Mock(return_value=fake_response) - - response = self.my_client.get_pool_operation_progress(fake_pool['id']) - - url = self.my_client.RESOURCE_PATHS.get('pool_operation_progress') - self.my_client._invoke.assert_called_once_with('GET', url, - **{'object-id': - fake_pool['id']}) - self.assertEqual(fake_response, response) - - def test_extend_volume(self): - new_capacity = 10 - fake_volume = copy.deepcopy(eseries_fake.VOLUME) - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - self.my_client._invoke = mock.Mock(return_value=fake_volume) - - expanded_volume = self.my_client.expand_volume(fake_volume['id'], - new_capacity, False) - - url = self.my_client.RESOURCE_PATHS.get('volume_expand') - body = {'expansionSize': new_capacity, 'sizeUnit': 'gb'} - self.my_client._invoke.assert_called_once_with('POST', url, body, - **{'object-id': - fake_volume['id']}) - self.assertEqual(fake_volume, expanded_volume) - - def test_extend_volume_thin(self): - new_capacity = 10 - fake_volume = copy.deepcopy(eseries_fake.VOLUME) - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=True) - self.my_client._invoke = mock.Mock(return_value=fake_volume) - - expanded_volume = self.my_client.expand_volume(fake_volume['id'], - new_capacity, True) - - url = self.my_client.RESOURCE_PATHS.get('thin_volume_expand') - body = {'newVirtualSize': new_capacity, 'sizeUnit': 'gb', - 'newRepositorySize': new_capacity} - self.my_client._invoke.assert_called_once_with('POST', url, body, - **{'object-id': - fake_volume['id']}) - self.assertEqual(fake_volume, expanded_volume) - - @ddt.data(True, False) - def test_delete_volume(self, ssc_api_enabled): - fake_volume = copy.deepcopy(eseries_fake.VOLUME) - self.my_client.features = mock.Mock() - self.my_client.features.SSC_API_V2 = na_utils.FeatureState( - supported=ssc_api_enabled) - self.my_client._invoke = mock.Mock() - - self.my_client.delete_volume(fake_volume['id']) - - if ssc_api_enabled: - url = self.my_client.RESOURCE_PATHS.get('ssc_volume') - else: - url = self.my_client.RESOURCE_PATHS.get('volume') - - self.my_client._invoke.assert_called_once_with('DELETE', url, - **{'object-id': - fake_volume['id']}) - - def test_list_snapshot_group(self): - grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - invoke = self.mock_object(self.my_client, '_invoke', return_value=grp) - fake_ref = 'fake' - - result = self.my_client.list_snapshot_group(fake_ref) - - self.assertEqual(grp, result) - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['snapshot_group'], - **{'object-id': fake_ref}) - - def test_list_snapshot_groups(self): - grps = [copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)] - invoke = self.mock_object(self.my_client, '_invoke', return_value=grps) - - result = self.my_client.list_snapshot_groups() - - self.assertEqual(grps, result) - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['snapshot_groups']) - - def test_delete_snapshot_group(self): - invoke = self.mock_object(self.my_client, '_invoke') - fake_ref = 'fake' - - self.my_client.delete_snapshot_group(fake_ref) - - invoke.assert_called_once_with( - 'DELETE', self.my_client.RESOURCE_PATHS['snapshot_group'], - **{'object-id': fake_ref}) - - @ddt.data((None, None, None, None, None), ('1', 50, 75, 32, 'purgepit')) - @ddt.unpack - def test_create_snapshot_group(self, pool_id, repo, warn, limit, policy): - vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - invoke = self.mock_object(self.my_client, '_invoke', return_value=vol) - snap_grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - - result = self.my_client.create_snapshot_group( - snap_grp['label'], snap_grp['id'], pool_id, repo, warn, limit, - policy) - - self.assertEqual(vol, result) - invoke.assert_called_once_with( - 'POST', self.my_client.RESOURCE_PATHS['snapshot_groups'], - {'baseMappableObjectId': snap_grp['id'], 'name': snap_grp['label'], - 'storagePoolId': pool_id, 'repositoryPercentage': repo, - 'warningThreshold': warn, 'autoDeleteLimit': limit, - 'fullPolicy': policy}) - - def test_list_snapshot_volumes(self): - vols = [copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)] - invoke = self.mock_object(self.my_client, '_invoke', return_value=vols) - - result = self.my_client.list_snapshot_volumes() - - self.assertEqual(vols, result) - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['snapshot_volumes']) - - def test_delete_snapshot_volume(self): - invoke = self.mock_object(self.my_client, '_invoke') - fake_ref = 'fake' - - self.my_client.delete_snapshot_volume(fake_ref) - - invoke.assert_called_once_with( - 'DELETE', self.my_client.RESOURCE_PATHS['snapshot_volume'], - **{'object-id': fake_ref}) - - @ddt.data((None, None, None, None), ('1', 50, 75, 'readWrite')) - @ddt.unpack - def test_create_snapshot_volume(self, pool_id, repo, warn, mode): - vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - invoke = self.mock_object(self.my_client, '_invoke', return_value=vol) - - result = self.my_client.create_snapshot_volume( - vol['basePIT'], vol['label'], vol['id'], pool_id, - repo, warn, mode) - - self.assertEqual(vol, result) - invoke.assert_called_once_with( - 'POST', self.my_client.RESOURCE_PATHS['snapshot_volumes'], - mock.ANY) - - def test_update_snapshot_volume(self): - snap_id = '1' - label = 'name' - pct = 99 - vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - invoke = self.mock_object(self.my_client, '_invoke', return_value=vol) - - result = self.my_client.update_snapshot_volume(snap_id, label, pct) - - self.assertEqual(vol, result) - invoke.assert_called_once_with( - 'POST', self.my_client.RESOURCE_PATHS['snapshot_volume'], - {'name': label, 'fullThreshold': pct}, **{'object-id': snap_id}) - - def test_create_snapshot_image(self): - img = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - invoke = self.mock_object(self.my_client, '_invoke', return_value=img) - grp_id = '1' - - result = self.my_client.create_snapshot_image(grp_id) - - self.assertEqual(img, result) - invoke.assert_called_once_with( - 'POST', self.my_client.RESOURCE_PATHS['snapshot_images'], - {'groupId': grp_id}) - - def test_list_snapshot_image(self): - img = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - invoke = self.mock_object(self.my_client, '_invoke', return_value=img) - fake_ref = 'fake' - - result = self.my_client.list_snapshot_image(fake_ref) - - self.assertEqual(img, result) - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['snapshot_image'], - **{'object-id': fake_ref}) - - def test_list_snapshot_images(self): - imgs = [copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)] - invoke = self.mock_object(self.my_client, '_invoke', return_value=imgs) - - result = self.my_client.list_snapshot_images() - - self.assertEqual(imgs, result) - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['snapshot_images']) - - def test_delete_snapshot_image(self): - invoke = self.mock_object(self.my_client, '_invoke') - fake_ref = 'fake' - - self.my_client.delete_snapshot_image(fake_ref) - - invoke.assert_called_once_with( - 'DELETE', self.my_client.RESOURCE_PATHS['snapshot_image'], - **{'object-id': fake_ref}) - - def test_create_consistency_group(self): - invoke = self.mock_object(self.my_client, '_invoke') - name = 'fake' - - self.my_client.create_consistency_group(name) - - invoke.assert_called_once_with( - 'POST', self.my_client.RESOURCE_PATHS['cgroups'], mock.ANY) - - def test_list_consistency_group(self): - invoke = self.mock_object(self.my_client, '_invoke') - ref = 'fake' - - self.my_client.get_consistency_group(ref) - - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['cgroup'], - **{'object-id': ref}) - - def test_list_consistency_groups(self): - invoke = self.mock_object(self.my_client, '_invoke') - - self.my_client.list_consistency_groups() - - invoke.assert_called_once_with( - 'GET', self.my_client.RESOURCE_PATHS['cgroups']) - - def test_delete_consistency_group(self): - invoke = self.mock_object(self.my_client, '_invoke') - ref = 'fake' - - self.my_client.delete_consistency_group(ref) - - invoke.assert_called_once_with( - 'DELETE', self.my_client.RESOURCE_PATHS['cgroup'], - **{'object-id': ref}) - - def test_add_consistency_group_member(self): - invoke = self.mock_object(self.my_client, '_invoke') - vol_id = eseries_fake.VOLUME['id'] - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.my_client.add_consistency_group_member(vol_id, cg_id) - - invoke.assert_called_once_with( - 'POST', self.my_client.RESOURCE_PATHS['cgroup_members'], - mock.ANY, **{'object-id': cg_id}) - - def test_remove_consistency_group_member(self): - invoke = self.mock_object(self.my_client, '_invoke') - vol_id = eseries_fake.VOLUME['id'] - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.my_client.remove_consistency_group_member(vol_id, cg_id) - - invoke.assert_called_once_with( - 'DELETE', self.my_client.RESOURCE_PATHS['cgroup_member'], - **{'object-id': cg_id, 'vol-id': vol_id}) - - def test_create_consistency_group_snapshot(self): - invoke = self.mock_object(self.my_client, '_invoke') - path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshots') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.my_client.create_consistency_group_snapshot(cg_id) - - invoke.assert_called_once_with('POST', path, **{'object-id': cg_id}) - - @ddt.data(0, 32) - def test_delete_consistency_group_snapshot(self, seq_num): - invoke = self.mock_object(self.my_client, '_invoke') - path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshot') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.my_client.delete_consistency_group_snapshot(cg_id, seq_num) - - invoke.assert_called_once_with( - 'DELETE', path, **{'object-id': cg_id, 'seq-num': seq_num}) - - def test_get_consistency_group_snapshots(self): - invoke = self.mock_object(self.my_client, '_invoke') - path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshots') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.my_client.get_consistency_group_snapshots(cg_id) - - invoke.assert_called_once_with( - 'GET', path, **{'object-id': cg_id}) - - def test_create_cg_snapshot_view(self): - cg_snap_view = copy.deepcopy( - eseries_fake.FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME) - view = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - invoke = self.mock_object(self.my_client, '_invoke', - return_value=cg_snap_view) - list_views = self.mock_object( - self.my_client, 'list_cg_snapshot_views', return_value=[view]) - name = view['name'] - snap_id = view['basePIT'] - path = self.my_client.RESOURCE_PATHS.get('cgroup_cgsnap_views') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.my_client.create_cg_snapshot_view(cg_id, name, snap_id) - - invoke.assert_called_once_with( - 'POST', path, mock.ANY, **{'object-id': cg_id}) - list_views.assert_called_once_with(cg_id, cg_snap_view['cgViewRef']) - - def test_create_cg_snapshot_view_not_found(self): - cg_snap_view = copy.deepcopy( - eseries_fake.FAKE_CONSISTENCY_GROUP_SNAPSHOT_VOLUME) - view = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - invoke = self.mock_object(self.my_client, '_invoke', - return_value=cg_snap_view) - list_views = self.mock_object( - self.my_client, 'list_cg_snapshot_views', return_value=[view]) - del_view = self.mock_object(self.my_client, 'delete_cg_snapshot_view') - name = view['name'] - # Ensure we don't get a match on the retrieved views - snap_id = None - path = self.my_client.RESOURCE_PATHS.get('cgroup_cgsnap_views') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - - self.assertRaises( - exception.NetAppDriverException, - self.my_client.create_cg_snapshot_view, cg_id, name, snap_id) - - invoke.assert_called_once_with( - 'POST', path, mock.ANY, **{'object-id': cg_id}) - list_views.assert_called_once_with(cg_id, cg_snap_view['cgViewRef']) - del_view.assert_called_once_with(cg_id, cg_snap_view['id']) - - def test_list_cg_snapshot_views(self): - invoke = self.mock_object(self.my_client, '_invoke') - path = self.my_client.RESOURCE_PATHS.get('cgroup_snapshot_views') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - view_id = 'id' - - self.my_client.list_cg_snapshot_views(cg_id, view_id) - - invoke.assert_called_once_with( - 'GET', path, **{'object-id': cg_id, 'view-id': view_id}) - - def test_delete_cg_snapshot_view(self): - invoke = self.mock_object(self.my_client, '_invoke') - path = self.my_client.RESOURCE_PATHS.get('cgroup_snap_view') - cg_id = eseries_fake.FAKE_CONSISTENCY_GROUP['id'] - view_id = 'id' - - self.my_client.delete_cg_snapshot_view(cg_id, view_id) - - invoke.assert_called_once_with( - 'DELETE', path, **{'object-id': cg_id, 'view-id': view_id}) - - @ddt.data('00.00.00.00', '01.52.9000.2', '01.52.9001.2', '01.51.9000.3', - '01.51.9001.3', '01.51.9010.5', '0.53.9000.3', '0.53.9001.4') - def test_api_version_not_support_asup(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertFalse(self.my_client.features.AUTOSUPPORT.supported) - - @ddt.data('01.52.9000.3', '01.52.9000.4', '01.52.8999.2', - '01.52.8999.3', '01.53.8999.3', '01.53.9000.2', - '02.51.9000.3', '02.52.8999.3', '02.51.8999.2') - def test_api_version_supports_asup(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertTrue(bool(self.my_client.features.AUTOSUPPORT)) - - @ddt.data('00.00.00.00', '01.52.9000.2', '01.52.9001.2', '01.51.9000.3', - '01.51.9001.3', '01.51.9010.5', '0.53.9000.3', '0.53.9001.4') - def test_api_version_not_support_chap(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertFalse(bool(self.my_client.features.CHAP_AUTHENTICATION)) - - @ddt.data('01.53.9000.15', '01.53.9000.16', '01.53.8999.15', - '01.54.8999.16', '01.54.9010.15', '01.54.9090.15', - '02.52.9000.15', '02.53.8999.15', '02.54.8999.14') - def test_api_version_supports_chap(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertTrue(bool(self.my_client.features.CHAP_AUTHENTICATION)) - - @ddt.data('00.00.00.00', '01.52.9000.1', '01.52.9001.2', '00.53.9001.3', - '01.53.9090.1', '1.53.9010.14', '0.53.9011.15') - def test_api_version_not_support_ssc_api(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertFalse(self.my_client.features.SSC_API_V2.supported) - - @ddt.data('01.53.9000.1', '01.53.9000.5', '01.53.8999.1', - '01.53.9010.20', '01.53.9010.17', '01.54.9000.1', - '02.51.9000.3', '02.52.8999.3', '02.51.8999.2') - def test_api_version_supports_ssc_api(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertTrue(self.my_client.features.SSC_API_V2.supported) - - @ddt.data('00.00.00.00', '01.52.9000.5', '01.52.9001.2', '00.53.9001.3', - '01.52.9090.1', '1.52.9010.7', '0.53.9011.7') - def test_api_version_not_support_1_3(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertFalse(self.my_client.features.REST_1_3_RELEASE.supported) - - @ddt.data('01.53.9000.1', '01.53.9000.5', '01.53.8999.1', - '01.54.9010.20', '01.54.9000.1', '02.51.9000.3', - '02.52.8999.3', '02.51.8999.2') - def test_api_version_1_3(self, api_version): - - self.mock_object(client.RestClient, - 'get_eseries_api_info', - return_value=('proxy', api_version)) - - client.RestClient._init_features(self.my_client) - - self.assertTrue(self.my_client.features.REST_1_3_RELEASE.supported) - - def test_invoke_bad_content_type(self): - """Tests the invoke behavior with a non-JSON response""" - fake_response = mock.Mock() - fake_response.json = mock.Mock(side_effect=ValueError( - '', '{}', 1)) - fake_response.status_code = http_client.FAILED_DEPENDENCY - fake_response.text = "Fake Response" - self.mock_object(self.my_client, 'invoke_service', - return_value=fake_response) - - self.assertRaises(es_exception.WebServiceException, - self.my_client._invoke, 'GET', - eseries_fake.FAKE_ENDPOINT_HTTP) - - def test_list_backend_store(self): - path = self.my_client.RESOURCE_PATHS.get('persistent-store') - fake_store = copy.deepcopy(eseries_fake.FAKE_BACKEND_STORE) - invoke = self.mock_object( - self.my_client, '_invoke', return_value=fake_store) - expected = json.loads(fake_store.get('value')) - - result = self.my_client.list_backend_store('key') - - self.assertEqual(expected, result) - invoke.assert_called_once_with('GET', path, key='key') - - def test_save_backend_store(self): - path = self.my_client.RESOURCE_PATHS.get('persistent-stores') - fake_store = copy.deepcopy(eseries_fake.FAKE_BACKEND_STORE) - key = 'key' - invoke = self.mock_object(self.my_client, '_invoke') - - self.my_client.save_backend_store(key, fake_store) - - invoke.assert_called_once_with('POST', path, mock.ANY) - - -@ddt.ddt -class TestWebserviceClientTestCase(test.TestCase): - def setUp(self): - """sets up the mock tests""" - super(TestWebserviceClientTestCase, self).setUp() - self.mock_log = mock.Mock() - self.mock_object(client, 'LOG', self.mock_log) - self.webclient = client.WebserviceClient('http', 'host', '80', - '/test', 'user', '****') - - @ddt.data({'params': {'host': None, 'scheme': 'https', 'port': '80'}}, - {'params': {'host': 'host', 'scheme': None, 'port': '80'}}, - {'params': {'host': 'host', 'scheme': 'http', 'port': None}}) - @ddt.unpack - def test__validate_params_value_error(self, params): - """Tests various scenarios for ValueError in validate method""" - self.assertRaises(exception.InvalidInput, - self.webclient._validate_params, **params) - - def test_invoke_service_no_endpoint_error(self): - """Tests Exception and Log error if no endpoint is provided""" - self.webclient._endpoint = None - log_error = 'Unexpected error while invoking web service' - - self.assertRaises(exception.NetAppDriverException, - self.webclient.invoke_service) - self.assertTrue(bool(self.mock_log.exception.find(log_error))) - - def test_invoke_service(self): - """Tests if invoke_service evaluates the right response""" - self.webclient._endpoint = eseries_fake.FAKE_ENDPOINT_HTTP - self.mock_object(self.webclient.conn, 'request', - return_value=eseries_fake.FAKE_INVOC_MSG) - result = self.webclient.invoke_service() - - self.assertIsNotNone(result) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py deleted file mode 100644 index d61023e9270..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_driver.py +++ /dev/null @@ -1,543 +0,0 @@ -# Copyright (c) 2015 Alex Meade. All rights reserved. -# Copyright (c) 2015 Michael Price. All rights reserved. -# 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. - -import abc -import copy -import ddt -import mock -import socket - -from cinder import exception -from cinder import utils as cinder_utils -from cinder.volume import configuration as conf - -from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \ - fakes -from cinder.volume.drivers.netapp import common -from cinder.volume.drivers.netapp.eseries import client -from cinder.volume.drivers.netapp.eseries import library -from cinder.volume.drivers.netapp.eseries import utils -from cinder.volume.drivers.netapp import options -import cinder.volume.drivers.netapp.utils as na_utils - - -@ddt.ddt -class NetAppESeriesDriverTestCase(object): - """Test case for NetApp e-series iscsi driver.""" - - volume = {'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1, - 'volume_name': 'lun1', 'host': 'hostname@backend#DDP', - 'os_type': 'linux', 'provider_location': 'lun1', - 'name_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', - 'provider_auth': 'provider a b', 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None} - snapshot = {'id': '17928122-553b-4da9-9737-e5c3dcd97f75', - 'volume_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', - 'size': 2, 'volume_name': 'lun1', - 'volume_size': 2, 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None} - volume_sec = {'id': 'b6c01641-8955-4917-a5e3-077147478575', - 'size': 2, 'volume_name': 'lun1', - 'os_type': 'linux', 'provider_location': 'lun1', - 'name_id': 'b6c01641-8955-4917-a5e3-077147478575', - 'provider_auth': None, 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None} - volume_clone = {'id': 'b4b24b27-c716-4647-b66d-8b93ead770a5', 'size': 3, - 'volume_name': 'lun1', - 'os_type': 'linux', 'provider_location': 'cl_sm', - 'name_id': 'b4b24b27-c716-4647-b66d-8b93ead770a5', - 'provider_auth': None, - 'project_id': 'project', 'display_name': None, - 'display_description': 'lun1', - 'volume_type_id': None} - volume_clone_large = {'id': 'f6ef5bf5-e24f-4cbb-b4c4-11d631d6e553', - 'size': 6, 'volume_name': 'lun1', - 'os_type': 'linux', 'provider_location': 'cl_lg', - 'name_id': 'f6ef5bf5-e24f-4cbb-b4c4-11d631d6e553', - 'provider_auth': None, - 'project_id': 'project', 'display_name': None, - 'display_description': 'lun1', - 'volume_type_id': None} - fake_eseries_volume_label = utils.convert_uuid_to_es_fmt(volume['id']) - fake_size_gb = volume['size'] - fake_eseries_pool_label = 'DDP' - fake_ref = {'source-name': 'CFDGJSLS'} - fake_ret_vol = {'id': 'vol_id', 'label': 'label', - 'worldWideName': 'wwn', 'capacity': '2147583648'} - PROTOCOL = 'iscsi' - - def setUp(self): - super(NetAppESeriesDriverTestCase, self).setUp() - self._custom_setup() - - def _custom_setup(self): - self.mock_object(na_utils, 'OpenStackInfo') - - configuration = self._set_config(self.create_configuration()) - self.driver = common.NetAppDriver(configuration=configuration) - self.library = self.driver.library - self.mock_object(self.library, - '_check_mode_get_or_register_storage_system') - self.mock_object(self.library, '_version_check') - self.mock_object(self.driver.library, '_check_storage_system') - self.driver.do_setup(context='context') - self.driver.library._client._endpoint = fakes.FAKE_ENDPOINT_HTTP - self.driver.library._client.features = mock.Mock() - self.driver.library._client.features.REST_1_4_RELEASE = True - - def _set_config(self, configuration): - configuration.netapp_storage_family = 'eseries' - configuration.netapp_storage_protocol = self.PROTOCOL - configuration.netapp_transport_type = 'http' - configuration.netapp_server_hostname = '127.0.0.1' - configuration.netapp_server_port = None - configuration.netapp_webservice_path = '/devmgr/vn' - configuration.netapp_controller_ips = '127.0.0.2,127.0.0.3' - configuration.netapp_sa_password = 'pass1234' - configuration.netapp_login = 'rw' - configuration.netapp_password = 'rw' - configuration.netapp_storage_pools = 'DDP' - configuration.netapp_enable_multiattach = False - return configuration - - @staticmethod - def create_configuration(): - configuration = conf.Configuration(None) - configuration.append_config_values(options.netapp_basicauth_opts) - configuration.append_config_values(options.netapp_eseries_opts) - configuration.append_config_values(options.netapp_san_opts) - return configuration - - @abc.abstractmethod - @mock.patch.object(na_utils, 'validate_instantiation') - def test_instantiation(self, mock_validate_instantiation): - pass - - def test_embedded_mode(self): - self.mock_object(client.RestClient, '_init_features') - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '127.0.0.1,127.0.0.3' - driver = common.NetAppDriver(configuration=configuration) - self.mock_object(driver.library, '_version_check') - self.mock_object(client.RestClient, 'list_storage_systems', - return_value=[fakes.STORAGE_SYSTEM]) - driver.do_setup(context='context') - - self.assertEqual('1fa6efb5-f07b-4de4-9f0e-52e5f7ff5d1b', - driver.library._client.get_system_id()) - - def test_check_system_pwd_not_sync(self): - def list_system(): - if getattr(self, 'test_count', None): - self.test_count = 1 - return {'status': 'passwordoutofsync'} - return {'status': 'needsAttention'} - - self.library._client.list_storage_system = mock.Mock(wraps=list_system) - result = self.library._check_storage_system() - self.assertTrue(bool(result)) - - def test_create_destroy(self): - self.mock_object(client.RestClient, 'delete_volume', - return_value='None') - self.mock_object(self.driver.library, 'create_volume', - return_value=self.volume) - self.mock_object(self.library._client, 'list_volume', - return_value=fakes.VOLUME) - - self.driver.create_volume(self.volume) - self.driver.delete_volume(self.volume) - - def test_vol_stats(self): - self.driver.get_volume_stats(refresh=False) - - def test_get_pool(self): - self.mock_object(self.library, '_get_volume', - return_value={'volumeGroupRef': 'fake_ref'}) - self.mock_object(self.library._client, "get_storage_pool", - return_value={'volumeGroupRef': 'fake_ref', - 'label': 'ddp1'}) - - pool = self.driver.get_pool({'name_id': 'fake-uuid'}) - - self.assertEqual('ddp1', pool) - - def test_get_pool_no_pools(self): - self.mock_object(self.library, '_get_volume', - return_value={'volumeGroupRef': 'fake_ref'}) - self.mock_object(self.library._client, "get_storage_pool", - return_value=None) - - pool = self.driver.get_pool({'name_id': 'fake-uuid'}) - - self.assertIsNone(pool) - - @mock.patch.object(library.NetAppESeriesLibrary, '_create_volume', - mock.Mock()) - def test_create_volume(self): - - self.driver.create_volume(self.volume) - - self.library._create_volume.assert_called_with( - 'DDP', self.fake_eseries_volume_label, self.volume['size'], {}) - - def test_create_volume_no_pool_provided_by_scheduler(self): - volume = copy.deepcopy(self.volume) - volume['host'] = "host@backend" # missing pool - self.assertRaises(exception.InvalidHost, self.driver.create_volume, - volume) - - @mock.patch.object(client.RestClient, 'list_storage_pools') - def test_helper_create_volume_fail(self, fake_list_pools): - fake_pool = {} - fake_pool['label'] = self.fake_eseries_pool_label - fake_pool['volumeGroupRef'] = 'foo' - fake_pool['raidLevel'] = 'raidDiskPool' - fake_pools = [fake_pool] - fake_list_pools.return_value = fake_pools - wrong_eseries_pool_label = 'hostname@backend' - self.assertRaises(exception.NetAppDriverException, - self.library._create_volume, - wrong_eseries_pool_label, - self.fake_eseries_volume_label, - self.fake_size_gb) - - @mock.patch.object(library.LOG, 'info') - @mock.patch.object(client.RestClient, 'list_storage_pools') - @mock.patch.object(client.RestClient, 'create_volume', - mock.MagicMock(return_value='CorrectVolume')) - def test_helper_create_volume(self, storage_pools, log_info): - fake_pool = {} - fake_pool['label'] = self.fake_eseries_pool_label - fake_pool['volumeGroupRef'] = 'foo' - fake_pool['raidLevel'] = 'raidDiskPool' - fake_pools = [fake_pool] - storage_pools.return_value = fake_pools - storage_vol = self.library._create_volume( - self.fake_eseries_pool_label, - self.fake_eseries_volume_label, - self.fake_size_gb) - log_info.assert_called_once_with("Created volume with label %s.", - self.fake_eseries_volume_label) - self.assertEqual('CorrectVolume', storage_vol) - - @mock.patch.object(client.RestClient, 'list_storage_pools') - @mock.patch.object(client.RestClient, 'create_volume', - mock.MagicMock( - side_effect=exception.NetAppDriverException)) - @mock.patch.object(library.LOG, 'info', mock.Mock()) - def test_create_volume_check_exception(self, fake_list_pools): - fake_pool = {} - fake_pool['label'] = self.fake_eseries_pool_label - fake_pool['volumeGroupRef'] = 'foo' - fake_pool['raidLevel'] = 'raidDiskPool' - fake_pools = [fake_pool] - fake_list_pools.return_value = fake_pools - self.assertRaises(exception.NetAppDriverException, - self.library._create_volume, - self.fake_eseries_pool_label, - self.fake_eseries_volume_label, self.fake_size_gb) - - def test_portal_for_vol_controller(self): - volume = {'id': 'vol_id', 'currentManager': 'ctrl1'} - vol_nomatch = {'id': 'vol_id', 'currentManager': 'ctrl3'} - portals = [{'controller': 'ctrl2', 'iqn': 'iqn2'}, - {'controller': 'ctrl1', 'iqn': 'iqn1'}] - portal = self.library._get_iscsi_portal_for_vol(volume, portals) - self.assertEqual({'controller': 'ctrl1', 'iqn': 'iqn1'}, portal) - portal = self.library._get_iscsi_portal_for_vol(vol_nomatch, portals) - self.assertEqual({'controller': 'ctrl2', 'iqn': 'iqn2'}, portal) - - def test_portal_for_vol_any_false(self): - vol_nomatch = {'id': 'vol_id', 'currentManager': 'ctrl3'} - portals = [{'controller': 'ctrl2', 'iqn': 'iqn2'}, - {'controller': 'ctrl1', 'iqn': 'iqn1'}] - self.assertRaises(exception.NetAppDriverException, - self.library._get_iscsi_portal_for_vol, - vol_nomatch, portals, False) - - def test_do_setup_all_default(self): - configuration = self._set_config(self.create_configuration()) - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system = mock.Mock() - mock_invoke = self.mock_object(client, 'RestClient') - driver.do_setup(context='context') - mock_invoke.assert_called_with(**fakes.FAKE_CLIENT_PARAMS) - - def test_do_setup_http_default_port(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_transport_type = 'http' - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system = mock.Mock() - mock_invoke = self.mock_object(client, 'RestClient') - driver.do_setup(context='context') - mock_invoke.assert_called_with(**fakes.FAKE_CLIENT_PARAMS) - - def test_do_setup_https_default_port(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_transport_type = 'https' - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system = mock.Mock() - mock_invoke = self.mock_object(client, 'RestClient') - driver.do_setup(context='context') - FAKE_EXPECTED_PARAMS = dict(fakes.FAKE_CLIENT_PARAMS, port=8443, - scheme='https') - mock_invoke.assert_called_with(**FAKE_EXPECTED_PARAMS) - - def test_do_setup_http_non_default_port(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_server_port = 81 - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system = mock.Mock() - mock_invoke = self.mock_object(client, 'RestClient') - driver.do_setup(context='context') - FAKE_EXPECTED_PARAMS = dict(fakes.FAKE_CLIENT_PARAMS, port=81) - mock_invoke.assert_called_with(**FAKE_EXPECTED_PARAMS) - - def test_do_setup_https_non_default_port(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_transport_type = 'https' - configuration.netapp_server_port = 446 - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system = mock.Mock() - mock_invoke = self.mock_object(client, 'RestClient') - driver.do_setup(context='context') - FAKE_EXPECTED_PARAMS = dict(fakes.FAKE_CLIENT_PARAMS, port=446, - scheme='https') - mock_invoke.assert_called_with(**FAKE_EXPECTED_PARAMS) - - def test_setup_good_controller_ip(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '127.0.0.1' - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system - - def test_setup_good_controller_ips(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '127.0.0.2,127.0.0.1' - driver = common.NetAppDriver(configuration=configuration) - driver.library._check_mode_get_or_register_storage_system - - def test_setup_missing_controller_ip(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = None - driver = common.NetAppDriver(configuration=configuration) - self.assertRaises(exception.InvalidInput, - driver.do_setup, context='context') - - def test_setup_error_invalid_controller_ip(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '987.65.43.21' - driver = common.NetAppDriver(configuration=configuration) - self.mock_object(cinder_utils, 'resolve_hostname', - side_effect=socket.gaierror) - - self.assertRaises( - exception.NoValidBackend, - driver.library._check_mode_get_or_register_storage_system) - - def test_setup_error_invalid_first_controller_ip(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '987.65.43.21,127.0.0.1' - driver = common.NetAppDriver(configuration=configuration) - self.mock_object(cinder_utils, 'resolve_hostname', - side_effect=socket.gaierror) - - self.assertRaises( - exception.NoValidBackend, - driver.library._check_mode_get_or_register_storage_system) - - def test_setup_error_invalid_second_controller_ip(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '127.0.0.1,987.65.43.21' - driver = common.NetAppDriver(configuration=configuration) - self.mock_object(cinder_utils, 'resolve_hostname', - side_effect=socket.gaierror) - - self.assertRaises( - exception.NoValidBackend, - driver.library._check_mode_get_or_register_storage_system) - - def test_setup_error_invalid_both_controller_ips(self): - configuration = self._set_config(self.create_configuration()) - configuration.netapp_controller_ips = '564.124.1231.1,987.65.43.21' - driver = common.NetAppDriver(configuration=configuration) - self.mock_object(cinder_utils, 'resolve_hostname', - side_effect=socket.gaierror) - - self.assertRaises( - exception.NoValidBackend, - driver.library._check_mode_get_or_register_storage_system) - - def test_manage_existing_get_size(self): - self.library._get_existing_vol_with_manage_ref = mock.Mock( - return_value=self.fake_ret_vol) - size = self.driver.manage_existing_get_size(self.volume, self.fake_ref) - self.assertEqual(3, size) - self.library._get_existing_vol_with_manage_ref.assert_called_once_with( - self.fake_ref) - - def test_get_exist_vol_source_name_missing(self): - self.library._client.list_volume = mock.Mock( - side_effect=exception.InvalidInput) - self.assertRaises(exception.ManageExistingInvalidReference, - self.library._get_existing_vol_with_manage_ref, - {'id': '1234'}) - - @ddt.data('source-id', 'source-name') - def test_get_exist_vol_source_not_found(self, attr_name): - def _get_volume(v_id): - d = {'id': '1', 'name': 'volume1', 'worldWideName': '0'} - if v_id in d: - return d[v_id] - else: - raise exception.VolumeNotFound(message=v_id) - - self.library._client.list_volume = mock.Mock(wraps=_get_volume) - self.assertRaises(exception.ManageExistingInvalidReference, - self.library._get_existing_vol_with_manage_ref, - {attr_name: 'name2'}) - - self.library._client.list_volume.assert_called_once_with( - 'name2') - - def test_get_exist_vol_with_manage_ref(self): - fake_ret_vol = {'id': 'right'} - self.library._client.list_volume = mock.Mock(return_value=fake_ret_vol) - - actual_vol = self.library._get_existing_vol_with_manage_ref( - {'source-name': 'name2'}) - - self.library._client.list_volume.assert_called_once_with('name2') - self.assertEqual(fake_ret_vol, actual_vol) - - @mock.patch.object(utils, 'convert_uuid_to_es_fmt') - def test_manage_existing_same_label(self, mock_convert_es_fmt): - self.library._get_existing_vol_with_manage_ref = mock.Mock( - return_value=self.fake_ret_vol) - mock_convert_es_fmt.return_value = 'label' - self.driver.manage_existing(self.volume, self.fake_ref) - self.library._get_existing_vol_with_manage_ref.assert_called_once_with( - self.fake_ref) - mock_convert_es_fmt.assert_called_once_with( - '114774fb-e15a-4fae-8ee2-c9723e3645ef') - - @mock.patch.object(utils, 'convert_uuid_to_es_fmt') - def test_manage_existing_new(self, mock_convert_es_fmt): - self.library._get_existing_vol_with_manage_ref = mock.Mock( - return_value=self.fake_ret_vol) - mock_convert_es_fmt.return_value = 'vol_label' - self.library._client.update_volume = mock.Mock( - return_value={'id': 'update', 'worldWideName': 'wwn'}) - self.driver.manage_existing(self.volume, self.fake_ref) - self.library._get_existing_vol_with_manage_ref.assert_called_once_with( - self.fake_ref) - mock_convert_es_fmt.assert_called_once_with( - '114774fb-e15a-4fae-8ee2-c9723e3645ef') - self.library._client.update_volume.assert_called_once_with( - 'vol_id', 'vol_label') - - @mock.patch.object(library.LOG, 'info') - def test_unmanage(self, log_info): - self.library._get_volume = mock.Mock(return_value=self.fake_ret_vol) - self.driver.unmanage(self.volume) - self.library._get_volume.assert_called_once_with( - '114774fb-e15a-4fae-8ee2-c9723e3645ef') - self.assertEqual(1, log_info.call_count) - - @mock.patch.object(library.NetAppESeriesLibrary, 'ensure_export', - mock.Mock()) - def test_ensure_export(self): - self.driver.ensure_export('context', self.fake_ret_vol) - self.assertTrue(self.library.ensure_export.called) - - @mock.patch.object(library.NetAppESeriesLibrary, 'extend_volume', - mock.Mock()) - def test_extend_volume(self): - capacity = 10 - self.driver.extend_volume(self.fake_ret_vol, capacity) - self.library.extend_volume.assert_called_with(self.fake_ret_vol, - capacity) - - @mock.patch.object(library.NetAppESeriesLibrary, - 'create_cgsnapshot', mock.Mock()) - def test_create_cgsnapshot(self): - cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) - snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE]) - - self.driver.create_cgsnapshot('ctx', cgsnapshot, snapshots) - - self.library.create_cgsnapshot.assert_called_with(cgsnapshot, - snapshots) - - @mock.patch.object(library.NetAppESeriesLibrary, - 'delete_cgsnapshot', mock.Mock()) - def test_delete_cgsnapshot(self): - cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) - snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE]) - - self.driver.delete_cgsnapshot('ctx', cgsnapshot, snapshots) - - self.library.delete_cgsnapshot.assert_called_with(cgsnapshot, - snapshots) - - @mock.patch.object(library.NetAppESeriesLibrary, - 'create_consistencygroup', mock.Mock()) - def test_create_consistencygroup(self): - cg = copy.deepcopy(fakes.FAKE_CINDER_CG) - - self.driver.create_consistencygroup('ctx', cg) - - self.library.create_consistencygroup.assert_called_with(cg) - - @mock.patch.object(library.NetAppESeriesLibrary, - 'delete_consistencygroup', mock.Mock()) - def test_delete_consistencygroup(self): - cg = copy.deepcopy(fakes.FAKE_CINDER_CG) - volumes = copy.deepcopy([fakes.VOLUME]) - - self.driver.delete_consistencygroup('ctx', cg, volumes) - - self.library.delete_consistencygroup.assert_called_with(cg, volumes) - - @mock.patch.object(library.NetAppESeriesLibrary, - 'update_consistencygroup', mock.Mock()) - def test_update_consistencygroup(self): - group = copy.deepcopy(fakes.FAKE_CINDER_CG) - - self.driver.update_consistencygroup('ctx', group, {}, {}) - - self.library.update_consistencygroup.assert_called_with(group, {}, {}) - - @mock.patch.object(library.NetAppESeriesLibrary, - 'create_consistencygroup_from_src', mock.Mock()) - def test_create_consistencygroup_from_src(self): - cg = copy.deepcopy(fakes.FAKE_CINDER_CG) - volumes = copy.deepcopy([fakes.VOLUME]) - source_vols = copy.deepcopy([fakes.VOLUME]) - cgsnapshot = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) - source_cg = copy.deepcopy(fakes.FAKE_CINDER_CG_SNAPSHOT) - snapshots = copy.deepcopy([fakes.SNAPSHOT_IMAGE]) - - self.driver.create_consistencygroup_from_src( - 'ctx', cg, volumes, cgsnapshot, snapshots, source_cg, - source_vols) - - self.library.create_consistencygroup_from_src.assert_called_with( - cg, volumes, cgsnapshot, snapshots, source_cg, source_vols) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py deleted file mode 100644 index 7a8c8a2c3d8..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_fc_driver.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2015 Alex Meade -# Copyright (c) 2015 Yogesh Kshirsagar -# 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. - -import mock - -from cinder import test -from cinder.tests.unit.volume.drivers.netapp.eseries import test_driver -import cinder.volume.drivers.netapp.eseries.fc_driver as fc -from cinder.volume.drivers.netapp import utils as na_utils - - -class NetAppESeriesFibreChannelDriverTestCase(test_driver - .NetAppESeriesDriverTestCase, - test.TestCase): - - PROTOCOL = 'fc' - - @mock.patch.object(na_utils, 'validate_instantiation') - def test_instantiation(self, mock_validate_instantiation): - fc.NetAppEseriesFibreChannelDriver(configuration=mock.Mock()) - - self.assertTrue(mock_validate_instantiation.called) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py deleted file mode 100644 index 096df6a516a..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_host_mapper.py +++ /dev/null @@ -1,662 +0,0 @@ -# Copyright (c) 2015 Alex Meade. All rights reserved. -# 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. -"""Mock unit tests for the NetApp E-series iscsi driver.""" - -import copy - -import mock -import six - -from cinder import exception -from cinder.objects import fields -from cinder import test -from cinder.tests.unit.volume.drivers.netapp.eseries \ - import fakes as eseries_fakes -from cinder.volume.drivers.netapp.eseries import host_mapper -from cinder.volume.drivers.netapp.eseries import utils - - -def get_fake_volume(): - return { - 'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1, - 'volume_name': 'lun1', 'host': 'hostname@backend#DDP', - 'os_type': 'linux', 'provider_location': 'lun1', - 'name_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', - 'provider_auth': 'provider a b', 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None, 'migration_status': None, 'attach_status': - fields.VolumeAttachStatus.DETACHED, "status": "available" - } - -FAKE_MAPPINGS = [{u'lun': 1}] - -FAKE_USED_UP_MAPPINGS = [{u'lun': n} for n in range(256)] - -FAKE_USED_UP_LUN_ID_DICT = {n: 1 for n in range(256)} - -FAKE_UNUSED_LUN_ID = set([]) - -FAKE_USED_LUN_ID_DICT = ({0: 1, 1: 1}) - -FAKE_USED_LUN_IDS = [1, 2] - -FAKE_SINGLE_USED_LUN_ID = 1 - -FAKE_USED_UP_LUN_IDS = range(256) - - -class NetAppEseriesHostMapperTestCase(test.TestCase): - def setUp(self): - super(NetAppEseriesHostMapperTestCase, self).setUp() - - self.client = eseries_fakes.FakeEseriesClient() - - def test_unmap_volume_from_host_volume_mapped_to_host(self): - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - eseries_fakes.VOLUME_MAPPING - ] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - self.mock_object(self.client, 'delete_volume_mapping') - - host_mapper.unmap_volume_from_host(self.client, get_fake_volume(), - eseries_fakes.HOST, - eseries_fakes.VOLUME_MAPPING) - - self.assertTrue(self.client.delete_volume_mapping.called) - - def test_unmap_volume_from_host_volume_mapped_to_different_host(self): - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - # Mapped to host 1 - fake_eseries_volume['listOfMappings'] = [ - eseries_fakes.VOLUME_MAPPING - ] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - self.mock_object(self.client, 'delete_volume_mapping') - self.mock_object(self.client, 'get_host_group', - side_effect=exception.NotFound) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.unmap_volume_from_host, - self.client, get_fake_volume(), - eseries_fakes.HOST_2, - eseries_fakes.VOLUME_MAPPING) - self.assertIn("not currently mapped to host", six.text_type(err)) - - def test_unmap_volume_from_host_volume_mapped_to_host_group_but_not_host( - self): - """Test volume mapped to host not in specified host group. - - Ensure an error is raised if the specified host is not in the - host group the volume is mapped to. - """ - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_volume_mapping['mapRef'] = eseries_fakes.MULTIATTACH_HOST_GROUP[ - 'clusterRef'] - fake_eseries_volume['listOfMappings'] = [fake_volume_mapping] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - fake_host = copy.deepcopy(eseries_fakes.HOST) - fake_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'list_hosts', - return_value=[fake_host]) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.unmap_volume_from_host, - self.client, get_fake_volume(), - fake_host, - fake_volume_mapping) - self.assertIn("not currently mapped to host", six.text_type(err)) - - def test_unmap_volume_from_host_volume_mapped_to_multiattach_host_group( - self): - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_volume_mapping['mapRef'] = eseries_fakes.MULTIATTACH_HOST_GROUP[ - 'clusterRef'] - fake_eseries_volume['listOfMappings'] = [fake_volume_mapping] - self.mock_object(self.client, 'delete_volume_mapping') - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - fake_volume = get_fake_volume() - fake_volume['status'] = 'detaching' - - host_mapper.unmap_volume_from_host(self.client, fake_volume, - eseries_fakes.HOST, - fake_volume_mapping) - - self.assertTrue(self.client.delete_volume_mapping.called) - - def test_unmap_volume_from_host_volume_mapped_to_multiattach_host_group_and_migrating( # noqa - self): - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_volume_mapping['mapRef'] = eseries_fakes.MULTIATTACH_HOST_GROUP[ - 'clusterRef'] - fake_eseries_volume['listOfMappings'] = [fake_volume_mapping] - self.mock_object(self.client, 'delete_volume_mapping') - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - fake_volume = get_fake_volume() - fake_volume['status'] = 'in-use' - - host_mapper.unmap_volume_from_host(self.client, fake_volume, - eseries_fakes.HOST, - fake_volume_mapping) - - self.assertFalse(self.client.delete_volume_mapping.called) - - def test_unmap_volume_from_host_volume_mapped_to_outside_host_group(self): - """Test volume mapped to host group without host. - - Ensure we raise error when we find a volume is mapped to an unknown - host group that does not have the host. - """ - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_ref = "8500000060080E500023C7340036035F515B78FD" - fake_volume_mapping['mapRef'] = fake_ref - fake_eseries_volume['listOfMappings'] = [fake_volume_mapping] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - fake_host = copy.deepcopy(eseries_fakes.HOST) - fake_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'list_hosts', - return_value=[fake_host]) - self.mock_object(self.client, 'get_host_group', - return_value=eseries_fakes.FOREIGN_HOST_GROUP) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.unmap_volume_from_host, - self.client, get_fake_volume(), - eseries_fakes.HOST, - fake_volume_mapping) - self.assertIn("unsupported host group", six.text_type(err)) - - def test_unmap_volume_from_host_volume_mapped_to_outside_host_group_w_host( - self): - """Test volume mapped to host in unknown host group. - - Ensure we raise error when we find a volume is mapped to an unknown - host group that has the host. - """ - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_ref = "8500000060080E500023C7340036035F515B78FD" - fake_volume_mapping['mapRef'] = fake_ref - fake_eseries_volume['clusterRef'] = fake_ref - fake_eseries_volume['listOfMappings'] = [fake_volume_mapping] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - fake_host = copy.deepcopy(eseries_fakes.HOST) - fake_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'list_hosts', return_value=[fake_host]) - self.mock_object(self.client, 'get_host_group', - return_value=eseries_fakes.FOREIGN_HOST_GROUP) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.unmap_volume_from_host, - self.client, get_fake_volume(), - eseries_fakes.HOST, - fake_volume_mapping) - - self.assertIn("unsupported host group", six.text_type(err)) - - def test_map_volume_to_single_host_volume_not_mapped(self): - self.mock_object(self.client, 'create_volume_mapping', - return_value=eseries_fakes.VOLUME_MAPPING) - - host_mapper.map_volume_to_single_host(self.client, get_fake_volume(), - eseries_fakes.VOLUME, - eseries_fakes.HOST, - None, - False) - - self.assertTrue(self.client.create_volume_mapping.called) - - def test_map_volume_to_single_host_volume_already_mapped_to_target_host( - self): - """Should be a no-op""" - self.mock_object(self.client, 'create_volume_mapping') - - host_mapper.map_volume_to_single_host(self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - eseries_fakes.HOST, - eseries_fakes.VOLUME_MAPPING, - False) - - self.assertFalse(self.client.create_volume_mapping.called) - - def test_map_volume_to_single_host_volume_mapped_to_multiattach_host_group( - self): - """Test map volume to a single host. - - Should move mapping to target host if volume is not migrating or - attached(in-use). If volume is not in use then it should not require a - mapping making it ok to sever the mapping to the host group. - """ - fake_mapping_to_other_host = copy.deepcopy( - eseries_fakes.VOLUME_MAPPING) - fake_mapping_to_other_host['mapRef'] = \ - eseries_fakes.MULTIATTACH_HOST_GROUP['clusterRef'] - self.mock_object(self.client, 'move_volume_mapping_via_symbol', - return_value={'lun': 5}) - - host_mapper.map_volume_to_single_host(self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - eseries_fakes.HOST, - fake_mapping_to_other_host, - False) - - self.assertTrue(self.client.move_volume_mapping_via_symbol.called) - - def test_map_volume_to_single_host_volume_mapped_to_multiattach_host_group_and_migrating( # noqa - self): - """Should raise error saying multiattach not enabled""" - fake_mapping_to_other_host = copy.deepcopy( - eseries_fakes.VOLUME_MAPPING) - fake_mapping_to_other_host['mapRef'] = \ - eseries_fakes.MULTIATTACH_HOST_GROUP['clusterRef'] - fake_volume = get_fake_volume() - fake_volume['attach_status'] = fields.VolumeAttachStatus.ATTACHED - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_single_host, - self.client, fake_volume, - eseries_fakes.VOLUME, - eseries_fakes.HOST, - fake_mapping_to_other_host, - False) - - self.assertIn('multiattach is disabled', six.text_type(err)) - - def test_map_volume_to_single_host_volume_mapped_to_multiattach_host_group_and_attached( # noqa - self): - """Should raise error saying multiattach not enabled""" - fake_mapping_to_other_host = copy.deepcopy( - eseries_fakes.VOLUME_MAPPING) - fake_mapping_to_other_host['mapRef'] = \ - eseries_fakes.MULTIATTACH_HOST_GROUP['clusterRef'] - fake_volume = get_fake_volume() - fake_volume['attach_status'] = fields.VolumeAttachStatus.ATTACHED - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_single_host, - self.client, fake_volume, - eseries_fakes.VOLUME, - eseries_fakes.HOST, - fake_mapping_to_other_host, - False) - - self.assertIn('multiattach is disabled', six.text_type(err)) - - def test_map_volume_to_single_host_volume_mapped_to_another_host(self): - """Should raise error saying multiattach not enabled""" - fake_mapping_to_other_host = copy.deepcopy( - eseries_fakes.VOLUME_MAPPING) - fake_mapping_to_other_host['mapRef'] = eseries_fakes.HOST_2[ - 'hostRef'] - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_single_host, - self.client, get_fake_volume(), - eseries_fakes.VOLUME, - eseries_fakes.HOST, - fake_mapping_to_other_host, - False) - - self.assertIn('multiattach is disabled', six.text_type(err)) - - def test_map_volume_to_multiple_hosts_volume_already_mapped_to_target_host( - self): - """Should be a no-op.""" - self.mock_object(self.client, 'create_volume_mapping') - - host_mapper.map_volume_to_multiple_hosts(self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - eseries_fakes.HOST, - eseries_fakes.VOLUME_MAPPING) - - self.assertFalse(self.client.create_volume_mapping.called) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_multiattach_host_group( # noqa - self): - """Should ensure target host is in the multiattach host group.""" - fake_host = copy.deepcopy(eseries_fakes.HOST_2) - fake_host['clusterRef'] = utils.NULL_REF - - fake_mapping_to_host_group = copy.deepcopy( - eseries_fakes.VOLUME_MAPPING) - fake_mapping_to_host_group['mapRef'] = \ - eseries_fakes.MULTIATTACH_HOST_GROUP['clusterRef'] - - self.mock_object(self.client, 'set_host_group_for_host') - self.mock_object(self.client, 'get_host_group', - return_value=eseries_fakes.MULTIATTACH_HOST_GROUP) - - host_mapper.map_volume_to_multiple_hosts(self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - fake_host, - fake_mapping_to_host_group) - - self.assertEqual( - 1, self.client.set_host_group_for_host.call_count) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_multiattach_host_group_with_lun_collision( # noqa - self): - """Should ensure target host is in the multiattach host group.""" - fake_host = copy.deepcopy(eseries_fakes.HOST_2) - fake_host['clusterRef'] = utils.NULL_REF - fake_mapping_to_host_group = copy.deepcopy( - eseries_fakes.VOLUME_MAPPING) - fake_mapping_to_host_group['mapRef'] = \ - eseries_fakes.MULTIATTACH_HOST_GROUP['clusterRef'] - self.mock_object(self.client, 'set_host_group_for_host', - side_effect=exception.NetAppDriverException) - - self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_multiple_hosts, - self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - fake_host, - fake_mapping_to_host_group) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_another_host(self): - """Test that mapping moves to another host group. - - Should ensure both existing host and destination host are in - multiattach host group and move the mapping to the host group. - """ - - existing_host = copy.deepcopy(eseries_fakes.HOST) - existing_host['clusterRef'] = utils.NULL_REF - target_host = copy.deepcopy(eseries_fakes.HOST_2) - target_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'get_host', return_value=existing_host) - self.mock_object(self.client, 'set_host_group_for_host') - self.mock_object(self.client, 'get_host_group', - side_effect=exception.NotFound) - mock_move_mapping = mock.Mock( - return_value=eseries_fakes.VOLUME_MAPPING_TO_MULTIATTACH_GROUP) - self.mock_object(self.client, - 'move_volume_mapping_via_symbol', - mock_move_mapping) - - host_mapper.map_volume_to_multiple_hosts(self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - target_host, - eseries_fakes.VOLUME_MAPPING) - - self.assertEqual( - 2, self.client.set_host_group_for_host.call_count) - - self.assertTrue(self.client.move_volume_mapping_via_symbol - .called) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_another_host_with_lun_collision_with_source_host( # noqa - self): - """Test moving source host to multiattach host group. - - Should fail attempting to move source host to multiattach host - group and raise an error. - """ - - existing_host = copy.deepcopy(eseries_fakes.HOST) - existing_host['clusterRef'] = utils.NULL_REF - target_host = copy.deepcopy(eseries_fakes.HOST_2) - target_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'get_host', return_value=existing_host) - self.mock_object(self.client, 'set_host_group_for_host', - side_effect=[None, exception.NetAppDriverException]) - self.mock_object(self.client, 'get_host_group', - side_effect=exception.NotFound) - mock_move_mapping = mock.Mock( - return_value=eseries_fakes.VOLUME_MAPPING_TO_MULTIATTACH_GROUP) - self.mock_object(self.client, - 'move_volume_mapping_via_symbol', - mock_move_mapping) - - self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_multiple_hosts, - self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - target_host, - eseries_fakes.VOLUME_MAPPING) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_another_host_with_lun_collision_with_dest_host( # noqa - self): - """Test moving destination host to multiattach host group. - - Should fail attempting to move destination host to multiattach host - group and raise an error. - """ - - existing_host = copy.deepcopy(eseries_fakes.HOST) - existing_host['clusterRef'] = utils.NULL_REF - target_host = copy.deepcopy(eseries_fakes.HOST_2) - target_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'get_host', return_value=existing_host) - self.mock_object(self.client, 'set_host_group_for_host', - side_effect=[exception.NetAppDriverException, None]) - self.mock_object(self.client, 'get_host_group', - side_effect=exception.NotFound) - mock_move_mapping = mock.Mock( - return_value=eseries_fakes.VOLUME_MAPPING_TO_MULTIATTACH_GROUP) - self.mock_object(self.client, - 'move_volume_mapping_via_symbol', - mock_move_mapping) - - self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_multiple_hosts, - self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - target_host, - eseries_fakes.VOLUME_MAPPING) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_foreign_host_group( - self): - """Test a target when the host is in a foreign host group. - - Should raise an error stating the volume is mapped to an - unsupported host group. - """ - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_ref = "8500000060080E500023C7340036035F515B78FD" - fake_volume_mapping['mapRef'] = fake_ref - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - fake_host = copy.deepcopy(eseries_fakes.HOST) - fake_host['clusterRef'] = utils.NULL_REF - self.mock_object(self.client, 'get_host_group', - return_value=eseries_fakes.FOREIGN_HOST_GROUP) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_multiple_hosts, - self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - fake_host, - fake_volume_mapping) - self.assertIn("unsupported host group", six.text_type(err)) - - def test_map_volume_to_multiple_hosts_volume_mapped_to_host_in_foreign_host_group( # noqa - self): - """Test a target when the host is in a foreign host group. - - Should raise an error stating the volume is mapped to a - host that is in an unsupported host group. - """ - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_host = copy.deepcopy(eseries_fakes.HOST_2) - fake_host['clusterRef'] = eseries_fakes.FOREIGN_HOST_GROUP[ - 'clusterRef'] - fake_volume_mapping['mapRef'] = fake_host['hostRef'] - fake_eseries_volume['listOfMappings'] = [fake_volume_mapping] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - self.mock_object(self.client, 'get_host', return_value=fake_host) - self.mock_object(self.client, 'get_host_group', - side_effect=[eseries_fakes.FOREIGN_HOST_GROUP]) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_multiple_hosts, - self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - eseries_fakes.HOST, - fake_volume_mapping) - - self.assertIn("unsupported host group", six.text_type(err)) - - def test_map_volume_to_multiple_hosts_volume_target_host_in_foreign_host_group( # noqa - self): - """Test a target when the host is in a foreign host group. - - Should raise an error stating the target host is in an - unsupported host group. - """ - fake_eseries_volume = copy.deepcopy(eseries_fakes.VOLUME) - fake_volume_mapping = copy.deepcopy(eseries_fakes.VOLUME_MAPPING) - fake_host = copy.deepcopy(eseries_fakes.HOST_2) - fake_host['clusterRef'] = eseries_fakes.FOREIGN_HOST_GROUP[ - 'clusterRef'] - self.mock_object(self.client, 'list_volumes', - return_value=[fake_eseries_volume]) - self.mock_object(self.client, 'get_host', - return_value=eseries_fakes.HOST) - self.mock_object(self.client, 'get_host_group', - side_effect=[eseries_fakes.FOREIGN_HOST_GROUP]) - - err = self.assertRaises(exception.NetAppDriverException, - host_mapper.map_volume_to_multiple_hosts, - self.client, - get_fake_volume(), - eseries_fakes.VOLUME, - fake_host, - fake_volume_mapping) - - self.assertIn("unsupported host group", six.text_type(err)) - - def test_get_unused_lun_ids(self): - unused_lun_ids = host_mapper._get_unused_lun_ids(FAKE_MAPPINGS) - self.assertEqual(set(range(2, 256)), unused_lun_ids) - - def test_get_unused_lun_id_counter(self): - used_lun_id_count = host_mapper._get_used_lun_id_counter( - FAKE_MAPPINGS) - self.assertEqual(FAKE_USED_LUN_ID_DICT, used_lun_id_count) - - def test_get_unused_lun_ids_used_up_luns(self): - unused_lun_ids = host_mapper._get_unused_lun_ids( - FAKE_USED_UP_MAPPINGS) - self.assertEqual(FAKE_UNUSED_LUN_ID, unused_lun_ids) - - def test_get_lun_id_counter_used_up_luns(self): - used_lun_ids = host_mapper._get_used_lun_id_counter( - FAKE_USED_UP_MAPPINGS) - self.assertEqual(FAKE_USED_UP_LUN_ID_DICT, used_lun_ids) - - def test_host_not_full(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - self.assertFalse(host_mapper._is_host_full(self.client, fake_host)) - - def test_host_full(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - self.mock_object(self.client, 'get_volume_mappings_for_host', - return_value=FAKE_USED_UP_MAPPINGS) - self.assertTrue(host_mapper._is_host_full(self.client, fake_host)) - - def test_get_free_lun(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - with mock.patch('random.sample') as mock_random: - mock_random.return_value = [3] - lun = host_mapper._get_free_lun(self.client, fake_host, False, - []) - self.assertEqual(3, lun) - - def test_get_free_lun_host_full(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - self.mock_object(host_mapper, '_is_host_full', return_value=True) - self.assertRaises( - exception.NetAppDriverException, - host_mapper._get_free_lun, - self.client, fake_host, False, FAKE_USED_UP_MAPPINGS) - - def test_get_free_lun_no_unused_luns(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - lun = host_mapper._get_free_lun(self.client, fake_host, False, - FAKE_USED_UP_MAPPINGS) - self.assertEqual(255, lun) - - def test_get_free_lun_no_unused_luns_host_not_full(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - self.mock_object(host_mapper, '_is_host_full', return_value=False) - lun = host_mapper._get_free_lun(self.client, fake_host, False, - FAKE_USED_UP_MAPPINGS) - self.assertEqual(255, lun) - - def test_get_free_lun_no_lun_available(self): - fake_host = copy.deepcopy(eseries_fakes.HOST_3) - self.mock_object(self.client, 'get_volume_mappings_for_host', - return_value=FAKE_USED_UP_MAPPINGS) - - self.assertRaises(exception.NetAppDriverException, - host_mapper._get_free_lun, - self.client, fake_host, False, - FAKE_USED_UP_MAPPINGS) - - def test_get_free_lun_multiattach_enabled_no_unused_ids(self): - fake_host = copy.deepcopy(eseries_fakes.HOST_3) - self.mock_object(self.client, 'get_volume_mappings', - return_value=FAKE_USED_UP_MAPPINGS) - - self.assertRaises(exception.NetAppDriverException, - host_mapper._get_free_lun, - self.client, fake_host, True, - FAKE_USED_UP_MAPPINGS) - - def test_get_lun_by_mapping(self): - used_luns = host_mapper._get_used_lun_ids_for_mappings(FAKE_MAPPINGS) - self.assertEqual(set([0, 1]), used_luns) - - def test_get_lun_by_mapping_no_mapping(self): - used_luns = host_mapper._get_used_lun_ids_for_mappings([]) - self.assertEqual(set([0]), used_luns) - - def test_lun_id_available_on_host(self): - fake_host = copy.deepcopy(eseries_fakes.HOST) - self.assertTrue(host_mapper._is_lun_id_available_on_host( - self.client, fake_host, FAKE_UNUSED_LUN_ID)) - - def test_no_lun_id_available_on_host(self): - fake_host = copy.deepcopy(eseries_fakes.HOST_3) - self.mock_object(self.client, 'get_volume_mappings_for_host', - return_value=FAKE_USED_UP_MAPPINGS) - - self.assertFalse(host_mapper._is_lun_id_available_on_host( - self.client, fake_host, FAKE_SINGLE_USED_LUN_ID)) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_iscsi_driver.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_iscsi_driver.py deleted file mode 100644 index 213de960399..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_iscsi_driver.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2015 Alex Meade. All rights reserved. -# Copyright (c) 2015 Michael Price. All rights reserved. -# 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. - -import ddt -import mock - -from cinder import test - -from cinder.tests.unit.volume.drivers.netapp.eseries import test_driver -from cinder.volume.drivers.netapp.eseries import iscsi_driver as iscsi -import cinder.volume.drivers.netapp.utils as na_utils - - -@ddt.ddt -class NetAppESeriesIscsiDriverTestCase(test_driver.NetAppESeriesDriverTestCase, - test.TestCase): - - @mock.patch.object(na_utils, 'validate_instantiation') - def test_instantiation(self, mock_validate_instantiation): - iscsi.NetAppEseriesISCSIDriver(configuration=mock.Mock()) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py deleted file mode 100644 index 10a41ae4a24..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py +++ /dev/null @@ -1,2570 +0,0 @@ -# Copyright (c) 2014 Andrew Kerr -# Copyright (c) 2015 Alex Meade -# Copyright (c) 2015 Rushil Chugh -# Copyright (c) 2015 Yogesh Kshirsagar -# Copyright (c) 2015 Michael Price -# 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. - -import copy -import ddt -import time -import uuid - -import mock -from oslo_utils import units -import six -from six.moves import range -from six.moves import reduce - -from cinder import context -from cinder import exception -from cinder.objects import fields -from cinder import test - -from cinder.tests.unit import fake_snapshot -from cinder.tests.unit import utils as cinder_utils -from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \ - eseries_fake -from cinder.volume.drivers.netapp.eseries import client as es_client -from cinder.volume.drivers.netapp.eseries import exception as eseries_exc -from cinder.volume.drivers.netapp.eseries import host_mapper -from cinder.volume.drivers.netapp.eseries import library -from cinder.volume.drivers.netapp.eseries import utils -from cinder.volume.drivers.netapp import utils as na_utils -from cinder.volume import utils as volume_utils -from cinder.zonemanager import utils as fczm_utils - - -def get_fake_volume(): - """Return a fake Cinder Volume that can be used as a parameter""" - return { - 'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1, - 'volume_name': 'lun1', 'host': 'hostname@backend#DDP', - 'os_type': 'linux', 'provider_location': 'lun1', - 'name_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', - 'provider_auth': 'provider a b', 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None, 'migration_status': None, 'attach_status': - fields.VolumeAttachStatus.DETACHED - } - - -@ddt.ddt -class NetAppEseriesLibraryTestCase(test.TestCase): - def setUp(self): - super(NetAppEseriesLibraryTestCase, self).setUp() - - kwargs = {'configuration': - eseries_fake.create_configuration_eseries()} - - self.library = library.NetAppESeriesLibrary('FAKE', **kwargs) - - # We don't want the looping calls to run - self.mock_object(self.library, '_start_periodic_tasks') - # Deprecated Option - self.library.configuration.netapp_storage_pools = None - self.library._client = eseries_fake.FakeEseriesClient() - - self.mock_object(self.library, '_start_periodic_tasks') - - self.mock_object(library.cinder_utils, 'synchronized', - return_value=lambda f: f) - - with mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', - new = cinder_utils.ZeroIntervalLoopingCall): - self.library.check_for_setup_error() - - self.ctxt = context.get_admin_context() - - def test_do_setup(self): - self.mock_object(self.library, - '_check_mode_get_or_register_storage_system') - self.mock_object(es_client, 'RestClient', - eseries_fake.FakeEseriesClient) - mock_check_flags = self.mock_object(na_utils, 'check_flags') - self.library.do_setup(mock.Mock()) - - self.assertTrue(mock_check_flags.called) - - @ddt.data('linux_dm_mp', 'linux_atto', 'linux_mpp_rdac', - 'linux_pathmanager', 'linux_sf', 'ontap', 'ontap_rdac', - 'vmware', 'windows_atto', 'windows_clustered', - 'factoryDefault', 'windows', None) - def test_check_host_type(self, host_type): - config = mock.Mock() - default_host_type = self.library.host_type - config.netapp_host_type = host_type - self.mock_object(self.library, 'configuration', config) - - result = self.library._check_host_type() - - self.assertIsNone(result) - if host_type: - self.assertEqual(self.library.HOST_TYPES.get(host_type), - self.library.host_type) - else: - self.assertEqual(default_host_type, self.library.host_type) - - def test_check_host_type_invalid(self): - config = mock.Mock() - config.netapp_host_type = 'invalid' - self.mock_object(self.library, 'configuration', config) - - self.assertRaises(exception.NetAppDriverException, - self.library._check_host_type) - - def test_check_host_type_new(self): - config = mock.Mock() - config.netapp_host_type = 'new_host_type' - expected = 'host_type' - self.mock_object(self.library, 'configuration', config) - host_types = [{ - 'name': 'new_host_type', - 'index': 0, - 'code': expected, - }] - self.mock_object(self.library._client, 'list_host_types', - return_value=host_types) - - result = self.library._check_host_type() - - self.assertIsNone(result) - self.assertEqual(expected, self.library.host_type) - - @ddt.data(('optimal', True), ('offline', False), ('needsAttn', True), - ('neverContacted', False), ('newKey', True), (None, True)) - @ddt.unpack - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_check_storage_system_status(self, status, status_valid): - system = copy.deepcopy(eseries_fake.STORAGE_SYSTEM) - system['status'] = status - status = status.lower() if status is not None else '' - - actual_status, actual_valid = ( - self.library._check_storage_system_status(system)) - - self.assertEqual(status, actual_status) - self.assertEqual(status_valid, actual_valid) - - @ddt.data(('valid', True), ('invalid', False), ('unknown', False), - ('newKey', True), (None, True)) - @ddt.unpack - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_check_password_status(self, status, status_valid): - system = copy.deepcopy(eseries_fake.STORAGE_SYSTEM) - system['passwordStatus'] = status - status = status.lower() if status is not None else '' - - actual_status, actual_valid = ( - self.library._check_password_status(system)) - - self.assertEqual(status, actual_status) - self.assertEqual(status_valid, actual_valid) - - def test_check_storage_system_bad_system(self): - exc_str = "bad_system" - controller_ips = self.library.configuration.netapp_controller_ips - self.library._client.list_storage_system = mock.Mock( - side_effect=exception.NetAppDriverException(message=exc_str)) - info_log = self.mock_object(library.LOG, 'info') - - self.assertRaisesRegex(exception.NetAppDriverException, exc_str, - self.library._check_storage_system) - - info_log.assert_called_once_with(mock.ANY, controller_ips) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_check_storage_system(self): - system = copy.deepcopy(eseries_fake.STORAGE_SYSTEM) - self.mock_object(self.library._client, 'list_storage_system', - return_value=system) - update_password = self.mock_object(self.library._client, - 'update_stored_system_password') - info_log = self.mock_object(library.LOG, 'info') - - self.library._check_storage_system() - - self.assertTrue(update_password.called) - self.assertTrue(info_log.called) - - @ddt.data({'status': 'optimal', 'passwordStatus': 'invalid'}, - {'status': 'offline', 'passwordStatus': 'valid'}) - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_check_storage_system_bad_status(self, system): - self.mock_object(self.library._client, 'list_storage_system', - return_value=system) - self.mock_object(self.library._client, 'update_stored_system_password') - self.mock_object(time, 'time', side_effect=range(0, 60, 5)) - - self.assertRaisesRegex(exception.NetAppDriverException, - 'bad.*?status', - self.library._check_storage_system) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_check_storage_system_update_password(self): - self.library.configuration.netapp_sa_password = 'password' - - def get_system_iter(): - key = 'passwordStatus' - system = copy.deepcopy(eseries_fake.STORAGE_SYSTEM) - system[key] = 'invalid' - yield system - yield system - - system[key] = 'valid' - yield system - - self.mock_object(self.library._client, 'list_storage_system', - side_effect=get_system_iter()) - update_password = self.mock_object(self.library._client, - 'update_stored_system_password') - info_log = self.mock_object(library.LOG, 'info') - - self.library._check_storage_system() - - update_password.assert_called_once_with( - self.library.configuration.netapp_sa_password) - self.assertTrue(info_log.called) - - def test_get_storage_pools_empty_result(self): - """Verify an exception is raised if no pools are returned.""" - self.library.configuration.netapp_pool_name_search_pattern = '$' - - def test_get_storage_pools_invalid_conf(self): - """Verify an exception is raised if the regex pattern is invalid.""" - self.library.configuration.netapp_pool_name_search_pattern = '(.*' - - self.assertRaises(exception.InvalidConfigurationValue, - self.library._get_storage_pools) - - def test_get_storage_pools_default(self): - """Verify that all pools are returned if the search option is empty.""" - filtered_pools = self.library._get_storage_pools() - - self.assertEqual(eseries_fake.STORAGE_POOLS, filtered_pools) - - @ddt.data((r'[\d]+,a', ['1', '2', 'a', 'b'], ['1', '2', 'a']), - ('1 , 3', ['1', '2', '3'], ['1', '3']), - ('$,3', ['1', '2', '3'], ['3']), - ('[a-zA-Z]+', ['1', 'a', 'B'], ['a', 'B']), - ('', ['1', '2'], ['1', '2']) - ) - @ddt.unpack - def test_get_storage_pools(self, pool_filter, pool_labels, - expected_pool_labels): - """Verify that pool filtering via the search_pattern works correctly - - :param pool_filter: A regular expression to be used for filtering via - pool labels - :param pool_labels: A list of pool labels - :param expected_pool_labels: The labels from 'pool_labels' that - should be matched by 'pool_filter' - """ - self.library.configuration.netapp_pool_name_search_pattern = ( - pool_filter) - pools = [{'label': label} for label in pool_labels] - - self.library._client.list_storage_pools = mock.Mock( - return_value=pools) - - filtered_pools = self.library._get_storage_pools() - - filtered_pool_labels = [pool['label'] for pool in filtered_pools] - self.assertEqual(expected_pool_labels, filtered_pool_labels) - - def test_get_volume(self): - fake_volume = copy.deepcopy(get_fake_volume()) - volume = copy.deepcopy(eseries_fake.VOLUME) - self.library._client.list_volume = mock.Mock(return_value=volume) - - result = self.library._get_volume(fake_volume['id']) - - self.assertEqual(1, self.library._client.list_volume.call_count) - self.assertDictEqual(volume, result) - - def test_get_volume_bad_input(self): - volume = copy.deepcopy(eseries_fake.VOLUME) - self.library._client.list_volume = mock.Mock(return_value=volume) - - self.assertRaises(exception.InvalidInput, self.library._get_volume, - None) - - def test_get_volume_bad_uuid(self): - volume = copy.deepcopy(eseries_fake.VOLUME) - self.library._client.list_volume = mock.Mock(return_value=volume) - - self.assertRaises(ValueError, self.library._get_volume, '1') - - def test_update_ssc_info_no_ssc(self): - drives = [{'currentVolumeGroupRef': 'test_vg1', - 'driveMediaType': 'ssd'}] - pools = [{'volumeGroupRef': 'test_vg1', 'label': 'test_vg1', - 'raidLevel': 'raid6', 'securityType': 'enabled'}] - self.library._client = mock.Mock() - self.library._client.features.SSC_API_V2 = na_utils.FeatureState( - False, minimum_version="1.53.9000.1") - self.library._client.SSC_VALID_VERSIONS = [(1, 53, 9000, 1), - (1, 53, 9010, 15)] - self.library.configuration.netapp_pool_name_search_pattern = "test_vg1" - self.library._client.list_storage_pools = mock.Mock(return_value=pools) - self.library._client.list_drives = mock.Mock(return_value=drives) - - self.library._update_ssc_info() - - self.assertEqual( - {'test_vg1': {'netapp_disk_encryption': 'true', - 'netapp_disk_type': 'SSD', - 'netapp_raid_type': 'raid6'}}, - self.library._ssc_stats) - - @ddt.data(True, False) - def test_update_ssc_info(self, data_assurance_supported): - self.library._client = mock.Mock() - self.library._client.features.SSC_API_V2 = na_utils.FeatureState( - True, minimum_version="1.53.9000.1") - self.library._client.list_ssc_storage_pools = mock.Mock( - return_value=eseries_fake.SSC_POOLS) - self.library._get_storage_pools = mock.Mock( - return_value=eseries_fake.STORAGE_POOLS) - # Data Assurance is not supported on some storage backends - self.library._is_data_assurance_supported = mock.Mock( - return_value=data_assurance_supported) - - self.library._update_ssc_info() - - for pool in eseries_fake.SSC_POOLS: - poolId = pool['poolId'] - - raid_lvl = self.library.SSC_RAID_TYPE_MAPPING.get( - pool['raidLevel'], 'unknown') - - if pool['pool']["driveMediaType"] == 'ssd': - disk_type = 'SSD' - else: - disk_type = pool['pool']['drivePhysicalType'] - disk_type = ( - self.library.SSC_DISK_TYPE_MAPPING.get( - disk_type, 'unknown')) - - da_enabled = pool['dataAssuranceCapable'] and ( - data_assurance_supported) - - thin_provisioned = pool['thinProvisioningCapable'] - - expected = { - 'consistencygroup_support': True, - 'netapp_disk_encryption': - six.text_type(pool['encrypted']).lower(), - 'netapp_eseries_flash_read_cache': - six.text_type(pool['flashCacheCapable']).lower(), - 'netapp_thin_provisioned': - six.text_type(thin_provisioned).lower(), - 'netapp_eseries_data_assurance': - six.text_type(da_enabled).lower(), - 'netapp_eseries_disk_spindle_speed': pool['spindleSpeed'], - 'netapp_raid_type': raid_lvl, - 'netapp_disk_type': disk_type - } - actual = self.library._ssc_stats[poolId] - self.assertDictEqual(expected, actual) - - @ddt.data(('FC', True), ('iSCSI', False)) - @ddt.unpack - def test_is_data_assurance_supported(self, backend_storage_protocol, - enabled): - self.mock_object(self.library, 'driver_protocol', - backend_storage_protocol) - - actual = self.library._is_data_assurance_supported() - - self.assertEqual(enabled, actual) - - @ddt.data('scsi', 'fibre', 'sas', 'sata', 'garbage') - def test_update_ssc_disk_types(self, disk_type): - drives = [{'currentVolumeGroupRef': 'test_vg1', - 'interfaceType': {'driveType': disk_type}}] - pools = [{'volumeGroupRef': 'test_vg1'}] - - self.library._client.list_drives = mock.Mock(return_value=drives) - self.library._client.get_storage_pool = mock.Mock(return_value=pools) - - ssc_stats = self.library._update_ssc_disk_types(pools) - - expected = self.library.SSC_DISK_TYPE_MAPPING.get(disk_type, 'unknown') - self.assertEqual({'test_vg1': {'netapp_disk_type': expected}}, - ssc_stats) - - @ddt.data('scsi', 'fibre', 'sas', 'sata', 'garbage') - def test_update_ssc_disk_types_ssd(self, disk_type): - drives = [{'currentVolumeGroupRef': 'test_vg1', - 'driveMediaType': 'ssd', 'driveType': disk_type}] - pools = [{'volumeGroupRef': 'test_vg1'}] - - self.library._client.list_drives = mock.Mock(return_value=drives) - self.library._client.get_storage_pool = mock.Mock(return_value=pools) - - ssc_stats = self.library._update_ssc_disk_types(pools) - - self.assertEqual({'test_vg1': {'netapp_disk_type': 'SSD'}}, - ssc_stats) - - @ddt.data('enabled', 'none', 'capable', 'unknown', '__UNDEFINED', - 'garbage') - def test_update_ssc_disk_encryption(self, securityType): - pools = [{'volumeGroupRef': 'test_vg1', 'securityType': securityType}] - self.library._client.list_storage_pools = mock.Mock(return_value=pools) - - ssc_stats = self.library._update_ssc_disk_encryption(pools) - - # Convert the boolean value to a lower-case string value - expected = 'true' if securityType == "enabled" else 'false' - self.assertEqual({'test_vg1': {'netapp_disk_encryption': expected}}, - ssc_stats) - - def test_update_ssc_disk_encryption_multiple(self): - pools = [{'volumeGroupRef': 'test_vg1', 'securityType': 'none'}, - {'volumeGroupRef': 'test_vg2', 'securityType': 'enabled'}] - self.library._client.list_storage_pools = mock.Mock(return_value=pools) - - ssc_stats = self.library._update_ssc_disk_encryption(pools) - - self.assertEqual({'test_vg1': {'netapp_disk_encryption': 'false'}, - 'test_vg2': {'netapp_disk_encryption': 'true'}}, - ssc_stats) - - @ddt.data(True, False) - def test_get_volume_stats(self, refresh): - fake_stats = {'key': 'val'} - - def populate_stats(): - self.library._stats = fake_stats - - self.library._update_volume_stats = mock.Mock( - side_effect=populate_stats) - self.library._update_ssc_info = mock.Mock() - self.library._ssc_stats = {self.library.THIN_UQ_SPEC: True} - - actual = self.library.get_volume_stats(refresh = refresh) - - if(refresh): - self.library._update_volume_stats.assert_called_once_with() - self.assertEqual(fake_stats, actual) - else: - self.assertEqual(0, self.library._update_volume_stats.call_count) - self.assertEqual(0, self.library._update_ssc_info.call_count) - - def test_get_volume_stats_no_ssc(self): - """Validate that SSC data is collected if not yet populated""" - fake_stats = {'key': 'val'} - - def populate_stats(): - self.library._stats = fake_stats - - self.library._update_volume_stats = mock.Mock( - side_effect=populate_stats) - self.library._update_ssc_info = mock.Mock() - self.library._ssc_stats = None - - actual = self.library.get_volume_stats(refresh = True) - - self.library._update_volume_stats.assert_called_once_with() - self.library._update_ssc_info.assert_called_once_with() - self.assertEqual(fake_stats, actual) - - def test_update_volume_stats_provisioning(self): - """Validate pool capacity calculations""" - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - self.library._get_storage_pools = mock.Mock(return_value=[fake_pool]) - self.mock_object(self.library, '_ssc_stats', - {fake_pool["volumeGroupRef"]: { - self.library.THIN_UQ_SPEC: True}}) - self.library.configuration = mock.Mock() - reserved_pct = 5 - over_subscription_ratio = 1.0 - self.library.configuration.max_over_subscription_ratio = ( - over_subscription_ratio) - self.library.configuration.reserved_percentage = reserved_pct - total_gb = int(fake_pool['totalRaidedSpace']) / units.Gi - used_gb = int(fake_pool['usedSpace']) / units.Gi - free_gb = total_gb - used_gb - provisioned_gb = int(fake_eseries_volume['capacity']) * 10 / units.Gi - - # Testing with 10 fake volumes - self.library._client.list_volumes = mock.Mock( - return_value=[eseries_fake.VOLUME for _ in range(10)]) - - self.library._update_volume_stats() - - self.assertEqual(1, len(self.library._stats['pools'])) - pool_stats = self.library._stats['pools'][0] - self.assertEqual(fake_pool['label'], pool_stats.get('pool_name')) - self.assertEqual(reserved_pct, pool_stats['reserved_percentage']) - self.assertEqual(over_subscription_ratio, - pool_stats['max_over_subscription_ratio']) - self.assertEqual(total_gb, pool_stats.get('total_capacity_gb')) - self.assertEqual(provisioned_gb, - pool_stats.get('provisioned_capacity_gb')) - self.assertEqual(free_gb, pool_stats.get('free_capacity_gb')) - - @ddt.data(False, True) - def test_update_volume_stats_thin_provisioning(self, thin_provisioning): - """Validate that thin provisioning support is correctly reported""" - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - self.library._get_storage_pools = mock.Mock(return_value=[fake_pool]) - self.mock_object(self.library, '_ssc_stats', - {fake_pool["volumeGroupRef"]: { - self.library.THIN_UQ_SPEC: thin_provisioning}}) - - self.library._update_volume_stats() - - self.assertEqual(1, len(self.library._stats['pools'])) - pool_stats = self.library._stats['pools'][0] - self.assertEqual(thin_provisioning, pool_stats.get( - 'thin_provisioning_support')) - # Should always be True - self.assertTrue(pool_stats.get('thick_provisioning_support')) - - def test_update_volume_stats_ssc(self): - """Ensure that the SSC data is correctly reported in the pool stats""" - ssc = {self.library.THIN_UQ_SPEC: True, 'key': 'val'} - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - self.library._get_storage_pools = mock.Mock(return_value=[fake_pool]) - self.mock_object(self.library, '_ssc_stats', - {fake_pool["volumeGroupRef"]: ssc}) - - self.library._update_volume_stats() - - self.assertEqual(1, len(self.library._stats['pools'])) - pool_stats = self.library._stats['pools'][0] - for key in ssc: - self.assertIn(key, pool_stats) - self.assertEqual(ssc[key], pool_stats[key]) - - def test_update_volume_stats_no_ssc(self): - """Ensure that pool stats are correctly reported without SSC""" - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - self.library._get_storage_pools = mock.Mock(return_value=[fake_pool]) - self.library._update_volume_stats() - - self.assertEqual(1, len(self.library._stats['pools'])) - pool_stats = self.library._stats['pools'][0] - self.assertFalse(pool_stats.get('thin_provisioning_support')) - # Should always be True - self.assertTrue(pool_stats.get('thick_provisioning_support')) - - def test_terminate_connection_iscsi_no_hosts(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - - self.mock_object(self.library._client, 'list_hosts', return_value=[]) - - self.assertRaises(exception.NotFound, - self.library.terminate_connection_iscsi, - get_fake_volume(), - connector) - - def test_terminate_connection_iscsi_volume_not_mapped(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - volume = copy.deepcopy(eseries_fake.VOLUME) - volume['listOfMappings'] = [] - self.library._get_volume = mock.Mock(return_value=volume) - self.assertRaises(eseries_exc.VolumeNotMapped, - self.library.terminate_connection_iscsi, - get_fake_volume(), - connector) - - def test_terminate_connection_iscsi_volume_mapped(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - eseries_fake.VOLUME_MAPPING - ] - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - self.mock_object(host_mapper, 'unmap_volume_from_host') - - self.library.terminate_connection_iscsi(get_fake_volume(), connector) - - self.assertTrue(host_mapper.unmap_volume_from_host.called) - - def test_terminate_connection_iscsi_not_mapped_initiator_does_not_exist( - self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(self.library._client, 'list_hosts', - return_value=[eseries_fake.HOST_2]) - self.assertRaises(exception.NotFound, - self.library.terminate_connection_iscsi, - get_fake_volume(), - connector) - - def test_initialize_connection_iscsi_volume_not_mapped(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - eseries_fake.VOLUME_MAPPING - ] - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - - self.library.initialize_connection_iscsi(get_fake_volume(), connector) - - self.assertTrue( - self.library._client.get_volume_mappings_for_volume.called) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_initialize_connection_iscsi_without_chap(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(host_mapper, - 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - mock_configure_chap = self.mock_object(self.library, '_configure_chap') - - self.library.initialize_connection_iscsi(get_fake_volume(), connector) - - self.assertTrue( - self.library._client.get_volume_mappings_for_volume.called) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - self.assertFalse(mock_configure_chap.called) - - def test_initialize_connection_iscsi_volume_not_mapped_host_does_not_exist( - self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(self.library._client, 'list_hosts', return_value=[]) - self.mock_object(self.library._client, 'create_host_with_ports', - return_value=eseries_fake.HOST) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - eseries_fake.VOLUME_MAPPING - ] - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - - self.library.initialize_connection_iscsi(get_fake_volume(), connector) - - self.assertTrue( - self.library._client.get_volume_mappings_for_volume.called) - self.assertTrue(self.library._client.list_hosts.called) - self.assertTrue(self.library._client.create_host_with_ports.called) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_initialize_connection_iscsi_volume_already_mapped_to_target_host( - self): - """Should be a no-op""" - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - - self.library.initialize_connection_iscsi(get_fake_volume(), connector) - - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_initialize_connection_iscsi_volume_mapped_to_another_host(self): - """Should raise error saying multiattach not enabled""" - connector = {'initiator': eseries_fake.INITIATOR_NAME} - fake_mapping_to_other_host = copy.deepcopy( - eseries_fake.VOLUME_MAPPING) - fake_mapping_to_other_host['mapRef'] = eseries_fake.HOST_2[ - 'hostRef'] - self.mock_object(host_mapper, 'map_volume_to_single_host', - side_effect=exception.NetAppDriverException) - - self.assertRaises(exception.NetAppDriverException, - self.library.initialize_connection_iscsi, - get_fake_volume(), connector) - - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - @ddt.data(eseries_fake.WWPN, - fczm_utils.get_formatted_wwn(eseries_fake.WWPN)) - def test_get_host_with_matching_port_wwpn(self, port_id): - port_ids = [port_id] - host = copy.deepcopy(eseries_fake.HOST) - host.update( - { - 'hostSidePorts': [{'label': 'NewStore', 'type': 'fc', - 'address': eseries_fake.WWPN}] - } - ) - host_2 = copy.deepcopy(eseries_fake.HOST_2) - host_2.update( - { - 'hostSidePorts': [{'label': 'NewStore', 'type': 'fc', - 'address': eseries_fake.WWPN_2}] - } - ) - host_list = [host, host_2] - self.mock_object(self.library._client, - 'list_hosts', - return_value=host_list) - - actual_host = self.library._get_host_with_matching_port( - port_ids) - - self.assertEqual(host, actual_host) - - def test_get_host_with_matching_port_iqn(self): - port_ids = [eseries_fake.INITIATOR_NAME] - host = copy.deepcopy(eseries_fake.HOST) - host.update( - { - 'hostSidePorts': [{'label': 'NewStore', 'type': 'iscsi', - 'address': eseries_fake.INITIATOR_NAME}] - } - ) - host_2 = copy.deepcopy(eseries_fake.HOST_2) - host_2.update( - { - 'hostSidePorts': [{'label': 'NewStore', 'type': 'iscsi', - 'address': eseries_fake.INITIATOR_NAME_2}] - } - ) - host_list = [host, host_2] - self.mock_object(self.library._client, - 'list_hosts', - return_value=host_list) - - actual_host = self.library._get_host_with_matching_port( - port_ids) - - self.assertEqual(host, actual_host) - - def test_terminate_connection_fc_no_hosts(self): - connector = {'wwpns': [eseries_fake.WWPN]} - - self.mock_object(self.library._client, 'list_hosts', - return_value=[]) - - self.assertRaises(exception.NotFound, - self.library.terminate_connection_fc, - get_fake_volume(), - connector) - - def test_terminate_connection_fc_volume_not_mapped(self): - connector = {'wwpns': [eseries_fake.WWPN]} - fake_host = copy.deepcopy(eseries_fake.HOST) - fake_host['hostSidePorts'] = [{ - 'label': 'NewStore', - 'type': 'fc', - 'address': eseries_fake.WWPN - }] - volume = copy.deepcopy(eseries_fake.VOLUME) - volume['listOfMappings'] = [] - self.mock_object(self.library, '_get_volume', return_value=volume) - - self.mock_object(self.library._client, 'list_hosts', - return_value=[fake_host]) - - self.assertRaises(eseries_exc.VolumeNotMapped, - self.library.terminate_connection_fc, - get_fake_volume(), - connector) - - def test_terminate_connection_fc_volume_mapped(self): - connector = {'wwpns': [eseries_fake.WWPN]} - fake_host = copy.deepcopy(eseries_fake.HOST) - fake_host['hostSidePorts'] = [{ - 'label': 'NewStore', - 'type': 'fc', - 'address': eseries_fake.WWPN - }] - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - copy.deepcopy(eseries_fake.VOLUME_MAPPING) - ] - self.mock_object(self.library._client, 'list_hosts', - return_value=[fake_host]) - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - self.mock_object(host_mapper, 'unmap_volume_from_host') - - self.library.terminate_connection_fc(get_fake_volume(), connector) - - self.assertTrue(host_mapper.unmap_volume_from_host.called) - - def test_terminate_connection_fc_volume_mapped_no_cleanup_zone(self): - connector = {'wwpns': [eseries_fake.WWPN]} - fake_host = copy.deepcopy(eseries_fake.HOST) - fake_host['hostSidePorts'] = [{ - 'label': 'NewStore', - 'type': 'fc', - 'address': eseries_fake.WWPN - }] - expected_target_info = { - 'driver_volume_type': 'fibre_channel', - 'data': {}, - } - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - copy.deepcopy(eseries_fake.VOLUME_MAPPING) - ] - self.mock_object(self.library._client, 'list_hosts', - return_value=[fake_host]) - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - self.mock_object(host_mapper, 'unmap_volume_from_host') - self.mock_object(self.library._client, 'get_volume_mappings_for_host', - return_value=[ - copy.deepcopy(eseries_fake.VOLUME_MAPPING)]) - - target_info = self.library.terminate_connection_fc(get_fake_volume(), - connector) - self.assertDictEqual(expected_target_info, target_info) - - self.assertTrue(host_mapper.unmap_volume_from_host.called) - - def test_terminate_connection_fc_volume_mapped_cleanup_zone(self): - connector = {'wwpns': [eseries_fake.WWPN]} - fake_host = copy.deepcopy(eseries_fake.HOST) - fake_host['hostSidePorts'] = [{ - 'label': 'NewStore', - 'type': 'fc', - 'address': eseries_fake.WWPN - }] - expected_target_info = { - 'driver_volume_type': 'fibre_channel', - 'data': { - 'target_wwn': [eseries_fake.WWPN_2], - 'initiator_target_map': { - eseries_fake.WWPN: [eseries_fake.WWPN_2] - }, - }, - } - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_eseries_volume['listOfMappings'] = [ - copy.deepcopy(eseries_fake.VOLUME_MAPPING) - ] - self.mock_object(self.library._client, 'list_hosts', - return_value=[fake_host]) - self.mock_object(self.library._client, 'list_volume', - return_value=fake_eseries_volume) - self.mock_object(host_mapper, 'unmap_volume_from_host') - self.mock_object(self.library._client, 'get_volume_mappings_for_host', - return_value=[]) - - target_info = self.library.terminate_connection_fc(get_fake_volume(), - connector) - self.assertDictEqual(expected_target_info, target_info) - - self.assertTrue(host_mapper.unmap_volume_from_host.called) - - def test_terminate_connection_fc_not_mapped_host_with_wwpn_does_not_exist( - self): - connector = {'wwpns': [eseries_fake.WWPN]} - self.mock_object(self.library._client, 'list_hosts', - return_value=[eseries_fake.HOST_2]) - self.assertRaises(exception.NotFound, - self.library.terminate_connection_fc, - get_fake_volume(), - connector) - - def test_initialize_connection_fc_volume_not_mapped(self): - connector = {'wwpns': [eseries_fake.WWPN]} - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - expected_target_info = { - 'driver_volume_type': 'fibre_channel', - 'data': { - 'target_discovered': True, - 'target_lun': 1, - 'target_wwn': [eseries_fake.WWPN_2], - 'initiator_target_map': { - eseries_fake.WWPN: [eseries_fake.WWPN_2] - }, - }, - } - - target_info = self.library.initialize_connection_fc(get_fake_volume(), - connector) - - self.assertTrue( - self.library._client.get_volume_mappings_for_volume.called) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - self.assertDictEqual(expected_target_info, target_info) - - def test_initialize_connection_fc_volume_not_mapped_host_does_not_exist( - self): - connector = {'wwpns': [eseries_fake.WWPN]} - self.library.driver_protocol = 'FC' - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(self.library._client, 'list_hosts', - return_value=[]) - self.mock_object(self.library._client, 'create_host_with_ports', - return_value=eseries_fake.HOST) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - - self.library.initialize_connection_fc(get_fake_volume(), connector) - - self.library._client.create_host_with_ports.assert_called_once_with( - mock.ANY, mock.ANY, - [fczm_utils.get_formatted_wwn(eseries_fake.WWPN)], - port_type='fc', group_id=None - ) - - def test_initialize_connection_fc_volume_already_mapped_to_target_host( - self): - """Should be a no-op""" - connector = {'wwpns': [eseries_fake.WWPN]} - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - - self.library.initialize_connection_fc(get_fake_volume(), connector) - - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_initialize_connection_fc_volume_mapped_to_another_host(self): - """Should raise error saying multiattach not enabled""" - connector = {'wwpns': [eseries_fake.WWPN]} - fake_mapping_to_other_host = copy.deepcopy( - eseries_fake.VOLUME_MAPPING) - fake_mapping_to_other_host['mapRef'] = eseries_fake.HOST_2[ - 'hostRef'] - self.mock_object(host_mapper, 'map_volume_to_single_host', - side_effect=exception.NetAppDriverException) - - self.assertRaises(exception.NetAppDriverException, - self.library.initialize_connection_fc, - get_fake_volume(), connector) - - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_initialize_connection_fc_no_target_wwpns(self): - """Should be a no-op""" - connector = {'wwpns': [eseries_fake.WWPN]} - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - self.mock_object(self.library._client, 'list_target_wwpns', - return_value=[]) - - self.assertRaises(exception.VolumeBackendAPIException, - self.library.initialize_connection_fc, - get_fake_volume(), connector) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_build_initiator_target_map_fc_with_lookup_service( - self): - connector = {'wwpns': [eseries_fake.WWPN, eseries_fake.WWPN_2]} - self.library.lookup_service = mock.Mock() - self.library.lookup_service.get_device_mapping_from_network = ( - mock.Mock(return_value=eseries_fake.FC_FABRIC_MAP)) - - (target_wwpns, initiator_target_map, num_paths) = ( - self.library._build_initiator_target_map_fc(connector)) - - self.assertSetEqual(set(eseries_fake.FC_TARGET_WWPNS), - set(target_wwpns)) - - for i in eseries_fake.FC_I_T_MAP: - for t in eseries_fake.FC_I_T_MAP[i]: - self.assertIn(t, initiator_target_map[i]) - - self.assertEqual(4, num_paths) - - @ddt.data(('raid0', 'raid0'), ('raid1', 'raid1'), ('raid3', 'raid5'), - ('raid5', 'raid5'), ('raid6', 'raid6'), ('raidDiskPool', 'DDP')) - @ddt.unpack - def test_update_ssc_raid_type(self, raid_lvl, raid_lvl_mapping): - pools = [{'volumeGroupRef': 'test_vg1', 'raidLevel': raid_lvl}] - self.library._client.list_storage_pools = mock.Mock(return_value=pools) - - ssc_stats = self.library._update_ssc_raid_type(pools) - - self.assertEqual({'test_vg1': {'netapp_raid_type': raid_lvl_mapping}}, - ssc_stats) - - @ddt.data('raidAll', '__UNDEFINED', 'unknown', - 'raidUnsupported', 'garbage') - def test_update_ssc_raid_type_invalid(self, raid_lvl): - pools = [{'volumeGroupRef': 'test_vg1', 'raidLevel': raid_lvl}] - self.library._client.list_storage_pools = mock.Mock(return_value=pools) - - ssc_stats = self.library._update_ssc_raid_type(pools) - - self.assertEqual({'test_vg1': {'netapp_raid_type': 'unknown'}}, - ssc_stats) - - def test_create_asup(self): - self.library._client = mock.Mock() - self.library._client.features.AUTOSUPPORT = na_utils.FeatureState() - self.library._client.api_operating_mode = ( - eseries_fake.FAKE_ASUP_DATA['operating-mode']) - self.library._app_version = eseries_fake.FAKE_APP_VERSION - self.mock_object( - self.library._client, 'get_asup_info', - return_value=eseries_fake.GET_ASUP_RETURN) - self.mock_object( - self.library._client, 'set_counter', return_value={'value': 1}) - mock_invoke = self.mock_object( - self.library._client, 'add_autosupport_data') - - self.library._create_asup(eseries_fake.FAKE_CINDER_HOST) - - mock_invoke.assert_called_with(eseries_fake.FAKE_KEY, - eseries_fake.FAKE_ASUP_DATA) - - def test_create_asup_not_supported(self): - self.library._client = mock.Mock() - self.library._client.features.AUTOSUPPORT = na_utils.FeatureState( - supported=False) - mock_invoke = self.mock_object( - self.library._client, 'add_autosupport_data') - - self.library._create_asup(eseries_fake.FAKE_CINDER_HOST) - - mock_invoke.assert_not_called() - - @mock.patch.object(library, 'LOG', mock.Mock()) - def test_create_volume_fail_clean(self): - """Test volume creation fail w/o a partial volume being created. - - Test the failed creation of a volume where a partial volume with - the name has not been created, thus no cleanup is required. - """ - self.library._get_volume = mock.Mock( - side_effect = exception.VolumeNotFound(message='')) - self.library._client.create_volume = mock.Mock( - side_effect = exception.NetAppDriverException) - self.library._client.delete_volume = mock.Mock() - fake_volume = copy.deepcopy(get_fake_volume()) - - self.assertRaises(exception.NetAppDriverException, - self.library.create_volume, fake_volume) - - self.assertTrue(self.library._get_volume.called) - self.assertFalse(self.library._client.delete_volume.called) - self.assertEqual(1, library.LOG.error.call_count) - - @mock.patch.object(library, 'LOG', mock.Mock()) - def test_create_volume_fail_dirty(self): - """Test volume creation fail where a partial volume has been created. - - Test scenario where the creation of a volume fails and a partial - volume is created with the name/id that was supplied by to the - original creation call. In this situation the partial volume should - be detected and removed. - """ - fake_volume = copy.deepcopy(get_fake_volume()) - self.library._get_volume = mock.Mock(return_value=fake_volume) - self.library._client.list_volume = mock.Mock(return_value=fake_volume) - self.library._client.create_volume = mock.Mock( - side_effect = exception.NetAppDriverException) - self.library._client.delete_volume = mock.Mock() - - self.assertRaises(exception.NetAppDriverException, - self.library.create_volume, fake_volume) - - self.assertTrue(self.library._get_volume.called) - self.assertTrue(self.library._client.delete_volume.called) - self.library._client.delete_volume.assert_called_once_with( - fake_volume["id"]) - self.assertEqual(1, library.LOG.error.call_count) - - @mock.patch.object(library, 'LOG', mock.Mock()) - def test_create_volume_fail_dirty_fail_delete(self): - """Volume creation fail with partial volume deletion fails - - Test scenario where the creation of a volume fails and a partial - volume is created with the name/id that was supplied by to the - original creation call. The partial volume is detected but when - the cleanup deletetion of that fragment volume is attempted it fails. - """ - fake_volume = copy.deepcopy(get_fake_volume()) - self.library._get_volume = mock.Mock(return_value=fake_volume) - self.library._client.list_volume = mock.Mock(return_value=fake_volume) - self.library._client.create_volume = mock.Mock( - side_effect = exception.NetAppDriverException) - self.library._client.delete_volume = mock.Mock( - side_effect = exception.NetAppDriverException) - - self.assertRaises(exception.NetAppDriverException, - self.library.create_volume, fake_volume) - - self.assertTrue(self.library._get_volume.called) - self.assertTrue(self.library._client.delete_volume.called) - self.library._client.delete_volume.assert_called_once_with( - fake_volume["id"]) - self.assertEqual(2, library.LOG.error.call_count) - - def test_create_consistencygroup(self): - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - expected = {'status': 'available'} - create_cg = self.mock_object(self.library, - '_create_consistency_group', - return_value=expected) - - actual = self.library.create_consistencygroup(fake_cg) - - create_cg.assert_called_once_with(fake_cg) - self.assertEqual(expected, actual) - - def test_create_consistency_group(self): - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - expected = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - create_cg = self.mock_object(self.library._client, - 'create_consistency_group', - return_value=expected) - - result = self.library._create_consistency_group(fake_cg) - - name = utils.convert_uuid_to_es_fmt(fake_cg['id']) - create_cg.assert_called_once_with(name) - self.assertEqual(expected, result) - - def test_delete_consistencygroup(self): - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - volumes = [get_fake_volume()] * 3 - model_update = {'status': 'deleted'} - volume_update = [{'status': 'deleted', 'id': vol['id']} for vol in - volumes] - delete_cg = self.mock_object(self.library._client, - 'delete_consistency_group') - updt_index = self.mock_object( - self.library, '_merge_soft_delete_changes') - delete_vol = self.mock_object(self.library, 'delete_volume') - self.mock_object(self.library, '_get_consistencygroup', - return_value=cg) - - result = self.library.delete_consistencygroup(fake_cg, volumes) - - self.assertEqual(len(volumes), delete_vol.call_count) - delete_cg.assert_called_once_with(cg['id']) - self.assertEqual((model_update, volume_update), result) - updt_index.assert_called_once_with(None, [cg['id']]) - - def test_delete_consistencygroup_index_update_failure(self): - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - volumes = [get_fake_volume()] * 3 - model_update = {'status': 'deleted'} - volume_update = [{'status': 'deleted', 'id': vol['id']} for vol in - volumes] - delete_cg = self.mock_object(self.library._client, - 'delete_consistency_group') - delete_vol = self.mock_object(self.library, 'delete_volume') - self.mock_object(self.library, '_get_consistencygroup', - return_value=cg) - - result = self.library.delete_consistencygroup(fake_cg, volumes) - - self.assertEqual(len(volumes), delete_vol.call_count) - delete_cg.assert_called_once_with(cg['id']) - self.assertEqual((model_update, volume_update), result) - - def test_delete_consistencygroup_not_found(self): - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - delete_cg = self.mock_object(self.library._client, - 'delete_consistency_group') - updt_index = self.mock_object( - self.library, '_merge_soft_delete_changes') - delete_vol = self.mock_object(self.library, 'delete_volume') - exc = exception.ConsistencyGroupNotFound(consistencygroup_id='') - self.mock_object(self.library, '_get_consistencygroup', - side_effect=exc) - - self.library.delete_consistencygroup(fake_cg, []) - - delete_cg.assert_not_called() - delete_vol.assert_not_called() - updt_index.assert_not_called() - - def test_get_consistencygroup(self): - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - name = utils.convert_uuid_to_es_fmt(fake_cg['id']) - cg['name'] = name - list_cgs = self.mock_object(self.library._client, - 'list_consistency_groups', - return_value=[cg]) - - result = self.library._get_consistencygroup(fake_cg) - - self.assertEqual(cg, result) - list_cgs.assert_called_once_with() - - def test_get_consistencygroup_not_found(self): - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - list_cgs = self.mock_object(self.library._client, - 'list_consistency_groups', - return_value=[cg]) - - self.assertRaises(exception.ConsistencyGroupNotFound, - self.library._get_consistencygroup, - copy.deepcopy(eseries_fake.FAKE_CINDER_CG)) - - list_cgs.assert_called_once_with() - - def test_update_consistencygroup(self): - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - vol = copy.deepcopy(eseries_fake.VOLUME) - volumes = [get_fake_volume()] * 3 - self.mock_object( - self.library, '_get_volume', return_value=vol) - self.mock_object(self.library, '_get_consistencygroup', - return_value=cg) - - self.library.update_consistencygroup(fake_cg, volumes, volumes) - - def test_create_consistencygroup_from_src(self): - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - volumes = [cinder_utils.create_volume(self.ctxt) for i in range(3)] - src_volumes = [cinder_utils.create_volume(self.ctxt) for v in volumes] - update_cg = self.mock_object( - self.library, '_update_consistency_group_members') - create_cg = self.mock_object( - self.library, '_create_consistency_group', return_value=cg) - self.mock_object( - self.library, '_create_volume_from_snapshot') - - self.mock_object(self.library, '_get_snapshot', return_value=snap) - - self.library.create_consistencygroup_from_src( - fake_cg, volumes, None, None, None, src_volumes) - - create_cg.assert_called_once_with(fake_cg) - update_cg.assert_called_once_with(cg, volumes, []) - - def test_create_consistencygroup_from_src_cgsnapshot(self): - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - fake_cg = copy.deepcopy(eseries_fake.FAKE_CINDER_CG) - fake_vol = cinder_utils.create_volume(self.ctxt) - cgsnap = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) - volumes = [fake_vol] - snapshots = [cinder_utils.create_snapshot(self.ctxt, v['id']) for v - in volumes] - update_cg = self.mock_object( - self.library, '_update_consistency_group_members') - create_cg = self.mock_object( - self.library, '_create_consistency_group', return_value=cg) - clone_vol = self.mock_object( - self.library, '_create_volume_from_snapshot') - - self.library.create_consistencygroup_from_src( - fake_cg, volumes, cgsnap, snapshots, None, None) - - create_cg.assert_called_once_with(fake_cg) - update_cg.assert_called_once_with(cg, volumes, []) - self.assertEqual(clone_vol.call_count, len(volumes)) - - @ddt.data({'consistencyGroupId': utils.NULL_REF}, - {'consistencyGroupId': None}, {'consistencyGroupId': '1'}, {}) - def test_is_cgsnapshot(self, snapshot_image): - if snapshot_image.get('consistencyGroupId'): - result = not (utils.NULL_REF == snapshot_image[ - 'consistencyGroupId']) - else: - result = False - - actual = self.library._is_cgsnapshot(snapshot_image) - - self.assertEqual(result, actual) - - def test_add_volume_to_consistencygroup(self): - fake_volume = cinder_utils.create_volume(self.ctxt) - fake_volume['consistencygroup'] = ( - cinder_utils.create_consistencygroup(self.ctxt)) - fake_volume['consistencygroup_id'] = fake_volume[ - 'consistencygroup']['id'] - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - self.mock_object(self.library, '_get_consistencygroup', - return_value=cg) - update_members = self.mock_object(self.library, - '_update_consistency_group_members') - - self.library._add_volume_to_consistencygroup(fake_volume) - - update_members.assert_called_once_with(cg, [fake_volume], []) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_copy_volume_high_priority_readonly(self): - src_vol = copy.deepcopy(eseries_fake.VOLUME) - dst_vol = copy.deepcopy(eseries_fake.VOLUME) - vc = copy.deepcopy(eseries_fake.VOLUME_COPY_JOB) - self.mock_object(self.library._client, 'create_volume_copy_job', - return_value=vc) - self.mock_object(self.library._client, 'list_vol_copy_job', - return_value=vc) - delete_copy = self.mock_object(self.library._client, - 'delete_vol_copy_job') - - result = self.library._copy_volume_high_priority_readonly( - src_vol, dst_vol) - - self.assertIsNone(result) - delete_copy.assert_called_once_with(vc['volcopyRef']) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', new= - cinder_utils.ZeroIntervalLoopingCall) - def test_copy_volume_high_priority_readonly_job_create_failure(self): - src_vol = copy.deepcopy(eseries_fake.VOLUME) - dst_vol = copy.deepcopy(eseries_fake.VOLUME) - self.mock_object(self.library._client, 'create_volume_copy_job', - side_effect=exception.NetAppDriverException) - - self.assertRaises( - exception.NetAppDriverException, - self.library._copy_volume_high_priority_readonly, src_vol, - dst_vol) - - -@ddt.ddt -class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase): - """Test driver when netapp_enable_multiattach is enabled. - - Test driver behavior when the netapp_enable_multiattach configuration - option is True. - """ - - def setUp(self): - super(NetAppEseriesLibraryMultiAttachTestCase, self).setUp() - config = eseries_fake.create_configuration_eseries() - config.netapp_enable_multiattach = True - - kwargs = {'configuration': config} - - self.library = library.NetAppESeriesLibrary("FAKE", **kwargs) - self.library._client = eseries_fake.FakeEseriesClient() - - self.mock_object(library.cinder_utils, 'synchronized', - return_value=lambda f: f) - self.mock_object(self.library, '_start_periodic_tasks') - - self.ctxt = context.get_admin_context() - - with mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', - new = cinder_utils.ZeroIntervalLoopingCall): - self.library.check_for_setup_error() - - def test_do_setup_host_group_already_exists(self): - mock_check_flags = self.mock_object(na_utils, 'check_flags') - self.mock_object(self.library, - '_check_mode_get_or_register_storage_system') - fake_rest_client = eseries_fake.FakeEseriesClient() - self.mock_object(self.library, '_create_rest_client', - return_value=fake_rest_client) - mock_create = self.mock_object(fake_rest_client, 'create_host_group') - - self.library.do_setup(mock.Mock()) - - self.assertTrue(mock_check_flags.called) - self.assertEqual(0, mock_create.call_count) - - def test_do_setup_host_group_does_not_exist(self): - mock_check_flags = self.mock_object(na_utils, 'check_flags') - fake_rest_client = eseries_fake.FakeEseriesClient() - self.mock_object(self.library, '_create_rest_client', - return_value=fake_rest_client) - mock_get_host_group = self.mock_object( - fake_rest_client, "get_host_group_by_name", - side_effect=exception.NotFound) - self.mock_object(self.library, - '_check_mode_get_or_register_storage_system') - - self.library.do_setup(mock.Mock()) - - self.assertTrue(mock_check_flags.called) - self.assertLess(0, mock_get_host_group.call_count) - - def test_create_volume(self): - self.library._client.create_volume = mock.Mock( - return_value=eseries_fake.VOLUME) - update_members = self.mock_object(self.library, - '_update_consistency_group_members') - - self.library.create_volume(get_fake_volume()) - self.assertLess(0, self.library._client.create_volume.call_count) - - update_members.assert_not_called() - - @ddt.data(('netapp_eseries_flash_read_cache', 'flash_cache', 'true'), - ('netapp_eseries_flash_read_cache', 'flash_cache', 'false'), - ('netapp_eseries_flash_read_cache', 'flash_cache', None), - ('netapp_thin_provisioned', 'thin_provision', 'true'), - ('netapp_thin_provisioned', 'thin_provision', 'false'), - ('netapp_thin_provisioned', 'thin_provision', None), - ('netapp_eseries_data_assurance', 'data_assurance', 'true'), - ('netapp_eseries_data_assurance', 'data_assurance', 'false'), - ('netapp_eseries_data_assurance', 'data_assurance', None), - ('netapp:write_cache', 'write_cache', 'true'), - ('netapp:write_cache', 'write_cache', 'false'), - ('netapp:write_cache', 'write_cache', None), - ('netapp:read_cache', 'read_cache', 'true'), - ('netapp:read_cache', 'read_cache', 'false'), - ('netapp:read_cache', 'read_cache', None), - ('netapp_eseries_flash_read_cache', 'flash_cache', 'True'), - ('netapp_eseries_flash_read_cache', 'flash_cache', '1'), - ('netapp_eseries_data_assurance', 'data_assurance', '')) - @ddt.unpack - def test_create_volume_with_extra_spec(self, spec, key, value): - fake_volume = get_fake_volume() - extra_specs = {spec: value} - volume = copy.deepcopy(eseries_fake.VOLUME) - - self.library._client.create_volume = mock.Mock( - return_value=volume) - # Make this utility method return our extra spec - mocked_spec_method = self.mock_object(na_utils, - 'get_volume_extra_specs') - mocked_spec_method.return_value = extra_specs - - self.library.create_volume(fake_volume) - - self.assertEqual(1, self.library._client.create_volume.call_count) - # Ensure create_volume is called with the correct argument - args, kwargs = self.library._client.create_volume.call_args - self.assertIn(key, kwargs) - if(value is not None): - expected = na_utils.to_bool(value) - else: - expected = value - self.assertEqual(expected, kwargs[key]) - - def test_create_volume_too_many_volumes(self): - self.library._client.list_volumes = mock.Mock( - return_value=[eseries_fake.VOLUME for __ in - range(utils.MAX_LUNS_PER_HOST_GROUP + 1)]) - self.library._client.create_volume = mock.Mock( - return_value=eseries_fake.VOLUME) - - self.assertRaises(exception.NetAppDriverException, - self.library.create_volume, - get_fake_volume()) - self.assertEqual(0, self.library._client.create_volume.call_count) - - @ddt.data(0, 1, 2) - def test_create_snapshot(self, group_count): - """Successful Snapshot creation test""" - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - self.library._get_volume = mock.Mock(return_value=fake_eseries_volume) - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - self.library._get_storage_pools = mock.Mock(return_value=[fake_pool]) - fake_cinder_snapshot = copy.deepcopy( - eseries_fake.FAKE_CINDER_SNAPSHOT) - fake_snapshot_group_list = eseries_fake.list_snapshot_groups( - group_count) - fake_snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - fake_snapshot_image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - self.library._client.create_snapshot_group = mock.Mock( - return_value=fake_snapshot_group) - self.library._client.list_snapshot_groups = mock.Mock( - return_value=fake_snapshot_group_list) - self.library._client.create_snapshot_image = mock.Mock( - return_value=fake_snapshot_image) - - self.library.create_snapshot(fake_cinder_snapshot) - - @ddt.data(0, 1, 3) - def test_create_cloned_volume(self, snapshot_group_count): - """Test creating cloned volume with different exist group counts. """ - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - self.library._get_volume = mock.Mock(return_value=fake_eseries_volume) - fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - self.library._get_storage_pools = mock.Mock(return_value=[fake_pool]) - fake_snapshot_group_list = eseries_fake.list_snapshot_groups( - snapshot_group_count) - self.library._client.list_snapshot_groups = mock.Mock( - return_value=fake_snapshot_group_list) - fake_snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - self.library._client.create_snapshot_group = mock.Mock( - return_value=fake_snapshot_group) - fake_snapshot_image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - self.library._client.create_snapshot_image = mock.Mock( - return_value=fake_snapshot_image) - self.library._get_snapshot_group_for_snapshot = mock.Mock( - return_value=copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)) - fake_created_volume = copy.deepcopy(eseries_fake.VOLUMES[1]) - self.library.create_volume_from_snapshot = mock.Mock( - return_value = fake_created_volume) - fake_cinder_volume = copy.deepcopy(eseries_fake.FAKE_CINDER_VOLUME) - extend_vol = {'id': uuid.uuid4(), 'size': 10} - self.mock_object(self.library, '_create_volume_from_snapshot') - - self.library.create_cloned_volume(extend_vol, fake_cinder_volume) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', - new = cinder_utils.ZeroIntervalLoopingCall) - def test_create_volume_from_snapshot(self): - fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - self.mock_object(self.library, "_schedule_and_create_volume", - return_value=fake_eseries_volume) - self.mock_object(self.library, "_get_snapshot", - return_value=copy.deepcopy( - eseries_fake.SNAPSHOT_IMAGE)) - - self.library.create_volume_from_snapshot( - get_fake_volume(), fake_snap) - - self.assertEqual( - 1, self.library._schedule_and_create_volume.call_count) - - def test_create_volume_from_snapshot_create_fails(self): - fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - self.mock_object(self.library, "_schedule_and_create_volume", - return_value=fake_dest_eseries_volume) - self.mock_object(self.library._client, "delete_volume") - self.mock_object(self.library._client, "delete_snapshot_volume") - self.mock_object(self.library, "_get_snapshot", - return_value=copy.deepcopy( - eseries_fake.SNAPSHOT_IMAGE)) - self.mock_object(self.library._client, "create_snapshot_volume", - side_effect=exception.NetAppDriverException) - - self.assertRaises(exception.NetAppDriverException, - self.library.create_volume_from_snapshot, - get_fake_volume(), - fake_snapshot.fake_snapshot_obj(None)) - - self.assertEqual( - 1, self.library._schedule_and_create_volume.call_count) - # Ensure the volume we were going to copy to is cleaned up - self.library._client.delete_volume.assert_called_once_with( - fake_dest_eseries_volume['volumeRef']) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', - new = cinder_utils.ZeroIntervalLoopingCall) - def test_create_volume_from_snapshot_copy_job_fails(self): - fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - self.mock_object(self.library, "_schedule_and_create_volume", - return_value=fake_dest_eseries_volume) - self.mock_object(self.library, "_create_snapshot_volume", - return_value=fake_dest_eseries_volume) - self.mock_object(self.library._client, "delete_volume") - self.mock_object(self.library, "_get_snapshot", - return_value=copy.deepcopy( - eseries_fake.SNAPSHOT_IMAGE)) - - fake_failed_volume_copy_job = copy.deepcopy( - eseries_fake.VOLUME_COPY_JOB) - fake_failed_volume_copy_job['status'] = 'failed' - self.mock_object(self.library._client, - "create_volume_copy_job", - return_value=fake_failed_volume_copy_job) - self.mock_object(self.library._client, - "list_vol_copy_job", - return_value=fake_failed_volume_copy_job) - - self.assertRaises(exception.NetAppDriverException, - self.library.create_volume_from_snapshot, - get_fake_volume(), - fake_snapshot.fake_snapshot_obj(None)) - - self.assertEqual( - 1, self.library._schedule_and_create_volume.call_count) - # Ensure the volume we were going to copy to is cleaned up - self.library._client.delete_volume.assert_called_once_with( - fake_dest_eseries_volume['volumeRef']) - - @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', - new = cinder_utils.ZeroIntervalLoopingCall) - def test_create_volume_from_snapshot_fail_to_delete_snapshot_volume(self): - fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME) - fake_dest_eseries_volume['volumeRef'] = 'fake_volume_ref' - self.mock_object(self.library, "_schedule_and_create_volume", - return_value=fake_dest_eseries_volume) - self.mock_object(self.library, "_get_snapshot", - return_value=copy.deepcopy( - eseries_fake.SNAPSHOT_IMAGE)) - self.mock_object(self.library, '_create_snapshot_volume', - return_value=copy.deepcopy( - eseries_fake.SNAPSHOT_VOLUME)) - self.mock_object(self.library, "_create_snapshot_volume", - return_value=copy.deepcopy( - eseries_fake.VOLUME)) - self.mock_object(self.library._client, "delete_snapshot_volume", - side_effect=exception.NetAppDriverException) - self.mock_object(self.library._client, "delete_volume") - - self.library.create_volume_from_snapshot( - get_fake_volume(), fake_snapshot.fake_snapshot_obj(None)) - - self.assertEqual( - 1, self.library._schedule_and_create_volume.call_count) - self.assertEqual( - 1, self.library._client.delete_snapshot_volume.call_count) - # Ensure the volume we created is not cleaned up - self.assertEqual(0, self.library._client.delete_volume.call_count) - - def test_create_snapshot_volume_cgsnap(self): - image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - self.mock_object(self.library, '_get_snapshot_group', return_value=grp) - expected = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - self.mock_object(self.library, '_is_cgsnapshot', return_value=True) - create_view = self.mock_object( - self.library._client, 'create_cg_snapshot_view', - return_value=expected) - - result = self.library._create_snapshot_volume(image) - - self.assertEqual(expected, result) - create_view.assert_called_once_with(image['consistencyGroupId'], - mock.ANY, image['id']) - - def test_create_snapshot_volume(self): - image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - self.mock_object(self.library, '_get_snapshot_group', return_value=grp) - expected = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - self.mock_object(self.library, '_is_cgsnapshot', return_value=False) - create_view = self.mock_object( - self.library._client, 'create_snapshot_volume', - return_value=expected) - - result = self.library._create_snapshot_volume(image) - - self.assertEqual(expected, result) - create_view.assert_called_once_with( - image['pitRef'], mock.ANY, image['baseVol']) - - def test_create_snapshot_group(self): - label = 'label' - - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snapshot_group['baseVolume'] = vol['id'] - get_call = self.mock_object( - self.library, '_get_storage_pools', return_value=None) - create_call = self.mock_object( - self.library._client, 'create_snapshot_group', - return_value=snapshot_group) - - actual = self.library._create_snapshot_group(label, vol) - - get_call.assert_not_called() - create_call.assert_called_once_with(label, vol['id'], repo_percent=20) - self.assertEqual(snapshot_group, actual) - - def test_create_snapshot_group_legacy_ddp(self): - self.library._client.features.REST_1_3_RELEASE = False - vol = copy.deepcopy(eseries_fake.VOLUME) - pools = copy.deepcopy(eseries_fake.STORAGE_POOLS) - pool = pools[-1] - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snapshot_group['baseVolume'] = vol['id'] - vol['volumeGroupRef'] = pool['id'] - pool['raidLevel'] = 'raidDiskPool' - get_call = self.mock_object( - self.library, '_get_storage_pools', return_value=pools) - create_call = self.mock_object( - self.library._client, 'create_snapshot_group', - return_value=snapshot_group) - - actual = self.library._create_snapshot_group('label', vol) - - create_call.assert_called_with('label', vol['id'], - vol['volumeGroupRef'], - repo_percent=mock.ANY) - get_call.assert_called_once_with() - self.assertEqual(snapshot_group, actual) - - def test_create_snapshot_group_legacy_vg(self): - self.library._client.features.REST_1_3_RELEASE = False - vol = copy.deepcopy(eseries_fake.VOLUME) - vol_size_gb = int(vol['totalSizeInBytes']) / units.Gi - pools = copy.deepcopy(eseries_fake.STORAGE_POOLS) - pool = pools[0] - pool['raidLevel'] = 'raid6' - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snapshot_group['baseVolume'] = vol['id'] - vol['volumeGroupRef'] = pool['id'] - - get_call = self.mock_object( - self.library, '_get_sorted_available_storage_pools', - return_value=pools) - self.mock_object(self.library._client, 'create_snapshot_group', - return_value=snapshot_group) - actual = self.library._create_snapshot_group('label', vol) - - get_call.assert_called_once_with(vol_size_gb) - self.assertEqual(snapshot_group, actual) - - def test_get_snapshot(self): - fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - get_snap = self.mock_object( - self.library._client, 'list_snapshot_image', return_value=snap) - - result = self.library._get_snapshot(fake_snap) - - self.assertEqual(snap, result) - get_snap.assert_called_once_with(fake_snap['provider_id']) - - def test_get_snapshot_fail(self): - fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - get_snap = self.mock_object( - self.library._client, 'list_snapshot_image', - side_effect=exception.NotFound) - - self.assertRaises(exception.NotFound, self.library._get_snapshot, - fake_snap) - - get_snap.assert_called_once_with(fake_snap['provider_id']) - - def test_get_snapshot_group_for_snapshot(self): - fake_id = 'id' - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - get_snap = self.mock_object( - self.library, '_get_snapshot', return_value=snap) - get_grp = self.mock_object(self.library._client, 'list_snapshot_group', - return_value=grp) - - result = self.library._get_snapshot_group_for_snapshot(fake_id) - - self.assertEqual(grp, result) - get_grp.assert_called_once_with(snap['pitGroupRef']) - get_snap.assert_called_once_with(fake_id) - - def test_get_snapshot_group_for_snapshot_fail(self): - fake_id = 'id' - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - get_snap = self.mock_object( - self.library, '_get_snapshot', return_value=snap) - get_grp = self.mock_object(self.library._client, 'list_snapshot_group', - side_effect=exception.NotFound) - - self.assertRaises(exception.NotFound, - self.library._get_snapshot_group_for_snapshot, - fake_id) - - get_grp.assert_called_once_with(snap['pitGroupRef']) - get_snap.assert_called_once_with(fake_id) - - def test_get_snapshot_groups_for_volume(self): - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snapshot_group['baseVolume'] = vol['id'] - # Generate some snapshot groups that will not match - snapshot_groups = [copy.deepcopy(snapshot_group) for i in range( - self.library.MAX_SNAPSHOT_GROUP_COUNT)] - for i, group in enumerate(snapshot_groups): - group['baseVolume'] = str(i) - snapshot_groups.append(snapshot_group) - get_call = self.mock_object( - self.library._client, 'list_snapshot_groups', - return_value=snapshot_groups) - - groups = self.library._get_snapshot_groups_for_volume(vol) - - get_call.assert_called_once_with() - self.assertEqual([snapshot_group], groups) - - def test_get_available_snapshot_group(self): - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snapshot_group['baseVolume'] = vol['id'] - snapshot_group['snapshotCount'] = 0 - # Generate some snapshot groups that will not match - - reserved_group = copy.deepcopy(snapshot_group) - reserved_group['label'] += self.library.SNAPSHOT_VOL_COPY_SUFFIX - - full_group = copy.deepcopy(snapshot_group) - full_group['snapshotCount'] = self.library.MAX_SNAPSHOT_COUNT - - cgroup = copy.deepcopy(snapshot_group) - cgroup['consistencyGroup'] = True - - snapshot_groups = [snapshot_group, reserved_group, full_group, cgroup] - get_call = self.mock_object( - self.library, '_get_snapshot_groups_for_volume', - return_value=snapshot_groups) - - group = self.library._get_available_snapshot_group(vol) - - get_call.assert_called_once_with(vol) - self.assertEqual(snapshot_group, group) - - def test_get_snapshot_groups_for_volume_not_found(self): - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snapshot_group['baseVolume'] = vol['id'] - snapshot_group['snapshotCount'] = self.library.MAX_SNAPSHOT_COUNT - # Generate some snapshot groups that will not match - - get_call = self.mock_object( - self.library, '_get_snapshot_groups_for_volume', - return_value=[snapshot_group]) - - group = self.library._get_available_snapshot_group(vol) - - get_call.assert_called_once_with(vol) - self.assertIsNone(group) - - def test_create_snapshot_available_snap_group(self): - expected_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - expected = {'provider_id': expected_snap['id']} - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - fake_label = 'fakeName' - self.mock_object(self.library, '_get_volume', return_value=vol) - create_call = self.mock_object( - self.library._client, 'create_snapshot_image', - return_value=expected_snap) - self.mock_object(self.library, '_get_available_snapshot_group', - return_value=snapshot_group) - self.mock_object(utils, 'convert_uuid_to_es_fmt', - return_value=fake_label) - fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - - model_update = self.library.create_snapshot(fake_snapshot) - - self.assertEqual(expected, model_update) - create_call.assert_called_once_with(snapshot_group['id']) - - @ddt.data(False, True) - def test_create_snapshot_failure(self, cleanup_failure): - """Validate the behavior for a failure during snapshot creation""" - - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snap_vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - fake_label = 'fakeName' - create_fail_exc = exception.NetAppDriverException('fail_create') - cleanup_fail_exc = exception.NetAppDriverException('volume_deletion') - if cleanup_failure: - exc_msg = cleanup_fail_exc.msg - delete_snap_grp = self.mock_object( - self.library, '_delete_snapshot_group', - side_effect=cleanup_fail_exc) - else: - exc_msg = create_fail_exc.msg - delete_snap_grp = self.mock_object( - self.library, '_delete_snapshot_group') - self.mock_object(self.library, '_get_volume', return_value=vol) - self.mock_object(self.library._client, 'create_snapshot_image', - side_effect=create_fail_exc) - self.mock_object(self.library._client, 'create_snapshot_volume', - return_value=snap_vol) - self.mock_object(self.library, '_get_available_snapshot_group', - return_value=snapshot_group) - self.mock_object(utils, 'convert_uuid_to_es_fmt', - return_value=fake_label) - fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - - self.assertRaisesRegex(exception.NetAppDriverException, - exc_msg, - self.library.create_snapshot, - fake_snapshot) - self.assertTrue(delete_snap_grp.called) - - def test_create_snapshot_no_snap_group(self): - self.library._client.features = mock.Mock() - expected_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - fake_label = 'fakeName' - self.mock_object(self.library, '_get_volume', return_value=vol) - create_call = self.mock_object( - self.library._client, 'create_snapshot_image', - return_value=expected_snap) - self.mock_object(self.library, '_get_snapshot_groups_for_volume', - return_value=[snapshot_group]) - self.mock_object(self.library, '_get_available_snapshot_group', - return_value=None) - self.mock_object(utils, 'convert_uuid_to_es_fmt', - return_value=fake_label) - fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - - snapshot = self.library.create_snapshot(fake_snapshot) - - expected = {'provider_id': expected_snap['id']} - self.assertEqual(expected, snapshot) - create_call.assert_called_once_with(snapshot_group['id']) - - def test_create_snapshot_no_snapshot_groups_remaining(self): - """Test the failure condition where all snap groups are allocated""" - - self.library._client.features = mock.Mock() - expected_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - vol = copy.deepcopy(eseries_fake.VOLUME) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snap_vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME) - grp_count = (self.library.MAX_SNAPSHOT_GROUP_COUNT - - self.library.RESERVED_SNAPSHOT_GROUP_COUNT) - fake_label = 'fakeName' - self.mock_object(self.library, '_get_volume', return_value=vol) - self.mock_object(self.library._client, 'create_snapshot_image', - return_value=expected_snap) - self.mock_object(self.library._client, 'create_snapshot_volume', - return_value=snap_vol) - self.mock_object(self.library, '_get_available_snapshot_group', - return_value=None) - self.mock_object(self.library, '_get_snapshot_groups_for_volume', - return_value=[snapshot_group] * grp_count) - self.mock_object(utils, 'convert_uuid_to_es_fmt', - return_value=fake_label) - fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - - # Error message should contain the maximum number of supported - # snapshots - self.assertRaisesRegex(exception.SnapshotLimitExceeded, - str(self.library.MAX_SNAPSHOT_COUNT * - grp_count), - self.library.create_snapshot, fake_snapshot) - - def test_delete_snapshot(self): - fake_vol = cinder_utils.create_volume(self.ctxt) - fake_snap = cinder_utils.create_snapshot(self.ctxt, fake_vol['id']) - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - vol = copy.deepcopy(eseries_fake.VOLUME) - self.mock_object(self.library, '_get_volume', return_value=vol) - self.mock_object(self.library, '_get_snapshot', return_value=snap) - - del_snap = self.mock_object(self.library, '_delete_es_snapshot') - - self.library.delete_snapshot(fake_snap) - - del_snap.assert_called_once_with(snap) - - def test_delete_es_snapshot(self): - vol = copy.deepcopy(eseries_fake.VOLUME) - snap_count = 30 - # Ensure that it's the oldest PIT - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - fake_volume_refs = ['1', '2', snap['baseVol']] - fake_snapshot_group_refs = ['3', '4', snapshot_group['id']] - snapshots = [copy.deepcopy(snap) for i in range(snap_count)] - bitset = na_utils.BitSet(0) - for i, snapshot in enumerate(snapshots): - volume_ref = fake_volume_refs[i % len(fake_volume_refs)] - group_ref = fake_snapshot_group_refs[i % - len(fake_snapshot_group_refs)] - snapshot['pitGroupRef'] = group_ref - snapshot['baseVol'] = volume_ref - snapshot['pitSequenceNumber'] = str(i) - snapshot['id'] = i - bitset.set(i) - snapshots.append(snap) - - filtered_snaps = [x for x in snapshots - if x['pitGroupRef'] == snap['pitGroupRef']] - - self.mock_object(self.library, '_get_volume', return_value=vol) - self.mock_object(self.library, '_get_snapshot', return_value=snap) - self.mock_object(self.library, '_get_soft_delete_map', - return_value={snap['pitGroupRef']: repr(bitset)}) - self.mock_object(self.library._client, 'list_snapshot_images', - return_value=snapshots) - delete_image = self.mock_object( - self.library, '_cleanup_snapshot_images', - return_value=({snap['pitGroupRef']: repr(bitset)}, None)) - - self.library._delete_es_snapshot(snap) - - delete_image.assert_called_once_with(filtered_snaps, bitset) - - def test_delete_snapshot_oldest(self): - vol = copy.deepcopy(eseries_fake.VOLUME) - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - snapshots = [snap] - self.mock_object(self.library, '_get_volume', return_value=vol) - self.mock_object(self.library, '_get_snapshot', return_value=snap) - self.mock_object(self.library, '_get_soft_delete_map', return_value={}) - self.mock_object(self.library._client, 'list_snapshot_images', - return_value=snapshots) - delete_image = self.mock_object( - self.library, '_cleanup_snapshot_images', - return_value=(None, [snap['pitGroupRef']])) - - self.library._delete_es_snapshot(snap) - - delete_image.assert_called_once_with(snapshots, - na_utils.BitSet(1)) - - def test_get_soft_delete_map(self): - fake_val = 'fake' - self.mock_object(self.library._client, 'list_backend_store', - return_value=fake_val) - - actual = self.library._get_soft_delete_map() - - self.assertEqual(fake_val, actual) - - def test_cleanup_snapshot_images_delete_all(self): - image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - images = [image] * 32 - bitset = na_utils.BitSet() - for i, image in enumerate(images): - image['pitSequenceNumber'] = i - bitset.set(i) - delete_grp = self.mock_object(self.library._client, - 'delete_snapshot_group') - - updt, keys = self.library._cleanup_snapshot_images( - images, bitset) - - delete_grp.assert_called_once_with(image['pitGroupRef']) - self.assertIsNone(updt) - self.assertEqual([image['pitGroupRef']], keys) - - def test_cleanup_snapshot_images_delete_all_fail(self): - image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - bitset = na_utils.BitSet(2 ** 32 - 1) - delete_grp = self.mock_object( - self.library._client, 'delete_snapshot_group', - side_effect=exception.NetAppDriverException) - - updt, keys = self.library._cleanup_snapshot_images( - [image], bitset) - - delete_grp.assert_called_once_with(image['pitGroupRef']) - self.assertIsNone(updt) - self.assertEqual([image['pitGroupRef']], keys) - - def test_cleanup_snapshot_images(self): - image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - images = [image] * 32 - del_count = 16 - bitset = na_utils.BitSet() - for i, image in enumerate(images): - image['pitSequenceNumber'] = i - if i < del_count: - bitset.set(i) - exp_bitset = copy.deepcopy(bitset) - exp_bitset >>= 16 - delete_img = self.mock_object( - self.library, '_delete_snapshot_image') - - updt, keys = self.library._cleanup_snapshot_images( - images, bitset) - - self.assertEqual(del_count, delete_img.call_count) - self.assertIsNone(keys) - self.assertEqual({image['pitGroupRef']: exp_bitset}, updt) - - def test_delete_snapshot_image(self): - snap_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - - self.mock_object(self.library._client, 'list_snapshot_group', - return_value=snap_group) - - self.library._delete_snapshot_image(snap) - - def test_delete_snapshot_image_fail_cleanup(self): - snap_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP) - snap_group['snapshotCount'] = 0 - snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - - self.mock_object(self.library._client, 'list_snapshot_group', - return_value=snap_group) - - self.library._delete_snapshot_image(snap) - - def test_delete_snapshot_not_found(self): - fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT) - get_snap = self.mock_object(self.library, '_get_snapshot', - side_effect=exception.NotFound) - - with mock.patch.object(library, 'LOG', mock.Mock()): - self.library.delete_snapshot(fake_snapshot) - get_snap.assert_called_once_with(fake_snapshot) - self.assertTrue(library.LOG.warning.called) - - @ddt.data(['key1', 'key2'], [], None) - def test_merge_soft_delete_changes_keys(self, keys_to_del): - count = len(keys_to_del) if keys_to_del is not None else 0 - save_store = self.mock_object( - self.library._client, 'save_backend_store') - index = {'key1': 'val'} - get_store = self.mock_object(self.library, '_get_soft_delete_map', - return_value=index) - - self.library._merge_soft_delete_changes(None, keys_to_del) - - if count: - expected = copy.deepcopy(index) - for key in keys_to_del: - expected.pop(key, None) - get_store.assert_called_once_with() - save_store.assert_called_once_with( - self.library.SNAPSHOT_PERSISTENT_STORE_KEY, expected) - else: - get_store.assert_not_called() - save_store.assert_not_called() - - def test_create_cgsnapshot(self): - fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) - fake_vol = cinder_utils.create_volume(self.ctxt) - fake_snapshots = [cinder_utils.create_snapshot(self.ctxt, - fake_vol['id'])] - vol = copy.deepcopy(eseries_fake.VOLUME) - image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - image['baseVol'] = vol['id'] - cg_snaps = [image] - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - - for snap in cg_snaps: - snap['baseVol'] = vol['id'] - get_cg = self.mock_object( - self.library, '_get_consistencygroup_by_name', return_value=cg) - get_vol = self.mock_object( - self.library, '_get_volume', return_value=vol) - mk_snap = self.mock_object( - self.library._client, 'create_consistency_group_snapshot', - return_value=cg_snaps) - - model_update, snap_updt = self.library.create_cgsnapshot( - fake_cgsnapshot, fake_snapshots) - - self.assertIsNone(model_update) - for snap in cg_snaps: - self.assertIn({'id': fake_snapshots[0]['id'], - 'provider_id': snap['id'], - 'status': 'available'}, snap_updt) - self.assertEqual(len(cg_snaps), len(snap_updt)) - - get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt( - fake_cgsnapshot['consistencygroup_id'])) - self.assertEqual(get_vol.call_count, len(fake_snapshots)) - mk_snap.assert_called_once_with(cg['id']) - - def test_create_cgsnapshot_cg_fail(self): - fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) - fake_snapshots = [copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)] - self.mock_object( - self.library, '_get_consistencygroup_by_name', - side_effect=exception.NetAppDriverException) - - self.assertRaises( - exception.NetAppDriverException, - self.library.create_cgsnapshot, fake_cgsnapshot, fake_snapshots) - - def test_delete_cgsnapshot(self): - """Test the deletion of a cgsnapshot when a soft delete is required""" - fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) - fake_vol = cinder_utils.create_volume(self.ctxt) - fake_snapshots = [cinder_utils.create_snapshot( - self.ctxt, fake_vol['id'])] - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - # Ensure that the snapshot to be deleted is not the oldest - cg_snap['pitSequenceNumber'] = str(max(cg['uniqueSequenceNumber'])) - cg_snaps = [cg_snap] - for snap in fake_snapshots: - snap['provider_id'] = cg_snap['id'] - vol = copy.deepcopy(eseries_fake.VOLUME) - for snap in cg_snaps: - snap['baseVol'] = vol['id'] - get_cg = self.mock_object( - self.library, '_get_consistencygroup_by_name', return_value=cg) - self.mock_object( - self.library._client, 'delete_consistency_group_snapshot') - self.mock_object( - self.library._client, 'get_consistency_group_snapshots', - return_value=cg_snaps) - soft_del = self.mock_object( - self.library, '_soft_delete_cgsnapshot', return_value=(None, None)) - - # Mock the locking mechanism - model_update, snap_updt = self.library.delete_cgsnapshot( - fake_cgsnapshot, fake_snapshots) - - self.assertIsNone(model_update) - self.assertIsNone(snap_updt) - get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt( - fake_cgsnapshot['consistencygroup_id'])) - soft_del.assert_called_once_with( - cg, cg_snap['pitSequenceNumber']) - - @ddt.data(True, False) - def test_soft_delete_cgsnapshot(self, bitset_exists): - """Test the soft deletion of a cgsnapshot""" - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - seq_num = 10 - cg_snap['pitSequenceNumber'] = seq_num - cg_snaps = [cg_snap] - self.mock_object( - self.library._client, 'delete_consistency_group_snapshot') - self.mock_object( - self.library._client, 'get_consistency_group_snapshots', - return_value=cg_snaps) - bitset = na_utils.BitSet(1) - index = {cg['id']: repr(bitset)} if bitset_exists else {} - bitset >>= len(cg_snaps) - updt = {cg['id']: repr(bitset)} - self.mock_object(self.library, '_get_soft_delete_map', - return_value=index) - save_map = self.mock_object(self.library, '_merge_soft_delete_changes') - - model_update, snap_updt = self.library._soft_delete_cgsnapshot( - cg, seq_num) - - self.assertIsNone(model_update) - self.assertIsNone(snap_updt) - save_map.assert_called_once_with(updt, None) - - def test_delete_cgsnapshot_single(self): - """Test the backend deletion of the oldest cgsnapshot""" - fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) - fake_vol = cinder_utils.create_volume(self.ctxt) - fake_snapshots = [cinder_utils.create_snapshot(self.ctxt, - fake_vol['id'])] - cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - cg_snaps = [cg_snap] - for snap in fake_snapshots: - snap['provider_id'] = cg_snap['id'] - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - cg['uniqueSequenceNumber'] = [cg_snap['pitSequenceNumber']] - vol = copy.deepcopy(eseries_fake.VOLUME) - for snap in cg_snaps: - snap['baseVol'] = vol['id'] - get_cg = self.mock_object( - self.library, '_get_consistencygroup_by_name', return_value=cg) - del_snap = self.mock_object( - self.library._client, 'delete_consistency_group_snapshot', - return_value=cg_snaps) - - model_update, snap_updt = self.library.delete_cgsnapshot( - fake_cgsnapshot, fake_snapshots) - - self.assertIsNone(model_update) - self.assertIsNone(snap_updt) - get_cg.assert_called_once_with(utils.convert_uuid_to_es_fmt( - fake_cgsnapshot['consistencygroup_id'])) - del_snap.assert_called_once_with(cg['id'], cg_snap[ - 'pitSequenceNumber']) - - def test_delete_cgsnapshot_snap_not_found(self): - fake_cgsnapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_CG_SNAPSHOT) - fake_vol = cinder_utils.create_volume(self.ctxt) - fake_snapshots = [cinder_utils.create_snapshot( - self.ctxt, fake_vol['id'])] - cg_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE) - cg_snaps = [cg_snap] - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - self.mock_object(self.library, '_get_consistencygroup_by_name', - return_value=cg) - self.mock_object( - self.library._client, 'delete_consistency_group_snapshot', - return_value=cg_snaps) - - self.assertRaises( - exception.CgSnapshotNotFound, - self.library.delete_cgsnapshot, fake_cgsnapshot, fake_snapshots) - - @ddt.data(0, 1, 10, 32) - def test_cleanup_cg_snapshots(self, count): - # Set the soft delete bit for 'count' snapshot images - bitset = na_utils.BitSet() - for i in range(count): - bitset.set(i) - cg = copy.deepcopy(eseries_fake.FAKE_CONSISTENCY_GROUP) - # Define 32 snapshots for the CG - cg['uniqueSequenceNumber'] = list(range(32)) - cg_id = cg['id'] - del_snap = self.mock_object( - self.library._client, 'delete_consistency_group_snapshot') - expected_bitset = copy.deepcopy(bitset) >> count - expected_updt = {cg_id: repr(expected_bitset)} - - updt = self.library._cleanup_cg_snapshots( - cg_id, cg['uniqueSequenceNumber'], bitset) - - self.assertEqual(count, del_snap.call_count) - self.assertEqual(expected_updt, updt) - - @ddt.data(False, True) - def test_get_pool_operation_progress(self, expect_complete): - """Validate the operation progress is interpreted correctly""" - - pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - if expect_complete: - pool_progress = [] - else: - pool_progress = copy.deepcopy( - eseries_fake.FAKE_POOL_ACTION_PROGRESS) - - expected_actions = set(action['currentAction'] for action in - pool_progress) - expected_eta = reduce(lambda x, y: x + y['estimatedTimeToCompletion'], - pool_progress, 0) - - self.library._client.get_pool_operation_progress = mock.Mock( - return_value=pool_progress) - - complete, actions, eta = self.library._get_pool_operation_progress( - pool['id']) - self.assertEqual(expect_complete, complete) - self.assertEqual(expected_actions, actions) - self.assertEqual(expected_eta, eta) - - @ddt.data(False, True) - def test_get_pool_operation_progress_with_action(self, expect_complete): - """Validate the operation progress is interpreted correctly""" - - expected_action = 'fakeAction' - pool = copy.deepcopy(eseries_fake.STORAGE_POOL) - if expect_complete: - pool_progress = copy.deepcopy( - eseries_fake.FAKE_POOL_ACTION_PROGRESS) - for progress in pool_progress: - progress['currentAction'] = 'none' - else: - pool_progress = copy.deepcopy( - eseries_fake.FAKE_POOL_ACTION_PROGRESS) - pool_progress[0]['currentAction'] = expected_action - - expected_actions = set(action['currentAction'] for action in - pool_progress) - expected_eta = reduce(lambda x, y: x + y['estimatedTimeToCompletion'], - pool_progress, 0) - - self.library._client.get_pool_operation_progress = mock.Mock( - return_value=pool_progress) - - complete, actions, eta = self.library._get_pool_operation_progress( - pool['id'], expected_action) - self.assertEqual(expect_complete, complete) - self.assertEqual(expected_actions, actions) - self.assertEqual(expected_eta, eta) - - @mock.patch('eventlet.greenthread.sleep') - def test_extend_volume(self, _mock_sleep): - """Test volume extend with a thick-provisioned volume""" - - def get_copy_progress(): - for eta in range(5, -1, -1): - action_status = 'none' if eta == 0 else 'remappingDve' - complete = action_status == 'none' - yield complete, action_status, eta - - fake_volume = copy.deepcopy(get_fake_volume()) - volume = copy.deepcopy(eseries_fake.VOLUME) - new_capacity = 10 - volume['objectType'] = 'volume' - self.library._client.expand_volume = mock.Mock() - self.library._get_pool_operation_progress = mock.Mock( - side_effect=get_copy_progress()) - self.library._get_volume = mock.Mock(return_value=volume) - - self.library.extend_volume(fake_volume, new_capacity) - - # Ensure that the extend method waits until the expansion is completed - self.assertEqual(6, - self.library._get_pool_operation_progress.call_count - ) - self.library._client.expand_volume.assert_called_with(volume['id'], - new_capacity, - False) - - def test_extend_volume_thin(self): - """Test volume extend with a thin-provisioned volume""" - - fake_volume = copy.deepcopy(get_fake_volume()) - volume = copy.deepcopy(eseries_fake.VOLUME) - new_capacity = 10 - volume['objectType'] = 'thinVolume' - self.library._client.expand_volume = mock.Mock(return_value=volume) - self.library._get_volume_operation_progress = mock.Mock() - self.library._get_volume = mock.Mock(return_value=volume) - - self.library.extend_volume(fake_volume, new_capacity) - - self.assertFalse(self.library._get_volume_operation_progress.called) - self.library._client.expand_volume.assert_called_with(volume['id'], - new_capacity, - True) - - def test_delete_non_existing_volume(self): - volume2 = get_fake_volume() - # Change to a nonexistent id. - volume2['name_id'] = '88888888-4444-4444-4444-cccccccccccc' - self.assertIsNone(self.library.delete_volume(volume2)) - - def test_map_volume_to_host_volume_not_mapped(self): - """Map the volume directly to destination host.""" - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - - self.library.map_volume_to_host(get_fake_volume(), - eseries_fake.VOLUME, - eseries_fake.INITIATOR_NAME_2) - - self.assertTrue( - self.library._client.get_volume_mappings_for_volume.called) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_map_volume_to_host_volume_not_mapped_host_does_not_exist(self): - """Should create the host map directly to the host.""" - self.mock_object(self.library._client, 'list_hosts', - return_value=[]) - self.mock_object(self.library._client, 'create_host_with_ports', - return_value=eseries_fake.HOST_2) - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - - self.library.map_volume_to_host(get_fake_volume(), - eseries_fake.VOLUME, - eseries_fake.INITIATOR_NAME_2) - - self.assertTrue(self.library._client.create_host_with_ports.called) - self.assertTrue( - self.library._client.get_volume_mappings_for_volume.called) - self.assertTrue(host_mapper.map_volume_to_single_host.called) - - def test_map_volume_to_host_volume_already_mapped(self): - """Should be a no-op.""" - self.mock_object(host_mapper, 'map_volume_to_multiple_hosts', - return_value=eseries_fake.VOLUME_MAPPING) - - self.library.map_volume_to_host(get_fake_volume(), - eseries_fake.VOLUME, - eseries_fake.INITIATOR_NAME) - - self.assertTrue(host_mapper.map_volume_to_multiple_hosts.called) - - -class NetAppEseriesISCSICHAPAuthenticationTestCase(test.TestCase): - """Test behavior when the use_chap_auth configuration option is True.""" - - def setUp(self): - super(NetAppEseriesISCSICHAPAuthenticationTestCase, self).setUp() - config = eseries_fake.create_configuration_eseries() - config.use_chap_auth = True - config.chap_password = None - config.chap_username = None - - kwargs = {'configuration': config} - - self.library = library.NetAppESeriesLibrary("FAKE", **kwargs) - self.library._client = eseries_fake.FakeEseriesClient() - self.library._client.features = mock.Mock() - self.library._client.features = na_utils.Features() - self.library._client.features.add_feature('CHAP_AUTHENTICATION', - supported=True, - min_version="1.53.9010.15") - self.mock_object(self.library, - '_check_storage_system') - self.library.check_for_setup_error() - - def test_initialize_connection_with_chap(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(self.library._client, 'get_volume_mappings', - return_value=[]) - self.mock_object(self.library._client, 'list_hosts', - return_value=[]) - self.mock_object(self.library._client, 'create_host_with_ports', - return_value=[eseries_fake.HOST]) - self.mock_object(host_mapper, 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - mock_configure_chap = ( - self.mock_object(self.library, - '_configure_chap', - return_value=(eseries_fake.FAKE_CHAP_USERNAME, - eseries_fake.FAKE_CHAP_SECRET))) - - properties = self.library.initialize_connection_iscsi( - get_fake_volume(), connector) - - mock_configure_chap.assert_called_with(eseries_fake.FAKE_TARGET_IQN) - self.assertDictEqual(eseries_fake.FAKE_TARGET_DICT, properties) - - def test_configure_chap_with_no_chap_secret_specified(self): - mock_invoke_generate_random_secret = self.mock_object( - volume_utils, - 'generate_password', - return_value=eseries_fake.FAKE_CHAP_SECRET) - mock_invoke_set_chap_authentication = self.mock_object( - self.library._client, - 'set_chap_authentication', - return_value=eseries_fake.FAKE_CHAP_POST_DATA) - - username, password = self.library._configure_chap( - eseries_fake.FAKE_TARGET_IQN) - - self.assertTrue(mock_invoke_generate_random_secret.called) - mock_invoke_set_chap_authentication.assert_called_with( - *eseries_fake.FAKE_CLIENT_CHAP_PARAMETERS) - self.assertEqual(eseries_fake.FAKE_CHAP_USERNAME, username) - self.assertEqual(eseries_fake.FAKE_CHAP_SECRET, password) - - def test_configure_chap_with_no_chap_username_specified(self): - mock_invoke_generate_random_secret = self.mock_object( - volume_utils, - 'generate_password', - return_value=eseries_fake.FAKE_CHAP_SECRET) - mock_invoke_set_chap_authentication = self.mock_object( - self.library._client, - 'set_chap_authentication', - return_value=eseries_fake.FAKE_CHAP_POST_DATA) - mock_log = self.mock_object(library, 'LOG') - warn_msg = 'No CHAP username found for CHAP user' - - username, password = self.library._configure_chap( - eseries_fake.FAKE_TARGET_IQN) - - self.assertTrue(mock_invoke_generate_random_secret.called) - self.assertTrue(bool(mock_log.warning.find(warn_msg))) - mock_invoke_set_chap_authentication.assert_called_with( - *eseries_fake.FAKE_CLIENT_CHAP_PARAMETERS) - self.assertEqual(eseries_fake.FAKE_CHAP_USERNAME, username) - self.assertEqual(eseries_fake.FAKE_CHAP_SECRET, password) - - def test_configure_chap_with_invalid_version(self): - connector = {'initiator': eseries_fake.INITIATOR_NAME} - self.mock_object(self.library._client, - 'get_volume_mappings_for_volume', - return_value=[]) - self.mock_object(host_mapper, - 'map_volume_to_single_host', - return_value=eseries_fake.VOLUME_MAPPING) - self.library._client.features.CHAP_AUTHENTICATION.supported = False - self.library._client.api_version = "1.52.9010.01" - - self.assertRaises(exception.NetAppDriverException, - self.library.initialize_connection_iscsi, - get_fake_volume(), - connector) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_utils.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_utils.py deleted file mode 100644 index 8afe41e7dbe..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_utils.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2014 Clinton Knight. All rights reserved. -# 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. -""" -Mock unit tests for the NetApp E-series driver utility module -""" - -import six - -from cinder import test -from cinder.volume.drivers.netapp.eseries import utils - - -class NetAppEseriesDriverUtilsTestCase(test.TestCase): - - def test_convert_uuid_to_es_fmt(self): - value = 'e67e931a-b2ed-4890-938b-3acc6a517fac' - result = utils.convert_uuid_to_es_fmt(value) - self.assertEqual('4Z7JGGVS5VEJBE4LHLGGUUL7VQ', result) - - def test_convert_es_fmt_to_uuid(self): - value = '4Z7JGGVS5VEJBE4LHLGGUUL7VQ' - result = six.text_type(utils.convert_es_fmt_to_uuid(value)) - self.assertEqual('e67e931a-b2ed-4890-938b-3acc6a517fac', result) diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 3fec3fc8e95..55b312f735d 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -32,7 +32,6 @@ from cinder.volume.drivers.netapp import utils as na_utils LOG = logging.getLogger(__name__) DATAONTAP_PATH = 'cinder.volume.drivers.netapp.dataontap' -ESERIES_PATH = 'cinder.volume.drivers.netapp.eseries' # Add new drivers here, no other code changes required. NETAPP_UNIFIED_DRIVER_REGISTRY = { @@ -41,11 +40,6 @@ NETAPP_UNIFIED_DRIVER_REGISTRY = { 'iscsi': DATAONTAP_PATH + '.iscsi_cmode.NetAppCmodeISCSIDriver', 'nfs': DATAONTAP_PATH + '.nfs_cmode.NetAppCmodeNfsDriver', 'fc': DATAONTAP_PATH + '.fc_cmode.NetAppCmodeFibreChannelDriver' - }, - 'eseries': - { - 'iscsi': ESERIES_PATH + '.iscsi_driver.NetAppEseriesISCSIDriver', - 'fc': ESERIES_PATH + '.fc_driver.NetAppEseriesFibreChannelDriver' }} diff --git a/cinder/volume/drivers/netapp/eseries/__init__.py b/cinder/volume/drivers/netapp/eseries/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py deleted file mode 100644 index 774969b5e4f..00000000000 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ /dev/null @@ -1,1055 +0,0 @@ -# Copyright (c) 2014 NetApp, Inc -# Copyright (c) 2014 Navneet Singh -# Copyright (c) 2015 Alex Meade -# Copyright (c) 2015 Rushil Chugh -# Copyright (c) 2015 Yogesh Kshirsagar -# Copyright (c) 2015 Jose Porrua -# Copyright (c) 2015 Michael Price -# 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. -""" -Client classes for web services. -""" - -import copy -import json -import uuid - -from oslo_log import log as logging -import requests -import six -from six.moves import urllib - -from cinder import exception -from cinder.i18n import _ -import cinder.utils as cinder_utils -from cinder.volume.drivers.netapp.eseries import exception as es_exception -from cinder.volume.drivers.netapp.eseries import utils -from cinder.volume.drivers.netapp import utils as na_utils - - -LOG = logging.getLogger(__name__) - - -class WebserviceClient(object): - """Base client for NetApp Storage web services.""" - - def __init__(self, scheme, host, port, service_path, username, - password, **kwargs): - self._validate_params(scheme, host, port) - self._create_endpoint(scheme, host, port, service_path) - self._username = username - self._password = password - self._init_connection() - - def _validate_params(self, scheme, host, port): - """Does some basic validation for web service params.""" - if host is None or port is None or scheme is None: - msg = _('One of the required inputs from host, ' - 'port or scheme was not found.') - raise exception.InvalidInput(reason=msg) - if scheme not in ('http', 'https'): - raise exception.InvalidInput(reason=_("Invalid transport type.")) - - def _create_endpoint(self, scheme, host, port, service_path): - """Creates end point url for the service.""" - netloc = '%s:%s' % (host, port) - self._endpoint = urllib.parse.urlunparse((scheme, netloc, service_path, - None, None, None)) - - def _init_connection(self): - """Do client specific set up for session and connection pooling.""" - self.conn = requests.Session() - if self._username and self._password: - self.conn.auth = (self._username, self._password) - - def invoke_service(self, method='GET', url=None, params=None, data=None, - headers=None, timeout=None, verify=False): - url = url or self._endpoint - try: - response = self.conn.request(method, url, params, data, - headers=headers, timeout=timeout, - verify=verify) - # Catching error conditions other than the perceived ones. - # Helps propagating only known exceptions back to the caller. - except Exception as e: - LOG.exception("Unexpected error while invoking web service." - " Error - %s.", e) - raise exception.NetAppDriverException( - _("Invoking web service failed.")) - return response - - -class RestClient(WebserviceClient): - """REST client specific to e-series storage service.""" - - ID = 'id' - WWN = 'worldWideName' - NAME = 'label' - - ASUP_VALID_VERSION = (1, 52, 9000, 3) - CHAP_VALID_VERSION = (1, 53, 9010, 15) - # We need to check for both the release and the pre-release versions - SSC_VALID_VERSIONS = ((1, 53, 9000, 1), (1, 53, 9010, 17)) - REST_1_3_VERSION = (1, 53, 9000, 1) - REST_1_4_VERSIONS = ((1, 54, 9000, 1), (1, 54, 9090, 0)) - - RESOURCE_PATHS = { - 'volumes': '/storage-systems/{system-id}/volumes', - 'volume': '/storage-systems/{system-id}/volumes/{object-id}', - 'pool_operation_progress': - '/storage-systems/{system-id}/storage-pools/{object-id}' - '/action-progress', - 'volume_expand': - '/storage-systems/{system-id}/volumes/{object-id}/expand', - 'thin_volume_expand': - '/storage-systems/{system-id}/thin-volumes/{object-id}/expand', - 'ssc_volumes': '/storage-systems/{system-id}/ssc/volumes', - 'ssc_volume': '/storage-systems/{system-id}/ssc/volumes/{object-id}', - 'snapshot_groups': '/storage-systems/{system-id}/snapshot-groups', - 'snapshot_group': - '/storage-systems/{system-id}/snapshot-groups/{object-id}', - 'snapshot_volumes': '/storage-systems/{system-id}/snapshot-volumes', - 'snapshot_volume': - '/storage-systems/{system-id}/snapshot-volumes/{object-id}', - 'snapshot_images': '/storage-systems/{system-id}/snapshot-images', - 'snapshot_image': - '/storage-systems/{system-id}/snapshot-images/{object-id}', - 'cgroup': - '/storage-systems/{system-id}/consistency-groups/{object-id}', - 'cgroups': '/storage-systems/{system-id}/consistency-groups', - 'cgroup_members': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/member-volumes', - 'cgroup_member': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/member-volumes/{vol-id}', - 'cgroup_snapshots': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/snapshots', - 'cgroup_snapshot': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/snapshots/{seq-num}', - 'cgroup_snapshots_by_seq': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/snapshots/{seq-num}', - 'cgroup_cgsnap_view': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/views/{seq-num}', - 'cgroup_cgsnap_views': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/views/', - 'cgroup_snapshot_views': - '/storage-systems/{system-id}/consistency-groups/{object-id}' - '/views/{view-id}/views', - 'persistent-stores': '/storage-systems/{' - 'system-id}/persistent-records/', - 'persistent-store': '/storage-systems/{' - 'system-id}/persistent-records/{key}' - } - - def __init__(self, scheme, host, port, service_path, username, - password, **kwargs): - - super(RestClient, self).__init__(scheme, host, port, service_path, - username, password, **kwargs) - - kwargs = kwargs or {} - - self._system_id = kwargs.get('system_id') - self._content_type = kwargs.get('content_type') or 'json' - - def _init_features(self): - """Sets up and initializes E-Series feature support map.""" - self.features = na_utils.Features() - self.api_operating_mode, self.api_version = self.get_eseries_api_info( - verify=False) - - api_version_tuple = tuple(int(version) - for version in self.api_version.split('.')) - - chap_valid_version = self._validate_version( - self.CHAP_VALID_VERSION, api_version_tuple) - self.features.add_feature('CHAP_AUTHENTICATION', - supported=chap_valid_version, - min_version=self._version_tuple_to_str( - self.CHAP_VALID_VERSION)) - - asup_api_valid_version = self._validate_version( - self.ASUP_VALID_VERSION, api_version_tuple) - self.features.add_feature('AUTOSUPPORT', - supported=asup_api_valid_version, - min_version=self._version_tuple_to_str( - self.ASUP_VALID_VERSION)) - - rest_1_3_api_valid_version = self._validate_version( - self.REST_1_3_VERSION, api_version_tuple) - - rest_1_4_api_valid_version = any( - self._validate_version(valid_version, api_version_tuple) - for valid_version in self.REST_1_4_VERSIONS) - - ssc_api_valid_version = any(self._validate_version(valid_version, - api_version_tuple) - for valid_version - in self.SSC_VALID_VERSIONS) - self.features.add_feature('SSC_API_V2', - supported=ssc_api_valid_version, - min_version=self._version_tuple_to_str( - self.SSC_VALID_VERSIONS[0])) - self.features.add_feature( - 'REST_1_3_RELEASE', supported=rest_1_3_api_valid_version, - min_version=self._version_tuple_to_str(self.REST_1_3_VERSION)) - self.features.add_feature( - 'REST_1_4_RELEASE', supported=rest_1_4_api_valid_version, - min_version=self._version_tuple_to_str(self.REST_1_4_VERSIONS[0])) - - def _version_tuple_to_str(self, version): - return ".".join([str(part) for part in version]) - - def _validate_version(self, version, actual_version): - """Determine if version is newer than, or equal to the actual version - - The proxy version number is formatted as AA.BB.CCCC.DDDD - A: Major version part 1 - B: Major version part 2 - C: Release version: 9000->Release, 9010->Pre-release, 9090->Integration - D: Minor version - - Examples: - 02.53.9000.0010 - 02.52.9010.0001 - - Note: The build version is actually 'newer' the lower the release - (CCCC) number is. - - :param version: The version to validate - :param actual_version: The running version of the Webservice - :returns: True if the actual_version is equal or newer than the - current running version, otherwise False - """ - major_1, major_2, release, minor = version - actual_major_1, actual_major_2, actual_release, actual_minor = ( - actual_version) - - # We need to invert the release number for it to work with this - # comparison - return (actual_major_1, actual_major_2, 10000 - actual_release, - actual_minor) >= (major_1, major_2, 10000 - release, minor) - - def set_system_id(self, system_id): - """Set the storage system id.""" - self._system_id = system_id - - def get_system_id(self): - """Get the storage system id.""" - return getattr(self, '_system_id', None) - - def _get_resource_url(self, path, use_system=True, **kwargs): - """Creates end point url for rest service.""" - kwargs = kwargs or {} - if use_system: - if not self._system_id: - raise exception.NotFound(_('Storage system id not set.')) - kwargs['system-id'] = self._system_id - path = path.format(**kwargs) - if not self._endpoint.endswith('/'): - self._endpoint = '%s/' % self._endpoint - return urllib.parse.urljoin(self._endpoint, path.lstrip('/')) - - def _invoke(self, method, path, data=None, use_system=True, - timeout=None, verify=False, **kwargs): - """Invokes end point for resource on path.""" - url = self._get_resource_url(path, use_system, **kwargs) - if self._content_type == 'json': - headers = {'Accept': 'application/json', - 'Content-Type': 'application/json'} - if cinder_utils.TRACE_API: - self._log_http_request(method, url, headers, data) - data = json.dumps(data) if data else None - res = self.invoke_service(method, url, data=data, - headers=headers, - timeout=timeout, verify=verify) - - try: - res_dict = res.json() if res.text else None - # This should only occur if we expected JSON, but were sent - # something else - except ValueError: - res_dict = None - - if cinder_utils.TRACE_API: - self._log_http_response(res.status_code, dict(res.headers), - res_dict) - - self._eval_response(res) - return res_dict - else: - raise exception.NetAppDriverException( - _("Content type not supported.")) - - def _to_pretty_dict_string(self, data): - """Convert specified dict to pretty printed string.""" - return json.dumps(data, sort_keys=True, - indent=2, separators=(',', ': ')) - - def _log_http_request(self, verb, url, headers, body): - scrubbed_body = copy.deepcopy(body) - if scrubbed_body: - if 'password' in scrubbed_body: - scrubbed_body['password'] = "****" - if 'storedPassword' in scrubbed_body: - scrubbed_body['storedPassword'] = "****" - - params = {'verb': verb, 'path': url, - 'body': self._to_pretty_dict_string(scrubbed_body) or "", - 'headers': self._to_pretty_dict_string(headers)} - LOG.debug("Invoking ESeries Rest API, Request:\n" - "HTTP Verb: %(verb)s\n" - "URL Path: %(path)s\n" - "HTTP Headers:\n" - "%(headers)s\n" - "Body:\n" - "%(body)s\n", (params)) - - def _log_http_response(self, status, headers, body): - params = {'status': status, - 'body': self._to_pretty_dict_string(body) or "", - 'headers': self._to_pretty_dict_string(headers)} - LOG.debug("ESeries Rest API, Response:\n" - "HTTP Status Code: %(status)s\n" - "HTTP Headers:\n" - "%(headers)s\n" - "Body:\n" - "%(body)s\n", (params)) - - def _eval_response(self, response): - """Evaluates response before passing result to invoker.""" - status_code = int(response.status_code) - # codes >= 300 are not ok and to be treated as errors - if status_code >= 300: - # Response code 422 returns error code and message - if status_code == 422: - msg = _("Response error - %s.") % response.text - json_response = response.json() - if json_response is not None: - ret_code = json_response.get('retcode', '') - if ret_code == '30' or ret_code == 'authFailPassword': - msg = _("The storage array password for %s is " - "incorrect, please update the configured " - "password.") % self._system_id - elif status_code == 424: - msg = _("Response error - The storage-system is offline.") - else: - msg = _("Response error code - %s.") % status_code - raise es_exception.WebServiceException(msg, - status_code=status_code) - - def _get_volume_api_path(self, path_key): - """Retrieve the correct API path based on API availability - - :param path_key: The volume API to request (volume or volumes) - :raise KeyError: If the path_key is not valid - """ - if self.features.SSC_API_V2: - path_key = 'ssc_' + path_key - return self.RESOURCE_PATHS[path_key] - - def create_volume(self, pool, label, size, unit='gb', seg_size=0, - read_cache=None, write_cache=None, flash_cache=None, - data_assurance=None, thin_provision=False): - """Creates a volume on array with the configured attributes - - Note: if read_cache, write_cache, flash_cache, or data_assurance - are not provided, the default will be utilized by the Webservice. - - :param pool: The pool unique identifier - :param label: The unqiue label for the volume - :param size: The capacity in units - :param unit: The unit for capacity - :param seg_size: The segment size for the volume, expressed in KB. - Default will allow the Webservice to choose. - :param read_cache: If true, enable read caching, if false, - explicitly disable it. - :param write_cache: If true, enable write caching, if false, - explicitly disable it. - :param flash_cache: If true, add the volume to a Flash Cache - :param data_assurance: If true, enable the Data Assurance capability - :returns: The created volume - """ - - # Utilize the new API if it is available - if self.features.SSC_API_V2: - path = "/storage-systems/{system-id}/ssc/volumes" - data = {'poolId': pool, 'name': label, 'sizeUnit': unit, - 'size': int(size), 'dataAssuranceEnable': data_assurance, - 'flashCacheEnable': flash_cache, - 'readCacheEnable': read_cache, - 'writeCacheEnable': write_cache, - 'thinProvision': thin_provision} - # Use the old API - else: - # Determine if there are were extra specs provided that are not - # supported - extra_specs = [read_cache, write_cache] - unsupported_spec = any([spec is not None for spec in extra_specs]) - if(unsupported_spec): - msg = _("E-series proxy API version %(current_version)s does " - "not support full set of SSC extra specs. The proxy" - " version must be at at least %(min_version)s.") - min_version = self.features.SSC_API_V2.minimum_version - raise exception.NetAppDriverException(msg % - {'current_version': - self.api_version, - 'min_version': - min_version}) - - path = "/storage-systems/{system-id}/volumes" - data = {'poolId': pool, 'name': label, 'sizeUnit': unit, - 'size': int(size), 'segSize': seg_size} - return self._invoke('POST', path, data) - - def delete_volume(self, object_id): - """Deletes given volume from array.""" - if self.features.SSC_API_V2: - path = self.RESOURCE_PATHS.get('ssc_volume') - else: - path = self.RESOURCE_PATHS.get('volume') - return self._invoke('DELETE', path, **{'object-id': object_id}) - - def list_volumes(self): - """Lists all volumes in storage array.""" - if self.features.SSC_API_V2: - path = self.RESOURCE_PATHS.get('ssc_volumes') - else: - path = self.RESOURCE_PATHS.get('volumes') - - return self._invoke('GET', path) - - def list_volume(self, object_id): - """Retrieve the given volume from array. - - :param object_id: The volume id, label, or wwn - :returns: The volume identified by object_id - :raise: VolumeNotFound if the volume could not be found - """ - - if self.features.SSC_API_V2: - return self._list_volume_v2(object_id) - # The new API is not available, - else: - # Search for the volume with label, id, or wwn. - return self._list_volume_v1(object_id) - - def _list_volume_v1(self, object_id): - # Search for the volume with label, id, or wwn. - for vol in self.list_volumes(): - if (object_id == vol.get(self.NAME) or object_id == vol.get( - self.WWN) or object_id == vol.get(self.ID)): - return vol - # The volume could not be found - raise exception.VolumeNotFound(volume_id=object_id) - - def _list_volume_v2(self, object_id): - path = self.RESOURCE_PATHS.get('ssc_volume') - try: - return self._invoke('GET', path, **{'object-id': object_id}) - except es_exception.WebServiceException as e: - if 404 == e.status_code: - raise exception.VolumeNotFound(volume_id=object_id) - else: - raise - - def update_volume(self, object_id, label): - """Renames given volume on array.""" - if self.features.SSC_API_V2: - path = self.RESOURCE_PATHS.get('ssc_volume') - else: - path = self.RESOURCE_PATHS.get('volume') - data = {'name': label} - return self._invoke('POST', path, data, **{'object-id': object_id}) - - def create_consistency_group(self, name, warn_at_percent_full=75, - rollback_priority='medium', - full_policy='failbasewrites'): - """Define a new consistency group""" - path = self.RESOURCE_PATHS.get('cgroups') - data = { - 'name': name, - 'fullWarnThresholdPercent': warn_at_percent_full, - 'repositoryFullPolicy': full_policy, - # A non-zero threshold enables auto-deletion - 'autoDeleteThreshold': 0, - 'rollbackPriority': rollback_priority, - } - - return self._invoke('POST', path, data) - - def get_consistency_group(self, object_id): - """Retrieve the consistency group identified by object_id""" - path = self.RESOURCE_PATHS.get('cgroup') - - return self._invoke('GET', path, **{'object-id': object_id}) - - def list_consistency_groups(self): - """Retrieve all consistency groups defined on the array""" - path = self.RESOURCE_PATHS.get('cgroups') - - return self._invoke('GET', path) - - def delete_consistency_group(self, object_id): - path = self.RESOURCE_PATHS.get('cgroup') - - self._invoke('DELETE', path, **{'object-id': object_id}) - - def add_consistency_group_member(self, volume_id, cg_id, - repo_percent=20.0): - """Add a volume to a consistency group - - :param volume_id the eseries volume id - :param cg_id: the eseries cg id - :param repo_percent: percentage capacity of the volume to use for - capacity of the copy-on-write repository - """ - path = self.RESOURCE_PATHS.get('cgroup_members') - data = {'volumeId': volume_id, 'repositoryPercent': repo_percent} - - return self._invoke('POST', path, data, **{'object-id': cg_id}) - - def remove_consistency_group_member(self, volume_id, cg_id): - """Remove a volume from a consistency group""" - path = self.RESOURCE_PATHS.get('cgroup_member') - - self._invoke('DELETE', path, **{'object-id': cg_id, - 'vol-id': volume_id}) - - def create_consistency_group_snapshot(self, cg_id): - """Define a consistency group snapshot""" - path = self.RESOURCE_PATHS.get('cgroup_snapshots') - - return self._invoke('POST', path, **{'object-id': cg_id}) - - def delete_consistency_group_snapshot(self, cg_id, seq_num): - """Define a consistency group snapshot""" - path = self.RESOURCE_PATHS.get('cgroup_snapshot') - - return self._invoke('DELETE', path, **{'object-id': cg_id, - 'seq-num': seq_num}) - - def get_consistency_group_snapshots(self, cg_id): - """Retrieve all snapshots defined for a consistency group""" - path = self.RESOURCE_PATHS.get('cgroup_snapshots') - - return self._invoke('GET', path, **{'object-id': cg_id}) - - def create_cg_snapshot_view(self, cg_id, name, snap_id): - """Define a snapshot view for the cgsnapshot - - In order to define a snapshot view for a snapshot defined under a - consistency group, the view must be defined at the cgsnapshot - level. - - :param cg_id: E-Series cg identifier - :param name: the label for the view - :param snap_id: E-Series snapshot view to locate - :raise NetAppDriverException: if the snapshot view cannot be - located for the snapshot identified - by snap_id - :return: snapshot view for snapshot identified by snap_id - """ - path = self.RESOURCE_PATHS.get('cgroup_cgsnap_views') - - data = { - 'name': name, - 'accessMode': 'readOnly', - # Only define a view for this snapshot - 'pitId': snap_id, - } - # Define a view for the cgsnapshot - cgsnapshot_view = self._invoke( - 'POST', path, data, **{'object-id': cg_id}) - - # Retrieve the snapshot views associated with our cgsnapshot view - views = self.list_cg_snapshot_views(cg_id, cgsnapshot_view[ - 'cgViewRef']) - # Find the snapshot view defined for our snapshot - for view in views: - if view['basePIT'] == snap_id: - return view - else: - try: - self.delete_cg_snapshot_view(cg_id, cgsnapshot_view['id']) - finally: - raise exception.NetAppDriverException( - 'Unable to create snapshot view.') - - def list_cg_snapshot_views(self, cg_id, view_id): - path = self.RESOURCE_PATHS.get('cgroup_snapshot_views') - - return self._invoke('GET', path, **{'object-id': cg_id, - 'view-id': view_id}) - - def delete_cg_snapshot_view(self, cg_id, view_id): - path = self.RESOURCE_PATHS.get('cgroup_snap_view') - - return self._invoke('DELETE', path, **{'object-id': cg_id, - 'view-id': view_id}) - - def get_pool_operation_progress(self, object_id): - """Retrieve the progress long-running operations on a storage pool - - Example: - - .. code-block:: python - - [ - { - "volumeRef": "3232....", # Volume being referenced - "progressPercentage": 0, # Approxmate percent complete - "estimatedTimeToCompletion": 0, # ETA in minutes - "currentAction": "none" # Current volume action - } - ... - ] - - :param object_id: A pool id - :returns: A dict representing the action progress - """ - path = self.RESOURCE_PATHS.get('pool_operation_progress') - return self._invoke('GET', path, **{'object-id': object_id}) - - def expand_volume(self, object_id, new_capacity, thin_provisioned, - capacity_unit='gb'): - """Increase the capacity of a volume""" - if thin_provisioned: - path = self.RESOURCE_PATHS.get('thin_volume_expand') - data = {'newVirtualSize': new_capacity, 'sizeUnit': capacity_unit, - 'newRepositorySize': new_capacity} - return self._invoke('POST', path, data, **{'object-id': object_id}) - else: - path = self.RESOURCE_PATHS.get('volume_expand') - data = {'expansionSize': new_capacity, 'sizeUnit': capacity_unit} - return self._invoke('POST', path, data, **{'object-id': object_id}) - - def get_volume_mappings(self): - """Creates volume mapping on array.""" - path = "/storage-systems/{system-id}/volume-mappings" - return self._invoke('GET', path) - - def get_volume_mappings_for_volume(self, volume): - """Gets all host mappings for given volume from array.""" - mappings = self.get_volume_mappings() or [] - return [x for x in mappings - if x.get('volumeRef') == volume['volumeRef']] - - def get_volume_mappings_for_host(self, host_ref): - """Gets all volume mappings for given host from array.""" - mappings = self.get_volume_mappings() or [] - return [x for x in mappings if x.get('mapRef') == host_ref] - - def get_volume_mappings_for_host_group(self, hg_ref): - """Gets all volume mappings for given host group from array.""" - mappings = self.get_volume_mappings() or [] - return [x for x in mappings if x.get('mapRef') == hg_ref] - - def create_volume_mapping(self, object_id, target_id, lun): - """Creates volume mapping on array.""" - path = "/storage-systems/{system-id}/volume-mappings" - data = {'mappableObjectId': object_id, 'targetId': target_id, - 'lun': lun} - return self._invoke('POST', path, data) - - def delete_volume_mapping(self, map_object_id): - """Deletes given volume mapping from array.""" - path = "/storage-systems/{system-id}/volume-mappings/{object-id}" - return self._invoke('DELETE', path, **{'object-id': map_object_id}) - - def move_volume_mapping_via_symbol(self, map_ref, to_ref, lun_id): - """Moves a map from one host/host_group object to another.""" - - path = "/storage-systems/{system-id}/symbol/moveLUNMapping" - data = {'lunMappingRef': map_ref, - 'lun': int(lun_id), - 'mapRef': to_ref} - return_code = self._invoke('POST', path, data) - if return_code == 'ok': - return {'lun': lun_id} - msg = _("Failed to move LUN mapping. Return code: %s") % return_code - raise exception.NetAppDriverException(msg) - - def list_hardware_inventory(self): - """Lists objects in the hardware inventory.""" - path = "/storage-systems/{system-id}/hardware-inventory" - return self._invoke('GET', path) - - def list_target_wwpns(self): - """Lists the world-wide port names of the target.""" - inventory = self.list_hardware_inventory() - fc_ports = inventory.get("fibrePorts", []) - wwpns = [port['portName'] for port in fc_ports] - return wwpns - - def create_host_group(self, label): - """Creates a host group on the array.""" - path = "/storage-systems/{system-id}/host-groups" - data = {'name': label} - return self._invoke('POST', path, data) - - def get_host_group(self, host_group_ref): - """Gets a single host group from the array.""" - path = "/storage-systems/{system-id}/host-groups/{object-id}" - try: - return self._invoke('GET', path, **{'object-id': host_group_ref}) - except exception.NetAppDriverException: - raise exception.NotFound(_("Host group with ref %s not found") % - host_group_ref) - - def get_host_group_by_name(self, name): - """Gets a single host group by name from the array.""" - host_groups = self.list_host_groups() - matching = [host_group for host_group in host_groups - if host_group['label'] == name] - if len(matching): - return matching[0] - raise exception.NotFound(_("Host group with name %s not found") % name) - - def list_host_groups(self): - """Lists host groups on the array.""" - path = "/storage-systems/{system-id}/host-groups" - return self._invoke('GET', path) - - def list_hosts(self): - """Lists host objects in the system.""" - path = "/storage-systems/{system-id}/hosts" - return self._invoke('GET', path) - - def create_host(self, label, host_type, ports=None, group_id=None): - """Creates host on array.""" - path = "/storage-systems/{system-id}/hosts" - data = {'name': label, 'hostType': host_type} - data.setdefault('groupId', group_id if group_id else None) - data.setdefault('ports', ports if ports else None) - return self._invoke('POST', path, data) - - def create_host_with_ports(self, label, host_type, port_ids, - port_type='iscsi', group_id=None): - """Creates host on array with given port information.""" - if port_type == 'fc': - port_ids = [six.text_type(wwpn).replace(':', '') - for wwpn in port_ids] - ports = [] - for port_id in port_ids: - port_label = utils.convert_uuid_to_es_fmt(uuid.uuid4()) - port = {'type': port_type, 'port': port_id, 'label': port_label} - ports.append(port) - return self.create_host(label, host_type, ports, group_id) - - def update_host(self, host_ref, data): - """Updates host type for a given host.""" - path = "/storage-systems/{system-id}/hosts/{object-id}" - return self._invoke('POST', path, data, **{'object-id': host_ref}) - - def get_host(self, host_ref): - """Gets a single host from the array.""" - path = "/storage-systems/{system-id}/hosts/{object-id}" - return self._invoke('GET', path, **{'object-id': host_ref}) - - def update_host_type(self, host_ref, host_type): - """Updates host type for a given host.""" - data = {'hostType': host_type} - return self.update_host(host_ref, data) - - def set_host_group_for_host(self, host_ref, host_group_ref=utils.NULL_REF): - """Sets or clears which host group a host is in.""" - data = {'groupId': host_group_ref} - self.update_host(host_ref, data) - - def list_host_types(self): - """Lists host types in storage system.""" - path = "/storage-systems/{system-id}/host-types" - return self._invoke('GET', path) - - def list_snapshot_groups(self): - """Lists snapshot groups.""" - path = self.RESOURCE_PATHS['snapshot_groups'] - return self._invoke('GET', path) - - def list_snapshot_group(self, object_id): - """Retrieve given snapshot group from the array.""" - path = self.RESOURCE_PATHS['snapshot_group'] - return self._invoke('GET', path, **{'object-id': object_id}) - - def create_snapshot_group(self, label, object_id, storage_pool_id=None, - repo_percent=99, warn_thres=99, auto_del_limit=0, - full_policy='failbasewrites'): - """Creates snapshot group on array.""" - path = self.RESOURCE_PATHS['snapshot_groups'] - data = {'baseMappableObjectId': object_id, 'name': label, - 'storagePoolId': storage_pool_id, - 'repositoryPercentage': repo_percent, - 'warningThreshold': warn_thres, - 'autoDeleteLimit': auto_del_limit, 'fullPolicy': full_policy} - return self._invoke('POST', path, data) - - def update_snapshot_group(self, group_id, label): - """Modify a snapshot group on the array.""" - path = self.RESOURCE_PATHS['snapshot_group'] - data = {'name': label} - return self._invoke('POST', path, data, **{'object-id': group_id}) - - def delete_snapshot_group(self, object_id): - """Deletes given snapshot group from array.""" - path = self.RESOURCE_PATHS['snapshot_group'] - return self._invoke('DELETE', path, **{'object-id': object_id}) - - def create_snapshot_image(self, group_id): - """Creates snapshot image in snapshot group.""" - path = self.RESOURCE_PATHS['snapshot_images'] - data = {'groupId': group_id} - return self._invoke('POST', path, data) - - def delete_snapshot_image(self, object_id): - """Deletes given snapshot image in snapshot group.""" - path = self.RESOURCE_PATHS['snapshot_image'] - return self._invoke('DELETE', path, **{'object-id': object_id}) - - def list_snapshot_image(self, object_id): - """Retrieve given snapshot image from the array.""" - path = self.RESOURCE_PATHS['snapshot_image'] - return self._invoke('GET', path, **{'object-id': object_id}) - - def list_snapshot_images(self): - """Lists snapshot images.""" - path = self.RESOURCE_PATHS['snapshot_images'] - return self._invoke('GET', path) - - def create_snapshot_volume(self, image_id, label, base_object_id, - storage_pool_id=None, - repo_percent=99, full_thres=99, - view_mode='readOnly'): - """Creates snapshot volume.""" - path = self.RESOURCE_PATHS['snapshot_volumes'] - data = {'snapshotImageId': image_id, 'fullThreshold': full_thres, - 'storagePoolId': storage_pool_id, - 'name': label, 'viewMode': view_mode, - 'repositoryPercentage': repo_percent, - 'baseMappableObjectId': base_object_id, - 'repositoryPoolId': storage_pool_id} - return self._invoke('POST', path, data) - - def update_snapshot_volume(self, snap_vol_id, label=None, full_thres=None): - """Modify an existing snapshot volume.""" - path = self.RESOURCE_PATHS['snapshot_volume'] - data = {'name': label, 'fullThreshold': full_thres} - return self._invoke('POST', path, data, **{'object-id': snap_vol_id}) - - def delete_snapshot_volume(self, object_id): - """Deletes given snapshot volume.""" - path = self.RESOURCE_PATHS['snapshot_volume'] - return self._invoke('DELETE', path, **{'object-id': object_id}) - - def list_snapshot_volumes(self): - """Lists snapshot volumes/views defined on the array.""" - path = self.RESOURCE_PATHS['snapshot_volumes'] - return self._invoke('GET', path) - - def list_ssc_storage_pools(self): - """Lists pools and their service quality defined on the array.""" - path = "/storage-systems/{system-id}/ssc/pools" - return self._invoke('GET', path) - - def get_ssc_storage_pool(self, volume_group_ref): - """Get storage pool service quality information from the array.""" - path = "/storage-systems/{system-id}/ssc/pools/{object-id}" - return self._invoke('GET', path, **{'object-id': volume_group_ref}) - - def list_storage_pools(self): - """Lists storage pools in the array.""" - path = "/storage-systems/{system-id}/storage-pools" - return self._invoke('GET', path) - - def get_storage_pool(self, volume_group_ref): - """Get storage pool information from the array.""" - path = "/storage-systems/{system-id}/storage-pools/{object-id}" - return self._invoke('GET', path, **{'object-id': volume_group_ref}) - - def list_drives(self): - """Lists drives in the array.""" - path = "/storage-systems/{system-id}/drives" - return self._invoke('GET', path) - - def list_storage_systems(self): - """Lists managed storage systems registered with web service.""" - path = "/storage-systems" - return self._invoke('GET', path, use_system=False) - - def list_storage_system(self): - """List current storage system registered with web service.""" - path = "/storage-systems/{system-id}" - return self._invoke('GET', path) - - def register_storage_system(self, controller_addresses, password=None, - wwn=None): - """Registers storage system with web service.""" - path = "/storage-systems" - data = {'controllerAddresses': controller_addresses} - data.setdefault('wwn', wwn if wwn else None) - data.setdefault('password', password if password else None) - return self._invoke('POST', path, data, use_system=False) - - def update_stored_system_password(self, password): - """Update array password stored on web service.""" - path = "/storage-systems/{system-id}" - data = {'storedPassword': password} - return self._invoke('POST', path, data) - - def create_volume_copy_job(self, src_id, tgt_id, priority='priority4', - tgt_wrt_protected='true'): - """Creates a volume copy job.""" - path = "/storage-systems/{system-id}/volume-copy-jobs" - data = {'sourceId': src_id, 'targetId': tgt_id, - 'copyPriority': priority, - 'targetWriteProtected': tgt_wrt_protected} - return self._invoke('POST', path, data) - - def control_volume_copy_job(self, obj_id, control='start'): - """Controls a volume copy job.""" - path = ("/storage-systems/{system-id}/volume-copy-jobs-control" - "/{object-id}?control={String}") - return self._invoke('PUT', path, **{'object-id': obj_id, - 'String': control}) - - def list_vol_copy_job(self, object_id): - """List volume copy job.""" - path = "/storage-systems/{system-id}/volume-copy-jobs/{object-id}" - return self._invoke('GET', path, **{'object-id': object_id}) - - def delete_vol_copy_job(self, object_id): - """Delete volume copy job.""" - path = "/storage-systems/{system-id}/volume-copy-jobs/{object-id}" - return self._invoke('DELETE', path, **{'object-id': object_id}) - - def set_chap_authentication(self, target_iqn, chap_username, - chap_password): - """Configures CHAP credentials for target IQN from backend.""" - path = "/storage-systems/{system-id}/iscsi/target-settings/" - data = { - 'iqn': target_iqn, - 'enableChapAuthentication': True, - 'alias': chap_username, - 'authMethod': 'chap', - 'chapSecret': chap_password, - } - return self._invoke('POST', path, data) - - def add_autosupport_data(self, key, data): - """Register driver statistics via autosupport log.""" - path = ('/key-values/%s' % key) - self._invoke('POST', path, json.dumps(data)) - - def set_counter(self, key, value): - path = ('/counters/%s/setCounter?value=%d' % (key, value)) - self._invoke('POST', path) - - def get_asup_info(self): - """Returns a dictionary of relevant autosupport information. - - Currently returned fields are: - model -- E-series model name - serial_numbers -- Serial number for each controller - firmware_version -- Version of active firmware - chassis_sn -- Serial number for whole chassis - """ - asup_info = {} - - controllers = self.list_hardware_inventory().get('controllers') - if controllers: - asup_info['model'] = controllers[0].get('modelName', 'unknown') - serial_numbers = [value['serialNumber'].rstrip() - for __, value in enumerate(controllers)] - serial_numbers.sort() - for index, value in enumerate(serial_numbers): - if not value: - serial_numbers[index] = 'unknown' - asup_info['serial_numbers'] = serial_numbers - else: - asup_info['model'] = 'unknown' - asup_info['serial_numbers'] = ['unknown', 'unknown'] - - system_info = self.list_storage_system() - if system_info: - asup_info['firmware_version'] = system_info['fwVersion'] - asup_info['chassis_sn'] = system_info['chassisSerialNumber'] - else: - asup_info['firmware_version'] = 'unknown' - asup_info['chassis_sn'] = 'unknown' - - return asup_info - - def get_eseries_api_info(self, verify=False): - """Get E-Series API information from the array.""" - api_operating_mode = 'embedded' - path = 'devmgr/utils/about' - headers = {'Content-Type': 'application/json', - 'Accept': 'application/json'} - url = self._get_resource_url(path, True).replace( - '/devmgr/v2', '', 1) - result = self.invoke_service(method='GET', url=url, - headers=headers, - verify=verify) - about_response_dict = result.json() - mode_is_proxy = about_response_dict['runningAsProxy'] - if mode_is_proxy: - api_operating_mode = 'proxy' - return api_operating_mode, about_response_dict['version'] - - def list_backend_store(self, key): - """Retrieve data by key from the persistent store on the backend. - - Example response: {"key": "cinder-snapshots", "value": "[]"} - - :param key: the persistent store to retrieve - :returns: a json body representing the value of the store, - or an empty json object - """ - path = self.RESOURCE_PATHS.get('persistent-store') - try: - resp = self._invoke('GET', path, **{'key': key}) - except exception.NetAppDriverException: - return dict() - else: - data = resp['value'] - if data: - return json.loads(data) - return dict() - - def save_backend_store(self, key, store_data): - """Save a json value to the persistent storage on the backend. - - The storage backend provides a small amount of persistent storage - that we can utilize for storing driver information. - - :param key: The key utilized for storing/retrieving the data - :param store_data: a python data structure that will be stored as a - json value - """ - path = self.RESOURCE_PATHS.get('persistent-stores') - store_data = json.dumps(store_data, separators=(',', ':')) - - data = { - 'key': key, - 'value': store_data - } - - self._invoke('POST', path, data) diff --git a/cinder/volume/drivers/netapp/eseries/exception.py b/cinder/volume/drivers/netapp/eseries/exception.py deleted file mode 100644 index c2c517127cf..00000000000 --- a/cinder/volume/drivers/netapp/eseries/exception.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2015 Alex Meade. All Rights Reserved. -# Copyright (c) 2015 Michael Price. 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 import exception -from cinder.i18n import _ - - -class VolumeNotMapped(exception.NetAppDriverException): - message = _("Volume %(volume_id)s is not currently mapped to host " - "%(host)s") - - -class UnsupportedHostGroup(exception.NetAppDriverException): - message = _("Volume %(volume_id)s is currently mapped to unsupported " - "host group %(group)s") - - -class WebServiceException(exception.NetAppDriverException): - def __init__(self, message=None, status_code=None): - self.status_code = status_code - super(WebServiceException, self).__init__(message=message) diff --git a/cinder/volume/drivers/netapp/eseries/fc_driver.py b/cinder/volume/drivers/netapp/eseries/fc_driver.py deleted file mode 100644 index 87da283ece0..00000000000 --- a/cinder/volume/drivers/netapp/eseries/fc_driver.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) - 2014, Alex Meade. All rights reserved. -# Copyright (c) - 2015, Yogesh Kshirsagar. 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. -""" -Volume driver for NetApp E-Series FibreChannel storage systems. -""" - -from cinder import interface -from cinder.volume import driver -from cinder.volume.drivers.netapp.eseries import library -from cinder.volume.drivers.netapp import utils as na_utils -from cinder.zonemanager import utils as fczm_utils - - -@interface.volumedriver -class NetAppEseriesFibreChannelDriver(driver.BaseVD, - driver.ManageableVD): - """NetApp E-Series FibreChannel volume driver.""" - - DRIVER_NAME = 'NetApp_FibreChannel_ESeries' - - # ThirdPartySystems wiki page - CI_WIKI_NAME = "NetApp_Eseries_CI" - VERSION = library.NetAppESeriesLibrary.VERSION - - def __init__(self, *args, **kwargs): - super(NetAppEseriesFibreChannelDriver, self).__init__(*args, **kwargs) - na_utils.validate_instantiation(**kwargs) - self.library = library.NetAppESeriesLibrary(self.DRIVER_NAME, - 'FC', **kwargs) - - def do_setup(self, context): - self.library.do_setup(context) - - def check_for_setup_error(self): - self.library.check_for_setup_error() - - def create_volume(self, volume): - self.library.create_volume(volume) - - def create_volume_from_snapshot(self, volume, snapshot): - self.library.create_volume_from_snapshot(volume, snapshot) - - def create_cloned_volume(self, volume, src_vref): - self.library.create_cloned_volume(volume, src_vref) - - def delete_volume(self, volume): - self.library.delete_volume(volume) - - def create_snapshot(self, snapshot): - return self.library.create_snapshot(snapshot) - - def delete_snapshot(self, snapshot): - self.library.delete_snapshot(snapshot) - - def get_volume_stats(self, refresh=False): - return self.library.get_volume_stats(refresh) - - def extend_volume(self, volume, new_size): - self.library.extend_volume(volume, new_size) - - def ensure_export(self, context, volume): - return self.library.ensure_export(context, volume) - - def create_export(self, context, volume, connector): - return self.library.create_export(context, volume) - - def remove_export(self, context, volume): - self.library.remove_export(context, volume) - - def manage_existing(self, volume, existing_ref): - return self.library.manage_existing(volume, existing_ref) - - def manage_existing_get_size(self, volume, existing_ref): - return self.library.manage_existing_get_size(volume, existing_ref) - - def unmanage(self, volume): - return self.library.unmanage(volume) - - def initialize_connection(self, volume, connector, **kwargs): - conn_info = self.library.initialize_connection_fc(volume, connector) - fczm_utils.add_fc_zone(conn_info) - return conn_info - - def terminate_connection(self, volume, connector, **kwargs): - conn_info = self.library.terminate_connection_fc(volume, connector, - **kwargs) - fczm_utils.remove_fc_zone(conn_info) - return conn_info - - def get_pool(self, volume): - return self.library.get_pool(volume) - - def create_cgsnapshot(self, context, cgsnapshot, snapshots): - return self.library.create_cgsnapshot(cgsnapshot, snapshots) - - def delete_cgsnapshot(self, context, cgsnapshot, snapshots): - return self.library.delete_cgsnapshot(cgsnapshot, snapshots) - - def create_consistencygroup(self, context, group): - return self.library.create_consistencygroup(group) - - def delete_consistencygroup(self, context, group, volumes): - return self.library.delete_consistencygroup(group, volumes) - - def update_consistencygroup(self, context, group, - add_volumes=None, remove_volumes=None): - return self.library.update_consistencygroup( - group, add_volumes, remove_volumes) - - def create_consistencygroup_from_src(self, context, group, volumes, - cgsnapshot=None, snapshots=None, - source_cg=None, source_vols=None): - return self.library.create_consistencygroup_from_src( - group, volumes, cgsnapshot, snapshots, source_cg, source_vols) - - def create_group(self, context, group): - return self.library.create_consistencygroup(group) - - def delete_group(self, context, group, volumes): - return self.library.delete_consistencygroup(group, volumes) diff --git a/cinder/volume/drivers/netapp/eseries/host_mapper.py b/cinder/volume/drivers/netapp/eseries/host_mapper.py deleted file mode 100644 index 1d8ed300bd7..00000000000 --- a/cinder/volume/drivers/netapp/eseries/host_mapper.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright (c) 2015 Alex Meade. All Rights Reserved. -# Copyright (c) 2015 Yogesh Kshirsagar. 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. - -""" This module handles mapping E-Series volumes to E-Series Hosts and Host -Groups. -""" - -import collections -import random - -from oslo_log import log as logging -from six.moves import range - -from cinder import exception -from cinder.i18n import _ -from cinder.objects import fields -from cinder import utils as cinder_utils -from cinder.volume.drivers.netapp.eseries import exception as eseries_exc -from cinder.volume.drivers.netapp.eseries import utils - - -LOG = logging.getLogger(__name__) - - -@cinder_utils.trace_method -@cinder_utils.synchronized('map_es_volume') -def map_volume_to_single_host(client, volume, eseries_vol, host, - vol_map, multiattach_enabled): - """Maps the e-series volume to host with initiator.""" - LOG.debug("Attempting to map volume %s to single host.", volume['id']) - - # If volume is not mapped on the backend, map directly to host - if not vol_map: - mappings = client.get_volume_mappings_for_host(host['hostRef']) - lun = _get_free_lun(client, host, multiattach_enabled, mappings) - return client.create_volume_mapping(eseries_vol['volumeRef'], - host['hostRef'], lun) - - # If volume is already mapped to desired host - if vol_map.get('mapRef') == host['hostRef']: - return vol_map - - multiattach_cluster_ref = None - try: - host_group = client.get_host_group_by_name( - utils.MULTI_ATTACH_HOST_GROUP_NAME) - multiattach_cluster_ref = host_group['clusterRef'] - except exception.NotFound: - pass - - # Volume is mapped to the multiattach host group - if vol_map.get('mapRef') == multiattach_cluster_ref: - LOG.debug("Volume %s is mapped to multiattach host group.", - volume['id']) - - # If volume is not currently attached according to Cinder, it is - # safe to delete the mapping - if not (volume['attach_status'] == fields.VolumeAttachStatus.ATTACHED): - LOG.debug("Volume %(vol)s is not currently attached, moving " - "existing mapping to host %(host)s.", - {'vol': volume['id'], 'host': host['label']}) - mappings = client.get_volume_mappings_for_host( - host['hostRef']) - lun = _get_free_lun(client, host, multiattach_enabled, mappings) - return client.move_volume_mapping_via_symbol( - vol_map.get('mapRef'), host['hostRef'], lun - ) - - # If we got this far, volume must be mapped to something else - msg = _("Cannot attach already attached volume %s; " - "multiattach is disabled via the " - "'netapp_enable_multiattach' configuration option.") - raise exception.NetAppDriverException(msg % volume['id']) - - -@cinder_utils.trace_method -@cinder_utils.synchronized('map_es_volume') -def map_volume_to_multiple_hosts(client, volume, eseries_vol, target_host, - mapping): - """Maps the e-series volume to multiattach host group.""" - - LOG.debug("Attempting to map volume %s to multiple hosts.", volume['id']) - - # If volume is already mapped to desired host, return the mapping - if mapping['mapRef'] == target_host['hostRef']: - LOG.debug("Volume %(vol)s already mapped to host %(host)s", - {'vol': volume['id'], 'host': target_host['label']}) - return mapping - - # If target host in a host group, ensure it is the multiattach host group - if target_host['clusterRef'] != utils.NULL_REF: - host_group = client.get_host_group(target_host[ - 'clusterRef']) - if host_group['label'] != utils.MULTI_ATTACH_HOST_GROUP_NAME: - msg = _("Specified host to map to volume %(vol)s is in " - "unsupported host group with %(group)s.") - params = {'vol': volume['id'], 'group': host_group['label']} - raise eseries_exc.UnsupportedHostGroup(msg % params) - - mapped_host_group = None - multiattach_host_group = None - try: - mapped_host_group = client.get_host_group(mapping['mapRef']) - # If volume is mapped to a foreign host group raise an error - if mapped_host_group['label'] != utils.MULTI_ATTACH_HOST_GROUP_NAME: - raise eseries_exc.UnsupportedHostGroup( - volume_id=volume['id'], group=mapped_host_group['label']) - multiattach_host_group = mapped_host_group - except exception.NotFound: - pass - - if not multiattach_host_group: - multiattach_host_group = client.get_host_group_by_name( - utils.MULTI_ATTACH_HOST_GROUP_NAME) - - # If volume is mapped directly to a host, move the host into the - # multiattach host group. Error if the host is in a foreign host group - if not mapped_host_group: - current_host = client.get_host(mapping['mapRef']) - if current_host['clusterRef'] != utils.NULL_REF: - host_group = client.get_host_group(current_host[ - 'clusterRef']) - if host_group['label'] != utils.MULTI_ATTACH_HOST_GROUP_NAME: - msg = _("Currently mapped host for volume %(vol)s is in " - "unsupported host group with %(group)s.") - params = {'vol': volume['id'], 'group': host_group['label']} - raise eseries_exc.UnsupportedHostGroup(msg % params) - client.set_host_group_for_host(current_host['hostRef'], - multiattach_host_group['clusterRef']) - - # Move destination host into multiattach host group - client.set_host_group_for_host(target_host[ - 'hostRef'], multiattach_host_group['clusterRef']) - - # Once both existing and target hosts are in the multiattach host group, - # move the volume mapping to said group. - if not mapped_host_group: - LOG.debug("Moving mapping for volume %s to multiattach host group.", - volume['id']) - return client.move_volume_mapping_via_symbol( - mapping.get('lunMappingRef'), - multiattach_host_group['clusterRef'], - mapping['lun'] - ) - - return mapping - - -def _get_free_lun(client, host, multiattach_enabled, mappings): - """Returns least used LUN ID available on the given host.""" - if not _is_host_full(client, host): - unused_luns = _get_unused_lun_ids(mappings) - if unused_luns: - chosen_lun = random.sample(unused_luns, 1) - return chosen_lun[0] - elif multiattach_enabled: - msg = _("No unused LUN IDs are available on the host; " - "multiattach is enabled which requires that all LUN IDs " - "to be unique across the entire host group.") - raise exception.NetAppDriverException(msg) - used_lun_counts = _get_used_lun_id_counter(mappings) - # most_common returns an arbitrary tuple of members with same frequency - for lun_id, __ in reversed(used_lun_counts.most_common()): - if _is_lun_id_available_on_host(client, host, lun_id): - return lun_id - msg = _("No free LUN IDs left. Maximum number of volumes that can be " - "attached to host (%s) has been exceeded.") - raise exception.NetAppDriverException(msg % utils.MAX_LUNS_PER_HOST) - - -def _get_unused_lun_ids(mappings): - """Returns unused LUN IDs given mappings.""" - used_luns = _get_used_lun_ids_for_mappings(mappings) - - unused_luns = (set(range(utils.MAX_LUNS_PER_HOST)) - set(used_luns)) - return unused_luns - - -def _get_used_lun_id_counter(mapping): - """Returns used LUN IDs with count as a dictionary.""" - used_luns = _get_used_lun_ids_for_mappings(mapping) - used_lun_id_counter = collections.Counter(used_luns) - return used_lun_id_counter - - -def _is_host_full(client, host): - """Checks whether maximum volumes attached to a host have been reached.""" - luns = client.get_volume_mappings_for_host(host['hostRef']) - return len(luns) >= utils.MAX_LUNS_PER_HOST - - -def _is_lun_id_available_on_host(client, host, lun_id): - """Returns a boolean value depending on whether a LUN ID is available.""" - mapping = client.get_volume_mappings_for_host(host['hostRef']) - used_lun_ids = _get_used_lun_ids_for_mappings(mapping) - return lun_id not in used_lun_ids - - -def _get_used_lun_ids_for_mappings(mappings): - """Returns used LUNs when provided with mappings.""" - used_luns = set(map(lambda lun: int(lun['lun']), mappings)) - # E-Series uses LUN ID 0 for special purposes and should not be - # assigned for general use - used_luns.add(0) - return used_luns - - -def unmap_volume_from_host(client, volume, host, mapping): - # Volume is mapped directly to host, so delete the mapping - if mapping.get('mapRef') == host['hostRef']: - LOG.debug("Volume %(vol)s is mapped directly to host %(host)s; " - "removing mapping.", {'vol': volume['id'], - 'host': host['label']}) - client.delete_volume_mapping(mapping['lunMappingRef']) - return - - try: - host_group = client.get_host_group(mapping['mapRef']) - except exception.NotFound: - # Volumes is mapped but to a different initiator - raise eseries_exc.VolumeNotMapped(volume_id=volume['id'], - host=host['label']) - # If volume is mapped to a foreign host group raise error - if host_group['label'] != utils.MULTI_ATTACH_HOST_GROUP_NAME: - raise eseries_exc.UnsupportedHostGroup(volume_id=volume['id'], - group=host_group['label']) - # If target host is not in the multiattach host group - if host['clusterRef'] != host_group['clusterRef']: - raise eseries_exc.VolumeNotMapped(volume_id=volume['id'], - host=host['label']) - - # Volume is mapped to multiattach host group - # Remove mapping if volume should no longer be attached after this - # operation. - if volume['status'] == 'detaching': - LOG.debug("Volume %s is mapped directly to multiattach host group but " - "is not currently attached; removing mapping.", volume['id']) - client.delete_volume_mapping(mapping['lunMappingRef']) diff --git a/cinder/volume/drivers/netapp/eseries/iscsi_driver.py b/cinder/volume/drivers/netapp/eseries/iscsi_driver.py deleted file mode 100644 index c6b157bbbb4..00000000000 --- a/cinder/volume/drivers/netapp/eseries/iscsi_driver.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2014 NetApp, Inc. All Rights Reserved. -# Copyright (c) 2015 Alex Meade. All Rights Reserved. -# Copyright (c) 2015 Rushil Chugh. All Rights Reserved. -# Copyright (c) 2015 Navneet Singh. 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. -""" -Volume driver for NetApp E-Series iSCSI storage systems. -""" - -from cinder import interface -from cinder.volume import driver -from cinder.volume.drivers.netapp.eseries import library -from cinder.volume.drivers.netapp import utils as na_utils - - -@interface.volumedriver -class NetAppEseriesISCSIDriver(driver.BaseVD, - driver.ManageableVD): - """NetApp E-Series iSCSI volume driver.""" - - DRIVER_NAME = 'NetApp_iSCSI_ESeries' - - # ThirdPartySystems wiki page - CI_WIKI_NAME = "NetApp_Eseries_CI" - VERSION = library.NetAppESeriesLibrary.VERSION - - def __init__(self, *args, **kwargs): - super(NetAppEseriesISCSIDriver, self).__init__(*args, **kwargs) - na_utils.validate_instantiation(**kwargs) - self.library = library.NetAppESeriesLibrary(self.DRIVER_NAME, - 'iSCSI', **kwargs) - - def do_setup(self, context): - self.library.do_setup(context) - - def check_for_setup_error(self): - self.library.check_for_setup_error() - - def create_volume(self, volume): - self.library.create_volume(volume) - - def create_volume_from_snapshot(self, volume, snapshot): - self.library.create_volume_from_snapshot(volume, snapshot) - - def create_cloned_volume(self, volume, src_vref): - self.library.create_cloned_volume(volume, src_vref) - - def delete_volume(self, volume): - self.library.delete_volume(volume) - - def create_snapshot(self, snapshot): - return self.library.create_snapshot(snapshot) - - def delete_snapshot(self, snapshot): - self.library.delete_snapshot(snapshot) - - def get_volume_stats(self, refresh=False): - return self.library.get_volume_stats(refresh) - - def extend_volume(self, volume, new_size): - self.library.extend_volume(volume, new_size) - - def ensure_export(self, context, volume): - return self.library.ensure_export(context, volume) - - def create_export(self, context, volume, connector): - return self.library.create_export(context, volume) - - def remove_export(self, context, volume): - self.library.remove_export(context, volume) - - def manage_existing(self, volume, existing_ref): - return self.library.manage_existing(volume, existing_ref) - - def manage_existing_get_size(self, volume, existing_ref): - return self.library.manage_existing_get_size(volume, existing_ref) - - def unmanage(self, volume): - return self.library.unmanage(volume) - - def initialize_connection(self, volume, connector): - return self.library.initialize_connection_iscsi(volume, connector) - - def terminate_connection(self, volume, connector, **kwargs): - return self.library.terminate_connection_iscsi(volume, connector, - **kwargs) - - def get_pool(self, volume): - return self.library.get_pool(volume) - - def create_cgsnapshot(self, context, cgsnapshot, snapshots): - return self.library.create_cgsnapshot(cgsnapshot, snapshots) - - def delete_cgsnapshot(self, context, cgsnapshot, snapshots): - return self.library.delete_cgsnapshot(cgsnapshot, snapshots) - - def create_consistencygroup(self, context, group): - return self.library.create_consistencygroup(group) - - def delete_consistencygroup(self, context, group, volumes): - return self.library.delete_consistencygroup(group, volumes) - - def update_consistencygroup(self, context, group, - add_volumes=None, remove_volumes=None): - return self.library.update_consistencygroup( - group, add_volumes, remove_volumes) - - def create_consistencygroup_from_src(self, context, group, volumes, - cgsnapshot=None, snapshots=None, - source_cg=None, source_vols=None): - return self.library.create_consistencygroup_from_src( - group, volumes, cgsnapshot, snapshots, source_cg, source_vols) - - def create_group(self, context, group): - return self.create_consistencygroup(context, group) - - def delete_group(self, context, group, volumes): - return self.library.delete_consistencygroup(group, volumes) diff --git a/cinder/volume/drivers/netapp/eseries/library.py b/cinder/volume/drivers/netapp/eseries/library.py deleted file mode 100644 index 5d3815b006e..00000000000 --- a/cinder/volume/drivers/netapp/eseries/library.py +++ /dev/null @@ -1,2147 +0,0 @@ -# Copyright (c) 2015 Alex Meade -# Copyright (c) 2015 Rushil Chugh -# Copyright (c) 2015 Navneet Singh -# Copyright (c) 2015 Yogesh Kshirsagar -# Copyright (c) 2015 Jose Porrua -# Copyright (c) 2015 Michael Price -# Copyright (c) 2015 Tom Barron -# 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. - -import copy -import math -import socket -import time -import uuid - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_log import versionutils -from oslo_service import loopingcall -from oslo_utils import excutils -from oslo_utils import units -import six - -from cinder import exception -from cinder.i18n import _ -from cinder import utils as cinder_utils -from cinder.volume.drivers.netapp.eseries import client -from cinder.volume.drivers.netapp.eseries import exception as eseries_exc -from cinder.volume.drivers.netapp.eseries import host_mapper -from cinder.volume.drivers.netapp.eseries import utils -from cinder.volume.drivers.netapp import options as na_opts -from cinder.volume.drivers.netapp import utils as na_utils -from cinder.volume import utils as volume_utils -from cinder.zonemanager import utils as fczm_utils - - -LOG = logging.getLogger(__name__) - -CONF = cfg.CONF - - -@six.add_metaclass(cinder_utils.TraceWrapperMetaclass) -class NetAppESeriesLibrary(object): - """Executes commands relating to Volumes.""" - - DRIVER_NAME = 'NetApp_iSCSI_ESeries' - AUTOSUPPORT_INTERVAL_SECONDS = 3600 # hourly - VERSION = "1.0.0" - REQUIRED_FLAGS = ['netapp_server_hostname', 'netapp_controller_ips', - 'netapp_login', 'netapp_password'] - SLEEP_SECS = 5 - HOST_TYPES = {'factoryDefault': 'FactoryDefault', - 'linux_atto': 'LnxTPGSALUA', - 'linux_dm_mp': 'LnxALUA', - 'linux_mpp_rdac': 'LNX', - 'linux_pathmanager': 'LnxTPGSALUA_PM', - 'linux_sf': 'LnxTPGSALUA_SF', - 'ontap': 'ONTAP_ALUA', - 'ontap_rdac': 'ONTAP_RDAC', - 'vmware': 'VmwTPGSALUA', - 'windows': 'W2KNETNCL', - 'windows_atto': 'WinTPGSALUA', - 'windows_clustered': 'W2KNETCL', - } - # NOTE(ameade): This maps what is reported by the e-series api to a - # consistent set of values that are reported by all NetApp drivers - # to the cinder scheduler. - SSC_DISK_TYPE_MAPPING = { - 'scsi': 'SCSI', - 'fibre': 'FCAL', - 'sas': 'SAS', - 'sata': 'SATA', - 'ssd': 'SSD', - } - SSC_RAID_TYPE_MAPPING = { - 'raidDiskPool': 'DDP', - 'raid0': 'raid0', - 'raid1': 'raid1', - # RAID3 is being deprecated and is actually implemented as RAID5 - 'raid3': 'raid5', - 'raid5': 'raid5', - 'raid6': 'raid6', - } - READ_CACHE_Q_SPEC = 'netapp:read_cache' - WRITE_CACHE_Q_SPEC = 'netapp:write_cache' - DA_UQ_SPEC = 'netapp_eseries_data_assurance' - FLASH_CACHE_UQ_SPEC = 'netapp_eseries_flash_read_cache' - DISK_TYPE_UQ_SPEC = 'netapp_disk_type' - ENCRYPTION_UQ_SPEC = 'netapp_disk_encryption' - SPINDLE_SPD_UQ_SPEC = 'netapp_eseries_disk_spindle_speed' - RAID_UQ_SPEC = 'netapp_raid_type' - THIN_UQ_SPEC = 'netapp_thin_provisioned' - SSC_UPDATE_INTERVAL = 60 # seconds - SA_COMM_TIMEOUT = 30 - WORLDWIDENAME = 'worldWideName' - - DEFAULT_HOST_TYPE = 'linux_dm_mp' - DEFAULT_CHAP_USER_NAME = 'eserieschapuser' - - # Define name marker string to use in snapshot groups that are for copying - # volumes. This is to differentiate them from ordinary snapshot groups. - SNAPSHOT_VOL_COPY_SUFFIX = 'SGCV' - # Define a name marker string used to identify snapshot volumes that have - # an underlying snapshot that is awaiting deletion. - SNAPSHOT_VOL_DEL_SUFFIX = '_DEL' - # Maximum number of snapshots per snapshot group - MAX_SNAPSHOT_COUNT = 32 - # Maximum number of snapshot groups - MAX_SNAPSHOT_GROUP_COUNT = 4 - RESERVED_SNAPSHOT_GROUP_COUNT = 1 - SNAPSHOT_PERSISTENT_STORE_KEY = 'cinder-snapshots' - SNAPSHOT_PERSISTENT_STORE_LOCK = str(uuid.uuid4()) - - def __init__(self, driver_name, driver_protocol="iSCSI", - configuration=None, **kwargs): - self.configuration = configuration - self._app_version = kwargs.pop("app_version", "unknown") - self.configuration.append_config_values(na_opts.netapp_basicauth_opts) - self.configuration.append_config_values( - na_opts.netapp_connection_opts) - self.configuration.append_config_values(na_opts.netapp_transport_opts) - self.configuration.append_config_values(na_opts.netapp_eseries_opts) - self.configuration.append_config_values(na_opts.netapp_san_opts) - self.lookup_service = fczm_utils.create_lookup_service() - self._backend_name = self.configuration.safe_get( - "volume_backend_name") or "NetApp_ESeries" - self.driver_name = driver_name - self.driver_protocol = driver_protocol - self._stats = {} - self._ssc_stats = {} - - def do_setup(self, context): - """Any initialization the volume driver does while starting.""" - self.context = context - na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration) - - self._client = self._create_rest_client(self.configuration) - self._check_mode_get_or_register_storage_system() - self._version_check() - if self.configuration.netapp_enable_multiattach: - self._ensure_multi_attach_host_group_exists() - - # This driver has been marked 'deprecated' in the Rocky release and - # will be removed in Stein. - msg = _("The NetApp E-Series driver is deprecated and will be " - "removed in a future release.") - versionutils.report_deprecated_feature(LOG, msg) - - def _create_rest_client(self, configuration): - port = configuration.netapp_server_port - scheme = configuration.netapp_transport_type.lower() - if port is None: - if scheme == 'http': - port = 8080 - elif scheme == 'https': - port = 8443 - - return client.RestClient( - scheme=scheme, - host=configuration.netapp_server_hostname, - port=port, - service_path=configuration.netapp_webservice_path, - username=configuration.netapp_login, - password=configuration.netapp_password) - - def _version_check(self): - """Ensure that the minimum version of the REST API is available""" - if not self._client.features.REST_1_4_RELEASE: - min_version = ( - self._client.features.REST_1_4_RELEASE.minimum_version) - raise exception.NetAppDriverException( - 'This version (%(cur)s of the NetApp SANtricity Webservices ' - 'Proxy is not supported. Install version %(supp)s or ' - 'later.' % {'cur': self._client.api_version, - 'supp': min_version}) - - def _start_periodic_tasks(self): - ssc_periodic_task = loopingcall.FixedIntervalLoopingCall( - self._update_ssc_info) - ssc_periodic_task.start(interval=self.SSC_UPDATE_INTERVAL) - - # Start the task that logs autosupport (ASUP) data to the controller - asup_periodic_task = loopingcall.FixedIntervalLoopingCall( - self._create_asup, CONF.host) - asup_periodic_task.start(interval=self.AUTOSUPPORT_INTERVAL_SECONDS, - initial_delay=0) - - def check_for_setup_error(self): - self._check_host_type() - self._check_multipath() - # It is important that this be called before any other methods that - # interact with the storage-system. It blocks until the - # storage-system comes online. - self._check_storage_system() - self._check_pools() - self._start_periodic_tasks() - - def _check_host_type(self): - """Validate that the configured host-type is available for the array. - - Not all host-types are available on every firmware version. - """ - requested_host_type = (self.configuration.netapp_host_type - or self.DEFAULT_HOST_TYPE) - actual_host_type = ( - self.HOST_TYPES.get(requested_host_type, requested_host_type)) - - for host_type in self._client.list_host_types(): - if(host_type.get('code') == actual_host_type or - host_type.get('name') == actual_host_type): - self.host_type = host_type.get('code') - return - exc_msg = _("The host-type '%s' is not supported on this storage " - "system.") - raise exception.NetAppDriverException(exc_msg % requested_host_type) - - def _check_multipath(self): - if not self.configuration.use_multipath_for_image_xfer: - LOG.warning('Production use of "%(backend)s" backend requires ' - 'the Cinder controller to have multipathing ' - 'properly set up and the configuration option ' - '"%(mpflag)s" to be set to "True".', - {'backend': self._backend_name, - 'mpflag': 'use_multipath_for_image_xfer'}) - - def _check_pools(self): - """Ensure that the pool listing contains at least one pool""" - if not self._get_storage_pools(): - msg = _('No pools are available for provisioning volumes. ' - 'Ensure that the configuration option ' - 'netapp_pool_name_search_pattern is set correctly.') - raise exception.NetAppDriverException(msg) - - def _ensure_multi_attach_host_group_exists(self): - try: - host_group = self._client.get_host_group_by_name( - utils.MULTI_ATTACH_HOST_GROUP_NAME) - LOG.info("The multi-attach E-Series host group '%(label)s' " - "already exists with clusterRef %(clusterRef)s", - host_group) - except exception.NotFound: - host_group = self._client.create_host_group( - utils.MULTI_ATTACH_HOST_GROUP_NAME) - LOG.info("Created multi-attach E-Series host group %(label)s " - "with clusterRef %(clusterRef)s", host_group) - - def _check_mode_get_or_register_storage_system(self): - """Does validity checks for storage system registry and health.""" - def _resolve_host(host): - try: - ip = cinder_utils.resolve_hostname(host) - return ip - except socket.gaierror as e: - LOG.error('Error resolving host %(host)s. Error - %(e)s.', - {'host': host, 'e': e}) - raise exception.NoValidBackend( - _("Controller IP '%(host)s' could not be resolved: %(e)s.") - % {'host': host, 'e': e}) - - ips = self.configuration.netapp_controller_ips - ips = [i.strip() for i in ips.split(",")] - ips = [x for x in ips if _resolve_host(x)] - host = cinder_utils.resolve_hostname( - self.configuration.netapp_server_hostname) - if host in ips: - LOG.info('Embedded mode detected.') - system = self._client.list_storage_systems()[0] - else: - LOG.info('Proxy mode detected.') - system = self._client.register_storage_system( - ips, password=self.configuration.netapp_sa_password) - self._client.set_system_id(system.get('id')) - self._client._init_features() - - def _check_password_status(self, system): - """Determine if the storage system's password status is valid. - - The password status has the following possible states: unknown, valid, - invalid. - - If the password state cannot be retrieved from the storage system, - an empty string will be returned as the status, and the password - status will be assumed to be valid. This is done to ensure that - access to a storage system will not be blocked in the event of a - problem with the API. - - This method returns a tuple consisting of the storage system's - password status and whether or not the status is valid. - - Example: (invalid, True) - - :returns: (str, bool) - """ - - status = system.get('passwordStatus') - status = status.lower() if status else '' - return status, status not in ['invalid', 'unknown'] - - def _check_storage_system_status(self, system): - """Determine if the storage system's status is valid. - - The storage system status has the following possible states: - neverContacted, offline, optimal, needsAttn. - - If the storage system state cannot be retrieved, an empty string will - be returned as the status, and the storage system's status will be - assumed to be valid. This is done to ensure that access to a storage - system will not be blocked in the event of a problem with the API. - - This method returns a tuple consisting of the storage system's - password status and whether or not the status is valid. - - Example: (needsAttn, True) - - :returns: (str, bool) - """ - status = system.get('status') - status = status.lower() if status else '' - return status, status not in ['nevercontacted', 'offline'] - - def _check_storage_system(self): - """Checks whether system is registered and has good status.""" - try: - self._client.list_storage_system() - except exception.NetAppDriverException: - with excutils.save_and_reraise_exception(): - LOG.info("System with controller addresses [%s] is not " - "registered with web service.", - self.configuration.netapp_controller_ips) - - # Update the stored password - # We do this to trigger the webservices password validation routine - new_pwd = self.configuration.netapp_sa_password - self._client.update_stored_system_password(new_pwd) - - start_time = int(time.time()) - - def check_system_status(): - system = self._client.list_storage_system() - pass_status, pass_status_valid = ( - self._check_password_status(system)) - status, status_valid = self._check_storage_system_status(system) - msg_dict = {'id': system.get('id'), 'status': status, - 'pass_status': pass_status} - # wait if array not contacted or - # password was not in sync previously. - if not (pass_status_valid and status_valid): - if not pass_status_valid: - LOG.info('Waiting for web service to validate the ' - 'configured password.') - else: - LOG.info('Waiting for web service array communication.') - if int(time.time() - start_time) >= self.SA_COMM_TIMEOUT: - if not status_valid: - raise exception.NetAppDriverException( - _("System %(id)s found with bad status - " - "%(status)s.") % msg_dict) - else: - raise exception.NetAppDriverException( - _("System %(id)s found with bad password status - " - "%(pass_status)s.") % msg_dict) - - # The system was found to have a good status - else: - LOG.info("System %(id)s has %(status)s status.", msg_dict) - raise loopingcall.LoopingCallDone() - - checker = loopingcall.FixedIntervalLoopingCall(f=check_system_status) - checker.start(interval=self.SLEEP_SECS, - initial_delay=self.SLEEP_SECS).wait() - - return True - - def _get_volume(self, uid): - """Retrieve a volume by its label""" - if uid is None: - raise exception.InvalidInput(_('The volume label is required' - ' as input.')) - - uid = utils.convert_uuid_to_es_fmt(uid) - - return self._client.list_volume(uid) - - def _get_snapshot_group_for_snapshot(self, snapshot): - snapshot = self._get_snapshot(snapshot) - try: - return self._client.list_snapshot_group(snapshot['pitGroupRef']) - except (exception.NetAppDriverException, - eseries_exc.WebServiceException): - msg = _("Specified snapshot group with id %s could not be found.") - raise exception.NotFound(msg % snapshot['pitGroupRef']) - - def _get_snapshot_legacy(self, snapshot): - """Find an E-Series snapshot by the name of the snapshot group. - - Snapshots were previously identified by the unique name of the - snapshot group. A snapshot volume is now utilized to uniquely - identify the snapshot, so any snapshots previously defined in this - way must be updated. - - :param snapshot_id: Cinder snapshot identifer - :return: An E-Series snapshot image - """ - label = utils.convert_uuid_to_es_fmt(snapshot['id']) - for group in self._client.list_snapshot_groups(): - if group['label'] == label: - image = self._get_oldest_image_in_snapshot_group(group['id']) - group_label = utils.convert_uuid_to_es_fmt(uuid.uuid4()) - # Modify the group label so we don't have a name collision - self._client.update_snapshot_group(group['id'], - group_label) - - snapshot.update({'provider_id': image['id']}) - snapshot.save() - - return image - - raise exception.NotFound(_('Snapshot with id of %s could not be ' - 'found.') % snapshot['id']) - - def _get_snapshot(self, snapshot): - """Find an E-Series snapshot by its Cinder identifier - - An E-Series snapshot image does not have a configuration name/label, - so we define a snapshot volume underneath of it that will help us to - identify it. We retrieve the snapshot volume with the matching name, - and then we find its underlying snapshot. - - :param snapshot_id: Cinder snapshot identifer - :return: An E-Series snapshot image - """ - try: - return self._client.list_snapshot_image( - snapshot.get('provider_id')) - except (eseries_exc.WebServiceException, - exception.NetAppDriverException): - try: - LOG.debug('Unable to locate snapshot by its id, falling ' - 'back to legacy behavior.') - return self._get_snapshot_legacy(snapshot) - except exception.NetAppDriverException: - raise exception.NotFound(_('Snapshot with id of %s could not' - ' be found.') % snapshot['id']) - - def _get_snapshot_group(self, snapshot_group_id): - try: - return self._client.list_snapshot_group(snapshot_group_id) - except exception.NetAppDriverException: - raise exception.NotFound(_('Unable to retrieve snapshot group ' - 'with id of %s.') % snapshot_group_id) - - def _get_ordered_images_in_snapshot_group(self, snapshot_group_id): - images = self._client.list_snapshot_images() - if images: - filtered_images = [img for img in images if img['pitGroupRef'] == - snapshot_group_id] - sorted_imgs = sorted(filtered_images, key=lambda x: x[ - 'pitTimestamp']) - return sorted_imgs - return list() - - def _get_oldest_image_in_snapshot_group(self, snapshot_group_id): - group = self._get_snapshot_group(snapshot_group_id) - images = self._get_ordered_images_in_snapshot_group(snapshot_group_id) - if images: - return images[0] - - msg = _("No snapshot image found in snapshot group %s.") - raise exception.NotFound(msg % group['label']) - - def _get_latest_image_in_snapshot_group(self, snapshot_group_id): - group = self._get_snapshot_group(snapshot_group_id) - images = self._get_ordered_images_in_snapshot_group(snapshot_group_id) - if images: - return images[-1] - - msg = _("No snapshot image found in snapshot group %s.") - raise exception.NotFound(msg % group['label']) - - def _is_volume_containing_snaps(self, label): - """Checks if volume contains snapshot groups.""" - vol_id = utils.convert_es_fmt_to_uuid(label) - for snap in self._client.list_snapshot_groups(): - if snap['baseVolume'] == vol_id: - return True - return False - - def get_pool(self, volume): - """Return pool name where volume resides. - - :param volume: The volume hosted by the driver. - :returns: Name of the pool where given volume is hosted. - """ - eseries_volume = self._get_volume(volume['name_id']) - storage_pool = self._client.get_storage_pool( - eseries_volume['volumeGroupRef']) - if storage_pool: - return storage_pool.get('label') - - def _add_volume_to_consistencygroup(self, volume): - if volume.get('consistencygroup_id'): - es_cg = self._get_consistencygroup(volume['consistencygroup']) - self._update_consistency_group_members(es_cg, [volume], []) - - def create_volume(self, volume): - """Creates a volume.""" - - LOG.debug('create_volume on %s', volume['host']) - - # get E-series pool label as pool name - eseries_pool_label = volume_utils.extract_host(volume['host'], - level='pool') - - if eseries_pool_label is None: - msg = _("Pool is not available in the volume host field.") - raise exception.InvalidHost(reason=msg) - - eseries_volume_label = utils.convert_uuid_to_es_fmt(volume['name_id']) - - extra_specs = na_utils.get_volume_extra_specs(volume) - - # get size of the requested volume creation - size_gb = int(volume['size']) - self._create_volume(eseries_pool_label, eseries_volume_label, size_gb, - extra_specs) - - self._add_volume_to_consistencygroup(volume) - - def _create_volume(self, eseries_pool_label, eseries_volume_label, - size_gb, extra_specs=None): - """Creates volume with given label and size.""" - if extra_specs is None: - extra_specs = {} - - if self.configuration.netapp_enable_multiattach: - volumes = self._client.list_volumes() - # NOTE(ameade): Ensure we do not create more volumes than we could - # map to the multi attach ESeries host group. - if len(volumes) > utils.MAX_LUNS_PER_HOST_GROUP: - msg = (_("Cannot create more than %(req)s volumes on the " - "ESeries array when 'netapp_enable_multiattach' is " - "set to true.") % - {'req': utils.MAX_LUNS_PER_HOST_GROUP}) - raise exception.NetAppDriverException(msg) - - # These must be either boolean values, or None - read_cache = extra_specs.get(self.READ_CACHE_Q_SPEC) - if read_cache is not None: - read_cache = na_utils.to_bool(read_cache) - - write_cache = extra_specs.get(self.WRITE_CACHE_Q_SPEC) - if write_cache is not None: - write_cache = na_utils.to_bool(write_cache) - - flash_cache = extra_specs.get(self.FLASH_CACHE_UQ_SPEC) - if flash_cache is not None: - flash_cache = na_utils.to_bool(flash_cache) - - data_assurance = extra_specs.get(self.DA_UQ_SPEC) - if data_assurance is not None: - data_assurance = na_utils.to_bool(data_assurance) - - thin_provision = extra_specs.get(self.THIN_UQ_SPEC) - if(thin_provision is not None): - thin_provision = na_utils.to_bool(thin_provision) - - target_pool = None - - pools = self._get_storage_pools() - for pool in pools: - if pool["label"] == eseries_pool_label: - target_pool = pool - break - - if not target_pool: - msg = _("Pools %s does not exist") - raise exception.NetAppDriverException(msg % eseries_pool_label) - - try: - vol = self._client.create_volume(target_pool['volumeGroupRef'], - eseries_volume_label, size_gb, - read_cache=read_cache, - write_cache=write_cache, - flash_cache=flash_cache, - data_assurance=data_assurance, - thin_provision=thin_provision) - LOG.info("Created volume with label %s.", eseries_volume_label) - except exception.NetAppDriverException as e: - with excutils.save_and_reraise_exception(): - LOG.error("Error creating volume. Msg - %s.", e) - # There was some kind failure creating the volume, make sure no - # partial flawed work exists - try: - bad_vol = self._get_volume(eseries_volume_label) - except Exception: - # Swallowing the exception intentionally because this is - # emergency cleanup to make sure no intermediate volumes - # were left. In this whole error situation, the more - # common route would be for no volume to have been created. - pass - else: - # Some sort of partial volume was created despite the - # error. Lets clean it out so no partial state volumes or - # orphans are left. - try: - self._client.delete_volume(bad_vol["id"]) - except exception.NetAppDriverException as e2: - LOG.error( - "Error cleaning up failed volume creation. " - "Msg - %s.", e2) - - return vol - - def _is_data_assurance_supported(self): - """Determine if the storage backend is PI (DataAssurance) compatible""" - return self.driver_protocol != "iSCSI" - - def _schedule_and_create_volume(self, label, size_gb): - """Creates volume with given label and size.""" - avl_pools = self._get_sorted_available_storage_pools(size_gb) - for pool in avl_pools: - try: - vol = self._client.create_volume(pool['volumeGroupRef'], - label, size_gb) - LOG.info("Created volume with label %s.", label) - return vol - except exception.NetAppDriverException as e: - LOG.error("Error creating volume. Msg - %s.", e) - msg = _("Failure creating volume %s.") - raise exception.NetAppDriverException(msg % label) - - def _create_volume_from_snapshot(self, volume, image): - """Define a new volume based on an E-Series snapshot image. - - This method should be synchronized on the snapshot id. - - :param volume: a Cinder volume - :param image: an E-Series snapshot image - :return: the clone volume - """ - label = utils.convert_uuid_to_es_fmt(volume['id']) - size = volume['size'] - - dst_vol = self._schedule_and_create_volume(label, size) - src_vol = None - try: - src_vol = self._create_snapshot_volume(image) - self._copy_volume_high_priority_readonly(src_vol, dst_vol) - LOG.info("Created volume with label %s.", label) - except exception.NetAppDriverException: - with excutils.save_and_reraise_exception(): - self._client.delete_volume(dst_vol['volumeRef']) - finally: - if src_vol: - try: - self._client.delete_snapshot_volume(src_vol['id']) - except exception.NetAppDriverException as e: - LOG.error("Failure restarting snap vol. Error: %s.", e) - else: - LOG.warning("Snapshot volume creation failed for " - "snapshot %s.", image['id']) - - return dst_vol - - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - es_snapshot = self._get_snapshot(snapshot) - cinder_utils.synchronized(snapshot['id'])( - self._create_volume_from_snapshot)(volume, es_snapshot) - - self._add_volume_to_consistencygroup(volume) - - def _copy_volume_high_priority_readonly(self, src_vol, dst_vol): - """Copies src volume to dest volume.""" - LOG.info("Copying src vol %(src)s to dest vol %(dst)s.", - {'src': src_vol['label'], 'dst': dst_vol['label']}) - job = None - try: - job = self._client.create_volume_copy_job( - src_vol['id'], dst_vol['volumeRef']) - - def wait_for_copy(): - j_st = self._client.list_vol_copy_job(job['volcopyRef']) - if (j_st['status'] in ['inProgress', 'pending', 'unknown']): - return - if j_st['status'] == 'failed' or j_st['status'] == 'halted': - LOG.error("Vol copy job status %s.", j_st['status']) - raise exception.NetAppDriverException( - _("Vol copy job for dest %s failed.") % - dst_vol['label']) - LOG.info("Vol copy job completed for dest %s.", - dst_vol['label']) - raise loopingcall.LoopingCallDone() - - checker = loopingcall.FixedIntervalLoopingCall(wait_for_copy) - checker.start(interval=self.SLEEP_SECS, - initial_delay=self.SLEEP_SECS, - stop_on_exception=True).wait() - finally: - if job: - try: - self._client.delete_vol_copy_job(job['volcopyRef']) - except exception.NetAppDriverException: - LOG.warning("Failure deleting job %s.", job['volcopyRef']) - else: - LOG.warning('Volume copy job for src vol %s not found.', - src_vol['id']) - LOG.info('Copy job to dest vol %s completed.', dst_vol['label']) - - def create_cloned_volume(self, volume, src_vref): - """Creates a clone of the specified volume.""" - es_vol = self._get_volume(src_vref['id']) - - es_snapshot = self._create_es_snapshot_for_clone(es_vol) - - try: - self._create_volume_from_snapshot(volume, es_snapshot) - self._add_volume_to_consistencygroup(volume) - finally: - try: - self._client.delete_snapshot_group(es_snapshot['pitGroupRef']) - except exception.NetAppDriverException: - LOG.warning("Failure deleting temp snapshot %s.", - es_snapshot['id']) - - def delete_volume(self, volume): - """Deletes a volume.""" - try: - vol = self._get_volume(volume['name_id']) - self._client.delete_volume(vol['volumeRef']) - except (exception.NetAppDriverException, exception.VolumeNotFound): - LOG.warning("Volume %s already deleted.", volume['id']) - return - - def _is_cgsnapshot(self, snapshot_image): - """Determine if an E-Series snapshot image is part of a cgsnapshot""" - cg_id = snapshot_image.get('consistencyGroupId') - # A snapshot that is not part of a consistency group may have a - # cg_id of either none or a string of all 0's, so we check for both - return not (cg_id is None or utils.NULL_REF == cg_id) - - def _create_snapshot_volume(self, image): - """Creates snapshot volume for given group with snapshot_id.""" - group = self._get_snapshot_group(image['pitGroupRef']) - - LOG.debug("Creating snap vol for group %s", group['label']) - - label = utils.convert_uuid_to_es_fmt(uuid.uuid4()) - - if self._is_cgsnapshot(image): - return self._client.create_cg_snapshot_view( - image['consistencyGroupId'], label, image['id']) - else: - return self._client.create_snapshot_volume( - image['pitRef'], label, image['baseVol']) - - def _create_snapshot_group(self, label, volume, percentage_capacity=20.0): - """Define a new snapshot group for a volume - - :param label: the label for the snapshot group - :param volume: an E-Series volume - :param percentage_capacity: an optional repository percentage - :return: a new snapshot group - """ - - # Newer versions of the REST API are capable of automatically finding - # the best pool candidate - if not self._client.features.REST_1_3_RELEASE: - vol_size_gb = int(volume['totalSizeInBytes']) / units.Gi - pools = self._get_sorted_available_storage_pools(vol_size_gb) - volume_pool = next(pool for pool in pools if volume[ - 'volumeGroupRef'] == pool['id']) - - # A disk pool can only utilize a candidate from its own pool - if volume_pool.get('raidLevel') == 'raidDiskPool': - pool_id_to_use = volume_pool['volumeGroupRef'] - - # Otherwise, choose the best available pool - else: - pool_id_to_use = pools[0]['volumeGroupRef'] - group = self._client.create_snapshot_group( - label, volume['volumeRef'], pool_id_to_use, - repo_percent=percentage_capacity) - - else: - group = self._client.create_snapshot_group( - label, volume['volumeRef'], repo_percent=percentage_capacity) - - return group - - def _get_snapshot_groups_for_volume(self, vol): - """Find all snapshot groups associated with an E-Series volume - - :param vol: An E-Series volume object - :return: A list of snapshot groups - :raise NetAppDriverException: if the list of snapshot groups cannot be - retrieved - """ - return [grp for grp in self._client.list_snapshot_groups() - if grp['baseVolume'] == vol['id']] - - def _get_available_snapshot_group(self, vol): - """Find a snapshot group that has remaining capacity for snapshots. - - In order to minimize repository usage, we prioritize the snapshot - group with remaining snapshot capacity that has most recently had a - snapshot defined on it. - - :param vol: An E-Series volume object - :return: A valid snapshot group that has available snapshot capacity, - or None - :raise NetAppDriverException: if the list of snapshot groups cannot be - retrieved - """ - groups_for_v = self._get_snapshot_groups_for_volume(vol) - - # Filter out reserved snapshot groups - groups = [g for g in groups_for_v - if self.SNAPSHOT_VOL_COPY_SUFFIX not in g['label']] - - # Filter out groups that are part of a consistency group - groups = [g for g in groups if not g['consistencyGroup']] - # Find all groups with free snapshot capacity - groups = [group for group in groups if group.get('snapshotCount') < - self.MAX_SNAPSHOT_COUNT] - - # Order by the last defined snapshot on the group - if len(groups) > 1: - group_by_id = {g['id']: g for g in groups} - - snap_imgs = list() - for group in groups: - try: - snap_imgs.append( - self._get_latest_image_in_snapshot_group(group['id'])) - except exception.NotFound: - pass - - snap_imgs = sorted(snap_imgs, key=lambda x: x['pitSequenceNumber']) - - if snap_imgs: - # The newest image - img = snap_imgs[-1] - return group_by_id[img['pitGroupRef']] - else: - return groups[0] if groups else None - - # Skip the snapshot image checks if there is only one snapshot group - elif groups: - return groups[0] - else: - return None - - def _create_es_snapshot_for_clone(self, vol): - group_name = (utils.convert_uuid_to_es_fmt(uuid.uuid4()) + - self.SNAPSHOT_VOL_COPY_SUFFIX) - return self._create_es_snapshot(vol, group_name) - - def _create_es_snapshot(self, vol, group_name=None): - snap_grp, snap_image = None, None - try: - snap_grp = self._get_available_snapshot_group(vol) - # If a snapshot group is not available, create one if possible - if snap_grp is None: - snap_groups_for_vol = self._get_snapshot_groups_for_volume( - vol) - - # We need a reserved snapshot group - if (group_name is not None and - (self.SNAPSHOT_VOL_COPY_SUFFIX in group_name)): - - # First we search for an existing reserved group - for grp in snap_groups_for_vol: - if grp['label'].endswith( - self.SNAPSHOT_VOL_COPY_SUFFIX): - snap_grp = grp - break - - # No reserved group exists, so we create it - if (snap_grp is None and - (len(snap_groups_for_vol) < - self.MAX_SNAPSHOT_GROUP_COUNT)): - snap_grp = self._create_snapshot_group(group_name, - vol) - - # Ensure we don't exceed the snapshot group limit - elif (len(snap_groups_for_vol) < - (self.MAX_SNAPSHOT_GROUP_COUNT - - self.RESERVED_SNAPSHOT_GROUP_COUNT)): - - label = group_name if group_name is not None else ( - utils.convert_uuid_to_es_fmt(uuid.uuid4())) - - snap_grp = self._create_snapshot_group(label, vol) - LOG.info("Created snap grp with label %s.", label) - - # We couldn't retrieve or create a snapshot group - if snap_grp is None: - raise exception.SnapshotLimitExceeded( - allowed=(self.MAX_SNAPSHOT_COUNT * - (self.MAX_SNAPSHOT_GROUP_COUNT - - self.RESERVED_SNAPSHOT_GROUP_COUNT))) - - return self._client.create_snapshot_image( - snap_grp['id']) - - except exception.NetAppDriverException: - with excutils.save_and_reraise_exception(): - if snap_image is None and snap_grp: - self._delete_snapshot_group(snap_grp['id']) - - def create_snapshot(self, snapshot): - """Creates a snapshot. - - :param snapshot: The Cinder snapshot - :param group_name: An optional label for the snapshot group - :returns: An E-Series snapshot image - """ - - os_vol = snapshot['volume'] - vol = self._get_volume(os_vol['name_id']) - - snap_image = cinder_utils.synchronized(vol['id'])( - self._create_es_snapshot)(vol) - model_update = { - 'provider_id': snap_image['id'] - } - - return model_update - - def _delete_es_snapshot(self, es_snapshot): - """Perform a soft-delete on an E-Series snapshot. - - Mark the snapshot image as no longer needed, so that it can be - purged from the backend when no other snapshots are dependent upon it. - - :param es_snapshot: an E-Series snapshot image - :return: None - """ - index = self._get_soft_delete_map() - snapgroup_ref = es_snapshot['pitGroupRef'] - if snapgroup_ref in index: - bitset = na_utils.BitSet(int((index[snapgroup_ref]))) - else: - bitset = na_utils.BitSet(0) - - images = [img for img in self._client.list_snapshot_images() if - img['pitGroupRef'] == snapgroup_ref] - for i, image in enumerate(sorted(images, key=lambda x: x[ - 'pitSequenceNumber'])): - if(image['pitSequenceNumber'] == es_snapshot[ - 'pitSequenceNumber']): - bitset.set(i) - break - - index_update, keys_to_del = ( - self._cleanup_snapshot_images(images, bitset)) - - self._merge_soft_delete_changes(index_update, keys_to_del) - - def delete_snapshot(self, snapshot): - """Delete a snapshot.""" - try: - es_snapshot = self._get_snapshot(snapshot) - except exception.NotFound: - LOG.warning("Snapshot %s already deleted.", snapshot['id']) - else: - os_vol = snapshot['volume'] - vol = self._get_volume(os_vol['name_id']) - - cinder_utils.synchronized(vol['id'])(self._delete_es_snapshot)( - es_snapshot) - - def _get_soft_delete_map(self): - """Retrieve the snapshot index from the storage backend""" - return self._client.list_backend_store( - self.SNAPSHOT_PERSISTENT_STORE_KEY) - - @cinder_utils.synchronized(SNAPSHOT_PERSISTENT_STORE_LOCK) - def _merge_soft_delete_changes(self, index_update, keys_to_del): - """Merge changes to the snapshot index and save it on the backend - - This method merges provided changes into the index, locking, to ensure - that concurrent changes that don't overlap are not overwritten. No - update will occur if neither an update or keys to delete are provided. - - :param index_update: a dict of keys/value pairs to update in the index - :param keys_to_del: a list of keys to purge from the index - """ - if index_update or keys_to_del: - index = self._get_soft_delete_map() - if index_update: - index.update(index_update) - if keys_to_del: - for key in keys_to_del: - if key in index: - del index[key] - - self._client.save_backend_store( - self.SNAPSHOT_PERSISTENT_STORE_KEY, index) - - def _cleanup_snapshot_images(self, images, bitset): - """Delete snapshot images that are marked for removal from the backend. - - This method will iterate over all snapshots (beginning with the - oldest), that are defined on the same snapshot group as the provided - snapshot image. If the snapshot is marked for deletion, it will be - purged from the backend. Otherwise, the method will return because - no further snapshots can be purged. - - The bitset will be updated based on the return from this method. - Any updates to the index will be provided as a dict, and any keys - to be removed from the index should be returned as (dict, list). - - :param images: a list of E-Series snapshot images - :param bitset: a bitset representing the snapshot images that are - no longer needed on the backend (and may be deleted when possible) - :return (dict, list): a tuple containing a dict of updates for the - index and a list of keys to remove from the index - """ - snap_grp_ref = images[0]['pitGroupRef'] - # All images are marked as deleted, we can delete the snapshot group - if bitset == 2 ** len(images) - 1: - try: - self._delete_snapshot_group(snap_grp_ref) - except exception.NetAppDriverException as e: - LOG.warning("Unable to remove snapshot group - %s.", e.msg) - return None, [snap_grp_ref] - else: - # Order by their sequence number, from oldest to newest - snapshots = sorted(images, - key=lambda x: x['pitSequenceNumber']) - deleted = 0 - - for i, snapshot in enumerate(snapshots): - if bitset.is_set(i): - self._delete_snapshot_image(snapshot) - deleted += 1 - else: - # Snapshots must be deleted in order, so if the current - # snapshot is not pending deletion, we don't want to - # process any more - break - - if deleted: - # Update the bitset based on the deleted snapshots - bitset >>= deleted - LOG.debug('Deleted %(count)s snapshot images from snapshot ' - 'group: %(grp)s.', {'count': deleted, - 'grp': snap_grp_ref}) - if deleted >= len(images): - try: - self._delete_snapshot_group(snap_grp_ref) - except exception.NetAppDriverException as e: - LOG.warning("Unable to remove snapshot group - %s.", - e.msg) - return None, [snap_grp_ref] - - return {snap_grp_ref: repr(bitset)}, None - - def _delete_snapshot_group(self, group_id): - try: - self._client.delete_snapshot_group(group_id) - except eseries_exc.WebServiceException as e: - raise exception.NetAppDriverException(e.msg) - - def _delete_snapshot_image(self, es_snapshot): - """Remove a snapshot image from the storage backend - - If a snapshot group has no remaining snapshot images associated with - it, it will be deleted as well. When the snapshot is deleted, - any snapshot volumes that are associated with it will be orphaned, - so they are also deleted. - - :param es_snapshot: An E-Series snapshot image - :param snapshot_volumes: Snapshot volumes associated with the snapshot - """ - self._client.delete_snapshot_image(es_snapshot['id']) - - def ensure_export(self, context, volume): - """Synchronously recreates an export for a volume.""" - pass - - def create_export(self, context, volume): - """Exports the volume.""" - pass - - def remove_export(self, context, volume): - """Removes an export for a volume.""" - pass - - def map_volume_to_host(self, volume, eseries_volume, initiators): - """Ensures the specified initiator has access to the volume.""" - existing_maps = self._client.get_volume_mappings_for_volume( - eseries_volume) - host = self._get_or_create_host(initiators, self.host_type) - # There can only be one or zero mappings on a volume in E-Series - current_map = existing_maps[0] if existing_maps else None - - if self.configuration.netapp_enable_multiattach and current_map: - self._ensure_multi_attach_host_group_exists() - mapping = host_mapper.map_volume_to_multiple_hosts(self._client, - volume, - eseries_volume, - host, - current_map) - else: - mapping = host_mapper.map_volume_to_single_host( - self._client, volume, eseries_volume, host, current_map, - self.configuration.netapp_enable_multiattach) - return mapping - - def initialize_connection_fc(self, volume, connector): - """Initializes the connection and returns connection info. - - Assigns the specified volume to a compute node/host so that it can be - used from that host. - - The driver returns a driver_volume_type of 'fibre_channel'. - The target_wwn can be a single entry or a list of wwns that - correspond to the list of remote wwn(s) that will export the volume. - Example return values: - - .. code-block:: python - - { - 'driver_volume_type': 'fibre_channel' - 'data': { - 'target_discovered': True, - 'target_lun': 1, - 'target_wwn': '500a098280feeba5', - 'initiator_target_map': { - '21000024ff406cc3': ['500a098280feeba5'], - '21000024ff406cc2': ['500a098280feeba5'] - } - } - } - - or - - .. code-block:: python - - { - 'driver_volume_type': 'fibre_channel' - 'data': { - 'target_discovered': True, - 'target_lun': 1, - 'target_wwn': ['500a098280feeba5', '500a098290feeba5', - '500a098190feeba5', '500a098180feeba5'], - 'initiator_target_map': { - '21000024ff406cc3': ['500a098280feeba5', - '500a098290feeba5'], - '21000024ff406cc2': ['500a098190feeba5', - '500a098180feeba5'] - } - } - } - """ - - initiators = [fczm_utils.get_formatted_wwn(wwpn) - for wwpn in connector['wwpns']] - - eseries_vol = self._get_volume(volume['name_id']) - mapping = self.map_volume_to_host(volume, eseries_vol, - initiators) - lun_id = mapping['lun'] - - initiator_info = self._build_initiator_target_map_fc(connector) - target_wwpns, initiator_target_map, num_paths = initiator_info - - if target_wwpns: - msg = ("Successfully fetched target details for LUN %(id)s " - "and initiator(s) %(initiators)s.") - msg_fmt = {'id': volume['id'], 'initiators': initiators} - LOG.debug(msg, msg_fmt) - else: - msg = _('Failed to get LUN target details for the LUN %s.') - raise exception.VolumeBackendAPIException(data=msg % volume['id']) - - target_info = {'driver_volume_type': 'fibre_channel', - 'data': {'target_discovered': True, - 'target_lun': int(lun_id), - 'target_wwn': target_wwpns, - 'initiator_target_map': initiator_target_map}} - - return target_info - - def terminate_connection_fc(self, volume, connector, **kwargs): - """Disallow connection from connector. - - Return empty data if other volumes are in the same zone. - The FibreChannel ZoneManager doesn't remove zones - if there isn't an initiator_target_map in the - return of terminate_connection. - - :returns: data - the target_wwns and initiator_target_map if the - zone is to be removed, otherwise the same map with - an empty dict for the 'data' key - """ - - eseries_vol = self._get_volume(volume['name_id']) - initiators = [fczm_utils.get_formatted_wwn(wwpn) - for wwpn in connector['wwpns']] - host = self._get_host_with_matching_port(initiators) - mappings = eseries_vol.get('listOfMappings', []) - - # There can only be one or zero mappings on a volume in E-Series - mapping = mappings[0] if mappings else None - - if not mapping: - raise eseries_exc.VolumeNotMapped(volume_id=volume['id'], - host=host['label']) - host_mapper.unmap_volume_from_host(self._client, volume, host, mapping) - - info = {'driver_volume_type': 'fibre_channel', - 'data': {}} - - if len(self._client.get_volume_mappings_for_host( - host['hostRef'])) == 0: - # No more exports for this host, so tear down zone. - LOG.info("Need to remove FC Zone, building initiator target map.") - - initiator_info = self._build_initiator_target_map_fc(connector) - target_wwpns, initiator_target_map, num_paths = initiator_info - - info['data'] = {'target_wwn': target_wwpns, - 'initiator_target_map': initiator_target_map} - - return info - - def _build_initiator_target_map_fc(self, connector): - """Build the target_wwns and the initiator target map.""" - - # get WWPNs from controller and strip colons - all_target_wwpns = self._client.list_target_wwpns() - all_target_wwpns = [six.text_type(wwpn).replace(':', '') - for wwpn in all_target_wwpns] - - target_wwpns = [] - init_targ_map = {} - num_paths = 0 - - if self.lookup_service: - # Use FC SAN lookup to determine which ports are visible. - dev_map = self.lookup_service.get_device_mapping_from_network( - connector['wwpns'], - all_target_wwpns) - - for fabric_name in dev_map: - fabric = dev_map[fabric_name] - target_wwpns += fabric['target_port_wwn_list'] - for initiator in fabric['initiator_port_wwn_list']: - if initiator not in init_targ_map: - init_targ_map[initiator] = [] - init_targ_map[initiator] += fabric['target_port_wwn_list'] - init_targ_map[initiator] = list(set( - init_targ_map[initiator])) - for target in init_targ_map[initiator]: - num_paths += 1 - target_wwpns = list(set(target_wwpns)) - else: - initiator_wwns = connector['wwpns'] - target_wwpns = all_target_wwpns - - for initiator in initiator_wwns: - init_targ_map[initiator] = target_wwpns - - return target_wwpns, init_targ_map, num_paths - - def initialize_connection_iscsi(self, volume, connector): - """Allow connection to connector and return connection info.""" - initiator_name = connector['initiator'] - eseries_vol = self._get_volume(volume['name_id']) - mapping = self.map_volume_to_host(volume, eseries_vol, - [initiator_name]) - - lun_id = mapping['lun'] - msg_fmt = {'id': volume['id'], 'initiator_name': initiator_name} - LOG.debug("Mapped volume %(id)s to the initiator %(initiator_name)s.", - msg_fmt) - - iscsi_details = self._get_iscsi_service_details() - iscsi_portal = self._get_iscsi_portal_for_vol(eseries_vol, - iscsi_details) - LOG.debug("Successfully fetched target details for volume %(id)s and " - "initiator %(initiator_name)s.", msg_fmt) - iqn = iscsi_portal['iqn'] - address = iscsi_portal['ip'] - port = iscsi_portal['tcp_port'] - properties = na_utils.get_iscsi_connection_properties(lun_id, volume, - iqn, address, - port) - if self.configuration.use_chap_auth: - if self._client.features.CHAP_AUTHENTICATION: - chap_username, chap_password = self._configure_chap(iqn) - properties['data']['auth_username'] = chap_username - properties['data']['auth_password'] = chap_password - properties['data']['auth_method'] = 'CHAP' - properties['data']['discovery_auth_username'] = chap_username - properties['data']['discovery_auth_password'] = chap_password - properties['data']['discovery_auth_method'] = 'CHAP' - else: - msg = _("E-series proxy API version %(current_version)s does " - "not support CHAP authentication. The proxy version " - "must be at least %(min_version)s.") - min_version = (self._client.features. - CHAP_AUTHENTICATION.minimum_version) - msg = msg % {'current_version': self._client.api_version, - 'min_version': min_version} - - LOG.info(msg) - raise exception.NetAppDriverException(msg) - return properties - - def _configure_chap(self, target_iqn): - chap_username = self.DEFAULT_CHAP_USER_NAME - chap_password = volume_utils.generate_password() - self._client.set_chap_authentication(target_iqn, - chap_username, - chap_password) - return chap_username, chap_password - - def _get_iscsi_service_details(self): - """Gets iscsi iqn, ip and port information.""" - ports = [] - hw_inventory = self._client.list_hardware_inventory() - iscsi_ports = hw_inventory.get('iscsiPorts') - if iscsi_ports: - for port in iscsi_ports: - if (port.get('ipv4Enabled') and port.get('iqn') and - port.get('ipv4Data') and - port['ipv4Data'].get('ipv4AddressData') and - port['ipv4Data']['ipv4AddressData'] - .get('ipv4Address') and port['ipv4Data'] - ['ipv4AddressData'].get('configState') - == 'configured'): - iscsi_det = {} - iscsi_det['ip'] =\ - port['ipv4Data']['ipv4AddressData']['ipv4Address'] - iscsi_det['iqn'] = port['iqn'] - iscsi_det['tcp_port'] = port.get('tcpListenPort') - iscsi_det['controller'] = port.get('controllerId') - ports.append(iscsi_det) - if not ports: - msg = _('No good iscsi portals found for %s.') - raise exception.NetAppDriverException( - msg % self._client.get_system_id()) - return ports - - def _get_iscsi_portal_for_vol(self, volume, portals, anyController=True): - """Get the iscsi portal info relevant to volume.""" - for portal in portals: - if portal.get('controller') == volume.get('currentManager'): - return portal - if anyController and portals: - return portals[0] - msg = _('No good iscsi portal found in supplied list for %s.') - raise exception.NetAppDriverException( - msg % self._client.get_system_id()) - - def _get_or_create_host(self, port_ids, host_type): - """Fetch or create a host by given port.""" - try: - host = self._get_host_with_matching_port(port_ids) - ht_def = self._get_host_type_definition(host_type) - if host.get('hostTypeIndex') != ht_def.get('index'): - try: - host = self._client.update_host_type( - host['hostRef'], ht_def) - except exception.NetAppDriverException as e: - LOG.warning("Unable to update host type for host with " - "label %(l)s. %(e)s", - {'l': host['label'], 'e': e.msg}) - return host - except exception.NotFound as e: - LOG.warning("Message - %s.", e.msg) - return self._create_host(port_ids, host_type) - - def _get_host_with_matching_port(self, port_ids): - """Gets or creates a host with given port id.""" - # Remove any extra colons - port_ids = [six.text_type(wwpn).replace(':', '') - for wwpn in port_ids] - hosts = self._client.list_hosts() - for port_id in port_ids: - for host in hosts: - if host.get('hostSidePorts'): - ports = host.get('hostSidePorts') - for port in ports: - address = port.get('address').upper().replace(':', '') - if address == port_id.upper(): - return host - msg = _("Host with ports %(ports)s not found.") - raise exception.NotFound(msg % {'ports': port_ids}) - - def _create_host(self, port_ids, host_type, host_group=None): - """Creates host on system with given initiator as port_id.""" - LOG.info("Creating host with ports %s.", port_ids) - host_label = utils.convert_uuid_to_es_fmt(uuid.uuid4()) - host_type = self._get_host_type_definition(host_type) - port_type = self.driver_protocol.lower() - return self._client.create_host_with_ports(host_label, - host_type, - port_ids, - group_id=host_group, - port_type=port_type) - - def _get_host_type_definition(self, host_type): - """Gets supported host type if available on storage system.""" - host_types = self._client.list_host_types() - for ht in host_types: - if ht.get('name', 'unknown').lower() == host_type.lower(): - return ht - raise exception.NotFound(_("Host type %s not supported.") % host_type) - - def terminate_connection_iscsi(self, volume, connector, **kwargs): - """Disallow connection from connector.""" - eseries_vol = self._get_volume(volume['name_id']) - initiator = connector['initiator'] - host = self._get_host_with_matching_port([initiator]) - mappings = eseries_vol.get('listOfMappings', []) - - # There can only be one or zero mappings on a volume in E-Series - mapping = mappings[0] if mappings else None - - if not mapping: - raise eseries_exc.VolumeNotMapped(volume_id=volume['id'], - host=host['label']) - host_mapper.unmap_volume_from_host(self._client, volume, host, mapping) - - def get_volume_stats(self, refresh=False): - """Return the current state of the volume service.""" - if refresh: - if not self._ssc_stats: - self._update_ssc_info() - self._update_volume_stats() - - return self._stats - - def _update_volume_stats(self): - """Update volume statistics.""" - LOG.debug("Updating volume stats.") - data = dict() - data["volume_backend_name"] = self._backend_name - data["vendor_name"] = "NetApp" - data["driver_version"] = self.VERSION - data["storage_protocol"] = self.driver_protocol - data["pools"] = [] - storage_volumes = self._client.list_volumes() - - for storage_pool in self._get_storage_pools(): - cinder_pool = {} - cinder_pool["pool_name"] = storage_pool.get("label") - cinder_pool["QoS_support"] = False - cinder_pool["reserved_percentage"] = ( - self.configuration.reserved_percentage) - cinder_pool["max_over_subscription_ratio"] = ( - self.configuration.max_over_subscription_ratio) - tot_bytes = int(storage_pool.get("totalRaidedSpace", 0)) - used_bytes = int(storage_pool.get("usedSpace", 0)) - - provisioned_capacity = 0 - for volume in storage_volumes: - if (volume["volumeGroupRef"] == storage_pool.get('id') and - not volume['label'].startswith('repos_')): - provisioned_capacity += float(volume["capacity"]) - - cinder_pool["provisioned_capacity_gb"] = (provisioned_capacity / - units.Gi) - cinder_pool["free_capacity_gb"] = ((tot_bytes - used_bytes) / - units.Gi) - cinder_pool["total_capacity_gb"] = tot_bytes / units.Gi - - pool_ssc_stats = self._ssc_stats.get( - storage_pool["volumeGroupRef"]) - - if pool_ssc_stats: - thin = pool_ssc_stats.get(self.THIN_UQ_SPEC) or False - cinder_pool.update(pool_ssc_stats) - else: - thin = False - cinder_pool["thin_provisioning_support"] = thin - # All E-Series pools support thick provisioning - cinder_pool["thick_provisioning_support"] = True - - data["pools"].append(cinder_pool) - - self._stats = data - self._garbage_collect_tmp_vols() - - def _create_asup(self, cinder_host): - if not self._client.features.AUTOSUPPORT: - LOG.info("E-series proxy API version %s does not support " - "autosupport logging.", self._client.api_version) - return - - event_source = ("Cinder driver %s" % self.DRIVER_NAME) - category = "provisioning" - event_description = "OpenStack Cinder connected to E-Series proxy" - asup_info = self._client.get_asup_info() - model = asup_info.get('model') - firmware_version = asup_info.get('firmware_version') - serial_numbers = asup_info.get('serial_numbers') - chassis_sn = asup_info.get('chassis_sn') - - key = ("openstack-%s-%s-%s" - % (cinder_host, serial_numbers[0], serial_numbers[1])) - - # The counter is being set here to a key-value combination - # comprised of serial numbers and cinder host with a default - # heartbeat of 1. The counter is set to inform the user that the - # key does not have a stale value. - self._client.set_counter("%s-heartbeat" % key, value=1) - data = { - 'computer-name': cinder_host, - 'event-source': event_source, - 'app-version': self._app_version, - 'category': category, - 'event-description': event_description, - 'controller1-serial': serial_numbers[0], - 'controller2-serial': serial_numbers[1], - 'chassis-serial-number': chassis_sn, - 'model': model, - 'system-version': firmware_version, - 'operating-mode': self._client.api_operating_mode - } - self._client.add_autosupport_data(key, data) - - @cinder_utils.synchronized("netapp_update_ssc_info", external=False) - def _update_ssc_info(self): - """Periodically runs to update ssc information from the backend. - - The self._ssc_stats attribute is updated with the following format. - { : {: }} - """ - LOG.info("Updating storage service catalog information for " - "backend '%s'", self._backend_name) - - relevant_pools = self._get_storage_pools() - - if self._client.features.SSC_API_V2: - self._update_ssc_info_v2(relevant_pools) - else: - self._update_ssc_info_v1(relevant_pools) - - def _update_ssc_info_v1(self, relevant_pools): - """Update ssc data using the legacy API - - :param relevant_pools: The pools that this driver cares about - """ - LOG.info("E-series proxy API version %(version)s does not " - "support full set of SSC extra specs. The proxy version" - " must be at at least %(min_version)s.", - {'version': self._client.api_version, - 'min_version': - self._client.features.SSC_API_V2.minimum_version}) - - self._ssc_stats = ( - self._update_ssc_disk_encryption(relevant_pools)) - self._ssc_stats = ( - self._update_ssc_disk_types(relevant_pools)) - self._ssc_stats = ( - self._update_ssc_raid_type(relevant_pools)) - - def _update_ssc_info_v2(self, relevant_pools): - """Update the ssc dictionary with ssc info for relevant pools - - :param relevant_pools: The pools that this driver cares about - """ - ssc_stats = copy.deepcopy(self._ssc_stats) - - storage_pool_labels = [pool['label'] for pool in relevant_pools] - - ssc_data = self._client.list_ssc_storage_pools() - ssc_data = [pool for pool in ssc_data - if pool['name'] in storage_pool_labels] - - for pool in ssc_data: - poolId = pool['poolId'] - if poolId not in ssc_stats: - ssc_stats[poolId] = {} - - pool_ssc_info = ssc_stats[poolId] - - pool_ssc_info['consistencygroup_support'] = True - - pool_ssc_info[self.ENCRYPTION_UQ_SPEC] = ( - six.text_type(pool['encrypted']).lower()) - - pool_ssc_info[self.SPINDLE_SPD_UQ_SPEC] = (pool['spindleSpeed']) - - flash_cache_capable = pool['flashCacheCapable'] - pool_ssc_info[self.FLASH_CACHE_UQ_SPEC] = ( - six.text_type(flash_cache_capable).lower()) - - # Data Assurance is not compatible with some backend types - da_capable = pool['dataAssuranceCapable'] and ( - self._is_data_assurance_supported()) - pool_ssc_info[self.DA_UQ_SPEC] = ( - six.text_type(da_capable).lower()) - - pool_ssc_info[self.RAID_UQ_SPEC] = ( - self.SSC_RAID_TYPE_MAPPING.get(pool['raidLevel'], 'unknown')) - - pool_ssc_info[self.THIN_UQ_SPEC] = ( - six.text_type(pool['thinProvisioningCapable']).lower()) - - if pool['pool'].get("driveMediaType") == 'ssd': - pool_ssc_info[self.DISK_TYPE_UQ_SPEC] = 'SSD' - else: - pool_ssc_info[self.DISK_TYPE_UQ_SPEC] = ( - self.SSC_DISK_TYPE_MAPPING.get( - pool['pool'].get('drivePhysicalType'), 'unknown')) - - self._ssc_stats = ssc_stats - - def _update_ssc_disk_types(self, storage_pools): - """Updates the given ssc dictionary with new disk type information. - - :param storage_pools: The storage pools this driver cares about - """ - ssc_stats = copy.deepcopy(self._ssc_stats) - all_disks = self._client.list_drives() - - pool_ids = set(pool.get("volumeGroupRef") for pool in storage_pools) - - relevant_disks = [x for x in all_disks - if x.get('currentVolumeGroupRef') in pool_ids] - for drive in relevant_disks: - current_vol_group = drive.get('currentVolumeGroupRef') - if current_vol_group not in ssc_stats: - ssc_stats[current_vol_group] = {} - - if drive.get("driveMediaType") == 'ssd': - ssc_stats[current_vol_group][self.DISK_TYPE_UQ_SPEC] = 'SSD' - else: - disk_type = drive.get('interfaceType').get('driveType') - ssc_stats[current_vol_group][self.DISK_TYPE_UQ_SPEC] = ( - self.SSC_DISK_TYPE_MAPPING.get(disk_type, 'unknown')) - - return ssc_stats - - def _update_ssc_disk_encryption(self, storage_pools): - """Updates the given ssc dictionary with new disk encryption information. - - :param storage_pools: The storage pools this driver cares about - """ - ssc_stats = copy.deepcopy(self._ssc_stats) - for pool in storage_pools: - current_vol_group = pool.get('volumeGroupRef') - if current_vol_group not in ssc_stats: - ssc_stats[current_vol_group] = {} - - ssc_stats[current_vol_group][self.ENCRYPTION_UQ_SPEC] = ( - six.text_type(pool['securityType'] == 'enabled').lower() - ) - - return ssc_stats - - def _update_ssc_raid_type(self, storage_pools): - """Updates the given ssc dictionary with new RAID type information. - - :param storage_pools: The storage pools this driver cares about - """ - ssc_stats = copy.deepcopy(self._ssc_stats) - for pool in storage_pools: - current_vol_group = pool.get('volumeGroupRef') - if current_vol_group not in ssc_stats: - ssc_stats[current_vol_group] = {} - - raid_type = pool.get('raidLevel') - ssc_stats[current_vol_group]['netapp_raid_type'] = ( - self.SSC_RAID_TYPE_MAPPING.get(raid_type, 'unknown')) - - return ssc_stats - - def _get_storage_pools(self): - """Retrieve storage pools that match user-configured search pattern.""" - - # Inform deprecation of legacy option. - if self.configuration.safe_get('netapp_storage_pools'): - msg = ("The option 'netapp_storage_pools' is deprecated and " - "will be removed in the future releases. Please use " - "the option 'netapp_pool_name_search_pattern' instead.") - versionutils.report_deprecated_feature(LOG, msg) - - pool_regex = na_utils.get_pool_name_filter_regex(self.configuration) - - storage_pools = self._client.list_storage_pools() - - filtered_pools = [] - for pool in storage_pools: - pool_name = pool['label'] - - if pool_regex.match(pool_name): - msg = ("Pool '%(pool_name)s' matches against regular " - "expression: %(pool_pattern)s") - LOG.debug(msg, {'pool_name': pool_name, - 'pool_pattern': pool_regex.pattern}) - filtered_pools.append(pool) - else: - msg = ("Pool '%(pool_name)s' does not match against regular " - "expression: %(pool_pattern)s") - LOG.debug(msg, {'pool_name': pool_name, - 'pool_pattern': pool_regex.pattern}) - - return filtered_pools - - def _get_sorted_available_storage_pools(self, size_gb): - """Returns storage pools sorted on available capacity.""" - size = size_gb * units.Gi - sorted_pools = sorted(self._get_storage_pools(), key=lambda x: - (int(x.get('totalRaidedSpace', 0)) - - int(x.get('usedSpace', 0))), reverse=True) - avl_pools = filter(lambda x: ((int(x.get('totalRaidedSpace', 0)) - - int(x.get('usedSpace', 0)) >= size)), - sorted_pools) - - if not avl_pools: - LOG.warning("No storage pool found with available capacity %s.", - size_gb) - return avl_pools - - def _is_thin_provisioned(self, volume): - """Determine if a volume is thin provisioned""" - return volume.get('objectType') == 'thinVolume' or volume.get( - 'thinProvisioned', False) - - def _get_pool_operation_progress(self, pool_id, action=None): - """Retrieve the progress of a long running operation on a pool - - The return will be a tuple containing: a bool representing whether - or not the operation is complete, a set of actions that are - currently running on the storage pool, and the estimated time - remaining in minutes. - - An action type may be passed in such that once no actions of that type - remain active on the pool, the operation will be considered - completed. If no action str is passed in, it is assumed that - multiple actions compose the operation, and none are terminal, - so the operation will not be considered completed until there are no - actions remaining to be completed on any volume on the pool. - - :param pool_id: The id of a storage pool - :param action: The anticipated action - :returns: A tuple (bool, set(str), int) - """ - actions = set() - eta = 0 - for progress in self._client.get_pool_operation_progress(pool_id): - actions.add(progress.get('currentAction')) - eta += progress.get('estimatedTimeToCompletion', 0) - if action is not None: - complete = action not in actions - else: - complete = not actions - return complete, actions, eta - - def extend_volume(self, volume, new_size): - """Extend an existing volume to the new size.""" - src_vol = self._get_volume(volume['name_id']) - thin_provisioned = self._is_thin_provisioned(src_vol) - self._client.expand_volume(src_vol['id'], new_size, thin_provisioned) - - # If the volume is thin or defined on a disk pool, there is no need - # to block. - if not (thin_provisioned or src_vol.get('diskPool')): - # Wait for the expansion to start - - def check_progress(): - complete, actions, eta = ( - self._get_pool_operation_progress(src_vol[ - 'volumeGroupRef'], - 'remappingDve')) - if complete: - raise loopingcall.LoopingCallDone() - else: - LOG.info("Waiting for volume expansion of %(vol)s to " - "complete, current remaining actions are " - "%(action)s. ETA: %(eta)s mins.", - {'vol': volume['name_id'], - 'action': ', '.join(actions), 'eta': eta}) - - checker = loopingcall.FixedIntervalLoopingCall( - check_progress) - - checker.start(interval=self.SLEEP_SECS, - initial_delay=self.SLEEP_SECS, - stop_on_exception=True).wait() - - def create_cgsnapshot(self, cgsnapshot, snapshots): - """Creates a cgsnapshot.""" - cg_id = cgsnapshot['consistencygroup_id'] - cg_name = utils.convert_uuid_to_es_fmt(cg_id) - - # Retrieve the E-Series consistency group - es_cg = self._get_consistencygroup_by_name(cg_name) - - # Define an E-Series CG Snapshot - es_snaphots = self._client.create_consistency_group_snapshot( - es_cg['id']) - - # Build the snapshot updates - snapshot_updates = list() - for snap in snapshots: - es_vol = self._get_volume(snap['volume']['id']) - for es_snap in es_snaphots: - if es_snap['baseVol'] == es_vol['id']: - snapshot_updates.append({ - 'id': snap['id'], - # Directly track the backend snapshot ID - 'provider_id': es_snap['id'], - 'status': 'available' - }) - - return None, snapshot_updates - - def delete_cgsnapshot(self, cgsnapshot, snapshots): - """Deletes a cgsnapshot.""" - - cg_id = cgsnapshot['consistencygroup_id'] - cg_name = utils.convert_uuid_to_es_fmt(cg_id) - - # Retrieve the E-Series consistency group - es_cg = self._get_consistencygroup_by_name(cg_name) - - # Find the smallest sequence number defined on the group - min_seq_num = min(es_cg['uniqueSequenceNumber']) - - es_snapshots = self._client.get_consistency_group_snapshots( - es_cg['id']) - es_snap_ids = set(snap.get('provider_id') for snap in snapshots) - - # We need to find a single snapshot that is a part of the CG snap - seq_num = None - for snap in es_snapshots: - if snap['id'] in es_snap_ids: - seq_num = snap['pitSequenceNumber'] - break - - if seq_num is None: - raise exception.CgSnapshotNotFound(cgsnapshot_id=cg_id) - - # Perform a full backend deletion of the cgsnapshot - if int(seq_num) <= int(min_seq_num): - self._client.delete_consistency_group_snapshot( - es_cg['id'], seq_num) - return None, None - else: - # Perform a soft-delete, removing this snapshot from cinder - # management, and marking it as available for deletion. - return cinder_utils.synchronized(cg_id)( - self._soft_delete_cgsnapshot)( - es_cg, seq_num) - - def _soft_delete_cgsnapshot(self, es_cg, snap_seq_num): - """Mark a cgsnapshot as available for deletion from the backend. - - E-Series snapshots cannot be deleted out of order, as older - snapshots in the snapshot group are dependent on the newer - snapshots. A "soft delete" results in the cgsnapshot being removed - from Cinder management, with the snapshot marked as available for - deletion once all snapshots dependent on it are also deleted. - - :param es_cg: E-Series consistency group - :param snap_seq_num: unique sequence number of the cgsnapshot - :return: an update to the snapshot index - """ - - index = self._get_soft_delete_map() - cg_ref = es_cg['id'] - if cg_ref in index: - bitset = na_utils.BitSet(int((index[cg_ref]))) - else: - bitset = na_utils.BitSet(0) - - seq_nums = ( - set([snap['pitSequenceNumber'] for snap in - self._client.get_consistency_group_snapshots(cg_ref)])) - - # Determine the relative index of the snapshot's sequence number - for i, seq_num in enumerate(sorted(seq_nums)): - if snap_seq_num == seq_num: - bitset.set(i) - break - - index_update = ( - self._cleanup_cg_snapshots(cg_ref, seq_nums, bitset)) - - self._merge_soft_delete_changes(index_update, None) - - return None, None - - def _cleanup_cg_snapshots(self, cg_ref, seq_nums, bitset): - """Delete cg snapshot images that are marked for removal - - The snapshot index tracks all snapshots that have been removed from - Cinder, and are therefore available for deletion when this operation - is possible. - - CG snapshots are tracked by unique sequence numbers that are - associated with 1 or more snapshot images. The sequence numbers are - tracked (relative to the 32 images allowed per group), within the - snapshot index. - - This method will purge CG snapshots that have been marked as - available for deletion within the backend persistent store. - - :param cg_ref: reference to an E-Series consistent group - :param seq_nums: set of unique sequence numbers associated with the - consistency group - :param bitset: the bitset representing which sequence numbers are - marked for deletion - :return: update for the snapshot index - """ - deleted = 0 - # Order by their sequence number, from oldest to newest - for i, seq_num in enumerate(sorted(seq_nums)): - if bitset.is_set(i): - self._client.delete_consistency_group_snapshot(cg_ref, - seq_num) - deleted += 1 - else: - # Snapshots must be deleted in order, so if the current - # snapshot is not pending deletion, we don't want to - # process any more - break - - if deleted: - # We need to update the bitset to reflect the fact that older - # snapshots have been deleted, so snapshot relative indexes - # have now been updated. - bitset >>= deleted - - LOG.debug('Deleted %(count)s snapshot images from ' - 'consistency group: %(grp)s.', {'count': deleted, - 'grp': cg_ref}) - # Update the index - return {cg_ref: repr(bitset)} - - def create_consistencygroup(self, cinder_cg): - """Define a consistency group.""" - self._create_consistency_group(cinder_cg) - - return {'status': 'available'} - - def _create_consistency_group(self, cinder_cg): - """Define a new consistency group on the E-Series backend""" - name = utils.convert_uuid_to_es_fmt(cinder_cg['id']) - return self._client.create_consistency_group(name) - - def _get_consistencygroup(self, cinder_cg): - """Retrieve an E-Series consistency group""" - name = utils.convert_uuid_to_es_fmt(cinder_cg['id']) - return self._get_consistencygroup_by_name(name) - - def _get_consistencygroup_by_name(self, name): - """Retrieve an E-Series consistency group by name""" - - for cg in self._client.list_consistency_groups(): - if name == cg['name']: - return cg - - raise exception.ConsistencyGroupNotFound(consistencygroup_id=name) - - def delete_consistencygroup(self, group, volumes): - """Deletes a consistency group.""" - - volume_update = list() - - for volume in volumes: - LOG.info('Deleting volume %s.', volume['id']) - volume_update.append({ - 'status': 'deleted', 'id': volume['id'], - }) - self.delete_volume(volume) - - try: - cg = self._get_consistencygroup(group) - except exception.ConsistencyGroupNotFound: - LOG.warning('Consistency group already deleted.') - else: - self._client.delete_consistency_group(cg['id']) - try: - self._merge_soft_delete_changes(None, [cg['id']]) - except (exception.NetAppDriverException, - eseries_exc.WebServiceException): - LOG.warning('Unable to remove CG from the deletion map.') - - model_update = {'status': 'deleted'} - - return model_update, volume_update - - def _update_consistency_group_members(self, es_cg, - add_volumes, remove_volumes): - """Add or remove consistency group members - - :param es_cg: The E-Series consistency group - :param add_volumes: A list of Cinder volumes to add to the - consistency group - :param remove_volumes: A list of Cinder volumes to remove from the - consistency group - :return: None - """ - for volume in remove_volumes: - es_vol = self._get_volume(volume['id']) - LOG.info( - 'Removing volume %(v)s from consistency group %(''cg)s.', - {'v': es_vol['label'], 'cg': es_cg['label']}) - self._client.remove_consistency_group_member(es_vol['id'], - es_cg['id']) - - for volume in add_volumes: - es_vol = self._get_volume(volume['id']) - LOG.info('Adding volume %(v)s to consistency group %(cg)s.', - {'v': es_vol['label'], 'cg': es_cg['label']}) - self._client.add_consistency_group_member( - es_vol['id'], es_cg['id']) - - def update_consistencygroup(self, group, - add_volumes, remove_volumes): - """Add or remove volumes from an existing consistency group""" - cg = self._get_consistencygroup(group) - - self._update_consistency_group_members( - cg, add_volumes, remove_volumes) - - return None, None, None - - def create_consistencygroup_from_src(self, group, volumes, - cgsnapshot, snapshots, - source_cg, source_vols): - """Define a consistency group based on an existing group - - Define a new consistency group from a source consistency group. If - only a source_cg is provided, then clone each base volume and add - it to a new consistency group. If a cgsnapshot is provided, - clone each snapshot image to a new volume and add it to the cg. - - :param group: The new consistency group to define - :param volumes: The volumes to add to the consistency group - :param cgsnapshot: The cgsnapshot to base the group on - :param snapshots: The list of snapshots on the source cg - :param source_cg: The source consistency group - :param source_vols: The volumes added to the source cg - """ - cg = self._create_consistency_group(group) - if cgsnapshot: - for vol, snap in zip(volumes, snapshots): - image = self._get_snapshot(snap) - self._create_volume_from_snapshot(vol, image) - else: - for vol, src in zip(volumes, source_vols): - es_vol = self._get_volume(src['id']) - es_snapshot = self._create_es_snapshot_for_clone(es_vol) - try: - self._create_volume_from_snapshot(vol, es_snapshot) - finally: - self._delete_es_snapshot(es_snapshot) - - self._update_consistency_group_members(cg, volumes, []) - - return None, None - - def _garbage_collect_tmp_vols(self): - """Removes tmp vols with no snapshots.""" - try: - if not na_utils.set_safe_attr(self, 'clean_job_running', True): - LOG.warning('Returning as clean tmp vol job already running.') - return - - for vol in self._client.list_volumes(): - label = vol['label'] - if (label.startswith('tmp-') and - not self._is_volume_containing_snaps(label)): - try: - self._client.delete_volume(vol['volumeRef']) - except exception.NetAppDriverException as e: - LOG.debug("Error deleting vol with label %(label)s:" - " %(error)s.", {'label': label, 'error': e}) - finally: - na_utils.set_safe_attr(self, 'clean_job_running', False) - - @cinder_utils.synchronized('manage_existing') - def manage_existing(self, volume, existing_ref): - """Brings an existing storage object under Cinder management.""" - vol = self._get_existing_vol_with_manage_ref(existing_ref) - label = utils.convert_uuid_to_es_fmt(volume['id']) - if label == vol['label']: - LOG.info("Volume with given ref %s need not be renamed during" - " manage operation.", existing_ref) - managed_vol = vol - else: - managed_vol = self._client.update_volume(vol['id'], label) - LOG.info("Manage operation completed for volume with new label" - " %(label)s and wwn %(wwn)s.", - {'label': label, 'wwn': managed_vol[self.WORLDWIDENAME]}) - - def manage_existing_get_size(self, volume, existing_ref): - """Return size of volume to be managed by manage_existing. - - When calculating the size, round up to the next GB. - """ - vol = self._get_existing_vol_with_manage_ref(existing_ref) - return int(math.ceil(float(vol['capacity']) / units.Gi)) - - def _get_existing_vol_with_manage_ref(self, existing_ref): - try: - vol_id = existing_ref.get('source-name') or existing_ref.get( - 'source-id') - if vol_id is None: - raise exception.InvalidInput(message='No valid identifier ' - 'was available for the ' - 'volume.') - return self._client.list_volume(vol_id) - except exception.InvalidInput: - reason = _('Reference must contain either source-name' - ' or source-id element.') - raise exception.ManageExistingInvalidReference( - existing_ref=existing_ref, reason=reason) - except exception.VolumeNotFound: - raise exception.ManageExistingInvalidReference( - existing_ref=existing_ref, - reason=_('Volume not found on configured storage pools.')) - - def unmanage(self, volume): - """Removes the specified volume from Cinder management. - - Does not delete the underlying backend storage object. Logs a - message to indicate the volume is no longer under Cinder's control. - """ - managed_vol = self._get_volume(volume['id']) - LOG.info("Unmanaged volume with current label %(label)s and wwn " - "%(wwn)s.", {'label': managed_vol['label'], - 'wwn': managed_vol[self.WORLDWIDENAME]}) diff --git a/cinder/volume/drivers/netapp/eseries/utils.py b/cinder/volume/drivers/netapp/eseries/utils.py deleted file mode 100644 index 25c053f3cb2..00000000000 --- a/cinder/volume/drivers/netapp/eseries/utils.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2014 Navneet Singh. All rights reserved. -# Copyright (c) 2014 Clinton Knight. 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. -""" -Utilities for NetApp E-series drivers. -""" - -import base64 -import binascii -import uuid - -import six - - -MULTI_ATTACH_HOST_GROUP_NAME = 'cinder-multi-attach' -NULL_REF = '0000000000000000000000000000000000000000' -MAX_LUNS_PER_HOST = 256 -MAX_LUNS_PER_HOST_GROUP = 256 - - -def encode_hex_to_base32(hex_string): - """Encodes hex to base32 bit as per RFC4648.""" - bin_form = binascii.unhexlify(hex_string) - return base64.b32encode(bin_form) - - -def decode_base32_to_hex(base32_string): - """Decodes base32 string to hex string.""" - bin_form = base64.b32decode(base32_string) - return binascii.hexlify(bin_form) - - -def convert_uuid_to_es_fmt(uuid_str): - """Converts uuid to e-series compatible name format.""" - uuid_base32 = encode_hex_to_base32(uuid.UUID(six.text_type(uuid_str)).hex) - es_label = uuid_base32.strip(b'=') - if six.PY3: - es_label = es_label.decode('ascii') - return es_label - - -def convert_es_fmt_to_uuid(es_label): - """Converts e-series name format to uuid.""" - if isinstance(es_label, six.text_type): - es_label = es_label.encode('utf-8') - if es_label.startswith(b'tmp-'): - es_label = es_label[4:] - es_label = es_label.ljust(32, b'=') - es_label = binascii.hexlify(base64.b32decode(es_label)) - if six.PY3: - es_label = es_label.decode('ascii') - return uuid.UUID(es_label) diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index 17ef9e5c409..17e41064ba3 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -34,10 +34,10 @@ NETAPP_SIZE_MULTIPLIER_DEFAULT = 1.2 netapp_proxy_opts = [ cfg.StrOpt('netapp_storage_family', default='ontap_cluster', - choices=['ontap_cluster', 'eseries'], + choices=['ontap_cluster'], help=('The storage family type used on the storage system; ' - 'valid values are ontap_cluster for using clustered ' - 'Data ONTAP, or eseries for using E-Series.')), + 'the only valid value is ontap_cluster for using ' + 'clustered Data ONTAP.')), cfg.StrOpt('netapp_storage_protocol', choices=['iscsi', 'fc', 'nfs'], help=('The storage protocol to be used on the data path with ' @@ -50,8 +50,7 @@ netapp_connection_opts = [ cfg.IntOpt('netapp_server_port', help=('The TCP port to use for communication with the storage ' 'system or proxy server. If not specified, Data ONTAP ' - 'drivers will use 80 for HTTP and 443 for HTTPS; ' - 'E-Series will use 8080 for HTTP and 8443 for HTTPS.')), ] + 'drivers will use 80 for HTTP and 443 for HTTPS.')), ] netapp_transport_opts = [ cfg.StrOpt('netapp_transport_type', @@ -115,35 +114,6 @@ netapp_img_cache_opts = [ 'the value of this parameter, will be deleted from the ' 'cache to create free space on the NFS share.')), ] -netapp_eseries_opts = [ - cfg.StrOpt('netapp_webservice_path', - default='/devmgr/v2', - help=('This option is used to specify the path to the E-Series ' - 'proxy application on a proxy server. The value is ' - 'combined with the value of the netapp_transport_type, ' - 'netapp_server_hostname, and netapp_server_port options ' - 'to create the URL used by the driver to connect to the ' - 'proxy application.')), - cfg.StrOpt('netapp_controller_ips', - help=('This option is only utilized when the storage family ' - 'is configured to eseries. This option is used to ' - 'restrict provisioning to the specified controllers. ' - 'Specify the value of this option to be a comma ' - 'separated list of controller hostnames or IP addresses ' - 'to be used for provisioning.')), - cfg.StrOpt('netapp_sa_password', - help=('Password for the NetApp E-Series storage array.'), - secret=True), - cfg.BoolOpt('netapp_enable_multiattach', - default=False, - help='This option specifies whether the driver should allow ' - 'operations that require multiple attachments to a ' - 'volume. An example would be live migration of servers ' - 'that have volumes attached. When enabled, this backend ' - 'is limited to 256 total volumes in order to ' - 'guarantee volumes can be accessed by more than one ' - 'host.'), -] netapp_nfs_extra_opts = [ cfg.StrOpt('netapp_copyoffload_tool_path', help=('This option specifies the path of the NetApp copy ' @@ -214,7 +184,6 @@ CONF.register_opts(netapp_basicauth_opts, group=conf.SHARED_CONF_GROUP) CONF.register_opts(netapp_cluster_opts, group=conf.SHARED_CONF_GROUP) CONF.register_opts(netapp_provisioning_opts, group=conf.SHARED_CONF_GROUP) CONF.register_opts(netapp_img_cache_opts, group=conf.SHARED_CONF_GROUP) -CONF.register_opts(netapp_eseries_opts, group=conf.SHARED_CONF_GROUP) CONF.register_opts(netapp_nfs_extra_opts, group=conf.SHARED_CONF_GROUP) CONF.register_opts(netapp_san_opts, group=conf.SHARED_CONF_GROUP) CONF.register_opts(netapp_replication_opts, group=conf.SHARED_CONF_GROUP) diff --git a/doc/source/admin/blockstorage-groups.rst b/doc/source/admin/blockstorage-groups.rst index f40058e8265..21f762c3f1c 100644 --- a/doc/source/admin/blockstorage-groups.rst +++ b/doc/source/admin/blockstorage-groups.rst @@ -22,7 +22,7 @@ consistency groups and the release when the support was added: - Liberty: Dell Storage Center, EMC XtremIO, HPE 3Par and LeftHand -- Mitaka: EMC ScaleIO, NetApp Data ONTAP and E-Series, SolidFire +- Mitaka: EMC ScaleIO, NetApp Data ONTAP, SolidFire - Newton: CoprHD, FalconStor, Huawei diff --git a/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst b/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst index 917fd88e631..e6bb1e7ed84 100644 --- a/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst +++ b/doc/source/configuration/block-storage/drivers/netapp-volume-driver.rst @@ -3,8 +3,8 @@ NetApp unified driver ===================== The NetApp unified driver is a Block Storage driver that supports -multiple storage families and protocols. A storage family corresponds to -storage systems built on either clustered Data ONTAP or E-Series. The +multiple storage families and protocols. Currently, the only storage +family supported by this driver is the clustered Data ONTAP. The storage protocol refers to the protocol used to initiate data storage and access operations on those storage systems like iSCSI and NFS. The NetApp unified driver can be configured to provision and manage OpenStack volumes @@ -27,8 +27,8 @@ that can support new storage families and protocols. In releases prior to Juno, the NetApp unified driver contained some scheduling logic that determined which NetApp storage container - (namely, a FlexVol volume for Data ONTAP, or a dynamic disk pool for - E-Series) that a new Block Storage volume would be placed into. + (namely, a FlexVol volume for Data ONTAP) that a new Block Storage + volume would be placed into. With the introduction of pools, all scheduling logic is performed completely within the Block Storage scheduler, as each @@ -248,106 +248,3 @@ Define Block Storage volume types by using the :command:`openstack volume type set` command. .. include:: ../../tables/manual/cinder-netapp_cdot_extraspecs.inc - - -NetApp E-Series storage family -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The NetApp E-Series storage family represents a configuration group which -provides OpenStack compute instances access to E-Series storage systems. At -present it can be configured in Block Storage to work with the iSCSI -storage protocol. - -NetApp iSCSI configuration for E-Series ---------------------------------------- - -The NetApp iSCSI configuration for E-Series is an interface from OpenStack to -E-Series storage systems. It provisions and manages the SAN block storage -entity, which is a NetApp LUN which can be accessed using the iSCSI protocol. - -The iSCSI configuration for E-Series is an interface from Block -Storage to the E-Series proxy instance and as such requires the deployment of -the proxy instance in order to achieve the desired functionality. The driver -uses REST APIs to interact with the E-Series proxy instance, which in turn -interacts directly with the E-Series controllers. - -The use of multipath and DM-MP are required when using the Block -Storage driver for E-Series. In order for Block Storage and OpenStack -Compute to take advantage of multiple paths, the following configuration -options must be correctly configured: - -- The ``use_multipath_for_image_xfer`` option should be set to ``True`` in the - ``cinder.conf`` file within the driver-specific stanza (for example, - ``[myDriver]``). - -- The ``volume_use_multipath`` option should be set to ``True`` in the - ``nova.conf`` file within the ``[libvirt]`` stanza. - In versions prior to Newton, the option was called ``iscsi_use_multipath``. - -**Configuration options** - -Configure the volume driver, storage family, and storage protocol to the -NetApp unified driver, E-Series, and iSCSI respectively by setting the -``volume_driver``, ``netapp_storage_family`` and -``netapp_storage_protocol`` options in the ``cinder.conf`` file as follows: - -.. code-block:: ini - - volume_driver = cinder.volume.drivers.netapp.common.NetAppDriver - netapp_storage_family = eseries - netapp_storage_protocol = iscsi - netapp_server_hostname = myhostname - netapp_server_port = 80 - netapp_login = username - netapp_password = password - netapp_controller_ips = 1.2.3.4,5.6.7.8 - netapp_sa_password = arrayPassword - netapp_storage_pools = pool1,pool2 - use_multipath_for_image_xfer = True - -.. note:: - - To use the E-Series driver, you must override the default value of - ``netapp_storage_family`` with ``eseries``. - - To use the iSCSI protocol, you must override the default value of - ``netapp_storage_protocol`` with ``iscsi``. - -.. include:: ../../tables/cinder-netapp_eseries_iscsi.inc - -.. tip:: - - For more information on these options and other deployment and - operational scenarios, visit the `NetApp OpenStack website - `_. - -NetApp-supported extra specs for E-Series ------------------------------------------ - -Extra specs enable vendors to specify extra filter criteria. -The Block Storage scheduler uses the specs when the scheduler determines -which volume node should fulfill a volume provisioning request. -When you use the NetApp unified driver with an E-Series storage system, -you can leverage extra specs with Block Storage volume types to ensure -that Block Storage volumes are created on storage back ends that have -certain properties. An example of this is when you configure thin -provisioning for a storage back end. - -Extra specs are associated with Block Storage volume types. -When users request volumes of a particular volume type, the volumes are -created on storage back ends that meet the list of requirements. -An example of this is the back ends that have the available space or -extra specs. Use the specs in the following table to configure volumes. -Define Block Storage volume types by using the :command:`openstack volume -type set` command. - -.. list-table:: Description of extra specs options for NetApp Unified Driver with E-Series - :header-rows: 1 - - * - Extra spec - - Type - - Description - * - ``netapp_thin_provisioned`` - - Boolean - - Limit the candidate volume list to only the ones that support thin - provisioning on the storage controller. diff --git a/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc b/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc index 2a1a5eaac38..0c5a8130aee 100644 --- a/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc +++ b/doc/source/configuration/tables/cinder-netapp_cdot_iscsi.inc @@ -33,13 +33,13 @@ * - ``netapp_server_hostname`` = ``None`` - (String) The hostname (or IP address) for the storage system or proxy server. * - ``netapp_server_port`` = ``None`` - - (Integer) The TCP port to use for communication with the storage system or proxy server. If not specified, Data ONTAP drivers will use 80 for HTTP and 443 for HTTPS; E-Series will use 8080 for HTTP and 8443 for HTTPS. + - (Integer) The TCP port to use for communication with the storage system or proxy server. If not specified, Data ONTAP drivers will use 80 for HTTP and 443 for HTTPS. * - ``netapp_size_multiplier`` = ``1.2`` - (Floating point) The quantity to be multiplied by the requested volume size to ensure enough space is available on the virtual storage server (Vserver) to fulfill the volume creation request. Note: this option is deprecated and will be removed in favor of "reserved_percentage" in the Mitaka release. * - ``netapp_snapmirror_quiesce_timeout`` = ``3600`` - (Integer) The maximum time in seconds to wait for existing SnapMirror transfers to complete before aborting during a failover. * - ``netapp_storage_family`` = ``ontap_cluster`` - - (String) The storage family type used on the storage system; valid values are ontap_cluster for using clustered Data ONTAP, or eseries for using E-Series. + - (String) The storage family type used on the storage system; the only valid value is ontap_cluster for using clustered Data ONTAP. * - ``netapp_storage_protocol`` = ``None`` - (String) The storage protocol to be used on the data path with the storage system. * - ``netapp_transport_type`` = ``http`` diff --git a/doc/source/configuration/tables/cinder-netapp_cdot_nfs.inc b/doc/source/configuration/tables/cinder-netapp_cdot_nfs.inc index 62d4a4c71c4..d2b9abad241 100644 --- a/doc/source/configuration/tables/cinder-netapp_cdot_nfs.inc +++ b/doc/source/configuration/tables/cinder-netapp_cdot_nfs.inc @@ -39,11 +39,11 @@ * - ``netapp_server_hostname`` = ``None`` - (String) The hostname (or IP address) for the storage system or proxy server. * - ``netapp_server_port`` = ``None`` - - (Integer) The TCP port to use for communication with the storage system or proxy server. If not specified, Data ONTAP drivers will use 80 for HTTP and 443 for HTTPS; E-Series will use 8080 for HTTP and 8443 for HTTPS. + - (Integer) The TCP port to use for communication with the storage system or proxy server. If not specified, Data ONTAP drivers will use 80 for HTTP and 443 for HTTPS. * - ``netapp_snapmirror_quiesce_timeout`` = ``3600`` - (Integer) The maximum time in seconds to wait for existing SnapMirror transfers to complete before aborting during a failover. * - ``netapp_storage_family`` = ``ontap_cluster`` - - (String) The storage family type used on the storage system; valid values are ontap_cluster for using clustered Data ONTAP, or eseries for using E-Series. + - (String) The storage family type used on the storage system; the only valid value is ontap_cluster for using clustered Data ONTAP. * - ``netapp_storage_protocol`` = ``None`` - (String) The storage protocol to be used on the data path with the storage system. * - ``netapp_transport_type`` = ``http`` diff --git a/doc/source/configuration/tables/cinder-netapp_eseries_iscsi.inc b/doc/source/configuration/tables/cinder-netapp_eseries_iscsi.inc deleted file mode 100644 index a92a416ca5f..00000000000 --- a/doc/source/configuration/tables/cinder-netapp_eseries_iscsi.inc +++ /dev/null @@ -1,48 +0,0 @@ -.. - Warning: Do not edit this file. It is automatically generated from the - software project's code and your changes will be overwritten. - - The tool to generate this file lives in openstack-doc-tools repository. - - Please make any changes needed in the code, then run the - autogenerate-config-doc tool from the openstack-doc-tools repository, or - ask for help on the documentation mailing list, IRC channel or meeting. - -.. _cinder-netapp_eseries_iscsi: - -.. list-table:: Description of NetApp E-Series driver configuration options - :header-rows: 1 - :class: config-ref-table - - * - Configuration option = Default value - - Description - * - **[DEFAULT]** - - - * - ``netapp_controller_ips`` = ``None`` - - (String) This option is only utilized when the storage family is configured to eseries. This option is used to restrict provisioning to the specified controllers. Specify the value of this option to be a comma separated list of controller hostnames or IP addresses to be used for provisioning. - * - ``netapp_enable_multiattach`` = ``False`` - - (Boolean) This option specifies whether the driver should allow operations that require multiple attachments to a volume. An example would be live migration of servers that have volumes attached. When enabled, this backend is limited to 256 total volumes in order to guarantee volumes can be accessed by more than one host. - * - ``netapp_host_type`` = ``None`` - - (String) This option defines the type of operating system for all initiators that can access a LUN. This information is used when mapping LUNs to individual hosts or groups of hosts. - * - ``netapp_login`` = ``None`` - - (String) Administrative user account name used to access the storage system or proxy server. - * - ``netapp_password`` = ``None`` - - (String) Password for the administrative user account specified in the netapp_login option. - * - ``netapp_pool_name_search_pattern`` = ``(.+)`` - - (String) This option is used to restrict provisioning to the specified pools. Specify the value of this option to be a regular expression which will be applied to the names of objects from the storage backend which represent pools in Cinder. This option is only utilized when the storage protocol is configured to use iSCSI or FC. - * - ``netapp_replication_aggregate_map`` = ``None`` - - (Unknown) Multi opt of dictionaries to represent the aggregate mapping between source and destination back ends when using whole back end replication. For every source aggregate associated with a cinder pool (NetApp FlexVol), you would need to specify the destination aggregate on the replication target device. A replication target device is configured with the configuration option replication_device. Specify this option as many times as you have replication devices. Each entry takes the standard dict config form: netapp_replication_aggregate_map = backend_id:,src_aggr_name1:dest_aggr_name1,src_aggr_name2:dest_aggr_name2,... - * - ``netapp_sa_password`` = ``None`` - - (String) Password for the NetApp E-Series storage array. - * - ``netapp_server_hostname`` = ``None`` - - (String) The hostname (or IP address) for the storage system or proxy server. - * - ``netapp_server_port`` = ``None`` - - (Integer) The TCP port to use for communication with the storage system or proxy server. If not specified, Data ONTAP drivers will use 80 for HTTP and 443 for HTTPS; E-Series will use 8080 for HTTP and 8443 for HTTPS. - * - ``netapp_snapmirror_quiesce_timeout`` = ``3600`` - - (Integer) The maximum time in seconds to wait for existing SnapMirror transfers to complete before aborting during a failover. - * - ``netapp_storage_family`` = ``ontap_cluster`` - - (String) The storage family type used on the storage system; valid values are ontap_cluster for using clustered Data ONTAP, or eseries for using E-Series. - * - ``netapp_transport_type`` = ``http`` - - (String) The transport protocol used when communicating with the storage system or proxy server. - * - ``netapp_webservice_path`` = ``/devmgr/v2`` - - (String) This option is used to specify the path to the E-Series proxy application on a proxy server. The value is combined with the value of the netapp_transport_type, netapp_server_hostname, and netapp_server_port options to create the URL used by the driver to connect to the proxy application. diff --git a/doc/source/contributor/groups.rst b/doc/source/contributor/groups.rst index f03b1cd710a..cac0d257b1d 100644 --- a/doc/source/contributor/groups.rst +++ b/doc/source/contributor/groups.rst @@ -33,7 +33,7 @@ Drivers currently supporting consistency groups are in the following: - Liberty: Dell Storage Center, EMC XtremIO, HPE 3Par and LeftHand -- Mitaka: EMC ScaleIO, NetApp Data ONTAP and E-Series, SolidFire +- Mitaka: EMC ScaleIO, NetApp Data ONTAP, SolidFire - Newton: CoprHD, FalconStor, Huawei diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 6cfddc84eb2..921846c4191 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -120,9 +120,6 @@ title=NEC Storage M Series Driver (iSCSI, FC) [driver.netapp_ontap] title=NetApp Data ONTAP Driver (iSCSI, NFS, FC) -[driver.netapp_e_ef] -title=NetApp E/EF Series Driver (iSCSI, FC) - [driver.netapp_solidfire] title=NetApp Solidfire Driver (iSCSI) @@ -234,7 +231,6 @@ driver.linbit_drbd=complete driver.lvm=complete driver.nec=complete driver.netapp_ontap=complete -driver.netapp_e_ef=complete driver.netapp_solidfire=complete driver.nexenta=complete driver.nfs=complete @@ -298,7 +294,6 @@ driver.linbit_drbd=complete driver.lvm=complete driver.nec=complete driver.netapp_ontap=missing -driver.netapp_e_ef=complete driver.netapp_solidfire=complete driver.nexenta=complete driver.nfs=complete @@ -362,7 +357,6 @@ driver.linbit_drbd=missing driver.lvm=missing driver.nec=complete driver.netapp_ontap=missing -driver.netapp_e_ef=missing driver.netapp_solidfire=missing driver.nexenta=missing driver.nfs=missing @@ -427,7 +421,6 @@ driver.linbit_drbd=missing driver.lvm=missing driver.nec=complete driver.netapp_ontap=complete -driver.netapp_e_ef=missing driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=missing @@ -493,7 +486,6 @@ driver.linbit_drbd=missing driver.lvm=missing driver.nec=missing driver.netapp_ontap=complete -driver.netapp_e_ef=missing driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=missing @@ -560,7 +552,6 @@ driver.linbit_drbd=missing driver.lvm=missing driver.nec=missing driver.netapp_ontap=complete -driver.netapp_e_ef=complete driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=missing @@ -626,7 +617,6 @@ driver.linbit_drbd=missing driver.lvm=complete driver.nec=missing driver.netapp_ontap=complete -driver.netapp_e_ef=complete driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=complete @@ -693,7 +683,6 @@ driver.linbit_drbd=missing driver.lvm=missing driver.nec=missing driver.netapp_ontap=missing -driver.netapp_e_ef=missing driver.netapp_solidfire=missing driver.nexenta=missing driver.nfs=missing @@ -760,7 +749,6 @@ driver.linbit_drbd=missing driver.lvm=complete driver.nec=missing driver.netapp_ontap=complete -driver.netapp_e_ef=missing driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=missing diff --git a/releasenotes/notes/remove_eseries-bb1bc134645aee50.yaml b/releasenotes/notes/remove_eseries-bb1bc134645aee50.yaml new file mode 100644 index 00000000000..162e2a3bac7 --- /dev/null +++ b/releasenotes/notes/remove_eseries-bb1bc134645aee50.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Support for NetApp E-Series has been removed. The + NetApp Unified driver can now only be used with + NetApp Clustered Data ONTAP.