/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.volumecontroller.placement;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
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.constraint.ContainmentConstraint;
import com.emc.storageos.db.client.constraint.URIQueryResultList;
import com.emc.storageos.db.client.model.BlockObject;
import com.emc.storageos.db.client.model.ExportGroup;
import com.emc.storageos.db.client.model.ExportMask;
import com.emc.storageos.db.client.model.ExportPathParams;
import com.emc.storageos.db.client.model.Initiator;
import com.emc.storageos.db.client.model.StorageSystem;
import com.emc.storageos.db.client.util.NullColumnValueGetter;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.util.ConnectivityUtil;
import com.emc.storageos.util.ExportUtils;
import com.emc.storageos.volumecontroller.impl.block.MaskingOrchestrator;
import com.emc.storageos.volumecontroller.impl.utils.ExportMaskUtils;
import com.emc.storageos.workflow.Workflow;
import com.google.common.base.Joiner;
public class ExportPathUpdater {
private static final Logger _log = LoggerFactory.getLogger(ExportPathUpdater.class);
private DbClient _dbClient;
public ExportPathUpdater(DbClient dbClient) {
this._dbClient = dbClient;
}
/**
* This routine is called by the API service to validate that the change path parameters
* call will succeed. For now only the following are allowed:
* A. Increasing maxPaths with pathsPerInitiator the same.
* The following are disallowed:
* X. Decreasing maxPaths
* Y. Changing pathsPerInitiator.
*
* @param storageURI
* @param exportGroupURI
* @param volume (BlockObject)
* @param newParam ExportPathParam new parameters being proposed
*/
private void validateChangePathParams(URI storageURI, URI exportGroupURI,
BlockObject volume, ExportPathParams newParam) {
ExportGroup exportGroup = _dbClient.queryObject(ExportGroup.class,
exportGroupURI);
_log.info(String.format("Validating path parameters for volume %s (%s)",
volume.getLabel(), volume.getId()));
Set<URI> volumeURISet = new HashSet<URI>();
volumeURISet.add(volume.getId());
// Check that the ExportGroup has not overridden the Vpool path parameters.
if (exportGroup.getPathParameters().containsKey(volume.getId().toString())) {
// Cannot do a Vpool path change because parameters have been set in Export Group
_log.info(String.format(
"No changes will be made to ExportGroup %s (%s) because it has explicit path parameters overiding the Vpool",
exportGroup.getLabel(), exportGroup.getId()));
return;
}
// Search through the Export Masks looking for any containing this Volume.
// We only process ViPR created Export Masks, others are ignored.
List<ExportMask> masks = ExportMaskUtils.getExportMasks(_dbClient, exportGroup, storageURI);
for (ExportMask mask : masks) {
if (!mask.hasVolume(volume.getId())) {
continue;
}
if (mask.getCreatedBySystem() == false || mask.getZoningMap() == null) {
_log.info(String.format("ExportMask %s not ViPR created, and will be ignored", mask.getMaskName()));
continue;
}
ExportPathParams maskParam = BlockStorageScheduler
.calculateExportPathParamForExportMask(_dbClient, mask);
if (newParam.getPathsPerInitiator() > maskParam.getPathsPerInitiator()) {
// We want to increase pathsPerInitiator
throw APIException.badRequests.cannotChangeVpoolPathsPerInitiator(exportGroup.getLabel(), mask.getMaskName());
} else if (newParam.getMaxPaths() < maskParam.getMaxPaths()) {
// We want to decreates maxPaths
throw APIException.badRequests.cannotReduceVpoolMaxPaths(exportGroup.getLabel(), mask.getMaskName());
}
}
}
/**
* This routine is called by the API service to validate that the change path parameters
* call will succeed. It locates all the ExportGroups that are exporting the volume.
*
* @param volume (BlockObject)
* @param newParam ExportPathParams new parameters being proposed
*/
public void validateChangePathParams(URI volumeURI, ExportPathParams newParam) {
BlockObject volume = BlockObject.fetch(_dbClient, volumeURI);
_log.info(String.format("Validating path parameters for volume %s (%s) new path parameters %s",
volume.getLabel(), volume.getId(), newParam.toString()));
// Locate all the ExportMasks containing the given volume, and their Export Group.
Map<ExportMask, ExportGroup> maskToGroupMap =
ExportUtils.getExportMasks(volume, _dbClient);
// These steps are serialized, but there is no requirement that they be serialized
// that I know of. It does make it easier to figure out what is going on in the logs.
for (ExportGroup exportGroup : maskToGroupMap.values()) {
validateChangePathParams(volume.getStorageController(), exportGroup.getId(),
volume, newParam);
}
}
/**
* Look through the ExportMask for initiators that are not mapped to any targets.
* This is indicated by the initiators list containing an initiator that is not
* present in the zoningMap as a key.
* Also, we look in the host to see if there are any Initiators
* that could be added to the Export Mask.
* If found, the ExportMask and ExportGroup are updated.
*
* @param mask ExportMask
* @return List<URI> list of Initiator URIs
*/
private List<URI> getUnusedInitiators(ExportGroup group, ExportMask mask) {
List<URI> unusedInitiators = new ArrayList<URI>();
List<URI> hostURIs = new ArrayList<URI>();
for (String initiatorId : mask.getInitiators()) {
Initiator initiator = _dbClient.queryObject(Initiator.class, URI.create(initiatorId));
if (initiator == null || initiator.getInactive()) {
continue;
}
if (!NullColumnValueGetter.isNullURI(initiator.getHost())) {
hostURIs.add(initiator.getHost());
}
if (mask.getZoningMap().get(initiatorId) == null) {
unusedInitiators.add(initiator.getId());
}
}
// Any initiators not in the exportMask should be checked to see if they can be added
for (URI hostURI : hostURIs) {
URIQueryResultList initiatorUris = new URIQueryResultList();
_dbClient.queryByConstraint(
ContainmentConstraint.Factory.getContainedObjectsConstraint(
hostURI, Initiator.class, "host"), initiatorUris);
List<Initiator> initiators = _dbClient.queryObject(Initiator.class, initiatorUris);
for (Initiator initiator : initiators) {
if (!mask.hasInitiator(initiator.getId().toString())) {
StorageSystem storageSystem = _dbClient.queryObject(StorageSystem.class, mask.getStorageDevice());
// Determine what varrays we can use from the ExportGroup
List<URI> varrays = new ArrayList<URI>();
varrays.add(group.getVirtualArray());
if (group.hasAltVirtualArray(storageSystem.getId().toString())) {
URI altVarray = URI.create(group.getAltVirtualArrays().get(storageSystem.getId().toString()));
varrays.add(altVarray);
}
// Check to see if the initiator is connected through the storage array.
if (ConnectivityUtil.isInitiatorConnectedToStorageSystem(
initiator, storageSystem, varrays, _dbClient)) {
_log.info(String.format("Adding host %s initiator %s (%s) to ExportMask %s",
initiator.getHostName(), initiator.getInitiatorPort(), initiator.getId(), mask.getMaskName()));
// Add the initiator into the Export Mask and unusedInitiators list.
mask.addInitiator(initiator);
mask.addToUserCreatedInitiators(initiator);
_dbClient.updateAndReindexObject(mask);
group.addInitiator(initiator);
_dbClient.updateAndReindexObject(group);
unusedInitiators.add(initiator.getId());
}
}
}
}
_log.info("Unused initiators that will be provisioned: ", Joiner.on(",").join(unusedInitiators));
return unusedInitiators;
}
/**
* Generates the workflow steps to change path parameters of an ExportGroup.
* The volume is used to determine the path parameters (from its Vpool).
*
* @param workflow
* @param blockScheduler
* @param orchestrator
* @param storage -- StorageSystem
* @param exportGroup
* @param volume
* @param token -- Task token
* @throws Exception
*/
public void generateExportGroupChangePathParamsWorkflow(
Workflow workflow, BlockStorageScheduler blockScheduler,
MaskingOrchestrator orchestrator,
StorageSystem storage, ExportGroup exportGroup,
BlockObject volume, String token) throws Exception {
Set<URI> volumeURISet = new HashSet<URI>();
volumeURISet.add(volume.getId());
// Check that the ExportGroup has not overridden the Vpool path parameters.
if (exportGroup.getPathParameters().containsKey(volume.getId().toString())) {
// Cannot do a Vpool path change because parameters have been set in Export Group
_log.info(String.format(
"No changes will be made to ExportGroup %s (%s) because it has explicit path parameters overiding the Vpool",
exportGroup.getLabel(), exportGroup.getId()));
return;
}
ExportPathParams newParam = blockScheduler.calculateExportPathParamForVolumes(
volumeURISet, 0, storage.getId(), exportGroup.getId());
_log.info("New path parameters requested: " + newParam.toString());
// Search through the Export Masks looking for any containing this Volume.
// We only process ViPR created Export Masks, others are ignored.
List<ExportMask> masks = ExportMaskUtils.getExportMasks(_dbClient, exportGroup, storage.getId());
for (ExportMask mask : masks) {
if (!mask.hasVolume(volume.getId())) {
continue;
}
// We don't attempt to update the path parameters for Export Masks that were not
// ViPR created, or if the don't have a zoning Map. The later means that ViPR 1.0
// created Export Masks will not be updated, because the zoningMap was not
// introduced until ViPR 1.1.
if (mask.getCreatedBySystem() == false || mask.getZoningMap() == null) {
_log.info(String.format("ExportMask %s not ViPR created, and will be ignored", mask.getMaskName()));
continue;
}
ExportPathParams maskParam = BlockStorageScheduler
.calculateExportPathParamForExportMask(_dbClient, mask);
_log.info(String.format(
"Existing mask %s (%s) path parameters: %s",
mask.getMaskName(), mask.getId(), maskParam.toString()));
if (newParam.getPathsPerInitiator() > maskParam.getPathsPerInitiator()) {
_log.info("Increase paths per initiator not supported");
// We want to increase paths per initiator
// Not supported yet, code will be added here.
} else if (newParam.getMaxPaths() > maskParam.getMaxPaths()) {
// We want to increase MaxPaths.
// Determine the currently unused Initiators.
List<URI> unusedInitiators = getUnusedInitiators(exportGroup, mask);
if (!unusedInitiators.isEmpty()) {
_log.info(String.format("Increasing max_paths from %d to %d",
maskParam.getMaxPaths(), newParam.getMaxPaths()));
orchestrator.increaseMaxPaths(workflow, storage, exportGroup,
mask, unusedInitiators, token);
}
} else if (newParam.getMaxPaths() < maskParam.getMaxPaths()) {
_log.info("Decrease max paths not supported");
// We want to lower MaxPaths. See if no other volume has a higher MaxPaths.
// Not supported yet, code will be added here.
}
}
}
}