/*
* Copyright (c) 2015. EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.vplexcontroller;
import static com.emc.storageos.util.ExportUtils.getVPlexExportGroup;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.db.client.DbClient;
import com.emc.storageos.db.client.model.ExportGroup;
import com.emc.storageos.db.client.model.ExportMask;
import com.emc.storageos.db.client.model.Initiator;
import com.emc.storageos.db.client.model.StorageSystem;
import com.emc.storageos.db.client.model.Volume;
import com.emc.storageos.volumecontroller.impl.block.ExportMaskPlacementDescriptor;
/**
* This strategy assumes that the volumes have been divided amongst equally acceptable ExportMasks.
*/
public class VplexBackendVolumesToSeparateMasksStrategy implements VPlexBackendPlacementStrategy {
private static Logger log = LoggerFactory.getLogger(VplexBackendVolumesToSeparateMasksStrategy.class);
private final DbClient dbClient;
private final ExportMaskPlacementDescriptor placementDescriptor;
public VplexBackendVolumesToSeparateMasksStrategy(DbClient dbClient, ExportMaskPlacementDescriptor descriptor) {
this.dbClient = dbClient;
this.placementDescriptor = descriptor;
}
/**
* Placement descriptor should have a set of Masks that match. We will get the ExportGroup that should be
* associated with the ExportMask and map them. This will allow the VPlex backend workflow to create
* the AddVolume steps required for each ExportGroup+ExportMask combination.
*/
@Override
public void execute() {
// https://coprhd.atlassian.net/browse/COP-19956
// TL;DR: Examine the placement descriptor and see if it shows that the same volumes are being placed
// against different ExportMasks. If so, we want to place the volume into a single ExportMask instead.
//
// This code path runs after we have called into the backend array masking orchestration to suggest
// viable ExportMasks for volume placement. The ExportMasks should also have been validated and so we
// know that they are for a single VPlex cluster and are good to use. But it is possible that the
// ExportMasks that are suggested contain non-overlapping initiators, but point to the same VPlex
// cluster. In this situation, we are presented with a placement descriptor that says to place the
// volume in multiple ExportMasks. It would seem to make sense, looking at it purely from the
// initiators perspective; since none of suggested ExportMasks share initiators, they look like they
// point to different compute resources to which we would like to expose the volume(s). But, in the
// case of VPlex, we should understand that regardless of which ExportMask that's used, the volume
// will be made visible to a VPlex cluster. So, there's no need to place the volume into multiple
// ExportMasks, hence, we have the logic below to re-place the volume into only one of the ExportMasks.
Map<URI, Set<URI>> volumeToMasks = createVolumeToMasksMap();
Set<URI> multiplyPlacedVolumes = new HashSet<>(); // Filled in by anyVolumesPlacedToMultipleMasks()
if (anyVolumesPlacedToMultipleMasks(multiplyPlacedVolumes, volumeToMasks)) {
// Adjust placement to select the ExportMask that has the least number of total volumes.
for (URI volumeURI : multiplyPlacedVolumes) {
Set<URI> placedMasks = volumeToMasks.get(volumeURI);
Map<URI, ExportMask> exportMaskMap = createExportMaskMap(placedMasks);
// Re-place the volume into the ExportMask with the least volumes
placeVolumeToMaskWithLeastNumberOfVolumes(volumeURI, exportMaskMap);
}
}
// For each ExportMask URI to ExportMask entry, lookup the ExportGroup associated with it and map it
Map<URI, ExportMask> maskSetCopy = new HashMap<>(placementDescriptor.getMasks());
for (Map.Entry<URI, ExportMask> entry : maskSetCopy.entrySet()) {
URI exportMaskURI = entry.getKey();
ExportMask exportMask = entry.getValue();
// Get contextual information from the placement
URI tenant = placementDescriptor.getTenant();
URI project = placementDescriptor.getProject();
StorageSystem vplex = placementDescriptor.getVplex();
StorageSystem array = placementDescriptor.getBackendArray();
URI virtualArray = placementDescriptor.getVirtualArray();
Collection<Initiator> initiators = placementDescriptor.getInitiators();
// Determine ExportGroup
ExportGroup exportGroup = getVPlexExportGroup(dbClient, vplex, array, virtualArray, exportMask, initiators, tenant, project);
placementDescriptor.mapExportMaskToExportGroup(exportMaskURI, exportGroup);
}
}
/**
* Routine will adjust the placementDescriptor.placedVolumes, such that the volume will be placed only to one ExportMask (the one
* with least number of volumes).
*
* NB:
* This routine is to be run in the context of determining volumes that are placed against multiple ExportMasks. The ExportMasks
* would be pointing to the same cluster, so only a single ExportMask would be required. So, the placedMasks map should contain
* ExportMasks that point to the same VPlex cluster.
*
* @param volumeURI [IN] - Volume URI
* @param placedMasks [IN] - Mapping of ExportMask URI to ExportMask object
*/
private void placeVolumeToMaskWithLeastNumberOfVolumes(URI volumeURI, Map<URI, ExportMask> placedMasks) {
log.info("These exportMasks are pointing to the same cluster: {}", placedMasks.keySet());
// We're going to try the smallest number of volumes in the ExportMask, so start
// with the largest possible count size
int leastNumberOfVolumes = Integer.MAX_VALUE;
// As we go through the ExportMask URIs, save off those that don't have the least
// number of volumes
Set<URI> exportMaskWithMoreVolumes = new HashSet<>();
ExportMask currMaskWithLeastVolumes = null;
for (ExportMask mask : placedMasks.values()) {
// Try to determine the ExportMask with the least number of total volumes.
int totalVolumeCount = mask.returnTotalVolumeCount();
if (totalVolumeCount < leastNumberOfVolumes) {
if (currMaskWithLeastVolumes != null) {
exportMaskWithMoreVolumes.add(currMaskWithLeastVolumes.getId());
}
leastNumberOfVolumes = totalVolumeCount;
currMaskWithLeastVolumes = mask;
} else {
exportMaskWithMoreVolumes.add(mask.getId());
}
}
if (currMaskWithLeastVolumes != null) {
log.info(String.format("ExportMask %s was selected for volume %s, as it has %d total volumes",
currMaskWithLeastVolumes.getId(), volumeURI, currMaskWithLeastVolumes.returnTotalVolumeCount()));
}
log.info("Determined that this volume {} can be unplaced from these ExportMasks: {}", volumeURI, exportMaskWithMoreVolumes);
log.info("placeVolumeToMaskWithLeastNumberOfVolumes - PlacementDescriptor before:\n{}", placementDescriptor.toString());
// For any export masks that were found to have more (or the same) number of volumes as
// the ExportMask with the least, invalidate its placement in the descriptor
for (URI exportMaskURI : exportMaskWithMoreVolumes) {
placementDescriptor.unplaceVolumeFromMask(volumeURI, exportMaskURI);
}
log.info("placeVolumeToMaskWithLeastNumberOfVolumes - PlacementDescriptor after:\n{}", placementDescriptor.toString());
}
/**
* Given a set of ExportMask URIs, return a map of ExportMask URI to ExportMask object
*
* @param exportMaskMap [OUT] - Mapping of ExportMask URI to ExportMask object
* @param placedMasks [IN] - ExportMask URIs
* @return Map of ExportMask URI to ExportMask objects
*/
private Map<URI, ExportMask> createExportMaskMap(Set<URI> placedMasks) {
Map<URI, ExportMask> exportMaskMap = new HashMap<>();
Iterator<ExportMask> exportMaskIterator = dbClient.queryIterativeObjects(ExportMask.class, placedMasks, true);
// Iterator through the list of masks that indicate that have the
// same volumes placed against them.
while (exportMaskIterator.hasNext()) {
ExportMask exportMask = exportMaskIterator.next();
exportMaskMap.put(exportMask.getId(), exportMask);
}
return exportMaskMap;
}
/**
* Routine will examine the volumeToMasks map to see if there are any volumes that placed to multiple ExportMasks.
* If any, it returns true and the 'multiplePlaced' set will contain the volume URIs that are multiply placed.
*
* @param multiplyPlaced [OUT] - Set of Volume URIs that are multiple placed
* @param volumeToMasks [IN] - Mapping of volume URI to ExportMask URIs
* @return true, iff we found at least one volume that's placed to multiple ExportMasks.
*/
private boolean anyVolumesPlacedToMultipleMasks(Set<URI> multiplyPlaced, Map<URI, Set<URI>> volumeToMasks) {
boolean atLeastOneVolumePlacedToMultipleMasks = false;
for (URI volumeURI : volumeToMasks.keySet()) {
boolean placedToMultipleMasks = volumeToMasks.get(volumeURI).size() > 1;
if (placedToMultipleMasks) {
multiplyPlaced.add(volumeURI);
atLeastOneVolumePlacedToMultipleMasks = true;
}
}
return atLeastOneVolumePlacedToMultipleMasks;
}
/**
* Effectively, this routine reverses the placementDescriptor.placedVolumes map, which maps an ExportMask URI to Volumes.
* Here we will generate a map of Volume URI to set of ExportMask URIs.
*
* @return Map of Volume URI to set of ExportMask URIs.
*/
private Map<URI, Set<URI>> createVolumeToMasksMap() {
Map<URI, Set<URI>> volumeToMasks = new HashMap<>();
// For each ExportMask URI found ...
for (URI maskURI : placementDescriptor.getMasks().keySet()) {
Map<URI, Volume> volumeMap = placementDescriptor.getPlacedVolumes(maskURI);
// If there are any volumes placed to this ExportMask, iterate through
// them and associate the ExportMask URI to the volume.
for (URI volumeURI : volumeMap.keySet()) {
Set<URI> masks = volumeToMasks.get(volumeURI);
if (masks == null) {
masks = new HashSet<>();
volumeToMasks.put(volumeURI, masks);
}
masks.add(maskURI);
}
}
return volumeToMasks;
}
}