@xen-orchestra/backups 0.61.0 → 0.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ImportVmBackup.mjs +2 -2
- package/RemoteAdapter.mjs +3 -3
- package/RestoreMetadataBackup.mjs +1 -1
- package/_cleanVm.mjs +1 -1
- package/_getOldEntries.mjs +2 -2
- package/_incrementalVm.mjs +1 -1
- package/_isValidXva.mjs +1 -1
- package/_otherConfig.mjs +4 -4
- package/_runners/_vmRunners/FullRemote.mjs +1 -1
- package/_runners/_vmRunners/FullXapi.mjs +2 -2
- package/_runners/_vmRunners/IncrementalRemote.mjs +3 -10
- package/_runners/_vmRunners/IncrementalXapi.mjs +4 -10
- package/_runners/_vmRunners/_AbstractRemote.mjs +1 -1
- package/_runners/_vmRunners/_AbstractXapi.mjs +2 -2
- package/_runners/_vmRunners/_forkDeltaExport.mjs +16 -0
- package/_runners/_writers/IncrementalRemoteWriter.mjs +1 -1
- package/_runners/_writers/IncrementalXapiWriter.mjs +1 -1
- package/package.json +2 -2
package/ImportVmBackup.mjs
CHANGED
|
@@ -168,13 +168,13 @@ export class ImportVmBackup {
|
|
|
168
168
|
vdis[vdiRef].baseVdi = snapshotCandidate
|
|
169
169
|
} catch (error) {
|
|
170
170
|
// can be a broken VHD chain, a vhd chain with a key backup, ....
|
|
171
|
-
// not an irrecuperable error, don't dispose parentVhd, and
|
|
171
|
+
// not an irrecuperable error, don't dispose parentVhd, and fall back to full restore
|
|
172
172
|
warn(`can't use differential restore`, { error })
|
|
173
173
|
descendant?.close()
|
|
174
174
|
negativeDisk?.close()
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
// didn't make a negative stream :
|
|
177
|
+
// didn't make a negative stream : fall back to classic stream
|
|
178
178
|
if (disk === undefined) {
|
|
179
179
|
debug('use legacy restore')
|
|
180
180
|
disk = parent
|
package/RemoteAdapter.mjs
CHANGED
|
@@ -205,7 +205,7 @@ export class RemoteAdapter {
|
|
|
205
205
|
})
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// check if we will be allowed to merge a
|
|
208
|
+
// check if we will be allowed to merge a vhd created in this adapter
|
|
209
209
|
// with the vhd at path `path`
|
|
210
210
|
async isMergeableParent(packedParentUid, path) {
|
|
211
211
|
return await Disposable.use(VhdSynthetic.fromVhdChain(this.handler, path), vhd => {
|
|
@@ -776,7 +776,7 @@ export class RemoteAdapter {
|
|
|
776
776
|
let json
|
|
777
777
|
let isImmutable = false
|
|
778
778
|
let remoteIsImmutable = false
|
|
779
|
-
// if the remote is immutable, check if this
|
|
779
|
+
// if the remote is immutable, check if this metadata is also immutable
|
|
780
780
|
try {
|
|
781
781
|
// this file is not encrypted
|
|
782
782
|
await this._handler._readFile(IMMUTABILTY_METADATA_FILENAME)
|
|
@@ -792,7 +792,7 @@ export class RemoteAdapter {
|
|
|
792
792
|
json = await this.handler.readFile(path, { flag: 'r+' })
|
|
793
793
|
// s3 handler don't respect flags
|
|
794
794
|
} catch (err) {
|
|
795
|
-
// retry without
|
|
795
|
+
// retry without triggering immutability check ,only on immutable remote
|
|
796
796
|
if (err.code === 'EPERM' && remoteIsImmutable) {
|
|
797
797
|
isImmutable = true
|
|
798
798
|
json = await this._handler.readFile(path, { flag: 'r' })
|
|
@@ -24,7 +24,7 @@ export class RestoreMetadataBackup {
|
|
|
24
24
|
const dataFileName = resolve('/', backupId, metadata.data ?? 'data.json').slice(1)
|
|
25
25
|
const data = await handler.readFile(dataFileName)
|
|
26
26
|
|
|
27
|
-
// if data is JSON, sent it as a plain string
|
|
27
|
+
// if data is JSON, sent it as a plain string; otherwise, consider the data as binary and encode it
|
|
28
28
|
const isJson = dataFileName.endsWith('.json')
|
|
29
29
|
return isJson ? data.toString() : { encoding: 'base64', data: data.toString('base64') }
|
|
30
30
|
}
|
package/_cleanVm.mjs
CHANGED
|
@@ -512,7 +512,7 @@ export async function cleanVm(
|
|
|
512
512
|
remove,
|
|
513
513
|
mergeBlockConcurrency,
|
|
514
514
|
})
|
|
515
|
-
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same
|
|
515
|
+
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metadata file
|
|
516
516
|
metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalVhdSize
|
|
517
517
|
})
|
|
518
518
|
}
|
package/_getOldEntries.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const LTR_DEFINITIONS = {
|
|
|
26
26
|
|
|
27
27
|
copy.date(date.date() - firstDayOfWeek)
|
|
28
28
|
// warning, the year in term of week may different from YYYY
|
|
29
|
-
// since the computation of the first week of a year is timezone
|
|
29
|
+
// since the computation of the first week of a year is timezone dependent
|
|
30
30
|
return copy.format('gggg-ww')
|
|
31
31
|
}
|
|
32
32
|
},
|
|
@@ -59,7 +59,7 @@ const LTR_DEFINITIONS = {
|
|
|
59
59
|
* return the entries too old to be kept
|
|
60
60
|
* if multiple entries are i the same time bucket : keep only the most recent one
|
|
61
61
|
* if an entry is valid in any of the bucket OR the minRetentionCount : keep it
|
|
62
|
-
* if a bucket is
|
|
62
|
+
* if a bucket is completely empty : it does not count as one, thus it may extend the retention
|
|
63
63
|
* @returns Array<Backup>
|
|
64
64
|
*/
|
|
65
65
|
export function getOldEntries(minRetentionCount, entries, { longTermRetention = {}, timezone } = {}) {
|
package/_incrementalVm.mjs
CHANGED
|
@@ -231,7 +231,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
231
231
|
cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
|
|
232
232
|
for (const disk of ensureArray(disks[id])) {
|
|
233
233
|
if (disk === null) {
|
|
234
|
-
// we restore a backup and reuse
|
|
234
|
+
// we restore a backup and reuse completely a local snapshot
|
|
235
235
|
continue
|
|
236
236
|
}
|
|
237
237
|
await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
|
package/_isValidXva.mjs
CHANGED
|
@@ -44,7 +44,7 @@ const isValidTar = async (handler, size, fd) => {
|
|
|
44
44
|
return buf.every(_ => _ === 0)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// TODO: find
|
|
47
|
+
// TODO: find a heuristic for compressed files
|
|
48
48
|
export async function isValidXva(path) {
|
|
49
49
|
const handler = this._handler
|
|
50
50
|
|
package/_otherConfig.mjs
CHANGED
|
@@ -43,7 +43,7 @@ async function getDeltaChainLength(xapi, type, ref) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* set the delta chain
|
|
46
|
+
* set the delta chain length ( number of delta since last base backup) to a VM and its associated VDIs
|
|
47
47
|
*
|
|
48
48
|
* @param {Xapi} xapi
|
|
49
49
|
* @param {String} vmRef
|
|
@@ -58,7 +58,7 @@ export async function setVmDeltaChainLength(xapi, vmRef, length) {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Compute the delta chain length of a VM and its associated VDIs
|
|
61
|
-
* if there is a
|
|
61
|
+
* if there is a discrepancy, use, the highest value
|
|
62
62
|
* @param {Xapi} xapi
|
|
63
63
|
* @param {String} vmRef
|
|
64
64
|
* @returns {Promise}
|
|
@@ -92,7 +92,7 @@ export function resetVmOtherConfig(xapi, vmRef) {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
*
|
|
95
|
-
* used to ensure
|
|
95
|
+
* used to ensure compatibility with the previous snapshots that were having the config stored only into VM
|
|
96
96
|
*
|
|
97
97
|
* @param {Xapi} xapi
|
|
98
98
|
* @param {String} vmRef
|
|
@@ -152,7 +152,7 @@ export async function setVmOtherConfig(xapi, vmRef, { timestamp, jobId, schedule
|
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
154
|
*
|
|
155
|
-
* mark the export of he VM and its VDIs as
|
|
155
|
+
* mark the export of he VM and its VDIs as successful
|
|
156
156
|
*
|
|
157
157
|
* @param {Xapi} xapi
|
|
158
158
|
* @param {String} vmRef
|
|
@@ -24,7 +24,7 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
|
|
|
24
24
|
writer =>
|
|
25
25
|
writer.run({
|
|
26
26
|
stream: forkStreamUnpipe(stream),
|
|
27
|
-
// stream will be forked and transformed, it's not safe to attach
|
|
27
|
+
// stream will be forked and transformed, it's not safe to attach additional properties to it
|
|
28
28
|
streamLength: stream.length,
|
|
29
29
|
maxStreamLength: stream.maxStreamLength, // on encrypted source
|
|
30
30
|
timestamp: metadata.timestamp,
|
|
@@ -45,8 +45,8 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
45
45
|
for (const vdiRef of vdis) {
|
|
46
46
|
const vdi = await this._xapi.getRecord('VDI', vdiRef)
|
|
47
47
|
|
|
48
|
-
// the size a of fully allocated vdi will be virtual_size
|
|
49
|
-
// of the real stream size in general, since a disk is never
|
|
48
|
+
// the size a of fully allocated vdi will be virtual_size exactly, it's a gross over evaluation
|
|
49
|
+
// of the real stream size in general, since a disk is never completely full
|
|
50
50
|
// vdi.physical_size seems to underevaluate a lot the real disk usage of a VDI, as of 2023-10-30
|
|
51
51
|
maxStreamLength += vdi.virtual_size
|
|
52
52
|
}
|
|
@@ -5,6 +5,7 @@ import assert from 'node:assert'
|
|
|
5
5
|
import * as UUID from 'uuid'
|
|
6
6
|
|
|
7
7
|
import { AbstractRemote } from './_AbstractRemote.mjs'
|
|
8
|
+
import { forkDeltaExport } from './_forkDeltaExport.mjs'
|
|
8
9
|
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
|
|
9
10
|
import { Disposable } from 'promise-toolbox'
|
|
10
11
|
import { openVhd } from 'vhd-lib'
|
|
@@ -17,7 +18,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
17
18
|
return IncrementalRemoteWriter
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// we'll transfer the full list if at least one backup should be
|
|
21
|
+
// we'll transfer the full list if at least one backup should be transferred
|
|
21
22
|
// to ensure we don't cut the delta chain
|
|
22
23
|
_filterTransferList(transferList) {
|
|
23
24
|
if (transferList.some(vmBackupMetadata => this._filterPredicate(vmBackupMetadata))) {
|
|
@@ -88,18 +89,10 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
|
|
|
88
89
|
await this._selectBaseVm(metadata)
|
|
89
90
|
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
|
|
90
91
|
|
|
91
|
-
function fork(incrementalExport, label) {
|
|
92
|
-
const { disks, ...forked } = incrementalExport
|
|
93
|
-
forked.disks = {}
|
|
94
|
-
for (const key in disks) {
|
|
95
|
-
forked.disks[key] = disks[key].fork(label)
|
|
96
|
-
}
|
|
97
|
-
return forked
|
|
98
|
-
}
|
|
99
92
|
await this._callWriters(
|
|
100
93
|
writer =>
|
|
101
94
|
writer.transfer({
|
|
102
|
-
deltaExport:
|
|
95
|
+
deltaExport: forkDeltaExport(incrementalExport, writer.constructor.name),
|
|
103
96
|
isVhdDifferencing,
|
|
104
97
|
timestamp: metadata.timestamp,
|
|
105
98
|
vm: metadata.vm,
|
|
@@ -2,6 +2,7 @@ import { createLogger } from '@xen-orchestra/log'
|
|
|
2
2
|
import keyBy from 'lodash/keyBy.js'
|
|
3
3
|
|
|
4
4
|
import { AbstractXapi } from './_AbstractXapi.mjs'
|
|
5
|
+
import { forkDeltaExport } from './_forkDeltaExport.mjs'
|
|
5
6
|
import { exportIncrementalVm } from '../../_incrementalVm.mjs'
|
|
6
7
|
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
|
|
7
8
|
import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
|
|
@@ -50,21 +51,14 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
|
|
50
51
|
if (useNbd) {
|
|
51
52
|
Task.info('Transfer data using NBD')
|
|
52
53
|
}
|
|
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
|
|
60
|
-
}
|
|
61
54
|
|
|
62
|
-
// @todo : reimplement throttle
|
|
55
|
+
// @todo : reimplement throttle
|
|
56
|
+
|
|
63
57
|
const timestamp = Date.now()
|
|
64
58
|
await this._callWriters(
|
|
65
59
|
writer =>
|
|
66
60
|
writer.transfer({
|
|
67
|
-
deltaExport:
|
|
61
|
+
deltaExport: forkDeltaExport(deltaExport, writer.constructor.name),
|
|
68
62
|
isVhdDifferencing,
|
|
69
63
|
timestamp,
|
|
70
64
|
vm,
|
|
@@ -90,7 +90,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
|
|
|
90
90
|
for (const timestamp of timestamps) {
|
|
91
91
|
if (remoteMetadatas[timestamp] !== nbRemotes) {
|
|
92
92
|
// this backup is not present in all the remote
|
|
93
|
-
// should be
|
|
93
|
+
// should be retransferred if not found later
|
|
94
94
|
transferList.push(localMetada.get(timestamp))
|
|
95
95
|
} else {
|
|
96
96
|
// backup is present in local and remote : the chain has already been transferred
|
|
@@ -195,7 +195,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
195
195
|
}
|
|
196
196
|
})
|
|
197
197
|
await Promise.all(vmSnapshots.map(snapshot => populateVdisOtherConfig(xapi, snapshot.$ref)))
|
|
198
|
-
// end of
|
|
198
|
+
// end of compatibility handling
|
|
199
199
|
|
|
200
200
|
// handle snapshot by VDI
|
|
201
201
|
this._jobSnapshotVdis = []
|
|
@@ -298,7 +298,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
298
298
|
// preferNbd is not a guarantee that the backup used NBD, depending on the network configuration,
|
|
299
299
|
// in that case next runs will be full, but there is not an easy way to prevent that
|
|
300
300
|
this._settings.preferNbd &&
|
|
301
|
-
// only delete
|
|
301
|
+
// only delete snapshot data if the config allows it
|
|
302
302
|
this._settings.cbtDestroySnapshotData
|
|
303
303
|
) {
|
|
304
304
|
Task.info('will delete snapshot data')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SynchronizedDisk } from '@xen-orchestra/disk-transform'
|
|
2
|
+
import cloneDeep from 'lodash/cloneDeep.js'
|
|
3
|
+
|
|
4
|
+
export function forkDeltaExport(deltaExport, label) {
|
|
5
|
+
label += ' ' + Math.random()
|
|
6
|
+
const { disks, ...rest } = deltaExport
|
|
7
|
+
const fork = cloneDeep(rest)
|
|
8
|
+
fork.disks = {}
|
|
9
|
+
for (const [key, disk] of Object.entries(disks)) {
|
|
10
|
+
if (!(disk instanceof SynchronizedDisk)) {
|
|
11
|
+
throw new Error('Can only fork synchronized disks ')
|
|
12
|
+
}
|
|
13
|
+
fork.disks[key] = disk.fork(label)
|
|
14
|
+
}
|
|
15
|
+
return fork
|
|
16
|
+
}
|
|
@@ -230,7 +230,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
230
230
|
const path = `${this._vmBackupDir}/${vhds[diskRef]}`
|
|
231
231
|
await adapter.writeVhd(path, disk, {
|
|
232
232
|
// no checksum for VHDs, because they will be invalidated by
|
|
233
|
-
// merges and
|
|
233
|
+
// merges and chains
|
|
234
234
|
checksum: false,
|
|
235
235
|
validator: tmpPath => checkVhd(handler, tmpPath),
|
|
236
236
|
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
|
@@ -21,7 +21,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// @todo use an index if possible
|
|
24
|
-
// @todo : this seems
|
|
24
|
+
// @todo : this seems similar to decorateVmMetadata
|
|
25
25
|
const replicatedVdis = sr.$VDIs
|
|
26
26
|
.filter(vdi => {
|
|
27
27
|
// REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
|
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.61.
|
|
11
|
+
"version": "0.61.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
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/generator-toolbox": "^1.0.
|
|
28
|
+
"@vates/generator-toolbox": "^1.0.3",
|
|
29
29
|
"@vates/nbd-client": "^3.1.3",
|
|
30
30
|
"@vates/parse-duration": "^0.1.1",
|
|
31
31
|
"@xen-orchestra/async-map": "^0.1.2",
|