Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
Expand Down Expand Up @@ -1363,11 +1364,13 @@ private void createManagedVolumeCopyTemplateAsync(VolumeInfo volumeInfo, Primary
primaryDataStore.setDetails(details);

grantAccess(volumeInfo, destHost, primaryDataStore);
volumeInfo = volFactory.getVolume(volumeInfo.getId(), primaryDataStore);
// For Netapp ONTAP iscsiName or Lun path is available only after grantAccess
String managedStoreTarget = ObjectUtils.defaultIfNull(volumeInfo.get_iScsiName(), volumeInfo.getUuid());
details.put(PrimaryDataStore.MANAGED_STORE_TARGET, managedStoreTarget);
primaryDataStore.setDetails(details);
if (DataStoreProvider.ONTAP_PLUGIN_NAME.equals(primaryDataStore.getStorageProviderName())) {
// For Netapp ONTAP iscsiName or Lun path is available only after grantAccess
volumeInfo = volFactory.getVolume(volumeInfo.getId(), primaryDataStore);
String managedStoreTarget = ObjectUtils.defaultIfNull(volumeInfo.get_iScsiName(), volumeInfo.getUuid());
Comment thread
piyush5netapp marked this conversation as resolved.
details.put(PrimaryDataStore.MANAGED_STORE_TARGET, managedStoreTarget);
Comment thread
piyush5netapp marked this conversation as resolved.
primaryDataStore.setDetails(details);
}

try {
motionSrv.copyAsync(srcTemplateInfo, destTemplateInfo, destHost, caller);
Expand Down
17 changes: 3 additions & 14 deletions plugins/storage/volume/ontap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@
<jackson-databind.version>2.13.4</jackson-databind.version>
<assertj.version>3.24.2</assertj.version>
<junit-jupiter.version>5.8.1</junit-jupiter.version>
<mockito.version>3.12.4</mockito.version>
<mockito-junit-jupiter.version>5.2.0</mockito-junit-jupiter.version>
<byte-buddy-agent.version>1.11.13</byte-buddy-agent.version>
<byte-buddy-agent.version>1.15.11</byte-buddy-agent.version>
</properties>
<dependencyManagement>
<dependencies>
Expand Down Expand Up @@ -105,21 +103,13 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<version>${cs.mockito.version}</version>
<scope>test</scope>
Comment thread
weizhouapache marked this conversation as resolved.
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito-junit-jupiter.version}</version>
<scope>test</scope>
</dependency>

<!-- Mockito Inline (for static method mocking) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<version>${cs.mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -163,7 +153,6 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<skipTests>false</skipTests>
<includes>
<include>**/*Test.java</include>
</includes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore
private void grantAccessIscsi(Host host, VolumeVO volumeVO, Map<String, String> details, String svmName, StoragePoolVO storagePool) {
String cloudStackVolumeName = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME).getValue();
UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) OntapStorageUtils.getStrategyByStoragePoolDetails(details);
String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getName());
String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());

// Validate if Igroup exist ONTAP for this host as we may be using delete_on_unmap= true and igroup may be deleted by ONTAP automatically
Map<String, String> getAccessGroupMap = Map.of(
Expand Down Expand Up @@ -506,7 +506,7 @@ private void revokeAccessForVolume(StoragePoolVO storagePool, VolumeVO volumeVO,
String svmName = details.get(OntapStorageConstants.SVM_NAME);

if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getName());
String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());

// Retrieve LUN name from volume details; if missing, volume may not have been fully created
VolumeDetailVO lunDetail = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

package org.apache.cloudstack.storage.feign.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -50,6 +52,9 @@ public class Volume {
@JsonProperty("space")
private VolumeSpace space;

@JsonProperty("guarantee")
private Guarantee guarantee;

@JsonProperty("anti_ransomware")
private AntiRansomware antiRansomware;

Expand Down Expand Up @@ -112,6 +117,14 @@ public void setSpace(VolumeSpace space) {
this.space = space;
}

public Guarantee getGuarantee() {
return guarantee;
}

public void setGuarantee(Guarantee guarantee) {
this.guarantee = guarantee;
}

public AntiRansomware getAntiRansomware() {
return antiRansomware;
}
Expand Down Expand Up @@ -139,4 +152,66 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hashCode(uuid);
}

public static class Guarantee {

/**
* ONTAP FlexVolume space guarantee (provisioning) type.
* <ul>
* <li>{@link #NONE} - thin provisioning (space is not reserved up front)</li>
* <li>{@link #VOLUME} - thick provisioning (full volume size is reserved on the aggregate)</li>
* </ul>
*/
public enum TypeEnum {
NONE("none"),

VOLUME("volume");

private String value;

TypeEnum(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}

@Override
public String toString() {
return String.valueOf(value);
}

@JsonCreator
public static TypeEnum fromValue(String text) {
if (text == null) return null;
for (TypeEnum b : TypeEnum.values()) {
if (text.equalsIgnoreCase(b.value)) {
return b;
}
}
return null;
}
}

@JsonProperty("type")
private TypeEnum type;

public Guarantee() {
}

public Guarantee(TypeEnum type) {
this.type = type;
}

public TypeEnum getType() {
return type;
}

public void setType(TypeEnum type) {
this.type = type;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,12 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper
logger.error("attachZone : Storage Pool not found for id: " + dataStore.getId());
throw new CloudRuntimeException("Storage Pool not found for id: " + dataStore.getId());
}

if (!Hypervisor.HypervisorType.KVM.equals(hypervisorType)) {
logger.error("attachZone : ONTAP primary storage is supported only for KVM hypervisor");
throw new CloudRuntimeException("ONTAP primary storage is supported only for KVM hypervisor");
}
PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)dataStore;
List<HostVO> hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(dataStore, scope.getScopeId(), Hypervisor.HypervisorType.KVM);
List<HostVO> hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(dataStore, scope.getScopeId(), hypervisorType);
logger.debug(String.format("In createPool. Attaching the pool to each of the hosts in %s.", hostsToConnect));

Map<String, String> details = storagePoolDetailsDao.listDetailsKeyPairs(primaryStore.getId());
Expand All @@ -364,7 +367,7 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper
return false;
}
}
_dataStoreHelper.attachZone(dataStore);
_dataStoreHelper.attachZone(dataStore, hypervisorType);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.apache.logging.log4j.Logger;

import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -135,31 +136,42 @@ public boolean connect() {
logger.error("No aggregates are assigned to SVM " + svmName);
throw new CloudRuntimeException("No aggregates are assigned to SVM " + svmName);
}
// Collect all online aggregates assigned to the SVM. Capacity-based selection is
// intentionally deferred to createStorageVolume(name, size), which validates the
// available space against the actual requested volume size.
List<Aggregate> eligibleAggregates = new ArrayList<>();
for (Aggregate aggr : aggrs) {
logger.debug("Found aggregate: " + aggr.getName() + " with UUID: " + aggr.getUuid());
Aggregate aggrResp = aggregateFeignClient.getAggregateByUUID(authHeader, aggr.getUuid());
if (aggrResp == null) {
logger.warn("Aggregate details response is null for aggregate " + aggr.getName() + ". Skipping.");
break;
continue;
}
if (!Objects.equals(aggrResp.getState(), Aggregate.StateEnum.ONLINE)) {
logger.warn("Aggregate " + aggr.getName() + " is not in online state. Skipping this aggregate.");
continue;
} else if (aggrResp.getSpace() == null || aggrResp.getAvailableBlockStorageSpace() == null ||
aggrResp.getAvailableBlockStorageSpace() <= storage.getSize().doubleValue()) {
logger.warn("Aggregate " + aggr.getName() + " does not have sufficient available space. Skipping this aggregate.");
continue;
}
logger.info("Selected aggregate: " + aggr.getName() + " for volume operations.");
this.aggregates = List.of(aggr);
break;
logger.debug("Aggregate " + aggr.getName() + " is online and eligible for volume operations.");
eligibleAggregates.add(aggr);
}
if (this.aggregates == null || this.aggregates.isEmpty()) {
logger.error("No suitable aggregates found on SVM " + svmName + " for volume creation.");
throw new CloudRuntimeException("No suitable aggregates found on SVM " + svmName + " for volume creation.");
if (eligibleAggregates.isEmpty()) {
logger.error("No suitable aggregates found on SVM " + svmName + " for volume operations.");
throw new CloudRuntimeException("No suitable aggregates found on SVM " + svmName + " for volume operations.");
}
this.aggregates = eligibleAggregates;
logger.info("Found " + eligibleAggregates.size() + " online aggregate(s) on SVM " + svmName + " for volume operations.");

logger.info("Successfully connected to ONTAP cluster and validated ONTAP details provided");
} catch (FeignException.Unauthorized e) {
logger.error("Authentication failed while connecting to ONTAP cluster at " + storage.getStorageIP() +
". Please verify the username and password.", e);
throw new CloudRuntimeException("Authentication failed: Invalid credentials for ONTAP cluster at " +
storage.getStorageIP() + ". Please verify the username and password.");
} catch (FeignException.Forbidden e) {
logger.error("Authorization failed while connecting to ONTAP cluster at " + storage.getStorageIP() +
". The user does not have sufficient privileges.", e);
throw new CloudRuntimeException("Authorization failed: User does not have sufficient privileges on ONTAP cluster at " +
storage.getStorageIP() + ". Please verify user permissions.");
} catch (Exception e) {
logger.error("Failed to connect to ONTAP cluster: " + e.getMessage(), e);
throw new CloudRuntimeException("Failed to connect to ONTAP cluster: " + e.getMessage(), e);
Expand Down Expand Up @@ -211,7 +223,7 @@ public Volume createStorageVolume(String volumeName, Long size) {

if (aggrResp == null) {
logger.warn("Aggregate details response is null for aggregate " + aggr.getName() + ". Skipping.");
break;
continue;
}

if (!Objects.equals(aggrResp.getState(), Aggregate.StateEnum.ONLINE)) {
Expand All @@ -227,7 +239,7 @@ public Volume createStorageVolume(String volumeName, Long size) {
final long availableBytes = aggrResp.getAvailableBlockStorageSpace().longValue();
logger.debug("Aggregate " + aggr.getName() + " available bytes=" + availableBytes + ", requested=" + size);

if (availableBytes <= size) {
if (availableBytes < size) {
logger.warn("Aggregate " + aggr.getName() + " does not have sufficient available space. Required=" +
size + " bytes, available=" + availableBytes + " bytes. Skipping this aggregate.");
continue;
Expand All @@ -251,6 +263,7 @@ public Volume createStorageVolume(String volumeName, Long size) {
volumeRequest.setAggregates(List.of(aggr));
volumeRequest.setSize(size);
volumeRequest.setNas(nas);
volumeRequest.setGuarantee(new Volume.Guarantee(Volume.Guarantee.TypeEnum.NONE));
Comment thread
winterhazel marked this conversation as resolved.
try {
JobResponse jobResponse = volumeFeignClient.createVolumeWithJob(authHeader, volumeRequest);
if (jobResponse == null || jobResponse.getJob() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) {
igroupRequest.setOsType(Igroup.OsTypeEnum.Linux);

for (HostVO host : accessGroup.getHostsToConnect()) {
igroupName = OntapStorageUtils.getIgroupName(svmName, host.getName());
igroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());
igroupRequest.setName(igroupName);

List<Initiator> initiators = new ArrayList<>();
Expand Down Expand Up @@ -271,7 +271,7 @@ public void deleteAccessGroup(AccessGroup accessGroup) {
//Get iGroup name per host
if(!CollectionUtils.isEmpty(accessGroup.getHostsToConnect())) {
for (HostVO host : accessGroup.getHostsToConnect()) {
String igroupName = OntapStorageUtils.getIgroupName(svmName, host.getName());
String igroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());
logger.info("deleteAccessGroup: iGroup name '{}'", igroupName);

// Get the iGroup to retrieve its UUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public class OntapStorageConstants {
public static final String IGROUP_DOT_UUID = "igroup.uuid";
public static final String UNDERSCORE = "_";
public static final String CS = "cs";
public static final int IGROUP_NAME_MAX_LENGTH = 96;
public static final String SRC_CS_VOLUME_ID = "src_cs_volume_id";
public static final String BASE_ONTAP_FV_ID = "base_ontap_fv_id";
public static final String ONTAP_SNAP_ID = "ontap_snap_id";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,15 @@ public static StorageStrategy getStrategyByStoragePoolDetails(Map<String, String
}
}

public static String getIgroupName(String svmName, String hostName) {
//Igroup name format: cs_svmName_hostName
String sanitizedHostName = hostName.split("\\.")[0].replaceAll("[^a-zA-Z0-9_-]", "_");
return OntapStorageConstants.CS + OntapStorageConstants.UNDERSCORE + svmName + OntapStorageConstants.UNDERSCORE + sanitizedHostName;
public static String getIgroupName(String svmName, String hostUuid) {
//Igroup name format: cs_hostUuid_svmName
String sanitizedHostUuid = hostUuid.replaceAll("[^a-zA-Z0-9_-]", "_");
String igroupName = OntapStorageConstants.CS + OntapStorageConstants.UNDERSCORE + sanitizedHostUuid + OntapStorageConstants.UNDERSCORE + svmName;
// ONTAP igroup names are limited to 96 characters; truncate if longer.
if (igroupName.length() > OntapStorageConstants.IGROUP_NAME_MAX_LENGTH) {
igroupName = igroupName.substring(0, OntapStorageConstants.IGROUP_NAME_MAX_LENGTH);
}
return igroupName;
}

public static String generateExportPolicyName(String svmName, String volumeName){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ void testGrantAccess_ClusterScope_Success() {
when(volumeVO.getId()).thenReturn(100L);

when(host.getName()).thenReturn("host1");
when(host.getUuid()).thenReturn("host-uuid-1");

VolumeDetailVO lunNameDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/lun1", false);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME)).thenReturn(lunNameDetail);
Expand Down Expand Up @@ -384,6 +385,7 @@ void testGrantAccess_IgroupNotFound_CreatesNewIgroup() {
// Setup - use HostVO mock since production code casts Host to HostVO
HostVO hostVO = mock(HostVO.class);
when(hostVO.getName()).thenReturn("host1");
when(hostVO.getUuid()).thenReturn("host-uuid-1");

when(dataStore.getId()).thenReturn(1L);
when(volumeInfo.getType()).thenReturn(VOLUME);
Expand Down Expand Up @@ -477,6 +479,7 @@ void testRevokeAccess_ISCSIVolume_Success() {

when(host.getStorageUrl()).thenReturn("iqn.1993-08.org.debian:01:host1");
when(host.getName()).thenReturn("host1");
when(host.getUuid()).thenReturn("host-uuid-1");

VolumeDetailVO lunNameDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/lun1", false);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME)).thenReturn(lunNameDetail);
Expand Down
Loading
Loading