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?

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

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 ?

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

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