# Copyright (c) 2020 Dell Inc. or its subsidiaries. # 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 uuid from oslo_log import log as logging from cinder import exception from cinder.i18n import _ from cinder.volume.drivers.dell_emc.vmax import masking from cinder.volume.drivers.dell_emc.vmax import provision from cinder.volume.drivers.dell_emc.vmax import utils LOG = logging.getLogger(__name__) class VMAXMigrate(object): """Upgrade class for Rest based PowerMax volume drivers. This upgrade class is for Dell EMC PowerMax volume drivers based on UniSphere Rest API. It supports VMAX 3 and VMAX All Flash and PowerMax arrays. """ def __init__(self, prtcl, rest): self.rest = rest self.utils = utils.VMAXUtils() self.masking = masking.VMAXMasking(prtcl, self.rest) self.provision = provision.VMAXProvision(self.rest) def do_migrate_if_candidate( self, array, srp, device_id, volume, connector): """Check and migrate if the volume is a candidate If the volume is in the legacy (SMIS) masking view structure move it to staging storage group within a staging masking view. :param array: array serial number :param srp: the SRP :param device_id: the volume device id :param volume: the volume object :param connector: the connector object """ mv_detail_list = list() masking_view_list, storage_group_list = ( self._get_mvs_and_sgs_from_volume( array, device_id)) for masking_view in masking_view_list: masking_view_dict = self.get_masking_view_component_dict( masking_view, srp) if masking_view_dict: mv_detail_list.append(masking_view_dict) if not mv_detail_list: return False if len(storage_group_list) != 1: LOG.warning("MIGRATE - The volume %(dev_id)s is not in one " "storage group as is expected for migration. " "The volume is in storage groups %(sg_list)s." "Migration will not proceed.", {'dev_id': device_id, 'sg_list': storage_group_list}) return False else: source_storage_group_name = storage_group_list[0] # Get the host that OpenStack has volume exposed to (it should only # be one host). os_host_list = self.get_volume_host_list(volume, connector) if len(os_host_list) != 1: LOG.warning("MIGRATE - OpenStack has recorded that " "%(dev_id)s is attached to hosts %(os_hosts)s " "and not 1 host as is expected. " "Migration will not proceed.", {'dev_id': device_id, 'os_hosts': os_host_list}) return False else: os_host_name = os_host_list[0] LOG.info("MIGRATE - Volume %(dev_id)s is a candidate for " "migration. The OpenStack host is %(os_host_name)s." "The volume is in storage group %(sg_name)s.", {'dev_id': device_id, 'os_host_name': os_host_name, 'sg_name': source_storage_group_name}) return self._perform_migration( array, device_id, mv_detail_list, source_storage_group_name, os_host_name) def _perform_migration( self, array, device_id, mv_detail_list, source_storage_group_name, os_host_name): """Perform steps so we can get the volume in a correct state. :param array: the storage array :param device_id: the device_id :param mv_detail_list: the masking view list :param source_storage_group_name: the source storage group :param os_host_name: the host the volume is exposed to :returns: boolean """ extra_specs = {utils.INTERVAL: 3, utils.RETRIES: 200} stg_sg_name = self._create_stg_storage_group_with_vol( array, os_host_name, extra_specs) if not stg_sg_name: # Throw an exception here exception_message = _("MIGRATE - Unable to create staging " "storage group.") LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) LOG.info("MIGRATE - Staging storage group %(stg_sg_name)s has " "been successfully created.", {'stg_sg_name': stg_sg_name}) new_stg_mvs = self._create_stg_masking_views( array, mv_detail_list, stg_sg_name, extra_specs) LOG.info("MIGRATE - Staging masking views %(new_stg_mvs)s have " "been successfully created.", {'new_stg_mvs': new_stg_mvs}) if not new_stg_mvs: exception_message = _("MIGRATE - Unable to create staging " "masking views.") LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) # Move volume from old storage group to new staging storage group self.move_volume_from_legacy_to_staging( array, device_id, source_storage_group_name, stg_sg_name, extra_specs) LOG.info("MIGRATE - Device id %(device_id)s has been successfully " "moved from %(src_sg)s to %(tgt_sg)s.", {'device_id': device_id, 'src_sg': source_storage_group_name, 'tgt_sg': stg_sg_name}) new_masking_view_list, new_storage_group_list = ( self._get_mvs_and_sgs_from_volume( array, device_id)) if len(new_storage_group_list) != 1: exception_message = (_( "MIGRATE - The current storage group list has %(list_len)d " "members. The list is %(sg_list)s. Will not proceed with " "cleanup. Please contact customer representative.") % { 'list_len': len(new_storage_group_list), 'sg_list': new_storage_group_list}) LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) else: current_storage_group_name = new_storage_group_list[0] if current_storage_group_name.lower() != stg_sg_name.lower(): exception_message = (_( "MIGRATE - The current storage group %(sg_1)s " "does not match %(sg_2)s. Will not proceed with " "cleanup. Please contact customer representative.") % { 'sg_1': current_storage_group_name, 'sg_2': stg_sg_name}) LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) if not self._delete_staging_masking_views( array, new_masking_view_list, os_host_name): exception_message = _("MIGRATE - Unable to delete staging masking " "views. Please contact customer " "representative.") LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) final_masking_view_list, final_storage_group_list = ( self._get_mvs_and_sgs_from_volume( array, device_id)) if len(final_masking_view_list) != 1: exception_message = (_( "MIGRATE - The final masking view list has %(list_len)d " "entries and not 1 entry as is expected. The list is " "%(mv_list)s. Please contact customer representative.") % { 'list_len': len(final_masking_view_list), 'sg_list': final_masking_view_list}) LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) return True def move_volume_from_legacy_to_staging( self, array, device_id, source_storage_group_name, stg_sg_name, extra_specs): """Move the volume from legacy SG to staging SG :param array: array serial number :param device_id: the device id of the volume :param source_storage_group_name: the source storage group :param stg_sg_name: the target staging storage group :param extra_specs: the extra specs """ num_vol_in_sg = self.rest.get_num_vols_in_sg( array, source_storage_group_name) if num_vol_in_sg == 1: # Can't move last volume and leave masking view empty # so creating a holder volume temp_vol_size = '1' hold_vol_name = 'hold-' + str(uuid.uuid1()) self.provision.create_volume_from_sg( array, hold_vol_name, source_storage_group_name, temp_vol_size, extra_specs) LOG.info("MIGRATE - Volume %(vol)s has been created because " "there was only one volume remaining in storage group " "%(src_sg)s and we are attempting a move it to staging " "storage group %(tgt_sg)s.", {'vol': hold_vol_name, 'src_sg': source_storage_group_name, 'tgt_sg': stg_sg_name}) self.rest.move_volume_between_storage_groups( array, device_id, source_storage_group_name, stg_sg_name, extra_specs) def _delete_staging_masking_views( self, array, masking_view_list, os_host_name): """Delete the staging masking views Delete the staging masking views except the masking view exposed to the OpenStack compute :param array: array serial number :param masking_view_list: masking view namelist :param os_host_name: the host the volume is exposed to in OpenStack :returns: boolean """ delete_mv_list = list() safe_to_delete = False for masking_view_name in masking_view_list: if os_host_name in masking_view_name: safe_to_delete = True else: delete_mv_list.append(masking_view_name) if safe_to_delete: for delete_mv in delete_mv_list: self.rest.delete_masking_view(array, delete_mv) LOG.info("MIGRATE - Masking view %(delete_mv)s has been " "successfully deleted.", {'delete_mv': delete_mv}) return safe_to_delete def _create_stg_masking_views( self, array, mv_detail_list, stg_sg_name, extra_specs): """Create a staging masking views :param array: array serial number :param mv_detail_list: masking view detail list :param stg_sg_name: staging storage group name :param extra_specs: the extra specs :returns: masking view list """ new_masking_view_list = list() for mv_detail in mv_detail_list: host_name = mv_detail.get('host') masking_view_name = mv_detail.get('mv_name') masking_view_components = self.rest.get_masking_view( array, masking_view_name) # Create a staging masking view random_uuid = uuid.uuid1() staging_mv_name = 'STG-' + host_name + '-' + str( random_uuid) + '-MV' if masking_view_components: self.rest.create_masking_view( array, staging_mv_name, stg_sg_name, masking_view_components.get('portGroupId'), masking_view_components.get('hostId'), extra_specs) masking_view_dict = self.rest.get_masking_view( array, staging_mv_name) if masking_view_dict: new_masking_view_list.append(staging_mv_name) else: LOG.warning("Failed to create staging masking view " "%(mv_name)s. Migration cannot proceed.", {'mv_name': masking_view_name}) return None return new_masking_view_list def _create_stg_storage_group_with_vol(self, array, os_host_name, extra_specs): """Create a staging storage group and add volume :param array: array serial number :param os_host_name: the openstack host name :param extra_specs: the extra specs :returns: storage group name """ random_uuid = uuid.uuid1() # Create a staging SG stg_sg_name = 'STG-' + os_host_name + '-' + ( str(random_uuid) + '-SG') temp_vol_name = 'tempvol-' + str(random_uuid) temp_vol_size = '1' _stg_storage_group = self.provision.create_storage_group( array, stg_sg_name, None, None, None, extra_specs) if _stg_storage_group: self.provision.create_volume_from_sg( array, temp_vol_name, stg_sg_name, temp_vol_size, extra_specs) return stg_sg_name else: return None def _get_mvs_and_sgs_from_volume(self, array, device_id): """Given a device Id get its storage groups and masking views. :param array: array serial number :param device_id: the volume device id :returns: masking view list, storage group list """ final_masking_view_list = [] storage_group_list = self.rest.get_storage_groups_from_volume( array, device_id) for sg in storage_group_list: masking_view_list = self.rest.get_masking_views_from_storage_group( array, sg) final_masking_view_list.extend(masking_view_list) return final_masking_view_list, storage_group_list def get_masking_view_component_dict( self, masking_view_name, srp): """Get components from input string. :param masking_view_name: the masking view name -- str :param srp: the srp -- str :returns: object components -- dict """ regex_str_share = ( r'^(?POS)-(?P.+?)((?P' + srp + r')-' r'(?P.+?)-(?P.+?)|(?PNo_SLO))' r'((?P-I|-F)|)' r'(?P-CD|)(?P-RE|)' r'(?P-[0-9A-Fa-f]{8}|)' r'-(?PMV)$') object_dict = self.utils.get_object_components_and_correct_host( regex_str_share, masking_view_name) if object_dict: object_dict['mv_name'] = masking_view_name return object_dict def get_volume_host_list(self, volume, connector): """Get host list attachments from connector object :param volume: the volume object :param connector: the connector object :returns os_host_list """ os_host_list = list() if connector is not None: attachment_list = volume.volume_attachment LOG.debug("Volume attachment list: %(atl)s. " "Attachment type: %(at)s", {'atl': attachment_list, 'at': type(attachment_list)}) try: att_list = attachment_list.objects except AttributeError: att_list = attachment_list if att_list is not None: host_list = [att.connector['host'] for att in att_list if att is not None and att.connector is not None] for host_name in host_list: os_host_list.append(self.utils.get_host_short_name(host_name)) return os_host_list def cleanup_staging_objects( self, array, storage_group_names, extra_specs): """Delete the staging masking views and storage groups :param array: the array serial number :param storage_group_names: a list of storage group names :param extra_specs: the extra specs """ def _do_cleanup(sg_name, device_id): masking_view_list = ( self.rest.get_masking_views_from_storage_group( array, sg_name)) for masking_view in masking_view_list: if 'STG-' in masking_view: self.rest.delete_masking_view(array, masking_view) self.rest.remove_vol_from_sg( array, sg_name, device_id, extra_specs) self.rest.delete_volume(array, device_id) self.rest.delete_storage_group(array, sg_name) for storage_group_name in storage_group_names: if 'STG-' in storage_group_name: volume_list = self.rest.get_volumes_in_storage_group( array, storage_group_name) if len(volume_list) == 1: try: _do_cleanup(storage_group_name, volume_list[0]) except Exception: LOG.warning("MIGRATE - An attempt was made to " "cleanup after a legacy live migration, " "but it failed. You may choose to " "cleanup manually.")