@xen-orchestra/backups 0.64.2 → 0.65.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/_incrementalVm.mjs +4 -2
- package/_runners/VmsRemote.mjs +5 -2
- package/_runners/VmsXapi.mjs +4 -1
- package/_runners/_vmRunners/FullRemote.mjs +1 -1
- package/_runners/_vmRunners/FullXapi.mjs +2 -4
- package/_runners/_vmRunners/IncrementalRemote.mjs +3 -2
- package/_runners/_vmRunners/IncrementalXapi.mjs +4 -3
- package/_runners/_vmRunners/_AbstractRemote.mjs +2 -0
- package/_runners/_vmRunners/_AbstractXapi.mjs +2 -0
- package/_runners/_writers/IncrementalXapiWriter.mjs +13 -4
- package/formatVmBackups.mjs +11 -3
- package/package.json +8 -8
package/_incrementalVm.mjs
CHANGED
|
@@ -94,11 +94,13 @@ export async function exportIncrementalVm(
|
|
|
94
94
|
}
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
+
// Get a fresh list of VM's VTPM to avoid `vm.VTPMs: [undefined]`
|
|
98
|
+
const vmVtpms = await vm.$xapi.getField('VM', vm.$ref, 'VTPMs')
|
|
97
99
|
const vtpms = await Promise.all(
|
|
98
|
-
|
|
100
|
+
vmVtpms.map(async vtpmRef => {
|
|
99
101
|
let content
|
|
100
102
|
try {
|
|
101
|
-
content = await vm.$xapi.call('VTPM.get_contents',
|
|
103
|
+
content = await vm.$xapi.call('VTPM.get_contents', vtpmRef)
|
|
102
104
|
} catch (err) {
|
|
103
105
|
console.error(err)
|
|
104
106
|
}
|
package/_runners/VmsRemote.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
|
9
9
|
import { FullRemote } from './_vmRunners/FullRemote.mjs'
|
|
10
10
|
import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
|
|
11
11
|
import { Throttle } from '@vates/generator-toolbox'
|
|
12
|
+
import createStreamThrottle from './_createStreamThrottle.mjs'
|
|
12
13
|
|
|
13
14
|
const noop = Function.prototype
|
|
14
15
|
|
|
@@ -41,7 +42,8 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
41
42
|
const schedule = this._schedule
|
|
42
43
|
const settings = this._settings
|
|
43
44
|
|
|
44
|
-
const throttleGenerator = new Throttle()
|
|
45
|
+
const throttleGenerator = new Throttle(settings.maxExportRate)
|
|
46
|
+
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
45
47
|
|
|
46
48
|
const config = this._config
|
|
47
49
|
|
|
@@ -89,7 +91,8 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
89
91
|
schedule,
|
|
90
92
|
settings: vmSettings,
|
|
91
93
|
sourceRemoteAdapter,
|
|
92
|
-
throttleGenerator,
|
|
94
|
+
throttleGenerator, // for incrementals
|
|
95
|
+
throttleStream, // for full
|
|
93
96
|
vmUuid,
|
|
94
97
|
}
|
|
95
98
|
let vmBackup
|
package/_runners/VmsXapi.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
|
10
10
|
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
|
|
11
11
|
import { FullXapi } from './_vmRunners/FullXapi.mjs'
|
|
12
12
|
import { Throttle } from '@vates/generator-toolbox'
|
|
13
|
+
import createStreamThrottle from './_createStreamThrottle.mjs'
|
|
13
14
|
|
|
14
15
|
const noop = Function.prototype
|
|
15
16
|
|
|
@@ -55,7 +56,8 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
55
56
|
const schedule = this._schedule
|
|
56
57
|
const settings = this._settings
|
|
57
58
|
|
|
58
|
-
const throttleGenerator = new Throttle()
|
|
59
|
+
const throttleGenerator = new Throttle(settings.maxExportRate)
|
|
60
|
+
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
59
61
|
|
|
60
62
|
const config = this._config
|
|
61
63
|
|
|
@@ -148,6 +150,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
148
150
|
settings: vmSettings,
|
|
149
151
|
srs,
|
|
150
152
|
throttleGenerator,
|
|
153
|
+
throttleStream,
|
|
151
154
|
vm,
|
|
152
155
|
}
|
|
153
156
|
|
|
@@ -16,7 +16,7 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
|
|
|
16
16
|
const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
|
|
17
17
|
|
|
18
18
|
for (const metadata of transferList) {
|
|
19
|
-
const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
|
|
19
|
+
const stream = this._throttleStream(await this._sourceRemoteAdapter.readFullVmBackup(metadata))
|
|
20
20
|
const sizeContainer = watchStreamSize(stream)
|
|
21
21
|
|
|
22
22
|
// @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
|
|
@@ -49,16 +49,14 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
49
49
|
const { compression } = this.job
|
|
50
50
|
const vm = this._vm
|
|
51
51
|
const exportedVm = this._exportedVm
|
|
52
|
-
|
|
53
|
-
const stream =
|
|
54
|
-
/* this._throttleStream( */
|
|
52
|
+
const stream = this._throttleStream(
|
|
55
53
|
(
|
|
56
54
|
await this._xapi.VM_export(exportedVm.$ref, {
|
|
57
55
|
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
58
56
|
useSnapshot: false,
|
|
59
57
|
})
|
|
60
58
|
).body
|
|
61
|
-
|
|
59
|
+
)
|
|
62
60
|
|
|
63
61
|
const vdis = await exportedVm.$getDisks()
|
|
64
62
|
let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
|
|
@@ -10,7 +10,7 @@ import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs
|
|
|
10
10
|
import { Disposable } from 'promise-toolbox'
|
|
11
11
|
import { openVhd } from 'vhd-lib'
|
|
12
12
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
13
|
-
import { SynchronizedDisk } from '@xen-orchestra/disk-transform'
|
|
13
|
+
import { SynchronizedDisk, ThrottledDisk } from '@xen-orchestra/disk-transform'
|
|
14
14
|
|
|
15
15
|
const { warn } = createLogger('xo:backups:Incrementalremote')
|
|
16
16
|
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
@@ -71,8 +71,9 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
71
71
|
const isVhdDifferencing = {}
|
|
72
72
|
|
|
73
73
|
for (const key in incrementalExport.disks) {
|
|
74
|
-
|
|
74
|
+
let disk = incrementalExport.disks[key]
|
|
75
75
|
isVhdDifferencing[key] = disk.isDifferencing()
|
|
76
|
+
disk = new ThrottledDisk(disk, this._throttleGenerator)
|
|
76
77
|
incrementalExport.disks[key] = new SynchronizedDisk(disk)
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
setVmDeltaChainLength,
|
|
15
15
|
markExportSuccessfull,
|
|
16
16
|
} from '../../_otherConfig.mjs'
|
|
17
|
-
import { SynchronizedDisk } from '@xen-orchestra/disk-transform'
|
|
17
|
+
import { ThrottledDisk, SynchronizedDisk } from '@xen-orchestra/disk-transform'
|
|
18
18
|
|
|
19
19
|
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
|
|
20
20
|
|
|
@@ -43,13 +43,14 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
43
43
|
const isVhdDifferencing = {}
|
|
44
44
|
let useNbd = false
|
|
45
45
|
for (const key in deltaExport.disks) {
|
|
46
|
-
|
|
46
|
+
let disk = deltaExport.disks[key]
|
|
47
47
|
isVhdDifferencing[key] = disk.isDifferencing()
|
|
48
48
|
if (!isFull && !isVhdDifferencing[key] && key !== exportedVm.$suspend_VDI?.$ref) {
|
|
49
49
|
Task.warning('Backup fell back to a full')
|
|
50
50
|
}
|
|
51
|
-
deltaExport.disks[key] = new SynchronizedDisk(disk)
|
|
52
51
|
useNbd = useNbd || disk.useNbd()
|
|
52
|
+
disk = new ThrottledDisk(disk, this._throttleGenerator)
|
|
53
|
+
deltaExport.disks[key] = new SynchronizedDisk(disk)
|
|
53
54
|
}
|
|
54
55
|
if (useNbd) {
|
|
55
56
|
Task.info('Transfer data using NBD')
|
|
@@ -24,6 +24,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
|
|
|
24
24
|
settings,
|
|
25
25
|
sourceRemoteAdapter,
|
|
26
26
|
throttleGenerator,
|
|
27
|
+
throttleStream,
|
|
27
28
|
vmUuid,
|
|
28
29
|
}) {
|
|
29
30
|
super()
|
|
@@ -37,6 +38,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
|
|
|
37
38
|
this._healthCheckSr = healthCheckSr
|
|
38
39
|
this._sourceRemoteAdapter = sourceRemoteAdapter
|
|
39
40
|
this._throttleGenerator = throttleGenerator
|
|
41
|
+
this._throttleStream = throttleStream
|
|
40
42
|
this._vmUuid = vmUuid
|
|
41
43
|
|
|
42
44
|
const allSettings = job.settings
|
|
@@ -29,6 +29,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
29
29
|
settings,
|
|
30
30
|
srs,
|
|
31
31
|
throttleGenerator,
|
|
32
|
+
throttleStream,
|
|
32
33
|
vm,
|
|
33
34
|
}) {
|
|
34
35
|
super()
|
|
@@ -63,6 +64,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
63
64
|
this._jobId = job.id
|
|
64
65
|
this._jobSnapshotVdis = undefined
|
|
65
66
|
this._throttleGenerator = throttleGenerator
|
|
67
|
+
this._throttleStream = throttleStream
|
|
66
68
|
this._xapi = vm.$xapi
|
|
67
69
|
|
|
68
70
|
// Base VM for the export
|
|
@@ -10,6 +10,7 @@ import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
|
|
|
10
10
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
11
11
|
import { listReplicatedVms } from './_listReplicatedVms.mjs'
|
|
12
12
|
import { COPY_OF, setVmOtherConfig, BASE_DELTA_VDI } from '../../_otherConfig.mjs'
|
|
13
|
+
import assert from 'node:assert'
|
|
13
14
|
|
|
14
15
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
15
16
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
@@ -25,7 +26,11 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
25
26
|
const replicatedVdis = sr.$VDIs
|
|
26
27
|
.filter(vdi => {
|
|
27
28
|
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
28
|
-
return
|
|
29
|
+
return (
|
|
30
|
+
vdi?.managed &&
|
|
31
|
+
!vdi?.is_a_snapshot /* only look for real vdi */ &&
|
|
32
|
+
baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
|
|
33
|
+
)
|
|
29
34
|
})
|
|
30
35
|
.map(({ other_config }) => other_config?.[COPY_OF])
|
|
31
36
|
.filter(_ => !!_)
|
|
@@ -108,17 +113,21 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
108
113
|
|
|
109
114
|
const replicatedVdis = sr.$VDIs.filter(vdi => {
|
|
110
115
|
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
|
111
|
-
return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
|
|
116
|
+
return vdi?.managed && !vdi?.is_a_snapshot && sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
|
|
112
117
|
})
|
|
113
118
|
|
|
114
119
|
Object.values(backup.vdis).forEach(vdi => {
|
|
115
120
|
vdi.other_config[COPY_OF] = vdi.uuid
|
|
116
121
|
if (sourceVdiUuids.length > 0) {
|
|
117
|
-
const baseReplicatedTo = replicatedVdis.
|
|
122
|
+
const baseReplicatedTo = replicatedVdis.filter(
|
|
118
123
|
replicatedVdi => replicatedVdi.other_config[COPY_OF] === vdi.other_config[BASE_DELTA_VDI]
|
|
119
124
|
)
|
|
125
|
+
assert.ok(
|
|
126
|
+
baseReplicatedTo.length <= 1,
|
|
127
|
+
`Target of a replication must be unique, got ${baseReplicatedTo.length} candidates`
|
|
128
|
+
)
|
|
120
129
|
// baseReplicatedTo can be undefined if a new disk is added and other are already replicated
|
|
121
|
-
vdi.baseVdi = baseReplicatedTo
|
|
130
|
+
vdi.baseVdi = baseReplicatedTo[0]
|
|
122
131
|
} else {
|
|
123
132
|
// first replication of this disk
|
|
124
133
|
vdi.baseVdi = undefined
|
package/formatVmBackups.mjs
CHANGED
|
@@ -18,6 +18,8 @@ function formatVmBackup(backup) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
|
+
type: 'xo-vm-backup',
|
|
22
|
+
backupRepository: backup.backupRepositoryId,
|
|
21
23
|
disks:
|
|
22
24
|
backup.vhds === undefined
|
|
23
25
|
? []
|
|
@@ -38,6 +40,7 @@ function formatVmBackup(backup) {
|
|
|
38
40
|
size: backup.size,
|
|
39
41
|
timestamp: backup.timestamp,
|
|
40
42
|
vm: {
|
|
43
|
+
uuid: backup.vm.uuid,
|
|
41
44
|
name_description: backup.vm.name_description,
|
|
42
45
|
name_label: backup.vm.name_label,
|
|
43
46
|
},
|
|
@@ -48,7 +51,12 @@ function formatVmBackup(backup) {
|
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
/**
|
|
55
|
+
* format all backups as returned by RemoteAdapter#listAllVmBackups()
|
|
56
|
+
* @param {Record<string, object[]>} backupsByVM
|
|
57
|
+
* @param {string} backupRepositoryId
|
|
58
|
+
* @returns {Record<string, object[]>}
|
|
59
|
+
*/
|
|
60
|
+
export function formatVmBackups(backupsByVM, backupRepositoryId) {
|
|
61
|
+
return mapValues(backupsByVM, backups => backups.map(backup => formatVmBackup({ ...backup, backupRepositoryId })))
|
|
54
62
|
}
|
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.65.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@iarna/toml": "^2.2.5",
|
|
21
21
|
"@kldzj/stream-throttle": "^1.1.1",
|
|
22
|
-
"@vates/async-each": "^1.0.
|
|
22
|
+
"@vates/async-each": "^1.0.1",
|
|
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
26
|
"@vates/disposable": "^0.1.6",
|
|
27
27
|
"@vates/fuse-vhd": "^2.1.2",
|
|
28
|
-
"@vates/generator-toolbox": "^1.0
|
|
28
|
+
"@vates/generator-toolbox": "^1.1.0",
|
|
29
29
|
"@vates/nbd-client": "^3.2.1",
|
|
30
30
|
"@vates/parse-duration": "^0.1.1",
|
|
31
31
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
32
|
-
"@xen-orchestra/disk-transform": "^1.
|
|
33
|
-
"@xen-orchestra/fs": "^4.6.
|
|
32
|
+
"@xen-orchestra/disk-transform": "^1.2.0",
|
|
33
|
+
"@xen-orchestra/fs": "^4.6.4",
|
|
34
34
|
"@xen-orchestra/log": "^0.7.1",
|
|
35
35
|
"@xen-orchestra/qcow2": "^1.0.0",
|
|
36
36
|
"@xen-orchestra/template": "^0.1.0",
|
|
@@ -51,18 +51,18 @@
|
|
|
51
51
|
"tar": "^6.1.15",
|
|
52
52
|
"uuid": "^9.0.0",
|
|
53
53
|
"value-matcher": "^0.2.0",
|
|
54
|
-
"vhd-lib": "^4.14.
|
|
54
|
+
"vhd-lib": "^4.14.2",
|
|
55
55
|
"xen-api": "^4.7.4",
|
|
56
56
|
"yazl": "^2.5.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"fs-extra": "^11.1.0",
|
|
60
|
-
"rimraf": "^
|
|
60
|
+
"rimraf": "^6.0.1",
|
|
61
61
|
"sinon": "^18.0.0",
|
|
62
62
|
"tmp": "^0.2.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@xen-orchestra/xapi": "^8.4.
|
|
65
|
+
"@xen-orchestra/xapi": "^8.4.2"
|
|
66
66
|
},
|
|
67
67
|
"license": "AGPL-3.0-or-later",
|
|
68
68
|
"author": {
|