/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package com.emc.storageos.vplexcontroller; import static com.google.common.collect.Collections2.transform; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; import com.emc.storageos.cinder.CinderConstants; import com.emc.storageos.db.client.DbClient; import com.emc.storageos.db.client.URIUtil; import com.emc.storageos.db.client.constraint.AlternateIdConstraint; import com.emc.storageos.db.client.constraint.URIQueryResultList; import com.emc.storageos.db.client.model.DataObject; import com.emc.storageos.db.client.model.DiscoveredDataObject; 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.StoragePort; import com.emc.storageos.db.client.model.StorageProvider; import com.emc.storageos.db.client.model.StorageSystem; import com.emc.storageos.db.client.model.StringMap; import com.emc.storageos.db.client.model.Volume; import com.emc.storageos.db.client.util.CommonTransformerFunctions; import com.emc.storageos.db.client.util.NullColumnValueGetter; import com.emc.storageos.db.exceptions.DatabaseException; import com.emc.storageos.exceptions.DeviceControllerException; import com.emc.storageos.networkcontroller.impl.NetworkDeviceController; import com.emc.storageos.recoverpoint.utils.WwnUtils; import com.emc.storageos.util.ConnectivityUtil; import com.emc.storageos.util.ExportUtils; import com.emc.storageos.util.NetworkUtil; import com.emc.storageos.util.VPlexUtil; import com.emc.storageos.volumecontroller.ControllerException; import com.emc.storageos.volumecontroller.impl.utils.ExportMaskUtils; import com.emc.storageos.vplex.api.VPlexApiClient; import com.emc.storageos.vplex.api.VPlexApiConstants; import com.emc.storageos.vplex.api.VPlexApiException; import com.emc.storageos.vplex.api.VPlexApiFactory; import com.emc.storageos.vplex.api.VPlexResourceInfo; import com.emc.storageos.vplex.api.VPlexStorageViewInfo; import com.emc.storageos.vplex.api.VPlexStorageVolumeInfo; import com.emc.storageos.vplex.api.VPlexTargetInfo; import com.google.common.base.Joiner; import com.google.common.collect.Collections2; public class VPlexControllerUtils { // logger reference. private static final Logger log = LoggerFactory .getLogger(VPlexControllerUtils.class); private static final String VPLEX = "vplex"; /** * Get a DataObject. Throw exception if not found or inactive. * * @param clazz * @param id * @return * @throws ControllerException */ protected static <T extends DataObject> T getDataObject(Class<T> clazz, URI id, DbClient dbClient) throws DeviceControllerException { try { T object = null; if (id != null) { object = dbClient.queryObject(clazz, id); if (object == null) { throw VPlexApiException.exceptions.getDataObjectFailedNotFound( clazz.getSimpleName(), id.toString()); } if (object.getInactive()) { log.info("database object is inactive: " + object.getId().toString()); throw VPlexApiException.exceptions.getDataObjectFailedInactive(id.toString()); } } return object; } catch (DatabaseException ex) { throw VPlexApiException.exceptions.getDataObjectFailedExc(id.toString(), ex); } } /** * Get the HTTP client for making requests to the passed VPlex management server. * * @param vplexApiFactory A reference to the VPlex API factory. * @param vplexMnmgtSvr A VPlex management server. * @param dbClient A reference to a DB client. * * @return A reference to the VPlex API HTTP client. * @throws URISyntaxException */ public static VPlexApiClient getVPlexAPIClient(VPlexApiFactory vplexApiFactory, StorageProvider vplexMnmgtSvr, DbClient dbClient) throws URISyntaxException { URI vplexEndpointURI = new URI("https", null, vplexMnmgtSvr.getIPAddress(), vplexMnmgtSvr.getPortNumber(), "/", null, null); VPlexApiClient client = vplexApiFactory.getClient(vplexEndpointURI, vplexMnmgtSvr.getUserName(), vplexMnmgtSvr.getPassword()); return client; } /** * Get the HTTP client for making requests to the passed VPlex storage system. * * @param vplexApiFactory A reference to the VPlex API factory. * @param vplexSystem The VPlex storage system. * @param dbClient A reference to a DB client. * * @return A reference to the VPlex API HTTP client. * @throws URISyntaxException */ public static VPlexApiClient getVPlexAPIClient(VPlexApiFactory vplexApiFactory, StorageSystem vplexSystem, DbClient dbClient) throws URISyntaxException { // Create the URI to access the VPlex Management Server based // on the IP and port for the active provider for the passed // VPlex system. StorageProvider activeMgmntSvr = null; URI activeMgmntSvrURI = vplexSystem.getActiveProviderURI(); if (!NullColumnValueGetter.isNullURI(activeMgmntSvrURI)) { activeMgmntSvr = dbClient.queryObject(StorageProvider.class, activeMgmntSvrURI); } if (activeMgmntSvr == null) { log.error("No active management server for VPLEX system {}", vplexSystem.getId()); throw VPlexApiException.exceptions.connectionFailure(vplexSystem.getId().toString()); } return getVPlexAPIClient(vplexApiFactory, activeMgmntSvr, dbClient); } /** * Get the HTTP client for making requests to the passed VPlex storage system URI. * Performs some helpful validation on the requested VPLEX system before returning * successfully. * * @param vplexApiFactory A reference to the VPlex API factory. * @param vplexSystemUri The VPlex storage system URI. * @param dbClient A reference to a DB client. * * @return A reference to the VPlex API HTTP client. * @throws URISyntaxException */ public static VPlexApiClient getVPlexAPIClient(VPlexApiFactory vplexApiFactory, URI vplexUri, DbClient dbClient) throws URISyntaxException { if (vplexUri == null) { log.error("The provided VPLEX Storage System URI was null."); throw VPlexApiException.exceptions.vplexUriIsNull(); } StorageSystem vplex = dbClient.queryObject(StorageSystem.class, vplexUri); if (vplex == null) { log.error("No VPLEX Storage System was found with URI {}.", vplexUri.toString()); throw VPlexApiException.exceptions.vplexSystemNotFound(vplexUri.toString()); } if (!vplex.getSystemType().equals(DiscoveredDataObject.Type.vplex.name())) { log.error("The Storage System (of type {}) with URI {} is not a VPLEX system.", vplex.getSystemType(), vplexUri.toString()); throw VPlexApiException.exceptions.invalidStorageSystemType( vplex.getSystemType(), vplexUri.toString()); } return getVPlexAPIClient(vplexApiFactory, vplex, dbClient); } /** * Determines the cluster name for a VPlex virtual volume based on the * volume's virtual array. * * @param dbClient db client * @param vplexVolume The VPlex volume whose cluster we want to find. * @return The VPlex cluster name * @throws Exception * @throws URISyntaxException */ public static String getVPlexClusterName(DbClient dbClient, Volume vplexVolume) throws Exception { // Get the virtual array from the vplex virtual volume. This will be used // to determine the volume's vplex cluster. URI vaURI = vplexVolume.getVirtualArray(); URI vplexURI = vplexVolume.getStorageController(); return getVPlexClusterName(dbClient, vaURI, vplexURI); } /** * Determines the cluster name based on the volume's virtual array. * * @param dbClient db client * @param vplexURI The vplex system URI * @param vaURI The virtual array URI * @return The VPlex cluster name * @throws Exception * @throws URISyntaxException */ public static String getVPlexClusterName(DbClient dbClient, URI vaURI, URI vplexURI) { String clusterName = null; // Get the vplex storage system so we can a handle on the vplex client StorageSystem vplexSystem = getDataObject(StorageSystem.class, vplexURI, dbClient); VPlexApiClient client = null; try { client = VPlexControllerUtils.getVPlexAPIClient(VPlexApiFactory.getInstance(), vplexSystem, dbClient); } catch (URISyntaxException e) { throw VPlexApiException.exceptions.connectionFailure(vplexURI.toString()); } String vplexCluster = ConnectivityUtil.getVplexClusterForVarray(vaURI, vplexSystem.getId(), dbClient); if (vplexCluster.equals(ConnectivityUtil.CLUSTER_UNKNOWN)) { throw VPlexApiException.exceptions.couldNotFindCluster(vplexCluster); } clusterName = client.getClusterNameForId(vplexCluster); return clusterName; } /** * Returns the cluster name (free form, user-configurable) * for a given VPLEX cluster id (either "1" or "2"). * * @param clusterId the cluster id (either "1" or "2") * @param vplexUri URI of the VPLEX to look at * @param dbClient a database client instance * * @return the cluster name as configured by the user, or null * if it couldn't be determined */ public static String getClusterNameForId(String clusterId, URI vplexUri, DbClient dbClient) { String clusterName = null; VPlexApiClient client = null; try { VPlexApiFactory vplexApiFactory = VPlexApiFactory.getInstance(); client = VPlexControllerUtils.getVPlexAPIClient(vplexApiFactory, vplexUri, dbClient); } catch (URISyntaxException e) { log.error("cannot load vplex api client", e); } if (null != client) { clusterName = client.getClusterNameForId(clusterId); } log.info("VPLEX cluster name for cluster id {} is {}", clusterId, clusterName); return clusterName; } /** * Returns a VPlexResourceInfo object for the given device name based * on its virtual volume type (local or distributed). * * @param deviceName the name of the device * @param virtualVolumeType the type of virtual volume (local or distributed) * @param vplexUri the URI of the VPLEX system * @param dbClient a reference to the database client * * @return a VPlexResourceInfo object for the device name * @throws VPlexApiException */ public static VPlexResourceInfo getDeviceInfo(String deviceName, String virtualVolumeType, URI vplexUri, DbClient dbClient) throws VPlexApiException { VPlexResourceInfo device = null; VPlexApiClient client = null; try { VPlexApiFactory vplexApiFactory = VPlexApiFactory.getInstance(); client = VPlexControllerUtils.getVPlexAPIClient(vplexApiFactory, vplexUri, dbClient); } catch (URISyntaxException e) { log.error("cannot load vplex api client", e); } if (null != client) { device = client.getDeviceStructure(deviceName, virtualVolumeType); } return device; } /** * Returns a Map of lowest-level storage-volume resource's WWN to its VPlexStorageVolumeInfo * object for a given device name, virtual volume type, and cluster name. If * hasMirror is true, this indicates the top-level device is composed of a * RAID-1 mirror, so there's an extra layers of components to traverse in finding * the lowest-level storage-volume resources. * * @param deviceName the name of the top-level device to look at * @param virtualVolumeType the type of virtual volume (local or distributed) * @param clusterName the cluster name * @param hasMirror indicates if the top-level device is a RAID-1 mirror * @param vplexUri the URI of the VPLEX system * @param dbClient a reference to the database client * * @return a map of WWNs to VPlexStorageVolumeInfo objects * @throws VPlexApiException */ public static Map<String, VPlexStorageVolumeInfo> getStorageVolumeInfoForDevice( String deviceName, String virtualVolumeType, String clusterName, boolean hasMirror, URI vplexUri, DbClient dbClient) throws VPlexApiException { Map<String, VPlexStorageVolumeInfo> storageVolumeInfo = null; VPlexApiClient client = null; try { VPlexApiFactory vplexApiFactory = VPlexApiFactory.getInstance(); client = VPlexControllerUtils.getVPlexAPIClient(vplexApiFactory, vplexUri, dbClient); } catch (URISyntaxException e) { log.error("cannot load vplex api client", e); } if (null != client) { storageVolumeInfo = client.getStorageVolumeInfoForDevice( deviceName, virtualVolumeType, clusterName, hasMirror); } log.info("Backend storage volume wwns for {} are {}", deviceName, storageVolumeInfo); return storageVolumeInfo; } /** * Gets the list of ITLs from the volume extensions * * @param volume * @return */ public static List<String> getVolumeITLs(Volume volume) { List<String> itlList = new ArrayList<>(); StringMap extensions = volume.getExtensions(); if (null != extensions) { Set<String> keys = extensions.keySet(); for (String key : keys) { if (key.startsWith(CinderConstants.PREFIX_ITL)) { itlList.add(extensions.get(key)); } } } return itlList; } /** * Returns true if the Initiator object represents a VPLEX StoragePort. * * @param initiator the Initiator to test * @param dbClient a reference to the database client * * @return true if the Initiator object represents a VPLEX StoragePort */ public static boolean isVplexInitiator(Initiator initiator, DbClient dbClient) { StoragePort port = NetworkUtil.getStoragePort(initiator.getInitiatorPort(), dbClient); if (null != port) { StorageSystem vplex = dbClient.queryObject(StorageSystem.class, port.getStorageDevice()); if (null != vplex && VPLEX.equals(vplex.getSystemType())) { return true; } } return false; } /** * Returns a Map of distributed device component context * paths from the VPLEX API to VPLEX cluster names. * * @param vplexUri the VPLEX to query * @param dbClient a reference to the database client * @return a Map of distributed device component context * paths from the VPLEX API to VPLEX cluster names * * @throws VPlexApiException */ public static Map<String, String> getDistributedDevicePathToClusterMap( URI vplexUri, DbClient dbClient) throws VPlexApiException { VPlexApiClient client = null; try { VPlexApiFactory vplexApiFactory = VPlexApiFactory.getInstance(); client = VPlexControllerUtils.getVPlexAPIClient(vplexApiFactory, vplexUri, dbClient); } catch (URISyntaxException e) { log.error("cannot load vplex api client", e); } Map<String, String> distributedDevicePathToClusterMap = Collections.emptyMap(); if (null != client) { distributedDevicePathToClusterMap = client.getDistributedDevicePathToClusterMap(); } return distributedDevicePathToClusterMap; } /** * Validates that the underlying structure of the given device name * satisfies the constraints for compatibility with ViPR. Used for * validating unmanaged VPLEX volumes before ingestion. * * @param deviceName the device to validate * @param vplexUri the VPLEX to query * @param dbClient a reference to the database client * @throws VPlexApiException if the device structure is incompatible with ViPR */ public static void validateSupportingDeviceStructure(String deviceName, URI vplexUri, DbClient dbClient) throws VPlexApiException { VPlexApiClient client = null; try { VPlexApiFactory vplexApiFactory = VPlexApiFactory.getInstance(); client = VPlexControllerUtils.getVPlexAPIClient(vplexApiFactory, vplexUri, dbClient); } catch (URISyntaxException e) { log.error("cannot load vplex api client", e); } if (null != client) { String drillDownResponse = client.getDrillDownInfoForDevice(deviceName); if (!VPlexUtil.isDeviceStructureValid(deviceName, drillDownResponse)) { throw VPlexApiException.exceptions.deviceStructureIsIncompatibleForIngestion(drillDownResponse); } } else { throw VPlexApiException.exceptions.failedToExecuteDrillDownCommand( deviceName, "cannot load vplex api client"); } } /** * Creates a Map of target-port to port-wwn values for a VPLEX device * For example: target port - P0000000046E01E80-A0-FC02 * port wwn - 0x50001442601e8002 * * @param client a reference to the VPlexApiClient to query for port info * @param clusterName the cluster for this port name search; port names can potentially be different * across clusters. * @return a Map of target-port to port-wwn values for a VPLEX device */ public static Map<String, String> getTargetPortToPwwnMap(VPlexApiClient client, String clusterName) { Map<String, String> targetPortToPwwnMap = new HashMap<String, String>(); List<VPlexTargetInfo> targetInfos = client.getTargetInfoForCluster(clusterName); if (targetInfos != null) { for (VPlexTargetInfo vplexTargetPortInfo : targetInfos) { if (null != vplexTargetPortInfo.getPortWwn()) { targetPortToPwwnMap.put(vplexTargetPortInfo.getName(), vplexTargetPortInfo.getPortWwn()); } } } log.info("target port map for cluster {} is {}", clusterName, targetPortToPwwnMap); return targetPortToPwwnMap; } /** * Refreshes the given ExportMask with the latest export information from the VPLEX. * * This method follows the same basic logic as VmaxExportOperations.refreshExportMask, * but adjusted for VPLEX storage views and other concepts. * * @param dbClient a reference to the database client * @param storageView a reference to the VPlexStorageViewInfo for the ExportMask's mask name * @param exportMask the ExportMask to refresh * @param vplexClusterName the VPLEX cluster name on which to find the ExportMask * @param targetPortToPwwnMap a Map of VPLEX target port names to WWNs * @param networkDeviceController the NetworkDeviceController, used for refreshing the zoning map */ public static void refreshExportMask(DbClient dbClient, VPlexStorageViewInfo storageView, ExportMask exportMask, Map<String, String> targetPortToPwwnMap, NetworkDeviceController networkDeviceController) { refreshExportMask(dbClient, storageView, exportMask, targetPortToPwwnMap, networkDeviceController, false); } /** * Refreshes the given ExportMask with the latest export information from the VPLEX, with * an option to indicate whether or not the caller is a remove volume or initiator operation. * If removing volumes or initiators, we will not remove anything from the existing collections * so that any logic for data unavailability prevention can be made on the state before ViPR * attempts to make any changes to the storage view. * * This method follows the same basic logic as VmaxExportOperations.refreshExportMask, * but adjusted for VPLEX storage views and other concepts. * * @param dbClient a reference to the database client * @param storageView a reference to the VPlexStorageViewInfo for the ExportMask's mask name * @param exportMask the ExportMask to refresh * @param vplexClusterName the VPLEX cluster name on which to find the ExportMask * @param targetPortToPwwnMap a Map of VPLEX target port names to WWNs * @param networkDeviceController the NetworkDeviceController, used for refreshing the zoning map * @param isRemoveOperation flag to indicate whether the caller is a operation that removes inits or vols */ private static void refreshExportMask(DbClient dbClient, VPlexStorageViewInfo storageView, ExportMask exportMask, Map<String, String> targetPortToPwwnMap, NetworkDeviceController networkDeviceController, boolean isRemoveOperation) { try { if (null == exportMask || null == storageView || null == targetPortToPwwnMap || targetPortToPwwnMap.isEmpty()) { int portNameMapEntryCount = targetPortToPwwnMap != null ? targetPortToPwwnMap.size() : 0; String message = String.format("export mask was %s, storage view was %s, and port name to wwn map had %d entries", exportMask, storageView, portNameMapEntryCount); log.error(message); if (null == storageView) { if (null != exportMask) { log.warn(String.format("storage view %s could not be found on VPLEX device %s", exportMask.getMaskName(), exportMask.getStorageDevice())); cleanStaleExportMasks(dbClient, exportMask.getStorageDevice()); } return; } else { throw new IllegalArgumentException("export mask refresh arguments are invalid: " + message); } } // Get volumes and initiators for the masking instance Map<String, Integer> discoveredVolumes = storageView.getWwnToHluMap(); List<String> discoveredInitiators = storageView.getInitiatorPwwns(); Set<String> existingVolumes = (exportMask.getExistingVolumes() != null) ? exportMask.getExistingVolumes().keySet() : Collections.emptySet(); Set<String> existingInitiators = (exportMask.getExistingInitiators() != null) ? exportMask.getExistingInitiators() : Collections.emptySet(); List<String> viprVolumes = new ArrayList<String>(); if (exportMask.getVolumes() != null) { List<Volume> vols = dbClient.queryObject(Volume.class, URIUtil.toURIList(exportMask.getVolumes().keySet())); for (Volume volume : vols) { viprVolumes.add(volume.getWWN()); } } List<String> viprInits = new ArrayList<String>(); if (exportMask.getInitiators() != null && !exportMask.getInitiators().isEmpty()) { List<Initiator> inits = dbClient.queryObject(Initiator.class, URIUtil.toURIList(exportMask.getInitiators())); for (Initiator init : inits) { viprInits.add(Initiator.normalizePort(init.getInitiatorPort())); } } // Update user added volume's HLU information in ExportMask and ExportGroup ExportMaskUtils.updateHLUsInExportMask(exportMask, discoveredVolumes, dbClient); String name = exportMask.getMaskName(); log.info(String.format("%nExportMask %s in the ViPR database: ViPR Vols:{%s} ViPR Inits:{%s} Existing Inits:{%s} Existing Vols:{%s}%n", name, Joiner.on(',').join(viprVolumes), Joiner.on(',').join(viprInits), Joiner.on(',').join(existingInitiators), Joiner.on(',').join(existingVolumes))); log.info(String.format("StorageView %s discovered on VPLEX: Inits:{%s} Vols:{%s}%n", name, Joiner.on(',').join(discoveredInitiators), Joiner.on(',').join(discoveredVolumes.keySet()))); // Check the initiators and update the lists as necessary List<String> initiatorsToAddToExisting = new ArrayList<String>(); List<Initiator> initiatorsToAddToUserAddedAndInitiatorList = new ArrayList<Initiator>(); for (String port : discoveredInitiators) { String normalizedPort = Initiator.normalizePort(port); if (!exportMask.hasExistingInitiator(normalizedPort) && !exportMask.hasUserInitiator(normalizedPort) ) { // If the initiator is in our DB, and it's in our compute resource, it gets added to to the initiator list. // Otherwise it gets added to the existing list. Initiator knownInitiator = ExportUtils.getInitiator(Initiator.toPortNetworkId(port), dbClient); if (knownInitiator != null && !ExportMaskUtils.checkIfDifferentResource(exportMask, knownInitiator)) { initiatorsToAddToUserAddedAndInitiatorList.add(knownInitiator); } else { initiatorsToAddToExisting.add(normalizedPort); } } } // Existing Initiators that are not part of the Storage View discovered initiators List<String> initiatorsToRemoveFromExistingList= new ArrayList<String>(); if (exportMask.getExistingInitiators() != null && !exportMask.getExistingInitiators().isEmpty()) { for (String existingInitiatorStr : exportMask.getExistingInitiators()) { if (!discoveredInitiators.contains(existingInitiatorStr)) { initiatorsToRemoveFromExistingList.add(existingInitiatorStr); } else { Initiator existingInitiator = ExportUtils.getInitiator(Initiator.toPortNetworkId(existingInitiatorStr), dbClient); if (existingInitiator != null && !ExportMaskUtils.checkIfDifferentResource(exportMask, existingInitiator)) { log.info( "Initiator {}->{} belonging to same compute, removing from existing and adding to userAdded and initiator list", existingInitiatorStr, existingInitiator.getId()); initiatorsToAddToUserAddedAndInitiatorList.add(existingInitiator); initiatorsToRemoveFromExistingList.add(existingInitiatorStr); } } } } // Initiators that are not part of the Storage View discovered initiators List<URI> initiatorsToRemoveFromUserAddedAndInitiatorList = new ArrayList<URI>(); if (exportMask.getInitiators() != null && !exportMask.getInitiators().isEmpty()) { initiatorsToRemoveFromUserAddedAndInitiatorList.addAll(Collections2.transform(exportMask.getInitiators(), CommonTransformerFunctions.FCTN_STRING_TO_URI)); for (String port : discoveredInitiators) { String normalizedPort = Initiator.normalizePort(port); Initiator initiatorDiscoveredInViPR = ExportUtils.getInitiator(Initiator.toPortNetworkId(port), dbClient); if (initiatorDiscoveredInViPR != null) { initiatorsToRemoveFromUserAddedAndInitiatorList.remove(initiatorDiscoveredInViPR.getId()); } else if (!exportMask.hasExistingInitiator(normalizedPort)) { log.info("Initiator {} not found in database, removing from user Added and initiator list," + " and adding to existing list.", port); initiatorsToAddToExisting.add(normalizedPort); } } } boolean removeInitiators = !initiatorsToRemoveFromExistingList.isEmpty() || !initiatorsToRemoveFromUserAddedAndInitiatorList.isEmpty(); boolean addInitiators = !initiatorsToAddToUserAddedAndInitiatorList.isEmpty() || !initiatorsToAddToExisting.isEmpty(); // Check the volumes and update the lists as necessary Map<String, Integer> volumesToAdd = ExportMaskUtils.diffAndFindNewVolumes(exportMask, discoveredVolumes); boolean addVolumes = !volumesToAdd.isEmpty(); boolean removeVolumes = false; List<String> volumesToRemoveFromExisting = new ArrayList<String>(); if (exportMask.getExistingVolumes() != null && !exportMask.getExistingVolumes().isEmpty()) { volumesToRemoveFromExisting.addAll(exportMask.getExistingVolumes().keySet()); volumesToRemoveFromExisting.removeAll(discoveredVolumes.keySet()); } // if volume is in userAddedVolumes now, but also in existing volumes, // we should remove it from existing volumes if (!isRemoveOperation) { for (String wwn : discoveredVolumes.keySet()) { if (exportMask.hasExistingVolume(wwn)) { URIQueryResultList volumeList = new URIQueryResultList(); dbClient.queryByConstraint(AlternateIdConstraint.Factory.getVolumeWwnConstraint(wwn), volumeList); if (volumeList.iterator().hasNext()) { URI volumeURI = volumeList.iterator().next(); if (exportMask.hasUserCreatedVolume(volumeURI)) { log.info("\texisting volumes contain wwn {}, but it is also in the " + "export mask's user added volumes, so removing from existing volumes", wwn); volumesToRemoveFromExisting.add(wwn); } } } } } removeVolumes = !volumesToRemoveFromExisting.isEmpty(); // Grab the storage ports that have been allocated for this // existing mask and update them. List<String> storagePorts = storageView.getPorts(); List<String> portWwns = new ArrayList<String>(); for (String storagePort : storagePorts) { if (targetPortToPwwnMap.keySet().contains(storagePort)) { portWwns.add(WwnUtils.convertWWN(targetPortToPwwnMap.get(storagePort), WwnUtils.FORMAT.COLON)); } } List<String> storagePortURIs = ExportUtils.storagePortNamesToURIs(dbClient, portWwns); // Check the storagePorts and update the lists as necessary boolean addStoragePorts = false; List<String> storagePortsToAdd = new ArrayList<String>(); if (exportMask.getStoragePorts() == null) { exportMask.setStoragePorts(new ArrayList<String>()); } for (String portID : storagePortURIs) { if (!exportMask.getStoragePorts().contains(portID)) { storagePortsToAdd.add(portID); addStoragePorts = true; } } boolean removeStoragePorts = false; List<String> storagePortsToRemove = new ArrayList<String>(); if (exportMask.getStoragePorts() != null && !exportMask.getStoragePorts().isEmpty()) { storagePortsToRemove.addAll(exportMask.getStoragePorts()); storagePortsToRemove.removeAll(storagePortURIs); removeStoragePorts = !storagePortsToRemove.isEmpty(); } log.info( String.format( "ExportMask %s refresh initiators; addToExisting:{%s} removeAndUpdateZoning:{%s} removeFromExistingOnly:{%s}%n", name, Joiner.on(',').join(initiatorsToAddToExisting), Joiner.on(',').join(initiatorsToRemoveFromUserAddedAndInitiatorList), Joiner.on(',').join(initiatorsToRemoveFromExistingList))); log.info( String.format( "ExportMask %s refresh initiators; user Added and initiator List; add:{%s} remove:{%s}%n", name, Joiner.on(',').join(initiatorsToAddToUserAddedAndInitiatorList), Joiner.on(',').join(initiatorsToRemoveFromUserAddedAndInitiatorList))); log.info( String.format("ExportMask %s refresh volumes; addToExisting:{%s} removeFromExistingOnly:{%s}%n", name, Joiner.on(',').join(volumesToAdd.keySet()), Joiner.on(',').join(volumesToRemoveFromExisting))); log.info( String.format("ExportMask %s refresh ports; add:{%s} remove:{%s}%n", name, Joiner.on(',').join(storagePortsToAdd), Joiner.on(',').join(storagePortsToRemove))); // Any changes indicated, then update the mask and persist it if (addInitiators || removeInitiators || addVolumes || removeVolumes || addStoragePorts || removeStoragePorts) { log.info("ExportMask refresh: There are changes to mask, updating it...\n"); exportMask.removeFromExistingInitiators(initiatorsToRemoveFromExistingList); if (initiatorsToRemoveFromUserAddedAndInitiatorList != null && !initiatorsToRemoveFromUserAddedAndInitiatorList.isEmpty()) { exportMask.removeInitiators(dbClient.queryObject(Initiator.class, initiatorsToRemoveFromUserAddedAndInitiatorList)); exportMask.removeFromUserCreatedInitiators(dbClient.queryObject(Initiator.class, initiatorsToRemoveFromUserAddedAndInitiatorList)); } exportMask.addToUserCreatedInitiators(initiatorsToAddToUserAddedAndInitiatorList); exportMask.addInitiators(initiatorsToAddToUserAddedAndInitiatorList); exportMask.addToExistingInitiatorsIfAbsent(initiatorsToAddToExisting); exportMask.removeFromExistingVolumes(volumesToRemoveFromExisting); exportMask.addToExistingVolumesIfAbsent(volumesToAdd); exportMask.getStoragePorts().addAll(storagePortsToAdd); exportMask.getStoragePorts().removeAll(storagePortsToRemove); // update native id (this is the context path to the storage view on the vplex) exportMask.setNativeId(storageView.getPath()); ExportMaskUtils.sanitizeExportMaskContainers(dbClient, exportMask); dbClient.updateObject(exportMask); log.info("ExportMask is now:\n" + exportMask.toString()); } else { log.info("ExportMask refresh: There are no changes to the mask\n"); } networkDeviceController.refreshZoningMap(exportMask, transform(initiatorsToRemoveFromUserAddedAndInitiatorList, CommonTransformerFunctions.FCTN_URI_TO_STRING), Collections.emptyList(), (addInitiators || removeInitiators), true); } catch (Exception ex) { log.error("Failed to refresh VPLEX Storage View: " + ex.getLocalizedMessage(), ex); String storageViewName = exportMask != null ? exportMask.getMaskName() : "unknown"; throw VPlexApiException.exceptions.failedToRefreshVplexStorageView(storageViewName, ex.getLocalizedMessage()); } } /** * Returns all VPLEX storage systems in ViPR. * * @param dbClient a database client reference * @return a List of StorageSystems that are "vplex" type */ public static List<StorageSystem> getAllVplexStorageSystems(DbClient dbClient) { List<StorageSystem> vplexStorageSystems = new ArrayList<StorageSystem>(); List<URI> allStorageSystemUris = dbClient.queryByType(StorageSystem.class, true); List<StorageSystem> allStorageSystems = dbClient.queryObject(StorageSystem.class, allStorageSystemUris); for (StorageSystem storageSystem : allStorageSystems) { if ((storageSystem != null) && (DiscoveredDataObject.Type.vplex.name().equals(storageSystem.getSystemType()))) { vplexStorageSystems.add(storageSystem); } } return vplexStorageSystems; } /** * Returns all VPLEX Storage Systems in ViPR that have the given assembly id count. The * VPLEX assembly id is another term for the cluster serial number. * * @param dbClient a database client reference * @param assemblyIdCount the VPLEX assembly id count * @return a List of StorageSystems with a matching assembly id count */ private static List<StorageSystem> getVplexesByAssemblyIdCount(DbClient dbClient, Integer assemblyIdCount) { List<StorageSystem> vplexStorageSystems = getAllVplexStorageSystems(dbClient); Iterator<StorageSystem> it = vplexStorageSystems.iterator(); while (it.hasNext()) { StorageSystem vplex = it.next(); if (null != vplex.getVplexAssemblyIdtoClusterId() && (assemblyIdCount != vplex.getVplexAssemblyIdtoClusterId().size())) { it.remove(); } } return vplexStorageSystems; } /** * Returns all VPLEX local storage systems in ViPR. * * @param dbClient a database client reference * @return a List of StorageSystems that are in a VPLEX local configuration */ public static List<StorageSystem> getAllVplexLocalStorageSystems(DbClient dbClient) { return getVplexesByAssemblyIdCount(dbClient, VPlexApiConstants.VPLEX_LOCAL_ASSEMBLY_COUNT); } /** * Returns all VPLEX metro storage systems in ViPR. * * @param dbClient a database client reference * @return a List of StorageSystems that are in a VPLEX metro configuration */ public static List<StorageSystem> getAllVplexMetroStorageSystems(DbClient dbClient) { return getVplexesByAssemblyIdCount(dbClient, VPlexApiConstants.VPLEX_METRO_ASSEMBLY_COUNT); } /** * Cleans stale instances of ExportMasks from the database. A stale instance is * one which no longer exists as a storage view on the VPLEX and also contains * no more user added volumes. * * @param dbClient a reference to the database client * @param vplexUri the VPLEX system URI */ public static void cleanStaleExportMasks(DbClient dbClient, URI vplexUri) { log.info("starting clean up of stale export masks for vplex {}", vplexUri); List<ExportMask> exportMasks = ExportMaskUtils.getExportMasksForStorageSystem(dbClient, vplexUri); // get a VPLEX API client for this VPLEX URI VPlexApiClient client = null; try { client = VPlexControllerUtils.getVPlexAPIClient(VPlexApiFactory.getInstance(), vplexUri, dbClient); } catch (URISyntaxException ex) { log.error("URISyntaxException encountered: ", ex); } if (null == client) { log.error("Couldn't load vplex api client, skipping stale export mask cleanup."); return; } // assemble collections of storage view native ids (VPLEX API context paths) // and export mask names (VPLEX API storage view names) for comparison with ViPR List<VPlexStorageViewInfo> storageViewsOnDevice = client.getStorageViewsLite(); Set<String> svNativeIds = new HashSet<String>(); Set<String> svNames = new HashSet<String>(); for (VPlexStorageViewInfo sv : storageViewsOnDevice) { svNativeIds.add(sv.getPath()); svNames.add(sv.getName()); } // create collections to hold any stale data we find, for clean up all at once at the very end Set<ExportMask> staleExportMasks = new HashSet<ExportMask>(); Map<ExportGroup, Set<ExportMask>> exportGroupToStaleMaskMap = new HashMap<ExportGroup, Set<ExportMask>>(); Map<URI, ExportGroup> exportGroupUriMap = new HashMap<URI, ExportGroup>(); // check all export masks in the database to make sure they still exist on the VPLEX. // a null or empty ExportMask.nativeId would indicate an ExportMask that has been created // by ViPR in the database, but not yet created on the VPLEX device itself. skip those of course. for (ExportMask exportMask : exportMasks) { if (null != exportMask && !exportMask.getInactive() && (exportMask.getNativeId() != null && !exportMask.getNativeId().isEmpty())) { // we need to check both native id and export mask name to make sure we are NOT finding the storage view. // native id is most accurate, but greenfield ExportMasks for VPLEX don't have this property set. // native id will be set on ingested export masks, however, and we should check it, in case the same // storage view name is used on both vplex clusters. // greenfield VPLEX ExportMasks will always have unique mask names on both clusters (prefixed by V1_ or V2_), // so for greenfield export masks, we can check mask names if the native id property is not set. boolean noNativeIdMatch = (null != exportMask.getNativeId()) && !svNativeIds.contains(exportMask.getNativeId()); boolean noMaskNameMatch = (null != exportMask.getMaskName()) && !svNames.contains(exportMask.getMaskName()); if (noNativeIdMatch || noMaskNameMatch) { log.info("ExportMask {} is not found on VPLEX", exportMask.getMaskName()); // if any user added volumes are still present, we will not do anything with this export mask boolean hasActiveVolumes = false; if (exportMask.hasAnyUserAddedVolumes()) { List<URI> userAddedVolUris = URIUtil.toURIList(exportMask.getUserAddedVolumes().values()); List<Volume> userAddedVols = dbClient.queryObject(Volume.class, userAddedVolUris); for (Volume vol : userAddedVols) { if (null != vol && !vol.getInactive()) { hasActiveVolumes = true; break; } } } if (hasActiveVolumes) { log.warn("ExportMask {} has active user added volumes, so will not remove from database.", exportMask.forDisplay()); continue; } // this is a stale export mask because it doesn't exist on the VPLEX and doesn't have user-added volumes staleExportMasks.add(exportMask); // we need to remove this stale ExportMask from any ExportGroups that contain it. // we use the exportGroupUriMap so that at the end of this process we will only // be updating a single ExportGroup instance from the database in case // multiple ExportMasks from the same ExportGroup need to be removed. List<ExportGroup> egList = ExportUtils.getExportGroupsForMask(exportMask.getId(), dbClient); if (!CollectionUtils.isEmpty(egList)) { for (ExportGroup exportGroup : egList) { // skip this one if the export group is no longer existent or active if (null == exportGroup || exportGroup.getInactive()) { continue; } // update or reuse from the cache of already-loaded ExportGroups if (!exportGroupUriMap.containsKey(exportGroup.getId())) { // add this one to the cache exportGroupUriMap.put(exportGroup.getId(), exportGroup); } else { // just reuse the one already loaded from the database exportGroup = exportGroupUriMap.get(exportGroup.getId()); } // map the current ExportGroup and ExportMask for breaking // of associations at the end of this whole process if (!exportGroupToStaleMaskMap.containsKey(exportGroup)) { exportGroupToStaleMaskMap.put(exportGroup, new HashSet<ExportMask>()); } exportGroupToStaleMaskMap.get(exportGroup).add(exportMask); log.info("Stale ExportMask {} will be removed from ExportGroup {}", exportMask.getMaskName(), exportGroup.getLabel()); } } } } } if (!CollectionUtils.isEmpty(staleExportMasks)) { dbClient.markForDeletion(staleExportMasks); log.info("Deleted {} stale ExportMasks from database.", staleExportMasks.size()); if (!CollectionUtils.isEmpty(exportGroupToStaleMaskMap.keySet())) { for (Entry<ExportGroup, Set<ExportMask>> entry : exportGroupToStaleMaskMap.entrySet()) { ExportGroup exportGroup = entry.getKey(); for (ExportMask exportMask : entry.getValue()) { log.info("Removing ExportMask {} from ExportGroup {}", exportMask.getMaskName(), exportGroup.getLabel()); exportGroup.removeExportMask(exportMask.getId()); } } dbClient.updateObject(exportGroupToStaleMaskMap.keySet()); } } log.info("Stale Export Mask cleanup complete."); } }