Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.lifecycle.BasePrimaryDataStoreLifeCycleImpl;
import org.apache.cloudstack.storage.feign.model.Aggregate;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Volume;
import org.apache.cloudstack.storage.provider.StorageProviderFactory;
Expand Down Expand Up @@ -143,10 +144,28 @@ public DataStore initialize(Map<String, Object> dsInfos) {
if (storageStrategy.getResolvedSvmUuid() != null && !storageStrategy.getResolvedSvmUuid().isEmpty()) {
details.put(OntapStorageConstants.SVM_UUID, storageStrategy.getResolvedSvmUuid());
}
Aggregate aggregate;
try {
aggregate = storageStrategy.chooseAggregate(capacityBytes);
} catch (Exception e) {
logger.error("Exception occurred while choosing aggregate for pool: " + storagePoolName, e);
throw new CloudRuntimeException("Failed to choose ONTAP aggregate for pool: " + storagePoolName
+ ". Error: " + e.getMessage(), e);
}

Pair<String, String> lifResult;
try {
lifResult = storageStrategy.getNetworkInterface(aggregate);
} catch (Exception e) {
logger.error("Exception occurred while retrieving network interface for pool: " + storagePoolName, e);
throw new CloudRuntimeException("Failed to retrieve Data LIF from ONTAP: " + e.getMessage(), e);
}
processDataLifSelection(lifResult, details, storagePoolName, zoneId, podId);

logger.info("Creating ONTAP volume '" + storagePoolName + "' with size: " + capacityBytes + " bytes (" +
(capacityBytes / (1024 * 1024 * 1024)) + " GB)");
try {
Volume volume = storageStrategy.createStorageVolume(storagePoolName, capacityBytes);
Volume volume = storageStrategy.createStorageVolume(storagePoolName, capacityBytes, aggregate);
if (volume == null) {
logger.error("createStorageVolume returned null for volume: " + storagePoolName);
throw new CloudRuntimeException("Failed to create ONTAP volume: " + storagePoolName);
Expand All @@ -158,15 +177,6 @@ public DataStore initialize(Map<String, Object> dsInfos) {
logger.error("Exception occurred while creating ONTAP volume: " + storagePoolName, e);
throw new CloudRuntimeException("Failed to create ONTAP volume: " + storagePoolName + ". Error: " + e.getMessage(), e);
}

Pair<String, String> lifResult;
try {
lifResult = storageStrategy.getNetworkInterface();
} catch (Exception e) {
logger.error("Exception occurred while retrieving network interface for pool: " + storagePoolName, e);
throw new CloudRuntimeException("Failed to retrieve Data LIF from ONTAP: " + e.getMessage(), e);
}
processDataLifSelection(lifResult, details, storagePoolName, zoneId, podId);
} else {
throw new CloudRuntimeException("ONTAP details validation failed, cannot create primary storage");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,6 @@ public abstract class StorageStrategy {

protected OntapStorage storage;

/**
* Holds the node name of the aggregate chosen during createStorageVolume().
* Used by getNetworkInterface() to prefer a LIF homed on the same node.
*/
private String chosenAggregateNode;

/**
* Presents aggregate object for the unified storage, not eligible for disaggregated
*/
Expand Down Expand Up @@ -220,19 +214,16 @@ private void validateAndSelectAggregatesForVolumeCreation(String authHeader, Str
// Common methods like create/delete etc., should be here

/**
* Creates ONTAP Flex-Volume
* Eligible only for Unified ONTAP storage
* throw exception in case of disaggregated ONTAP storage
* Selects the best aggregate for a volume of the given size from candidates populated by
* {@link #connect(boolean)} with aggregate validation enabled.
*
* @param volumeName the name of the volume to create
* @param size the size of the volume in bytes
* @return the created Volume object
* <p>Picks the online aggregate with the largest available block space that can fit
* {@code size}. The returned aggregate includes node information for LIF affinity.</p>
*
* @param size requested volume size in bytes
* @return the chosen aggregate detail response
*/
public Volume createStorageVolume(String volumeName, Long size) {
logger.info("Creating volume: " + volumeName + " of size: " + size + " bytes");

this.chosenAggregateNode = null;

public Aggregate chooseAggregate(Long size) {
String svmName = storage.getSvmName();
if (aggregates == null || aggregates.isEmpty()) {
logger.error("No aggregates available to create volume on SVM " + svmName);
Expand All @@ -243,18 +234,6 @@ public Volume createStorageVolume(String volumeName, Long size) {
}

String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());

// Generate the Create Volume Request
Volume volumeRequest = new Volume();
Svm svm = new Svm();
svm.setName(svmName);
Nas nas = new Nas();
nas.setPath(OntapStorageConstants.SLASH + volumeName);

volumeRequest.setName(volumeName);
volumeRequest.setSvm(svm);

// Pick the best aggregate for this specific request (largest available, online, and sufficient space).
long maxAvailableAggregateSpaceBytes = -1L;
Aggregate aggrChosen = null;
for (Aggregate aggr : aggregates) {
Expand Down Expand Up @@ -298,13 +277,55 @@ public Volume createStorageVolume(String volumeName, Long size) {
logger.error("No suitable aggregates found on SVM " + svmName + " for volume creation.");
throw new CloudRuntimeException("No suitable aggregates found on SVM " + svmName + " for volume operations.");
}
logger.info("Selected aggregate: " + aggrChosen.getName() + " for volume operations.");
if (aggrChosen.getNode() == null || aggrChosen.getNode().getName() == null
|| aggrChosen.getNode().getName().isEmpty()) {
logger.error("Selected aggregate " + aggrChosen.getName() + " does not have a node name.");
throw new CloudRuntimeException("Selected aggregate " + aggrChosen.getName()
+ " does not have a node name required for LIF affinity.");
}
logger.info("Selected aggregate: " + aggrChosen.getName() + " on node "
+ aggrChosen.getNode().getName() + " for volume operations.");
return aggrChosen;
}

this.chosenAggregateNode = aggrChosen.getNode() != null ? aggrChosen.getNode().getName() : null;
/**
* Creates ONTAP Flex-Volume on the given aggregate.
* Eligible only for Unified ONTAP storage
* throw exception in case of disaggregated ONTAP storage
*
* @param volumeName the name of the volume to create
* @param size the size of the volume in bytes
* @param aggregate the aggregate previously selected via {@link #chooseAggregate(Long)}
* @return the created Volume object
*/
public Volume createStorageVolume(String volumeName, Long size, Aggregate aggregate) {
logger.info("Creating volume: " + volumeName + " of size: " + size + " bytes");

String svmName = storage.getSvmName();
if (size == null || size <= 0) {
throw new CloudRuntimeException("Invalid volume size provided: " + size);
}
if (aggregate == null || aggregate.getName() == null || aggregate.getUuid() == null) {
throw new CloudRuntimeException("Aggregate is required to create volume on SVM " + svmName);
}

String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());

// Generate the Create Volume Request
Volume volumeRequest = new Volume();
Svm svm = new Svm();
svm.setName(svmName);
Nas nas = new Nas();
nas.setPath(OntapStorageConstants.SLASH + volumeName);

volumeRequest.setName(volumeName);
volumeRequest.setSvm(svm);

logger.info("Creating volume on aggregate: " + aggregate.getName() + " for volume operations.");

Aggregate aggr = new Aggregate();
aggr.setName(aggrChosen.getName());
aggr.setUuid(aggrChosen.getUuid());
aggr.setName(aggregate.getName());
aggr.setUuid(aggregate.getUuid());
volumeRequest.setAggregates(List.of(aggr));
volumeRequest.setSize(size);
volumeRequest.setNas(nas);
Expand Down Expand Up @@ -480,19 +501,26 @@ public String getStoragePath() {

/**
* Selects the best available data LIF for storage I/O, preferring one homed on the same node
* as the chosen aggregate to avoid inter-node traffic.
* as the given aggregate to avoid inter-node traffic.
*
* <p>Selection order:</p>
* <ol>
* <li>LIF whose {@code location.home_node} matches the chosen aggregate's node — no warning</li>
* <li>LIF whose {@code location.home_node} matches the aggregate's node — no warning</li>
* <li>LIF currently running on that node (e.g. after failover) — returned with a warning</li>
* <li>Any UP and enabled LIF — returned with a warning when aggregate node is known</li>
* <li>Any UP and enabled LIF — returned with a warning</li>
* </ol>
*
* @param aggregate the aggregate previously selected via {@link #chooseAggregate(Long)};
* must include a node name for LIF affinity
* @return {@link Pair} where {@code first()} is the LIF's IP address and {@code second()} is
* a warning message (null when no warning)
*/
public Pair<String, String> getNetworkInterface() {
public Pair<String, String> getNetworkInterface(Aggregate aggregate) {
if (aggregate == null || aggregate.getNode() == null || aggregate.getNode().getName() == null
|| aggregate.getNode().getName().isEmpty()) {
throw new CloudRuntimeException("Aggregate with a node name is required to select a network interface");
}
String aggregateNode = aggregate.getNode().getName();
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
try {
Map<String, Object> queryParams = new HashMap<>();
Expand Down Expand Up @@ -534,21 +562,19 @@ public Pair<String, String> getNetworkInterface() {
if (!isIPv4Address(iface.getIp().getAddress())) {
continue;
}
if (chosenAggregateNode != null) {
// LIF is homed on the aggregate's node
String homeNode = iface.getLocation() != null && iface.getLocation().getHomeNode() != null
? iface.getLocation().getHomeNode().getName() : null;
if (chosenAggregateNode.equals(homeNode)) {
return new Pair<>(iface.getIp().getAddress(), null);
}
// LIF has failed over and is currently running on the aggregate's node
// (home_node differs). Keep as a candidate; returned with a warning if no match is found earlier.
if (currentNodeInterface == null) {
String currentNode = iface.getLocation() != null && iface.getLocation().getNode() != null
? iface.getLocation().getNode().getName() : null;
if (chosenAggregateNode.equals(currentNode)) {
currentNodeInterface = iface;
}
// LIF is homed on the aggregate's node
String homeNode = iface.getLocation() != null && iface.getLocation().getHomeNode() != null
? iface.getLocation().getHomeNode().getName() : null;
if (aggregateNode.equals(homeNode)) {
return new Pair<>(iface.getIp().getAddress(), null);
}
// LIF has failed over and is currently running on the aggregate's node
// (home_node differs). Keep as a candidate; returned with a warning if no match is found earlier.
if (currentNodeInterface == null) {
String currentNode = iface.getLocation() != null && iface.getLocation().getNode() != null
? iface.getLocation().getNode().getName() : null;
if (aggregateNode.equals(currentNode)) {
currentNodeInterface = iface;
}
}
if (fallbackInterface == null) {
Expand All @@ -564,21 +590,20 @@ public Pair<String, String> getNetworkInterface() {

if (currentNodeInterface != null) {
String ip = currentNodeInterface.getIp().getAddress();
String warning = "No home-node LIF found for aggregate node '" + chosenAggregateNode
String warning = "No home-node LIF found for aggregate node '" + aggregateNode
+ "'; using LIF '" + ip + "' currently running on that node (home node LIF may be down).";
logger.warn(warning);
return new Pair<>(ip, warning);
}

String ip = fallbackInterface.getIp().getAddress();
if (chosenAggregateNode == null) {
return new Pair<>(ip, null);
}
String warning = "No operational LIF found on aggregate's home node '" + chosenAggregateNode
String warning = "No operational LIF found on aggregate's home node '" + aggregateNode
+ "'; using fallback LIF '" + ip + "' on a different node."
+ " I/O will traverse an inter-node path, increasing latency.";
logger.warn(warning);
return new Pair<>(ip, warning);
} catch (CloudRuntimeException e) {
throw e;
} catch (Exception e) {
logger.error("Exception while retrieving network interfaces: ", e);
throw new CloudRuntimeException("Failed to retrieve network interfaces: " + e.getMessage());
Expand Down
Loading
Loading