Skip to content

Commit 7f56e8e

Browse files
kvm: do not let a failed flush or a failed snapshot removal report success
Closing an RBD image is where librbd flushes, so a close that fails on an image just written is the last chance a lost write has to surface. Routing those two closes through the non-throwing helper turned that into a warning and returned the disk anyway, which would register a volume whose tail writes never landed. Close the written destination explicitly and let it fail the copy; the finally still covers the paths that did not get that far. deleteSnapshot had the same shape: a failed snapUnprotect or snapRemove was logged and the method still answered "removed successfully", dropping the record while the snapshot kept pinning space on the cluster, and one left protected also blocks removal of its parent volume. It now fails, except for ENOENT, where the snapshot is already gone and the delete has nothing left to do. The same ENOENT tolerance keeps the RBD branch of ManageSnapshotCommand idempotent for deletes. Also corrects the applyTimeouts javadoc, which said 0 means waiting forever for all three options; that is true of the two operation timeouts but the connect timeout falls back to the librados default of 300s. The snapshot test now pins the unwind order, since destroying an IO context after its cluster handle has been shut down would be a use after free. Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent 5759f20 commit 7f56e8e

7 files changed

Lines changed: 65 additions & 13 deletions

File tree

‎agent/src/main/java/com/cloud/agent/properties/AgentProperties.java‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,6 @@ public Property<Integer> getWorkers() {
942942
*/
943943
public static final Property<Integer> RADOS_CLIENT_MOUNT_TIMEOUT = new Property<>("rados.client.mount.timeout", 30);
944944

945-
946945
public static class Property <T>{
947946
private String name;
948947
private T defaultValue;

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtManageSnapshotCommandWrapper.java‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.ceph.rados.IoCTX;
3232
import com.ceph.rados.Rados;
3333
import com.ceph.rbd.Rbd;
34+
import com.ceph.rbd.RbdException;
3435
import com.ceph.rbd.RbdImage;
3536
import com.cloud.agent.api.Answer;
3637
import com.cloud.agent.api.ManageSnapshotAnswer;
@@ -49,6 +50,8 @@
4950
@ResourceWrapper(handles = ManageSnapshotCommand.class)
5051
public final class LibvirtManageSnapshotCommandWrapper extends CommandWrapper<ManageSnapshotCommand, Answer, LibvirtComputingResource> {
5152

53+
/** librados reports a missing object as -ENOENT. */
54+
private static final int RBD_ENOENT = -2;
5255

5356
@Override
5457
public Answer execute(final ManageSnapshotCommand command, final LibvirtComputingResource libvirtComputingResource) {
@@ -132,11 +135,22 @@ public Answer execute(final ManageSnapshotCommand command, final LibvirtComputin
132135
logger.debug("Attempting to remove RBD snapshot " + disk.getName() + "@" + snapshotName);
133136
image.snapRemove(snapshotName);
134137
}
138+
} catch (final RbdException e) {
139+
if (ManageSnapshotCommand.DESTROY_SNAPSHOT.equalsIgnoreCase(command.getCommandSwitch()) && e.getReturnValue() == RBD_ENOENT) {
140+
/*
141+
* Already gone. A delete whose end state is "the snapshot is not there" has
142+
* succeeded, and failing here would break a retried delete.
143+
*/
144+
logger.info("RBD snapshot " + disk.getName() + "@" + snapshotName + " was already gone.");
145+
} else {
146+
/*
147+
* Reporting success here would record a snapshot in CloudStack that does not
148+
* exist on the cluster, or drop one that is still there.
149+
*/
150+
logger.error("A RBD snapshot operation on " + disk.getName() + " failed. The error was: " + e.getMessage(), e);
151+
return new ManageSnapshotAnswer(command, false, "Failed to manage snapshot: " + e.toString());
152+
}
135153
} catch (final Exception e) {
136-
/*
137-
* Reporting success here would record a snapshot in CloudStack that does not exist
138-
* on the cluster.
139-
*/
140154
logger.error("A RBD snapshot operation on " + disk.getName() + " failed. The error was: " + e.getMessage(), e);
141155
return new ManageSnapshotAnswer(command, false, "Failed to manage snapshot: " + e.toString());
142156
} finally {

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
@ResourceWrapper(handles = RevertSnapshotCommand.class)
6363
public class LibvirtRevertSnapshotCommandWrapper extends CommandWrapper<RevertSnapshotCommand, Answer, LibvirtComputingResource> {
6464

65-
6665
protected Set<StoragePoolType> storagePoolTypesThatSupportRevertSnapshot = new HashSet<>(Arrays.asList(StoragePoolType.RBD, StoragePoolType.Filesystem,
6766
StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint));
6867

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/CephUtil.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
*/
4646
public final class CephUtil {
4747

48-
protected static Logger logger = LogManager.getLogger(CephUtil.class);
48+
private static final Logger logger = LogManager.getLogger(CephUtil.class);
4949

5050
private static final String CLIENT_MOUNT_TIMEOUT = "client_mount_timeout";
5151
private static final String RADOS_OSD_OP_TIMEOUT = "rados_osd_op_timeout";
@@ -106,7 +106,8 @@ public static Rados connect(String authUserName, String monHost, int monPort, St
106106

107107
/**
108108
* Applies the connect and operation timeouts from the agent properties. A timeout configured as 0 is
109-
* left unset, which keeps the librados default of waiting forever.
109+
* left unset, so librados uses its own default: waiting forever for the two operation timeouts, and
110+
* 300 seconds for the connect timeout.
110111
*/
111112
private static void applyTimeouts(Rados r) throws RadosException {
112113
int mountTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.RADOS_CLIENT_MOUNT_TIMEOUT);

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ public class KVMStorageProcessor implements StorageProcessor {
172172
private String _manageSnapshotPath;
173173
private int _cmdsTimeout;
174174

175+
/** librados reports a missing object as -ENOENT. */
176+
private static final int RBD_ENOENT = -2;
177+
175178
private static final String MANAGE_SNAPSTHOT_CREATE_OPTION = "-c";
176179
private static final String NAME_OPTION = "-n";
177180
/**
@@ -2948,8 +2951,22 @@ public Answer deleteSnapshot(final DeleteCommand cmd) {
29482951
logger.info("Snapshot " + snapshotFullName + " successfully removed from " +
29492952
primaryPool.getType().toString() + " pool.");
29502953
} catch (RbdException e) {
2951-
logger.error("Failed to remove snapshot " + snapshotFullName + ", with exception: " + e.toString() +
2952-
", RBD error: " + ErrorCode.getErrorMessage(e.getReturnValue()));
2954+
if (e.getReturnValue() == RBD_ENOENT) {
2955+
/*
2956+
* Already gone. A delete whose end state is "the snapshot is not there" has
2957+
* succeeded, and failing here would break a retried delete.
2958+
*/
2959+
logger.info("RBD snapshot " + snapshotFullName + " was already gone.");
2960+
} else {
2961+
/*
2962+
* Anything else means the snapshot is still on the cluster. Reporting success
2963+
* would drop the record while it keeps pinning space, and one left protected
2964+
* also blocks removal of its parent volume.
2965+
*/
2966+
logger.error("Failed to remove snapshot " + snapshotFullName + ", with exception: " + e.toString() +
2967+
", RBD error: " + ErrorCode.getErrorMessage(e.getReturnValue()));
2968+
throw e;
2969+
}
29532970
}
29542971
} finally {
29552972
closeRbdImage(rbd, image, disk.getName());

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,14 @@ private KVMPhysicalDisk createDiskFromTemplateOnRBD(KVMPhysicalDisk template,
14201420
rbd.copy(srcImage, destImage);
14211421

14221422
logger.debug("Finished copying " + srcImage.getName() + " to " + destImage.getName() + " in Ceph pool " + srcPool.getSourceDir());
1423+
1424+
/*
1425+
* rbd_close is where librbd flushes, so closing an image that was just
1426+
* written is the last point a lost write can surface. It has to fail the
1427+
* copy rather than be logged and forgotten.
1428+
*/
1429+
rbd.close(destImage);
1430+
destImage = null;
14231431
} finally {
14241432
CephUtil.closeQuietly(rbd, destImage, disk.getName());
14251433
}
@@ -1517,6 +1525,13 @@ private KVMPhysicalDisk createDiskFromTemplateOnRBD(KVMPhysicalDisk template,
15171525
logger.debug("Copying " + template.getName() + " from Ceph cluster " + rSrc.confGet("mon_host") + " to " + disk.getName()
15181526
+ " on cluster " + rDest.confGet("mon_host"));
15191527
sRbd.copy(srcImage, destImage);
1528+
1529+
/*
1530+
* rbd_close is where librbd flushes, so closing the destination is the last point
1531+
* a lost write can surface. It has to fail the copy rather than be logged.
1532+
*/
1533+
dRbd.close(destImage);
1534+
destImage = null;
15201535
} finally {
15211536
CephUtil.closeQuietly(sRbd, srcImage, template.getName());
15221537
CephUtil.closeQuietly(dRbd, destImage, disk.getName());

‎plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.mockito.Mock;
5151
import org.mockito.MockedConstruction;
5252
import org.mockito.MockedStatic;
53+
import org.mockito.InOrder;
5354
import org.mockito.Mockito;
5455
import org.mockito.MockitoAnnotations;
5556
import org.mockito.Spy;
@@ -549,9 +550,15 @@ public void takeRbdVolumeSnapshotOfStoppedVmTestCreatesSnapshotExactlyOnce() thr
549550

550551
Assert.assertEquals(Long.valueOf(SNAPSHOT_SIZE), result);
551552
Mockito.verify(rbdImageMock, Mockito.times(1)).snapCreate(SNAPSHOT_NAME);
552-
Mockito.verify(rbd.constructed().get(0)).close(rbdImageMock);
553-
Mockito.verify(radosMock).ioCtxDestroy(ioCtxMock);
554-
Mockito.verify(radosMock).shutDown();
553+
554+
/*
555+
* Order matters: Rados.shutDown() releases the native cluster handle, so destroying the IO
556+
* context after it would be a use after free.
557+
*/
558+
InOrder unwind = Mockito.inOrder(rbd.constructed().get(0), radosMock);
559+
unwind.verify(rbd.constructed().get(0)).close(rbdImageMock);
560+
unwind.verify(radosMock).ioCtxDestroy(ioCtxMock);
561+
unwind.verify(radosMock).shutDown();
555562
}
556563
}
557564

0 commit comments

Comments
 (0)