mirror of
https://github.com/apache/cloudstack
synced 2026-08-02 13:35:41 +00:00
Netapp ONTAP bugfixes for 4.23.0 (#13578)
* bugfix: Fix volume attach in iSCSI for ONTAP plugin (#54) * Bugfix: Zone scoped Instance creation fix
This commit is contained in:
parent
a6b31286f0
commit
028e1ef8ea
@ -49,6 +49,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
|
||||
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;
|
||||
@ -1368,11 +1369,13 @@ public class VolumeServiceImpl implements VolumeService {
|
||||
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());
|
||||
details.put(PrimaryDataStore.MANAGED_STORE_TARGET, managedStoreTarget);
|
||||
primaryDataStore.setDetails(details);
|
||||
}
|
||||
|
||||
try {
|
||||
motionSrv.copyAsync(srcTemplateInfo, destTemplateInfo, destHost, caller);
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
</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>
|
||||
@ -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>
|
||||
|
||||
@ -413,7 +413,7 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
|
||||
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(
|
||||
@ -506,7 +506,7 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
@ -50,6 +52,9 @@ public class Volume {
|
||||
@JsonProperty("space")
|
||||
private VolumeSpace space;
|
||||
|
||||
@JsonProperty("guarantee")
|
||||
private Guarantee guarantee;
|
||||
|
||||
@JsonProperty("anti_ransomware")
|
||||
private AntiRansomware antiRansomware;
|
||||
|
||||
@ -112,6 +117,14 @@ public class Volume {
|
||||
this.space = space;
|
||||
}
|
||||
|
||||
public Guarantee getGuarantee() {
|
||||
return guarantee;
|
||||
}
|
||||
|
||||
public void setGuarantee(Guarantee guarantee) {
|
||||
this.guarantee = guarantee;
|
||||
}
|
||||
|
||||
public AntiRansomware getAntiRansomware() {
|
||||
return antiRansomware;
|
||||
}
|
||||
@ -139,4 +152,66 @@ public class Volume {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -337,9 +337,12 @@ public class OntapPrimaryDatastoreLifecycle extends BasePrimaryDataStoreLifeCycl
|
||||
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());
|
||||
@ -364,7 +367,7 @@ public class OntapPrimaryDatastoreLifecycle extends BasePrimaryDataStoreLifeCycl
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_dataStoreHelper.attachZone(dataStore);
|
||||
_dataStoreHelper.attachZone(dataStore, hypervisorType);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
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;
|
||||
@ -135,31 +136,42 @@ public abstract class StorageStrategy {
|
||||
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);
|
||||
@ -211,7 +223,7 @@ public abstract class StorageStrategy {
|
||||
|
||||
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)) {
|
||||
@ -227,7 +239,7 @@ public abstract class StorageStrategy {
|
||||
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;
|
||||
@ -251,6 +263,7 @@ public abstract class StorageStrategy {
|
||||
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) {
|
||||
|
||||
@ -207,7 +207,7 @@ public class UnifiedSANStrategy extends SANStrategy {
|
||||
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<>();
|
||||
@ -271,7 +271,7 @@ public class UnifiedSANStrategy extends SANStrategy {
|
||||
//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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -139,10 +139,15 @@ public class OntapStorageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
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){
|
||||
|
||||
@ -348,6 +348,7 @@ class OntapPrimaryDatastoreDriverTest {
|
||||
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);
|
||||
@ -384,6 +385,7 @@ class OntapPrimaryDatastoreDriverTest {
|
||||
// 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);
|
||||
@ -477,6 +479,7 @@ class OntapPrimaryDatastoreDriverTest {
|
||||
|
||||
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);
|
||||
|
||||
@ -344,17 +344,24 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
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)) {
|
||||
@ -605,7 +612,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
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);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM))).thenReturn(dataStore);
|
||||
|
||||
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
|
||||
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
|
||||
@ -626,7 +633,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
verify(storagePoolDetailsDao, times(1)).listDetailsKeyPairs(1L);
|
||||
verify(storageStrategy, times(1)).createAccessGroup(any(AccessGroup.class));
|
||||
verify(_storageMgr, times(2)).connectHostToSharedPool(any(HostVO.class), eq(1L));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
@ -640,7 +647,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
when(_resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(any(), eq(1L), eq(Hypervisor.HypervisorType.KVM)))
|
||||
.thenReturn(singleHost);
|
||||
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(poolDetails);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class))).thenReturn(dataStore);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM))).thenReturn(dataStore);
|
||||
|
||||
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
|
||||
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
|
||||
@ -655,7 +662,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
// Verify
|
||||
assertTrue(result, "attachZone should return true with single host");
|
||||
verify(_storageMgr, times(1)).connectHostToSharedPool(any(HostVO.class), eq(1L));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
@ -672,7 +679,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
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);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM))).thenReturn(dataStore);
|
||||
|
||||
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
|
||||
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
|
||||
@ -687,7 +694,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
// Verify
|
||||
assertTrue(result, "attachZone should return true with multiple hosts");
|
||||
verify(_storageMgr, times(3)).connectHostToSharedPool(any(HostVO.class), eq(1L));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,7 +724,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
verify(storageStrategy, times(1)).createAccessGroup(any(AccessGroup.class));
|
||||
verify(_storageMgr, times(1)).connectHostToSharedPool(any(HostVO.class), eq(1L));
|
||||
// _dataStoreHelper.attachZone should NOT be called due to early return
|
||||
verify(_dataStoreHelper, times(0)).attachZone(any(DataStore.class));
|
||||
verify(_dataStoreHelper, times(0)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
@ -730,7 +737,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
when(_resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(any(), eq(1L), eq(Hypervisor.HypervisorType.KVM)))
|
||||
.thenReturn(emptyHosts);
|
||||
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(poolDetails);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class))).thenReturn(dataStore);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM))).thenReturn(dataStore);
|
||||
|
||||
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
|
||||
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
|
||||
@ -744,7 +751,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
// Verify
|
||||
assertTrue(result, "attachZone should return true even with no hosts");
|
||||
verify(_storageMgr, times(0)).connectHostToSharedPool(any(HostVO.class), anyLong());
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class));
|
||||
verify(_dataStoreHelper, times(1)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,7 +780,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
// Verify
|
||||
assertFalse(result, "attachZone should return false when any host connection fails");
|
||||
verify(_storageMgr, times(2)).connectHostToSharedPool(any(HostVO.class), eq(1L));
|
||||
verify(_dataStoreHelper, times(0)).attachZone(any(DataStore.class));
|
||||
verify(_dataStoreHelper, times(0)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
@ -784,7 +791,7 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
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);
|
||||
when(_dataStoreHelper.attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM))).thenReturn(dataStore);
|
||||
|
||||
try (MockedStatic<OntapStorageUtils> utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
|
||||
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
|
||||
@ -802,4 +809,49 @@ public class OntapPrimaryDatastoreLifecycleTest {
|
||||
}
|
||||
}
|
||||
|
||||
@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), eq(Hypervisor.HypervisorType.KVM))).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(_dataStoreHelper, times(1)).attachZone(any(DataStore.class), eq(Hypervisor.HypervisorType.KVM));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -230,6 +230,69 @@ public class StorageStrategyTest {
|
||||
verify(svmFeignClient, times(1)).getSvmResponse(anyMap(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnect_succeedsWhenAggregateSpaceBelowPoolCapacity() {
|
||||
// Regression: connect() must validate connectivity/SVM/aggregate-state ONLY.
|
||||
// Capacity is validated per-volume in createStorageVolume(name, size). Previously
|
||||
// connect() compared aggregate free space against the whole storage pool size
|
||||
// (storage.getSize()), which incorrectly failed data-path operations (volume/LUN
|
||||
// create, grant/revoke access, delete) once the pool FlexVolume already existed.
|
||||
Svm svm = new Svm();
|
||||
svm.setName("svm1");
|
||||
svm.setState(OntapStorageConstants.RUNNING);
|
||||
svm.setNfsEnabled(true);
|
||||
|
||||
Aggregate aggregate = new Aggregate();
|
||||
aggregate.setName("aggr1");
|
||||
aggregate.setUuid("aggr-uuid-1");
|
||||
svm.setAggregates(List.of(aggregate));
|
||||
|
||||
OntapResponse<Svm> svmResponse = new OntapResponse<>();
|
||||
svmResponse.setRecords(List.of(svm));
|
||||
|
||||
when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse);
|
||||
|
||||
// Aggregate is ONLINE but has far less free space than the configured pool size (5GB).
|
||||
Aggregate aggregateDetail = mock(Aggregate.class);
|
||||
when(aggregateDetail.getName()).thenReturn("aggr1");
|
||||
when(aggregateDetail.getUuid()).thenReturn("aggr-uuid-1");
|
||||
when(aggregateDetail.getState()).thenReturn(Aggregate.StateEnum.ONLINE);
|
||||
when(aggregateFeignClient.getAggregateByUUID(anyString(), eq("aggr-uuid-1"))).thenReturn(aggregateDetail);
|
||||
|
||||
// Execute & Verify - connect() should succeed regardless of available space.
|
||||
boolean result = storageStrategy.connect();
|
||||
assertTrue(result, "connect() should succeed for an online aggregate even when its free space is below the pool capacity");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnect_noOnlineAggregates() {
|
||||
// Setup - aggregate assigned to the SVM exists but is not ONLINE
|
||||
Svm svm = new Svm();
|
||||
svm.setName("svm1");
|
||||
svm.setState(OntapStorageConstants.RUNNING);
|
||||
svm.setNfsEnabled(true);
|
||||
|
||||
Aggregate aggregate = new Aggregate();
|
||||
aggregate.setName("aggr1");
|
||||
aggregate.setUuid("aggr-uuid-1");
|
||||
svm.setAggregates(List.of(aggregate));
|
||||
|
||||
OntapResponse<Svm> svmResponse = new OntapResponse<>();
|
||||
svmResponse.setRecords(List.of(svm));
|
||||
|
||||
when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse);
|
||||
|
||||
Aggregate aggregateDetail = mock(Aggregate.class);
|
||||
when(aggregateDetail.getName()).thenReturn("aggr1");
|
||||
when(aggregateDetail.getUuid()).thenReturn("aggr-uuid-1");
|
||||
when(aggregateDetail.getState()).thenReturn(null); // not online
|
||||
when(aggregateFeignClient.getAggregateByUUID(anyString(), eq("aggr-uuid-1"))).thenReturn(aggregateDetail);
|
||||
|
||||
// Execute & Verify
|
||||
CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect());
|
||||
assertTrue(ex.getMessage().contains("No suitable aggregates found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnect_svmNotFound() {
|
||||
// Setup
|
||||
@ -342,6 +405,20 @@ public class StorageStrategyTest {
|
||||
assertTrue(ex.getMessage().contains("No SVM found"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnect_invalidCredentials() {
|
||||
// Setup - ONTAP rejects the supplied username/password with HTTP 401 Unauthorized.
|
||||
when(svmFeignClient.getSvmResponse(anyMap(), anyString()))
|
||||
.thenThrow(mock(FeignException.Unauthorized.class));
|
||||
|
||||
// Execute & Verify - connect() must surface a clear "invalid credentials" error.
|
||||
CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect());
|
||||
assertTrue(ex.getMessage().contains("Authentication failed: Invalid credentials"),
|
||||
"Expected an authentication failure message but got: " + ex.getMessage());
|
||||
assertTrue(ex.getMessage().contains("Please verify the username and password"),
|
||||
"Expected the message to prompt verifying username/password but got: " + ex.getMessage());
|
||||
}
|
||||
|
||||
// ========== createStorageVolume() Tests ==========
|
||||
|
||||
@Test
|
||||
|
||||
@ -304,7 +304,7 @@ class UnifiedSANStrategyTest {
|
||||
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
when(host1.getName()).thenReturn("host1");
|
||||
when(host1.getUuid()).thenReturn("host1");
|
||||
when(host1.getStorageUrl()).thenReturn("iqn.1993-08.org.debian:01:host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
@ -357,7 +357,7 @@ class UnifiedSANStrategyTest {
|
||||
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
when(host1.getName()).thenReturn("host1");
|
||||
when(host1.getUuid()).thenReturn("host1");
|
||||
when(host1.getStorageUrl()).thenReturn("iqn.1993-08.org.debian:01:host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
@ -396,7 +396,7 @@ class UnifiedSANStrategyTest {
|
||||
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
lenient().when(host1.getName()).thenReturn("host1");
|
||||
lenient().when(host1.getUuid()).thenReturn("host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
|
||||
@ -444,7 +444,7 @@ class UnifiedSANStrategyTest {
|
||||
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
lenient().when(host1.getName()).thenReturn("host1");
|
||||
lenient().when(host1.getUuid()).thenReturn("host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
|
||||
@ -1042,7 +1042,7 @@ class UnifiedSANStrategyTest {
|
||||
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
when(host1.getName()).thenReturn("host1");
|
||||
when(host1.getUuid()).thenReturn("host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
|
||||
@ -1079,7 +1079,7 @@ class UnifiedSANStrategyTest {
|
||||
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
when(host1.getName()).thenReturn("host1");
|
||||
when(host1.getUuid()).thenReturn("host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
|
||||
@ -1728,7 +1728,7 @@ class UnifiedSANStrategyTest {
|
||||
accessGroup.setStoragePoolId(1L);
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
when(host1.getName()).thenReturn("host1");
|
||||
when(host1.getUuid()).thenReturn("host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
|
||||
@ -1754,7 +1754,7 @@ class UnifiedSANStrategyTest {
|
||||
accessGroup.setStoragePoolId(1L);
|
||||
List<HostVO> hosts = new ArrayList<>();
|
||||
HostVO host1 = mock(HostVO.class);
|
||||
when(host1.getName()).thenReturn("host1");
|
||||
when(host1.getUuid()).thenReturn("host1");
|
||||
hosts.add(host1);
|
||||
accessGroup.setHostsToConnect(hosts);
|
||||
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.cloudstack.storage.utils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class OntapStorageUtilsTest {
|
||||
|
||||
@Test
|
||||
public void getIgroupName_returnsExpectedFormat_whenWithinLimit() {
|
||||
String result = OntapStorageUtils.getIgroupName("svm1", "host-uuid-123");
|
||||
|
||||
assertEquals("cs_host-uuid-123_svm1", result);
|
||||
assertTrue(result.length() <= OntapStorageConstants.IGROUP_NAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIgroupName_sanitizesInvalidCharacters() {
|
||||
// Characters outside [a-zA-Z0-9_-] in the host uuid must be replaced with '_'.
|
||||
String result = OntapStorageUtils.getIgroupName("svm1", "host.uuid:123/abc");
|
||||
|
||||
assertEquals("cs_host_uuid_123_abc_svm1", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIgroupName_doesNotTruncate_whenExactlyAtMaxLength() {
|
||||
// Format: cs(2) + _(1) + hostUuid + _(1) + svmName
|
||||
// For an overall length of 96 with a 4-char uuid, svmName must be 88 chars.
|
||||
String svmName = "a".repeat(88);
|
||||
String hostUuid = "uuid";
|
||||
|
||||
String result = OntapStorageUtils.getIgroupName(svmName, hostUuid);
|
||||
|
||||
assertEquals(OntapStorageConstants.IGROUP_NAME_MAX_LENGTH, result.length());
|
||||
assertEquals("cs_" + hostUuid + "_" + svmName, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIgroupName_truncates_whenExceedingMaxLength() {
|
||||
String svmName = "a".repeat(200);
|
||||
String hostUuid = "host-uuid-123";
|
||||
|
||||
String result = OntapStorageUtils.getIgroupName(svmName, hostUuid);
|
||||
|
||||
assertEquals(OntapStorageConstants.IGROUP_NAME_MAX_LENGTH, result.length());
|
||||
// The truncated value must still be a prefix of the full, untruncated name.
|
||||
String fullName = "cs_" + hostUuid + "_" + svmName;
|
||||
assertEquals(fullName.substring(0, OntapStorageConstants.IGROUP_NAME_MAX_LENGTH), result);
|
||||
assertTrue(result.startsWith("cs_"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIgroupName_truncates_whenOneCharOverMaxLength() {
|
||||
// Build a name that is exactly one character over the limit (97 chars):
|
||||
// svmName of 89 chars + 4-char uuid -> 2 + 1 + 4 + 1 + 89 = 97.
|
||||
String svmName = "a".repeat(89);
|
||||
String hostUuid = "uuid";
|
||||
|
||||
String result = OntapStorageUtils.getIgroupName(svmName, hostUuid);
|
||||
|
||||
assertEquals(OntapStorageConstants.IGROUP_NAME_MAX_LENGTH, result.length());
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,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.DataStoreManager;
|
||||
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.HostScope;
|
||||
@ -5118,6 +5119,13 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
|
||||
|
||||
throw new CloudRuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
// Reload volume from DB after grantAccess — managed storage drivers (e.g. ONTAP)
|
||||
// may update the volume's path and iScsiName during grantAccess, so the local
|
||||
// volumeToAttach object can be stale.
|
||||
if (volumeToAttachStoragePool != null && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(volumeToAttachStoragePool.getStorageProviderName())) {
|
||||
volumeToAttach = _volsDao.findById(volumeToAttach.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (sendCommand) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user