@@ -95,6 +95,15 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
9595 true ,
9696 BackupFrameworkEnabled .key ());
9797
98+ ConfigKey <Boolean > NASBackupCompressionEnabled = new ConfigKey <>("Advanced" , Boolean .class ,
99+ "nas.backup.compression.enabled" ,
100+ "false" ,
101+ "Enable qcow2 compression for NAS backup files. Cannot be combined with nas.backup.encryption.enabled "
102+ + "(qcow2 cannot compress and encrypt the same image); backups fail with a clear error if both are on." ,
103+ true ,
104+ ConfigKey .Scope .Zone ,
105+ BackupFrameworkEnabled .key ());
106+
98107 ConfigKey <Boolean > NASBackupIncrementalEnabled = new ConfigKey <>("Advanced" , Boolean .class ,
99108 "nas.backup.incremental.enabled" ,
100109 "false" ,
@@ -108,6 +117,39 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co
108117 ConfigKey .Scope .Zone ,
109118 BackupFrameworkEnabled .key ());
110119
120+ ConfigKey <Boolean > NASBackupEncryptionEnabled = new ConfigKey <>("Advanced" , Boolean .class ,
121+ "nas.backup.encryption.enabled" ,
122+ "false" ,
123+ "Enable LUKS encryption for NAS backup files. Cannot be combined with nas.backup.compression.enabled." ,
124+ true ,
125+ ConfigKey .Scope .Zone ,
126+ BackupFrameworkEnabled .key ());
127+
128+ ConfigKey <String > NASBackupEncryptionPassphrase = new ConfigKey <>("Secure" , String .class ,
129+ "nas.backup.encryption.passphrase" ,
130+ "" ,
131+ "Passphrase for LUKS encryption of NAS backup files. Required when encryption is enabled. Restores use the "
132+ + "current value, so changing it makes backups taken with the old passphrase unrestorable until it is set back." ,
133+ true ,
134+ ConfigKey .Scope .Zone ,
135+ BackupFrameworkEnabled .key ());
136+
137+ ConfigKey <Integer > NASBackupBandwidthLimitMbps = new ConfigKey <>("Advanced" , Integer .class ,
138+ "nas.backup.bandwidth.limit.mbps" ,
139+ "0" ,
140+ "Bandwidth limit in MiB/s for backup operations (0 = unlimited)." ,
141+ true ,
142+ ConfigKey .Scope .Zone ,
143+ BackupFrameworkEnabled .key ());
144+
145+ ConfigKey <Boolean > NASBackupIntegrityCheckEnabled = new ConfigKey <>("Advanced" , Boolean .class ,
146+ "nas.backup.integrity.check" ,
147+ "false" ,
148+ "Run qemu-img check on backup files after creation to verify integrity." ,
149+ true ,
150+ ConfigKey .Scope .Zone ,
151+ BackupFrameworkEnabled .key ());
152+
111153 ConfigKey <Integer > NASBackupFullEvery = new ConfigKey <>("Advanced" , Integer .class ,
112154 "nas.backup.full.every" ,
113155 "10" ,
@@ -298,6 +340,16 @@ protected ChainDecision decideChain(VirtualMachine vm) {
298340 return ChainDecision .legacyFull ();
299341 }
300342
343+ // Compression and LUKS encryption rewrite each backup file with qemu-img convert. On an
344+ // incremental file that either flattens it through its backing chain (a full-size copy that
345+ // defeats the chain) or leaves an encrypted child whose parent needs its own secret to open.
346+ // Until both features are chain-aware, a zone with either one enabled takes full backups.
347+ Long zoneId = vm .getDataCenterId ();
348+ if (Boolean .TRUE .equals (NASBackupCompressionEnabled .valueIn (zoneId ))
349+ || Boolean .TRUE .equals (NASBackupEncryptionEnabled .valueIn (zoneId ))) {
350+ return ChainDecision .legacyFull ();
351+ }
352+
301353 // Incremental backups rely on QEMU dirty bitmaps / libvirt checkpoints, which only exist
302354 // on file-based qcow2 storage. Storage such as Ceph-RBD and Linstor cannot carry per-disk
303355 // checkpoints, so a VM with any volume on such a pool must stay on the full-only (legacy)
@@ -585,8 +637,11 @@ public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesce
585637 // (libvirt backup-begin requires a running QEMU process).
586638 ChainDecision decision = decideChain (vm );
587639
588- BackupVO backupVO = createBackupObject (vm , backupPath , decision .isIncremental () ? "INCREMENTAL" : "FULL" );
589640 TakeBackupCommand command = new TakeBackupCommand (vm .getInstanceName (), backupPath );
641+ // Validate and apply the zone's enhancement settings before any backup row exists, so a
642+ // misconfiguration (encryption without a passphrase, compression together with encryption)
643+ // fails the request without leaving a backup stuck in BackingUp.
644+ applyBackupEnhancementDetails (command , vm .getDataCenterId ());
590645 command .setBackupRepoType (backupRepository .getType ());
591646 command .setBackupRepoAddress (backupRepository .getAddress ());
592647 command .setMountOptions (backupRepository .getMountOptions ());
@@ -605,6 +660,7 @@ public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesce
605660 command .setVolumePaths (volumePoolsAndPaths .second ());
606661 }
607662
663+ BackupVO backupVO = createBackupObject (vm , backupPath , decision .isIncremental () ? "INCREMENTAL" : "FULL" );
608664 BackupAnswer answer ;
609665 try {
610666 answer = (BackupAnswer ) agentManager .send (host .getId (), command );
@@ -669,6 +725,39 @@ public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesce
669725 }
670726 }
671727
728+ /**
729+ * Translates the zone-scoped backup-enhancement settings (compression, encryption,
730+ * bandwidth limit, integrity check) into details on the {@link TakeBackupCommand}.
731+ * Fails fast, before any backup row is created, if encryption is enabled without a configured
732+ * passphrase or together with compression (qcow2 cannot compress and encrypt the same image).
733+ */
734+ protected void applyBackupEnhancementDetails (TakeBackupCommand command , Long zoneId ) {
735+ if (Boolean .TRUE .equals (NASBackupCompressionEnabled .valueIn (zoneId ))
736+ && Boolean .TRUE .equals (NASBackupEncryptionEnabled .valueIn (zoneId ))) {
737+ throw new CloudRuntimeException (String .format ("%s and %s are both enabled for zone %d, but qcow2 cannot compress and "
738+ + "encrypt the same image. Disable one of them for this zone; no backup was taken." ,
739+ NASBackupCompressionEnabled .key (), NASBackupEncryptionEnabled .key (), zoneId ));
740+ }
741+ if (Boolean .TRUE .equals (NASBackupCompressionEnabled .valueIn (zoneId ))) {
742+ command .addDetail (TakeBackupCommand .DETAIL_COMPRESSION , "true" );
743+ }
744+ if (Boolean .TRUE .equals (NASBackupEncryptionEnabled .valueIn (zoneId ))) {
745+ String passphrase = NASBackupEncryptionPassphrase .valueIn (zoneId );
746+ if (passphrase == null || passphrase .isEmpty ()) {
747+ throw new CloudRuntimeException ("NAS backup encryption is enabled but no passphrase is configured (nas.backup.encryption.passphrase)" );
748+ }
749+ command .addDetail (TakeBackupCommand .DETAIL_ENCRYPTION , "true" );
750+ command .addDetail (TakeBackupCommand .DETAIL_ENCRYPTION_PASSPHRASE , passphrase );
751+ }
752+ Integer bandwidthLimit = NASBackupBandwidthLimitMbps .valueIn (zoneId );
753+ if (bandwidthLimit != null && bandwidthLimit > 0 ) {
754+ command .addDetail (TakeBackupCommand .DETAIL_BANDWIDTH_LIMIT , String .valueOf (bandwidthLimit ));
755+ }
756+ if (Boolean .TRUE .equals (NASBackupIntegrityCheckEnabled .valueIn (zoneId ))) {
757+ command .addDetail (TakeBackupCommand .DETAIL_INTEGRITY_CHECK , "true" );
758+ }
759+ }
760+
672761 /**
673762 * Alerts when nasbackup.sh could not clean up after a failed backup. The script's output
674763 * names the backup repository mount point it left on the host.
@@ -706,6 +795,19 @@ private BackupVO createBackupObject(VirtualMachine vm, String backupPath, String
706795 return backupDao .persist (backup );
707796 }
708797
798+ /**
799+ * Restore-side counterpart of {@link #applyBackupEnhancementDetails}: hands the zone's LUKS passphrase
800+ * to the host so encrypted backup files can be checked and converted back. It is sent whenever a
801+ * passphrase is configured, not only while encryption is switched on, so backups taken before
802+ * encryption was disabled stay restorable; the host ignores it for plain backups.
803+ */
804+ protected void applyRestoreEncryptionDetails (RestoreBackupCommand command , Long zoneId ) {
805+ String passphrase = NASBackupEncryptionPassphrase .valueIn (zoneId );
806+ if (passphrase != null && !passphrase .isEmpty ()) {
807+ command .setEncryptionPassphrase (passphrase );
808+ }
809+ }
810+
709811 @ Override
710812 public Pair <Boolean , String > restoreBackupToVM (VirtualMachine vm , Backup backup , String hostIp , String dataStoreUuid ) {
711813 return restoreVMBackup (vm , backup );
@@ -745,6 +847,7 @@ private Pair<Boolean, String> restoreVMBackup(VirtualMachine vm, Backup backup)
745847 restoreCommand .setVmExists (vm .getRemoved () == null );
746848 restoreCommand .setVmState (vm .getState ());
747849 restoreCommand .setMountTimeout (NASBackupRestoreMountTimeout .value ());
850+ applyRestoreEncryptionDetails (restoreCommand , vm .getDataCenterId ());
748851
749852 BackupAnswer answer ;
750853 try {
@@ -870,6 +973,7 @@ public Pair<Boolean, String> restoreBackedUpVolume(Backup backup, Backup.VolumeI
870973 restoreCommand .setVmState (vmNameAndState .second ());
871974 restoreCommand .setMountTimeout (NASBackupRestoreMountTimeout .value ());
872975 restoreCommand .setBackupFiles (Collections .singletonList (matchingVolume .getPath ()));
976+ applyRestoreEncryptionDetails (restoreCommand , backup .getZoneId ());
873977
874978 BackupAnswer answer ;
875979 try {
@@ -1281,6 +1385,11 @@ public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) {
12811385 public ConfigKey <?>[] getConfigKeys () {
12821386 return new ConfigKey []{
12831387 NASBackupRestoreMountTimeout ,
1388+ NASBackupCompressionEnabled ,
1389+ NASBackupEncryptionEnabled ,
1390+ NASBackupEncryptionPassphrase ,
1391+ NASBackupBandwidthLimitMbps ,
1392+ NASBackupIntegrityCheckEnabled ,
12841393 NASBackupFullEvery ,
12851394 NASBackupIncrementalEnabled ,
12861395 NASBackupQuiesceAgentTimeout
0 commit comments