@xen-orchestra/backups 0.54.0 → 0.54.1
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/_cleanVm.mjs +15 -3
- package/_incrementalVm.mjs +26 -8
- package/_runners/_vmRunners/IncrementalRemote.mjs +19 -8
- package/_runners/_vmRunners/_AbstractXapi.mjs +21 -4
- package/_runners/_writers/IncrementalRemoteWriter.mjs +4 -1
- package/_runners/_writers/IncrementalXapiWriter.mjs +10 -7
- package/package.json +9 -9
package/_cleanVm.mjs
CHANGED
|
@@ -121,7 +121,19 @@ export async function checkAliases(
|
|
|
121
121
|
) {
|
|
122
122
|
const aliasFound = []
|
|
123
123
|
for (const alias of aliasPaths) {
|
|
124
|
-
|
|
124
|
+
let target
|
|
125
|
+
try {
|
|
126
|
+
target = await resolveVhdAlias(handler, alias)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err.code === 'ENOENT') {
|
|
129
|
+
logWarn('missing target of alias', { alias })
|
|
130
|
+
if (remove) {
|
|
131
|
+
logInfo('removing alias and non VHD target', { alias, target })
|
|
132
|
+
await handler.unlink(target)
|
|
133
|
+
await handler.unlink(alias)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
125
137
|
|
|
126
138
|
if (!isVhdFile(target)) {
|
|
127
139
|
logWarn('alias references non VHD target', { alias, target })
|
|
@@ -201,9 +213,9 @@ export async function cleanVm(
|
|
|
201
213
|
|
|
202
214
|
// remove broken VHDs
|
|
203
215
|
await asyncMap(vhds, async path => {
|
|
204
|
-
if(removeTmp && basename(path)[0] === '.'){
|
|
216
|
+
if (removeTmp && basename(path)[0] === '.') {
|
|
205
217
|
logInfo('deleting temporary VHD', { path })
|
|
206
|
-
|
|
218
|
+
return VhdAbstract.unlink(handler, path)
|
|
207
219
|
}
|
|
208
220
|
try {
|
|
209
221
|
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
package/_incrementalVm.mjs
CHANGED
|
@@ -52,14 +52,32 @@ export async function exportIncrementalVm(
|
|
|
52
52
|
$snapshot_of$uuid: vdi.$snapshot_of?.uuid,
|
|
53
53
|
$SR$uuid: vdi.$SR.uuid,
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
try {
|
|
56
|
+
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
|
|
57
|
+
baseRef: baseVdi?.$ref,
|
|
58
|
+
cancelToken,
|
|
59
|
+
format: 'vhd',
|
|
60
|
+
nbdConcurrency,
|
|
61
|
+
preferNbd,
|
|
62
|
+
})
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err.code === 'VDI_CANT_DO_DELTA') {
|
|
65
|
+
// fall back to a base
|
|
66
|
+
Task.info(`Can't do delta, will try to get a full stream`, { vdi })
|
|
67
|
+
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
|
|
68
|
+
cancelToken,
|
|
69
|
+
format: 'vhd',
|
|
70
|
+
nbdConcurrency,
|
|
71
|
+
preferNbd,
|
|
72
|
+
})
|
|
73
|
+
// only warn if the fall back succeed
|
|
74
|
+
Task.warning(`Can't do delta with this vdi, transfer will be a full`, {
|
|
75
|
+
vdi,
|
|
76
|
+
})
|
|
77
|
+
} else {
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
63
81
|
})
|
|
64
82
|
|
|
65
83
|
const suspendVdi = vm.$suspend_VDI
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from '@xen-orchestra/log'
|
|
2
|
+
|
|
1
3
|
import { asyncEach } from '@vates/async-each'
|
|
2
4
|
import assert from 'node:assert'
|
|
3
5
|
import * as UUID from 'uuid'
|
|
@@ -11,6 +13,7 @@ import { Disposable } from 'promise-toolbox'
|
|
|
11
13
|
import { openVhd } from 'vhd-lib'
|
|
12
14
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
13
15
|
|
|
16
|
+
const { warn } = createLogger('xo:backups:Incrementalremote')
|
|
14
17
|
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
15
18
|
_getRemoteWriter() {
|
|
16
19
|
return IncrementalRemoteWriter
|
|
@@ -46,11 +49,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
46
49
|
})
|
|
47
50
|
|
|
48
51
|
const presentBaseVdis = new Map(baseUuidToSrcVdi)
|
|
49
|
-
await this._callWriters(
|
|
50
|
-
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
|
|
51
|
-
'writer.checkBaseVdis()',
|
|
52
|
-
false
|
|
53
|
-
)
|
|
52
|
+
await this._callWriters(writer => writer.checkBaseVdis(presentBaseVdis), 'writer.checkBaseVdis()', false)
|
|
54
53
|
// check if the parent vdi are present in all the remotes
|
|
55
54
|
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
|
|
56
55
|
if (!presentBaseVdis.has(baseUuid)) {
|
|
@@ -64,17 +63,29 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
64
63
|
|
|
65
64
|
for (const metadata of transferList) {
|
|
66
65
|
assert.strictEqual(metadata.mode, 'delta')
|
|
67
|
-
await this._selectBaseVm(metadata)
|
|
68
|
-
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
69
66
|
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
|
|
70
67
|
useChain: false,
|
|
71
68
|
})
|
|
72
|
-
|
|
69
|
+
// don't trust metadata too much
|
|
70
|
+
// recompute if it's a base backup
|
|
71
|
+
// recompute if disks are differencing or not
|
|
73
72
|
const isVhdDifferencing = {}
|
|
74
73
|
|
|
75
74
|
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
|
|
76
75
|
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
|
|
77
76
|
})
|
|
77
|
+
const hasDifferencingDisk = Object.values(isVhdDifferencing).includes(true)
|
|
78
|
+
if (metadata.isBase === hasDifferencingDisk) {
|
|
79
|
+
warn(`Metadata isBase and real disk value are different`, {
|
|
80
|
+
metadataIsBase: metadata.isBase,
|
|
81
|
+
diskIsBase: !hasDifferencingDisk,
|
|
82
|
+
isVhdDifferencing,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
metadata.isBase = !hasDifferencingDisk
|
|
86
|
+
metadata.isVhdDifferencing = isVhdDifferencing
|
|
87
|
+
await this._selectBaseVm(metadata)
|
|
88
|
+
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
78
89
|
|
|
79
90
|
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
|
|
80
91
|
await this._callWriters(
|
|
@@ -203,7 +203,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
203
203
|
for (const srcVdi of srcVdis) {
|
|
204
204
|
const snapshots = await xapi.getRecords('VDI', srcVdi.snapshots)
|
|
205
205
|
for (const snapshot of snapshots) {
|
|
206
|
-
|
|
206
|
+
// only keep the snapshot related to this backup job
|
|
207
|
+
// and only if the job is still using purge snapshot data or if the disk
|
|
208
|
+
// is not a cbt metadata disk ( expect a type: user for normal disks)
|
|
209
|
+
if (
|
|
210
|
+
snapshot.other_config[JOB_ID] === jobId &&
|
|
211
|
+
(this._settings.cbtDestroySnapshotData || snapshot.type !== 'cbt_metadata')
|
|
212
|
+
) {
|
|
207
213
|
this._jobSnapshotVdis.push(snapshot)
|
|
208
214
|
}
|
|
209
215
|
}
|
|
@@ -219,6 +225,14 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
219
225
|
await xapi.barrier()
|
|
220
226
|
// ensure cached object are up to date
|
|
221
227
|
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
|
|
228
|
+
|
|
229
|
+
// get the datetime of the most recent snapshot
|
|
230
|
+
const lastSnapshotDateTime = this._jobSnapshotVdis
|
|
231
|
+
.map(({ other_config }) => other_config[DATETIME])
|
|
232
|
+
.sort()
|
|
233
|
+
.pop()
|
|
234
|
+
|
|
235
|
+
// remove older snapshot schedule per schedule
|
|
222
236
|
const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
|
|
223
237
|
await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
224
238
|
const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
|
|
@@ -231,10 +245,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
231
245
|
...allSettings[scheduleId],
|
|
232
246
|
...allSettings[this._vm.uuid],
|
|
233
247
|
}
|
|
234
|
-
|
|
235
|
-
const minRetention = this.job.mode === 'delta' ? 1 : 0
|
|
236
|
-
const retention = Math.max(settings.snapshotRetention ?? 0, minRetention)
|
|
248
|
+
const retention = settings.snapshotRetention ?? 0
|
|
237
249
|
await asyncMap(getOldEntries(retention, datetimes), async datetime => {
|
|
250
|
+
// keep the last snapshot across all schedules for delta
|
|
251
|
+
// since we'll need it to compute delta for next backup
|
|
252
|
+
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
238
255
|
const vdis = snapshotPerDatetime[datetime]
|
|
239
256
|
let vmRef
|
|
240
257
|
// if there is an attached VM => destroy the VM (Non CBT backups)
|
|
@@ -66,7 +66,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
66
66
|
|
|
67
67
|
async beforeBackup() {
|
|
68
68
|
await super.beforeBackup()
|
|
69
|
-
return this._cleanVm({ merge: true })
|
|
69
|
+
return this._cleanVm({ merge: true, remove: true })
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
prepare({ isFull }) {
|
|
@@ -149,6 +149,9 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
149
149
|
assert.notStrictEqual(parentPath, undefined, 'A differential VHD must have a parent')
|
|
150
150
|
// forbid any kind of loop
|
|
151
151
|
assert.ok(basename(parentPath) < basename(path), `vhd must be sorted to be chained`)
|
|
152
|
+
// re-chainVhd is mandatory
|
|
153
|
+
// since the parent may be a alias or not
|
|
154
|
+
// and the child may be the other
|
|
152
155
|
await chainVhd(handler, parentPath, handler, path)
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -15,11 +15,15 @@ import assert from 'node:assert'
|
|
|
15
15
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
16
16
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
17
17
|
const sr = this._sr
|
|
18
|
+
if (baseUuidToSrcVdi.size === 0) {
|
|
19
|
+
// searching for the vdis is expensive
|
|
20
|
+
// don't do it if there is nothing to find
|
|
21
|
+
return
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
// @todo use an index if possible
|
|
20
25
|
// @todo : this seems similare to decorateVmMetadata
|
|
21
|
-
|
|
22
|
-
const replicatedVdis = sr.$VDIs
|
|
26
|
+
const replicatedVdis = sr.$VDIs
|
|
23
27
|
.filter(vdi => {
|
|
24
28
|
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
25
29
|
return baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
|
|
@@ -103,11 +107,10 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
103
107
|
.filter(_ => !!_)
|
|
104
108
|
// @todo use index ?
|
|
105
109
|
|
|
106
|
-
const replicatedVdis = sr.$VDIs
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
110
|
+
const replicatedVdis = sr.$VDIs.filter(vdi => {
|
|
111
|
+
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
112
|
+
return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
|
|
113
|
+
})
|
|
111
114
|
|
|
112
115
|
Object.values(backup.vdis).forEach(vdi => {
|
|
113
116
|
vdi.other_config[COPY_OF] = vdi.uuid
|
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.
|
|
11
|
+
"version": "0.54.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"@vates/cached-dns.lookup": "^1.0.0",
|
|
24
24
|
"@vates/compose": "^2.1.0",
|
|
25
25
|
"@vates/decorate-with": "^2.1.0",
|
|
26
|
-
"@vates/disposable": "^0.1.
|
|
27
|
-
"@vates/fuse-vhd": "^2.1.
|
|
28
|
-
"@vates/nbd-client": "^3.1.
|
|
26
|
+
"@vates/disposable": "^0.1.6",
|
|
27
|
+
"@vates/fuse-vhd": "^2.1.2",
|
|
28
|
+
"@vates/nbd-client": "^3.1.1",
|
|
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.
|
|
31
|
+
"@xen-orchestra/fs": "^4.2.0",
|
|
32
|
+
"@xen-orchestra/log": "^0.7.0",
|
|
33
33
|
"@xen-orchestra/template": "^0.1.0",
|
|
34
34
|
"app-conf": "^3.0.0",
|
|
35
35
|
"compare-versions": "^6.0.0",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"tar": "^6.1.15",
|
|
48
48
|
"uuid": "^9.0.0",
|
|
49
49
|
"value-matcher": "^0.2.0",
|
|
50
|
-
"vhd-lib": "^4.11.
|
|
51
|
-
"xen-api": "^4.
|
|
50
|
+
"vhd-lib": "^4.11.1",
|
|
51
|
+
"xen-api": "^4.4.0",
|
|
52
52
|
"yazl": "^2.5.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"tmp": "^0.2.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@xen-orchestra/xapi": "^7.
|
|
62
|
+
"@xen-orchestra/xapi": "^7.6.1"
|
|
63
63
|
},
|
|
64
64
|
"license": "AGPL-3.0-or-later",
|
|
65
65
|
"author": {
|