@xen-orchestra/backups 0.54.3 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ export class ImportVmBackup {
58
58
  async _reuseNearestSnapshot($defer, ignoredVdis) {
59
59
  const metadata = this._metadata
60
60
  const { mapVdisSrs } = this._importIncrementalVmSettings
61
- const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
61
+ const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
62
62
  const streams = {}
63
63
  const metdataDir = dirname(metadata._filename)
64
64
  const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
@@ -191,6 +191,7 @@ export class ImportVmBackup {
191
191
  version: '1.0.0',
192
192
  vifs,
193
193
  vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
194
+ vtpms,
194
195
  }
195
196
  }
196
197
 
package/RemoteAdapter.mjs CHANGED
@@ -744,7 +744,7 @@ export class RemoteAdapter {
744
744
 
745
745
  async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
746
746
  const handler = this._handler
747
- const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
747
+ const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
748
748
  const dir = dirname(metadata._filename)
749
749
  const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
750
750
 
@@ -760,6 +760,7 @@ export class RemoteAdapter {
760
760
  version: '1.0.0',
761
761
  vifs,
762
762
  vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
763
+ vtpms,
763
764
  }
764
765
  }
765
766
 
@@ -1,4 +1,136 @@
1
+ import moment from 'moment-timezone'
2
+ import assert from 'node:assert'
3
+
4
+ function instantiateTimezonedDateCreator(timezone) {
5
+ return date => {
6
+ const transformed = timezone ? moment.tz(date, timezone) : moment(date)
7
+ assert.ok(transformed.isValid(), `date ${date} , timezone ${timezone} is invalid`)
8
+ return transformed
9
+ }
10
+ }
11
+
12
+ const LTR_DEFINITIONS = {
13
+ daily: {
14
+ makeDateFormatter: ({ firstHourOfTheDay = 0, dateCreator } = {}) => {
15
+ return date => {
16
+ const copy = dateCreator(date)
17
+ copy.hour(copy.hour() - firstHourOfTheDay)
18
+ return copy.format('YYYY-MM-DD')
19
+ }
20
+ },
21
+ },
22
+ weekly: {
23
+ makeDateFormatter: ({ firstDayOfWeek = 0 /* relative to timezone week start */, dateCreator } = {}) => {
24
+ return date => {
25
+ const copy = dateCreator(date)
26
+
27
+ copy.date(date.date() - firstDayOfWeek)
28
+ // warning, the year in term of week may different from YYYY
29
+ // since the computation of the first week of a year is timezone dependant
30
+ return copy.format('gggg-WW')
31
+ }
32
+ },
33
+ ancestor: 'daily',
34
+ },
35
+ monthly: {
36
+ makeDateFormatter: ({ firstDayOfMonth = 0, dateCreator } = {}) => {
37
+ return date => {
38
+ const copy = dateCreator(date)
39
+ copy.date(copy.date() - firstDayOfMonth)
40
+ return copy.format('YYYY-MM')
41
+ }
42
+ },
43
+ ancestor: 'weekly',
44
+ },
45
+ yearly: {
46
+ makeDateFormatter: ({ firstDayOfYear = 0, dateCreator } = {}) => {
47
+ return date => {
48
+ const copy = dateCreator(date)
49
+ copy.date(copy.date() - firstDayOfYear)
50
+ return copy.format('YYYY')
51
+ }
52
+ },
53
+ ancestor: 'monthly',
54
+ },
55
+ }
56
+
1
57
  // returns all entries but the last retention-th
2
- export function getOldEntries(retention, entries) {
3
- return entries === undefined ? [] : retention > 0 ? entries.slice(0, -retention) : entries
58
+ /**
59
+ * return the entries too old to be kept
60
+ * if multiple entries are i the same time bucket : keep only the most recent one
61
+ * if an entry is valid in any of the bucket OR the minRetentionCount : keep it
62
+ * if a bucket is completly empty : it does not count as one, thus it may extend the retention
63
+ * @returns Array<Backup>
64
+ */
65
+ export function getOldEntries(minRetentionCount, entries, { longTermRetention = {}, timezone } = {}) {
66
+ assert.strictEqual(
67
+ typeof minRetentionCount,
68
+ 'number',
69
+ `minRetentionCount must be a number, got ${JSON.stringify(minRetentionCount)}`
70
+ )
71
+ assert.strictEqual(
72
+ minRetentionCount >= 0,
73
+ true,
74
+ `minRetentionCount must be a positive number, got ${JSON.stringify(minRetentionCount)}`
75
+ )
76
+ const dateBuckets = {}
77
+ const dateCreator = instantiateTimezonedDateCreator(timezone)
78
+ // only check buckets that have a retention set
79
+ for (const [duration, { retention, settings }] of Object.entries(longTermRetention)) {
80
+ assert.notStrictEqual(LTR_DEFINITIONS[duration], undefined, `Retention of type ${duration} is not defined`)
81
+ assert.notStrictEqual(
82
+ timezone,
83
+ undefined,
84
+ `timezone must defined for ltr, got ${JSON.stringify({ minRetentionCount, longTermRetention, timezone })}`
85
+ )
86
+ assert.strictEqual(
87
+ typeof retention,
88
+ 'number',
89
+ `retention of type ${duration} must be a number, got ${JSON.stringify(retention)}`
90
+ )
91
+ assert.strictEqual(
92
+ retention > 0,
93
+ true,
94
+ `retention of type ${duration} must be a positive number, got ${JSON.stringify(retention)}`
95
+ )
96
+ dateBuckets[duration] = {
97
+ remaining: retention,
98
+ lastMatchingBucket: null,
99
+ formatter: LTR_DEFINITIONS[duration].makeDateFormatter({ ...settings, dateCreator }),
100
+ }
101
+ }
102
+ const nb = entries.length
103
+ const oldEntries = []
104
+
105
+ for (let i = nb - 1; i >= 0; i--) {
106
+ const entry = entries[i]
107
+ const entryDate = dateCreator(entry.timestamp)
108
+ let shouldBeKept = false
109
+ for (const [duration, { remaining, lastMatchingBucket, formatter }] of Object.entries(dateBuckets)) {
110
+ if (remaining === 0) {
111
+ continue
112
+ }
113
+ const bucket = formatter(entryDate)
114
+ if (lastMatchingBucket !== bucket) {
115
+ if (lastMatchingBucket !== null) {
116
+ assert.strictEqual(
117
+ lastMatchingBucket > bucket,
118
+ true,
119
+ `entries must be sorted in asc order ${lastMatchingBucket} ${bucket}`
120
+ )
121
+ }
122
+ shouldBeKept = true
123
+ dateBuckets[duration].remaining -= 1
124
+ dateBuckets[duration].lastMatchingBucket = bucket
125
+ }
126
+ }
127
+ if (i >= nb - minRetentionCount) {
128
+ shouldBeKept = true
129
+ }
130
+ if (!shouldBeKept) {
131
+ oldEntries.push(entry)
132
+ }
133
+ }
134
+ // we expect the entries to be in the right order
135
+ return oldEntries.reverse()
4
136
  }
@@ -104,6 +104,18 @@ export async function exportIncrementalVm(
104
104
  }
105
105
  })
106
106
 
107
+ const vtpms = await Promise.all(
108
+ vm.$VTPMs.map(async vtpm => {
109
+ let content
110
+ try {
111
+ content = await vm.$xapi.call('VTPM.get_contents', vtpm.$ref)
112
+ } catch (err) {
113
+ console.error(err)
114
+ }
115
+ return content
116
+ })
117
+ )
118
+
107
119
  return Object.defineProperty(
108
120
  {
109
121
  version: '1.1.0',
@@ -113,6 +125,7 @@ export async function exportIncrementalVm(
113
125
  vm: {
114
126
  ...vm,
115
127
  },
128
+ vtpms,
116
129
  },
117
130
  'streams',
118
131
  {
@@ -281,6 +294,12 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
281
294
  }
282
295
  }),
283
296
  ])
297
+ // recreate VTPMs
298
+ await Promise.all(
299
+ (incrementalVm.vtpms ?? []).map(async contents => {
300
+ await xapi.VTPM_create({ VM: vmRef, contents })
301
+ })
302
+ )
284
303
 
285
304
  await Promise.all([
286
305
  incrementalVm.vm.ha_always_run && xapi.setField('VM', vmRef, 'ha_always_run', true),
@@ -38,7 +38,8 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
38
38
 
39
39
  const oldBackups = getOldEntries(
40
40
  settings.exportRetention - 1,
41
- await adapter.listVmBackups(vm.uuid, _ => _.mode === 'full' && _.scheduleId === scheduleId)
41
+ await adapter.listVmBackups(vm.uuid, _ => _.mode === 'full' && _.scheduleId === scheduleId),
42
+ { longTermRetention: settings.longTermRetention, timezone: settings.timezone }
42
43
  )
43
44
  const deleteOldBackups = () => adapter.deleteFullVmBackups(oldBackups)
44
45
 
@@ -95,7 +95,8 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
95
95
 
96
96
  const oldEntries = getOldEntries(
97
97
  settings.exportRetention - 1,
98
- await adapter.listVmBackups(vmUuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
98
+ await adapter.listVmBackups(vmUuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId),
99
+ { longTermRetention: settings.longTermRetention, timezone: settings.timezone }
99
100
  )
100
101
  this._oldEntries = oldEntries
101
102
 
@@ -220,6 +221,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
220
221
  vhds,
221
222
  vm,
222
223
  vmSnapshot,
224
+ vtpms: deltaExport.vtpms,
223
225
  }
224
226
 
225
227
  const { size } = await Task.run({ name: 'transfer' }, async () => {
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/vatesfr/xen-orchestra.git"
10
10
  },
11
- "version": "0.54.3",
11
+ "version": "0.55.0",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -28,7 +28,7 @@
28
28
  "@vates/nbd-client": "^3.1.2",
29
29
  "@vates/parse-duration": "^0.1.1",
30
30
  "@xen-orchestra/async-map": "^0.1.2",
31
- "@xen-orchestra/fs": "^4.2.1",
31
+ "@xen-orchestra/fs": "^4.3.0",
32
32
  "@xen-orchestra/log": "^0.7.1",
33
33
  "@xen-orchestra/template": "^0.1.0",
34
34
  "app-conf": "^3.0.0",
@@ -39,6 +39,7 @@
39
39
  "human-format": "^1.2.0",
40
40
  "limit-concurrency-decorator": "^0.6.0",
41
41
  "lodash": "^4.17.20",
42
+ "moment-timezone": "^0.5.46",
42
43
  "ms": "^2.1.3",
43
44
  "node-zone": "^0.4.0",
44
45
  "parse-pairs": "^2.0.0",
@@ -47,7 +48,7 @@
47
48
  "tar": "^6.1.15",
48
49
  "uuid": "^9.0.0",
49
50
  "value-matcher": "^0.2.0",
50
- "vhd-lib": "^4.11.1",
51
+ "vhd-lib": "^4.11.2",
51
52
  "xen-api": "^4.5.0",
52
53
  "yazl": "^2.5.1"
53
54
  },
@@ -58,7 +59,7 @@
58
59
  "tmp": "^0.2.1"
59
60
  },
60
61
  "peerDependencies": {
61
- "@xen-orchestra/xapi": "^7.7.1"
62
+ "@xen-orchestra/xapi": "^7.8.0"
62
63
  },
63
64
  "license": "AGPL-3.0-or-later",
64
65
  "author": {