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