weizhouapache commented on code in PR #14386:
URL: https://github.com/apache/cloudstack/pull/14386#discussion_r4377206754
##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java:
##########
@@ -381,6 +457,212 @@ private boolean
replaceBlockDeviceWithBackup(KVMStoragePoolManager storagePoolMg
return true;
}
+ /**
+ * Rebuild an RBD image by replaying a chain of {@code rbd export-diff}
streams into it: the
+ * chain's full anchor first, then each incremental in the order it was
taken.
+ *
+ * <p>RBD images are raw and cannot hold a backing-file reference, so —
unlike the qcow2 path —
+ * a diff carries no pointer to the one before it and the chain cannot be
flattened by
+ * {@code qemu-img}. Ceph applies the deltas natively instead: each diff
ends at the snapshot
+ * the backup was taken from, {@code import-diff} recreates that snapshot
on the target, and the
+ * next diff in the chain is validated against it. A truncated or
out-of-order diff is therefore
+ * rejected by Ceph rather than silently producing a half-restored
image.</p>
+ *
+ * <p>The image is recreated first. A full diff only carries the extents
that were allocated
+ * when it was taken, so any block still allocated in the current image
would otherwise show
+ * through the restore as stale data. Recreating the image also drops
every RBD snapshot it
+ * carried: a restore rewinds the volume wholesale, and snapshots of
content that is being
+ * replaced cannot be kept. The chain anchors this path takes are ours to
drop, but a snapshot
+ * taken outside it — a CloudStack volume snapshot kept on primary
storage, say — is not, so an
+ * image carrying one is refused rather than rewound.</p>
+ */
+ private boolean restoreRbdImageFromDiffChain(KVMStoragePoolManager
storagePoolMgr, PrimaryDataStoreTO volumePool,
+ String volumePath,
List<String> diffFiles, Long size) {
+ if (!Storage.StoragePoolType.RBD.equals(volumePool.getPoolType())) {
+ logger.error("Backup [{}] holds RBD diffs but volume [{}] is on {}
storage; it cannot be restored there.",
+ diffFiles.get(0), volumePath, volumePool.getPoolType());
+ return false;
+ }
+ KVMStoragePool pool =
storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid());
+ Path keyFile = null;
+ try {
+ keyFile = writeCephKeyFile(pool.getAuthSecret());
+ List<String> connection = rbdConnectionArgs(pool, keyFile);
+ // Volume paths of RBD volumes are already "<ceph pool>/<image>".
+ String image = volumePath;
+
+ // Prefer the current image's own size: it is what the VM's disk
was sized to. Only a
+ // restore that creates a brand new volume has no image to read it
from.
+ Long imageSize = rbdImageSize(connection, image);
+ if (imageSize == null) {
+ imageSize = size;
+ }
+ if (imageSize == null || imageSize <= 0) {
+ logger.error("Unable to determine the size to recreate RBD
image {} with", image);
+ return false;
+ }
+
+ if (!recreateRbdImage(connection, image, imageSize,
rbdDataPool(pool))) {
+ return false;
+ }
+
+ for (String diff : diffFiles) {
+ logger.debug("Applying backup diff {} to RBD image {}", diff,
image);
+ if (Script.executeCommandForExitValue(rbdCommand(connection,
"import-diff", diff, image)) != 0) {
+ logger.error("Failed to apply backup diff {} to RBD image
{}", diff, image);
+ return false;
+ }
+ }
+
+ // The snapshots import-diff recreated only existed to chain the
diffs together; the
+ // restored volume should look like any other, so drop them.
+ if (Script.executeCommandForExitValue(rbdCommand(connection,
"snap", "purge", image)) != 0) {
+ logger.warn("Failed to purge the snapshots left behind by the
restore of RBD image {}", image);
+ }
+ logger.debug("Successfully restored RBD image {} from {} backup
diff(s)", image, diffFiles.size());
+ return true;
+ } catch (IOException e) {
+ logger.error("Failed to prepare the Ceph credentials needed to
restore volume [{}]", volumePath, e);
+ return false;
+ } finally {
+ deleteCephKeyFile(keyFile);
+ }
+ }
+
+ /**
+ * Hand the cephx secret to the {@code rbd} CLI through a 0600 temporary
file rather than an
+ * argument, so it never shows up in the host's process list.
+ */
+ private Path writeCephKeyFile(String authSecret) throws IOException {
+ if (StringUtils.isBlank(authSecret)) {
+ return null;
+ }
+ Path keyFile = Files.createTempFile("csrestore-ceph-", ".key",
+
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
+ Files.write(keyFile, authSecret.getBytes(StandardCharsets.UTF_8));
+ return keyFile;
+ }
+
+ private void deleteCephKeyFile(Path keyFile) {
+ if (keyFile == null) {
+ return;
+ }
+ try {
+ Files.deleteIfExists(keyFile);
+ } catch (IOException e) {
+ logger.warn("Failed to delete the temporary Ceph key file [{}].",
keyFile, e);
+ }
+ }
+
+ private List<String> rbdConnectionArgs(KVMStoragePool pool, Path keyFile) {
+ List<String> args = new ArrayList<>();
+ if (StringUtils.isNotBlank(pool.getSourceHost())) {
+ args.add("-m");
+ args.add(RbdUtils.getRbdMonitors(pool.getSourceHost(),
pool.getSourcePort()));
+ }
+ if (StringUtils.isNotBlank(pool.getAuthUserName())) {
+ args.add("--id");
+ args.add(pool.getAuthUserName());
+ if (keyFile != null) {
+ args.add("--keyfile");
+ args.add(keyFile.toString());
+ }
+ }
+ return args;
+ }
+
+ private String[] rbdCommand(List<String> connection, String... arguments) {
+ List<String> command = new ArrayList<>();
+ command.add(Script.getExecutableAbsolutePath("rbd"));
+ command.addAll(connection);
+ command.addAll(Arrays.asList(arguments));
+ return command.toArray(new String[0]);
+ }
+
+ /** Size of {@code image} in bytes, or {@code null} when the image does
not exist. */
+ private Long rbdImageSize(List<String> connection, String image) {
+ Pair<Integer, String> result = Script.executePipedCommands(
+ Collections.singletonList(rbdCommand(connection, "info",
image, "--format", "json")), 0);
+ if (result.first() == null || result.first() != 0 || result.second()
== null) {
+ return null;
+ }
+ Matcher matcher = RBD_INFO_SIZE.matcher(result.second());
+ return matcher.find() ? Long.valueOf(matcher.group(1)) : null;
+ }
+
+ private boolean recreateRbdImage(List<String> connection, String image,
long sizeBytes, String dataPool) {
+ // `rbd rm` refuses to delete an image that still has snapshots, and a
restore target
+ // normally carries the anchor snapshot of the backup chain it belongs
to. Ask first, so an
+ // image without snapshots is not handed a purge with nothing to do;
when the listing itself
+ // fails we cannot tell, and purging is what we would have done anyway.
+ List<String> snapshots = rbdSnapshots(connection, image);
+ failIfImageCarriesForeignSnapshots(image, snapshots);
Review Comment:
added a method `verifyRbdRestoreTargets` to address it
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]