@xen-orchestra/backups 0.54.2 → 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.
- package/HealthCheckVmBackup.mjs +2 -2
- package/ImportVmBackup.mjs +2 -1
- package/RemoteAdapter.mjs +2 -1
- package/_getOldEntries.mjs +134 -2
- package/_incrementalVm.mjs +19 -0
- package/_runners/_vmRunners/IncrementalXapi.mjs +4 -2
- package/_runners/_writers/FullRemoteWriter.mjs +2 -1
- package/_runners/_writers/IncrementalRemoteWriter.mjs +3 -1
- package/package.json +7 -6
package/HealthCheckVmBackup.mjs
CHANGED
|
@@ -59,8 +59,8 @@ export class HealthCheckVmBackup {
|
|
|
59
59
|
const running = new Date()
|
|
60
60
|
remainingTimeout -= running - started
|
|
61
61
|
|
|
62
|
-
// wait for the guest
|
|
63
|
-
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.
|
|
62
|
+
// wait for the guest tools to be detected
|
|
63
|
+
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_detected, {
|
|
64
64
|
timeout: remainingTimeout,
|
|
65
65
|
timeoutMessage: refOrUuid =>
|
|
66
66
|
`timeout reached while waiting for ${refOrUuid} to report the driver version through the Xen tools. Please check or update the Xen tools.`,
|
package/ImportVmBackup.mjs
CHANGED
|
@@ -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
|
|
package/_getOldEntries.mjs
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
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
|
}
|
package/_incrementalVm.mjs
CHANGED
|
@@ -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),
|
|
@@ -91,9 +91,11 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
91
91
|
'writer.updateUuidAndChain()'
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
if (
|
|
94
|
+
if (isFull) {
|
|
95
|
+
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, 0)
|
|
96
|
+
} else {
|
|
95
97
|
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, (this._deltaChainLength ?? 0) + 1)
|
|
96
|
-
}
|
|
98
|
+
}
|
|
97
99
|
|
|
98
100
|
// not the case if offlineBackup
|
|
99
101
|
if (exportedVm.is_a_snapshot) {
|
|
@@ -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.
|
|
11
|
+
"version": "0.55.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"@vates/decorate-with": "^2.1.0",
|
|
26
26
|
"@vates/disposable": "^0.1.6",
|
|
27
27
|
"@vates/fuse-vhd": "^2.1.2",
|
|
28
|
-
"@vates/nbd-client": "^3.1.
|
|
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.
|
|
32
|
-
"@xen-orchestra/log": "^0.7.
|
|
31
|
+
"@xen-orchestra/fs": "^4.3.0",
|
|
32
|
+
"@xen-orchestra/log": "^0.7.1",
|
|
33
33
|
"@xen-orchestra/template": "^0.1.0",
|
|
34
34
|
"app-conf": "^3.0.0",
|
|
35
35
|
"compare-versions": "^6.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.
|
|
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.
|
|
62
|
+
"@xen-orchestra/xapi": "^7.8.0"
|
|
62
63
|
},
|
|
63
64
|
"license": "AGPL-3.0-or-later",
|
|
64
65
|
"author": {
|