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 @@ -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,16 @@ 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");
}
storagePool.setHypervisor(hypervisorType);
storagePoolDao.update(storagePool.getId(),storagePool);
logger.debug("attachZone : Set Hypervisor type for storage pool {} to {}", storagePool.getName(), hypervisorType);

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 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 Down Expand Up @@ -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));
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_svmName_hostUuid
String sanitizedHostUuid = hostUuid.replaceAll("[^a-zA-Z0-9_-]", "_");
String igroupName = OntapStorageConstants.CS + OntapStorageConstants.UNDERSCORE + svmName + OntapStorageConstants.UNDERSCORE + sanitizedHostUuid;
// 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;
}
Comment on lines +142 to 151

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
Original file line number Diff line number Diff line change
Expand Up @@ -344,17 +344,24 @@ public void testInitialize_clusterNotKVM() {
clusterVO.setHypervisorType("XenServer");
when(_clusterDao.findById(2L)).thenReturn(clusterVO);

HashMap<String, String> detailsMap = new HashMap<>();
detailsMap.put(OntapStorageConstants.USERNAME, "testUser");
detailsMap.put(OntapStorageConstants.PASSWORD, "testPassword");
detailsMap.put(OntapStorageConstants.STORAGE_IP, "10.10.10.10");
detailsMap.put(OntapStorageConstants.SVM_NAME, "vs0");
detailsMap.put(OntapStorageConstants.PROTOCOL, "NFS3");

Map<String, Object> dsInfos = new HashMap<>();
dsInfos.put("zoneId",1L);
dsInfos.put("podId",1L);
dsInfos.put("zoneId", 1L);
dsInfos.put("podId", 1L);
dsInfos.put("clusterId", 2L);
dsInfos.put("name", "testStoragePool");
dsInfos.put("providerName", "testProvider");
dsInfos.put("capacityBytes",200000L);
dsInfos.put("managed",true);
dsInfos.put("capacityBytes", 200000L);
dsInfos.put("managed", true);
dsInfos.put("tags", "testTag");
dsInfos.put("isTagARule", false);
dsInfos.put("details", new HashMap<String, String>());
dsInfos.put("details", detailsMap);

Exception ex = assertThrows(CloudRuntimeException.class, () -> {
try (MockedStatic<StorageProviderFactory> storageProviderFactory = Mockito.mockStatic(StorageProviderFactory.class)) {
Expand Down Expand Up @@ -802,4 +809,49 @@ public void testAttachZone_createAccessGroupCalled() throws Exception {
}
}

@Test
public void testAttachZone_nonKvmHypervisorThrowsException() {
// Non-KVM hypervisor type must be rejected before any further processing
Exception ex = assertThrows(CloudRuntimeException.class, () ->
ontapPrimaryDatastoreLifecycle.attachZone(
dataStore, zoneScope, Hypervisor.HypervisorType.VMware));

assertTrue(ex.getMessage().contains("ONTAP primary storage is supported only for KVM hypervisor"));
// update should never be reached
verify(storagePoolDao, times(0)).update(anyLong(), any(StoragePoolVO.class));
}

@Test
public void testAttachZone_nullHypervisorThrowsException() {
// null hypervisorType is not a valid case — only KVM is accepted
Exception ex = assertThrows(CloudRuntimeException.class, () ->
ontapPrimaryDatastoreLifecycle.attachZone(dataStore, zoneScope, null));

assertTrue(ex.getMessage().contains("ONTAP primary storage is supported only for KVM hypervisor"));
verify(storagePoolDao, times(0)).update(anyLong(), any(StoragePoolVO.class));
}

@Test
public void testAttachZone_kvmHypervisorSetsAndUpdatesPool() throws Exception {
// KVM hypervisorType should be set on the pool and persisted via storagePoolDao.update
when(zoneScope.getScopeId()).thenReturn(1L);
when(_resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(any(), eq(1L), eq(Hypervisor.HypervisorType.KVM)))
.thenReturn(mockHosts);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(poolDetails);
when(_dataStoreHelper.attachZone(any(DataStore.class))).thenReturn(dataStore);

try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
.thenReturn(storageStrategy);
when(storageStrategy.createAccessGroup(any(AccessGroup.class))).thenReturn(null);
when(_storageMgr.connectHostToSharedPool(any(HostVO.class), anyLong())).thenReturn(true);

boolean result = ontapPrimaryDatastoreLifecycle.attachZone(
dataStore, zoneScope, Hypervisor.HypervisorType.KVM);

assertTrue(result, "attachZone should succeed for KVM hypervisor");
verify(storagePoolDao, times(1)).update(eq(1L), any(StoragePoolVO.class));
}
}

}
Loading