1# Copyright (c) 2017 Dell Inc. or its subsidiaries. 2# All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16import ast 17 18from oslo_log import log as logging 19 20from cinder import interface 21from cinder.volume import driver 22from cinder.volume.drivers.dell_emc.vmax import common 23from cinder.volume.drivers.san import san 24from cinder.zonemanager import utils as fczm_utils 25 26LOG = logging.getLogger(__name__) 27 28 29@interface.volumedriver 30class VMAXFCDriver(san.SanDriver, driver.FibreChannelDriver): 31 """FC Drivers for VMAX using REST. 32 33 Version history: 34 35 .. code-block:: none 36 37 1.0.0 - Initial driver 38 1.1.0 - Multiple pools and thick/thin provisioning, 39 performance enhancement. 40 2.0.0 - Add driver requirement functions 41 2.1.0 - Add consistency group functions 42 2.1.1 - Fixed issue with mismatched config (bug #1442376) 43 2.1.2 - Clean up failed clones (bug #1440154) 44 2.1.3 - Fixed a problem with FAST support (bug #1435069) 45 2.2.0 - Add manage/unmanage 46 2.2.1 - Support for SE 8.0.3 47 2.2.2 - Update Consistency Group 48 2.2.3 - Pool aware scheduler(multi-pool) support 49 2.2.4 - Create CG from CG snapshot 50 2.3.0 - Name change for MV and SG for FAST (bug #1515181) 51 - Fix for randomly choosing port group. (bug #1501919) 52 - get_short_host_name needs to be called in find_device_number 53 (bug #1520635) 54 - Proper error handling for invalid SLOs (bug #1512795) 55 - Extend Volume for VMAX3, SE8.1.0.3 56 https://blueprints.launchpad.net/cinder/+spec/vmax3-extend-volume 57 - Incorrect SG selected on an attach (#1515176) 58 - Cleanup Zoning (bug #1501938) NOTE: FC only 59 - Last volume in SG fix 60 - _remove_last_vol_and_delete_sg is not being called 61 for VMAX3 (bug #1520549) 62 - necessary updates for CG changes (#1534616) 63 - Changing PercentSynced to CopyState (bug #1517103) 64 - Getting iscsi ip from port in existing masking view 65 - Replacement of EMCGetTargetEndpoints api (bug #1512791) 66 - VMAX3 snapvx improvements (bug #1522821) 67 - Operations and timeout issues (bug #1538214) 68 2.4.0 - EMC VMAX - locking SG for concurrent threads (bug #1554634) 69 - SnapVX licensing checks for VMAX3 (bug #1587017) 70 - VMAX oversubscription Support (blueprint vmax-oversubscription) 71 - QoS support (blueprint vmax-qos) 72 2.5.0 - Attach and detach snapshot (blueprint vmax-attach-snapshot) 73 - MVs and SGs not reflecting correct protocol (bug #1640222) 74 - Storage assisted volume migration via retype 75 (bp vmax-volume-migration) 76 - Support for compression on All Flash 77 - Volume replication 2.1 (bp add-vmax-replication) 78 - rename and restructure driver (bp vmax-rename-dell-emc) 79 3.0.0 - REST based driver 80 - Retype (storage-assisted migration) 81 - QoS support 82 - Support for compression on All Flash 83 - Support for volume replication 84 - Support for live migration 85 - Support for Generic Volume Group 86 3.1.0 - Support for replication groups (Tiramisu) 87 - Deprecate backend xml configuration 88 - Support for async replication (vmax-replication-enhancements) 89 - Support for SRDF/Metro (vmax-replication-enhancements) 90 - Support for manage/unmanage snapshots 91 (vmax-manage-unmanage-snapshot) 92 - Support for revert to volume snapshot 93 backport from 3.2.0 94 - Fix for SSL verification/cert application (bug #1772924) 95 - Incorrect condition for an empty list (bug #1787219) 96 - Deleting snapshot that is source of multiple volumes fails 97 (bug #1768047) 98 - Block revert to snapshot for replicated volumes (bug #1777871) 99 - Fix for get-pools command (bug #1784856) 100 backport from 3.3.0 101 - Fix for initiator retrieval and short hostname unmapping 102 (bugs #1783855 #1783867) 103 - Fix for HyperMax OS Upgrade Bug (bug #1790141) 104 """ 105 106 VERSION = "3.1.1" 107 108 # ThirdPartySystems wiki 109 CI_WIKI_NAME = "EMC_VMAX_CI" 110 111 def __init__(self, *args, **kwargs): 112 113 super(VMAXFCDriver, self).__init__(*args, **kwargs) 114 self.active_backend_id = kwargs.get('active_backend_id', None) 115 self.common = common.VMAXCommon( 116 'FC', 117 self.VERSION, 118 configuration=self.configuration, 119 active_backend_id=self.active_backend_id) 120 self.zonemanager_lookup_service = fczm_utils.create_lookup_service() 121 122 def check_for_setup_error(self): 123 pass 124 125 def create_volume(self, volume): 126 """Creates a VMAX volume. 127 128 :param volume: the cinder volume object 129 :returns: provider location dict 130 """ 131 return self.common.create_volume(volume) 132 133 def create_volume_from_snapshot(self, volume, snapshot): 134 """Creates a volume from a snapshot. 135 136 :param volume: the cinder volume object 137 :param snapshot: the cinder snapshot object 138 :returns: provider location dict 139 """ 140 return self.common.create_volume_from_snapshot( 141 volume, snapshot) 142 143 def create_cloned_volume(self, volume, src_vref): 144 """Creates a cloned volume. 145 146 :param volume: the cinder volume object 147 :param src_vref: the source volume reference 148 :returns: provider location dict 149 """ 150 return self.common.create_cloned_volume(volume, src_vref) 151 152 def delete_volume(self, volume): 153 """Deletes a VMAX volume. 154 155 :param volume: the cinder volume object 156 """ 157 self.common.delete_volume(volume) 158 159 def create_snapshot(self, snapshot): 160 """Creates a snapshot. 161 162 :param snapshot: the cinder snapshot object 163 :returns: provider location dict 164 """ 165 src_volume = snapshot.volume 166 return self.common.create_snapshot(snapshot, src_volume) 167 168 def delete_snapshot(self, snapshot): 169 """Deletes a snapshot. 170 171 :param snapshot: the cinder snapshot object 172 """ 173 src_volume = snapshot.volume 174 self.common.delete_snapshot(snapshot, src_volume) 175 176 def ensure_export(self, context, volume): 177 """Driver entry point to get the export info for an existing volume. 178 179 :param context: the context 180 :param volume: the cinder volume object 181 """ 182 pass 183 184 def create_export(self, context, volume, connector): 185 """Driver entry point to get the export info for a new volume. 186 187 :param context: the context 188 :param volume: the cinder volume object 189 :param connector: the connector object 190 """ 191 pass 192 193 def remove_export(self, context, volume): 194 """Driver entry point to remove an export for a volume. 195 196 :param context: the context 197 :param volume: the cinder volume object 198 """ 199 pass 200 201 @staticmethod 202 def check_for_export(context, volume_id): 203 """Make sure volume is exported. 204 205 :param context: the context 206 :param volume_id: the volume id 207 """ 208 pass 209 210 @fczm_utils.add_fc_zone 211 def initialize_connection(self, volume, connector): 212 """Initializes the connection and returns connection info. 213 214 Assign any created volume to a compute node/host so that it can be 215 used from that host. 216 217 The driver returns a driver_volume_type of 'fibre_channel'. 218 The target_wwn can be a single entry or a list of wwns that 219 correspond to the list of remote wwn(s) that will export the volume. 220 Example return values: 221 { 222 'driver_volume_type': 'fibre_channel' 223 'data': { 224 'target_discovered': True, 225 'target_lun': 1, 226 'target_wwn': '1234567890123', 227 } 228 } 229 230 or 231 232 { 233 'driver_volume_type': 'fibre_channel' 234 'data': { 235 'target_discovered': True, 236 'target_lun': 1, 237 'target_wwn': ['1234567890123', '0987654321321'], 238 } 239 } 240 :param volume: the cinder volume object 241 :param connector: the connector object 242 :returns: dict -- the target_wwns and initiator_target_map 243 """ 244 device_info = self.common.initialize_connection( 245 volume, connector) 246 if device_info: 247 return self.populate_data(device_info, volume, connector) 248 else: 249 return {} 250 251 def populate_data(self, device_info, volume, connector): 252 """Populate data dict. 253 254 Add relevant data to data dict, target_lun, target_wwn and 255 initiator_target_map. 256 :param device_info: device_info 257 :param volume: the volume object 258 :param connector: the connector object 259 :returns: dict -- the target_wwns and initiator_target_map 260 """ 261 device_number = device_info['hostlunid'] 262 target_wwns, init_targ_map = self._build_initiator_target_map( 263 volume, connector) 264 265 data = {'driver_volume_type': 'fibre_channel', 266 'data': {'target_lun': device_number, 267 'target_discovered': True, 268 'target_wwn': target_wwns, 269 'initiator_target_map': init_targ_map}} 270 271 LOG.debug("Return FC data for zone addition: %(data)s.", 272 {'data': data}) 273 274 return data 275 276 @fczm_utils.remove_fc_zone 277 def terminate_connection(self, volume, connector, **kwargs): 278 """Disallow connection from connector. 279 280 Return empty data if other volumes are in the same zone. 281 The FibreChannel ZoneManager doesn't remove zones 282 if there isn't an initiator_target_map in the 283 return of terminate_connection. 284 285 :param volume: the volume object 286 :param connector: the connector object 287 :returns: dict -- the target_wwns and initiator_target_map if the 288 zone is to be removed, otherwise empty 289 """ 290 data = {'driver_volume_type': 'fibre_channel', 'data': {}} 291 zoning_mappings = {} 292 if connector: 293 zoning_mappings = self._get_zoning_mappings(volume, connector) 294 295 if zoning_mappings: 296 self.common.terminate_connection(volume, connector) 297 data = self._cleanup_zones(zoning_mappings) 298 return data 299 300 def _get_zoning_mappings(self, volume, connector): 301 """Get zoning mappings by building up initiator/target map. 302 303 :param volume: the volume object 304 :param connector: the connector object 305 :returns: dict -- the target_wwns and initiator_target_map if the 306 zone is to be removed, otherwise empty 307 """ 308 loc = volume.provider_location 309 name = ast.literal_eval(loc) 310 host = self.common.utils.get_host_short_name(connector['host']) 311 zoning_mappings = {} 312 try: 313 array = name['array'] 314 device_id = name['device_id'] 315 except KeyError: 316 array = name['keybindings']['SystemName'].split('+')[1].strip('-') 317 device_id = name['keybindings']['DeviceID'] 318 LOG.debug("Start FC detach process for volume: %(volume)s.", 319 {'volume': volume.name}) 320 321 masking_views, is_metro = ( 322 self.common.get_masking_views_from_volume( 323 array, volume, device_id, host)) 324 if masking_views: 325 portgroup = ( 326 self.common.get_port_group_from_masking_view( 327 array, masking_views[0])) 328 initiator_group = ( 329 self.common.get_initiator_group_from_masking_view( 330 array, masking_views[0])) 331 332 LOG.debug("Found port group: %(portGroup)s " 333 "in masking view %(maskingView)s.", 334 {'portGroup': portgroup, 335 'maskingView': masking_views[0]}) 336 # Map must be populated before the terminate_connection 337 target_wwns, init_targ_map = self._build_initiator_target_map( 338 volume, connector) 339 zoning_mappings = {'port_group': portgroup, 340 'initiator_group': initiator_group, 341 'target_wwns': target_wwns, 342 'init_targ_map': init_targ_map, 343 'array': array} 344 if is_metro: 345 rep_data = volume.replication_driver_data 346 name = ast.literal_eval(rep_data) 347 try: 348 metro_array = name['array'] 349 metro_device_id = name['device_id'] 350 except KeyError: 351 LOG.error("Cannot get remote Metro device information " 352 "for zone cleanup. Attempting terminate " 353 "connection...") 354 else: 355 masking_views, __ = ( 356 self.common.get_masking_views_from_volume( 357 metro_array, volume, metro_device_id, host)) 358 if masking_views: 359 metro_portgroup = ( 360 self.common.get_port_group_from_masking_view( 361 metro_array, masking_views[0])) 362 metro_ig = ( 363 self.common.get_initiator_group_from_masking_view( 364 metro_array, masking_views[0])) 365 zoning_mappings.update( 366 {'metro_port_group': metro_portgroup, 367 'metro_ig': metro_ig, 'metro_array': metro_array}) 368 if not masking_views: 369 LOG.warning("Volume %(volume)s is not in any masking view.", 370 {'volume': volume.name}) 371 return zoning_mappings 372 373 def _cleanup_zones(self, zoning_mappings): 374 """Cleanup zones after terminate connection. 375 376 :param zoning_mappings: zoning mapping dict 377 :returns: data - dict 378 """ 379 data = {'driver_volume_type': 'fibre_channel', 'data': {}} 380 try: 381 LOG.debug("Looking for masking views still associated with " 382 "Port Group %s.", zoning_mappings['port_group']) 383 masking_views = self.common.get_common_masking_views( 384 zoning_mappings['array'], zoning_mappings['port_group'], 385 zoning_mappings['initiator_group']) 386 except (KeyError, ValueError, TypeError): 387 masking_views = [] 388 389 if masking_views: 390 LOG.debug("Found %(numViews)d MaskingViews.", 391 {'numViews': len(masking_views)}) 392 else: # no masking views found 393 # Check if there any Metro masking views 394 if zoning_mappings.get('metro_array'): 395 masking_views = self.common.get_common_masking_views( 396 zoning_mappings['metro_array'], 397 zoning_mappings['metro_port_group'], 398 zoning_mappings['metro_ig']) 399 if not masking_views: 400 LOG.debug("No MaskingViews were found. Deleting zone.") 401 data = {'driver_volume_type': 'fibre_channel', 402 'data': {'target_wwn': zoning_mappings['target_wwns'], 403 'initiator_target_map': 404 zoning_mappings['init_targ_map']}} 405 406 LOG.debug("Return FC data for zone removal: %(data)s.", 407 {'data': data}) 408 409 return data 410 411 def _build_initiator_target_map(self, volume, connector): 412 """Build the target_wwns and the initiator target map. 413 414 :param volume: the cinder volume object 415 :param connector: the connector object 416 :returns: target_wwns -- list, init_targ_map -- dict 417 """ 418 target_wwns, init_targ_map = [], {} 419 initiator_wwns = connector['wwpns'] 420 fc_targets, metro_fc_targets = ( 421 self.common.get_target_wwns_from_masking_view( 422 volume, connector)) 423 424 if self.zonemanager_lookup_service: 425 fc_targets.extend(metro_fc_targets) 426 mapping = ( 427 self.zonemanager_lookup_service. 428 get_device_mapping_from_network(initiator_wwns, fc_targets)) 429 for entry in mapping: 430 map_d = mapping[entry] 431 target_wwns.extend(map_d['target_port_wwn_list']) 432 for initiator in map_d['initiator_port_wwn_list']: 433 init_targ_map[initiator] = map_d['target_port_wwn_list'] 434 else: # No lookup service, pre-zoned case. 435 target_wwns = fc_targets 436 fc_targets.extend(metro_fc_targets) 437 for initiator in initiator_wwns: 438 init_targ_map[initiator] = fc_targets 439 440 return list(set(target_wwns)), init_targ_map 441 442 def extend_volume(self, volume, new_size): 443 """Extend an existing volume. 444 445 :param volume: the cinder volume object 446 :param new_size: the required new size 447 """ 448 self.common.extend_volume(volume, new_size) 449 450 def get_volume_stats(self, refresh=False): 451 """Get volume stats. 452 453 :param refresh: boolean -- If True, run update the stats first. 454 :returns: dict -- the stats dict 455 """ 456 if refresh: 457 self.update_volume_stats() 458 459 return self._stats 460 461 def update_volume_stats(self): 462 """Retrieve stats info from volume group.""" 463 LOG.debug("Updating volume stats") 464 data = self.common.update_volume_stats() 465 data['storage_protocol'] = 'FC' 466 data['driver_version'] = self.VERSION 467 self._stats = data 468 469 def manage_existing(self, volume, external_ref): 470 """Manages an existing VMAX Volume (import to Cinder). 471 472 Renames the Volume to match the expected name for the volume. 473 Also need to consider things like QoS, Emulation, account/tenant. 474 :param volume: the volume object 475 :param external_ref: the reference for the VMAX volume 476 :returns: model_update 477 """ 478 return self.common.manage_existing(volume, external_ref) 479 480 def manage_existing_get_size(self, volume, external_ref): 481 """Return size of an existing VMAX volume to manage_existing. 482 483 :param self: reference to class 484 :param volume: the volume object including the volume_type_id 485 :param external_ref: reference to the existing volume 486 :returns: size of the volume in GB 487 """ 488 return self.common.manage_existing_get_size(volume, external_ref) 489 490 def unmanage(self, volume): 491 """Export VMAX volume from Cinder. 492 493 Leave the volume intact on the backend array. 494 """ 495 return self.common.unmanage(volume) 496 497 def manage_existing_snapshot(self, snapshot, existing_ref): 498 """Manage an existing VMAX Snapshot (import to Cinder). 499 500 Renames the Snapshot to prefix it with OS- to indicate 501 it is managed by Cinder. 502 503 :param snapshot: the snapshot object 504 :param existing_ref: the snapshot name on the backend VMAX 505 :returns: model_update 506 """ 507 return self.common.manage_existing_snapshot(snapshot, existing_ref) 508 509 def manage_existing_snapshot_get_size(self, snapshot, existing_ref): 510 """Return the size of the source volume for manage-existing-snapshot. 511 512 :param snapshot: the snapshot object 513 :param existing_ref: the snapshot name on the backend VMAX 514 :returns: size of the source volume in GB 515 """ 516 return self.common.manage_existing_snapshot_get_size(snapshot) 517 518 def unmanage_snapshot(self, snapshot): 519 """Export VMAX Snapshot from Cinder. 520 521 Leaves the snapshot intact on the backend VMAX. 522 523 :param snapshot: the snapshot object 524 """ 525 self.common.unmanage_snapshot(snapshot) 526 527 def retype(self, ctxt, volume, new_type, diff, host): 528 """Migrate volume to another host using retype. 529 530 :param ctxt: context 531 :param volume: the volume object including the volume_type_id 532 :param new_type: the new volume type. 533 :param diff: difference between old and new volume types. 534 Unused in driver. 535 :param host: the host dict holding the relevant 536 target(destination) information 537 :returns: boolean -- True if retype succeeded, False if error 538 """ 539 return self.common.retype(volume, new_type, host) 540 541 def failover_host(self, context, volumes, secondary_id=None, groups=None): 542 """Failover volumes to a secondary host/ backend. 543 544 :param context: the context 545 :param volumes: the list of volumes to be failed over 546 :param secondary_id: the backend to be failed over to, is 'default' 547 if fail back 548 :param groups: replication groups 549 :returns: secondary_id, volume_update_list, group_update_list 550 """ 551 return self.common.failover_host(volumes, secondary_id, groups) 552 553 def create_group(self, context, group): 554 """Creates a generic volume group. 555 556 :param context: the context 557 :param group: the group object 558 :returns: model_update 559 """ 560 return self.common.create_group(context, group) 561 562 def delete_group(self, context, group, volumes): 563 """Deletes a generic volume group. 564 565 :param context: the context 566 :param group: the group object 567 :param volumes: the member volumes 568 """ 569 return self.common.delete_group( 570 context, group, volumes) 571 572 def create_group_snapshot(self, context, group_snapshot, snapshots): 573 """Creates a group snapshot. 574 575 :param context: the context 576 :param group_snapshot: the grouop snapshot 577 :param snapshots: snapshots list 578 """ 579 return self.common.create_group_snapshot(context, 580 group_snapshot, snapshots) 581 582 def delete_group_snapshot(self, context, group_snapshot, snapshots): 583 """Deletes a group snapshot. 584 585 :param context: the context 586 :param group_snapshot: the grouop snapshot 587 :param snapshots: snapshots list 588 """ 589 return self.common.delete_group_snapshot(context, 590 group_snapshot, snapshots) 591 592 def update_group(self, context, group, 593 add_volumes=None, remove_volumes=None): 594 """Updates LUNs in generic volume group. 595 596 :param context: the context 597 :param group: the group object 598 :param add_volumes: flag for adding volumes 599 :param remove_volumes: flag for removing volumes 600 """ 601 return self.common.update_group(group, add_volumes, 602 remove_volumes) 603 604 def create_group_from_src( 605 self, context, group, volumes, group_snapshot=None, 606 snapshots=None, source_group=None, source_vols=None): 607 """Creates the volume group from source. 608 609 :param context: the context 610 :param group: the group object to be created 611 :param volumes: volumes in the group 612 :param group_snapshot: the source volume group snapshot 613 :param snapshots: snapshots of the source volumes 614 :param source_group: the dictionary of a volume group as source. 615 :param source_vols: a list of volume dictionaries in the source_group. 616 """ 617 return self.common.create_group_from_src( 618 context, group, volumes, group_snapshot, snapshots, source_group, 619 source_vols) 620 621 def enable_replication(self, context, group, volumes): 622 """Enable replication for a group. 623 624 :param context: the context 625 :param group: the group object 626 :param volumes: the list of volumes 627 :returns: model_update, None 628 """ 629 return self.common.enable_replication(context, group, volumes) 630 631 def disable_replication(self, context, group, volumes): 632 """Disable replication for a group. 633 634 :param context: the context 635 :param group: the group object 636 :param volumes: the list of volumes 637 :returns: model_update, None 638 """ 639 return self.common.disable_replication(context, group, volumes) 640 641 def failover_replication(self, context, group, volumes, 642 secondary_backend_id=None): 643 """Failover replication for a group. 644 645 :param context: the context 646 :param group: the group object 647 :param volumes: the list of volumes 648 :param secondary_backend_id: the secondary backend id - default None 649 :returns: model_update, vol_model_updates 650 """ 651 return self.common.failover_replication( 652 context, group, volumes, secondary_backend_id) 653 654 def revert_to_snapshot(self, context, volume, snapshot): 655 """Revert volume to snapshot 656 657 :param context: the context 658 :param volume: the cinder volume object 659 :param snapshot: the cinder snapshot object 660 """ 661 self.common.revert_to_snapshot(volume, snapshot) 662