/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package com.emc.storageos.volumecontroller.impl.block; import java.net.URI; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeSet; 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.db.client.util.CommonTransformerFunctions; import com.google.common.base.Joiner; /** * Data structure do be used for placing volumes into existing ExportMasks. * It will be shared between the VplexBackEndManager and the VplexBackEndMaskingOrchestrator. * */ public class ExportMaskPlacementDescriptor { // INPUT // ExportMask to ExportGroup mapping // This is a reference to the ExportGroup to which an ExportMask should belong private Map<URI, ExportGroup> maskExportGroupMap; // INPUT // Volume URI to Volume object mapping. // These are volumes that need to be placed private Map<URI, Volume> volumesToPlace; // INPUT - Default VOLUMES_TO_SINGLE_MASK // This is used to indicate how placement should be done private PlacementHint placementHint; // INPUT: Initiators that are applicable to this placement private Collection<Initiator> initiators; // INPUT: VPlex array to which the initiators belong private StorageSystem vplex; // INPUT: Backend array where the ExportMasks will be evaluated private StorageSystem backendArray; // INPUT: VirtualArray that covers the VPlex and the backend StorageSystem private URI virtualArray; // INPUT: Tenant to which the objects, such as the volumes, belong private URI tenant; // INPUT: Project to which the object, such as the volumes, belong private URI project; // OUTPUT: getter is read/write // ExportMask URI to ExportMask object mapping. // These are the ExportMasks that we have to work with for placement private Map<URI, ExportMask> masks; // OUTPUT: getter is ead-only // ExportMask URI to Volume mapping // This is the resultant mapping. Each ExportMask URI key has a Map<URI, Volume> value that // indicates that the volumes that are supposed placed. private Map<URI, Map<URI, Volume>> maskToVolumes; // OUTPUT: getter is read-only // Volume URI to Volume object mapping. // These are volumes that could not be placed in an ExportMask. private Map<URI, Volume> unplacedVolumes; // OUTPUT: // Mapping of ExportMask URI to its ExportMaskPolicy private Map<URI, ExportMaskPolicy> exportMaskPolicy; // OUTPUT: // This is a map of a key String to a set of ExportMasks that are // equivalent in terms of properties, but # of volumes can differ. // The key String is generated based on the ExportMask's ExportMaskPolicy // components. private Map<String, Set<URI>> equivalentMasks; // OUTPUT: // Mapping of volume to alternative ExportMask placements private Map<URI, Set<URI>> volumeToAlternativeMasks; public enum PlacementHint { VOLUMES_TO_SINGLE_MASK, VOLUMES_TO_SEPARATE_MASKS } /** * Creates a new instance of ExportMaskPlacementDescriptor with provided parameters as context * * @param tenantURI [IN] - Tenant to which the volumes belong * @param projectURI [IN] - Project to which the volumes belon * @param vplex [IN] - Vplex array to which this placement applies * @param array [IN] - Backend array to which this placement applies * @param virtualArrayURI [IN] - Vplex+Backend array associated VirtualArray * @param volumeMap [IN] - Volumes to be placed * @param initiators [IN] - VPlex initiators to be used for ExportMask selection (if ExportMask already exists) * @return ExportMaskPlacementDescriptor */ public static ExportMaskPlacementDescriptor create(URI tenantURI, URI projectURI, StorageSystem vplex, StorageSystem array, URI virtualArrayURI, Map<URI, Volume> volumeMap, Collection<Initiator> initiators) { ExportMaskPlacementDescriptor descriptor = new ExportMaskPlacementDescriptor(volumeMap); descriptor.setTenant(tenantURI); descriptor.setProject(projectURI); descriptor.setVplex(vplex); descriptor.setVirtualArray(virtualArrayURI); descriptor.setBackendArray(array); descriptor.setInitiators(initiators); descriptor.exportMaskPolicy = new HashMap<>(); descriptor.equivalentMasks = new HashMap<>(); descriptor.volumeToAlternativeMasks = new HashMap<>(); return descriptor; } /** * Initialize the placement with the set of volumes * * @param volumes * [IN] - Volume URI to Volume object mapping */ private ExportMaskPlacementDescriptor(Map<URI, Volume> volumes) { if (masks != null) { this.masks = new HashMap<>(masks); } this.unplacedVolumes = new HashMap<>(volumes); this.volumesToPlace = new HashMap<>(volumes); this.maskToVolumes = new HashMap<>(); this.maskExportGroupMap = new HashMap<>(); this.placementHint = PlacementHint.VOLUMES_TO_SINGLE_MASK; this.exportMaskPolicy = new HashMap<>(); this.equivalentMasks = new HashMap<>(); this.volumeToAlternativeMasks = new HashMap<>(); } /** * Create a mapping for the exportMaskURI to the set 'volumes' * * @param exportMaskURI * [IN] - ExportMask URI * @param volumes * [IN] - Mapping of Volume URI to Volume object */ public void placeVolumes(URI exportMaskURI, Map<URI, Volume> volumes) { maskToVolumes.put(exportMaskURI, new HashMap<>(volumes)); // Once a volume placement has taken place, we should remove it from the unplacedVolumes list for (URI volumeId : volumes.keySet()) { unplacedVolumes.remove(volumeId); } } /** * Remove the placement of the volume from the mappings for the ExportMask and * also remove the maskToVolumes entry if we remove the last volume for that * ExportMask URI key. * * @param volumeURI [IN] - Volume URI reference to be removed * @param exportMaskURI [IN] - ExportMask URI */ public void unplaceVolumeFromMask(URI volumeURI, URI exportMaskURI) { Map<URI, Volume> map = maskToVolumes.get(exportMaskURI); if (map != null) { map.remove(volumeURI); if (map.isEmpty()) { maskToVolumes.remove(exportMaskURI); } } } /** * Get the set of volumes that has been mapped to the ExportMask * * @param exportMaskURI * [IN] - ExportMask URI * * @return Volume URI to Volume object map (Read-only) */ public Map<URI, Volume> getPlacedVolumes(URI exportMaskURI) { Map<URI, Volume> map = maskToVolumes.get(exportMaskURI); return (map != null) ? Collections.unmodifiableMap(map) : Collections.<URI, Volume> emptyMap(); } /** * Return the set of volumes that need to be placed * * @return Volume URI to Volume object map (Read-only) */ public Map<URI, Volume> getVolumesToPlace() { return Collections.unmodifiableMap(volumesToPlace); } /** * Return the ExportMask mapping * * @return ExportMask URI to ExportMask object (Read/write) */ public Map<URI, ExportMask> getMasks() { return masks; } /** * Set the Masks that apply to this descriptor * * @param masks map of ExportMask URIs to ExportMask objects */ public void setMasks(Map<URI, ExportMask> masks) { this.masks = new HashMap<>(masks); } /** * Get all the ExportMask URIs for the descriptor * * @return Set or ExportMask URIs */ public Set<URI> getExportMaskURIs() { return masks.keySet(); } /** * Get the ExportMask object for a given exportMaskURI * * @param exportMaskURI * [IN] - ExportMask URI * @return ExportMask associated with the exportMaskURI */ public ExportMask getExportMask(URI exportMaskURI) { return masks.get(exportMaskURI); } /** * Map the ExportGroup to the ExportMask * * @param exportMask * [IN] - ExportMask URI * @param exportGroup * [IN] - ExportGroup URI */ public void mapExportMaskToExportGroup(URI exportMask, ExportGroup exportGroup) { maskExportGroupMap.put(exportMask, exportGroup); } /** * Get the ExportGroup for the ExportMask * * @param exportMaskURI * [IN] - ExportMask URI * @return ExportGroup that is associated with the ExportMask */ public ExportGroup getExportGroupForMask(URI exportMaskURI) { return maskExportGroupMap.get(exportMaskURI); } /** * Set the indicator of how the placement can be done. * * @param placementHint * [IN] - ExportMaskPlacementDescriptor.Strategy enum */ public void setPlacementHint(PlacementHint placementHint) { this.placementHint = placementHint; } /** * Get the indicator of how the placement can be done. * * @return ExportMaskPlacementDescriptor.Strategy enum */ public PlacementHint getPlacementHint() { return placementHint; } /** * Remove references to the ExportMask in internal data structures * * @param uri [IN] - ExportMask URI */ public void invalidateExportMask(URI uri) { masks.remove(uri); maskExportGroupMap.remove(uri); // Remove the entry from maskToVolumes, then get the volume map entry for the export // we removed. Use that as a tentative list of volumes that are unplaced. We will // determine below, if indeed the volumes are not placed elsewhere. Map<URI, Volume> leftoverMaskToVolumes = maskToVolumes.remove(uri); Map<URI, Volume> tentativelyUnplacedVolumes = (null != leftoverMaskToVolumes) ? leftoverMaskToVolumes : new HashMap<URI, Volume>(); // Search through the mask to volumes mapping to see if any of the // tentatively unplaced volumes show up there. for (URI exportURI : maskToVolumes.keySet()) { Map<URI, Volume> volumeMap = maskToVolumes.get(exportURI); // Go through the volumes mapped to this ExportMask ... for (URI volumeURI : volumeMap.keySet()) { // If the volume is in the tentative list, then we can remove // it because we know it's been associated with something if (tentativelyUnplacedVolumes.containsKey(volumeURI)) { tentativelyUnplacedVolumes.remove(volumeURI); } } } // Check if there's anything still left in tentative list after the above processing if (!tentativelyUnplacedVolumes.isEmpty()) { // Yep - there are volumes still not placed anywhere unplacedVolumes.putAll(tentativelyUnplacedVolumes); } // Invalid masks cannot be alternatives. for (URI volumeURI : volumeToAlternativeMasks.keySet()) { Set<URI> altMaskURIs = volumeToAlternativeMasks.get(volumeURI); if (altMaskURIs != null) { altMaskURIs.remove(uri); } } // Also update the equivalent masks map. for (String key : equivalentMasks.keySet()) { Set<URI> eqMaskURIs = equivalentMasks.get(key); if (eqMaskURIs != null) { eqMaskURIs.remove(uri); } } } /** * Get the initiators that are applicable to this placement * * @return Collection of Initiators */ public Collection<Initiator> getInitiators() { return initiators; } /** * Set the initiators that are applicable to this placement * * @param initiators [IN] - Collection of Initiators */ public void setInitiators(Collection<Initiator> initiators) { this.initiators = initiators; } /** * Get the VPlex array to which the initiators belong * * @return StorageSystem */ public StorageSystem getVplex() { return vplex; } /** * Set the VPlex array to which the initiators belong * * @param vplex [IN] - StorageSystem representing the VPlex array */ public void setVplex(StorageSystem vplex) { this.vplex = vplex; } /** * Get the array that will provide backend volumes * * @return StorageSystem representing the backend array */ public StorageSystem getBackendArray() { return backendArray; } /** * Set the array that will provider backend volumes * * @param backendArray [IN] - StorageSystem representing the backend array */ public void setBackendArray(StorageSystem backendArray) { this.backendArray = backendArray; } /** * Get the VirtualArray that brings together VPlex initiators with the backend array * * @return VirtualArray URI */ public URI getVirtualArray() { return virtualArray; } /** * Set the VirtualArray that brings together VPlex initiators with the backend array * * @param virtualArray [IN] - VirtualArray URI */ public void setVirtualArray(URI virtualArray) { this.virtualArray = virtualArray; } /** * Get the Tenant to which volume objects belong * * @return TenantOrg URI */ public URI getTenant() { return tenant; } /** * Set the Tenant to which volume objects belong * * @param tenant [IN] - TenantOrg URI */ public void setTenant(URI tenant) { this.tenant = tenant; } /** * Get the Project to which the volumes belong * * @return Project URI */ public URI getProject() { return project; } /** * Set the Project to which the volumes belong * * @param project [IN] - Project URI */ public void setProject(URI project) { this.project = project; } /** * Indicates if there are any masks found for placement * * @return True, IFF there are any masks */ public boolean hasMasks() { return (masks != null && !masks.isEmpty()); } /** * Indicates if there are any unplaced volumes * * @return true, IFF there are unplaced volumes list is not empty */ public boolean hasUnPlacedVolumes() { return unplacedVolumes != null && !unplacedVolumes.isEmpty(); } /** * Returns a copy of the unplaced volumes; * * @return Map of Volume URI to Volume object */ public Map<URI, Volume> getUnplacedVolumes() { return new HashMap<>(unplacedVolumes); } /** * Returns set of ExportMask URIs that have been matched and have associated Volumes * * @return Set of ExportMask URIs */ public Set<URI> getPlacedMasks() { return (maskToVolumes != null && !maskToVolumes.isEmpty()) ? Collections.unmodifiableSet(maskToVolumes.keySet()) : Collections.EMPTY_SET; } /** * Groups ExportMask based on ExportMaskPolicy equivalence (minus the #volumes) * * @param exportMask [IN] - ExportMask representing the mapping/masking component on the array * @param policy [IN] - ExportMaskPolicy describes attributes of the ExportMask */ public void addToEquivalentMasks(ExportMask exportMask, ExportMaskPolicy policy) { String key = generatePolicyKey(policy); Set<URI> similarMasks = equivalentMasks.get(key); if (similarMasks == null) { similarMasks = new HashSet<>(); equivalentMasks.put(key, similarMasks); } similarMasks.add(exportMask.getId()); exportMaskPolicy.put(exportMask.getId(), policy); } /** * Get a list of ExportMasks that are equivalent (except for #volumes) * * @param exportMaskURI [IN] - ExportMask representing the mapping/masking component on the array * @return Set URIs pointing to ExportMasks that are equivalent */ public Set<URI> getEquivalentExportMasks(URI exportMaskURI) { Set<URI> result = new HashSet<>(); ExportMaskPolicy policy = exportMaskPolicy.get(exportMaskURI); if (policy != null) { String key = generatePolicyKey(policy); Set<URI> equivalent = equivalentMasks.get(key); // If we found equivalent masks ... if (equivalent != null) { // Add all of them to the result ... result.addAll(equivalent); // And then remove the one we're looking at result.remove(exportMaskURI); } } return result; } /** * Generates a String key based on the fields in the ExportMaskPolicy * * @param policy [IN] - ExportMaskPolicy describes attributes of the ExportMask * @return String key representing the policy */ private String generatePolicyKey(ExportMaskPolicy policy) { // policy.tierPolicies is a StringSet, so lets make sure that the names are in some order // to prevent getting different keys with the same set of policy names Set<String> sortedPolicyNames = (policy.getTierPolicies() != null && policy.getTierPolicies().isEmpty()) ? new TreeSet<>(policy.getTierPolicies()) : Collections.<String> emptySet(); String sortedPolicyNamesString = Joiner.on(';').join(sortedPolicyNames); return String.format("type=%s,ig=%s,localTier=%s,policies=%s,cascaded=%s,simple=%s,hostIObw=%d,hostIOPs=%d", policy.getExportType(), policy.getIgType(), policy.getLocalTierPolicy(), sortedPolicyNamesString, policy.isCascadedIG(), policy.isSimpleMask(), policy.getHostIOLimitBandwidth(), policy.getHostIOLimitIOPs()); } /** * Return a set of ExportMask URIs that represent the ExportMasks that the volume could *have* been placed into * * @param volumeURI [IN] - Volume URI * @return Set of ExportMask URIs that represent the ExportMasks that the volume could *have* been placed into */ public Set<URI> getAlternativeExportsForVolume(URI volumeURI) { Set<URI> alternatesForVolume = volumeToAlternativeMasks.get(volumeURI); return (alternatesForVolume != null) ? Collections.unmodifiableSet(alternatesForVolume) : Collections.<URI> emptySet(); } /** * Associate that the volume, with given URI, can be potentially be placed into the ExportMask, given by its URI. * * @param volumeURI [IN] - Volume URI * @param exportMaskURI [IN] - ExportMask URI to associate with volume */ public void addAsAlternativeExportForVolume(URI volumeURI, URI exportMaskURI) { Set<URI> alternatesForVolume = volumeToAlternativeMasks.get(volumeURI); if (alternatesForVolume == null) { alternatesForVolume = new HashSet<>(); volumeToAlternativeMasks.put(volumeURI, alternatesForVolume); } alternatesForVolume.add(exportMaskURI); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("placementHint : ").append(placementHint.name()).append('\n'). append("vplexArray : ").append(vplex.forDisplay()).append('\n'). append("backendArray : ").append(backendArray.forDisplay()).append('\n'). append("masks : ").append(CommonTransformerFunctions.collectionString(masks.keySet())).append('\n'). append("unplacedVolumes : ").append(CommonTransformerFunctions.collectionString(unplacedVolumes.keySet())).append('\n'). append("volumesToPlace : ").append(CommonTransformerFunctions.collectionString(volumesToPlace.keySet())).append('\n'). append("maskToExportGroup : [").append(displayMaskToExportGroup()).append("]\n"). append("maskToVolumes :\n").append(displayMaskToVolumes()); return builder.toString(); } private String displayMaskToVolumes() { StringBuilder builder = new StringBuilder(); for (URI exportMaskURI : maskToVolumes.keySet()) { Map<URI, Volume> volumeMap = maskToVolumes.get(exportMaskURI); builder.append("\t\t").append(exportMaskURI).append(" ["); int count = 0; for (Volume volume : volumeMap.values()) { if (count > 0) { builder.append(", "); } builder.append(volume.getLabel()); count++; } builder.append("]\n"); } return builder.toString(); } private String displayMaskToExportGroup() { StringBuilder builder = new StringBuilder(); int count = 0; for (URI exportMaskURI : maskExportGroupMap.keySet()) { ExportGroup exportGroup = maskExportGroupMap.get(exportMaskURI); if (count > 0) { builder.append(", "); } builder.append(String.format("%s=%s", exportMaskURI, exportGroup.getId())); count++; } return builder.toString(); } }