@xen-orchestra/backups 0.44.7 → 0.46.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/_runners/VmsRemote.mjs +49 -4
- package/_runners/VmsXapi.mjs +83 -29
- package/_runners/_PoolMetadataBackup.mjs +1 -1
- package/_runners/_vmRunners/FullXapi.mjs +6 -4
- package/_runners/_vmRunners/_AbstractRemote.mjs +1 -0
- package/_runners/_writers/_MixinRemoteWriter.mjs +5 -5
- package/package.json +5 -5
package/_runners/VmsRemote.mjs
CHANGED
|
@@ -6,11 +6,12 @@ import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
|
|
|
6
6
|
import { Task } from '../Task.mjs'
|
|
7
7
|
import createStreamThrottle from './_createStreamThrottle.mjs'
|
|
8
8
|
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
|
|
9
|
-
import { runTask } from './_runTask.mjs'
|
|
10
9
|
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
11
10
|
import { FullRemote } from './_vmRunners/FullRemote.mjs'
|
|
12
11
|
import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
|
|
13
12
|
|
|
13
|
+
const noop = Function.prototype
|
|
14
|
+
|
|
14
15
|
const DEFAULT_REMOTE_VM_SETTINGS = {
|
|
15
16
|
concurrency: 2,
|
|
16
17
|
copyRetention: 0,
|
|
@@ -20,6 +21,7 @@ const DEFAULT_REMOTE_VM_SETTINGS = {
|
|
|
20
21
|
healthCheckVmsWithTags: [],
|
|
21
22
|
maxExportRate: 0,
|
|
22
23
|
maxMergedDeltasPerRun: Infinity,
|
|
24
|
+
nRetriesVmBackupFailures: 0,
|
|
23
25
|
timeout: 0,
|
|
24
26
|
validateVhdStreams: false,
|
|
25
27
|
vmTimeout: 0,
|
|
@@ -41,6 +43,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
41
43
|
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
42
44
|
|
|
43
45
|
const config = this._config
|
|
46
|
+
|
|
44
47
|
await Disposable.use(
|
|
45
48
|
() => this._getAdapter(job.sourceRemote),
|
|
46
49
|
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
|
@@ -62,8 +65,19 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
62
65
|
const allSettings = this._job.settings
|
|
63
66
|
const baseSettings = this._baseSettings
|
|
64
67
|
|
|
68
|
+
const queue = new Set(vmsUuids)
|
|
69
|
+
const taskByVmId = {}
|
|
70
|
+
const nTriesByVmId = {}
|
|
71
|
+
|
|
65
72
|
const handleVm = vmUuid => {
|
|
73
|
+
if (nTriesByVmId[vmUuid] === undefined) {
|
|
74
|
+
nTriesByVmId[vmUuid] = 0
|
|
75
|
+
}
|
|
76
|
+
nTriesByVmId[vmUuid]++
|
|
77
|
+
|
|
66
78
|
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
79
|
+
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
|
80
|
+
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
|
67
81
|
|
|
68
82
|
const opts = {
|
|
69
83
|
baseSettings,
|
|
@@ -72,7 +86,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
72
86
|
healthCheckSr,
|
|
73
87
|
remoteAdapters,
|
|
74
88
|
schedule,
|
|
75
|
-
settings:
|
|
89
|
+
settings: vmSettings,
|
|
76
90
|
sourceRemoteAdapter,
|
|
77
91
|
throttleStream,
|
|
78
92
|
vmUuid,
|
|
@@ -90,11 +104,42 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
90
104
|
.listVmBackups(vmUuid, ({ mode }) => mode === job.mode)
|
|
91
105
|
.then(vmBackups => {
|
|
92
106
|
// avoiding to create tasks for empty directories
|
|
93
|
-
if (vmBackups.length > 0)
|
|
107
|
+
if (vmBackups.length > 0) {
|
|
108
|
+
if (taskByVmId[vmUuid] === undefined) {
|
|
109
|
+
taskByVmId[vmUuid] = new Task(taskStart)
|
|
110
|
+
}
|
|
111
|
+
const task = taskByVmId[vmUuid]
|
|
112
|
+
return task
|
|
113
|
+
.run(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await vmBackup.run()
|
|
116
|
+
task.success(result)
|
|
117
|
+
return result
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (isLastRun) {
|
|
120
|
+
throw error
|
|
121
|
+
} else {
|
|
122
|
+
Task.warning(`Retry the VM mirror backup due to an error`, {
|
|
123
|
+
attempt: nTriesByVmId[vmUuid],
|
|
124
|
+
error: error.message,
|
|
125
|
+
})
|
|
126
|
+
queue.add(vmUuid)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
.catch(noop)
|
|
131
|
+
}
|
|
94
132
|
})
|
|
95
133
|
}
|
|
96
134
|
const { concurrency } = settings
|
|
97
|
-
|
|
135
|
+
const _handleVm = !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm)
|
|
136
|
+
|
|
137
|
+
while (queue.size > 0) {
|
|
138
|
+
const vmIds = Array.from(queue)
|
|
139
|
+
queue.clear()
|
|
140
|
+
|
|
141
|
+
await asyncMapSettled(vmIds, _handleVm)
|
|
142
|
+
}
|
|
98
143
|
}
|
|
99
144
|
)
|
|
100
145
|
}
|
package/_runners/VmsXapi.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
|
11
11
|
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
|
|
12
12
|
import { FullXapi } from './_vmRunners/FullXapi.mjs'
|
|
13
13
|
|
|
14
|
+
const noop = Function.prototype
|
|
15
|
+
|
|
14
16
|
const DEFAULT_XAPI_VM_SETTINGS = {
|
|
15
17
|
bypassVdiChainsCheck: false,
|
|
16
18
|
checkpointSnapshot: false,
|
|
@@ -24,6 +26,7 @@ const DEFAULT_XAPI_VM_SETTINGS = {
|
|
|
24
26
|
healthCheckVmsWithTags: [],
|
|
25
27
|
maxExportRate: 0,
|
|
26
28
|
maxMergedDeltasPerRun: Infinity,
|
|
29
|
+
nRetriesVmBackupFailures: 0,
|
|
27
30
|
offlineBackup: false,
|
|
28
31
|
offlineSnapshot: false,
|
|
29
32
|
snapshotRetention: 0,
|
|
@@ -53,6 +56,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
53
56
|
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
|
54
57
|
|
|
55
58
|
const config = this._config
|
|
59
|
+
|
|
56
60
|
await Disposable.use(
|
|
57
61
|
Disposable.all(
|
|
58
62
|
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
@@ -89,48 +93,98 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
89
93
|
const allSettings = this._job.settings
|
|
90
94
|
const baseSettings = this._baseSettings
|
|
91
95
|
|
|
96
|
+
const queue = new Set(vmIds)
|
|
97
|
+
const taskByVmId = {}
|
|
98
|
+
const nTriesByVmId = {}
|
|
99
|
+
|
|
92
100
|
const handleVm = vmUuid => {
|
|
101
|
+
const getVmTask = () => {
|
|
102
|
+
if (taskByVmId[vmUuid] === undefined) {
|
|
103
|
+
taskByVmId[vmUuid] = new Task(taskStart)
|
|
104
|
+
}
|
|
105
|
+
return taskByVmId[vmUuid]
|
|
106
|
+
}
|
|
107
|
+
const vmBackupFailed = error => {
|
|
108
|
+
if (isLastRun) {
|
|
109
|
+
throw error
|
|
110
|
+
} else {
|
|
111
|
+
Task.warning(`Retry the VM backup due to an error`, {
|
|
112
|
+
attempt: nTriesByVmId[vmUuid],
|
|
113
|
+
error: error.message,
|
|
114
|
+
})
|
|
115
|
+
queue.add(vmUuid)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (nTriesByVmId[vmUuid] === undefined) {
|
|
120
|
+
nTriesByVmId[vmUuid] = 0
|
|
121
|
+
}
|
|
122
|
+
nTriesByVmId[vmUuid]++
|
|
123
|
+
|
|
124
|
+
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
|
93
125
|
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
|
126
|
+
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
|
94
127
|
|
|
95
128
|
return this._getRecord('VM', vmUuid).then(
|
|
96
129
|
disposableVm =>
|
|
97
|
-
Disposable.use(disposableVm, vm => {
|
|
98
|
-
taskStart.data.name_label
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
Disposable.use(disposableVm, async vm => {
|
|
131
|
+
if (taskStart.data.name_label === undefined) {
|
|
132
|
+
taskStart.data.name_label = vm.name_label
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const task = getVmTask()
|
|
136
|
+
return task
|
|
137
|
+
.run(async () => {
|
|
138
|
+
const opts = {
|
|
139
|
+
baseSettings,
|
|
140
|
+
config,
|
|
141
|
+
getSnapshotNameLabel,
|
|
142
|
+
healthCheckSr,
|
|
143
|
+
job,
|
|
144
|
+
remoteAdapters,
|
|
145
|
+
schedule,
|
|
146
|
+
settings: vmSettings,
|
|
147
|
+
srs,
|
|
148
|
+
throttleStream,
|
|
149
|
+
vm,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let vmBackup
|
|
153
|
+
if (job.mode === 'delta') {
|
|
154
|
+
vmBackup = new IncrementalXapi(opts)
|
|
119
155
|
} else {
|
|
120
|
-
|
|
156
|
+
if (job.mode === 'full') {
|
|
157
|
+
vmBackup = new FullXapi(opts)
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error(`Job mode ${job.mode} not implemented`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const result = await vmBackup.run()
|
|
165
|
+
task.success(result)
|
|
166
|
+
return result
|
|
167
|
+
} catch (error) {
|
|
168
|
+
vmBackupFailed(error)
|
|
121
169
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
})
|
|
170
|
+
})
|
|
171
|
+
.catch(noop) // errors are handled by logs
|
|
125
172
|
}),
|
|
126
173
|
error =>
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
getVmTask().run(() => {
|
|
175
|
+
vmBackupFailed(error)
|
|
129
176
|
})
|
|
130
177
|
)
|
|
131
178
|
}
|
|
132
179
|
const { concurrency } = settings
|
|
133
|
-
|
|
180
|
+
const _handleVm = concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm)
|
|
181
|
+
|
|
182
|
+
while (queue.size > 0) {
|
|
183
|
+
const vmIds = Array.from(queue)
|
|
184
|
+
queue.clear()
|
|
185
|
+
|
|
186
|
+
await asyncMapSettled(vmIds, _handleVm)
|
|
187
|
+
}
|
|
134
188
|
}
|
|
135
189
|
)
|
|
136
190
|
}
|
|
@@ -31,7 +31,7 @@ export class PoolMetadataBackup {
|
|
|
31
31
|
const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${pool.$id}`
|
|
32
32
|
const dir = `${poolDir}/${formatFilenameDate(timestamp)}`
|
|
33
33
|
|
|
34
|
-
const stream = await this._exportPoolMetadata()
|
|
34
|
+
const stream = (await this._exportPoolMetadata()).body
|
|
35
35
|
const fileName = `${dir}/data`
|
|
36
36
|
|
|
37
37
|
const metadata = JSON.stringify(
|
|
@@ -30,10 +30,12 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
30
30
|
const vm = this._vm
|
|
31
31
|
const exportedVm = this._exportedVm
|
|
32
32
|
const stream = this._throttleStream(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
(
|
|
34
|
+
await this._xapi.VM_export(exportedVm.$ref, {
|
|
35
|
+
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
|
36
|
+
useSnapshot: false,
|
|
37
|
+
})
|
|
38
|
+
).body
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
const vdis = await exportedVm.$getDisks()
|
|
@@ -77,11 +77,11 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
77
77
|
healthCheck() {
|
|
78
78
|
const sr = this._healthCheckSr
|
|
79
79
|
assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
|
|
80
|
-
|
|
81
|
-
this
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
if (this._metadataFileName === undefined) {
|
|
81
|
+
// this can happen when making a mirror backup with nothing to transfer
|
|
82
|
+
Task.info('no health check, since no backup have been transferred')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
85
|
return Task.run(
|
|
86
86
|
{
|
|
87
87
|
name: 'health check',
|
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.46.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"@vates/decorate-with": "^2.1.0",
|
|
26
26
|
"@vates/disposable": "^0.1.5",
|
|
27
27
|
"@vates/fuse-vhd": "^2.1.0",
|
|
28
|
-
"@vates/nbd-client": "^3.0.
|
|
28
|
+
"@vates/nbd-client": "^3.0.1",
|
|
29
29
|
"@vates/parse-duration": "^0.1.1",
|
|
30
30
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
31
|
-
"@xen-orchestra/fs": "^4.1.
|
|
31
|
+
"@xen-orchestra/fs": "^4.1.6",
|
|
32
32
|
"@xen-orchestra/log": "^0.6.0",
|
|
33
33
|
"@xen-orchestra/template": "^0.1.0",
|
|
34
34
|
"app-conf": "^2.3.0",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"tar": "^6.1.15",
|
|
46
46
|
"uuid": "^9.0.0",
|
|
47
47
|
"vhd-lib": "^4.9.1",
|
|
48
|
-
"xen-api": "^
|
|
48
|
+
"xen-api": "^3.0.0",
|
|
49
49
|
"yazl": "^2.5.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tmp": "^0.2.1"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@xen-orchestra/xapi": "^
|
|
59
|
+
"@xen-orchestra/xapi": "^5.0.0"
|
|
60
60
|
},
|
|
61
61
|
"license": "AGPL-3.0-or-later",
|
|
62
62
|
"author": {
|