diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index a26e481f855..8d49e3d5f4a 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -17,6 +17,7 @@
- Add 404 page (PR [#8145](https://github.com/vatesfr/xen-orchestra/pull/8145))
- [backups] Handle VTPM content on incremental backup/replication/restore, including differential restore (PR [#8139](https://github.com/vatesfr/xen-orchestra/pull/8139))
- [Host/Advanced] Allow bypassing blocked migration in maintenance mode (PR [#8149](https://github.com/vatesfr/xen-orchestra/pull/8149))
+- [Backup] Long-term retention (GFS) (PRs [#7999](https://github.com/vatesfr/xen-orchestra/pull/7999) [#8141](https://github.com/vatesfr/xen-orchestra/pull/8141))
### Bug fixes
diff --git a/packages/xo-server/src/api/backup-ng.mjs b/packages/xo-server/src/api/backup-ng.mjs
index ef8f44ace2c..8c554b64a70 100644
--- a/packages/xo-server/src/api/backup-ng.mjs
+++ b/packages/xo-server/src/api/backup-ng.mjs
@@ -21,6 +21,10 @@ const SCHEMA_SETTINGS = {
minimum: 0,
optional: true,
},
+ longTermRetention: {
+ type: 'object',
+ optional: true,
+ },
maxExportRate: {
type: 'number',
minimum: 0,
@@ -40,6 +44,10 @@ const SCHEMA_SETTINGS = {
type: 'boolean',
optional: true,
},
+ timezone: {
+ type: 'string',
+ optional: true,
+ },
},
additionalProperties: true,
},
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index 3e538bcb989..810d45c848f 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -623,6 +623,11 @@ const messages = {
cbtDestroySnapshotDataDisabledInformation:
'Snapshot data can be purged only when NBD is enabled and rolling snapshot is not used',
shorterBackupReports: 'Shorter backup reports',
+ longTermRetention: 'Long-term retention of backups',
+ numberOfDailyBackupsKept: 'Number of daily backups kept',
+ numberOfWeeklyBackupsKept: 'Number of weekly backups kept',
+ numberOfMonthlyBackupsKept: 'Number of monthly backups kept',
+ numberOfYearlyBackupsKept: 'Number of yearly backups kept',
// ------ New Remote -----
newRemote: 'New file system remote',
diff --git a/packages/xo-web/src/xo-app/backup/new/index.js b/packages/xo-web/src/xo-app/backup/new/index.js
index e34c0c7c7b6..7d62c930932 100644
--- a/packages/xo-web/src/xo-app/backup/new/index.js
+++ b/packages/xo-web/src/xo-app/backup/new/index.js
@@ -53,12 +53,13 @@ export NewSequence from './sequence'
// ===================================================================
const DEFAULT_RETENTION = 1
+const DEFAULT_TIMEZONE = moment.tz.guess()
const DEFAULT_SCHEDULE = {
copyRetention: DEFAULT_RETENTION,
exportRetention: DEFAULT_RETENTION,
snapshotRetention: DEFAULT_RETENTION,
cron: '0 0 * * *',
- timezone: moment.tz.guess(),
+ timezone: DEFAULT_TIMEZONE,
}
const RETENTION_LIMIT = 50
@@ -273,10 +274,20 @@ const New = decorate([
}
}
+ if (settings[''] === undefined) {
+ settings[''] = { __proto__: null }
+ }
+
+ if (!state.backupMode && !state.deltaMode) {
+ delete settings[''].longTermRetention
+ }
+
if (settings['']?.maxExportRate <= 0) {
settings[''].maxExportRate = undefined
}
+ settings[''].timezone = DEFAULT_TIMEZONE
+
await createBackupNgJob({
name: state.name,
mode: state.isDelta ? 'delta' : 'full',
@@ -351,10 +362,20 @@ const New = decorate([
snapshotMode: state.snapshotMode,
}).toObject()
+ if (normalizedSettings[''] === undefined) {
+ normalizedSettings[''] = { __proto__: null }
+ }
+
+ if (!state.backupMode && !state.deltaMode) {
+ delete normalizedSettings[''].longTermRetention
+ }
+
if (normalizedSettings['']?.maxExportRate <= 0) {
normalizedSettings[''].maxExportRate = undefined
}
+ normalizedSettings[''].timezone = DEFAULT_TIMEZONE
+
await editBackupNgJob({
id: props.job.id,
name: state.name,
@@ -599,6 +620,18 @@ const New = decorate([
reportRecipients: (reportRecipients.splice(key, 1), reportRecipients),
})
},
+ setLongTermRetention({ setGlobalSettings }, retention, granularity) {
+ const { propSettings, settings = propSettings } = this.state
+ const longTermRetention = settings.getIn(['', 'longTermRetention']) ?? {}
+
+ if (retention > 0) {
+ longTermRetention[granularity] = { retention, settings: {} } // settings will be used for advanced configuration in the future
+ } else {
+ delete longTermRetention[granularity]
+ }
+
+ setGlobalSettings({ longTermRetention: isEmpty(longTermRetention) ? undefined : longTermRetention })
+ },
setReportWhen:
({ setGlobalSettings }, { value }) =>
() => {
@@ -679,6 +712,10 @@ const New = decorate([
inputNRetriesVmBackupFailures: generateId,
inputBackupReportTplId: generateId,
inputTimeoutId: generateId,
+ inputLongTermRetentionDaily: generateId,
+ inputLongTermRetentionWeekly: generateId,
+ inputLongTermRetentionMonthly: generateId,
+ inputLongTermRetentionYearly: generateId,
// In order to keep the user preference, the offline backup is kept in the DB
// and it's considered active only when the full mode is enabled
@@ -789,6 +826,7 @@ const New = decorate([
checkpointSnapshot,
concurrency,
fullInterval,
+ longTermRetention = {},
maxExportRate,
nbdConcurrency = 1,
nRetriesVmBackupFailures = 0,
@@ -1244,6 +1282,53 @@ const New = decorate([
+ {(state.backupMode || state.deltaMode) && (
+
+ {_('longTermRetention')}
+
+
+
+ effects.setLongTermRetention(value, 'daily')}
+ value={longTermRetention.daily?.retention}
+ />
+
+
+
+ effects.setLongTermRetention(value, 'weekly')}
+ value={longTermRetention.weekly?.retention}
+ />
+
+
+
+ effects.setLongTermRetention(value, 'monthly')}
+ value={longTermRetention.monthly?.retention}
+ />
+
+
+
+ effects.setLongTermRetention(value, 'yearly')}
+ value={longTermRetention.yearly?.retention}
+ />
+
+
+
+ )}