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 @@ -313,7 +313,7 @@ private void deleteCloudStackVolumeSnapshot(SnapshotInfo snapshotInfo, CommandRe
commandResult.setSuccess(true);
commandResult.setResult(null);
} catch (Exception e) {
if (isSnapshotNotFoundError(e)) {
if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
logger.warn("deleteCloudStackVolumeSnapshot: ONTAP snapshot for CloudStack snapshot [{}] "
+ "already absent (idempotent success): {}", snapshotId, e.getMessage());
commandResult.setSuccess(true);
Expand All @@ -327,25 +327,6 @@ private void deleteCloudStackVolumeSnapshot(SnapshotInfo snapshotInfo, CommandRe
}
}

/**
* Returns true when the exception indicates the ONTAP snapshot was already removed.
* Delete is idempotent: a missing backend snapshot is treated as success.
*/
private boolean isSnapshotNotFoundError(Throwable error) {
if (error == null) {
return false;
}
String message = error.getMessage();
if (message != null) {
String lower = message.toLowerCase();
if (lower.contains("404") || lower.contains("not found") || lower.contains("does not exist")
|| lower.contains("entry doesn't exist")) {
return true;
}
}
return isSnapshotNotFoundError(error.getCause());
}

private long resolveSnapshotPoolId(String poolIdStr, long snapshotId) {
if (poolIdStr != null && !poolIdStr.isEmpty()) {
return Long.parseLong(poolIdStr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -805,21 +805,35 @@ public void deleteFlexVolSnapshotForCloudStackVolume(String flexVolUuid, String
logger.info("deleteFlexVolSnapshotForCloudStackVolume: issuing ONTAP REST delete for snapshot [{}] "
+ "(uuid={}) on FlexVol [{}]", snapshotName, snapshotUuid, flexVolUuid);

JobResponse jobResponse = snapshotFeignClient.deleteSnapshot(getAuthHeader(), flexVolUuid, snapshotUuid);

if (jobResponse == null || jobResponse.getJob() == null) {
logger.debug("deleteFlexVolSnapshotForCloudStackVolume: no async job returned for snapshot [{}] "
+ "(uuid={}); treating HTTP success as completion", snapshotName, snapshotUuid);
} else {
logger.debug("deleteFlexVolSnapshotForCloudStackVolume: polling ONTAP delete job [{}] for snapshot [{}]",
jobResponse.getJob().getUuid(), snapshotName);
}
try {
JobResponse jobResponse = snapshotFeignClient.deleteSnapshot(getAuthHeader(), flexVolUuid, snapshotUuid);

if (jobResponse == null || jobResponse.getJob() == null) {
logger.debug("deleteFlexVolSnapshotForCloudStackVolume: no async job returned for snapshot [{}] "
+ "(uuid={}); treating HTTP success as completion", snapshotName, snapshotUuid);
} else {
logger.debug("deleteFlexVolSnapshotForCloudStackVolume: polling ONTAP delete job [{}] for snapshot [{}]",
jobResponse.getJob().getUuid(), snapshotName);
}

pollJobIfPresent(jobResponse, "delete FlexVol snapshot [" + snapshotName + "] uuid [" + snapshotUuid + "]",
OntapStorageConstants.ONTAP_SNAPSHOT_DELETE_JOB_MAX_RETRIES,
OntapStorageConstants.ONTAP_SNAPSHOT_DELETE_JOB_POLL_INTERVAL_MS);
pollJobIfPresent(jobResponse, "delete FlexVol snapshot [" + snapshotName + "] uuid [" + snapshotUuid + "]",
OntapStorageConstants.ONTAP_SNAPSHOT_DELETE_JOB_MAX_RETRIES,
OntapStorageConstants.ONTAP_SNAPSHOT_DELETE_JOB_POLL_INTERVAL_MS);

logger.info("deleteFlexVolSnapshotForCloudStackVolume: ONTAP FlexVol snapshot [{}] (uuid={}) removed from [{}]",
snapshotName, snapshotUuid, flexVolUuid);
logger.info("deleteFlexVolSnapshotForCloudStackVolume: ONTAP FlexVol snapshot [{}] (uuid={}) removed from [{}]",
snapshotName, snapshotUuid, flexVolUuid);
} catch (Exception e) {
if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
logger.warn("deleteFlexVolSnapshotForCloudStackVolume: ONTAP snapshot [{}] (uuid={}) on FlexVol [{}] "
+ "already absent; treating delete as success: {}", snapshotName, snapshotUuid, flexVolUuid,
e.getMessage());
return;
}
if (e instanceof CloudRuntimeException) {
throw (CloudRuntimeException) e;
}
throw new CloudRuntimeException("Failed to delete ONTAP FlexVol snapshot [" + snapshotName + "]: "
+ e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,23 @@ public static String extractUuidFromOntapJobDescription(String description, Stri
return remainder.isEmpty() ? null : remainder;
}

/**
* Returns true when the exception indicates the ONTAP Object was already removed.
* Delete workflows treat a missing backend object as idempotent success.
*/
public static boolean isOntapObjectNotFoundError(Throwable error) {
if (error == null) {
Comment thread
rajiv-jain-netapp marked this conversation as resolved.
return false;
}
String message = error.getMessage();
if (message != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if message is null somehow, I think func run infinitely ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't recurse indefinitely, as we already have a null check at the beginning of the method. My intent here is to ensure that a 404 (Not Found) condition is not missed simply because the exception is wrapped or thrown through multiple nested layers.

String lower = message.toLowerCase();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is better to check the status code as 404

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the implementation generic, I chose to rely on the exception message rather than a specific error code. We could certainly check for error codes as well, but that would make the logic more implementation-specific and reduce its reusability across different scenarios.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but for any entity which is not found, we will receive 404 error code,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these checks are placed at exception handling level. Error code related checks can be handled at feign response level.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot delete operation is asynchronous and initially returns a job response. As a result, any "snapshot not found" condition would typically be surfaced during job polling rather than as an immediate response to the DELETE request. Therefore, we should not expect a direct 404 from the initial delete call. ONTAP uses its own error codes for these scenarios, which are reported through the asynchronous job status and results.

if (lower.contains("404") || lower.contains("not found") || lower.contains("does not exist")
|| lower.contains("entry doesn't exist")) {
return true;
}
}
return isOntapObjectNotFoundError(error.getCause());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -929,4 +929,24 @@ void testDeleteFlexVolSnapshotForCloudStackVolume_PollsJobAndSucceeds() {

verify(snapshotFeignClient).deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1"));
}

@Test
void testDeleteFlexVolSnapshotForCloudStackVolume_AlreadyAbsentOnOntap() {
Job job = new Job();
job.setUuid("delete-job-missing");
JobResponse response = new JobResponse();
response.setJob(job);
when(snapshotFeignClient.deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1")))
.thenReturn(response);

Job failedJob = new Job();
failedJob.setUuid("delete-job-missing");
failedJob.setState(OntapStorageConstants.JOB_FAILURE);
failedJob.setMessage("entry doesn't exist");
when(jobFeignClient.getJobByUUID(anyString(), eq("delete-job-missing"))).thenReturn(failedJob);

storageStrategy.deleteFlexVolSnapshotForCloudStackVolume("fv-uuid-1", "snap-uuid-1", "snap-name-1");

verify(snapshotFeignClient).deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
*/
package org.apache.cloudstack.storage.utils;

import com.cloud.utils.exception.CloudRuntimeException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class OntapStorageUtilsTest {
Expand Down Expand Up @@ -79,4 +81,16 @@ public void getIgroupName_truncates_whenOneCharOverMaxLength() {

assertEquals(OntapStorageConstants.IGROUP_NAME_MAX_LENGTH, result.length());
}

@Test
public void isOntapSnapshotNotFoundError_matchesEntryDoesNotExist() {
CloudRuntimeException ex = new CloudRuntimeException("Job failed with error: entry doesn't exist");
assertTrue(OntapStorageUtils.isOntapObjectNotFoundError(ex));
}

@Test
public void isOntapSnapshotNotFoundError_rejectsUnrelatedErrors() {
assertFalse(OntapStorageUtils.isOntapObjectNotFoundError(
Comment on lines +85 to +93
new CloudRuntimeException("Job failed with error: permission denied")));
}
}
Comment thread
rajiv-jain-netapp marked this conversation as resolved.
Loading