@xen-orchestra/backups 0.71.3 → 0.72.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/HealthCheckVmBackup.mjs +2 -2
- package/ImportVmBackup.mjs +2 -2
- package/RemoteAdapter.mjs +3 -2
- package/_backupWorker.mjs +3 -3
- package/_cleanVm.mjs +15 -15
- package/_incrementalVm.mjs +1 -1
- package/_runners/Metadata.mjs +18 -13
- package/_runners/VmsRemote.mjs +21 -17
- package/_runners/VmsXapi.mjs +24 -23
- package/_runners/_Abstract.mjs +10 -5
- package/_runners/_PoolMetadataBackup.mjs +3 -3
- package/_runners/_XoMetadataBackup.mjs +3 -3
- package/_runners/_vmRunners/IncrementalXapi.mjs +1 -1
- package/_runners/_vmRunners/_Abstract.mjs +2 -2
- package/_runners/_vmRunners/_AbstractRemote.mjs +1 -1
- package/_runners/_vmRunners/_AbstractXapi.mjs +41 -4
- package/_runners/_writers/FullRemoteWriter.mjs +6 -5
- package/_runners/_writers/FullXapiWriter.mjs +5 -5
- package/_runners/_writers/IncrementalRemoteWriter.mjs +11 -11
- package/_runners/_writers/IncrementalXapiWriter.mjs +109 -75
- package/_runners/_writers/_MixinRemoteWriter.mjs +3 -3
- package/_runners/_writers/_MixinXapiWriter.mjs +4 -4
- package/disks/MergeRemoteDisk.mjs +1 -2
- package/package.json +15 -14
- package/Task.mjs +0 -155
- package/_runners/_runTask.mjs +0 -5
package/HealthCheckVmBackup.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Task } from '
|
|
1
|
+
import { Task } from '@vates/task'
|
|
2
2
|
|
|
3
3
|
export class HealthCheckVmBackup {
|
|
4
4
|
#restoredVm
|
|
@@ -14,7 +14,7 @@ export class HealthCheckVmBackup {
|
|
|
14
14
|
async run() {
|
|
15
15
|
return Task.run(
|
|
16
16
|
{
|
|
17
|
-
name: 'vmstart',
|
|
17
|
+
properties: { name: 'vmstart' },
|
|
18
18
|
},
|
|
19
19
|
async () => {
|
|
20
20
|
let restoredVm = this.#restoredVm
|
package/ImportVmBackup.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import assert from 'node:assert'
|
|
|
2
2
|
|
|
3
3
|
import { formatFilenameDate } from './_filenameDate.mjs'
|
|
4
4
|
import { importIncrementalVm } from './_incrementalVm.mjs'
|
|
5
|
-
import { Task } from './Task.mjs'
|
|
6
5
|
import { watchStreamSize } from './_watchStreamSize.mjs'
|
|
7
6
|
import { decorateClass } from '@vates/decorate-with'
|
|
7
|
+
import { Task } from '@vates/task'
|
|
8
8
|
import { createLogger } from '@xen-orchestra/log'
|
|
9
9
|
import { dirname, join } from 'node:path'
|
|
10
10
|
import pickBy from 'lodash/pickBy.js'
|
|
@@ -244,7 +244,7 @@ export class ImportVmBackup {
|
|
|
244
244
|
|
|
245
245
|
return Task.run(
|
|
246
246
|
{
|
|
247
|
-
name: 'transfer',
|
|
247
|
+
properties: { name: 'transfer' },
|
|
248
248
|
},
|
|
249
249
|
async () => {
|
|
250
250
|
const xapi = this._xapi
|
package/RemoteAdapter.mjs
CHANGED
|
@@ -702,7 +702,7 @@ export class RemoteAdapter {
|
|
|
702
702
|
const handler = this._handler
|
|
703
703
|
|
|
704
704
|
if (this.useVhdDirectory()) {
|
|
705
|
-
await writeToVhdDirectory({
|
|
705
|
+
return await writeToVhdDirectory({
|
|
706
706
|
disk,
|
|
707
707
|
target: {
|
|
708
708
|
handler,
|
|
@@ -714,8 +714,9 @@ export class RemoteAdapter {
|
|
|
714
714
|
})
|
|
715
715
|
} else {
|
|
716
716
|
const stream = await toVhdStream(disk)
|
|
717
|
-
await this.outputStream(path, stream, { validator, checksum: false })
|
|
717
|
+
const size = await this.outputStream(path, stream, { validator, checksum: false })
|
|
718
718
|
await validator(path)
|
|
719
|
+
return size
|
|
719
720
|
}
|
|
720
721
|
}
|
|
721
722
|
|
package/_backupWorker.mjs
CHANGED
|
@@ -14,10 +14,10 @@ import { decorateMethodsWith } from '@vates/decorate-with'
|
|
|
14
14
|
import { deduped } from '@vates/disposable/deduped.js'
|
|
15
15
|
import { getHandler } from '@xen-orchestra/fs'
|
|
16
16
|
import { parseDuration } from '@vates/parse-duration'
|
|
17
|
+
import { Task } from '@vates/task'
|
|
17
18
|
import { Xapi } from '@xen-orchestra/xapi'
|
|
18
19
|
|
|
19
20
|
import { RemoteAdapter } from './RemoteAdapter.mjs'
|
|
20
|
-
import { Task } from './Task.mjs'
|
|
21
21
|
|
|
22
22
|
createCachedLookup().patchGlobal()
|
|
23
23
|
|
|
@@ -178,8 +178,8 @@ process.on('message', async message => {
|
|
|
178
178
|
const result = message.runWithLogs
|
|
179
179
|
? await Task.run(
|
|
180
180
|
{
|
|
181
|
-
name: 'backup run',
|
|
182
|
-
|
|
181
|
+
properties: { name: 'backup run', ...message.data.jobData },
|
|
182
|
+
onProgress: data =>
|
|
183
183
|
emitMessage({
|
|
184
184
|
data,
|
|
185
185
|
type: 'log',
|
package/_cleanVm.mjs
CHANGED
|
@@ -9,8 +9,8 @@ import { RemoteVhdDisk } from './disks/RemoteVhdDisk.mjs'
|
|
|
9
9
|
import { RemoteVhdDiskChain } from './disks/RemoteVhdDiskChain.mjs'
|
|
10
10
|
import { MergeRemoteDisk } from './disks/MergeRemoteDisk.mjs'
|
|
11
11
|
|
|
12
|
-
import { Task } from './Task.mjs'
|
|
13
12
|
import { Disposable } from 'promise-toolbox'
|
|
13
|
+
import { Task } from '@vates/task'
|
|
14
14
|
import handlerPath from '@xen-orchestra/fs/path'
|
|
15
15
|
|
|
16
16
|
const { DISK_TYPES } = Constants
|
|
@@ -514,23 +514,27 @@ export async function cleanVm(
|
|
|
514
514
|
})
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
let totalMergedDataSize = 0
|
|
517
518
|
const metadataWithMergedVhd = {}
|
|
518
519
|
const doMerge = async () => {
|
|
519
520
|
await asyncMap(toMerge, async chain => {
|
|
520
|
-
const { finalDiskSize } = await limitedMergeVhdChain(handler, chain, {
|
|
521
|
+
const { finalDiskSize, mergedDataSize } = await limitedMergeVhdChain(handler, chain, {
|
|
521
522
|
logInfo,
|
|
522
523
|
logWarn,
|
|
523
524
|
remove,
|
|
524
525
|
mergeBlockConcurrency,
|
|
525
526
|
})
|
|
527
|
+
totalMergedDataSize += mergedDataSize
|
|
526
528
|
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metadata file
|
|
527
529
|
metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalDiskSize
|
|
528
530
|
})
|
|
531
|
+
|
|
532
|
+
return { size: totalMergedDataSize }
|
|
529
533
|
}
|
|
530
534
|
|
|
531
535
|
await Promise.all([
|
|
532
536
|
...unusedVhdsDeletion,
|
|
533
|
-
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : () => Promise.resolve()),
|
|
537
|
+
toMerge.length !== 0 && (merge ? Task.run({ properties: { name: 'merge' } }, doMerge) : () => Promise.resolve()),
|
|
534
538
|
asyncMap(unusedXvas, path => {
|
|
535
539
|
logWarn('unused XVA', { path })
|
|
536
540
|
if (remove) {
|
|
@@ -582,18 +586,13 @@ export async function cleanVm(
|
|
|
582
586
|
return
|
|
583
587
|
}
|
|
584
588
|
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
metadata.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// all disks are now key disk
|
|
593
|
-
metadata.isVhdDifferencing = {}
|
|
594
|
-
for (const id of Object.keys(metadata.vdis ?? {})) {
|
|
595
|
-
metadata.isVhdDifferencing[id] = false
|
|
596
|
-
}
|
|
589
|
+
// Rewrite metadata when a merge changed the backup layout.
|
|
590
|
+
if (mergedSize) {
|
|
591
|
+
metadata.size = mergedSize
|
|
592
|
+
// all disks are now key disk
|
|
593
|
+
metadata.isVhdDifferencing = {}
|
|
594
|
+
for (const id of Object.keys(metadata.vdis ?? {})) {
|
|
595
|
+
metadata.isVhdDifferencing[id] = false
|
|
597
596
|
}
|
|
598
597
|
mustRegenerateCache = true
|
|
599
598
|
try {
|
|
@@ -619,5 +618,6 @@ export async function cleanVm(
|
|
|
619
618
|
return {
|
|
620
619
|
// boolean whether some VHDs were merged (or should be merged)
|
|
621
620
|
merge: toMerge.length !== 0,
|
|
621
|
+
size: totalMergedDataSize,
|
|
622
622
|
}
|
|
623
623
|
}
|
package/_incrementalVm.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import { asyncMap } from '@xen-orchestra/async-map'
|
|
|
4
4
|
import { CancelToken } from 'promise-toolbox'
|
|
5
5
|
import { compareVersions } from 'compare-versions'
|
|
6
6
|
import { defer } from 'golike-defer'
|
|
7
|
+
import { Task } from '@vates/task'
|
|
7
8
|
|
|
8
9
|
import { cancelableMap } from './_cancelableMap.mjs'
|
|
9
|
-
import { Task } from './Task.mjs'
|
|
10
10
|
import pick from 'lodash/pick.js'
|
|
11
11
|
import { BASE_DELTA_VDI, CONTENT_KEY, COPY_OF, VM_UUID } from './_otherConfig.mjs'
|
|
12
12
|
|
package/_runners/Metadata.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
2
|
+
import { Task } from '@vates/task'
|
|
2
3
|
import Disposable from 'promise-toolbox/Disposable'
|
|
3
4
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
4
5
|
|
|
@@ -6,9 +7,10 @@ import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
|
|
|
6
7
|
import { PoolMetadataBackup } from './_PoolMetadataBackup.mjs'
|
|
7
8
|
import { XoMetadataBackup } from './_XoMetadataBackup.mjs'
|
|
8
9
|
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
|
|
9
|
-
import { runTask } from './_runTask.mjs'
|
|
10
10
|
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
11
11
|
|
|
12
|
+
const noop = Function.prototype
|
|
13
|
+
|
|
12
14
|
const DEFAULT_METADATA_SETTINGS = {
|
|
13
15
|
retentionPoolMetadata: 0,
|
|
14
16
|
retentionXoMetadata: 0,
|
|
@@ -55,13 +57,16 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
|
|
|
55
57
|
poolIds.map(id =>
|
|
56
58
|
this._getRecord('pool', id).catch(error => {
|
|
57
59
|
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
58
|
-
|
|
60
|
+
Task.run(
|
|
59
61
|
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
properties: {
|
|
63
|
+
id,
|
|
64
|
+
name: 'get pool record',
|
|
65
|
+
type: 'pool',
|
|
66
|
+
},
|
|
62
67
|
},
|
|
63
68
|
() => Promise.reject(error)
|
|
64
|
-
)
|
|
69
|
+
).catch(noop)
|
|
65
70
|
})
|
|
66
71
|
)
|
|
67
72
|
),
|
|
@@ -81,11 +86,11 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
|
|
|
81
86
|
if (pools.length !== 0 && settings.retentionPoolMetadata !== 0) {
|
|
82
87
|
promises.push(
|
|
83
88
|
asyncMap(pools, async pool =>
|
|
84
|
-
|
|
89
|
+
Task.run(
|
|
85
90
|
{
|
|
86
|
-
|
|
87
|
-
data: {
|
|
91
|
+
properties: {
|
|
88
92
|
id: pool.$id,
|
|
93
|
+
name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
|
|
89
94
|
pool,
|
|
90
95
|
poolMaster: await ignoreErrors.call(pool.$xapi.getRecord('host', pool.master)),
|
|
91
96
|
type: 'pool',
|
|
@@ -100,17 +105,17 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
|
|
|
100
105
|
schedule,
|
|
101
106
|
settings,
|
|
102
107
|
}).run()
|
|
103
|
-
)
|
|
108
|
+
).catch(noop)
|
|
104
109
|
)
|
|
105
110
|
)
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
if (job.xoMetadata !== undefined && settings.retentionXoMetadata !== 0) {
|
|
109
114
|
promises.push(
|
|
110
|
-
|
|
115
|
+
Task.run(
|
|
111
116
|
{
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
properties: {
|
|
118
|
+
name: `Starting XO metadata backup. (${job.id})`,
|
|
114
119
|
type: 'xo',
|
|
115
120
|
},
|
|
116
121
|
},
|
|
@@ -122,7 +127,7 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
|
|
|
122
127
|
schedule,
|
|
123
128
|
settings,
|
|
124
129
|
}).run()
|
|
125
|
-
)
|
|
130
|
+
).catch(noop)
|
|
126
131
|
)
|
|
127
132
|
}
|
|
128
133
|
await Promise.all(promises)
|
package/_runners/VmsRemote.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { asyncMapSettled } from '@xen-orchestra/async-map'
|
|
2
2
|
import Disposable from 'promise-toolbox/Disposable'
|
|
3
3
|
import { limitConcurrency } from 'limit-concurrency-decorator'
|
|
4
|
+
import { Task } from '@vates/task'
|
|
4
5
|
|
|
5
6
|
import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
|
|
6
|
-
import { Task } from '../Task.mjs'
|
|
7
7
|
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
|
|
8
8
|
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
9
9
|
import { FullRemote } from './_vmRunners/FullRemote.mjs'
|
|
@@ -78,7 +78,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
78
78
|
}
|
|
79
79
|
nTriesByVmId[vmUuid]++
|
|
80
80
|
|
|
81
|
-
const taskStart = { name: 'backup VM',
|
|
81
|
+
const taskStart = { properties: { id: vmUuid, name: 'backup VM', type: 'VM' } }
|
|
82
82
|
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
|
83
83
|
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
|
84
84
|
|
|
@@ -113,23 +113,27 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
|
|
113
113
|
taskByVmId[vmUuid] = new Task(taskStart)
|
|
114
114
|
}
|
|
115
115
|
const task = taskByVmId[vmUuid]
|
|
116
|
+
// error has to be caught in the task to prevent its failure, but handled outside the task to execute another task.run()
|
|
117
|
+
let taskError
|
|
116
118
|
return task
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} else {
|
|
126
|
-
Task.warning(`Retry the VM mirror backup due to an error`, {
|
|
127
|
-
attempt: nTriesByVmId[vmUuid],
|
|
128
|
-
error: error.message,
|
|
129
|
-
})
|
|
130
|
-
queue.add(vmUuid)
|
|
131
|
-
}
|
|
119
|
+
.runInside(async () =>
|
|
120
|
+
vmBackup.run().catch(error => {
|
|
121
|
+
taskError = error
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
.then(result => {
|
|
125
|
+
if (taskError === undefined) {
|
|
126
|
+
return task.success(result)
|
|
132
127
|
}
|
|
128
|
+
if (isLastRun) {
|
|
129
|
+
return task.failure(taskError)
|
|
130
|
+
}
|
|
131
|
+
// don't end the task
|
|
132
|
+
task.warning(`Retry the VM mirror backup due to an error`, {
|
|
133
|
+
attempt: nTriesByVmId[vmUuid],
|
|
134
|
+
error: taskError.message,
|
|
135
|
+
})
|
|
136
|
+
queue.add(vmUuid)
|
|
133
137
|
})
|
|
134
138
|
.catch(noop)
|
|
135
139
|
}
|
package/_runners/VmsXapi.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { asyncMapSettled } from '@xen-orchestra/async-map'
|
|
2
2
|
import Disposable from 'promise-toolbox/Disposable'
|
|
3
3
|
import { limitConcurrency } from 'limit-concurrency-decorator'
|
|
4
|
+
import { Task } from '@vates/task'
|
|
4
5
|
|
|
5
6
|
import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
|
|
6
|
-
import { Task } from '../Task.mjs'
|
|
7
7
|
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
|
|
8
|
-
import { runTask } from './_runTask.mjs'
|
|
9
8
|
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
|
10
9
|
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
|
|
11
10
|
import { FullXapi } from './_vmRunners/FullXapi.mjs'
|
|
@@ -65,13 +64,12 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
65
64
|
Disposable.all(
|
|
66
65
|
extractIdsFromSimplePattern(job.srs).map(id =>
|
|
67
66
|
this._getRecord('SR', id).catch(error => {
|
|
68
|
-
|
|
67
|
+
Task.run(
|
|
69
68
|
{
|
|
70
|
-
name: 'get SR record',
|
|
71
|
-
data: { type: 'SR', id },
|
|
69
|
+
properties: { id, name: 'get SR record', type: 'SR' },
|
|
72
70
|
},
|
|
73
71
|
() => Promise.reject(error)
|
|
74
|
-
)
|
|
72
|
+
).catch(noop)
|
|
75
73
|
})
|
|
76
74
|
)
|
|
77
75
|
),
|
|
@@ -108,11 +106,12 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
108
106
|
}
|
|
109
107
|
return taskByVmId[vmUuid]
|
|
110
108
|
}
|
|
111
|
-
const vmBackupFailed = error => {
|
|
109
|
+
const vmBackupFailed = async (error, task) => {
|
|
112
110
|
if (isLastRun) {
|
|
113
|
-
|
|
111
|
+
return task.failure(error)
|
|
114
112
|
} else {
|
|
115
|
-
|
|
113
|
+
// don't end the task
|
|
114
|
+
task.warning(`Retry the VM backup due to an error`, {
|
|
116
115
|
attempt: nTriesByVmId[vmUuid],
|
|
117
116
|
error: error.message,
|
|
118
117
|
})
|
|
@@ -126,19 +125,21 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
126
125
|
nTriesByVmId[vmUuid]++
|
|
127
126
|
|
|
128
127
|
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
|
129
|
-
const taskStart = { name: 'backup VM',
|
|
128
|
+
const taskStart = { properties: { id: vmUuid, name: 'backup VM', type: 'VM' } }
|
|
130
129
|
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
|
131
130
|
|
|
132
131
|
return this._getRecord('VM', vmUuid).then(
|
|
133
132
|
disposableVm =>
|
|
134
133
|
Disposable.use(disposableVm, async vm => {
|
|
135
|
-
if (taskStart.
|
|
136
|
-
taskStart.
|
|
134
|
+
if (taskStart.properties.name_label === undefined) {
|
|
135
|
+
taskStart.properties.name_label = vm.name_label
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
const task = getVmTask()
|
|
139
|
+
// error has to be caught in the task to prevent its failure, but handled outside the task to execute another task.run()
|
|
140
|
+
let taskError
|
|
140
141
|
return task
|
|
141
|
-
.
|
|
142
|
+
.runInside(async () => {
|
|
142
143
|
const opts = {
|
|
143
144
|
baseSettings,
|
|
144
145
|
config,
|
|
@@ -164,21 +165,21 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
|
|
164
165
|
throw new Error(`Job mode ${job.mode} not implemented`)
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
return vmBackup.run().catch(error => {
|
|
169
|
+
taskError = error
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
.then(result => {
|
|
173
|
+
if (taskError === undefined) {
|
|
170
174
|
task.success(result)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
vmBackupFailed(
|
|
175
|
+
} else {
|
|
176
|
+
// ending the task with error or not ending the task
|
|
177
|
+
vmBackupFailed(taskError, task)
|
|
174
178
|
}
|
|
175
179
|
})
|
|
176
180
|
.catch(noop) // errors are handled by logs
|
|
177
181
|
}),
|
|
178
|
-
error =>
|
|
179
|
-
getVmTask().run(() => {
|
|
180
|
-
vmBackupFailed(error)
|
|
181
|
-
})
|
|
182
|
+
error => vmBackupFailed(error, getVmTask())
|
|
182
183
|
)
|
|
183
184
|
}
|
|
184
185
|
const { concurrency } = settings
|
package/_runners/_Abstract.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import Disposable from 'promise-toolbox/Disposable'
|
|
2
2
|
import pTimeout from 'promise-toolbox/timeout'
|
|
3
3
|
import { compileTemplate } from '@xen-orchestra/template'
|
|
4
|
-
import { runTask } from './_runTask.mjs'
|
|
5
4
|
import { RemoteTimeoutError } from './_RemoteTimeoutError.mjs'
|
|
5
|
+
import { Task } from '@vates/task'
|
|
6
|
+
|
|
7
|
+
const noop = Function.prototype
|
|
6
8
|
|
|
7
9
|
export const DEFAULT_SETTINGS = {
|
|
8
10
|
getRemoteTimeout: 300e3,
|
|
@@ -36,13 +38,16 @@ export const Abstract = class AbstractRunner {
|
|
|
36
38
|
})
|
|
37
39
|
} catch (error) {
|
|
38
40
|
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
|
39
|
-
|
|
41
|
+
Task.run(
|
|
40
42
|
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
properties: {
|
|
44
|
+
id: remoteId,
|
|
45
|
+
name: 'get remote adapter',
|
|
46
|
+
type: 'remote',
|
|
47
|
+
},
|
|
43
48
|
},
|
|
44
49
|
() => Promise.reject(error)
|
|
45
|
-
)
|
|
50
|
+
).catch(noop)
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
2
|
+
import { Task } from '@vates/task'
|
|
2
3
|
|
|
3
4
|
import { DIR_XO_POOL_METADATA_BACKUPS } from '../RemoteAdapter.mjs'
|
|
4
5
|
import { forkStreamUnpipe } from './_forkStreamUnpipe.mjs'
|
|
5
6
|
import { formatFilenameDate } from '../_filenameDate.mjs'
|
|
6
|
-
import { Task } from '../Task.mjs'
|
|
7
7
|
|
|
8
8
|
export const PATH_DB_DUMP = '/pool/xmldbdump'
|
|
9
9
|
|
|
@@ -54,8 +54,8 @@ export class PoolMetadataBackup {
|
|
|
54
54
|
([remoteId, adapter]) =>
|
|
55
55
|
Task.run(
|
|
56
56
|
{
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
properties: {
|
|
58
|
+
name: `Starting metadata backup for the pool (${pool.$id}) for the remote (${remoteId}). (${job.id})`,
|
|
59
59
|
id: remoteId,
|
|
60
60
|
type: 'remote',
|
|
61
61
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
2
2
|
import { join } from '@xen-orchestra/fs/path'
|
|
3
|
+
import { Task } from '@vates/task'
|
|
3
4
|
|
|
4
5
|
import { DIR_XO_CONFIG_BACKUPS } from '../RemoteAdapter.mjs'
|
|
5
6
|
import { formatFilenameDate } from '../_filenameDate.mjs'
|
|
6
|
-
import { Task } from '../Task.mjs'
|
|
7
7
|
|
|
8
8
|
export class XoMetadataBackup {
|
|
9
9
|
constructor({ config, job, remoteAdapters, schedule, settings }) {
|
|
@@ -51,8 +51,8 @@ export class XoMetadataBackup {
|
|
|
51
51
|
([remoteId, adapter]) =>
|
|
52
52
|
Task.run(
|
|
53
53
|
{
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
properties: {
|
|
55
|
+
name: `Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
|
|
56
56
|
id: remoteId,
|
|
57
57
|
type: 'remote',
|
|
58
58
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '@xen-orchestra/log'
|
|
2
|
+
import { Task } from '@vates/task'
|
|
2
3
|
import keyBy from 'lodash/keyBy.js'
|
|
3
4
|
|
|
4
5
|
import { AbstractXapi } from './_AbstractXapi.mjs'
|
|
@@ -6,7 +7,6 @@ import { forkDeltaExport } from './_forkDeltaExport.mjs'
|
|
|
6
7
|
import { exportIncrementalVm } from '../../_incrementalVm.mjs'
|
|
7
8
|
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
|
|
8
9
|
import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
|
|
9
|
-
import { Task } from '../../Task.mjs'
|
|
10
10
|
import {
|
|
11
11
|
DATETIME,
|
|
12
12
|
DELTA_CHAIN_LENGTH,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
2
2
|
import { createLogger } from '@xen-orchestra/log'
|
|
3
|
-
import { Task } from '
|
|
3
|
+
import { Task } from '@vates/task'
|
|
4
4
|
|
|
5
5
|
const { debug, warn } = createLogger('xo:backups:AbstractVmRunner')
|
|
6
6
|
|
|
@@ -83,7 +83,7 @@ export const Abstract = class AbstractVmBackupRunner {
|
|
|
83
83
|
// create a task to have an info in the logs and reports
|
|
84
84
|
return Task.run(
|
|
85
85
|
{
|
|
86
|
-
name: 'health check',
|
|
86
|
+
properties: { name: 'health check' },
|
|
87
87
|
},
|
|
88
88
|
() => {
|
|
89
89
|
Task.info(`This VM doesn't match the health check's tags for this schedule`)
|
|
@@ -4,12 +4,12 @@ import { decorateMethodsWith } from '@vates/decorate-with'
|
|
|
4
4
|
import { defer } from 'golike-defer'
|
|
5
5
|
import { Disposable } from 'promise-toolbox'
|
|
6
6
|
import { createPredicate } from 'value-matcher'
|
|
7
|
+
import { Task } from '@vates/task'
|
|
7
8
|
|
|
8
9
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
9
10
|
|
|
10
11
|
import { Abstract } from './_Abstract.mjs'
|
|
11
12
|
import { extractIdsFromSimplePattern } from '../../extractIdsFromSimplePattern.mjs'
|
|
12
|
-
import { Task } from '../../Task.mjs'
|
|
13
13
|
|
|
14
14
|
export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
|
|
15
15
|
_filterPredicate
|
|
@@ -6,9 +6,9 @@ import { asyncMap } from '@xen-orchestra/async-map'
|
|
|
6
6
|
import { asyncEach } from '@vates/async-each'
|
|
7
7
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
|
8
8
|
import { defer } from 'golike-defer'
|
|
9
|
+
import { Task } from '@vates/task'
|
|
9
10
|
|
|
10
11
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
11
|
-
import { Task } from '../../Task.mjs'
|
|
12
12
|
import { Abstract } from './_Abstract.mjs'
|
|
13
13
|
import {
|
|
14
14
|
COPY_OF,
|
|
@@ -186,7 +186,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
186
186
|
const settings = this._settings
|
|
187
187
|
|
|
188
188
|
if (await this._mustDoSnapshot()) {
|
|
189
|
-
await Task.run({ name: 'snapshot' }, async () => {
|
|
189
|
+
await Task.run({ properties: { name: 'snapshot' } }, async () => {
|
|
190
190
|
if (!settings.bypassVdiChainsCheck) {
|
|
191
191
|
await vm.$assertHealthyVdiChains()
|
|
192
192
|
}
|
|
@@ -338,6 +338,20 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
this._jobSnapshotVdis = Object.values(vdiCandidates)
|
|
341
|
+
|
|
342
|
+
// For VMs with no disks, retention must be tracked directly on VM snapshots
|
|
343
|
+
// since there are no VDIs to anchor the discovery.
|
|
344
|
+
if (vdiUuids.length === 0) {
|
|
345
|
+
this._disklessJobSnapshotVms = this._vm.$snapshots.filter(Boolean).filter(
|
|
346
|
+
({ other_config, $snapshot_of }) =>
|
|
347
|
+
$snapshot_of !== undefined &&
|
|
348
|
+
other_config[JOB_ID] === jobId &&
|
|
349
|
+
other_config[VM_UUID] === this._vm.uuid &&
|
|
350
|
+
other_config[COPY_OF] === undefined
|
|
351
|
+
)
|
|
352
|
+
} else {
|
|
353
|
+
this._disklessJobSnapshotVms = []
|
|
354
|
+
}
|
|
341
355
|
}
|
|
342
356
|
|
|
343
357
|
async _removeUnusedSnapshots() {
|
|
@@ -349,9 +363,10 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
349
363
|
await xapi.barrier()
|
|
350
364
|
// ensure cached object are up to date
|
|
351
365
|
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
|
|
366
|
+
const disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
|
|
352
367
|
|
|
353
|
-
// get the datetime of the most recent snapshot
|
|
354
|
-
const lastSnapshotDateTime = this._jobSnapshotVdis
|
|
368
|
+
// get the datetime of the most recent snapshot across both VDI and diskless VM snapshots
|
|
369
|
+
const lastSnapshotDateTime = [...this._jobSnapshotVdis, ...disklessVmSnapshots]
|
|
355
370
|
.map(({ other_config }) => other_config[DATETIME])
|
|
356
371
|
.sort()
|
|
357
372
|
.pop()
|
|
@@ -428,6 +443,28 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
428
443
|
})
|
|
429
444
|
})
|
|
430
445
|
|
|
446
|
+
// Retention for VMs with no disks: VM snapshots are not reachable via VDIs
|
|
447
|
+
if (disklessVmSnapshots.length > 0) {
|
|
448
|
+
const snapshotsPerSchedule = groupBy(disklessVmSnapshots, _ => _.other_config[SCHEDULE_ID])
|
|
449
|
+
await asyncEach(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
|
|
450
|
+
// we only have one snapshot per date time since it's at the VM level
|
|
451
|
+
const snapshotPerDatetime = Object.fromEntries(snapshots.map(s => [s.other_config[DATETIME], s.$ref]))
|
|
452
|
+
const datetimes = Object.keys(snapshotPerDatetime).sort()
|
|
453
|
+
const settings = {
|
|
454
|
+
...baseSettings,
|
|
455
|
+
...allSettings[scheduleId],
|
|
456
|
+
...allSettings[this._vm.uuid],
|
|
457
|
+
}
|
|
458
|
+
const retention = settings.snapshotRetention ?? 0
|
|
459
|
+
await asyncEach(getOldEntries(retention, datetimes), async datetime => {
|
|
460
|
+
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
await xapi.VM_destroy(snapshotPerDatetime[datetime])
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
431
468
|
// list and remove the snapshot were the jobs failed between
|
|
432
469
|
// makesnapshot and update_other_config
|
|
433
470
|
const snapshots = this._vm.$snapshots.filter(_ => !!_).filter(({ name_label }) => name_label === TEMP_SNAPSHOT_NAME)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { Task } from '@vates/task'
|
|
2
|
+
|
|
1
3
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
2
4
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
3
|
-
import { Task } from '../../Task.mjs'
|
|
4
5
|
|
|
5
6
|
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
|
|
6
7
|
import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
|
|
@@ -9,11 +10,11 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
|
9
10
|
constructor(props) {
|
|
10
11
|
super(props)
|
|
11
12
|
|
|
12
|
-
this.run = Task.
|
|
13
|
+
this.run = Task.wrap(
|
|
13
14
|
{
|
|
14
|
-
|
|
15
|
-
data: {
|
|
15
|
+
properties: {
|
|
16
16
|
id: props.remoteId,
|
|
17
|
+
name: 'export',
|
|
17
18
|
type: 'remote',
|
|
18
19
|
|
|
19
20
|
// necessary?
|
|
@@ -64,7 +65,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
|
64
65
|
await deleteOldBackups()
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
await Task.run({ name: 'transfer' }, async () => {
|
|
68
|
+
await Task.run({ properties: { name: 'transfer' } }, async () => {
|
|
68
69
|
await adapter.outputStream(dataFilename, stream, {
|
|
69
70
|
maxStreamLength,
|
|
70
71
|
streamLength,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
2
2
|
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
|
|
3
|
+
import { Task } from '@vates/task'
|
|
3
4
|
|
|
4
5
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
5
6
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
6
|
-
import { Task } from '../../Task.mjs'
|
|
7
7
|
|
|
8
8
|
import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
|
|
9
9
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
@@ -14,11 +14,11 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
|
|
|
14
14
|
constructor(props) {
|
|
15
15
|
super(props)
|
|
16
16
|
|
|
17
|
-
this.run = Task.
|
|
17
|
+
this.run = Task.wrap(
|
|
18
18
|
{
|
|
19
|
-
|
|
20
|
-
data: {
|
|
19
|
+
properties: {
|
|
21
20
|
id: props.sr.uuid,
|
|
21
|
+
name: 'export',
|
|
22
22
|
name_label: this._sr.name_label,
|
|
23
23
|
type: 'SR',
|
|
24
24
|
|
|
@@ -52,7 +52,7 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
let targetVmRef
|
|
55
|
-
await Task.run({ name: 'transfer' }, async () => {
|
|
55
|
+
await Task.run({ properties: { name: 'transfer' } }, async () => {
|
|
56
56
|
targetVmRef = await xapi.VM_import(stream, sr.$ref, vm =>
|
|
57
57
|
Promise.all([
|
|
58
58
|
!_warmMigration && vm.add_tags('Disaster Recovery'),
|
|
@@ -7,10 +7,10 @@ import { createLogger } from '@xen-orchestra/log'
|
|
|
7
7
|
import { decorateClass } from '@vates/decorate-with'
|
|
8
8
|
import { defer } from 'golike-defer'
|
|
9
9
|
import { dirname, basename } from 'node:path'
|
|
10
|
+
import { Task } from '@vates/task'
|
|
10
11
|
|
|
11
12
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
12
13
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
13
|
-
import { Task } from '../../Task.mjs'
|
|
14
14
|
|
|
15
15
|
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
|
|
16
16
|
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
|
|
@@ -72,19 +72,20 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
72
72
|
prepare({ isFull }) {
|
|
73
73
|
// create the task related to this export and ensure all methods are called in this context
|
|
74
74
|
const task = new Task({
|
|
75
|
-
|
|
76
|
-
data: {
|
|
75
|
+
properties: {
|
|
77
76
|
id: this._remoteId,
|
|
78
77
|
isFull,
|
|
78
|
+
name: 'export',
|
|
79
79
|
type: 'remote',
|
|
80
80
|
},
|
|
81
81
|
})
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
this.
|
|
82
|
+
this._prepare = task.wrapInside(this._prepare)
|
|
83
|
+
this.transfer = task.wrapInside(this.transfer)
|
|
84
|
+
this.healthCheck = task.wrapInside(this.healthCheck)
|
|
85
|
+
this.cleanup = task.wrapInside(this.cleanup)
|
|
86
|
+
this.afterBackup = task.wrap(this.afterBackup)
|
|
86
87
|
|
|
87
|
-
return
|
|
88
|
+
return this._prepare()
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async _prepare() {
|
|
@@ -223,19 +224,18 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
223
224
|
vtpms: deltaExport.vtpms,
|
|
224
225
|
}
|
|
225
226
|
let size = 0
|
|
226
|
-
await Task.run({ name: 'transfer' }, async () => {
|
|
227
|
+
await Task.run({ properties: { name: 'transfer' } }, async () => {
|
|
227
228
|
await asyncEach(
|
|
228
229
|
Object.entries(deltaExport.disks),
|
|
229
230
|
async ([diskRef, disk]) => {
|
|
230
231
|
const path = `${this._vmBackupDir}/${vhds[diskRef]}`
|
|
231
|
-
await adapter.writeVhd(path, disk, {
|
|
232
|
+
size += await adapter.writeVhd(path, disk, {
|
|
232
233
|
// no checksum for VHDs, because they will be invalidated by
|
|
233
234
|
// merges and chains
|
|
234
235
|
checksum: false,
|
|
235
236
|
validator: tmpPath => checkVhd(handler, tmpPath),
|
|
236
237
|
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
|
237
238
|
})
|
|
238
|
-
size = size + disk.getNbGeneratedBlock() * disk.getBlockSize()
|
|
239
239
|
},
|
|
240
240
|
{
|
|
241
241
|
concurrency: settings.diskPerVmConcurrency,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import humanFormat from 'human-format'
|
|
2
2
|
|
|
3
3
|
import { asyncMapSettled } from '@xen-orchestra/async-map'
|
|
4
|
+
import { Task } from '@vates/task'
|
|
4
5
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
5
6
|
|
|
6
7
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
7
8
|
import { importIncrementalVm } from '../../_incrementalVm.mjs'
|
|
8
|
-
import { Task } from '../../Task.mjs'
|
|
9
9
|
|
|
10
10
|
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
|
|
11
11
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
@@ -57,71 +57,14 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
57
57
|
debug('checkBaseVdis, got snapshot candidates,', snapshotCandidates.length)
|
|
58
58
|
|
|
59
59
|
if (snapshotCandidates.length > 0) {
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let activeVdi
|
|
69
|
-
try {
|
|
70
|
-
activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
|
|
71
|
-
const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
72
|
-
if (userVbds.length !== 1) {
|
|
73
|
-
debug('checkBaseVdis, share vbd ', { ref: snapshot.$ref, userVbds })
|
|
74
|
-
// shared vdi ignore
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
const vm = userVbds[0].$VM
|
|
78
|
-
if (!('start' in vm.blocked_operations)) {
|
|
79
|
-
debug('checkBaseVdis, vm not blocked', { vmRef: vm.$ref })
|
|
80
|
-
// vm start unlocked
|
|
81
|
-
// not really an issue since we have check the delta
|
|
82
|
-
// but it indicates the users played with the blocked operations
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
diffDisk = new XapiDiskSource({
|
|
86
|
-
xapi: sr.$xapi,
|
|
87
|
-
vdiRef: activeVdi.$ref,
|
|
88
|
-
baseRef: snapshot.$ref,
|
|
89
|
-
onlyListChangedBlocks: true,
|
|
90
|
-
})
|
|
91
|
-
await diffDisk.init()
|
|
92
|
-
if (diffDisk.getBlockIndexes().length === 0) {
|
|
93
|
-
const sourceUuid = snapshot.other_config?.[COPY_OF]
|
|
94
|
-
if (sourceUuid) {
|
|
95
|
-
this.#baseVdisBySourceUuid.set(sourceUuid, activeVdi)
|
|
96
|
-
}
|
|
97
|
-
// Track the target VM (the replicated VM to update on the next transfer).
|
|
98
|
-
targetVmRef = vm.$ref
|
|
99
|
-
} else {
|
|
100
|
-
// not empty, we will create a new VM
|
|
101
|
-
canChainToTargetVm = false
|
|
102
|
-
debug('checkBaseVdis, data between snapshot and active disk', {
|
|
103
|
-
vdiRef: snapshot.$ref,
|
|
104
|
-
nbBlocks: diffDisk.getBlockIndexes().length,
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
debug('checkBaseVdis, skipping snapshot', { ref: snapshot.$ref, error })
|
|
109
|
-
return
|
|
110
|
-
} finally {
|
|
111
|
-
await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
|
|
112
|
-
await sr.$xapi.VDI_disconnectFromControlDomain(snapshot.$ref)
|
|
113
|
-
if (activeVdi !== undefined) {
|
|
114
|
-
await sr.$xapi.VDI_disconnectFromControlDomain(activeVdi.$ref)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
concurrency: 4,
|
|
120
|
-
}
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
if (canChainToTargetVm && targetVmRef !== undefined) {
|
|
124
|
-
debug('checkBaseVdis,got a valid vm target', targetVmRef)
|
|
60
|
+
// reset before searching for candidates
|
|
61
|
+
this.#baseVdisBySourceUuid = new Map()
|
|
62
|
+
this._targetVmRef = undefined
|
|
63
|
+
const { baseVdisBySourceUuid, targetVmRef } = await this.#validateSnapshotCandidates(snapshotCandidates)
|
|
64
|
+
for (const [sourceUuid, vdi] of baseVdisBySourceUuid) {
|
|
65
|
+
this.#baseVdisBySourceUuid.set(sourceUuid, vdi)
|
|
66
|
+
}
|
|
67
|
+
if (targetVmRef !== undefined) {
|
|
125
68
|
this._targetVmRef = targetVmRef
|
|
126
69
|
}
|
|
127
70
|
} else {
|
|
@@ -145,26 +88,117 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
145
88
|
}
|
|
146
89
|
}
|
|
147
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* 6.3+ snapshot-based validation: for each snapshot candidate, check whether
|
|
93
|
+
* the active VDI has diverged from the snapshot. Returns a baseVdisBySourceUuid
|
|
94
|
+
* map and, when all disks are clean, the targetVmRef to reuse.
|
|
95
|
+
*/
|
|
96
|
+
async #validateSnapshotCandidates(snapshotCandidates) {
|
|
97
|
+
const sr = this._sr
|
|
98
|
+
const baseVdisBySourceUuid = new Map()
|
|
99
|
+
let targetVmRef
|
|
100
|
+
let canChainToTargetVm = true
|
|
101
|
+
|
|
102
|
+
await asyncEach(
|
|
103
|
+
snapshotCandidates,
|
|
104
|
+
async snapshot => {
|
|
105
|
+
let diffDisk
|
|
106
|
+
let activeVdi
|
|
107
|
+
try {
|
|
108
|
+
activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
|
|
109
|
+
const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
|
|
110
|
+
if (userVbds.length !== 1) {
|
|
111
|
+
debug('checkBaseVdis, shared vbd ', { ref: snapshot.$ref, userVbds })
|
|
112
|
+
// shared vdi ignore
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const vm = userVbds[0].$VM
|
|
116
|
+
if (!('start' in vm.blocked_operations)) {
|
|
117
|
+
debug('checkBaseVdis, vm not blocked', { vmRef: vm.$ref })
|
|
118
|
+
// vm start unlocked
|
|
119
|
+
// not really an issue since we have check the delta
|
|
120
|
+
// but it indicates the users played with the blocked operations
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
diffDisk = new XapiDiskSource({
|
|
124
|
+
xapi: sr.$xapi,
|
|
125
|
+
vdiRef: activeVdi.$ref,
|
|
126
|
+
baseRef: snapshot.$ref,
|
|
127
|
+
onlyListChangedBlocks: true,
|
|
128
|
+
})
|
|
129
|
+
await diffDisk.init()
|
|
130
|
+
const sourceUuid = snapshot.other_config?.[COPY_OF]
|
|
131
|
+
if (diffDisk.getBlockIndexes().length === 0) {
|
|
132
|
+
// no block modification since the common snapshot, we can chain VM and disk
|
|
133
|
+
if (sourceUuid) {
|
|
134
|
+
baseVdisBySourceUuid.set(sourceUuid, activeVdi)
|
|
135
|
+
}
|
|
136
|
+
// Track the target VM (the replicated VM to update on the next transfer).
|
|
137
|
+
targetVmRef = vm.$ref
|
|
138
|
+
} else {
|
|
139
|
+
if (sourceUuid) {
|
|
140
|
+
baseVdisBySourceUuid.set(sourceUuid, snapshot)
|
|
141
|
+
}
|
|
142
|
+
// there are changed block since the snapshot
|
|
143
|
+
// we can reuse it to transfer a delta, but we will
|
|
144
|
+
// create a new VM
|
|
145
|
+
canChainToTargetVm = false
|
|
146
|
+
debug('checkBaseVdis, data between snapshot and active disk', {
|
|
147
|
+
vdiRef: snapshot.$ref,
|
|
148
|
+
nbBlocks: diffDisk.getBlockIndexes().length,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
debug('checkBaseVdis, skipping snapshot', { ref: snapshot.$ref, error })
|
|
153
|
+
return
|
|
154
|
+
} finally {
|
|
155
|
+
await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
|
|
156
|
+
await sr.$xapi.VDI_disconnectFromControlDomain(snapshot.$ref)
|
|
157
|
+
if (activeVdi !== undefined) {
|
|
158
|
+
await sr.$xapi.VDI_disconnectFromControlDomain(activeVdi.$ref)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
concurrency: 4,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if (!canChainToTargetVm) {
|
|
168
|
+
// if at least one disk has new data, create a new VM
|
|
169
|
+
// instead of updating it
|
|
170
|
+
targetVmRef = undefined
|
|
171
|
+
} else if (targetVmRef !== undefined) {
|
|
172
|
+
debug('checkBaseVdis,got a valid vm target', targetVmRef)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { baseVdisBySourceUuid, targetVmRef }
|
|
176
|
+
}
|
|
177
|
+
|
|
148
178
|
updateUuidAndChain() {
|
|
149
179
|
// nothing to do, the chaining is not modified in this case
|
|
150
180
|
}
|
|
151
181
|
prepare({ isFull }) {
|
|
152
182
|
// create the task related to this export and ensure all methods are called in this context
|
|
153
183
|
const task = new Task({
|
|
154
|
-
|
|
155
|
-
data: {
|
|
184
|
+
properties: {
|
|
156
185
|
id: this._sr.uuid,
|
|
157
186
|
isFull,
|
|
187
|
+
name: 'export',
|
|
158
188
|
name_label: this._sr.name_label,
|
|
159
189
|
type: 'SR',
|
|
160
190
|
},
|
|
161
191
|
})
|
|
162
|
-
|
|
163
|
-
this.transfer = task.
|
|
164
|
-
|
|
165
|
-
|
|
192
|
+
this._prepare = task.wrapInside(this._prepare)
|
|
193
|
+
this.transfer = task.wrapInside(this.transfer)
|
|
194
|
+
if (this._healthCheckSr !== undefined) {
|
|
195
|
+
this.cleanup = task.wrapInside(this.cleanup)
|
|
196
|
+
this.healthCheck = task.wrap(this.healthCheck)
|
|
197
|
+
} else {
|
|
198
|
+
this.cleanup = task.wrap(this.cleanup)
|
|
199
|
+
}
|
|
166
200
|
|
|
167
|
-
return
|
|
201
|
+
return this._prepare(isFull)
|
|
168
202
|
}
|
|
169
203
|
|
|
170
204
|
async _prepare(isFull) {
|
|
@@ -265,7 +299,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
265
299
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
266
300
|
|
|
267
301
|
let targetVmRef
|
|
268
|
-
await Task.run({ name: 'transfer' }, async () => {
|
|
302
|
+
await Task.run({ properties: { name: 'transfer' } }, async () => {
|
|
269
303
|
targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport, timestamp), sr, {
|
|
270
304
|
targetRef: this._targetVmRef,
|
|
271
305
|
})
|
|
@@ -289,7 +323,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
289
323
|
` -- last replication: ${formatFilenameDate(timestamp)} ${humanFormat.bytes(size)} read`
|
|
290
324
|
)
|
|
291
325
|
// take a snapshot to ensure these data are not modified until next snapshot
|
|
292
|
-
await Task.run({ name: 'target snapshot' }, async () => {
|
|
326
|
+
await Task.run({ properties: { name: 'target snapshot' } }, async () => {
|
|
293
327
|
await xapi.VM_snapshot(targetVmRef, {
|
|
294
328
|
name_label: `${vm.name_label} - ${job.name} / ${schedule.name} ${formatFilenameDate(timestamp)}`,
|
|
295
329
|
})
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createLogger } from '@xen-orchestra/log'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { Task } from '@vates/task'
|
|
3
4
|
import assert from 'node:assert'
|
|
4
5
|
|
|
5
6
|
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
6
7
|
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
7
8
|
import { HealthCheckVmBackup } from '../../HealthCheckVmBackup.mjs'
|
|
8
9
|
import { ImportVmBackup } from '../../ImportVmBackup.mjs'
|
|
9
|
-
import { Task } from '../../Task.mjs'
|
|
10
10
|
import * as MergeWorker from '../../merge-worker/index.mjs'
|
|
11
11
|
import ms from 'ms'
|
|
12
12
|
import { getEntryStatus } from '../../_getOldEntries.mjs'
|
|
@@ -28,7 +28,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
28
28
|
|
|
29
29
|
async _cleanVm(options) {
|
|
30
30
|
try {
|
|
31
|
-
return await Task.run({ name: 'clean-vm' }, () => {
|
|
31
|
+
return await Task.run({ properties: { name: 'clean-vm' } }, () => {
|
|
32
32
|
return this._adapter.cleanVm(this._vmBackupDir, {
|
|
33
33
|
...options,
|
|
34
34
|
fixMetadata: true,
|
|
@@ -100,7 +100,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
100
100
|
}
|
|
101
101
|
return Task.run(
|
|
102
102
|
{
|
|
103
|
-
name: 'health check',
|
|
103
|
+
properties: { name: 'health check' },
|
|
104
104
|
},
|
|
105
105
|
async () => {
|
|
106
106
|
const xapi = sr.$xapi
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { Task } from '@vates/task'
|
|
2
3
|
|
|
3
4
|
import { HealthCheckVmBackup } from '../../HealthCheckVmBackup.mjs'
|
|
4
|
-
import { Task } from '../../Task.mjs'
|
|
5
5
|
import ms from 'ms'
|
|
6
6
|
|
|
7
7
|
export const MixinXapiWriter = (BaseClass = Object) =>
|
|
@@ -33,7 +33,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
|
|
33
33
|
// copy VM
|
|
34
34
|
return Task.run(
|
|
35
35
|
{
|
|
36
|
-
name: 'health check',
|
|
36
|
+
properties: { name: 'health check' },
|
|
37
37
|
},
|
|
38
38
|
async () => {
|
|
39
39
|
const { $xapi: xapi } = sr
|
|
@@ -47,12 +47,12 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
|
|
47
47
|
}
|
|
48
48
|
if (await this.#isAlreadyOnHealthCheckSr(baseVm)) {
|
|
49
49
|
healthCheckVmRef = await Task.run(
|
|
50
|
-
{ name: 'cloning-vm' },
|
|
50
|
+
{ properties: { name: 'cloning-vm' } },
|
|
51
51
|
async () => await xapi.callAsync('VM.clone', this._targetVmRef, `Health Check - ${baseVm.name_label}`)
|
|
52
52
|
)
|
|
53
53
|
} else {
|
|
54
54
|
healthCheckVmRef = await Task.run(
|
|
55
|
-
{ name: 'copying-vm' },
|
|
55
|
+
{ properties: { name: 'copying-vm' } },
|
|
56
56
|
async () =>
|
|
57
57
|
await xapi.callAsync(
|
|
58
58
|
'VM.copy',
|
|
@@ -222,6 +222,7 @@ export class MergeRemoteDisk {
|
|
|
222
222
|
await this.#mergeBlocks(parentDisk, childDisk)
|
|
223
223
|
await parentDisk.flushMetadata(childDisk)
|
|
224
224
|
await parentDisk.mergeMetadata(childDisk)
|
|
225
|
+
this.#state.diskSize = parentDisk.getSizeOnDisk()
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
/**
|
|
@@ -266,8 +267,6 @@ export class MergeRemoteDisk {
|
|
|
266
267
|
|
|
267
268
|
await this.#writeState()
|
|
268
269
|
|
|
269
|
-
this.#state.diskSize = childDisk.getSizeOnDisk()
|
|
270
|
-
|
|
271
270
|
this.#onProgress({ total: nBlocks, done: nBlocks })
|
|
272
271
|
}
|
|
273
272
|
|
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.72.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -20,21 +20,22 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@iarna/toml": "^2.2.5",
|
|
22
22
|
"@kldzj/stream-throttle": "^1.1.1",
|
|
23
|
-
"@vates/async-each": "^1.0.
|
|
23
|
+
"@vates/async-each": "^1.0.3",
|
|
24
24
|
"@vates/cached-dns.lookup": "^1.0.0",
|
|
25
25
|
"@vates/compose": "^2.1.0",
|
|
26
26
|
"@vates/decorate-with": "^2.1.0",
|
|
27
27
|
"@vates/disposable": "^0.1.6",
|
|
28
28
|
"@vates/fuse-vhd": "^2.1.2",
|
|
29
|
-
"@vates/generator-toolbox": "^1.1.
|
|
30
|
-
"@vates/nbd-client": "^3.
|
|
29
|
+
"@vates/generator-toolbox": "^1.1.2",
|
|
30
|
+
"@vates/nbd-client": "^3.4.0",
|
|
31
31
|
"@vates/parse-duration": "^0.1.1",
|
|
32
|
-
"@
|
|
33
|
-
"@xen-orchestra/
|
|
34
|
-
"@xen-orchestra/
|
|
35
|
-
"@xen-orchestra/
|
|
36
|
-
"@xen-orchestra/
|
|
37
|
-
"@xen-orchestra/
|
|
32
|
+
"@vates/task": "^0.7.0",
|
|
33
|
+
"@xen-orchestra/async-map": "^0.1.3",
|
|
34
|
+
"@xen-orchestra/disk-transform": "^1.2.3",
|
|
35
|
+
"@xen-orchestra/fs": "^4.8.0",
|
|
36
|
+
"@xen-orchestra/log": "^0.7.2",
|
|
37
|
+
"@xen-orchestra/qcow2": "^1.3.0",
|
|
38
|
+
"@xen-orchestra/template": "^0.1.1",
|
|
38
39
|
"app-conf": "^3.0.0",
|
|
39
40
|
"compare-versions": "^6.0.0",
|
|
40
41
|
"d3-time-format": "^4.1.0",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"golike-defer": "^0.5.1",
|
|
43
44
|
"human-format": "^1.2.0",
|
|
44
45
|
"limit-concurrency-decorator": "^0.6.0",
|
|
45
|
-
"lodash": "^4.
|
|
46
|
+
"lodash": "^4.18.0",
|
|
46
47
|
"moment-timezone": "^0.5.46",
|
|
47
48
|
"ms": "^2.1.3",
|
|
48
49
|
"node-zone": "^0.4.0",
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
"tar": "^7.5.3",
|
|
53
54
|
"uuid": "^9.0.0",
|
|
54
55
|
"value-matcher": "^0.2.0",
|
|
55
|
-
"vhd-lib": "^4.
|
|
56
|
-
"xen-api": "^4.7.
|
|
56
|
+
"vhd-lib": "^4.16.0",
|
|
57
|
+
"xen-api": "^4.7.7",
|
|
57
58
|
"yazl": "^2.5.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"tmp": "^0.2.1"
|
|
64
65
|
},
|
|
65
66
|
"peerDependencies": {
|
|
66
|
-
"@xen-orchestra/xapi": "^8.7.
|
|
67
|
+
"@xen-orchestra/xapi": "^8.7.2"
|
|
67
68
|
},
|
|
68
69
|
"license": "AGPL-3.0-or-later",
|
|
69
70
|
"author": {
|
package/Task.mjs
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import CancelToken from 'promise-toolbox/CancelToken'
|
|
2
|
-
import Zone from 'node-zone'
|
|
3
|
-
|
|
4
|
-
const logAfterEnd = log => {
|
|
5
|
-
const error = new Error('task has already ended')
|
|
6
|
-
error.log = log
|
|
7
|
-
throw error
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const noop = Function.prototype
|
|
11
|
-
|
|
12
|
-
const serializeErrors = errors => (Array.isArray(errors) ? errors.map(serializeError) : errors)
|
|
13
|
-
|
|
14
|
-
// Create a serializable object from an error.
|
|
15
|
-
//
|
|
16
|
-
// Otherwise some fields might be non-enumerable and missing from logs.
|
|
17
|
-
const serializeError = error =>
|
|
18
|
-
error instanceof Error
|
|
19
|
-
? {
|
|
20
|
-
...error, // Copy enumerable properties.
|
|
21
|
-
code: error.code,
|
|
22
|
-
errors: serializeErrors(error.errors), // supports AggregateError
|
|
23
|
-
message: error.message,
|
|
24
|
-
name: error.name,
|
|
25
|
-
stack: error.stack,
|
|
26
|
-
}
|
|
27
|
-
: error
|
|
28
|
-
|
|
29
|
-
const $$task = Symbol('@xen-orchestra/backups/Task')
|
|
30
|
-
|
|
31
|
-
export class Task {
|
|
32
|
-
static get cancelToken() {
|
|
33
|
-
const task = Zone.current.data[$$task]
|
|
34
|
-
return task !== undefined ? task.#cancelToken : CancelToken.none
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static run(opts, fn) {
|
|
38
|
-
return new this(opts).run(fn, true)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
static wrapFn(opts, fn) {
|
|
42
|
-
// compatibility with @decorateWith
|
|
43
|
-
if (typeof fn !== 'function') {
|
|
44
|
-
;[fn, opts] = [opts, fn]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return function () {
|
|
48
|
-
return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
#cancelToken
|
|
53
|
-
#id = Math.random().toString(36).slice(2)
|
|
54
|
-
#onLog
|
|
55
|
-
#zone
|
|
56
|
-
|
|
57
|
-
constructor({ name, data, onLog }) {
|
|
58
|
-
let parentCancelToken, parentId
|
|
59
|
-
if (onLog === undefined) {
|
|
60
|
-
const parent = Zone.current.data[$$task]
|
|
61
|
-
if (parent === undefined) {
|
|
62
|
-
onLog = noop
|
|
63
|
-
} else {
|
|
64
|
-
onLog = log => parent.#onLog(log)
|
|
65
|
-
parentCancelToken = parent.#cancelToken
|
|
66
|
-
parentId = parent.#id
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const zone = Zone.current.fork('@xen-orchestra/backups/Task')
|
|
71
|
-
zone.data[$$task] = this
|
|
72
|
-
this.#zone = zone
|
|
73
|
-
|
|
74
|
-
const { cancel, token } = CancelToken.source(parentCancelToken && [parentCancelToken])
|
|
75
|
-
this.#cancelToken = token
|
|
76
|
-
this.cancel = cancel
|
|
77
|
-
|
|
78
|
-
this.#onLog = onLog
|
|
79
|
-
|
|
80
|
-
this.#log('start', {
|
|
81
|
-
data,
|
|
82
|
-
message: name,
|
|
83
|
-
parentId,
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
failure(error) {
|
|
88
|
-
this.#end('failure', serializeError(error))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
info(message, data) {
|
|
92
|
-
this.#log('info', { data, message })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Run a function in the context of this task
|
|
97
|
-
*
|
|
98
|
-
* In case of error, the task will be failed.
|
|
99
|
-
*
|
|
100
|
-
* @typedef Result
|
|
101
|
-
* @param {() => Result} fn
|
|
102
|
-
* @param {boolean} last - Whether the task should succeed if there is no error
|
|
103
|
-
* @returns Result
|
|
104
|
-
*/
|
|
105
|
-
run(fn, last = false) {
|
|
106
|
-
return this.#zone.run(() => {
|
|
107
|
-
try {
|
|
108
|
-
const result = fn()
|
|
109
|
-
let then
|
|
110
|
-
if (result != null && typeof (then = result.then) === 'function') {
|
|
111
|
-
then.call(result, last && (value => this.success(value)), error => this.failure(error))
|
|
112
|
-
} else if (last) {
|
|
113
|
-
this.success(result)
|
|
114
|
-
}
|
|
115
|
-
return result
|
|
116
|
-
} catch (error) {
|
|
117
|
-
this.failure(error)
|
|
118
|
-
throw error
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
success(value) {
|
|
124
|
-
this.#end('success', value)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
warning(message, data) {
|
|
128
|
-
this.#log('warning', { data, message })
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
wrapFn(fn, last) {
|
|
132
|
-
const task = this
|
|
133
|
-
return function () {
|
|
134
|
-
return task.run(() => fn.apply(this, arguments), last)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
#end(status, result) {
|
|
139
|
-
this.#log('end', { result, status })
|
|
140
|
-
this.#onLog = logAfterEnd
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
#log(event, props) {
|
|
144
|
-
this.#onLog({
|
|
145
|
-
...props,
|
|
146
|
-
event,
|
|
147
|
-
taskId: this.#id,
|
|
148
|
-
timestamp: Date.now(),
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
for (const method of ['info', 'warning']) {
|
|
154
|
-
Task[method] = (...args) => Zone.current.data[$$task]?.[method](...args)
|
|
155
|
-
}
|