abh1sar commented on code in PR #13074:
URL: https://github.com/apache/cloudstack/pull/13074#discussion_r3216411026


##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -85,6 +86,16 @@ public class NASBackupProvider extends AdapterBase 
implements BackupProvider, Co
             true,
             BackupFrameworkEnabled.key());
 

Review Comment:
   Add the config `nas.backup.incremental.enabled` to enable/disable 
incremental backups feature.
   Also please make sure the transition is seamless with existing backups when 
someone enables or disables the feature. I think the code should handle that 
already, but please double check.



##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -495,24 +693,244 @@ public boolean deleteBackup(Backup backup, boolean 
forced) {
             throw new CloudRuntimeException(String.format("Unable to find a 
running KVM host in zone %d to delete backup %s", backup.getZoneId(), 
backup.getUuid()));
         }
 
-        DeleteBackupCommand command = new 
DeleteBackupCommand(backup.getExternalId(), backupRepository.getType(),
-                backupRepository.getAddress(), 
backupRepository.getMountOptions());
+        // Repair the chain (if any) before removing the backup file. For 
chained backups,

Review Comment:
   The delete backup logic seems a bit complicated due to rebasing and chain 
manipulation on deleting a backup with children.
   
   The logic CloudStack uses for deleting incremental snapshots in cloudstack 
will apply to backups as follows:
   * Delete comes for backup B1
   * If B1 has child backups:
       * Mark B1 as `delete-pending` in the database
       * Do not delete the backup file
       * Do not modify the backup chain
   * If B1 has no child backups in the database:
       * call DeleteBackup(B1)
   
   DeleteBackup(B1)
   * Delete B1's backup record from the database
   * Delete B1's backup file from storage    
   * If B1's parent (P1) is marked with `delete-pending`
       * call DeleteBackup(P1)
   
   Please consider using this logic as it is seems less risky, maintainable and 
semantically consistent with the already present incremental snapshots logic.



##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -168,6 +182,168 @@ protected Host getVMHypervisorHost(VirtualMachine vm) {
         return 
resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM,
 vm.getDataCenterId());
     }
 
+    /**
+     * Returned by {@link #decideChain(VirtualMachine)} to describe the next 
backup's place in
+     * the chain: full vs incremental, the bitmap name to create, and (for 
incrementals) the
+     * parent bitmap and parent file path.
+     */
+    static final class ChainDecision {
+        final String mode;          // "full" or "incremental"
+        final String bitmapNew;
+        final String bitmapParent;  // null for full
+        final String parentPath;    // null for full
+        final String chainId;       // chain identifier this backup belongs to
+        final int chainPosition;    // 0 for full, N for the Nth incremental 
in the chain
+
+        private ChainDecision(String mode, String bitmapNew, String 
bitmapParent, String parentPath,
+                              String chainId, int chainPosition) {
+            this.mode = mode;
+            this.bitmapNew = bitmapNew;
+            this.bitmapParent = bitmapParent;
+            this.parentPath = parentPath;
+            this.chainId = chainId;
+            this.chainPosition = chainPosition;
+        }
+
+        static ChainDecision fullStart(String bitmapName) {
+            return new ChainDecision(NASBackupChainKeys.TYPE_FULL, bitmapName, 
null, null,
+                    UUID.randomUUID().toString(), 0);
+        }
+
+        static ChainDecision incremental(String bitmapNew, String 
bitmapParent, String parentPath,
+                                         String chainId, int chainPosition) {
+            return new ChainDecision(NASBackupChainKeys.TYPE_INCREMENTAL, 
bitmapNew, bitmapParent,
+                    parentPath, chainId, chainPosition);
+        }
+
+        boolean isIncremental() {
+            return NASBackupChainKeys.TYPE_INCREMENTAL.equals(mode);
+        }
+    }
+
+    /**
+     * Decides whether the next backup for {@code vm} should be a fresh full 
or an incremental
+     * appended to the existing chain. Stopped VMs are always full (libvirt 
{@code backup-begin}
+     * requires a running QEMU process). The {@code nas.backup.full.every} 
ConfigKey controls
+     * how many backups (full + incrementals) form one chain before a new full 
is forced.
+     */
+    protected ChainDecision decideChain(VirtualMachine vm) {
+        final String newBitmap = "backup-" + System.currentTimeMillis() / 
1000L;
+
+        // Stopped VMs cannot do incrementals — script will also fall back, 
but we make the
+        // decision here so we register the right type up-front.
+        if (VirtualMachine.State.Stopped.equals(vm.getState())) {
+            return ChainDecision.fullStart(newBitmap);
+        }
+
+        Integer fullEvery = NASBackupFullEvery.valueIn(vm.getDataCenterId());
+        if (fullEvery == null || fullEvery <= 1) {
+            // Disabled or every-backup-is-full mode.
+            return ChainDecision.fullStart(newBitmap);
+        }
+
+        // Walk this VM's backups newest→oldest, find the most recent BackedUp 
backup that has a
+        // bitmap stored. If we don't find one, this is the first backup in a 
chain — start full.
+        List<Backup> history = backupDao.listByVmId(vm.getDataCenterId(), 
vm.getId());
+        if (history == null || history.isEmpty()) {
+            return ChainDecision.fullStart(newBitmap);
+        }
+        history.sort(Comparator.comparing(Backup::getDate).reversed());

Review Comment:
   This logic breaks after restore. We can't use the last backup taken as 
parent in that case.
   May I suggest a way to tackle this:
   
   1. Keep an `active_checkpoint_id` in vm_details
   2. Take Backup will update VM's `active_checkpoint_id` and the backup's 
`checkpoint_id` to the same value.
   4. Restore will set the VM's `active_checkpoint_id` to null
   5. If VM's `active_checkpoint_id` is null, next backup is full.
   6. If VM's `active_checkpoint_id` is not null, it is an incremental backup.
       * The latest backup should have the `bitmap_name` equal to the VM's 
`active_checkpoint_id` which will become the parent backup. If not - force a 
full backup.
       *  I don't think we need the loop written on line 257.  Just checking 
the latest backup should be enough.



##########
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:
##########
@@ -168,6 +182,168 @@ protected Host getVMHypervisorHost(VirtualMachine vm) {
         return 
resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM,
 vm.getDataCenterId());
     }
 
+    /**
+     * Returned by {@link #decideChain(VirtualMachine)} to describe the next 
backup's place in
+     * the chain: full vs incremental, the bitmap name to create, and (for 
incrementals) the
+     * parent bitmap and parent file path.
+     */
+    static final class ChainDecision {
+        final String mode;          // "full" or "incremental"
+        final String bitmapNew;
+        final String bitmapParent;  // null for full
+        final String parentPath;    // null for full
+        final String chainId;       // chain identifier this backup belongs to
+        final int chainPosition;    // 0 for full, N for the Nth incremental 
in the chain
+
+        private ChainDecision(String mode, String bitmapNew, String 
bitmapParent, String parentPath,
+                              String chainId, int chainPosition) {
+            this.mode = mode;
+            this.bitmapNew = bitmapNew;
+            this.bitmapParent = bitmapParent;
+            this.parentPath = parentPath;
+            this.chainId = chainId;
+            this.chainPosition = chainPosition;
+        }
+
+        static ChainDecision fullStart(String bitmapName) {
+            return new ChainDecision(NASBackupChainKeys.TYPE_FULL, bitmapName, 
null, null,
+                    UUID.randomUUID().toString(), 0);
+        }
+
+        static ChainDecision incremental(String bitmapNew, String 
bitmapParent, String parentPath,
+                                         String chainId, int chainPosition) {
+            return new ChainDecision(NASBackupChainKeys.TYPE_INCREMENTAL, 
bitmapNew, bitmapParent,
+                    parentPath, chainId, chainPosition);
+        }
+
+        boolean isIncremental() {
+            return NASBackupChainKeys.TYPE_INCREMENTAL.equals(mode);
+        }
+    }
+
+    /**
+     * Decides whether the next backup for {@code vm} should be a fresh full 
or an incremental
+     * appended to the existing chain. Stopped VMs are always full (libvirt 
{@code backup-begin}
+     * requires a running QEMU process). The {@code nas.backup.full.every} 
ConfigKey controls
+     * how many backups (full + incrementals) form one chain before a new full 
is forced.
+     */
+    protected ChainDecision decideChain(VirtualMachine vm) {
+        final String newBitmap = "backup-" + System.currentTimeMillis() / 
1000L;
+
+        // Stopped VMs cannot do incrementals — script will also fall back, 
but we make the
+        // decision here so we register the right type up-front.
+        if (VirtualMachine.State.Stopped.equals(vm.getState())) {
+            return ChainDecision.fullStart(newBitmap);
+        }
+
+        Integer fullEvery = NASBackupFullEvery.valueIn(vm.getDataCenterId());
+        if (fullEvery == null || fullEvery <= 1) {
+            // Disabled or every-backup-is-full mode.
+            return ChainDecision.fullStart(newBitmap);
+        }
+
+        // Walk this VM's backups newest→oldest, find the most recent BackedUp 
backup that has a
+        // bitmap stored. If we don't find one, this is the first backup in a 
chain — start full.
+        List<Backup> history = backupDao.listByVmId(vm.getDataCenterId(), 
vm.getId());
+        if (history == null || history.isEmpty()) {
+            return ChainDecision.fullStart(newBitmap);
+        }
+        history.sort(Comparator.comparing(Backup::getDate).reversed());
+
+        Backup parent = null;
+        String parentBitmap = null;
+        String parentChainId = null;
+        int parentChainPosition = -1;
+        for (Backup b : history) {
+            if (!Backup.Status.BackedUp.equals(b.getStatus())) {
+                continue;
+            }
+            String bm = readDetail(b, NASBackupChainKeys.BITMAP_NAME);
+            if (bm == null) {
+                continue;
+            }
+            parent = b;
+            parentBitmap = bm;
+            parentChainId = readDetail(b, NASBackupChainKeys.CHAIN_ID);
+            String posStr = readDetail(b, NASBackupChainKeys.CHAIN_POSITION);
+            try {
+                parentChainPosition = posStr == null ? 0 : 
Integer.parseInt(posStr);
+            } catch (NumberFormatException e) {
+                parentChainPosition = 0;
+            }
+            break;
+        }
+        if (parent == null || parentBitmap == null || parentChainId == null) {
+            return ChainDecision.fullStart(newBitmap);
+        }
+
+        // Force a fresh full when the chain has reached the configured length.
+        if (parentChainPosition + 1 >= fullEvery) {
+            return ChainDecision.fullStart(newBitmap);
+        }
+
+        // The script needs the parent backup's on-NAS file path so it can 
rebase the new
+        // qcow2 onto it. The path is stored relative to the NAS mount point — 
the script
+        // resolves it inside its mount session.
+        String parentPath = composeParentBackupPath(parent);
+        return ChainDecision.incremental(newBitmap, parentBitmap, parentPath,
+                parentChainId, parentChainPosition + 1);
+    }
+
+    private String readDetail(Backup backup, String key) {
+        BackupDetailVO d = backupDetailsDao.findDetail(backup.getId(), key);
+        return d == null ? null : d.getValue();
+    }
+
+    /**
+     * Compose the on-NAS path of a parent backup's root-disk qcow2. Relative 
to the NAS mount,
+     * matches the layout written by {@code nasbackup.sh} ({@code 
<backupPath>/root.<volUuid>.qcow2}).
+     */
+    private String composeParentBackupPath(Backup parent) {
+        // backupPath is stored as externalId by createBackupObject — e.g. 
"i-2-1234-VM/2026.04.27.13.45.00".
+        // Volume UUID for the root volume is what the script keys backup 
files on.

Review Comment:
   The backup_files for different volumes have different uuids. They all don't 
share root volume's uuid.
   
   So, we need to pass separate parent backup path's for each volume.
   
   The parent backup path consists of two parts.
   1. The directory
   2. The backup file name
   
   The directory is `<nas root>/<instance-name>/<external-id>`
   the backup file name is  `[root|datadisk].<volume path uuid>.qcow2`
   
   RestoreBackupCommand passes the directory and backup files for the volumes 
to be restored.
   We have to do the same thing and pass the directory and backup files 
belonging to the parent to the TakeBackupCommand. So you can reuse that code.
   
   ```
           List<Backup.VolumeInfo> backedVolumes = backup.getBackedUpVolumes();
           restoreCommand.setBackupPath(backup.getExternalId());
           restoreCommand.setBackupFiles(getBackupFiles(backedVolumes));
   ```
   
   In similar terms we can write
   
   ```
           List<Backup.VolumeInfo> parentBackedVolumes = 
parentBackup.getBackedUpVolumes();
           TakeBackupCommand command = new 
TakeBackupCommand(vm.getInstanceName(), backupPath);
           command.setBackupPath(backup.getExternalId());
           command.setBackupFiles(getBackupFiles(backedVolumes));
   ```
   



##########
scripts/vm/hypervisor/kvm/nasbackup.sh:
##########
@@ -324,6 +504,36 @@ while [[ $# -gt 0 ]]; do
       shift
       shift
       ;;
+    -M|--mode)
+      MODE="$2"
+      shift
+      shift
+      ;;
+    --bitmap-new)
+      BITMAP_NEW="$2"

Review Comment:
   We should set the new bitmap in the qcow2 files for stopped VMs backup also 
so that the next backup when the vm is started is incremental.
   `qemu-img bitmap -add` can be used to do 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]

Reply via email to