/* * Copyright (c) 2013 EMC Corporation * All Rights Reserved */ package com.emc.storageos.protectioncontroller.impl.recoverpoint; import static com.emc.storageos.db.client.constraint.AlternateIdConstraint.Factory.getRpSourceVolumeByTarget; import static com.emc.storageos.db.client.constraint.AlternateIdConstraint.Factory.getVolumesByAssociatedId; import static com.emc.storageos.db.client.constraint.ContainmentConstraint.Factory.getVolumesByConsistencyGroup; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.blockorchestrationcontroller.VolumeDescriptor; import com.emc.storageos.db.client.DbClient; import com.emc.storageos.db.client.URIUtil; import com.emc.storageos.db.client.constraint.Constraint; import com.emc.storageos.db.client.constraint.ContainmentConstraint; import com.emc.storageos.db.client.constraint.URIQueryResultList; import com.emc.storageos.db.client.model.AbstractChangeTrackingSet; import com.emc.storageos.db.client.model.BlockConsistencyGroup; import com.emc.storageos.db.client.model.BlockSnapshot; import com.emc.storageos.db.client.model.DataObject.Flag; import com.emc.storageos.db.client.model.DiscoveredDataObject.Type; import com.emc.storageos.db.client.model.ExportGroup; import com.emc.storageos.db.client.model.ExportGroup.ExportGroupType; import com.emc.storageos.db.client.model.Initiator; import com.emc.storageos.db.client.model.NamedURI; import com.emc.storageos.db.client.model.Network; import com.emc.storageos.db.client.model.OpStatusMap; import com.emc.storageos.db.client.model.Project; import com.emc.storageos.db.client.model.ProtectionSet; import com.emc.storageos.db.client.model.ProtectionSystem; import com.emc.storageos.db.client.model.StoragePool; import com.emc.storageos.db.client.model.StoragePort; import com.emc.storageos.db.client.model.StorageSystem; import com.emc.storageos.db.client.model.StringMap; import com.emc.storageos.db.client.model.StringSet; import com.emc.storageos.db.client.model.VirtualArray; import com.emc.storageos.db.client.model.VirtualPool; import com.emc.storageos.db.client.model.Volume; import com.emc.storageos.db.client.model.Volume.PersonalityTypes; import com.emc.storageos.db.client.model.Volume.VolumeAccessState; import com.emc.storageos.db.client.model.VpoolProtectionVarraySettings; import com.emc.storageos.db.client.model.UnManagedDiscoveredObjects.UnManagedProtectionSet; import com.emc.storageos.db.client.model.UnManagedDiscoveredObjects.UnManagedVolume; import com.emc.storageos.db.client.model.UnManagedDiscoveredObjects.UnManagedVolume.SupportedVolumeInformation; import com.emc.storageos.db.client.util.CustomQueryUtility; import com.emc.storageos.db.client.util.NullColumnValueGetter; import com.emc.storageos.db.client.util.SizeUtil; import com.emc.storageos.exceptions.DeviceControllerException; import com.emc.storageos.exceptions.DeviceControllerExceptions; import com.emc.storageos.model.block.Copy; import com.emc.storageos.recoverpoint.exceptions.RecoverPointException; import com.emc.storageos.recoverpoint.impl.RecoverPointClient; import com.emc.storageos.recoverpoint.objectmodel.RPBookmark; import com.emc.storageos.recoverpoint.responses.GetBookmarksResponse; import com.emc.storageos.recoverpoint.responses.GetCGsResponse; import com.emc.storageos.recoverpoint.responses.GetRSetResponse; import com.emc.storageos.recoverpoint.responses.GetVolumeResponse; import com.emc.storageos.recoverpoint.utils.RecoverPointClientFactory; import com.emc.storageos.recoverpoint.utils.RecoverPointUtils; import com.emc.storageos.svcs.errorhandling.resources.APIException; import com.emc.storageos.svcs.errorhandling.resources.InternalException; import com.emc.storageos.util.ConnectivityUtil; import com.emc.storageos.util.ExportUtils; import com.emc.storageos.util.NetworkLite; import com.emc.storageos.util.NetworkUtil; import com.emc.storageos.util.VPlexUtil; import com.emc.storageos.volumecontroller.RPRecommendation; import com.emc.storageos.volumecontroller.Recommendation; import com.emc.storageos.volumecontroller.impl.BiosCommandResult; import com.emc.storageos.volumecontroller.impl.smis.MetaVolumeRecommendation; import com.emc.storageos.volumecontroller.impl.utils.MetaVolumeUtils; import com.google.common.base.Joiner; import com.google.common.collect.ComparisonChain; /** * RecoverPoint specific helper bean */ public class RPHelper { private static final String VOL_DELIMITER = "-"; private static final double RP_DEFAULT_JOURNAL_POLICY = 0.25; public static final String REMOTE = "remote"; public static final String LOCAL = "local"; public static final String SOURCE = "source"; public static final String TARGET = "target"; public static final String JOURNAL = "journal"; public static final Long DEFAULT_RP_JOURNAL_SIZE_IN_BYTES = 10737418240L; // default minimum journal size is 10GB (in bytes) private static final Logger _log = LoggerFactory.getLogger(RPHelper.class); private static final String HTTPS = "https"; private static final String WSDL = "wsdl"; private static final String RP_ENDPOINT = "/fapi/version4_1"; private static final String LOG_MSG_OPERATION_TYPE_DELETE = "delete"; private static final String LOG_MSG_OPERATION_TYPE_REMOVE_PROTECTION = "remove protection from"; private static final String LOG_MSG_VOLUME_TYPE_RP = "RP_SOURCE"; private static final String LOG_MSG_VOLUME_TYPE_RPVPLEX = "RP_VPLEX_VIRT_SOURCE"; public static final String REMOVE_PROTECTION = "REMOVE_PROTECTION"; /** * Get all of the replication set volumes for a list of volumes; the sources and all of their targets. * * @param volumeIds List of volumes to find all volume for their RSets * @param dbClient DbClient ref * @return all RSet volumes */ public static Set<URI> getReplicationSetVolumes(List<URI> volumeIds, DbClient dbClient) { Set<URI> volumeSet = new HashSet<URI>(); Iterator<Volume> volumes = dbClient.queryIterativeObjects(Volume.class, volumeIds); while (volumes.hasNext()) { Volume volume = volumes.next(); volumeSet.addAll(getReplicationSetVolumes(volume, dbClient)); } return volumeSet; } /** * Get all of the volumes in this replication set; the source and all of its targets. * For a multi-CG protection, it only returns the targets (and source) associated with this one volume. * * @param volume volume object * @param dbClient DbClient ref * @return list of volume URIs * @throws DeviceControllerException */ public static List<URI> getReplicationSetVolumes(Volume volume, DbClient dbClient) throws DeviceControllerException { if (volume == null) { throw DeviceControllerException.exceptions.invalidObjectNull(); } List<URI> volumeIDs = new ArrayList<URI>(); for (Volume vol : getVolumesInRSet(volume, dbClient)) { volumeIDs.add(vol.getId()); } return volumeIDs; } /** * Helper Method: The caller wants to get the protection settings associated with a specific virtual array * and virtual pool. Handle the exceptions appropriately. * * @param vpool VirtualPool to look for * @param varray VirtualArray to protect to * @param dbClient DbClient ref * @return the stored protection settings object * @throws InternalException */ public static VpoolProtectionVarraySettings getProtectionSettings(VirtualPool vpool, VirtualArray varray, DbClient dbClient) throws InternalException { if (vpool.getProtectionVarraySettings() != null) { String settingsID = vpool.getProtectionVarraySettings().get(varray.getId().toString()); try { return (dbClient.queryObject(VpoolProtectionVarraySettings.class, URI.create(settingsID))); } catch (IllegalArgumentException e) { throw DeviceControllerException.exceptions.invalidURI(e); } } throw DeviceControllerException.exceptions.objectNotFound(varray.getId()); } /** * Gets the virtual pool of the target copy. * * @param tgtVarray * @param dbClient DbClient ref * @param srcVpool the base virtual pool * @return vpool of the target copy */ public static VirtualPool getTargetVirtualPool(VirtualArray tgtVarray, VirtualPool srcVpool, DbClient dbClient) { VpoolProtectionVarraySettings settings = getProtectionSettings(srcVpool, tgtVarray, dbClient); // If there was no vpool specified use the source vpool for this varray. VirtualPool tgtVpool = srcVpool; if (settings.getVirtualPool() != null) { tgtVpool = dbClient.queryObject(VirtualPool.class, settings.getVirtualPool()); } return tgtVpool; } /** * Given one volume in an rset (either source or any target) return all source and target volumes in that rset * * @param vol A volume in the Rset * @param dbClient DbClient ref * @return All RSet volumes */ private static List<Volume> getVolumesInRSet(Volume volume, DbClient dbClient) { List<Volume> allVolumesInRSet = new ArrayList<Volume>(); Volume sourceVol = null; if (Volume.PersonalityTypes.SOURCE.name().equalsIgnoreCase(volume.getPersonality())) { sourceVol = volume; } else { sourceVol = getRPSourceVolumeFromTarget(dbClient, volume); } if (sourceVol != null) { allVolumesInRSet.add(sourceVol); if (sourceVol.getRpTargets() != null) { for (String tgtVolId : sourceVol.getRpTargets()) { if (tgtVolId.equals(volume.getId().toString())) { allVolumesInRSet.add(volume); } else { Volume tgt = dbClient.queryObject(Volume.class, URI.create(tgtVolId)); if (tgt != null && !tgt.getInactive()) { allVolumesInRSet.add(tgt); } // if this target was previously the Metropoint active source, go out and get the standby copy if (tgt != null && RPHelper.isMetroPointVolume(dbClient, tgt)) { allVolumesInRSet.addAll(getMetropointStandbyCopies(tgt, dbClient)); } } } } } else if (volume.checkInternalFlags(Flag.PARTIALLY_INGESTED)) { allVolumesInRSet.add(volume); } return allVolumesInRSet; } /** * Gets a volume's associated target volumes. * * @param volume the volume whose targets we want to find. * @param dbClient DbClient ref * @return the list of associated target volumes. */ public static List<Volume> getTargetVolumes(Volume volume, DbClient dbClient) { List<Volume> targets = new ArrayList<Volume>(); if (volume != null && PersonalityTypes.SOURCE.name().equals(volume.getPersonality())) { List<Volume> rsetVolumes = getVolumesInRSet(volume, dbClient); for (Volume rsetVolume : rsetVolumes) { if (PersonalityTypes.TARGET.name().equals(rsetVolume.getPersonality())) { targets.add(rsetVolume); } } } return targets; } /** * This method will return all volumes that should be deleted based on the entire list of volumes to be deleted. * If this is the last source volume in the CG, this method will return all journal volumes as well. * * @param reqDeleteVolumes all volumes in the delete request * @param dbClient DbClient * @return list of volumes to unexport and delete * @throws InternalException * @throws URISyntaxException */ public static Set<URI> getVolumesToDelete(Collection<URI> reqDeleteVolumes, DbClient dbClient) throws InternalException { _log.info(String.format("Getting all RP volumes to delete for requested list: %s", reqDeleteVolumes)); Set<URI> volumeIDs = new HashSet<URI>(); Set<URI> protectionSetIds = new HashSet<URI>(); Iterator<Volume> volumes = dbClient.queryIterativeObjects(Volume.class, reqDeleteVolumes, true); // Divide the RP volumes by BlockConsistencyGroup so we can determine if all volumes in the // RP consistency group are being removed. Map<URI, Set<URI>> cgsToVolumesForDelete = new HashMap<URI, Set<URI>>(); // for each volume requested to be deleted, add that volume plus any source or target related // to that volume to the list of volumes to be deleted while (volumes.hasNext()) { Volume volume = volumes.next(); // get the list of all source and target volumes in the same replication set as the // volume passed in List<Volume> allVolsInRSet = getVolumesInRSet(volume, dbClient); List<URI> allVolsInRSetURI = new ArrayList<URI>(); URI cgURI = null; // Loop through the replication set volumes to: // 1. Determine the consistency group. // 2. Keep track of the protection set if one is being referenced. This will be used // later to perform a cleanup operation. // 3. If partially ingested volume, clean up corresponding unmanaged protection set for (Volume vol : allVolsInRSet) { allVolsInRSetURI.add(vol.getId()); if (!NullColumnValueGetter.isNullURI(vol.getConsistencyGroup())) { cgURI = vol.getConsistencyGroup(); } if (!NullColumnValueGetter.isNullNamedURI(vol.getProtectionSet())) { // Keep track of the protection sets for a cleanup operation later in case we // find any stale volume references protectionSetIds.add(vol.getProtectionSet().getURI()); } // If this is a partially ingested RP volume, clean up the corresponding unmanaged protection set List<UnManagedProtectionSet> umpsets = CustomQueryUtility.getUnManagedProtectionSetByManagedVolumeId(dbClient, vol.getId().toString()); for (UnManagedProtectionSet umpset : umpsets) { umpset.getManagedVolumeIds().remove(vol.getId().toString()); // Clean up the volume's reference, if any, in the unmanaged volumes associated with the unmanaged protection set for (String umv : umpset.getUnManagedVolumeIds()) { UnManagedVolume umVolume = dbClient.queryObject(UnManagedVolume.class, URI.create(umv)); StringSet rpManagedSourceVolumeInfo = umVolume.getVolumeInformation() .get(SupportedVolumeInformation.RP_MANAGED_SOURCE_VOLUME.toString()); StringSet rpManagedTargetVolumeInfo = umVolume.getVolumeInformation() .get(SupportedVolumeInformation.RP_MANAGED_TARGET_VOLUMES.toString()); if (rpManagedSourceVolumeInfo != null && !rpManagedSourceVolumeInfo.isEmpty()) { rpManagedSourceVolumeInfo.remove(vol.getId().toString()); } if (rpManagedTargetVolumeInfo != null && !rpManagedTargetVolumeInfo.isEmpty()) { rpManagedTargetVolumeInfo.remove(vol.getId().toString()); } dbClient.updateObject(umVolume); } dbClient.updateObject(umpset); } } // Add the replication set volume IDs to the list of volumes to be deleted _log.info(String.format("Adding volume %s to the list of volumes to be deleted", allVolsInRSetURI.toString())); volumeIDs.addAll(allVolsInRSetURI); // Add a mapping of consistency groups to volumes to determine if we are deleting // the entire CG which would indicate journals are also being deleted. if (cgURI != null) { if (cgsToVolumesForDelete.get(cgURI) == null) { cgsToVolumesForDelete.put(cgURI, new HashSet<URI>()); } cgsToVolumesForDelete.get(cgURI).addAll(allVolsInRSetURI); } else { _log.warn(String .format("Unable to find a valid CG for replication set volumes %s. Unable to determine if the entire CG is being deleted as part of this request.", allVolsInRSetURI.toString())); } } // Determine if we're deleting all of the volumes in this consistency group for (Map.Entry<URI, Set<URI>> cgToVolumesForDelete : cgsToVolumesForDelete.entrySet()) { BlockConsistencyGroup cg = null; URI cgURI = cgToVolumesForDelete.getKey(); cg = dbClient.queryObject(BlockConsistencyGroup.class, cgURI); List<Volume> cgVolumes = getAllCgVolumes(cgURI, dbClient); // determine if all of the source and target volumes in the consistency group are on the list // of volumes to delete; if so, we will add the journal volumes to the list. // also create a list of stale volumes to be removed from the protection set boolean wholeCG = true; if (cgVolumes != null) { for (Volume cgVol : cgVolumes) { Set<URI> cgVolsToDelete = cgToVolumesForDelete.getValue(); // If the CG volume is not in the list of volumes to delete for this CG, we must // determine if it's a journal or another source/target not being deleted. if (!cgVolsToDelete.contains(cgVol.getId())) { // Do not consider VPlex backing volumes or inactive volumes if (!cgVol.getInactive() && NullColumnValueGetter.isNotNullValue(cgVol.getPersonality())) { if (!Volume.PersonalityTypes.METADATA.toString().equals(cgVol.getPersonality())) { // the volume is either a source or target; this means there are other volumes in the rset wholeCG = false; break; } } } } } if (wholeCG) { // We are removing the CG, determine all the journal volumes in it and // add them to the list of volumes to be removed if (cg != null) { List<Volume> allJournals = getCgVolumes(dbClient, cg.getId(), Volume.PersonalityTypes.METADATA.toString()); if (allJournals != null && !allJournals.isEmpty()) { Set<URI> allJournalURIs = new HashSet<URI>(); for (Volume journalVolume : allJournals) { allJournalURIs.add(journalVolume.getId()); } _log.info(String .format("Determined that this is a request to delete consistency group %s. Adding journal volumes to the list of volumes to delete: %s", cgURI, allJournalURIs.toString())); volumeIDs.addAll(allJournalURIs); } } else { _log.info(String.format( "Could not determine journal volumes for consistency group %s .", cgToVolumesForDelete.getKey())); } } else { _log.info(String.format( "Consistency group %s will not be removed. Only a subset of the replication sets are being removed.", cgToVolumesForDelete.getKey())); } } // Clean-up stale ProtectionSet volume references. This is just a cautionary operation to prevent // "bad things" from happening. for (URI protSetId : protectionSetIds) { List<String> staleVolumes = new ArrayList<String>(); ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, protSetId); if (protectionSet.getVolumes() != null) { for (String protSetVol : protectionSet.getVolumes()) { URI protSetVolUri = URI.create(protSetVol); if (!volumeIDs.contains(protSetVolUri)) { Volume vol = dbClient.queryObject(Volume.class, protSetVolUri); if (vol == null || vol.getInactive()) { // The ProtectionSet references a stale volume that no longer exists in the DB. _log.info("ProtectionSet " + protectionSet.getLabel() + " references volume " + protSetVol + " that no longer exists in the DB. Removing this volume reference."); staleVolumes.add(protSetVol); } } } } // remove stale entries from protection set if (!staleVolumes.isEmpty()) { for (String vol : staleVolumes) { protectionSet.getVolumes().remove(vol); } dbClient.updateObject(protectionSet); } } return volumeIDs; } /** * Gets volume descriptors for volumes in an RP protection to be deleted * handles vplex andnon-vplex as well as mixed storage configurations * (e.g. vplex source and non-vplex targets) * * @param systemURI System that the delete request belongs to * @param volumeURIs All volumes to be deleted * @param deletionType The type of deletion * @param newVpool Only used when removing protection, the new vpool to move the volume to * @param dbClient DbClient ref * @return All descriptors needed to clean up volumes */ public static List<VolumeDescriptor> getDescriptorsForVolumesToBeDeleted(URI systemURI, List<URI> volumeURIs, String deletionType, VirtualPool newVpool, DbClient dbClient) { List<VolumeDescriptor> volumeDescriptors = new ArrayList<VolumeDescriptor>(); try { Set<URI> allVolumeIds = getVolumesToDelete(volumeURIs, dbClient); for (URI volumeURI : allVolumeIds) { Volume volume = dbClient.queryObject(Volume.class, volumeURI); VolumeDescriptor descriptor = null; boolean isSourceVolume = false; // if RP source, add a descriptor for the RP source if (volume.getPersonality().equals(Volume.PersonalityTypes.SOURCE.toString())) { isSourceVolume = true; String volumeType = LOG_MSG_VOLUME_TYPE_RP; String operationType = LOG_MSG_OPERATION_TYPE_DELETE; if (volume.getAssociatedVolumes() != null && !volume.getAssociatedVolumes().isEmpty()) { volumeType = LOG_MSG_VOLUME_TYPE_RPVPLEX; descriptor = new VolumeDescriptor(VolumeDescriptor.Type.RP_VPLEX_VIRT_SOURCE, volume.getStorageController(), volume.getId(), null, null); } else { descriptor = new VolumeDescriptor(VolumeDescriptor.Type.RP_SOURCE, volume.getStorageController(), volume.getId(), null, null); } if (REMOVE_PROTECTION.equals(deletionType)) { operationType = LOG_MSG_OPERATION_TYPE_REMOVE_PROTECTION; Map<String, Object> volumeParams = new HashMap<String, Object>(); volumeParams.put(VolumeDescriptor.PARAM_DO_NOT_DELETE_VOLUME, Boolean.TRUE); volumeParams.put(VolumeDescriptor.PARAM_VPOOL_CHANGE_NEW_VPOOL_ID, newVpool.getId()); descriptor.setParameters(volumeParams); } _log.info(String.format("Adding %s descriptor to %s%s volume [%s] (%s)", volumeType, operationType, (volumeType.equals(LOG_MSG_VOLUME_TYPE_RP) ? "" : " virtual"), volume.getLabel(), volume.getId())); volumeDescriptors.add(descriptor); } // If this is a virtual volume, add a descriptor for the virtual volume if (RPHelper.isVPlexVolume(volume, dbClient)) { // VPLEX virtual volume descriptor = new VolumeDescriptor(VolumeDescriptor.Type.VPLEX_VIRT_VOLUME, volume.getStorageController(), volume.getId(), null, null); String operationType = LOG_MSG_OPERATION_TYPE_DELETE; // Add a flag to not delete this virtual volume if this is a Source volume and // the deletion type is Remove Protection if (isSourceVolume && REMOVE_PROTECTION.equals(deletionType)) { operationType = LOG_MSG_OPERATION_TYPE_REMOVE_PROTECTION; Map<String, Object> volumeParams = new HashMap<String, Object>(); volumeParams.put(VolumeDescriptor.PARAM_DO_NOT_DELETE_VOLUME, Boolean.TRUE); descriptor.setParameters(volumeParams); } _log.info(String.format("Adding VPLEX_VIRT_VOLUME descriptor to %s virtual volume [%s] (%s)", operationType, volume.getLabel(), volume.getId())); volumeDescriptors.add(descriptor); // Next, add all the BLOCK volume descriptors for the VPLEX back-end volumes for (String associatedVolumeId : volume.getAssociatedVolumes()) { operationType = LOG_MSG_OPERATION_TYPE_DELETE; Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(associatedVolumeId)); // a previous failed delete may have already removed associated volumes if (associatedVolume != null && !associatedVolume.getInactive()) { descriptor = new VolumeDescriptor(VolumeDescriptor.Type.BLOCK_DATA, associatedVolume.getStorageController(), associatedVolume.getId(), associatedVolume.getPool(), associatedVolume.getConsistencyGroup(), null); // Add a flag to not delete these backing volumes if this is a Source volume and // the deletion type is Remove Protection if (isSourceVolume && REMOVE_PROTECTION.equals(deletionType)) { operationType = LOG_MSG_OPERATION_TYPE_REMOVE_PROTECTION; Map<String, Object> volumeParams = new HashMap<String, Object>(); volumeParams.put(VolumeDescriptor.PARAM_DO_NOT_DELETE_VOLUME, Boolean.TRUE); descriptor.setParameters(volumeParams); } _log.info(String.format("Adding BLOCK_DATA descriptor to %s virtual volume backing volume [%s] (%s)", operationType, associatedVolume.getLabel(), associatedVolume.getId())); volumeDescriptors.add(descriptor); } } } else { String operationType = LOG_MSG_OPERATION_TYPE_DELETE; descriptor = new VolumeDescriptor(VolumeDescriptor.Type.BLOCK_DATA, volume.getStorageController(), volume.getId(), null, null); // Add a flag to not delete this volume if this is a Source volume and // the deletion type is Remove Protection if (isSourceVolume && REMOVE_PROTECTION.equals(deletionType)) { operationType = LOG_MSG_OPERATION_TYPE_REMOVE_PROTECTION; Map<String, Object> volumeParams = new HashMap<String, Object>(); volumeParams.put(VolumeDescriptor.PARAM_DO_NOT_DELETE_VOLUME, Boolean.TRUE); volumeParams.put(VolumeDescriptor.PARAM_VPOOL_CHANGE_NEW_VPOOL_ID, newVpool.getId()); descriptor.setParameters(volumeParams); } _log.info(String.format("Adding BLOCK_DATA descriptor to %s volume [%s] (%s)", operationType, volume.getLabel(), volume.getId())); volumeDescriptors.add(descriptor); } } } catch (Exception e) { throw RecoverPointException.exceptions.deletingRPVolume(e); } return volumeDescriptors; } /** * Determine if the protection set's source volumes are represented in the volumeIDs list. * Used to figure out if we can perform full CG operations or just partial CG operations. * * @param dbClient db client * @param protectionSet protection set * @param volumeIDs volume IDs * @return true if volumeIDs contains all of the source volumes in the protection set */ public static boolean containsAllRPSourceVolumes(DbClient dbClient, ProtectionSet protectionSet, Collection<URI> volumeIDs) { // find all source volumes. List<URI> sourceVolumeIDs = new ArrayList<URI>(); _log.info("Inspecting protection set: " + protectionSet.getLabel() + " to see if request contains all source volumes"); for (String volumeIDStr : protectionSet.getVolumes()) { Volume volume = dbClient.queryObject(Volume.class, URI.create(volumeIDStr)); if (volume != null) { _log.debug("Looking at volume: " + volume.getLabel()); if (!volume.getInactive() && volume.getPersonality().equals(Volume.PersonalityTypes.SOURCE.toString())) { _log.debug("Adding volume: " + volume.getLabel()); sourceVolumeIDs.add(volume.getId()); } } } // go through all volumes sent in, remove any volumes you find in the source list. sourceVolumeIDs.removeAll(volumeIDs); if (!sourceVolumeIDs.isEmpty()) { _log.info("Found that the volumes requested do not contain all source volumes in the protection set, namely: " + Joiner.on(',').join(sourceVolumeIDs)); return false; } _log.info("Found that all of the source volumes in the protection set are in the request."); return true; } /** * Determine if the consistency group's source volumes are represented in the volumeIDs list. * Used to figure out if we can perform full CG operations or just partial CG operations. * * @param dbClient db client * @param consistencyGroupUri the BlockConsistencyGroup ID * @param volumeIDs volume IDs * @return true if volumeIDs contains all of the source volumes in the protection set */ public static boolean cgSourceVolumesContainsAll(DbClient dbClient, URI consistencyGroupUri, Collection<URI> volumeIDs) { boolean cgSourceVolumesContainsAll = false; if (consistencyGroupUri != null) { // find all source volumes. List<URI> sourceVolumeIDs = new ArrayList<URI>(); BlockConsistencyGroup cg = dbClient.queryObject(BlockConsistencyGroup.class, consistencyGroupUri); _log.info("Inspecting consisency group: " + cg.getLabel() + " to see if request contains all source volumes"); List<Volume> sourceVolumes = getCgSourceVolumes(consistencyGroupUri, dbClient); if (sourceVolumes != null) { for (Volume srcVolume : sourceVolumes) { sourceVolumeIDs.add(srcVolume.getId()); } } // go through all volumes sent in, remove any volumes you find in the source list. sourceVolumeIDs.removeAll(volumeIDs); if (!sourceVolumeIDs.isEmpty()) { _log.info("Found that the volumes requested do not contain all source volumes in the consistency group, namely: " + Joiner.on(',').join(sourceVolumeIDs)); } else { _log.info("Found that all of the source volumes in the consistency group are in the request."); cgSourceVolumesContainsAll = true; } } return cgSourceVolumesContainsAll; } /** * Given an RP source volume and a protection virtual array, give me the corresponding target volume. * * @param id source volume id * @param virtualArray virtual array protected to * @return Volume of the target */ public static Volume getRPTargetVolumeFromSource(DbClient dbClient, Volume srcVolume, URI virtualArray) { if (srcVolume.getRpTargets() == null || srcVolume.getRpTargets().isEmpty()) { return null; } for (String targetId : srcVolume.getRpTargets()) { Volume target = dbClient.queryObject(Volume.class, URI.create(targetId)); if (target.getVirtualArray().equals(virtualArray)) { return target; } } return null; } /** * Get all the target volumes in the consistency group for specified target virtual array. * * @param dbClient the database client * @param consistencyGroup the consistency group id * @param virtualArray target virtual array * @return Volume of the target */ public static List<Volume> getTargetVolumesForVarray(DbClient dbClient, URI consistencyGroup, URI virtualArray) { List<Volume> targetVarrayVolumes = new ArrayList<Volume>(); List<Volume> cgTargetVolumes = getCgVolumes(dbClient, consistencyGroup, PersonalityTypes.TARGET.name()); if (cgTargetVolumes != null) { for (Volume target : cgTargetVolumes) { if (target.getVirtualArray().equals(virtualArray)) { targetVarrayVolumes.add(target); } } } return targetVarrayVolumes; } /** * Given a RP target volume, this method gets the corresponding source volume. * * @param dbClient the database client. * @param id target volume id. */ public static Volume getRPSourceVolumeFromTarget(DbClient dbClient, Volume tgtVolume) { Volume sourceVolume = null; if (tgtVolume == null) { return sourceVolume; } final List<Volume> sourceVolumes = CustomQueryUtility .queryActiveResourcesByConstraint(dbClient, Volume.class, getRpSourceVolumeByTarget(tgtVolume.getId().toString())); if (sourceVolumes != null && !sourceVolumes.isEmpty()) { // A RP target volume will only be associated to 1 source volume so return // the first entry. sourceVolume = sourceVolumes.get(0); } return sourceVolume; } /** * Gets the associated source volume given any type of RP volume. If a source volume * is given, that volume is returned. For a target journal volume, the associated target * volume is found and then its source volume is found and returned. * * @param dbClient the database client. * @param volume the volume for which we find the associated source volume. * @return the associated source volume. */ public static Volume getRPSourceVolume(DbClient dbClient, Volume volume) { Volume sourceVolume = null; if (volume == null) { return sourceVolume; } if (NullColumnValueGetter.isNotNullValue(volume.getPersonality())) { if (volume.getPersonality().equals(PersonalityTypes.SOURCE.name())) { _log.info("Attempting to find RP source volume corresponding to source volume " + volume.getId()); sourceVolume = volume; } else if (volume.getPersonality().equals(PersonalityTypes.TARGET.name())) { _log.info("Attempting to find RP source volume corresponding to target volume " + volume.getId()); sourceVolume = getRPSourceVolumeFromTarget(dbClient, volume); } else if (volume.getPersonality().equals(PersonalityTypes.METADATA.name())) { _log.info("Journal volume found, there is no associated RP source so just return null."); return sourceVolume; } else { _log.warn("Attempting to find RP source volume corresponding to an unknown RP volume type, for volume " + volume.getId()); } } if (sourceVolume == null) { _log.warn("Unable to find RP source volume corresponding to volume " + volume.getId()); } else { _log.info("Found RP source volume " + sourceVolume.getId() + ", corresponding to volume " + volume.getId()); } return sourceVolume; } /** * Convenience method that determines if the passed network is connected to the * passed varray. * * Check the assigned varrays list if it exist, if not check against the connect varrays. * * @param network Network to check * @param virtualArray varray to check against * @return true is the network is connected to the varray, false otherwise */ public static boolean isNetworkConnectedToVarray(NetworkLite network, VirtualArray virtualArray) { if (network != null && network.getConnectedVirtualArrays() != null && network.getConnectedVirtualArrays().contains(String.valueOf(virtualArray.getId()))) { return true; } return false; } /** * Check if initiator being added to export-group is good. * * @param exportGroup EG to check * @param initiator Initiator to validate * @param dbClient DbClient * @return true if the Initiator is valid, false otherwise * @throws InternalException */ public static boolean isInitiatorInVarray(VirtualArray varray, String wwn, DbClient dbClient) throws InternalException { // Get the networks assigned to the virtual array. List<Network> networks = CustomQueryUtility.queryActiveResourcesByRelation( dbClient, varray.getId(), Network.class, "connectedVirtualArrays"); for (Network network : networks) { if (network == null || network.getInactive() == true) { continue; } StringMap endpointMap = network.getEndpointsMap(); for (String endpointKey : endpointMap.keySet()) { String endpointValue = endpointMap.get(endpointKey); if (wwn.equals(endpointValue) || wwn.equals(endpointKey)) { return true; } } } return false; } /** * Check if any of the networks containing the RP site initiators contains storage * ports that are explicitly assigned or implicitly connected to the passed virtual * array. * * @param storageSystemURI The storage system who's connected networks we want to find. * @param protectionSystemURI The protection system used to find the site initiators. * @param siteId The side id for which we need to lookup associated initiators. * @param varrayURI The virtual array being used to check for network connectivity * @param dbClient DbClient ref * @return true if the networks containing the RP site initiators contains valid storage ports, false othwerwise * @throws InternalException */ public static boolean rpInitiatorsInStorageConnectedNework(URI storageSystemURI, URI protectionSystemURI, String siteId, URI varrayURI, DbClient dbClient) throws InternalException { // Determine what network the StorageSystem is part of and verify that the RP site initiators // are part of that network. // Then get the front end ports on the Storage array. Map<URI, List<StoragePort>> arrayTargetMap = ConnectivityUtil.getStoragePortsOfType(dbClient, storageSystemURI, StoragePort.PortType.frontend); Set<URI> arrayTargetNetworks = new HashSet<URI>(); arrayTargetNetworks.addAll(arrayTargetMap.keySet()); ProtectionSystem protectionSystem = dbClient.queryObject(ProtectionSystem.class, protectionSystemURI); StringSet siteInitiators = protectionSystem.getSiteInitiators().get(siteId); // Build a List of RP site initiator networks Set<URI> rpSiteInitiatorNetworks = new HashSet<URI>(); for (String wwn : siteInitiators) { NetworkLite rpSiteInitiatorNetwork = NetworkUtil.getEndpointNetworkLite(wwn, dbClient); if (rpSiteInitiatorNetwork != null) { rpSiteInitiatorNetworks.add(rpSiteInitiatorNetwork.getId()); } } // Eliminate any storage ports that are not explicitly assigned // or implicitly connected to the passed varray. Iterator<URI> arrayTargetNetworksIter = arrayTargetNetworks.iterator(); while (arrayTargetNetworksIter.hasNext()) { URI networkURI = arrayTargetNetworksIter.next(); Iterator<StoragePort> targetStoragePortsIter = arrayTargetMap.get(networkURI).iterator(); while (targetStoragePortsIter.hasNext()) { StoragePort targetStoragePort = targetStoragePortsIter.next(); StringSet taggedVArraysForPort = targetStoragePort.getTaggedVirtualArrays(); if ((taggedVArraysForPort == null) || (!taggedVArraysForPort.contains(varrayURI.toString()))) { targetStoragePortsIter.remove(); } } // Eliminate any storage array connected networks who's storage ports aren't // explicitly assigned or implicitly connected to the passed varray. if (arrayTargetMap.get(networkURI).isEmpty()) { arrayTargetMap.remove(networkURI); } } List<URI> initiators = new ArrayList<URI>(); Iterator<URI> rpSiteInitiatorsNetworksItr = rpSiteInitiatorNetworks.iterator(); while (rpSiteInitiatorsNetworksItr.hasNext()) { URI initiatorURI = rpSiteInitiatorsNetworksItr.next(); if (arrayTargetMap.keySet().contains(initiatorURI)) { initiators.add(initiatorURI); } } if (initiators.isEmpty()) { return false; } return true; } /** * Determines if the given storage system has any active RecoverPoint protected * volumes under management. * * @param id the storage system id * @param dbClient DbClient ref * @return true if the storage system has active RP volumes under management. false otherwise. */ public static boolean containsActiveRpVolumes(URI id, DbClient dbClient) { URIQueryResultList result = new URIQueryResultList(); dbClient.queryByConstraint(ContainmentConstraint.Factory.getStorageDeviceVolumeConstraint(id), result); Iterator<URI> volumeUriItr = result.iterator(); while (volumeUriItr.hasNext()) { Volume volume = dbClient.queryObject(Volume.class, volumeUriItr.next()); // Is this an active RP volume? if (volume != null && !volume.getInactive() && volume.getRpCopyName() != null && !volume.getRpCopyName().isEmpty()) { return true; } } return false; } /** * Helper method that determines what the potential provisioned capacity is of a VMAX volume. * The size returned may or may not be what the eventual provisioned capacity will turn out to be, but its pretty accurate estimate. * * @param requestedSize Size of the volume requested * @param volume volume * @param storageSystem storagesystem of the volume * @param dbClient DbClient ref * @return potential provisioned capacity */ public static Long computeVmaxVolumeProvisionedCapacity(long requestedSize, Volume volume, StorageSystem storageSystem, DbClient dbClient) { Long vmaxPotentialProvisionedCapacity = 0L; StoragePool expandVolumePool = dbClient.queryObject(StoragePool.class, volume.getPool()); long metaMemberSize = volume.getIsComposite() ? volume.getMetaMemberSize() : volume.getCapacity(); long metaCapacity = volume.getIsComposite() ? volume.getTotalMetaMemberCapacity() : volume.getCapacity(); MetaVolumeRecommendation metaRecommendation = MetaVolumeUtils.getExpandRecommendation(storageSystem, expandVolumePool, metaCapacity, requestedSize, metaMemberSize, volume.getThinlyProvisioned(), dbClient.queryObject(VirtualPool.class, volume.getVirtualPool()).getFastExpansion()); if (metaRecommendation.isCreateMetaVolumes()) { long metaMemberCount = volume.getIsComposite() ? metaRecommendation.getMetaMemberCount() + volume.getMetaMemberCount() : metaRecommendation.getMetaMemberCount() + 1; vmaxPotentialProvisionedCapacity = metaMemberCount * metaRecommendation.getMetaMemberSize(); } else { vmaxPotentialProvisionedCapacity = requestedSize; } return vmaxPotentialProvisionedCapacity; } /** * Get the FAPI RecoverPoint Client using the ProtectionSystem * * @param ps ProtectionSystem object * @return RecoverPointClient object * @throws RecoverPointException */ public static RecoverPointClient getRecoverPointClient(ProtectionSystem protectionSystem) throws RecoverPointException { RecoverPointClient recoverPointClient = null; if (protectionSystem.getUsername() != null && !protectionSystem.getUsername().isEmpty()) { try { List<URI> endpoints = new ArrayList<URI>(); // Main endpoint that was registered by the user endpoints.add(new URI(HTTPS, null, protectionSystem.getIpAddress(), protectionSystem.getPortNumber(), RP_ENDPOINT, WSDL, null)); // Add any other endpoints for cluster management ips we have for (String clusterManagementIp : protectionSystem.getClusterManagementIPs()) { endpoints.add(new URI(HTTPS, null, clusterManagementIp, protectionSystem.getPortNumber(), RP_ENDPOINT, WSDL, null)); } recoverPointClient = RecoverPointClientFactory.getClient(protectionSystem.getId(), endpoints, protectionSystem.getUsername(), protectionSystem.getPassword()); } catch (URISyntaxException ex) { throw DeviceControllerExceptions.recoverpoint.errorCreatingServerURL(protectionSystem.getIpAddress(), protectionSystem.getPortNumber(), ex); } } else { throw DeviceControllerExceptions.recoverpoint.noUsernamePasswordSpecified(protectionSystem .getIpAddress()); } return recoverPointClient; } /** * Determines if the given volume descriptor applies to an RP source volume. * * @param volumeDescriptor the volume descriptor. * @return true if the descriptor applies to an RP source volume, false otherwise. */ public static boolean isRPSource(VolumeDescriptor volumeDescriptor) { boolean isSource = false; if ((volumeDescriptor.getType().equals(VolumeDescriptor.Type.RP_SOURCE)) || (volumeDescriptor.getType().equals(VolumeDescriptor.Type.RP_EXISTING_SOURCE)) || (volumeDescriptor.getType().equals(VolumeDescriptor.Type.RP_EXISTING_PROTECTED_SOURCE)) || (volumeDescriptor.getType().equals(VolumeDescriptor.Type.RP_VPLEX_VIRT_SOURCE))) { isSource = true; } return isSource; } /** * Determines if the given volume descriptor applies to an RP target volume. * * @param volumeDescriptor the volume descriptor. * @return true if the descriptor applies to an RP target volume, false otherwise. */ public static boolean isRPTarget(VolumeDescriptor volumeDescriptor) { boolean isTarget = false; if ((volumeDescriptor.getType().equals(VolumeDescriptor.Type.RP_TARGET)) || (volumeDescriptor.getType().equals(VolumeDescriptor.Type.RP_VPLEX_VIRT_TARGET))) { isTarget = true; } return isTarget; } /** * Determines if a volume is part of a MetroPoint configuration. * * @param dbClient DbClient reference * @param volume the volume. * @return true if this is a MetroPoint volume, false otherwise. */ public static boolean isMetroPointVolume(DbClient dbClient, Volume volume) { if (volume != null) { VirtualPool vpool = dbClient.queryObject(VirtualPool.class, volume.getVirtualPool()); if (vpool != null && VirtualPool.vPoolSpecifiesMetroPoint(vpool)) { _log.info(String.format("Volume's vpool [%s](%s) specifies Metropoint", vpool.getLabel(), vpool.getId())); return true; } } return false; } /** * Checks to see if the volume is a production journal. We check to see if the * volume's rp copy name lines up with any of the given production copies. * * @param productionCopies the production copies. * @param volume the volume. * @return true if the volume is a production journal, false otherwise. */ public static boolean isProductionJournal(Set<String> productionCopies, Volume volume) { for (String productionCopy : productionCopies) { if (productionCopy.equalsIgnoreCase(volume.getRpCopyName())) { return true; } } return false; } /** * Gets a list of RecoverPoint consistency group volumes. * * @param blockConsistencyGroupUri The CG to check * @param dbClient The dbClient instance * @return List of volumes in the CG */ public static List<Volume> getAllCgVolumes(URI blockConsistencyGroupUri, DbClient dbClient) { final List<Volume> cgVolumes = CustomQueryUtility .queryActiveResourcesByConstraint(dbClient, Volume.class, getVolumesByConsistencyGroup(blockConsistencyGroupUri)); return cgVolumes; } /** * Gets all the source volumes that belong in the specified RecoverPoint * consistency group. * * @param blockConsistencyGroupUri The CG to check * @param dbClient The dbClient instance * @return All Source volumes in the CG */ public static List<Volume> getCgSourceVolumes(URI blockConsistencyGroupUri, DbClient dbClient) { List<Volume> cgSourceVolumes = new ArrayList<Volume>(); List<Volume> cgVolumes = getAllCgVolumes(blockConsistencyGroupUri, dbClient); // Filter only source volumes if (cgVolumes != null) { for (Volume cgVolume : cgVolumes) { if (NullColumnValueGetter.isNotNullValue(cgVolume.getPersonality()) && PersonalityTypes.SOURCE.toString().equals(cgVolume.getPersonality())) { cgSourceVolumes.add(cgVolume); } } } return cgSourceVolumes; } /** * Filters the list of volumes by source or target site; site is defined by a varray * * @param varrayId varray to determine site * @param vpoolId vpool of the volumes * @param volumes volumes to filter * @return list of filtered volumes by source or target site */ public static List<Volume> getVolumesForSite(URI varrayId, URI vpoolId, Collection<Volume> volumes) { List<Volume> volumesForSite = new ArrayList<Volume>(); String personality = null; for (Volume volume : volumes) { if (varrayId != null) { if (vpoolId != null) { // for CDP volumes we need both varray and vpool to identify source or target if (volume.getVirtualArray().equals(varrayId) && volume.getVirtualPool().equals(vpoolId)) { volumesForSite.add(volume); personality = volume.getPersonality(); } } else if (volume.getVirtualArray().equals(varrayId)) { // check the first volume and include all source volumes volumesForSite.add(volume); personality = volume.getPersonality(); } } else if (NullColumnValueGetter.isNotNullValue(volume.getPersonality()) && volume.getPersonality().equals(Volume.PersonalityTypes.SOURCE.name())) { volumesForSite.add(volume); personality = volume.getPersonality(); } } // if the personality is source, include all source volumes including those not matching the passed in varray if (Volume.PersonalityTypes.SOURCE.toString().equals(personality)) { for (Volume volume : volumes) { if (Volume.PersonalityTypes.SOURCE.toString().equals(volume.getPersonality()) && !volume.getVirtualArray().equals(varrayId)) { volumesForSite.add(volume); } } } return volumesForSite; } /** * Gets all the volumes of the specified personality type in RecoverPoint consistency group. * * @param dbClient * The dbClient instance * @param blockConsistencyGroupUri * The CG to check * @param personality * The personality of the volumes to filter with * @return All Source volumes in the CG */ public static List<Volume> getCgVolumes(DbClient dbClient, URI blockConsistencyGroupUri, String personality) { List<Volume> cgPersonalityVolumes = new ArrayList<Volume>(); List<Volume> cgVolumes = getAllCgVolumes(blockConsistencyGroupUri, dbClient); // Filter volumes based on personality if (cgVolumes != null) { for (Volume cgVolume : cgVolumes) { if (cgVolume.getPersonality() != null && cgVolume.getPersonality().equals(personality)) { cgPersonalityVolumes.add(cgVolume); } } } return cgPersonalityVolumes; } /** * Queries the CG to find all the Journals for a specific RP Copy, returns the matching journals * sorted from largest to smallest. * * @param dbClient DbClient reference * @param cgURI URI of the CG to query * @param rpCopyName Either a valid RP copy name. * @return Existing matching journals for the copy or internal site sorted from largest to smallest */ public static List<Volume> findExistingJournalsForCopy(DbClient dbClient, URI cgURI, String rpCopyName) { // Return as a list for easy consumption List<Volume> matchingJournals = new ArrayList<Volume>(); // Ensure we have been passed valid arguments if (dbClient == null || cgURI == null || rpCopyName == null) { return matchingJournals; } // Get all journals for this CG List<Volume> cgJournalVolumes = getCgVolumes(dbClient, cgURI, Volume.PersonalityTypes.METADATA.name()); // Filter journals based on the RP copy name. if (cgJournalVolumes != null && !cgJournalVolumes.isEmpty()) { for (Volume cgJournalVolume : cgJournalVolumes) { boolean copyNamesMatch = (NullColumnValueGetter.isNotNullValue(cgJournalVolume.getRpCopyName()) && cgJournalVolume.getRpCopyName().equals(rpCopyName)); if (copyNamesMatch) { matchingJournals.add(cgJournalVolume); } } } else { _log.info(String.format("No journals found for RP CG [%s].", cgURI)); return matchingJournals; } if (!matchingJournals.isEmpty()) { // Sort the journals by capacity Collections.sort(matchingJournals, new Comparator<Volume>() { @Override public int compare(Volume v1, Volume v2) { Long v1Capacity = (v1.getProvisionedCapacity() > 0L ? v1.getProvisionedCapacity() : v1.getCapacity()); Long v2Capacity = (v2.getProvisionedCapacity() > 0L ? v2.getProvisionedCapacity() : v2.getCapacity()); return Long.compare(v1Capacity, v2Capacity); } }); } else { _log.info(String.format("No existing journals found for RP Copy [%s].", rpCopyName)); } return matchingJournals; } /** * Determines if an additional journal is required for this RP Copy. * * @param journalPolicy The current journal policy * @param cgURI The ViPR CG URI * @param size The size requested * @param volumeCount Number of volumes in the request * @param copyName The RP copy name * @param dbClient DbClient reference * * @return a map which will contain the journal count and size if an additional journal is required, null otherwise. */ public static Map<Integer, Long> additionalJournalRequiredForRPCopy(String journalPolicy, URI cgURI, long size, Integer volumeCount, String copyName, DbClient dbClient) { Map<Integer, Long> additionalJournalInfo = null; _log.info("Running check for additionalJournalRequiredForRPCopy()..."); StringBuffer logMsg = new StringBuffer(); logMsg.append(String.format("\nChecking if additional journal(s) required for RP Copy [%s] in RP CG [%s]\n", copyName, cgURI.toString())); logMsg.append("--------------------------------------\n"); if (journalPolicy != null && (journalPolicy.endsWith("x") || journalPolicy.endsWith("X"))) { // Find all the journals for this RP Copy, calculate their total size in bytes, // and while we're at it keep track of the minimum sized journal. // We use the minimum size as RecoverPoint prefers striping across all journals. List<Volume> journalVolumesForCopy = RPHelper.findExistingJournalsForCopy(dbClient, cgURI, copyName); Long minJournalSizeForCopy = 0L; Long totalJournalSizeForCopy = 0L; for (Volume journalVolume : journalVolumesForCopy) { // If the journal volume is VPLEX get its backing volume instead. if (RPHelper.isVPlexVolume(journalVolume, dbClient)) { journalVolume = VPlexUtil.getVPLEXBackendVolume(journalVolume, true, dbClient); } // Prefer provisioned capacity, but in some cases like concurrent orders // the volume may not have been provisioned just yet. Long journalSize = (journalVolume.getProvisionedCapacity() > 0L ? journalVolume.getProvisionedCapacity() : journalVolume.getCapacity()); // Keep track of the minimum sized journal to allow for striping across all journals. // However, note that best practice is to have uniform sized journals. Long journalSizeInBytes = SizeUtil.translateSize(String.valueOf(journalSize)); if ((minJournalSizeForCopy == 0L) || (journalSizeInBytes < minJournalSizeForCopy)) { minJournalSizeForCopy = journalSizeInBytes; } // Running total of journal capacity for this RP Copy totalJournalSizeForCopy += journalSize; } // Never use a minimum less than the default journal size if (minJournalSizeForCopy < DEFAULT_RP_JOURNAL_SIZE_IN_BYTES) { minJournalSizeForCopy = DEFAULT_RP_JOURNAL_SIZE_IN_BYTES; } Long totalJournalSizeInBytesForCopy = SizeUtil.translateSize(String.valueOf(totalJournalSizeForCopy)); // Find all the volumes for this RP Copy (excluding journals) and calculate their cumulative size in bytes List<Volume> cgVolumes = RPHelper.getAllCgVolumes(cgURI, dbClient); Long totalVolumeSizeForCopy = 0L; for (Volume cgVolume : cgVolumes) { if (!cgVolume.checkPersonality(Volume.PersonalityTypes.METADATA.name()) && copyName.equalsIgnoreCase(cgVolume.getRpCopyName()) && !cgVolume.checkInternalFlags(Flag.INTERNAL_OBJECT)) { // If the journal volume is VPLEX get its backing volume instead. if (RPHelper.isVPlexVolume(cgVolume, dbClient)) { cgVolume = VPlexUtil.getVPLEXBackendVolume(cgVolume, true, dbClient); } // Prefer provisioned capacity, but in some cases like concurrent orders // the volume may not have been provisioned just yet. Long volumeSize = (cgVolume.getProvisionedCapacity() > 0L ? cgVolume.getProvisionedCapacity() : cgVolume.getCapacity()); // Running total of volume capacity for this RP Copy totalVolumeSizeForCopy += volumeSize; } } Long totalVolumeSizeInBytesForCopy = SizeUtil.translateSize(String.valueOf(totalVolumeSizeForCopy)); // Calculate the projected cumulative size in bytes for this RP Copy after we provision the new volume(s) Long newTotalVolumeSizeInBytesForCopy = totalVolumeSizeInBytesForCopy + (size * volumeCount); // Find the multiplier set in the journal policy Float journalMultiplier = Float.valueOf(journalPolicy.substring(0, journalPolicy.length() - 1)).floatValue(); // Report on findings... logMsg.append(String.format("\tCurrent allocated journal capacity for RP Copy [%s]: [%s] GB", copyName, SizeUtil.translateSize(totalJournalSizeInBytesForCopy, SizeUtil.SIZE_GB))); logMsg.append(String.format("\n\tCurrent volume capacity for RP Copy [%s]: [%s] GB", copyName, SizeUtil.translateSize(totalVolumeSizeInBytesForCopy, SizeUtil.SIZE_GB))); logMsg.append(String.format("\n\tNew volume capacity for RP Copy [%s] after new volume provisioning: [%s] GB", copyName, SizeUtil.translateSize(newTotalVolumeSizeInBytesForCopy, SizeUtil.SIZE_GB))); logMsg.append(String.format("\n\tBased on VirtualPool's journal policy, journal capacity required is: [%s]", (SizeUtil.translateSize(newTotalVolumeSizeInBytesForCopy, SizeUtil.SIZE_GB) * journalMultiplier))); // Now check to see if any new journals need to be provisioned for this RP Copy if (totalJournalSizeInBytesForCopy < (newTotalVolumeSizeInBytesForCopy * journalMultiplier)) { // Yes, we need to provision new journals for this RP Copy. Let's give a concrete example: // // RP Virtual Pool has a Journal Policy of: Multiplier 0.25x // The existing RP CG has 4 volumes at 10 GB each with one Journal at 10 GB. // // Each RP Copy would have a total of 40 GB. Using the 0.25 multiplier, that means // we would need (40 * 0.25) 10 GB of Journal space for proper coverage. Which we have! // All good so far. // // Let's add 13 volumes at 6 GB each to the existing CG. // // We'll use the RP Source Copy for the example // // Current allocated journal capacity for RP Copy [Source]: 10.0 GB // Current volume capacity for RP Copy [Source]: 40.0 GB // New volume capacity for RP Copy [Source] after new volume provisioning: 118.0 GB (40existing + (13 * 6)new volumes) // Based on VirtualPool's journal policy, journal capacity required is: 29.5 (118 * 0.25 journal multiplier) // // Now that we know the capacity required, we can determine the the size and the number of new // journals needed to fulfill the request. // // Example to be continued below... // Calculate the new journal space required. This is based on the existing journal capacity, // the new capacity of the volumes being provisioned, and using the journal policy multiplier. double existingJournalCapacity = SizeUtil.translateSize(totalJournalSizeInBytesForCopy, SizeUtil.SIZE_GB); double newCopyCapacity = SizeUtil.translateSize(newTotalVolumeSizeInBytesForCopy, SizeUtil.SIZE_GB); double newJournalSpaceRequiredInGB = Math.abs(existingJournalCapacity - (newCopyCapacity * journalMultiplier)); // Calculate the number of new journals required for this RP Copy. // If we get a non-round number we need to round up to the nearest one. double minJournalSizeForCopyInGB = SizeUtil.translateSize(minJournalSizeForCopy, SizeUtil.SIZE_GB); int numberOfNewJournals = (int)Math.ceil(newJournalSpaceRequiredInGB / minJournalSizeForCopyInGB); // Example continued: // Based on the example, the new journal space required would be 19.5 GB = 10 - (118 * 0.25). // We then figure out how many journals are needed: 19.5 / 10 = 1.95 = 2 (rounded up) journals at 10 GB each. // // So the below would print: // New journal(s) required. 2 journal(s) of size 10.0 GB will be provisioned for RP Copy [Source]. logMsg.append(String.format("\n\t%s journal(s) of size %s GB are required and will " + "be provisioned for RP Copy [%s].", numberOfNewJournals, minJournalSizeForCopyInGB, copyName)); additionalJournalInfo = new HashMap<Integer, Long>(1); additionalJournalInfo.put(numberOfNewJournals, minJournalSizeForCopy); } } if (additionalJournalInfo == null) { logMsg.append(String.format("\n\tSufficient journal space for RP Copy [%s]. No extra journals need to be provisioned.", copyName)); } logMsg.append("\n--------------------------------------\n"); _log.info(logMsg.toString()); return additionalJournalInfo; } /** * Since there are several ways to express journal size policy, this helper method will take * the source size and apply the policy string to come up with a resulting size. * * @param sourceSizeStr size of the source volume * @param journalSizePolicy the policy of the journal size. ("10gb", "min", or "3.5x" formats) * @param resourceCount Number of resources in the request * * @return journal volume size result */ public static long getJournalSizeGivenPolicy(String sourceSizeStr, String journalSizePolicy, int resourceCount) { // first, normalize the size. user can specify as GB,MB, TB, etc Long sourceSizeInBytes = 0L; // Convert the source size into bytes, if specified in KB, MB, etc. if (sourceSizeStr.contains(SizeUtil.SIZE_TB) || sourceSizeStr.contains(SizeUtil.SIZE_GB) || sourceSizeStr.contains(SizeUtil.SIZE_MB) || sourceSizeStr.contains(SizeUtil.SIZE_B)) { sourceSizeInBytes = SizeUtil.translateSize(sourceSizeStr); } else { sourceSizeInBytes = Long.valueOf(sourceSizeStr); } Long totalSourceSizeInBytes = sourceSizeInBytes * resourceCount; // First check: If the journalSizePolicy is not specified or is null, then perform the default math. // Default journal size is 10GB if source volume size times 0.25 is less than 10GB, else its 0.25x(source size) if (journalSizePolicy == null || journalSizePolicy.equals(NullColumnValueGetter.getNullStr())) { if (DEFAULT_RP_JOURNAL_SIZE_IN_BYTES < (totalSourceSizeInBytes * RP_DEFAULT_JOURNAL_POLICY)) { return (long) ((totalSourceSizeInBytes * RP_DEFAULT_JOURNAL_POLICY)); } else { return DEFAULT_RP_JOURNAL_SIZE_IN_BYTES; } } // Second Check: if the journal policy specifies min, then return default journal size if (journalSizePolicy.equalsIgnoreCase("min")) { return DEFAULT_RP_JOURNAL_SIZE_IN_BYTES; } // Third check: If the policy is a multiplier, perform the math, respecting the minimum value if (journalSizePolicy.endsWith("x") || journalSizePolicy.endsWith("X")) { float multiplier = Float.valueOf(journalSizePolicy.substring(0, journalSizePolicy.length() - 1)).floatValue(); long journalSize = ((long) (totalSourceSizeInBytes.longValue() * multiplier) < DEFAULT_RP_JOURNAL_SIZE_IN_BYTES) ? DEFAULT_RP_JOURNAL_SIZE_IN_BYTES : (long) (totalSourceSizeInBytes.longValue() * multiplier); return journalSize; } // If the policy is an abbreviated value. // This is the only way to get a value less than minimally allowed. // Good in case the minimum changes or we're wrong about it per version. return SizeUtil.translateSize(journalSizePolicy); } /** * Determines if a Volume is being referenced as an associated volume by an RP+VPlex * volume of a specified personality type (SOURCE, TARGET, METADATA, etc.). * * @param volume the volume we are trying to find a parent RP+VPlex volume reference for. * @param dbClient the DB client. * @param types the personality types. * @return true if this volume is associated to an RP+VPlex journal, false otherwise. */ public static boolean isAssociatedToRpVplexType(Volume volume, DbClient dbClient, PersonalityTypes... types) { final List<Volume> vplexVirtualVolumes = CustomQueryUtility .queryActiveResourcesByConstraint(dbClient, Volume.class, getVolumesByAssociatedId(volume.getId().toString())); for (Volume vplexVirtualVolume : vplexVirtualVolumes) { if (NullColumnValueGetter.isNotNullValue(vplexVirtualVolume.getPersonality())) { // If the personality type matches any of the passed in personality // types, we can return true. for (PersonalityTypes type : types) { if (vplexVirtualVolume.checkPersonality(type)) { return true; } } } } return false; } /** * Determines if a Volume is being referenced as an associated volume by an RP+VPlex * volume of any personality type (SOURCE, TARGET, METADATA). * * @param volume the volume we are trying to find a parent RP+VPlex volume reference for. * @param dbClient the DB client. * @param types the personality types. * @return true if this volume is associated to an RP+VPlex journal, false otherwise. */ public static boolean isAssociatedToAnyRpVplexTypes(Volume volume, DbClient dbClient) { return isAssociatedToRpVplexType(volume, dbClient, PersonalityTypes.SOURCE, PersonalityTypes.TARGET, PersonalityTypes.METADATA); } /** * Returns the list of copies residing on the standby varray given the active production volume in a * Metropoint environment * * @param volume the active production volume * @param dbClient DbClient ref * @return returns the list of copies on the standby varray */ public static List<Volume> getMetropointStandbyCopies(Volume volume, DbClient dbClient) { List<Volume> standbyCopies = new ArrayList<Volume>(); if (volume.getProtectionSet() == null) { return standbyCopies; } ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, volume.getProtectionSet()); if (protectionSet.getVolumes() == null) { return standbyCopies; } // look for the standby varray in the volume's vpool VirtualPool vpool = dbClient.queryObject(VirtualPool.class, volume.getVirtualPool()); if (vpool == null) { return standbyCopies; } StringMap varrayVpoolMap = vpool.getHaVarrayVpoolMap(); if (varrayVpoolMap != null && !varrayVpoolMap.isEmpty()) { URI standbyVarrayId = URI.create(varrayVpoolMap.keySet().iterator().next()); // now loop through the replication set volumes and look for any copies from the standby varray for (String rsetVolId : protectionSet.getVolumes()) { Volume rsetVol = dbClient.queryObject(Volume.class, URI.create(rsetVolId)); if (rsetVol != null && !rsetVol.getInactive() && rsetVol.getRpTargets() != null) { for (String targetVolId : rsetVol.getRpTargets()) { Volume targetVol = dbClient.queryObject(Volume.class, URI.create(targetVolId)); if (targetVol.getVirtualArray().equals(standbyVarrayId)) { standbyCopies.add(targetVol); } } } } } return standbyCopies; } /** * Check to see if the target volume (based on varray) has already been provisioned * * @param volume Source volume to check * @param varrayToCheckURI URI of the varray we're looking for Targets * @param dbClient DBClient * @return The target volume found or null otherwise */ public static Volume findAlreadyProvisionedTargetVolume(Volume volume, URI varrayToCheckURI, DbClient dbClient) { Volume alreadyProvisionedTarget = null; if (volume.checkForRp() && volume.getRpTargets() != null && NullColumnValueGetter.isNotNullValue(volume.getPersonality()) && volume.getPersonality().equals(Volume.PersonalityTypes.SOURCE.name())) { // Loop through all the targets, check to see if any of the target volumes have // the same varray URI as the one passed in. for (String targetVolumeId : volume.getRpTargets()) { Volume targetVolume = dbClient.queryObject(Volume.class, URI.create(targetVolumeId)); if (targetVolume.getVirtualArray().equals(varrayToCheckURI)) { alreadyProvisionedTarget = targetVolume; break; } } } return alreadyProvisionedTarget; } /** * Helper method to retrieve all related volumes from a Source Volume * * @param sourceVolumeURI The source volume URI * @param dbClient DBClient * @param includeBackendVolumes Flag to optionally have backend volumes included (VPLEX) * @param includeJournalVolumes Flag to optionally have journal volumes included * @return All volumes related to the source volume */ public static Set<Volume> getAllRelatedVolumesForSource(URI sourceVolumeURI, DbClient dbClient, boolean includeBackendVolumes, boolean includeJournalVolumes) { Set<Volume> allRelatedVolumes = new HashSet<Volume>(); if (sourceVolumeURI != null) { Volume sourceVolume = dbClient.queryObject(Volume.class, sourceVolumeURI); if (sourceVolume != null && NullColumnValueGetter.isNotNullValue(sourceVolume.getPersonality()) && sourceVolume.getPersonality().equals(Volume.PersonalityTypes.SOURCE.name())) { allRelatedVolumes.add(sourceVolume); if (includeJournalVolumes) { List<Volume> sourceJournals = RPHelper.findExistingJournalsForCopy(dbClient, sourceVolume.getConsistencyGroup(), sourceVolume.getRpCopyName()); // Check for Stanbdy journals in the case of MetroPoint String standbyCopyName = getStandbyProductionCopyName(dbClient, sourceVolume); if (standbyCopyName != null) { sourceJournals.addAll(RPHelper.findExistingJournalsForCopy(dbClient, sourceVolume.getConsistencyGroup(), standbyCopyName)); } allRelatedVolumes.addAll(sourceJournals); } if (sourceVolume.getRpTargets() != null) { for (String targetVolumeId : sourceVolume.getRpTargets()) { Volume targetVolume = dbClient.queryObject(Volume.class, URI.create(targetVolumeId)); allRelatedVolumes.add(targetVolume); if (includeJournalVolumes) { List<Volume> targetJournals = RPHelper.findExistingJournalsForCopy(dbClient, targetVolume.getConsistencyGroup(), targetVolume.getRpCopyName()); allRelatedVolumes.addAll(targetJournals); } } } List<Volume> allBackendVolumes = new ArrayList<Volume>(); if (includeBackendVolumes) { for (Volume volume : allRelatedVolumes) { if (volume.getAssociatedVolumes() != null && !volume.getAssociatedVolumes().isEmpty()) { for (String associatedVolId : volume.getAssociatedVolumes()) { Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(associatedVolId)); allBackendVolumes.add(associatedVolume); } } } } allRelatedVolumes.addAll(allBackendVolumes); } } return allRelatedVolumes; } /** * MetroPoint Source volumes are represented as two copies (aka targets) in RecoverPoint. * * The VPLEX Source volume has its internal site set as do both the associated/backing volumes. * * The associated/backing volume that has the same internal site name as its VPLEX Virtual volume * is generally considered the "Active" copy and the other associated/backing volume's internal site name * would be considered the "Standby" copy. * * This method is a convenience to find the "Standby" copy name as it's currently not plainly * found on the VPLEX Virtual Volume. * * @param dbClient DbClient reference * @param sourceVolume The sourceVolume to check, we assume it's a MetroPoint volume * @return The standby internal site name, null otherwise. */ public static String getStandbyInternalSite(DbClient dbClient, Volume sourceVolume) { String standbyInternalSite = null; if (sourceVolume != null && Volume.PersonalityTypes.SOURCE.name().equals(sourceVolume.getPersonality())) { if (isMetroPointVolume(dbClient, sourceVolume) && (null != sourceVolume.getAssociatedVolumes() && (!sourceVolume.getAssociatedVolumes().isEmpty()))) { // Check the associated volumes to find the non-matching internal site and return that one. for (String associatedVolId : sourceVolume.getAssociatedVolumes()) { Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(associatedVolId)); if (associatedVolume != null && !associatedVolume.getInactive()) { if (NullColumnValueGetter.isNotNullValue(associatedVolume.getInternalSiteName()) && !associatedVolume.getInternalSiteName().equals(sourceVolume.getInternalSiteName())) { // If the internal site names are different, this is the standby internal site standbyInternalSite = associatedVolume.getInternalSiteName(); break; } } } } } return standbyInternalSite; } /** * MetroPoint Source volumes are represented as two copies (aka targets) in RecoverPoint. * * The VPLEX Source volume has its internal site set as do both the associated/backing volumes. * * The associated/backing volume that has the same internal site name as its VPLEX Virtual volume * is generally considered the "Active" copy and the other associated/backing volume's internal site name * would be considered the "Standby" copy. * * This method is a convenience to find the "Standby" production copy name as it's currently not plainly * found on the VPLEX Virtual Volume. * * @param dbClient DbClient reference * @param sourceVolume The sourceVolume to check, we assume it's a MetroPoint volume * @return The standby internal site name, null otherwise. */ public static String getStandbyProductionCopyName(DbClient dbClient, Volume sourceVolume) { String standbyProductionCopyName = null; if (sourceVolume != null && Volume.PersonalityTypes.SOURCE.name().equals(sourceVolume.getPersonality()) && sourceVolume.getAssociatedVolumes() != null && sourceVolume.getAssociatedVolumes().size() > 1) { // Check the associated volumes to find the non-matching internal site and return that one. for (String associatedVolId : sourceVolume.getAssociatedVolumes()) { Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(associatedVolId)); if (associatedVolume != null && !associatedVolume.getInactive()) { if (NullColumnValueGetter.isNotNullValue(associatedVolume.getInternalSiteName()) && !associatedVolume.getInternalSiteName().equals(sourceVolume.getInternalSiteName()) && NullColumnValueGetter.isNotNullValue(associatedVolume.getRpCopyName())) { // If the internal site names are different, this is the standby volume standbyProductionCopyName = associatedVolume.getRpCopyName(); break; } } } } return standbyProductionCopyName; } /** * Determines if a volume is a VPLEX volume. * * @param volume the volume. * @param dbClient the database client. * @return true if this is a VPLEX volume, false otherwise. */ public static boolean isVPlexVolume(Volume volume, DbClient dbClient) { return volume.isVPlexVolume(dbClient); } /** * Rollback protection specific fields on the existing volume. This is normally invoked if there are * errors during a change vpool operation. We want to return the volume back to its un-protected state * or in the case of upgrade to MP then to remove any MP features from the protected volume. * * One of the biggest motivations is to ensure that the old vpool is set back on the existing volume. * * @param volume Volume to remove protection from * @param oldVpool The old vpool, this the original vpool of the volume before trying to add protection * @param dbClient DBClient object */ public static void rollbackProtectionOnVolume(Volume volume, VirtualPool oldVpool, DbClient dbClient) { // Rollback any RP specific changes to this volume if (volume.checkForRp()) { if (!VirtualPool.vPoolSpecifiesProtection(oldVpool)) { _log.info(String.format("Start rollback of RP protection changes for volume [%s] (%s)...", volume.getLabel(), volume.getId())); // List of volume IDs to clean up from the ProtectionSet List<String> protectionSetVolumeIdsToRemove = new ArrayList<String>(); protectionSetVolumeIdsToRemove.add(volume.getId().toString()); // All source volumes in this CG List<Volume> cgSourceVolumes = getCgSourceVolumes(volume.getConsistencyGroup(), dbClient); // Only rollback the Journals if there is only one volume in the CG and it's the one we're // trying to roll back. boolean lastSourceVolumeInCG = (cgSourceVolumes != null && cgSourceVolumes.size() == 1 && cgSourceVolumes.get(0).getId().equals(volume.getId())); // Potentially rollback all journal volumes from the CG, // the main use case for this is during a brand new CG create // and the order needs to be rolled back so the entire CG would be // blown away. This will quickly clean up the journals so a new // order can be placed immediately. if (lastSourceVolumeInCG) { List<Volume> journals = getCgVolumes(dbClient, volume.getConsistencyGroup(), Volume.PersonalityTypes.METADATA.name()); for (Volume journal : journals) { _log.info(String.format("Rolling back RP Journal (%s)", journal.getLabel())); protectionSetVolumeIdsToRemove.add(journal.getId().toString()); rollbackVolume(journal.getId(), dbClient); } } // Null out any RP specific fields on the volume volume.setConsistencyGroup(NullColumnValueGetter.getNullURI()); volume.setPersonality(NullColumnValueGetter.getNullStr()); volume.setProtectionController(NullColumnValueGetter.getNullURI()); volume.setRSetName(NullColumnValueGetter.getNullStr()); volume.setInternalSiteName(NullColumnValueGetter.getNullStr()); volume.setRpCopyName(NullColumnValueGetter.getNullStr()); StringSet resetRpTargets = volume.getRpTargets(); if (resetRpTargets != null) { // Rollback any target volumes that were created for (String rpTargetId : resetRpTargets) { protectionSetVolumeIdsToRemove.add(rpTargetId); rollbackVolume(URI.create(rpTargetId), dbClient); } resetRpTargets.clear(); volume.setRpTargets(resetRpTargets); } // Clean up the Protection Set if (!NullColumnValueGetter.isNullNamedURI(volume.getProtectionSet())) { ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, volume.getProtectionSet()); if (protectionSet != null) { // Remove volume IDs from the Protection Set protectionSet.getVolumes().removeAll(protectionSetVolumeIdsToRemove); _log.info(String.format("Removing the following volumes from Protection Set [%s] (%s): %s", protectionSet.getLabel(), protectionSet.getId(), Joiner.on(',').join(protectionSetVolumeIdsToRemove))); // If the Protection Set is empty, we can safely set it to // inactive. if (lastSourceVolumeInCG) { _log.info(String.format("Setting Protection Set [%s] (%s) to inactive", protectionSet.getLabel(), protectionSet.getId())); protectionSet.setInactive(true); } dbClient.updateObject(protectionSet); } } volume.setProtectionSet(NullColumnValueGetter.getNullNamedURI()); } else { _log.info(String.format("Rollback changes for existing protected RP volume [%s]...", volume.getLabel())); // No specific rollback steps for existing protected volumes } // If this is a VPLEX volume, update the virtual pool references to the old vpool on // the backing volumes if they were set to the new vpool. if (RPHelper.isVPlexVolume(volume, dbClient)) { if (null == volume.getAssociatedVolumes()) { // this is a rollback situation, so we probably don't want to // throw another exception... _log.warn("VPLEX volume {} has no backend volumes.", volume.forDisplay()); } else { for (String associatedVolId : volume.getAssociatedVolumes()) { Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(associatedVolId)); if (associatedVolume != null && !associatedVolume.getInactive()) { if (!NullColumnValueGetter.isNullURI(associatedVolume.getVirtualPool()) && associatedVolume.getVirtualPool().equals(volume.getVirtualPool())) { associatedVolume.setVirtualPool(oldVpool.getId()); _log.info(String.format("Backing volume [%s] has had its virtual pool rolled back to [%s].", associatedVolume.getLabel(), oldVpool.getLabel())); } associatedVolume.setConsistencyGroup(NullColumnValueGetter.getNullURI()); dbClient.updateObject(associatedVolume); } // If the old vpool did not specify multi volume consistency, // remove the CG reference of the volume since we are rolling back // in the case of VPLEX and Array rollback those steps will be fired // before we get here anyway. if (!oldVpool.getMultivolumeConsistency()) { associatedVolume.setConsistencyGroup(NullColumnValueGetter.getNullURI()); } dbClient.updateObject(associatedVolume); } } } // Set the old vpool back on the volume _log.info(String.format("Resetting vpool on volume [%s](%s) from (%s) back to its original vpool (%s)", volume.getLabel(), volume.getId(), volume.getVirtualPool(), oldVpool.getId())); volume.setVirtualPool(oldVpool.getId()); dbClient.updateObject(volume); _log.info(String.format("Rollback of RP protection changes for volume [%s] (%s) has completed.", volume.getLabel(), volume.getId())); } } /** * Cassandra level rollback of a volume. We set the volume to inactive and rename * the volume to indicate that rollback has occured. We do this so as to not * prevent subsequent use of the same volume name in the case of rollback/error. * * @param volumeURI URI of the volume to rollback * @param dbClient DBClient Object * @return The rolled back volume */ public static Volume rollbackVolume(URI volumeURI, DbClient dbClient) { Volume volume = dbClient.queryObject(Volume.class, volumeURI); if (volume != null && !volume.getInactive()) { _log.info(String.format("Rollback volume [%s]...", volume.getLabel())); if (volume.getProvisionedCapacity() == null || volume.getProvisionedCapacity() == 0) { // Only set the volume to inactive if it has never // been provisioned. Otherwise let regular rollback // steps take care of cleaning it up. dbClient.markForDeletion(volume); } else { // Normal rollback should clean up the volume, change the label // to allow re-orders. String rollbackLabel = "-ROLLBACK-" + Math.random(); volume.setLabel(volume.getLabel() + rollbackLabel); dbClient.updateObject(volume); } // Rollback any VPLEX backing volumes too if (RPHelper.isVPlexVolume(volume, dbClient) && (null != volume.getAssociatedVolumes())) { for (String associatedVolId : volume.getAssociatedVolumes()) { Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(associatedVolId)); if (associatedVolume != null && !associatedVolume.getInactive()) { _log.info(String.format("Rollback volume [%s]...", associatedVolume.getLabel())); if (associatedVolume.getProvisionedCapacity() == null || associatedVolume.getProvisionedCapacity() == 0) { // Only set the volume to inactive if it has never // been provisioned. Otherwise let regular rollback // steps take care of cleaning it up. dbClient.markForDeletion(associatedVolume); } else { // Normal rollback should clean up the volume, change the label // to allow re-orders. associatedVolume.setLabel(volume.getLabel() + "-ROLLBACK-" + Math.random()); dbClient.updateObject(associatedVolume); } } } } } return volume; } /** * Returns the list of journal volumes for one site. * * If this is a CDP volume, journal volumes from both the production and target copies are returned. * * @param varray varray to check * @param consistencyGroup RP CG * @param dbClient DbClient ref * @return Journal volumes for the RP site */ private static List<Volume> getJournalVolumesForSite(VirtualArray varray, BlockConsistencyGroup consistencyGroup, DbClient dbClient) { List<Volume> journalVols = new ArrayList<Volume>(); List<Volume> volsInCg = getAllCgVolumes(consistencyGroup.getId(), dbClient); if (volsInCg != null) { for (Volume volInCg : volsInCg) { if (Volume.PersonalityTypes.METADATA.toString().equals(volInCg.getPersonality()) && !NullColumnValueGetter.isNullURI(volInCg.getVirtualArray()) && volInCg.getVirtualArray().equals(varray.getId())) { journalVols.add(volInCg); } } } return journalVols; } /** * Returns a unique journal volume name by evaluating all journal volumes for the copy and increasing the count journal volume name is * in the form varrayName-cgname-journal-[count]. * * @param varray varray for the Journal * @param consistencyGroup RP CG * @return a journal name unique within the site */ public static String createJournalVolumeName(VirtualArray varray, BlockConsistencyGroup consistencyGroup, DbClient dbClient) { String journalPrefix = new StringBuilder(consistencyGroup.getLabel()).append(VOL_DELIMITER).append(varray.getLabel()) .append(VOL_DELIMITER) .append(JOURNAL).toString(); List<Volume> existingJournals = getJournalVolumesForSite(varray, consistencyGroup, dbClient); // filter out old style journal volumes // new style journal volumes are named with the virtual array as the first component // some journals may be ingested and not fit either style. Avoid those too. List<Volume> newStyleJournals = new ArrayList<Volume>(); for (Volume journalVol : existingJournals) { String volName = journalVol.getLabel(); if (volName != null && volName.length() >= journalPrefix.length() && volName.substring(0, journalPrefix.length()).equals(journalPrefix)) { newStyleJournals.add(journalVol); } } // For some platforms volume names with blank spaces are not allowed. // If the varray and/or CG name has spaces they may have been removed from // the volume label. If this is the case, we would not have added them // to the new style journal list. If the list is empty, try again with // the blank spaces removed from the journal name prefix. if (newStyleJournals.isEmpty()) { journalPrefix = journalPrefix.replaceAll("\\s+", ""); for (Volume journalVol : existingJournals) { String volName = journalVol.getLabel(); if (volName != null && volName.length() >= journalPrefix.length() && volName.substring(0, journalPrefix.length()).equals(journalPrefix)) { newStyleJournals.add(journalVol); } } } // calculate the largest index int largest = 0; for (Volume journalVol : newStyleJournals) { String journalVolName = journalVol.getLabel(); // For journal volumes that are VPLEX volumes, if custom naming was enabled, // then the journal volume may have an unexpected name. However, the backend // volume is not custom named and will have the expected label, but with a // well-known suffix. We simply remove the suffix and compare against the backend // volume name. If we use the VPLEX volume name and its name was customized, then // we could end up with a duplicate name error when we go to prepare the backend // volume for a new journal volume, such as when journal capacity is added to // an RP protected volume. See Jira COP-24930. if (journalVol.isVPlexVolume(dbClient)) { Volume journalBackendVol = VPlexUtil.getVPLEXBackendVolume(journalVol, true, dbClient); if (journalBackendVol != null) { journalVolName = journalBackendVol.getLabel(); journalVolName = journalVolName.substring(0, journalVolName.lastIndexOf("-0")); } } String[] parts = StringUtils.split(journalVolName, VOL_DELIMITER); try { int idx = Integer.parseInt(parts[parts.length - 1]); if (idx > largest) { largest = idx; } } catch (NumberFormatException e) { // this is not an error; just means the name is not in the standard format continue; } } String journalName = new StringBuilder(journalPrefix).append(VOL_DELIMITER).append(Integer.toString(largest + 1)).toString(); return journalName; } /** * Determine the wwn of the volume in the format RP is looking for. For xtremio * this is the 128 bit identifier. For other array types it is the deafault. * * @param volumeURI the URI of the volume the operation is being performed on * @param dbClient * @return the wwn of the volume which rp requires to perform the operation * in the case of xtremio this is the 128 bit identifier */ public static String getRPWWn(URI volumeURI, DbClient dbClient) { Volume volume = dbClient.queryObject(Volume.class, volumeURI); if (volume.getNativeGuid() != null && RecoverPointUtils.isXioVolume(volume.getNativeGuid())) { return RecoverPointUtils.getXioNativeGuid(volume.getNativeGuid()); } return volume.getWWN(); } /** * Determine if the volume being protected is provisioned on an Xtremio Storage array * * @param volume The volume being provisioned * @param dbClient DBClient object * @return boolean indicating if the volume being protected is provisioned on an Xtremio Storage array */ public static boolean protectXtremioVolume(Volume volume, DbClient dbClient) { StorageSystem storageSystem = dbClient.queryObject(StorageSystem.class, volume.getStorageController()); if (storageSystem.getSystemType() != null && storageSystem.getSystemType().equalsIgnoreCase(Type.xtremio.toString())) { return true; } return false; } /** * Returns a set of all RP ports as their related Initiator URIs. * * @param dbClient - database client instance * @return a Set of Initiator URIs */ public static Set<URI> getBackendPortInitiators(DbClient dbClient) { _log.info("Finding backend port initiators for all RP systems"); Set<URI> initiators = new HashSet<URI>(); List<URI> rpSystemUris = dbClient.queryByType(ProtectionSystem.class, true); List<ProtectionSystem> rpSystems = dbClient.queryObject(ProtectionSystem.class, rpSystemUris); for (ProtectionSystem rpSystem : rpSystems) { for (Entry<String, AbstractChangeTrackingSet<String>> rpSitePorts : rpSystem.getSiteInitiators().entrySet()) { for (String port : rpSitePorts.getValue()) { Initiator initiator = ExportUtils.getInitiator(port, dbClient); if (initiator != null) { // Review: OK to reduce to debug level _log.info("Adding initiator " + initiator.getId() + " with port: " + port); initiators.add(initiator.getId()); } } } } return initiators; } /** * Does this snapshot require any sort of protection intervention? If it's a local array-based * snapshot, probably not. If it's a protection-based snapshot or a remote array-based snapshot * that requires protection intervention to ensure consistency between the source and target, then * you should go to the protection controller * * @param volume source volume * @param snapshotType The snapshot technology type. * * @return true if this is a protection based snapshot, false otherwise. */ public static boolean isProtectionBasedSnapshot(Volume volume, String snapshotType, DbClient dbClient) { // if volume is part of CG, and is snapshot type is not RP, then always create native Array snaps String rgName = volume.getReplicationGroupInstance(); if (volume.isVPlexVolume(dbClient)) { Volume backendVol = VPlexUtil.getVPLEXBackendVolume(volume, true, dbClient); if (backendVol != null && !backendVol.getInactive()) { rgName = backendVol.getReplicationGroupInstance(); } } if (NullColumnValueGetter.isNotNullValue(rgName) && !snapshotType.equalsIgnoreCase(BlockSnapshot.TechnologyType.RP.toString())) { return false; } // This is a protection based snapshot request if: // The volume allows for bookmarking (it's under protection) and // - The param either asked for a bookmark, or // - The param didn't ask for a bookmark, but the volume is a remote volume if (volume.getProtectionController() != null && (snapshotType.equalsIgnoreCase(BlockSnapshot.TechnologyType.RP.toString()) || volume .getPersonality().equals(Volume.PersonalityTypes.TARGET.toString()))) { return true; } return false; } /** * Fetch the RP Protected target virtual pool uris. * * @param dbClient db client * @return set of vpools that are RP target virtual pools */ public static Set<URI> fetchRPTargetVirtualPools(DbClient dbClient) { Set<URI> rpProtectedTargetVPools = new HashSet<URI>(); try { List<URI> vpoolProtectionSettingsURIs = dbClient.queryByType(VpoolProtectionVarraySettings.class, true); Iterator<VpoolProtectionVarraySettings> vPoolProtectionSettingsItr = dbClient .queryIterativeObjects(VpoolProtectionVarraySettings.class, vpoolProtectionSettingsURIs, true); while (vPoolProtectionSettingsItr.hasNext()) { VpoolProtectionVarraySettings rSetting = vPoolProtectionSettingsItr.next(); if (null != rSetting && !NullColumnValueGetter.isNullURI(rSetting.getVirtualPool())) { rpProtectedTargetVPools.add(rSetting.getVirtualPool()); } } } catch (Exception ex) { _log.error("Exception occurred while fetching RP enabled virtualpools", ex); } return rpProtectedTargetVPools; } /** * Creates an export group with the proper settings for RP usage * * @param exportGroupGeneratedName the generated ExportGroup name to use * @param virtualArray virtual array * @param project project * @param numPaths number of paths * @param isJournalExport flag indicating if this is an ExportGroup intended only for journal volumes * @return an export group */ public static ExportGroup createRPExportGroup(String exportGroupGeneratedName, VirtualArray virtualArray, Project project, Integer numPaths, boolean isJournalExport) { ExportGroup exportGroup; exportGroup = new ExportGroup(); exportGroup.setId(URIUtil.createId(ExportGroup.class)); exportGroup.addInternalFlags(Flag.INTERNAL_OBJECT, Flag.SUPPORTS_FORCE, Flag.RECOVERPOINT); exportGroup.setProject(new NamedURI(project.getId(), project.getLabel())); exportGroup.setVirtualArray(virtualArray.getId()); exportGroup.setTenant(new NamedURI(project.getTenantOrg().getURI(), project.getTenantOrg().getName())); exportGroup.setGeneratedName(exportGroupGeneratedName); // When created by CoprHD natively, it's usually the CG name. exportGroup.setLabel(exportGroupGeneratedName); exportGroup.setVolumes(new StringMap()); exportGroup.setOpStatus(new OpStatusMap()); // TODO: May need to use a default size or compute based on the contents of the export mask. exportGroup.setNumPaths(numPaths); exportGroup.setType(ExportGroupType.Cluster.name()); exportGroup.setZoneAllInitiators(true); // If this is an exportGroup intended only for journal volumes, set the RECOVERPOINT_JOURNAL flag if (isJournalExport) { exportGroup.addInternalFlags(Flag.RECOVERPOINT_JOURNAL); } return exportGroup; } /** * Generates a RecoverPoint ExportGroup name based on the standard * ViPR RecoverPoint ExportGroup label pattern. * * @param protectionSystem the ProtectionSystem for the ExportGroup * @param storageSystem the StorageSystem for the ExportGroup * @param internalSiteName the RecoverPoint internal site name * @param virtualArray the VirtualArray for the ExportGroup * @param isJournalExport flag indicating if this is an ExportGroup intended only for journal volumes * @return a RecoverPoint ExportGroup name String */ public static String generateExportGroupName(ProtectionSystem protectionSystem, StorageSystem storageSystem, String internalSiteName, VirtualArray virtualArray, boolean isJournalExport) { // This name generation needs to match ingestion code found in RPDeviceController until // we come up with better export group matching criteria. String protectionSiteName = protectionSystem.getRpSiteNames().get(internalSiteName); String exportGroupGeneratedName = protectionSystem.getNativeGuid() + "_" + storageSystem.getLabel() + "_" + protectionSiteName + "_" + virtualArray.getLabel(); if (isJournalExport) { exportGroupGeneratedName = exportGroupGeneratedName + "_JOURNAL"; } // Remove all non alpha-numeric characters, excluding "_". exportGroupGeneratedName = exportGroupGeneratedName.replaceAll("[^A-Za-z0-9_]", ""); _log.info("ExportGroup generated name is " + exportGroupGeneratedName); return exportGroupGeneratedName; } /** * Get the name of the copy associated with the varray ID and personality of the incoming volume. * * @param dbClient db client * @param consistencyGroup cg * @param varrayId varray ID * @param productionCopy is this a production volume * @return String associated with the existing copy name */ public static String getCgCopyName(DbClient dbClient, BlockConsistencyGroup consistencyGroup, URI varrayId, boolean productionCopy) { List<Volume> cgVolumes = RPHelper.getAllCgVolumes(consistencyGroup.getId(), dbClient); if (cgVolumes == null) { return null; } for (Volume cgVolume : cgVolumes) { if (cgVolume.getPersonality() == null) { continue; } if (RPHelper.isMetroPointVolume(dbClient, cgVolume) && cgVolume.getPersonality().equalsIgnoreCase(PersonalityTypes.SOURCE.toString()) && productionCopy) { // If the volume is MetroPoint, check for varrayId in the associated volumes since their RP Copy names will be different. if (cgVolume.getAssociatedVolumes() != null) { for (String assocVolumeIdStr : cgVolume.getAssociatedVolumes()) { Volume associatedVolume = dbClient.queryObject(Volume.class, URI.create(assocVolumeIdStr)); if (URIUtil.identical(associatedVolume.getVirtualArray(), varrayId)) { return associatedVolume.getRpCopyName(); } } } } if (!URIUtil.identical(cgVolume.getVirtualArray(), varrayId)) { continue; } if (cgVolume.getPersonality().equalsIgnoreCase(PersonalityTypes.SOURCE.toString()) && productionCopy) { return cgVolume.getRpCopyName(); } if (cgVolume.getPersonality().equalsIgnoreCase(PersonalityTypes.TARGET.toString()) && !productionCopy) { return cgVolume.getRpCopyName(); } } return null; } /** * Validate the replication set for each volume to ensure the source volume size is not greater * than the target volume size. This validation is required for both restore and expand because * we delete and re-create the replication set. The re-create step will fail if the source volume is * larger in size than the target. This situation would likely only arise if a swap was performed. * * @param dbClient the database client * @param volumes the list of volumes to validate */ public static void validateRSetVolumeSizes(DbClient dbClient, List<Volume> volumes) { if (volumes != null) { for (Volume volume : volumes) { // We aren't sure if the volume is a source or target. We need to get a handle // on the source volume in order to proceed. Volume sourceVolume = getRPSourceVolume(dbClient, volume); // Validate the source volume size is not greater than the target volume size if (sourceVolume != null && sourceVolume.getRpTargets() != null) { for (String volumeID : sourceVolume.getRpTargets()) { try { Volume targetVolume = dbClient.queryObject(Volume.class, new URI(volumeID)); if (sourceVolume.getProvisionedCapacity() > targetVolume.getProvisionedCapacity()) { throw APIException.badRequests.invalidRPVolumeSizes(sourceVolume.getId()); } } catch (URISyntaxException e) { throw APIException.badRequests.invalidURI(volumeID, e); } } } } } } /** * For an RP configuration, get all RP bookmarks for the CGs provided * * @param system RP system * @param cgIDs IDs of the consistency groups to get the bookmarks * @return command result object, object list [0] has GetBookmarksResponse * @throws RecoverPointException */ private static BiosCommandResult getRPBookmarks(ProtectionSystem system, Set<Integer> cgIDs) throws RecoverPointException { _log.info("getRPBookmarks {} - start", system.getId()); RecoverPointClient rp = RPHelper.getRecoverPointClient(system); GetBookmarksResponse bookmarkResponse = rp.getRPBookmarks(cgIDs); _log.info("getRPBookmarks {} - complete", system.getId()); BiosCommandResult result = BiosCommandResult.createSuccessfulResult(); List<Object> returnList = new ArrayList<Object>(); returnList.add(bookmarkResponse); result.setObjectList(returnList); return result; } /** * Validate Block snapshots that correspond to RP bookmarks. Some may no longer exist in the RP system, and we * need to mark them as invalid. * * The strategy is as follows: * 1. Get all of the protection sets associated with the protection system * 2. Are there any Block Snapshots of type RP? (if not, don't bother cleaning up) * 3. Query the RP Appliance for all bookmarks for that CG (protection set) * 4. Find each block snapshot of type RP for each site * 5. If you can't find the bookmark in the RP list, move the block snapshot to inactive * * @param protectionSystem Protection System */ public static void cleanupSnapshots(DbClient dbClient, ProtectionSystem protectionSystem) throws RecoverPointException { // 1. Get all of the protection sets associated with the protection system Set<URI> protectionSetIDs = new HashSet<URI>(); Set<Integer> cgIDs = new HashSet<Integer>(); URIQueryResultList list = new URIQueryResultList(); Constraint constraint = ContainmentConstraint.Factory.getProtectionSystemProtectionSetConstraint(protectionSystem.getId()); dbClient.queryByConstraint(constraint, list); Iterator<URI> it = list.iterator(); while (it.hasNext()) { URI protectionSetId = it.next(); // Get all snapshots that are part of this protection set. URIQueryResultList plist = new URIQueryResultList(); Constraint pconstraint = ContainmentConstraint.Factory.getProtectionSetBlockSnapshotConstraint(protectionSetId); dbClient.queryByConstraint(pconstraint, plist); if (plist.iterator().hasNext()) { // OK, we know there are snapshots for this protection set/CG. // Retrieve all of the bookmarks associated with this protection set/CG later on by adding to the list now ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, protectionSetId); if (protectionSet != null && !protectionSet.getInactive()) { protectionSetIDs.add(protectionSet.getId()); cgIDs.add(Integer.valueOf(protectionSet.getProtectionId())); } } } // 2. No reason to bother the RPAs if there are no protection sets for this protection system. if (protectionSetIDs.isEmpty()) { _log.info("Block Snapshot of RP Bookmarks cleanup not run for this protection system. No Protections or RP Block Snapshots found on protection system: " + protectionSystem.getLabel()); return; } // 3. Query the RP appliance for all of the bookmarks for these CGs in one call BiosCommandResult result = getRPBookmarks(protectionSystem, cgIDs); GetBookmarksResponse bookmarkMap = (GetBookmarksResponse) result.getObjectList().get(0); // 4. Go through each protection set's snapshots and see if they're there. it = protectionSetIDs.iterator(); while (it.hasNext()) { URI protectionSetId = it.next(); ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, protectionSetId); // Now find this snapshot in the returned list of snapshots // The map should have an entry for that CG with an empty list if it looked and couldn't find any. (a successful empty set) if (protectionSet.getProtectionId() != null && bookmarkMap.getCgBookmarkMap() != null && bookmarkMap.getCgBookmarkMap().containsKey(new Integer(protectionSet.getProtectionId()))) { // If the list of RPBookmark objects corresponding to the CG is null, lets replace it with an empty // list to avoid issues further down. if (bookmarkMap.getCgBookmarkMap().get(new Integer(protectionSet.getProtectionId())) == null) { bookmarkMap.getCgBookmarkMap().put(new Integer(protectionSet.getProtectionId()), new ArrayList<RPBookmark>()); } // Get all snapshots that are part of this protection set. URIQueryResultList plist = new URIQueryResultList(); Constraint pconstraint = ContainmentConstraint.Factory.getProtectionSetBlockSnapshotConstraint(protectionSetId); dbClient.queryByConstraint(pconstraint, plist); Iterator<URI> snapshotIter = plist.iterator(); while (snapshotIter.hasNext()) { URI snapshotId = snapshotIter.next(); BlockSnapshot snapshot = dbClient.queryObject(BlockSnapshot.class, snapshotId); boolean deleteSnapshot = true; if (snapshot.getInactive()) { // Don't bother deleting or processing if the snapshot is already on its way out. deleteSnapshot = false; } else if (snapshot.getEmCGGroupCopyId() == null) { // If something bad happened and we weren't able to get the site information off of the snapshot _log.info("Found that ViPR Snapshot corresponding to RP Bookmark is missing Site information, thus not analyzing for automated deletion. " + snapshot.getId() + " - " + protectionSet.getLabel() + ":" + snapshot.getEmInternalSiteName() + ":" + snapshot.getEmName()); deleteSnapshot = false; } else if (!bookmarkMap.getCgBookmarkMap().get(Integer.valueOf(protectionSet.getProtectionId())).isEmpty()) { for (RPBookmark bookmark : bookmarkMap.getCgBookmarkMap().get(Integer.valueOf(protectionSet.getProtectionId()))) { // bookmark (from RP) vs. snapshot (from ViPR) if (snapshot.getEmName().equalsIgnoreCase(bookmark.getBookmarkName()) && snapshot.getEmCGGroupCopyId().equals(bookmark.getCGGroupCopyUID().getGlobalCopyUID().getCopyUID())) { deleteSnapshot = false; _log.info("Found that ViPR Snapshot corresponding to RP Bookmark still exists, thus saving in ViPR: " + snapshot.getId() + " - " + protectionSet.getLabel() + ":" + snapshot.getEmInternalSiteName() + ":" + snapshot.getEmCGGroupCopyId() + ":" + snapshot.getEmName()); } } } else { // Just for debugging, otherwise useless _log.debug("Found that ViPR Snapshot corresponding to RP Bookmark doesn't exist, thus going to delete from ViPR: " + snapshot.getId() + " - " + protectionSet.getLabel() + ":" + snapshot.getEmInternalSiteName() + ":" + snapshot.getEmCGGroupCopyId() + ":" + snapshot.getEmName()); } if (deleteSnapshot) { // 5. We couldn't find the bookmark, and the query for it was successful, so it's time to mark it as gone _log.info("Found that ViPR Snapshot corresponding to RP Bookmark no longer exists, thus deleting in ViPR: " + snapshot.getId() + " - " + protectionSet.getLabel() + ":" + snapshot.getEmInternalSiteName() + ":" + snapshot.getEmCGGroupCopyId() + ":" + snapshot.getEmName()); dbClient.markForDeletion(snapshot); } } } else if (protectionSet.getProtectionId() == null) { _log.error("Can not determine the consistency group ID of protection set: " + protectionSet.getLabel() + ", can not perform any cleanup of snapshots."); } else { _log.info("No consistency groups were found associated with protection system: " + protectionSystem.getLabel() + ", can not perform cleanup of snapshots."); } } } /** * Determines if the provided copy state is valid for creating RP bookmarks. * * @param copyState the copy state * @return true if the copy state if valid for creating bookmarks, false otherwise */ public static boolean isValidBookmarkState(String copyState) { // The only invalid copy bookmark states we care about are null and DIRECT_ACCESS. if (copyState == null || copyState.equalsIgnoreCase(Copy.ImageAccessMode.DIRECT_ACCESS.name())) { return false; } return true; } /** * Validate the CG before performing destructive operations. * If additional volumes appear in the RP CG on the hardware, this method returns false * Clerical errors (such as missing DB entries) result in an Exception * * @param dbClient * dbclient * @param system * protection system * @param cgId * BlockConsistencyGroup ID * @param volumes * list of volumes * @return true if CG is what we expect on the hardware, false otherwise */ public static boolean validateCGForDelete(DbClient dbClient, ProtectionSystem system, URI cgId, Set<URI> volumes) { _log.info("validateCGForDelete {} - start", system.getId()); // Retrieve all of the RP CGs, their RSets, and their volumes RecoverPointClient rp = RPHelper.getRecoverPointClient(system); Set<GetCGsResponse> cgList = rp.getAllCGs(); if (cgList == null || cgList.isEmpty()) { String errMsg = "Could not retrieve CGs from the RPA to perform validation."; throw DeviceControllerExceptions.recoverpoint.unableToPerformValidation(errMsg); } // Grab all of the source volumes from the CG according to ViPR List<Volume> srcVolumes = RPHelper.getCgVolumes(dbClient, cgId, PersonalityTypes.SOURCE.toString()); if (srcVolumes == null || srcVolumes.isEmpty()) { String errMsg = "Could not retrieve volumes from the database for CG to perform validation"; throw DeviceControllerExceptions.recoverpoint.unableToPerformValidation(errMsg); } // Get the protection set ID from the first source volume. All volumes will have the same pset ID. URI psetId = srcVolumes.get(0).getProtectionSet().getURI(); if (NullColumnValueGetter.isNullURI(psetId)) { String errMsg = "Could not retrieve protection set ID from the database for CG to perform validation"; throw DeviceControllerExceptions.recoverpoint.unableToPerformValidation(errMsg); } // Get the protection set, which is required to get the CG ID on the RPA ProtectionSet pset = dbClient.queryObject(ProtectionSet.class, psetId); if (pset == null) { String errMsg = "Could not retrieve protection set from the database for CG to perform validation"; throw DeviceControllerExceptions.recoverpoint.unableToPerformValidation(errMsg); } // Pre-populate the wwn fields for comparisons later. List<String> srcVolumeWwns = new ArrayList<>(); for (Volume srcVolume : srcVolumes) { srcVolumeWwns.add(srcVolume.getWWN()); } // This loop finds the CG on the hardware from the list of all CGs. Ignores all CGs that don't match our ID. for (GetCGsResponse cgResponse : cgList) { // Compare the stored CG ID (unique per RP System, doesn't change even if CG name changes) if (Long.parseLong(pset.getProtectionId()) != cgResponse.getCgId()) { continue; } // Make sure we have rsets before we continue. If the CG has no RSets on the hardware, throw if (cgResponse.getRsets() == null || cgResponse.getRsets().isEmpty()) { String errMsg = "Could not retrieve replication sets from the hardware to perform validation"; throw DeviceControllerExceptions.recoverpoint.unableToPerformValidation(errMsg); } // Find one of our volumes for (GetRSetResponse rsetResponse : cgResponse.getRsets()) { // Make sure we have volumes in the RSet before we continue if (rsetResponse == null || rsetResponse.getVolumes() == null || rsetResponse.getVolumes().isEmpty()) { String errMsg = "Could not retrieve the volumes in the replication set from the hardware to perform validation"; throw DeviceControllerExceptions.recoverpoint.unableToPerformValidation(errMsg); } // Check all of the volumes in the replication set. At least ONE volume needs to match one of our source // volumes. An RSet contains one source (or an active and stand-by source) and multiple targets. Our // list of WWNs is the list of source volumes we know about. for (GetVolumeResponse volumeResponse : rsetResponse.getVolumes()) { // This hardware volume should be represented in the list of srcVolumes if (!srcVolumeWwns.contains(volumeResponse.getWwn())) { _log.warn( "Found at least one volume that isn't in our list of source volumes {}, therefore we can not delete the entire CG.", volumeResponse.getWwn()); return false; } } } } _log.info("validateCGForDelete {} - end", system.getId()); return true; } /** * Updates the access state for a snapshot's target volume. If image access is being enabled, the * access state will be set to READWRITE. If image access is being disabled, the access state will * be set to READ_ONLY. * * @param snapshot the snapshot * @param volume the snapshot's parent volume * @param accessState the volume access state * @param dbClient the database client reference * @throws URISyntaxException */ private static void updateAccessState(BlockSnapshot snapshot, Volume volume, VolumeAccessState accessState, DbClient dbClient) throws URISyntaxException { // For RP+VPLEX volumes, we need to fetch the VPLEX volume. The snapshot object references the // block/back-end volume as its parent. Fetch the VPLEX volume that is created with this volume // as the back-end volume. Volume vol = volume; if (Volume.checkForVplexBackEndVolume(dbClient, volume)) { vol = Volume.fetchVplexVolume(dbClient, volume); } Volume targetVolume = null; // If the personality is SOURCE, then the enable image access request is part of export operation. if (vol.checkPersonality(Volume.PersonalityTypes.SOURCE.toString())) { // Now determine the target volume that corresponds to the site of the snapshot ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, vol.getProtectionSet()); targetVolume = ProtectionSet.getTargetVolumeFromSourceAndInternalSiteName(dbClient, protectionSet, vol, snapshot.getEmInternalSiteName()); } else if (vol.checkPersonality(Volume.PersonalityTypes.TARGET.toString())) { targetVolume = vol; } if (targetVolume != null) { if (accessState != null) { _log.info(String.format("Updating the access state to %s for target volume %s.", accessState.name(), targetVolume.getId())); targetVolume.setAccessState(accessState.name()); dbClient.updateObject(targetVolume); } else { _log.warn(String.format("Invalid access state. Could not update access state for target volume %s.", targetVolume.getId())); } } } /** * Update the snapshot's syncActive field and associated volume's access state post image access change. * * @param snapshot the snapshot * @param volume the snapshot's parent volume * @param accessState accessState the volume access state * @param setSnapshotSyncActive * @param dbClient the database client reference * @throws URISyntaxException */ public static void updateRPSnapshotPostImageAccessChange(BlockSnapshot snapshot, Volume volume, VolumeAccessState accessState, boolean setSnapshotSyncActive, DbClient dbClient) throws URISyntaxException { // If we are performing a disable image access as part of a snapshot create for an array snapshot + RP bookmark, // we want to set the syncActive field to true. This will enable us to perform snapshot exports and // remove snapshots from exports. snapshot.setIsSyncActive(setSnapshotSyncActive); _log.info(String.format("Updating the isSyncActive field to %s for BlockSnapshot %s.", setSnapshotSyncActive, snapshot.getId())); dbClient.updateObject(snapshot); // Update the access state of the snapshot's associated target volume updateAccessState(snapshot, volume, accessState, dbClient); } /** * Determines if the given snapshot references an RP bookmark. * * @param snapshot the snapshot to check * @return true if the given snapshot is an RP bookmark, false otherwise. */ public static boolean hasRpBookmark(BlockSnapshot snapshot) { return NullColumnValueGetter.isNotNullValue(snapshot.getEmName()); } /** * Checks to see if the journal multiplier policy is on the vpool passed in. * * @param vpool Vpool to check for journal multiplier policy * @return true is journal multiplier policy is on vpool, false otherwise. */ public static boolean vpoolHasJournalMultiplier(VirtualPool vpool) { if (vpool == null) { return false; } else { return (vpool.getJournalSize() != null && (vpool.getJournalSize().endsWith("x") || vpool.getJournalSize().endsWith("X"))); } } }