From 2ce6b05b5a96ebcd34f378f091e495d4ad2dd183 Mon Sep 17 00:00:00 2001 From: Pierre Donias Date: Thu, 28 Nov 2024 12:34:19 +0100 Subject: [PATCH] feat(xo-web/backup): long-term retention settings (#8141) --- CHANGELOG.unreleased.md | 1 + packages/xo-server/src/api/backup-ng.mjs | 8 ++ packages/xo-web/src/common/intl/messages.js | 5 ++ .../xo-web/src/xo-app/backup/new/index.js | 87 ++++++++++++++++++- 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index b654a45b0de..89f13944b59 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)) - [VM/Advanced] Add ability to block/unblock migration (PR [#8129](https://github.com/vatesfr/xen-orchestra/pull/8129)) ### 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 b56c2af25d6..aa3d7d8a049 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} + /> + + + + )}