@xen-orchestra/backups 0.52.2 → 0.53.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.
|
@@ -2,39 +2,39 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
|
|
|
2
2
|
import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
|
|
3
3
|
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
|
|
4
4
|
import { watchStreamSize } from '../../_watchStreamSize.mjs'
|
|
5
|
-
import { Task } from '../../Task.mjs'
|
|
6
5
|
|
|
7
6
|
export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote {
|
|
8
7
|
_getRemoteWriter() {
|
|
9
8
|
return FullRemoteWriter
|
|
10
9
|
}
|
|
10
|
+
|
|
11
|
+
_filterTransferList(transferList) {
|
|
12
|
+
return transferList.filter(this._filterPredicate)
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
async _run() {
|
|
12
16
|
const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const sizeContainer = watchStreamSize(stream)
|
|
18
|
+
for (const metadata of transferList) {
|
|
19
|
+
const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
|
|
20
|
+
const sizeContainer = watchStreamSize(stream)
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
Task.info('No new data to upload for this VM')
|
|
22
|
+
// @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
|
|
23
|
+
await this._callWriters(
|
|
24
|
+
writer =>
|
|
25
|
+
writer.run({
|
|
26
|
+
stream: forkStreamUnpipe(stream),
|
|
27
|
+
// stream will be forked and transformed, it's not safe to attach additionnal properties to it
|
|
28
|
+
streamLength: stream.length,
|
|
29
|
+
timestamp: metadata.timestamp,
|
|
30
|
+
vm: metadata.vm,
|
|
31
|
+
vmSnapshot: metadata.vmSnapshot,
|
|
32
|
+
sizeContainer,
|
|
33
|
+
}),
|
|
34
|
+
'writer.run()'
|
|
35
|
+
)
|
|
36
|
+
// for healthcheck
|
|
37
|
+
this._tags = metadata.vm.tags
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -7,7 +7,6 @@ import mapValues from 'lodash/mapValues.js'
|
|
|
7
7
|
import { AbstractRemote } from './_AbstractRemote.mjs'
|
|
8
8
|
import { forkDeltaExport } from './_forkDeltaExport.mjs'
|
|
9
9
|
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
|
|
10
|
-
import { Task } from '../../Task.mjs'
|
|
11
10
|
import { Disposable } from 'promise-toolbox'
|
|
12
11
|
import { openVhd } from 'vhd-lib'
|
|
13
12
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
@@ -16,6 +15,16 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
16
15
|
_getRemoteWriter() {
|
|
17
16
|
return IncrementalRemoteWriter
|
|
18
17
|
}
|
|
18
|
+
|
|
19
|
+
// we'll transfer the full list if at least one backup should be transfered
|
|
20
|
+
// to ensure we don't cut the delta chain
|
|
21
|
+
_filterTransferList(transferList) {
|
|
22
|
+
if (transferList.some(vmBackupMetadata => this._filterPredicate(vmBackupMetadata))) {
|
|
23
|
+
return transferList
|
|
24
|
+
}
|
|
25
|
+
return []
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
async _selectBaseVm(metadata) {
|
|
20
29
|
// for each disk , get the parent
|
|
21
30
|
const baseUuidToSrcVdi = new Map()
|
|
@@ -53,50 +62,46 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
53
62
|
async _run() {
|
|
54
63
|
const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
})
|
|
65
|
+
for (const metadata of transferList) {
|
|
66
|
+
assert.strictEqual(metadata.mode, 'delta')
|
|
67
|
+
await this._selectBaseVm(metadata)
|
|
68
|
+
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
69
|
+
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
|
|
70
|
+
useChain: false,
|
|
71
|
+
})
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
const isVhdDifferencing = {}
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
|
|
76
|
+
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
|
|
77
|
+
})
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
|
|
80
|
+
await this._callWriters(
|
|
81
|
+
writer =>
|
|
82
|
+
writer.transfer({
|
|
83
|
+
deltaExport: forkDeltaExport(incrementalExport),
|
|
84
|
+
isVhdDifferencing,
|
|
85
|
+
timestamp: metadata.timestamp,
|
|
86
|
+
vm: metadata.vm,
|
|
87
|
+
vmSnapshot: metadata.vmSnapshot,
|
|
88
|
+
}),
|
|
89
|
+
'writer.transfer()'
|
|
90
|
+
)
|
|
91
|
+
// this will update parent name with the needed alias
|
|
92
|
+
await this._callWriters(
|
|
93
|
+
writer =>
|
|
94
|
+
writer.updateUuidAndChain({
|
|
95
|
+
isVhdDifferencing,
|
|
96
|
+
timestamp: metadata.timestamp,
|
|
97
|
+
vdis: incrementalExport.vdis,
|
|
98
|
+
}),
|
|
99
|
+
'writer.updateUuidAndChain()'
|
|
100
|
+
)
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
Task.info('No new data to upload for this VM')
|
|
102
|
+
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
|
|
103
|
+
// for healthcheck
|
|
104
|
+
this._tags = metadata.vm.tags
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import groupBy from 'lodash/groupBy.js'
|
|
2
|
+
|
|
2
3
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
|
3
4
|
import { defer } from 'golike-defer'
|
|
4
5
|
import { Disposable } from 'promise-toolbox'
|
|
6
|
+
import { createPredicate } from 'value-matcher'
|
|
5
7
|
|
|
6
8
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
7
9
|
|
|
8
10
|
import { Abstract } from './_Abstract.mjs'
|
|
9
11
|
import { extractIdsFromSimplePattern } from '../../extractIdsFromSimplePattern.mjs'
|
|
12
|
+
import { Task } from '../../Task.mjs'
|
|
10
13
|
|
|
11
14
|
export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
|
|
15
|
+
_filterPredicate
|
|
12
16
|
constructor({
|
|
13
17
|
config,
|
|
14
18
|
job,
|
|
@@ -57,39 +61,74 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
|
|
|
57
61
|
})
|
|
58
62
|
)
|
|
59
63
|
})
|
|
64
|
+
const { filter } = job
|
|
65
|
+
if (filter === undefined) {
|
|
66
|
+
this._filterPredicate = () => true
|
|
67
|
+
} else {
|
|
68
|
+
this._filterPredicate = createPredicate(filter)
|
|
69
|
+
}
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
async
|
|
63
|
-
const vmBackups = await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, predicate)
|
|
72
|
+
async #computeTransferListPerJob(sourceBackups, remotesBackups) {
|
|
64
73
|
const localMetada = new Map()
|
|
65
|
-
|
|
74
|
+
sourceBackups.forEach(metadata => {
|
|
66
75
|
const timestamp = metadata.timestamp
|
|
67
76
|
localMetada.set(timestamp, metadata)
|
|
68
77
|
})
|
|
69
|
-
const nbRemotes =
|
|
78
|
+
const nbRemotes = remotesBackups.length
|
|
70
79
|
const remoteMetadatas = {}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
remoteMetadata.forEach(metadata => {
|
|
80
|
+
remotesBackups.forEach(async remoteBackups => {
|
|
81
|
+
remoteBackups.forEach(metadata => {
|
|
74
82
|
const timestamp = metadata.timestamp
|
|
75
83
|
remoteMetadatas[timestamp] = (remoteMetadatas[timestamp] ?? 0) + 1
|
|
76
84
|
})
|
|
77
85
|
})
|
|
78
86
|
|
|
79
|
-
let
|
|
87
|
+
let transferList = []
|
|
80
88
|
const timestamps = [...localMetada.keys()]
|
|
81
89
|
timestamps.sort()
|
|
82
90
|
for (const timestamp of timestamps) {
|
|
83
91
|
if (remoteMetadatas[timestamp] !== nbRemotes) {
|
|
84
92
|
// this backup is not present in all the remote
|
|
85
93
|
// should be retransfered if not found later
|
|
86
|
-
|
|
94
|
+
transferList.push(localMetada.get(timestamp))
|
|
87
95
|
} else {
|
|
88
96
|
// backup is present in local and remote : the chain has already been transferred
|
|
89
|
-
|
|
97
|
+
transferList = []
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (transferList.length > 0) {
|
|
101
|
+
const filteredTransferList = this._filterTransferList(transferList)
|
|
102
|
+
if (filteredTransferList.length > 0) {
|
|
103
|
+
return filteredTransferList
|
|
104
|
+
} else {
|
|
105
|
+
Task.info('This VM is excluded by the job filter')
|
|
106
|
+
return []
|
|
90
107
|
}
|
|
108
|
+
} else {
|
|
109
|
+
Task.info('No new data to upload for this VM')
|
|
91
110
|
}
|
|
92
|
-
|
|
111
|
+
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
*
|
|
117
|
+
* @param {*} vmPredicate a callback checking if backup is eligible for transfer. This filter MUST NOT cut delta chains
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
120
|
+
async _computeTransferList(vmPredicate) {
|
|
121
|
+
const sourceBackups = Object.values(await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, vmPredicate))
|
|
122
|
+
const remotesBackups = await Promise.all(
|
|
123
|
+
Object.values(this.remoteAdapters).map(remoteAdapter => remoteAdapter.listVmBackups(this._vmUuid, vmPredicate))
|
|
124
|
+
)
|
|
125
|
+
const sourceBackupByJobId = groupBy(sourceBackups, 'jobId')
|
|
126
|
+
const transferByJobs = await Promise.all(
|
|
127
|
+
Object.values(sourceBackupByJobId).map(vmBackupsByJob =>
|
|
128
|
+
this.#computeTransferListPerJob(vmBackupsByJob, remotesBackups)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
return transferByJobs.flat(1)
|
|
93
132
|
}
|
|
94
133
|
|
|
95
134
|
async run($defer) {
|
|
@@ -214,8 +214,12 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
214
214
|
const allSettings = this.job.settings
|
|
215
215
|
const baseSettings = this._baseSettings
|
|
216
216
|
|
|
217
|
-
const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
|
|
218
217
|
const xapi = this._xapi
|
|
218
|
+
// ensure all the event has been processed by the xapi
|
|
219
|
+
await xapi.barrier()
|
|
220
|
+
// ensure cached object are up to date
|
|
221
|
+
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
|
|
222
|
+
const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
|
|
219
223
|
await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
220
224
|
const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
|
|
221
225
|
|
|
@@ -235,13 +239,12 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
235
239
|
let vmRef
|
|
236
240
|
// if there is an attached VM => destroy the VM (Non CBT backups)
|
|
237
241
|
for (const vdi of vdis) {
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
const vbds = vdi.$VBDs.filter(({ $VM }) => $VM.is_control_domain === false)
|
|
243
|
+
if (vbds.length > 0) {
|
|
240
244
|
// only one VM linked to this vdi
|
|
241
245
|
// this will throw error for VDI still attached to control domain
|
|
242
246
|
assert.strictEqual(vbds.length, 1, 'VDI must be free or attached to exactly one VM')
|
|
243
247
|
const vm = vbds[0].$VM
|
|
244
|
-
assert.strictEqual(vm.is_control_domain, false, `Disk is still attached to DOM0 VM`) // don't delete a VM (especially a control domain)
|
|
245
248
|
assert.strictEqual(vm.is_a_snapshot, true, `VM must be a snapshot`) // don't delete a VM (especially a control domain)
|
|
246
249
|
|
|
247
250
|
const vmRefVdi = vm.$ref
|
|
@@ -335,15 +338,22 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
335
338
|
const reason = 'VM migration is blocked during backup'
|
|
336
339
|
await vm.update_blocked_operations({ pool_migrate: reason, migrate_send: reason })
|
|
337
340
|
|
|
338
|
-
$defer(() =>
|
|
341
|
+
$defer(async () => {
|
|
339
342
|
// delete the entries if they did not exist previously or if they were
|
|
340
343
|
// equal to reason (which happen if a previous backup was interrupted
|
|
341
344
|
// before resetting them)
|
|
342
|
-
vm.update_blocked_operations({
|
|
345
|
+
await vm.update_blocked_operations({
|
|
343
346
|
migrate_send: migrate_send === undefined || migrate_send === reason ? null : migrate_send,
|
|
344
347
|
pool_migrate: pool_migrate === undefined || pool_migrate === reason ? null : pool_migrate,
|
|
345
348
|
})
|
|
346
|
-
|
|
349
|
+
|
|
350
|
+
// 2024-08-19 - Work-around a XAPI bug where allowed_operations are not properly computed when blocked_operations is updated
|
|
351
|
+
//
|
|
352
|
+
// this is a problem because some clients (e.g. XenCenter) use this field to allow operations.
|
|
353
|
+
//
|
|
354
|
+
// internal source: https://team.vates.fr/vates/pl/mjmxnce9qfdx587r3qpe4z91ho
|
|
355
|
+
await vm.$call('update_allowed_operations')
|
|
356
|
+
})
|
|
347
357
|
}
|
|
348
358
|
|
|
349
359
|
await this._fetchJobSnapshots()
|
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.53.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"proper-lockfile": "^4.1.2",
|
|
47
47
|
"tar": "^6.1.15",
|
|
48
48
|
"uuid": "^9.0.0",
|
|
49
|
+
"value-matcher": "^0.2.0",
|
|
49
50
|
"vhd-lib": "^4.11.0",
|
|
50
51
|
"xen-api": "^4.2.0",
|
|
51
52
|
"yazl": "^2.5.1"
|
|
@@ -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.3.0"
|
|
62
63
|
},
|
|
63
64
|
"license": "AGPL-3.0-or-later",
|
|
64
65
|
"author": {
|