@xen-orchestra/backups 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,13 @@ import { formatFilenameDate } from './_filenameDate.mjs'
4
4
  import { importIncrementalVm } from './_incrementalVm.mjs'
5
5
  import { Task } from './Task.mjs'
6
6
  import { watchStreamSize } from './_watchStreamSize.mjs'
7
- import { VhdNegative, VhdSynthetic } from 'vhd-lib'
8
7
  import { decorateClass } from '@vates/decorate-with'
9
8
  import { createLogger } from '@xen-orchestra/log'
10
9
  import { dirname, join } from 'node:path'
11
10
  import pickBy from 'lodash/pickBy.js'
12
11
  import { defer } from 'golike-defer'
12
+ import { NegativeDisk } from '@xen-orchestra/disk-transform'
13
+ import { openDiskChain } from './disks/openDiskChain.mjs'
13
14
 
14
15
  const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
15
16
  async function resolveUuid(xapi, cache, uuid, type) {
@@ -59,7 +60,7 @@ export class ImportVmBackup {
59
60
  const metadata = this._metadata
60
61
  const { mapVdisSrs } = this._importIncrementalVmSettings
61
62
  const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
62
- const streams = {}
63
+ const disks = {}
63
64
  const metdataDir = dirname(metadata._filename)
64
65
  const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
65
66
 
@@ -110,20 +111,21 @@ export class ImportVmBackup {
110
111
  }
111
112
  }
112
113
 
113
- let stream
114
+ let disk
114
115
  const backupWithSnapshotPath = join(metdataDir, backupCandidate ?? '')
115
116
  if (vhdPath === backupWithSnapshotPath) {
116
117
  // all the data are already on the host
117
118
  debug('direct reuse of a snapshot')
118
- stream = null
119
+ disk = null
119
120
  vdis[vdiRef].baseVdi = snapshotCandidate
120
121
  // go next disk , we won't use this stream
121
122
  continue
122
123
  }
123
124
 
124
- let disposableDescendants
125
-
126
- const disposableSynthetic = await VhdSynthetic.fromVhdChain(this._adapter._handler, vhdPath)
125
+ const parent = await openDiskChain({
126
+ handler: this._adapter._handler,
127
+ path: vhdPath,
128
+ })
127
129
 
128
130
  // this will also clean if another disk of this VM backup fails
129
131
  // if user really only need to restore non failing disks he can retry with ignoredVdis
@@ -132,8 +134,7 @@ export class ImportVmBackup {
132
134
  if (!disposed) {
133
135
  disposed = true
134
136
  try {
135
- await disposableDescendants?.dispose()
136
- await disposableSynthetic?.dispose()
137
+ await parent?.close()
137
138
  } catch (error) {
138
139
  warn('openVhd: failed to dispose VHDs', { error })
139
140
  }
@@ -141,11 +142,10 @@ export class ImportVmBackup {
141
142
  }
142
143
  $defer.onFailure(() => disposeOnce())
143
144
 
144
- const parentVhd = disposableSynthetic.value
145
- await parentVhd.readBlockAllocationTable()
146
- debug('got vhd synthetic of parents', parentVhd.length)
145
+ debug('got vhd synthetic of parents', parent)
147
146
 
148
147
  if (snapshotCandidate !== undefined) {
148
+ let descendant, negativeDisk
149
149
  try {
150
150
  debug('will try to use differential restore', {
151
151
  backupWithSnapshotPath,
@@ -153,39 +153,37 @@ export class ImportVmBackup {
153
153
  vdiRef,
154
154
  })
155
155
 
156
- disposableDescendants = await VhdSynthetic.fromVhdChain(this._adapter._handler, backupWithSnapshotPath, {
156
+ descendant = await openDiskChain({
157
+ handler: this._adapter._handler,
158
+ path: backupWithSnapshotPath,
157
159
  until: vhdPath,
158
160
  })
159
- const descendantsVhd = disposableDescendants.value
160
- await descendantsVhd.readBlockAllocationTable()
161
+
161
162
  debug('got vhd synthetic of descendants')
162
- const negativeVhd = new VhdNegative(parentVhd, descendantsVhd)
163
+ negativeDisk = new NegativeDisk(parent, descendant)
163
164
  debug('got vhd negative')
164
165
 
165
166
  // update the stream with the negative vhd stream
166
- stream = await negativeVhd.stream()
167
+ disk = negativeDisk
167
168
  vdis[vdiRef].baseVdi = snapshotCandidate
168
169
  } catch (error) {
169
170
  // can be a broken VHD chain, a vhd chain with a key backup, ....
170
171
  // not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
171
172
  warn(`can't use differential restore`, { error })
172
- disposableDescendants?.dispose()
173
+ descendant?.close()
174
+ negativeDisk?.close()
173
175
  }
174
176
  }
175
177
  // didn't make a negative stream : fallback to classic stream
176
- if (stream === undefined) {
178
+ if (disk === undefined) {
177
179
  debug('use legacy restore')
178
- stream = await parentVhd.stream()
180
+ disk = parent
179
181
  }
180
-
181
- stream.on('end', disposeOnce)
182
- stream.on('close', disposeOnce)
183
- stream.on('error', disposeOnce)
184
- info('everything is ready, will transfer', stream.length)
185
- streams[`${vdiRef}.vhd`] = stream
182
+ info('everything is ready, will transfer', disk)
183
+ disks[vdiRef] = disk
186
184
  }
187
185
  return {
188
- streams,
186
+ disks,
189
187
  vbds,
190
188
  vdis,
191
189
  version: '1.0.0',
@@ -241,7 +239,6 @@ export class ImportVmBackup {
241
239
  assert.strictEqual(metadata.mode, 'delta')
242
240
 
243
241
  backup = await this.#decorateIncrementalVmMetadata()
244
- Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
245
242
  }
246
243
 
247
244
  return Task.run(
package/RemoteAdapter.mjs CHANGED
@@ -2,7 +2,7 @@ import { asyncEach } from '@vates/async-each'
2
2
  import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
3
3
  import { compose } from '@vates/compose'
4
4
  import { createLogger } from '@xen-orchestra/log'
5
- import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } from 'vhd-lib'
5
+ import { VhdDirectory, VhdSynthetic } from 'vhd-lib'
6
6
  import { decorateMethodsWith } from '@vates/decorate-with'
7
7
  import { deduped } from '@vates/disposable/deduped.js'
8
8
  import { dirname, join, resolve } from 'node:path'
@@ -10,7 +10,6 @@ import { execFile } from 'child_process'
10
10
  import { mount } from '@vates/fuse-vhd'
11
11
  import { readdir, lstat } from 'node:fs/promises'
12
12
  import { synchronized } from 'decorator-synchronized'
13
- import { v4 as uuidv4 } from 'uuid'
14
13
  import { ZipFile } from 'yazl'
15
14
  import Disposable from 'promise-toolbox/Disposable'
16
15
  import fromCallback from 'promise-toolbox/fromCallback'
@@ -32,6 +31,10 @@ import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
32
31
  import { lvs, pvs } from './_lvm.mjs'
33
32
  import { watchStreamSize } from './_watchStreamSize.mjs'
34
33
 
34
+ import { RemoteVhd } from './disks/RemoteVhd.mjs'
35
+ import { openDiskChain } from './disks/openDiskChain.mjs'
36
+ import { toVhdStream, writeToVhdDirectory } from 'vhd-lib/disk-consumer/index.mjs'
37
+
35
38
  export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
36
39
 
37
40
  export const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
@@ -689,22 +692,24 @@ export class RemoteAdapter {
689
692
  return path
690
693
  }
691
694
 
692
- async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency } = {}) {
695
+ async writeVhd(path, disk, { validator = noop, writeBlockConcurrency } = {}) {
693
696
  const handler = this._handler
697
+
694
698
  if (this.useVhdDirectory()) {
695
- const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
696
- const size = await createVhdDirectoryFromStream(handler, dataPath, input, {
697
- concurrency: writeBlockConcurrency,
698
- compression: this.#getCompressionType(),
699
- async validator() {
700
- await input.task
701
- return validator.apply(this, arguments)
699
+ await writeToVhdDirectory({
700
+ disk,
701
+ target: {
702
+ handler,
703
+ path,
704
+ concurrency: writeBlockConcurrency,
705
+ validator,
706
+ compression: 'brotli',
702
707
  },
703
708
  })
704
- await VhdAbstract.createAlias(handler, path, dataPath)
705
- return size
706
709
  } else {
707
- return this.outputStream(path, input, { checksum, validator })
710
+ const stream = await toVhdStream({ disk })
711
+ await this.outputStream(path, stream, { validator })
712
+ await validator(path)
708
713
  }
709
714
  }
710
715
 
@@ -728,30 +733,15 @@ export class RemoteAdapter {
728
733
  }
729
734
 
730
735
  // open the hierarchy of ancestors until we find a full one
731
- async _createVhdStream(handler, path, { useChain }) {
732
- const disposableSynthetic = useChain ? await VhdSynthetic.fromVhdChain(handler, path) : await openVhd(handler, path)
733
- // I don't want the vhds to be disposed on return
734
- // but only when the stream is done ( or failed )
735
-
736
- let disposed = false
737
- const disposeOnce = async () => {
738
- if (!disposed) {
739
- disposed = true
740
- try {
741
- await disposableSynthetic.dispose()
742
- } catch (error) {
743
- warn('openVhd: failed to dispose VHDs', { error })
744
- }
745
- }
736
+ async _createVhdDisk(handler, path, { useChain }) {
737
+ let disk
738
+ if (useChain) {
739
+ disk = await openDiskChain({ handler, path })
740
+ } else {
741
+ disk = new RemoteVhd({ handler, path })
742
+ await disk.init()
746
743
  }
747
- const synthetic = disposableSynthetic.value
748
- await synthetic.readBlockAllocationTable()
749
- const stream = await synthetic.stream()
750
-
751
- stream.on('end', disposeOnce)
752
- stream.on('close', disposeOnce)
753
- stream.on('error', disposeOnce)
754
- return stream
744
+ return disk
755
745
  }
756
746
 
757
747
  async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
@@ -759,14 +749,14 @@ export class RemoteAdapter {
759
749
  const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
760
750
  const dir = dirname(metadata._filename)
761
751
  const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
762
-
763
- const streams = {}
752
+ const disks = {}
764
753
  await asyncMapSettled(Object.keys(vdis), async ref => {
765
- streams[`${ref}.vhd`] = await this._createVhdStream(handler, join(dir, vhds[ref]), { useChain })
754
+ delete vdis[ref].baseVdi
755
+ disks[ref] = await this._createVhdDisk(handler, join(dir, vhds[ref]), { useChain })
766
756
  })
767
757
 
768
758
  return {
769
- streams,
759
+ disks,
770
760
  vbds,
771
761
  vdis,
772
762
  version: '1.0.0',
package/_cleanVm.mjs CHANGED
@@ -595,7 +595,7 @@ export async function cleanVm(
595
595
  // all disks are now key disk
596
596
  metadata.isVhdDifferencing = {}
597
597
  for (const id of Object.keys(metadata.vdis ?? {})) {
598
- metadata.isVhdDifferencing[`${id}.vhd`] = false
598
+ metadata.isVhdDifferencing[id] = false
599
599
  }
600
600
  }
601
601
  mustRegenerateCache = true
@@ -10,6 +10,9 @@ import { Task } from './Task.mjs'
10
10
  import pick from 'lodash/pick.js'
11
11
  import { BASE_DELTA_VDI, COPY_OF, VM_UUID } from './_otherConfig.mjs'
12
12
 
13
+ import { XapiDiskSource } from '@xen-orchestra/xapi'
14
+ import { toVhdStream } from 'vhd-lib/disk-consumer/index.mjs'
15
+
13
16
  const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
14
17
 
15
18
  export async function exportIncrementalVm(
@@ -19,7 +22,7 @@ export async function exportIncrementalVm(
19
22
  ) {
20
23
  // refs of VM's VDIs → base's VDIs.
21
24
 
22
- const streams = {}
25
+ const disks = {}
23
26
  const vdis = {}
24
27
  const vbds = {}
25
28
  await cancelableMap(cancelToken, vm.$VBDs, async (cancelToken, vbd) => {
@@ -52,32 +55,14 @@ export async function exportIncrementalVm(
52
55
  $snapshot_of$uuid: vdi.$snapshot_of?.uuid,
53
56
  $SR$uuid: vdi.$SR.uuid,
54
57
  }
55
- try {
56
- streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
57
- baseRef: baseVdi?.$ref,
58
- cancelToken,
59
- format: 'vhd',
60
- nbdConcurrency,
61
- preferNbd,
62
- })
63
- } catch (err) {
64
- if (err.code === 'VDI_CANT_DO_DELTA') {
65
- // fall back to a base
66
- Task.info(`Can't do delta, will try to get a full stream`, { vdi })
67
- streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
68
- cancelToken,
69
- format: 'vhd',
70
- nbdConcurrency,
71
- preferNbd,
72
- })
73
- // only warn if the fall back succeed
74
- Task.warning(`Can't do delta with this vdi, transfer will be a full`, {
75
- vdi,
76
- })
77
- } else {
78
- throw err
79
- }
80
- }
58
+ disks[vdiRef] = new XapiDiskSource({
59
+ vdiRef,
60
+ xapi: vm.$xapi,
61
+ baseRef: baseVdi?.$ref,
62
+ nbdConcurrency,
63
+ preferNbd,
64
+ })
65
+ await disks[vdiRef].init()
81
66
  })
82
67
 
83
68
  const suspendVdi = vm.$suspend_VDI
@@ -87,10 +72,13 @@ export async function exportIncrementalVm(
87
72
  ...suspendVdi,
88
73
  $SR$uuid: suspendVdi.$SR.uuid,
89
74
  }
90
- streams[`${vdiRef}.vhd`] = await suspendVdi.$exportContent({
91
- cancelToken,
92
- format: 'vhd',
75
+ disks[vdiRef] = new XapiDiskSource({
76
+ vdiRef: suspendVdi.$ref,
77
+ xapi: vm.$xapi,
78
+ nbdConcurrency,
79
+ preferNbd,
93
80
  })
81
+ await disks[vdiRef].init()
94
82
  }
95
83
 
96
84
  const vifs = {}
@@ -116,24 +104,17 @@ export async function exportIncrementalVm(
116
104
  })
117
105
  )
118
106
 
119
- return Object.defineProperty(
120
- {
121
- version: '1.1.0',
122
- vbds,
123
- vdis,
124
- vifs,
125
- vm: {
126
- ...vm,
127
- },
128
- vtpms,
107
+ return {
108
+ version: '1.1.0',
109
+ vbds,
110
+ vdis,
111
+ vifs,
112
+ vm: {
113
+ ...vm,
129
114
  },
130
- 'streams',
131
- {
132
- configurable: true,
133
- value: streams,
134
- writable: true,
135
- }
136
- )
115
+ vtpms,
116
+ disks,
117
+ }
137
118
  }
138
119
 
139
120
  export const importIncrementalVm = defer(async function importIncrementalVm(
@@ -199,8 +180,8 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
199
180
  const vdi = vdiRecords[vdiRef]
200
181
  let newVdi
201
182
 
202
- if (vdi.baseVdi !== undefined) {
203
- newVdi = await xapi.getRecord('VDI', await vdi.baseVdi.$clone())
183
+ if (vdi.baseVdi?.$ref !== undefined) {
184
+ newVdi = await xapi.getRecord('VDI', await xapi.VDI_clone(vdi.baseVdi.$ref))
204
185
  $defer.onFailure(() => newVdi.$destroy())
205
186
 
206
187
  await newVdi.update_other_config(COPY_OF, vdi.uuid)
@@ -244,20 +225,17 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
244
225
  }
245
226
  })
246
227
 
247
- const { streams } = incrementalVm
248
-
228
+ const { disks } = incrementalVm
249
229
  await Promise.all([
250
230
  // Import VDI contents.
251
231
  cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
252
- for (let stream of ensureArray(streams[`${id}.vhd`])) {
253
- if (stream === null) {
232
+ for (const disk of ensureArray(disks[id])) {
233
+ if (disk === null) {
254
234
  // we restore a backup and reuse completly a local snapshot
255
235
  continue
256
236
  }
257
- if (typeof stream === 'function') {
258
- stream = await stream()
259
- }
260
237
  await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
238
+ const stream = await toVhdStream({ disk })
261
239
  await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
262
240
  await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label)
263
241
  }
@@ -4,11 +4,11 @@ import { limitConcurrency } from 'limit-concurrency-decorator'
4
4
 
5
5
  import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
6
6
  import { Task } from '../Task.mjs'
7
- import createStreamThrottle from './_createStreamThrottle.mjs'
8
7
  import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
9
8
  import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
10
9
  import { FullRemote } from './_vmRunners/FullRemote.mjs'
11
10
  import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
11
+ import { Throttle } from '@vates/generator-toolbox'
12
12
 
13
13
  const noop = Function.prototype
14
14
 
@@ -41,7 +41,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
41
41
  const schedule = this._schedule
42
42
  const settings = this._settings
43
43
 
44
- const throttleStream = createStreamThrottle(settings.maxExportRate)
44
+ const throttleGenerator = new Throttle()
45
45
 
46
46
  const config = this._config
47
47
 
@@ -89,7 +89,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
89
89
  schedule,
90
90
  settings: vmSettings,
91
91
  sourceRemoteAdapter,
92
- throttleStream,
92
+ throttleGenerator,
93
93
  vmUuid,
94
94
  }
95
95
  let vmBackup
@@ -4,12 +4,12 @@ import { limitConcurrency } from 'limit-concurrency-decorator'
4
4
 
5
5
  import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
6
6
  import { Task } from '../Task.mjs'
7
- import createStreamThrottle from './_createStreamThrottle.mjs'
8
7
  import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
9
8
  import { runTask } from './_runTask.mjs'
10
9
  import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
11
10
  import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
12
11
  import { FullXapi } from './_vmRunners/FullXapi.mjs'
12
+ import { Throttle } from '@vates/generator-toolbox'
13
13
 
14
14
  const noop = Function.prototype
15
15
 
@@ -55,7 +55,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
55
55
  const schedule = this._schedule
56
56
  const settings = this._settings
57
57
 
58
- const throttleStream = createStreamThrottle(settings.maxExportRate)
58
+ const throttleGenerator = new Throttle()
59
59
 
60
60
  const config = this._config
61
61
 
@@ -147,7 +147,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
147
147
  schedule,
148
148
  settings: vmSettings,
149
149
  srs,
150
- throttleStream,
150
+ throttleGenerator,
151
151
  vm,
152
152
  }
153
153
 
@@ -26,6 +26,7 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
26
26
  stream: forkStreamUnpipe(stream),
27
27
  // stream will be forked and transformed, it's not safe to attach additionnal properties to it
28
28
  streamLength: stream.length,
29
+ maxStreamLength: stream.maxStreamLength, // on encrypted source
29
30
  timestamp: metadata.timestamp,
30
31
  vm: metadata.vm,
31
32
  vmSnapshot: metadata.vmSnapshot,
@@ -29,14 +29,16 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
29
29
  const { compression } = this.job
30
30
  const vm = this._vm
31
31
  const exportedVm = this._exportedVm
32
- const stream = this._throttleStream(
32
+ // @todo put back throttle for full backup/Replication
33
+ const stream =
34
+ /* this._throttleStream( */
33
35
  (
34
36
  await this._xapi.VM_export(exportedVm.$ref, {
35
37
  compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
36
38
  useSnapshot: false,
37
39
  })
38
40
  ).body
39
- )
41
+ /* ) */
40
42
 
41
43
  const vdis = await exportedVm.$getDisks()
42
44
  let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
@@ -3,11 +3,8 @@ import { createLogger } from '@xen-orchestra/log'
3
3
  import { asyncEach } from '@vates/async-each'
4
4
  import assert from 'node:assert'
5
5
  import * as UUID from 'uuid'
6
- import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
7
- import mapValues from 'lodash/mapValues.js'
8
6
 
9
7
  import { AbstractRemote } from './_AbstractRemote.mjs'
10
- import { forkDeltaExport } from './_forkDeltaExport.mjs'
11
8
  import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
12
9
  import { Disposable } from 'promise-toolbox'
13
10
  import { openVhd } from 'vhd-lib'
@@ -37,7 +34,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
37
34
  return
38
35
  }
39
36
  await asyncEach(Object.entries(metadata.vdis), async ([id, vdi]) => {
40
- const isDifferencing = metadata.isVhdDifferencing[`${id}.vhd`]
37
+ const isDifferencing = metadata.isVhdDifferencing[id]
41
38
  if (isDifferencing) {
42
39
  const vmDir = getVmBackupDir(metadata.vm.uuid)
43
40
  const path = `${vmDir}/${metadata.vhds[id]}`
@@ -71,8 +68,8 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
71
68
  // recompute if disks are differencing or not
72
69
  const isVhdDifferencing = {}
73
70
 
74
- await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
75
- isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
71
+ Object.entries(incrementalExport.disks).forEach(([key, disk]) => {
72
+ isVhdDifferencing[key] = disk.isDifferencing()
76
73
  })
77
74
  const hasDifferencingDisk = Object.values(isVhdDifferencing).includes(true)
78
75
  if (metadata.isBase === hasDifferencingDisk) {
@@ -87,11 +84,10 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
87
84
  await this._selectBaseVm(metadata)
88
85
  await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
89
86
 
90
- incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
91
87
  await this._callWriters(
92
88
  writer =>
93
89
  writer.transfer({
94
- deltaExport: forkDeltaExport(incrementalExport),
90
+ deltaExport: incrementalExport,
95
91
  isVhdDifferencing,
96
92
  timestamp: metadata.timestamp,
97
93
  vm: metadata.vm,
@@ -1,18 +1,11 @@
1
- import { asyncEach } from '@vates/async-each'
2
1
  import { createLogger } from '@xen-orchestra/log'
3
- import { pipeline } from 'node:stream'
4
- import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
5
2
  import keyBy from 'lodash/keyBy.js'
6
- import mapValues from 'lodash/mapValues.js'
7
- import vhdStreamValidator from 'vhd-lib/vhdStreamValidator.js'
8
3
 
9
4
  import { AbstractXapi } from './_AbstractXapi.mjs'
10
5
  import { exportIncrementalVm } from '../../_incrementalVm.mjs'
11
- import { forkDeltaExport } from './_forkDeltaExport.mjs'
12
6
  import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
13
7
  import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
14
8
  import { Task } from '../../Task.mjs'
15
- import { watchStreamSize } from '../../_watchStreamSize.mjs'
16
9
  import {
17
10
  DATETIME,
18
11
  DELTA_CHAIN_LENGTH,
@@ -20,9 +13,9 @@ import {
20
13
  setVmDeltaChainLength,
21
14
  markExportSuccessfull,
22
15
  } from '../../_otherConfig.mjs'
16
+ import { SynchronizedDisk } from '@xen-orchestra/disk-transform'
23
17
 
24
18
  const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
25
- const noop = Function.prototype
26
19
 
27
20
  export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
28
21
  _getWriters() {
@@ -45,42 +38,40 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
45
38
  nbdConcurrency: this._settings.nbdConcurrency,
46
39
  preferNbd: this._settings.preferNbd,
47
40
  })
48
- // since NBD is network based, if one disk use nbd , all the disk use them
49
- // except the suspended VDI
50
- if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
51
- Task.info('Transfer data using NBD')
52
- }
53
41
 
54
42
  const isVhdDifferencing = {}
55
- // since isVhdDifferencingDisk is reading and unshifting data in stream
56
- // it should be done BEFORE any other stream transform
57
- await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
58
- isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
59
- })
60
- const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
61
-
62
- if (this._settings.validateVhdStreams) {
63
- deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
43
+ let useNbd = false
44
+ for (const key in deltaExport.disks) {
45
+ const disk = deltaExport.disks[key]
46
+ isVhdDifferencing[key] = disk.isDifferencing()
47
+ deltaExport.disks[key] = new SynchronizedDisk(disk)
48
+ useNbd = useNbd || disk.useNbd()
49
+ }
50
+ if (useNbd) {
51
+ Task.info('Transfer data using NBD')
52
+ }
53
+ function fork(deltaExport, label) {
54
+ const { disks, ...forked } = deltaExport
55
+ forked.disks = {}
56
+ for (const key in disks) {
57
+ forked.disks[key] = disks[key].fork(label)
58
+ }
59
+ return forked
64
60
  }
65
- deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
66
61
 
62
+ // @todo : reimplement throttle,nbsource: d use
67
63
  const timestamp = Date.now()
68
-
69
64
  await this._callWriters(
70
65
  writer =>
71
66
  writer.transfer({
72
- deltaExport: forkDeltaExport(deltaExport),
67
+ deltaExport: fork(deltaExport, writer.constructor.name + ' ' + Math.random()),
73
68
  isVhdDifferencing,
74
- sizeContainers,
75
69
  timestamp,
76
70
  vm,
77
71
  vmSnapshot: exportedVm,
78
72
  }),
79
73
  'writer.transfer()'
80
74
  )
81
-
82
- // we want to control the uuid of the vhd in the chain
83
- // and ensure they are correctly chained
84
75
  await this._callWriters(
85
76
  writer =>
86
77
  writer.updateUuidAndChain({
@@ -102,15 +93,6 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
102
93
  await markExportSuccessfull(this._xapi, exportedVm.$ref)
103
94
  }
104
95
 
105
- const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
106
- const end = Date.now()
107
- const duration = end - timestamp
108
- debug('transfer complete', {
109
- duration,
110
- speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
111
- size,
112
- })
113
-
114
96
  await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
115
97
  }
116
98
 
@@ -21,7 +21,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
21
21
  schedule,
22
22
  settings,
23
23
  sourceRemoteAdapter,
24
- throttleStream,
24
+ throttleGenerator,
25
25
  vmUuid,
26
26
  }) {
27
27
  super()
@@ -34,7 +34,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
34
34
 
35
35
  this._healthCheckSr = healthCheckSr
36
36
  this._sourceRemoteAdapter = sourceRemoteAdapter
37
- this._throttleStream = throttleStream
37
+ this._throttleGenerator = throttleGenerator
38
38
  this._vmUuid = vmUuid
39
39
 
40
40
  const allSettings = job.settings
@@ -28,7 +28,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
28
28
  schedule,
29
29
  settings,
30
30
  srs,
31
- throttleStream,
31
+ throttleGenerator,
32
32
  vm,
33
33
  }) {
34
34
  super()
@@ -62,7 +62,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
62
62
  this._healthCheckSr = healthCheckSr
63
63
  this._jobId = job.id
64
64
  this._jobSnapshotVdis = undefined
65
- this._throttleStream = throttleStream
65
+ this._throttleGenerator = throttleGenerator
66
66
  this._xapi = vm.$xapi
67
67
 
68
68
  // Base VM for the export
@@ -136,7 +136,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
136
136
  const { handler } = this._adapter
137
137
  const vhds = this.#vhds
138
138
  await asyncEach(Object.entries(vdis), async ([id, vdi]) => {
139
- const isDifferencing = isVhdDifferencing[`${id}.vhd`]
139
+ const isDifferencing = isVhdDifferencing[id]
140
140
  const path = `${this._vmBackupDir}/${vhds[id]}`
141
141
  if (isDifferencing) {
142
142
  assert.notStrictEqual(
@@ -203,8 +203,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
203
203
  let metadataContent = await this._isAlreadyTransferred(timestamp)
204
204
  if (metadataContent !== undefined) {
205
205
  // skip backup while being vigilant to not stuck the forked stream
206
- Task.info('This backup has already been transfered')
207
- Object.values(deltaExport.streams).forEach(stream => stream.destroy())
206
+ /** @todo destroy fork */
208
207
  return { size: 0 }
209
208
  }
210
209
 
@@ -223,36 +222,33 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
223
222
  vmSnapshot,
224
223
  vtpms: deltaExport.vtpms,
225
224
  }
226
-
227
- const { size } = await Task.run({ name: 'transfer' }, async () => {
228
- let transferSize = 0
225
+ let size = 0
226
+ await Task.run({ name: 'transfer' }, async () => {
229
227
  await asyncEach(
230
- Object.keys(deltaExport.vdis),
231
- async id => {
232
- const path = `${this._vmBackupDir}/${vhds[id]}`
233
- // don't write it as transferSize += await async function
234
- // since i += await asyncFun lead to race condition
235
- // as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
236
- const transferSizeOneDisk = await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
228
+ Object.entries(deltaExport.disks),
229
+ async ([diskRef, disk]) => {
230
+ const path = `${this._vmBackupDir}/${vhds[diskRef]}`
231
+ await adapter.writeVhd(path, disk, {
237
232
  // no checksum for VHDs, because they will be invalidated by
238
233
  // merges and chainings
239
234
  checksum: false,
240
235
  validator: tmpPath => checkVhd(handler, tmpPath),
241
236
  writeBlockConcurrency: this._config.writeBlockConcurrency,
242
237
  })
243
- transferSize += transferSizeOneDisk
238
+ size = size + disk.getNbGeneratedBlock() * disk.getBlockSize()
244
239
  },
245
240
  {
246
241
  concurrency: settings.diskPerVmConcurrency,
247
242
  }
248
243
  )
249
244
 
250
- return { size: transferSize }
245
+ return { size }
251
246
  })
252
- metadataContent.size = size
247
+ metadataContent.size = size // @todo return exactly the size written by this writer
253
248
  this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadataContent)
254
249
 
255
250
  // TODO: run cleanup?
251
+ return { size }
256
252
  }
257
253
  }
258
254
  decorateClass(IncrementalRemoteWriter, {
@@ -130,7 +130,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
130
130
  return backup
131
131
  }
132
132
 
133
- async _transfer({ timestamp, deltaExport, sizeContainers, vm }) {
133
+ async _transfer({ timestamp, deltaExport, vm }) {
134
134
  const { _warmMigration } = this._settings
135
135
  const sr = this._sr
136
136
  const job = this._job
@@ -141,8 +141,12 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
141
141
  let targetVmRef
142
142
  await Task.run({ name: 'transfer' }, async () => {
143
143
  targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport), sr)
144
+ // size is mandatory to ensure the task have the right data
144
145
  return {
145
- size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
146
+ size: Object.values(deltaExport.disks).reduce(
147
+ (sum, disk) => sum + disk.getNbGeneratedBlock() * disk.getBlockSize(),
148
+ 0
149
+ ),
146
150
  }
147
151
  })
148
152
  this._targetVmRef = targetVmRef
@@ -20,10 +20,14 @@ export class AbstractIncrementalWriter extends AbstractWriter {
20
20
  async transfer({ deltaExport, ...other }) {
21
21
  try {
22
22
  return await this._transfer({ deltaExport, ...other })
23
+ } catch (err) {
24
+ console.error({ err })
23
25
  } finally {
24
- // ensure all streams are properly closed
25
- for (const stream of Object.values(deltaExport.streams)) {
26
- stream.destroy()
26
+ // ensure all sources are properly closed
27
+ for (const disk of Object.values(deltaExport.disks)) {
28
+ try {
29
+ await disk.close()
30
+ } catch (err) {}
27
31
  }
28
32
  }
29
33
  }
@@ -0,0 +1,177 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import('@xen-orchestra/disk-transform').FileAccessor} FileAccessor
5
+ * @typedef {import('@xen-orchestra/disk-transform').DiskBlock} DiskBlock
6
+ * @typedef {import('@xen-orchestra/disk-transform').Disk} Disk
7
+ * @typedef {import('vhd-lib/Vhd/VhdDirectory.js').VhdDirectory} VhdDirectory
8
+ * @typedef {import('vhd-lib/Vhd/VhdFile.js').VhdFile} VhdFile
9
+ */
10
+
11
+ import { openVhd } from 'vhd-lib'
12
+ import { DISK_TYPES } from 'vhd-lib/_constants.js'
13
+ import { dirname, join } from 'node:path'
14
+ import { RandomAccessDisk } from '@xen-orchestra/disk-transform'
15
+ /**
16
+ * Represents a remote VHD (Virtual Hard Disk) that extends RandomAccessDisk.
17
+ */
18
+ export class RemoteVhd extends RandomAccessDisk {
19
+ /**
20
+ * @type {string}
21
+ */
22
+ #path
23
+
24
+ /**
25
+ * @type {FileAccessor}
26
+ */
27
+ #handler
28
+
29
+ /**
30
+ * @type {VhdFile | VhdDirectory | undefined}
31
+ */
32
+ #vhd
33
+
34
+ /**
35
+ * @type {boolean | undefined}
36
+ */
37
+ #isDifferencing
38
+
39
+ /**
40
+ * @type {() => any}
41
+ */
42
+ #dispose = () => {}
43
+
44
+ /**
45
+ * @returns {string}
46
+ */
47
+ get path() {
48
+ return this.#path
49
+ }
50
+
51
+ /**
52
+ * @param {Object} params
53
+ * @param {FileAccessor} params.handler
54
+ * @param {string} params.path
55
+ */
56
+ constructor({ handler, path }) {
57
+ super()
58
+ // @todo : ensure this is the full path from the root of the remote
59
+ this.#path = path
60
+ this.#handler = handler
61
+ }
62
+
63
+ /**
64
+ * @returns {number}
65
+ */
66
+ getVirtualSize() {
67
+ if (this.#vhd === undefined) {
68
+ throw new Error(`can't call getvirtualsize of a RemoteVhd before init`)
69
+ }
70
+ return this.#vhd.footer.currentSize
71
+ }
72
+
73
+ /**
74
+ * @returns {number}
75
+ */
76
+ getBlockSize() {
77
+ return 2 * 1024 * 1024
78
+ }
79
+
80
+ /**
81
+ * Initializes the VHD.
82
+ * @returns {Promise<void>}
83
+ */
84
+ async init() {
85
+ const { value, dispose } = await openVhd(this.#handler, this.#path)
86
+ this.#vhd = value
87
+ this.#dispose = dispose
88
+ await this.#vhd.readBlockAllocationTable()
89
+ this.#isDifferencing = value.footer.diskType === DISK_TYPES.DIFFERENCING
90
+ }
91
+
92
+ /**
93
+ * Closes the VHD.
94
+ * @returns {Promise<void>}
95
+ */
96
+ async close() {
97
+ try {
98
+ await this.#dispose()
99
+ } catch (err) {
100
+ if (err.code !== 'EBADF') {
101
+ throw err // handle double dispose
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Checks if the VHD contains a specific block.
108
+ * @param {number} index
109
+ * @returns {boolean}
110
+ */
111
+ hasBlock(index) {
112
+ if (this.#vhd === undefined) {
113
+ throw new Error(`can't call hasblock of a RemoteVhd before init`)
114
+ }
115
+ return this.#vhd.containsBlock(index)
116
+ }
117
+
118
+ /**
119
+ * Gets the indexes of all blocks in the VHD.
120
+ * @returns {Array<number>}
121
+ */
122
+ getBlockIndexes() {
123
+ if (this.#vhd === undefined) {
124
+ throw new Error(`can't call getBlockIndexes of a RemoteVhd before init`)
125
+ }
126
+ const index = []
127
+ for (let blockIndex = 0; blockIndex < this.#vhd.header.maxTableEntries; blockIndex++) {
128
+ if (this.hasBlock(blockIndex)) {
129
+ index.push(blockIndex)
130
+ }
131
+ }
132
+ return index
133
+ }
134
+
135
+ /**
136
+ * Reads a specific block from the VHD.
137
+ * @param {number} index
138
+ * @returns {Promise<DiskBlock>}
139
+ */
140
+ async readBlock(index) {
141
+ if (this.#vhd === undefined) {
142
+ throw new Error(`can't call readBlock of a RemoteVhd before init`)
143
+ }
144
+ const { data } = await this.#vhd.readBlock(index)
145
+ return {
146
+ index,
147
+ data,
148
+ }
149
+ }
150
+ /**
151
+ *
152
+ * @returns {RandomAccessDisk}
153
+ */
154
+ instantiateParent() {
155
+ if (this.#vhd === undefined) {
156
+ throw new Error(`can't call openParent of a RemoteVhd before init`)
157
+ }
158
+ const parentPath = this.#vhd.header.parentUnicodeName
159
+ const fullParentPath = join(dirname(this.#path), parentPath)
160
+ if (!parentPath) {
161
+ throw new Error(`Disk ${this.#path} doesn't have parents`)
162
+ }
163
+ const parent = new RemoteVhd({ handler: this.#handler, path: fullParentPath })
164
+ return parent
165
+ }
166
+
167
+ /**
168
+ * Checks if the VHD is a differencing disk.
169
+ * @returns {boolean}
170
+ */
171
+ isDifferencing() {
172
+ if (this.#isDifferencing === undefined) {
173
+ throw new Error(`can't call isDifferencing of a RemoteVhd before init`)
174
+ }
175
+ return this.#isDifferencing
176
+ }
177
+ }
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ /**
3
+ *
4
+ * @typedef {import('../../disk-transform/src/FileAccessor.mjs').FileAccessor} FileAccessor
5
+ */
6
+ import { DiskChain } from '@xen-orchestra/disk-transform'
7
+ import { RemoteVhd } from './RemoteVhd.mjs'
8
+
9
+ import { defer } from 'golike-defer'
10
+ /**
11
+ * @param {Object} params
12
+ * @param {FileAccessor} params.handler
13
+ * @param {string} params.path
14
+ * @param {string | undefined} params.until
15
+ */
16
+ async function _openDiskChain($defer, { handler, path, until }) {
17
+ let disk
18
+ const disks = []
19
+ $defer.onFailure(() => Promise.all(disks.map(disk => disk.close())))
20
+ disk = new RemoteVhd({ handler, path })
21
+
22
+ await disk.init()
23
+ disks.push(disk)
24
+ while (disk.isDifferencing()) {
25
+ disk = await disk.openParent()
26
+ if (disk.path === until) {
27
+ break
28
+ }
29
+ disks.unshift(disk)
30
+ }
31
+ // the root disk
32
+ return new DiskChain({ disks })
33
+ }
34
+
35
+ export const openDiskChain = defer(_openDiskChain)
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.59.0",
11
+ "version": "0.60.0",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -25,10 +25,12 @@
25
25
  "@vates/decorate-with": "^2.1.0",
26
26
  "@vates/disposable": "^0.1.6",
27
27
  "@vates/fuse-vhd": "^2.1.2",
28
- "@vates/nbd-client": "^3.1.2",
28
+ "@vates/generator-toolbox": "^1.0.2",
29
+ "@vates/nbd-client": "^3.1.3",
29
30
  "@vates/parse-duration": "^0.1.1",
30
31
  "@xen-orchestra/async-map": "^0.1.2",
31
- "@xen-orchestra/fs": "^4.5.0",
32
+ "@xen-orchestra/disk-transform": "^1.0.0",
33
+ "@xen-orchestra/fs": "^4.5.1",
32
34
  "@xen-orchestra/log": "^0.7.1",
33
35
  "@xen-orchestra/template": "^0.1.0",
34
36
  "app-conf": "^3.0.0",
@@ -59,7 +61,7 @@
59
61
  "tmp": "^0.2.1"
60
62
  },
61
63
  "peerDependencies": {
62
- "@xen-orchestra/xapi": "^8.1.1"
64
+ "@xen-orchestra/xapi": "^8.2.0"
63
65
  },
64
66
  "license": "AGPL-3.0-or-later",
65
67
  "author": {
@@ -1,11 +0,0 @@
1
- import cloneDeep from 'lodash/cloneDeep.js'
2
- import mapValues from 'lodash/mapValues.js'
3
-
4
- import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
5
-
6
- export function forkDeltaExport(deltaExport) {
7
- const { streams, ...rest } = deltaExport
8
- const newMetadata = cloneDeep(rest)
9
- newMetadata.streams = mapValues(streams, forkStreamUnpipe)
10
- return newMetadata
11
- }