/* * Copyright (c) 2014 EMC Corporation * All Rights Reserved */ package com.emc.storageos.db.client.upgrade.callbacks; import static com.emc.storageos.db.client.constraint.AlternateIdConstraint.Factory.getVolumesByConsistencyGroup; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.db.client.DbClient; 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.BlockConsistencyGroup; import com.emc.storageos.db.client.model.BlockConsistencyGroup.Types; import com.emc.storageos.db.client.model.BlockMirror; import com.emc.storageos.db.client.model.BlockObject; import com.emc.storageos.db.client.model.BlockSnapshot; import com.emc.storageos.db.client.model.DiscoveredDataObject; import com.emc.storageos.db.client.model.NamedURI; import com.emc.storageos.db.client.model.ProtectionSet; import com.emc.storageos.db.client.model.StorageSystem; import com.emc.storageos.db.client.model.StringSet; import com.emc.storageos.db.client.model.Volume; import com.emc.storageos.db.client.model.util.BlockConsistencyGroupUtils; import com.emc.storageos.db.client.upgrade.BaseCustomMigrationCallback; import com.emc.storageos.db.client.util.CustomQueryUtility; import com.emc.storageos.db.client.util.NullColumnValueGetter; import com.emc.storageos.svcs.errorhandling.resources.MigrationCallbackException; /** * Migration handler to migrate BlockObject consistencyGroup to the new * consistencyGroups list field. * */ public class BlockObjectMultipleConsistencyGroupsMigration extends BaseCustomMigrationCallback { private static final Logger log = LoggerFactory.getLogger(BlockObjectMultipleConsistencyGroupsMigration.class); @Override public void process() throws MigrationCallbackException { migrateRpConsistencyGroups(); migrateBlockConsistencyGroups(); migrateBlockVolumes(); migrateBlockMirrors(); migrateBlockSnapshots(); } /** * Migrates the RP/RP+VPlex BlockConsistencyGroups and BlockObjects. */ private void migrateRpConsistencyGroups() { log.info("Migrating RP+VPlex BlockConsistencyGroup objects and Volume references."); List<URI> protectionSetURIs = dbClient.queryByType(ProtectionSet.class, true); log.info("Scanning ProtectionSets for stale Volume references."); for (URI protectionSetURI : protectionSetURIs) { // First, cleanup any stale volume references for the ProtectionSet if (cleanupStaleProtectionSetVolumes(protectionSetURI)) { log.info("ProtectionSet {} has been removed so continuing to the next one.", protectionSetURI); } } consolidateDuplicates(); protectionSetURIs = dbClient.queryByType(ProtectionSet.class, true); log.info("Scanning ProtectionSets for RP+VPlex Volume references."); for (URI protectionSetURI : protectionSetURIs) { // Lookup the ProtectionSet again after stale volume references have been removed. ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, protectionSetURI); log.info("Scanning ProtectionSet {}|{} ", protectionSet.getLabel(), protectionSet.getId()); if (protectionSet != null && protectionSet.getVolumes() != null) { // Get the first volume to determine if it has 2 BlockConsistencyGroup references. // This would identify that the volume, and all volumes in this ProtectionSet // are RP+VPlex volumes. if (protectionSet.getVolumes() != null && !protectionSet.getVolumes().isEmpty()) { Iterator<String> protectionSetVolumes = protectionSet.getVolumes().iterator(); Volume protectionSetVolume = null; while (protectionSetVolumes.hasNext()) { Volume vol = dbClient.queryObject(Volume.class, URI.create(protectionSetVolumes.next())); if (vol != null) { log.info("Using ProtectionSet Volume {}|{} to migrate consistency group.", vol.getLabel(), vol.getId()); protectionSetVolume = vol; break; } } BlockConsistencyGroup primaryCg = null; // RP+VPlex volumes will have references to exactly 2 BlockConsistencyGroups. Every // other type of volume will only reference a single BlockConsistencyGroup. if (protectionSetVolume.getConsistencyGroups() != null && !protectionSetVolume.getConsistencyGroups().isEmpty() && protectionSetVolume.getConsistencyGroups().size() == 2) { log.info("Found RP+VPlex ProtectionSet {}|{}. Preparing to migrated referenced RP+VPlex " + "volumes and associated BlockConsistencyGroups.", protectionSet.getLabel(), protectionSet.getId()); // There are references to 2 different BlockConsistencyGroup objects, // so this is an RP+VPlex volume. Iterator<String> cgUriItr = protectionSetVolume.getConsistencyGroups().iterator(); log.info("Attempting to locate the RP BlockConsistencyGroup for Volume {}|{}", protectionSetVolume.getLabel(), protectionSetVolume.getId()); while (cgUriItr.hasNext()) { BlockConsistencyGroup cg = dbClient.queryObject(BlockConsistencyGroup.class, URI.create(cgUriItr.next())); log.info("Found BlockConsistencyGroup {} of type {}", cg.getLabel(), cg.getType()); // The RP BlockConsistencyGroup will be the primary BlockConsistencyGroup so we must // first find that. All VPlex BlockConsistencyGroups for Volumes in the protection set // will be mapped to this primary BlockConsistencyGroup. if (cg.getTypes() != null && cg.getType().equals(Types.RP.name())) { log.info("Primary RP BlockConsistencyGroup {} found for ProtectionSet {}.", cg.getLabel(), protectionSet.getLabel()); primaryCg = cg; // Add the RP type primaryCg.addConsistencyGroupTypes(Types.RP.name()); break; } } if (primaryCg == null) { log.warn( "Unable to migration volumes/consistency groups associated with ProtectionSet {}. Could not find an RP associated BlockConsistencyGroup for Volume {}. ", protectionSet.getId(), protectionSetVolume.getId()); continue; } // Migrate the protection system/cg entry that replaces the use of the deviceName field. primaryCg.addSystemConsistencyGroup(protectionSet.getProtectionSystem().toString(), "ViPR-" + primaryCg.getLabel()); Iterator<String> volumeUriItr = protectionSet.getVolumes().iterator(); while (volumeUriItr.hasNext()) { String volumeUri = volumeUriItr.next(); Volume volume = dbClient.queryObject(Volume.class, URI.create(volumeUri)); if (volume != null) { log.info("Scanning volume {} for protection set {}.", volume.getLabel(), protectionSet.getLabel()); // Get the volume's VPlex CG, copy the info in the primary CG, and remove // the VPlex CG from Cassandra. BlockConsistencyGroup vplexCg = null; if (volume.getConsistencyGroups() != null && !volume.getConsistencyGroups().isEmpty()) { cgUriItr = volume.getConsistencyGroups().iterator(); while (cgUriItr.hasNext()) { BlockConsistencyGroup cg = dbClient.queryObject(BlockConsistencyGroup.class, URI.create(cgUriItr.next())); if (cg.getType() != null && cg.getType().equals(Types.VPLEX.name())) { vplexCg = cg; log.info("Volume {} belongs to VPLEX BlockConsistencyGroup {}.", volume.getLabel(), cg.getLabel()); break; } } } if (vplexCg != null && volume.getAssociatedVolumes() != null) { // Copy the VPlex CG info over to the primary CG StorageSystem vplexStorageSystem = dbClient.queryObject(StorageSystem.class, vplexCg.getStorageController()); String clusterId = getVPlexClusterFromVolume(volume); primaryCg.addSystemConsistencyGroup(vplexStorageSystem.getId().toString(), BlockConsistencyGroupUtils.buildClusterCgName(clusterId, vplexCg.getLabel())); if (NullColumnValueGetter.isNullURI(primaryCg.getStorageController())) { primaryCg.setStorageController(vplexStorageSystem.getId()); } if (!primaryCg.getTypes().contains(Types.VPLEX.name())) { // Add the VPlex type primaryCg.addConsistencyGroupTypes(Types.VPLEX.name()); } primaryCg.setType(NullColumnValueGetter.getNullStr()); primaryCg.setDeviceName(NullColumnValueGetter.getNullStr()); // Persist the changes to the primary CG, update the volume reference to the single // primary CG, and remove the VPlex CG. dbClient.persistObject(primaryCg); volume.setConsistencyGroup(primaryCg.getId()); StringSet cgs = volume.getConsistencyGroups(); cgs.remove(vplexCg.getId().toString()); volume.setConsistencyGroups(cgs); dbClient.persistObject(volume); log.info("Volume {} fields have been migrated.", volume.getLabel()); dbClient.markForDeletion(vplexCg); log.info( "VPlex BlockConsistencyGroup {} has been migrated over to RP BlockConsistencyGroup and has been deleted.", vplexCg.getLabel(), primaryCg.getLabel()); } } } } else if (protectionSetVolume != null && protectionSetVolume.getConsistencyGroups() != null && !protectionSetVolume.getConsistencyGroups().isEmpty() && protectionSetVolume.getConsistencyGroups().size() == 1) { Iterator<String> cgUriItr = protectionSetVolume.getConsistencyGroups().iterator(); BlockConsistencyGroup cg = dbClient.queryObject(BlockConsistencyGroup.class, URI.create(cgUriItr.next())); // Migration logic for RP only BlockConsistencyGroup cg.addConsistencyGroupTypes(cg.getType()); // In ViPR 2.0, the RP deviceName was not being prefixed with 'ViPR-', which is what is used on // the actual RP appliance for the CG name. cg.addSystemConsistencyGroup(protectionSet.getProtectionSystem().toString(), "ViPR-" + cg.getLabel()); // Remove type and deviceName fields cg.setType(NullColumnValueGetter.getNullStr()); cg.setDeviceName(NullColumnValueGetter.getNullStr()); dbClient.persistObject(cg); } } } } } /** * Migrates all BlockConsistencyGroups. Migrates the type and deviceName fields * too types and deviceNames fields. For VPlex only BlockConsistencyGroup objects, * a mapping of VPlex storage system to VPlex cluster/cg name is added. */ private void migrateBlockConsistencyGroups() { log.info("Migrating BlockConsistencyGroup objects."); List<URI> cgURIs = dbClient.queryByType(BlockConsistencyGroup.class, true); for (URI cgURI : cgURIs) { BlockConsistencyGroup cg = dbClient.queryObject(BlockConsistencyGroup.class, cgURI); // Determine if the types field has not been set. No types specified indicates // this BlockConsistencyGroup has not been migrated. We've already migrated the // RP+VPlex BlockConsistencyGroups at this point. if (cg.getTypes() == null || cg.getTypes().isEmpty()) { log.info("Migrating BlockConsistencyGroup {}.", cg.getLabel()); if (cg.getType() != null) { cg.addConsistencyGroupTypes(cg.getType()); } // If this is a VPlex BlockConsistencyGroup, we have a bit more work to do. // We need to keep a map of the volume's storage controller to VPlex cluster/cg. if (isVPlexCG(cg)) { log.info("Migrating fields for VPlex BlockConsistencyGroup {}.", cg.getLabel()); // Set the type, as VPLEX CGS prior to 2.2 do not have a type set. cg.addConsistencyGroupTypes(Types.VPLEX.name()); // Get the VPlex volumes associated with this CG. final List<Volume> activeCGVolumes = CustomQueryUtility .queryActiveResourcesByConstraint(dbClient, Volume.class, getVolumesByConsistencyGroup(cg.getId().toString())); log.info("Found " + activeCGVolumes.size() + " volumes that belong to BlockConsistencyGroup " + cg.getLabel()); for (Volume cgVolume : activeCGVolumes) { if (!NullColumnValueGetter.isNullURI(cgVolume.getStorageController())) { // Look at each of the associated volumes and add a mapping of VPlex storage // system to cluster/cg. String clusterId = getVPlexClusterFromVolume(cgVolume); log.info("Adding storage system to cluster/cg mapping for VPlex BlockConsistencyGroup " + cg.getLabel()); cg.addSystemConsistencyGroup(cgVolume.getStorageController().toString(), BlockConsistencyGroupUtils.buildClusterCgName(clusterId, cg.getLabel())); } } } else if (!NullColumnValueGetter.isNullURI(cg.getStorageController())) { // Non-RP/Non-VPLEX/Non-RP+VPLEX // Add an entry for the storage system -> consistency group name cg.addSystemConsistencyGroup(cg.getStorageController().toString(), cg.getDeviceName()); } // Remove type and deviceName fields cg.setType(NullColumnValueGetter.getNullStr()); cg.setDeviceName(NullColumnValueGetter.getNullStr()); dbClient.updateObject(cg); log.info("Migration of BlockConsistencyGroup {} complete.", cg.getLabel()); } } } /** * Update the Volume object to migrate the old consistencyGroups field * into the new consistencyGroup list field. */ private void migrateBlockVolumes() { log.info("Migrating BlockConsistencyGroup references on Volume objects."); DbClient dbClient = getDbClient(); List<URI> volumeURIs = dbClient.queryByType(Volume.class, false); Iterator<Volume> volumes = dbClient.queryIterativeObjects(Volume.class, volumeURIs, true); List<BlockObject> blockObjects = new ArrayList<BlockObject>(); while (volumes.hasNext()) { blockObjects.add(volumes.next()); } migrateBlockObjects(blockObjects); } /** * Update the BlockMirror object to migrate the old consistencyGroups field * into the new consistencyGroup list field. */ private void migrateBlockMirrors() { log.info("Migrating BlockConsistencyGroup references on BlockMirror objects."); DbClient dbClient = getDbClient(); List<URI> blockMirrorURIs = dbClient.queryByType(BlockMirror.class, false); Iterator<BlockMirror> blockMirrors = dbClient.queryIterativeObjects(BlockMirror.class, blockMirrorURIs, true); List<BlockObject> blockObjects = new ArrayList<BlockObject>(); while (blockMirrors.hasNext()) { blockObjects.add(blockMirrors.next()); } migrateBlockObjects(blockObjects); } /** * Update the BlockSnapshot object to migrate the old consistencyGroups field * into the new consistencyGroup list field. */ private void migrateBlockSnapshots() { log.info("Migrating BlockConsistencyGroup references on BlockSnapshot objects."); DbClient dbClient = getDbClient(); List<URI> blockSnapshotURIs = dbClient.queryByType(BlockSnapshot.class, false); Iterator<BlockSnapshot> blockSnapshots = dbClient.queryIterativeObjects(BlockSnapshot.class, blockSnapshotURIs, true); List<BlockObject> blockObjects = new ArrayList<BlockObject>(); while (blockSnapshots.hasNext()) { blockObjects.add(blockSnapshots.next()); } migrateBlockObjects(blockObjects); } /** * Gets the VPlex cluster Id for a given VPlex virtual volume. * * @param virtualVolume * @return */ private String getVPlexClusterFromVolume(Volume virtualVolume) { String clusterId = null; if (virtualVolume != null && virtualVolume.getNativeId() != null) { String[] nativeIdSplit = virtualVolume.getNativeId().split("/"); clusterId = nativeIdSplit[2]; } return clusterId; } /** * Performs the migration of BlockObjects. Ensures the BlockConsistencyGroup.consistencyGroups * field gets migrated to BlockConsistencyGroup.consistencyGroup. * * @param blockObjects */ private void migrateBlockObjects(List<BlockObject> blockObjects) { for (BlockObject blockObject : blockObjects) { String consistencyGroups = "No consistency groups to migrate"; // Only migrate BlockConsistencyGroup references if the BlockObject // references a single BlockConsistencyGroup. RP+VPlex BlockObjects // are the only ones that would reference 2 BlockConsistencyGroups and // those would have been handled earlier via migrateRpVplexConsistencyGroups() if (blockObject.getConsistencyGroups() != null && !blockObject.getConsistencyGroups().isEmpty() && blockObject.getConsistencyGroups().size() == 1) { consistencyGroups = blockObject.getConsistencyGroups().toString(); String cgUriStr = blockObject.getConsistencyGroups().iterator().next(); blockObject.setConsistencyGroup(URI.create(cgUriStr)); // Remove the old reference StringSet cgs = blockObject.getConsistencyGroups(); cgs.remove(cgUriStr); blockObject.setConsistencyGroups(cgs); dbClient.persistObject(blockObject); } log.info("Migrated BlockConsistencyGroups [{}] on BlockObject (label={}).", consistencyGroups, blockObject.getLabel()); } } /** * Determines if the passed CG is a VPLEX CG. * * @param cg A reference to the CG. * * @return true if the CG is a VPLEX CG, false otherwise. */ private boolean isVPlexCG(BlockConsistencyGroup cg) { boolean isVPlex = false; // If this is a VPlex BlockConsistencyGroup, we have a bit more work to do. // We need to keep a map of the volume's storage controller to VPlex cluster/cg. URI cgSystemURI = cg.getStorageController(); if (!NullColumnValueGetter.isNullURI(cgSystemURI)) { StorageSystem cgSystem = dbClient.queryObject(StorageSystem.class, cgSystemURI); if ((cgSystem != null) && (DiscoveredDataObject.Type.vplex.name().equals(cgSystem.getSystemType()))) { isVPlex = true; } } return isVPlex; } /** * searches for protection sets with duplicate names and consolidates into one protection set * * @return */ private void consolidateDuplicates() { Map<String, List<ProtectionSet>> labelURIListMap = new HashMap<String, List<ProtectionSet>>(); List<URI> protectionSetURIs = dbClient.queryByType(ProtectionSet.class, true); log.info("Scanning ProtectionSets for duplicate names."); for (URI protectionSetURI : protectionSetURIs) { ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, protectionSetURI); if (protectionSet == null || protectionSet.getInactive()) { log.info("Skipping null or inactive protection set {}", protectionSetURI); continue; } if (!labelURIListMap.containsKey(protectionSet.getLabel())) { labelURIListMap.put(protectionSet.getLabel(), new ArrayList<ProtectionSet>()); } labelURIListMap.get(protectionSet.getLabel()).add(protectionSet); } List<ProtectionSet> protectionSetsToDelete = new ArrayList<ProtectionSet>(); List<ProtectionSet> protectionSetsToPersist = new ArrayList<ProtectionSet>(); List<Volume> volumesToPersist = new ArrayList<Volume>(); List<BlockSnapshot> snapsToPersist = new ArrayList<BlockSnapshot>(); for (Entry<String, List<ProtectionSet>> entry : labelURIListMap.entrySet()) { if (entry.getValue().size() > 1) { log.info("Duplicate protection sets found {} | {}", entry.getKey(), entry.getValue().toArray()); ProtectionSet protectionSet = entry.getValue().iterator().next(); for (ProtectionSet duplicate : entry.getValue()) { if (duplicate.getId().equals(protectionSet.getId())) { continue; } log.info(String.format("duplicating %s protection set %s to %s)", protectionSet.getLabel(), duplicate.getId(), protectionSet.getId())); // add the volumes from the duplicate to the original protectionSet.getVolumes().addAll(duplicate.getVolumes()); // reset the protection set id on the volumes in the duplicate for (String volid : duplicate.getVolumes()) { Volume vol = dbClient.queryObject(Volume.class, URI.create(volid)); if (vol == null || vol.getInactive()) { log.info("Skipping null or inactive volume {}", volid); continue; } log.info(String.format("Changing protection set id on volume %s from %s to %s", vol.getId(), vol.getProtectionSet() .getURI(), protectionSet.getId())); vol.setProtectionSet(new NamedURI(protectionSet.getId(), protectionSet.getLabel())); volumesToPersist.add(vol); } // reset any block snapshot that points to the duplicate protection set URIQueryResultList blockSnapIds = new URIQueryResultList(); Constraint constraint = ContainmentConstraint.Factory.getProtectionSetBlockSnapshotConstraint(duplicate.getId()); dbClient.queryByConstraint(constraint, blockSnapIds); Iterator<URI> itr = blockSnapIds.iterator(); while (itr.hasNext()) { URI snapId = itr.next(); BlockSnapshot snap = dbClient.queryObject(BlockSnapshot.class, snapId); if (snap == null || snap.getInactive()) { log.info("Skipping null or inactive volume {}", snapId); continue; } log.info(String.format("Changing protection set id on snapshot %s from %s to %s", snap.getId(), snap.getProtectionSet(), protectionSet.getId())); snap.setProtectionSet(protectionSet.getId()); snapsToPersist.add(snap); } log.info("deleting duplicate protection set {}", duplicate.getId()); protectionSetsToDelete.add(duplicate); } protectionSetsToPersist.add(protectionSet); } } dbClient.persistObject(protectionSetsToPersist); dbClient.persistObject(volumesToPersist); dbClient.persistObject(snapsToPersist); dbClient.markForDeletion(protectionSetsToDelete); } /** * Cleans up stale ProtectionSet volume references. Meaning, volumes referenced * by the ProtectionSet that no longer exist in the DB will be removed. Also, * if the ProtectionSet ends up containing zero volumes after this operation, the * ProtectionSet itself will be removed from the DB. * * @param protectionSetUri the ProtectionSet to be cleaned-up. * @return boolean true if the ProtectionSet has been removed from the DB, false otherwise. */ private boolean cleanupStaleProtectionSetVolumes(URI protectionSetURI) { ProtectionSet protectionSet = dbClient.queryObject(ProtectionSet.class, protectionSetURI); boolean protectionSetRemoved = false; if (protectionSet != null) { StringSet protectionSetVolumes = protectionSet.getVolumes(); StringSet volumesToRemove = new StringSet(); if (protectionSetVolumes != null) { Iterator<String> volumesItr = protectionSetVolumes.iterator(); while (volumesItr.hasNext()) { String volumeUriStr = volumesItr.next(); Volume volume = dbClient.queryObject(Volume.class, URI.create(volumeUriStr)); if (volume == null) { volumesToRemove.add(volumeUriStr); // Volume is referenced by the ProtectionSet but it is not in the database. It should // be removed from the ProtectionSet. log.info("Removing stale Volume {} referenced by ProtectionSet {}.", volumeUriStr, protectionSet.getId()); } } if (protectionSetVolumes.size() == volumesToRemove.size()) { // There are no volume references for this ProtectionSet so remove it. log.info("ProtectionSet {} has no volume references so it is being removed.", protectionSet.getId()); dbClient.markForDeletion(protectionSet); protectionSetRemoved = true; } else { // Stale volume references have been removed. protectionSetVolumes.removeAll(volumesToRemove); dbClient.persistObject(protectionSet); } } } return protectionSetRemoved; } }