@xen-orchestra/backups 0.58.4 → 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.
- package/ImportVmBackup.mjs +25 -28
- package/RemoteAdapter.mjs +43 -45
- package/_cleanVm.mjs +1 -1
- package/_incrementalVm.mjs +34 -56
- package/_runners/VmsRemote.mjs +3 -3
- package/_runners/VmsXapi.mjs +3 -3
- package/_runners/_vmRunners/FullRemote.mjs +1 -0
- package/_runners/_vmRunners/FullXapi.mjs +4 -2
- package/_runners/_vmRunners/IncrementalRemote.mjs +4 -8
- package/_runners/_vmRunners/IncrementalXapi.mjs +20 -38
- package/_runners/_vmRunners/_AbstractRemote.mjs +2 -2
- package/_runners/_vmRunners/_AbstractXapi.mjs +2 -2
- package/_runners/_writers/IncrementalRemoteWriter.mjs +12 -16
- package/_runners/_writers/IncrementalXapiWriter.mjs +6 -2
- package/_runners/_writers/_AbstractIncrementalWriter.mjs +7 -3
- package/disks/RemoteVhd.mjs +177 -0
- package/disks/openDiskChain.mjs +35 -0
- package/package.json +6 -4
- package/_runners/_vmRunners/_forkDeltaExport.mjs +0 -11
package/ImportVmBackup.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
descendant = await openDiskChain({
|
|
157
|
+
handler: this._adapter._handler,
|
|
158
|
+
path: backupWithSnapshotPath,
|
|
157
159
|
until: vhdPath,
|
|
158
160
|
})
|
|
159
|
-
|
|
160
|
-
await descendantsVhd.readBlockAllocationTable()
|
|
161
|
+
|
|
161
162
|
debug('got vhd synthetic of descendants')
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
descendant?.close()
|
|
174
|
+
negativeDisk?.close()
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
// didn't make a negative stream : fallback to classic stream
|
|
176
|
-
if (
|
|
178
|
+
if (disk === undefined) {
|
|
177
179
|
debug('use legacy restore')
|
|
178
|
-
|
|
180
|
+
disk = parent
|
|
179
181
|
}
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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 {
|
|
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'
|
|
@@ -423,12 +426,20 @@ export class RemoteAdapter {
|
|
|
423
426
|
async listAllVms() {
|
|
424
427
|
const handler = this._handler
|
|
425
428
|
const vmsUuids = []
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
429
|
+
try {
|
|
430
|
+
await asyncEach(await handler.list(BACKUP_DIR), async entry => {
|
|
431
|
+
// ignore hidden and lock files
|
|
432
|
+
if (entry[0] !== '.' && !entry.endsWith('.lock')) {
|
|
433
|
+
vmsUuids.push(entry)
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
} catch (error) {
|
|
437
|
+
// remote without any VM backup are ok
|
|
438
|
+
if (error.code !== 'ENOENT') {
|
|
439
|
+
throw error
|
|
430
440
|
}
|
|
431
|
-
}
|
|
441
|
+
}
|
|
442
|
+
|
|
432
443
|
return vmsUuids
|
|
433
444
|
}
|
|
434
445
|
|
|
@@ -681,22 +692,24 @@ export class RemoteAdapter {
|
|
|
681
692
|
return path
|
|
682
693
|
}
|
|
683
694
|
|
|
684
|
-
async writeVhd(path,
|
|
695
|
+
async writeVhd(path, disk, { validator = noop, writeBlockConcurrency } = {}) {
|
|
685
696
|
const handler = this._handler
|
|
697
|
+
|
|
686
698
|
if (this.useVhdDirectory()) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
699
|
+
await writeToVhdDirectory({
|
|
700
|
+
disk,
|
|
701
|
+
target: {
|
|
702
|
+
handler,
|
|
703
|
+
path,
|
|
704
|
+
concurrency: writeBlockConcurrency,
|
|
705
|
+
validator,
|
|
706
|
+
compression: 'brotli',
|
|
694
707
|
},
|
|
695
708
|
})
|
|
696
|
-
await VhdAbstract.createAlias(handler, path, dataPath)
|
|
697
|
-
return size
|
|
698
709
|
} else {
|
|
699
|
-
|
|
710
|
+
const stream = await toVhdStream({ disk })
|
|
711
|
+
await this.outputStream(path, stream, { validator })
|
|
712
|
+
await validator(path)
|
|
700
713
|
}
|
|
701
714
|
}
|
|
702
715
|
|
|
@@ -720,30 +733,15 @@ export class RemoteAdapter {
|
|
|
720
733
|
}
|
|
721
734
|
|
|
722
735
|
// open the hierarchy of ancestors until we find a full one
|
|
723
|
-
async
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (!disposed) {
|
|
731
|
-
disposed = true
|
|
732
|
-
try {
|
|
733
|
-
await disposableSynthetic.dispose()
|
|
734
|
-
} catch (error) {
|
|
735
|
-
warn('openVhd: failed to dispose VHDs', { error })
|
|
736
|
-
}
|
|
737
|
-
}
|
|
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()
|
|
738
743
|
}
|
|
739
|
-
|
|
740
|
-
await synthetic.readBlockAllocationTable()
|
|
741
|
-
const stream = await synthetic.stream()
|
|
742
|
-
|
|
743
|
-
stream.on('end', disposeOnce)
|
|
744
|
-
stream.on('close', disposeOnce)
|
|
745
|
-
stream.on('error', disposeOnce)
|
|
746
|
-
return stream
|
|
744
|
+
return disk
|
|
747
745
|
}
|
|
748
746
|
|
|
749
747
|
async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
|
|
@@ -751,14 +749,14 @@ export class RemoteAdapter {
|
|
|
751
749
|
const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
|
|
752
750
|
const dir = dirname(metadata._filename)
|
|
753
751
|
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
|
|
754
|
-
|
|
755
|
-
const streams = {}
|
|
752
|
+
const disks = {}
|
|
756
753
|
await asyncMapSettled(Object.keys(vdis), async ref => {
|
|
757
|
-
|
|
754
|
+
delete vdis[ref].baseVdi
|
|
755
|
+
disks[ref] = await this._createVhdDisk(handler, join(dir, vhds[ref]), { useChain })
|
|
758
756
|
})
|
|
759
757
|
|
|
760
758
|
return {
|
|
761
|
-
|
|
759
|
+
disks,
|
|
762
760
|
vbds,
|
|
763
761
|
vdis,
|
|
764
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[
|
|
598
|
+
metadata.isVhdDifferencing[id] = false
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
mustRegenerateCache = true
|
package/_incrementalVm.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.$
|
|
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 {
|
|
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 (
|
|
253
|
-
if (
|
|
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
|
}
|
package/_runners/VmsRemote.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
92
|
+
throttleGenerator,
|
|
93
93
|
vmUuid,
|
|
94
94
|
}
|
|
95
95
|
let vmBackup
|
package/_runners/VmsXapi.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
75
|
-
isVhdDifferencing[key] =
|
|
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:
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isVhdDifferencing[key] =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
let transferSize = 0
|
|
225
|
+
let size = 0
|
|
226
|
+
await Task.run({ name: 'transfer' }, async () => {
|
|
229
227
|
await asyncEach(
|
|
230
|
-
Object.
|
|
231
|
-
async
|
|
232
|
-
const path = `${this._vmBackupDir}/${vhds[
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
25
|
-
for (const
|
|
26
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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
|
-
}
|