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.isOntapSnapshotNotFoundError(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.isOntapSnapshotNotFoundError(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 snapshot was already removed.
* Delete workflows treat a missing backend snapshot as idempotent success.
*/
public static boolean isOntapSnapshotNotFoundError(Throwable error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the idea is to throw a polished message back to the user, wouldn't it be better to have a generic ONTAP Exception wrapper class and have similar implementations for all possible ONTAP error codes?

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.

This was added purely to improve readability. You can treat it as a helper method rather than an exception class specific to ONTAP.

if (error == 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.

Agree, we can also use the same method for other ontap entities like igroup, export policy, volume etc

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.

We can reuse this method for any "not found" scenario, regardless of the object type. That's the reason I placed it in the utility class rather than tying it to a specific implementation.

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.

if (lower.contains("404") || lower.contains("not found") || lower.contains("does not exist")
|| lower.contains("entry doesn't exist")) {
return true;
}
}
return isOntapSnapshotNotFoundError(error.getCause());
}
Comment thread
rajiv-jain-netapp marked this conversation as resolved.

}
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.isOntapSnapshotNotFoundError(ex));
}

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