@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.
@@ -1,4 +1,4 @@
1
- import { Task } from './Task.mjs'
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
@@ -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
- onLog: data =>
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
- // systematically update size and differentials after a merge
586
-
587
- // @todo : after 2024-04-01 remove the fixmetadata options since the size computation is fixed
588
- if (mergedSize || (fixMetadata && fileSystemSize !== size)) {
589
- metadata.size = mergedSize ?? fileSystemSize ?? size
590
-
591
- if (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
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
  }
@@ -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
 
@@ -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
- runTask(
60
+ Task.run(
59
61
  {
60
- name: 'get pool record',
61
- data: { type: 'pool', id },
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
- runTask(
89
+ Task.run(
85
90
  {
86
- name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
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
- runTask(
115
+ Task.run(
111
116
  {
112
- name: `Starting XO metadata backup. (${job.id})`,
113
- data: {
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)
@@ -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', data: { type: 'VM', id: vmUuid } }
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
- .run(async () => {
118
- try {
119
- const result = await vmBackup.run()
120
- task.success(result)
121
- return result
122
- } catch (error) {
123
- if (isLastRun) {
124
- throw error
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
  }
@@ -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
- runTask(
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
- throw error
111
+ return task.failure(error)
114
112
  } else {
115
- Task.warning(`Retry the VM backup due to an error`, {
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', data: { type: 'VM', id: vmUuid } }
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.data.name_label === undefined) {
136
- taskStart.data.name_label = vm.name_label
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
- .run(async () => {
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
- try {
169
- const result = await vmBackup.run()
168
+ return vmBackup.run().catch(error => {
169
+ taskError = error
170
+ })
171
+ })
172
+ .then(result => {
173
+ if (taskError === undefined) {
170
174
  task.success(result)
171
- return result
172
- } catch (error) {
173
- vmBackupFailed(error)
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
@@ -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
- runTask(
41
+ Task.run(
40
42
  {
41
- name: 'get remote adapter',
42
- data: { type: 'remote', id: remoteId },
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
- name: `Starting metadata backup for the pool (${pool.$id}) for the remote (${remoteId}). (${job.id})`,
58
- data: {
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
- name: `Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
55
- data: {
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 '../../Task.mjs'
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.wrapFn(
13
+ this.run = Task.wrap(
13
14
  {
14
- name: 'export',
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.wrapFn(
17
+ this.run = Task.wrap(
18
18
  {
19
- name: 'export',
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
- name: 'export',
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.transfer = task.wrapFn(this.transfer)
83
- this.healthCheck = task.wrapFn(this.healthCheck)
84
- this.cleanup = task.wrapFn(this.cleanup)
85
- this.afterBackup = task.wrapFn(this.afterBackup, true)
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 task.run(() => this._prepare())
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
- // New snapshot-based flow (6.3+): verify no data was written between
61
- // the target snapshot and its active VDI.
62
- let targetVmRef
63
- let canChainToTargetVm = true
64
- await asyncEach(
65
- snapshotCandidates,
66
- async snapshot => {
67
- let diffDisk
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
- name: 'export',
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
- const hasHealthCheckSr = this._healthCheckSr !== undefined
163
- this.transfer = task.wrapFn(this.transfer)
164
- this.cleanup = task.wrapFn(this.cleanup, !hasHealthCheckSr)
165
- this.healthCheck = task.wrapFn(this.healthCheck, hasHealthCheckSr)
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 task.run(() => this._prepare(isFull))
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.71.3",
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.2",
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.1",
30
- "@vates/nbd-client": "^3.3.0",
29
+ "@vates/generator-toolbox": "^1.1.2",
30
+ "@vates/nbd-client": "^3.4.0",
31
31
  "@vates/parse-duration": "^0.1.1",
32
- "@xen-orchestra/async-map": "^0.1.2",
33
- "@xen-orchestra/disk-transform": "^1.2.2",
34
- "@xen-orchestra/fs": "^4.7.0",
35
- "@xen-orchestra/log": "^0.7.1",
36
- "@xen-orchestra/qcow2": "^1.2.0",
37
- "@xen-orchestra/template": "^0.1.0",
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.17.20",
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.15.0",
56
- "xen-api": "^4.7.6",
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.1"
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
- }
@@ -1,5 +0,0 @@
1
- import { Task } from '../Task.mjs'
2
-
3
- const noop = Function.prototype
4
-
5
- export const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs