@xen-orchestra/backups 0.66.0 → 0.67.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 +10 -10
- package/RemoteAdapter.mjs +8 -8
- package/_getOldEntries.mjs +43 -24
- package/_listPartitions.mjs +20 -2
- package/_runners/_vmRunners/_AbstractRemote.mjs +7 -7
- package/_runners/_writers/FullRemoteWriter.mjs +1 -1
- package/_runners/_writers/_MixinRemoteWriter.mjs +1 -1
- package/_runners/_writers/_MixinXapiWriter.mjs +1 -1
- package/formatVmBackups.mjs +1 -0
- package/package.json +4 -4
package/ImportVmBackup.mjs
CHANGED
|
@@ -29,10 +29,10 @@ export class ImportVmBackup {
|
|
|
29
29
|
metadata,
|
|
30
30
|
srUuid,
|
|
31
31
|
xapi,
|
|
32
|
-
settings: {
|
|
32
|
+
settings: { additionalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
|
|
33
33
|
}) {
|
|
34
34
|
this._adapter = adapter
|
|
35
|
-
this._importIncrementalVmSettings = {
|
|
35
|
+
this._importIncrementalVmSettings = { additionalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
|
|
36
36
|
this._metadata = metadata
|
|
37
37
|
this._srUuid = srUuid
|
|
38
38
|
this._xapi = xapi
|
|
@@ -61,11 +61,11 @@ export class ImportVmBackup {
|
|
|
61
61
|
const { mapVdisSrs } = this._importIncrementalVmSettings
|
|
62
62
|
const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
|
|
63
63
|
const disks = {}
|
|
64
|
-
const
|
|
64
|
+
const metadataDir = dirname(metadata._filename)
|
|
65
65
|
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
|
|
66
66
|
|
|
67
67
|
for (const [vdiRef, vdi] of Object.entries(vdis)) {
|
|
68
|
-
const vhdPath = join(
|
|
68
|
+
const vhdPath = join(metadataDir, vhds[vdiRef])
|
|
69
69
|
|
|
70
70
|
let xapiDisk
|
|
71
71
|
try {
|
|
@@ -77,7 +77,7 @@ export class ImportVmBackup {
|
|
|
77
77
|
|
|
78
78
|
let snapshotCandidate, backupCandidate
|
|
79
79
|
if (xapiDisk !== undefined) {
|
|
80
|
-
debug('found disks,
|
|
80
|
+
debug('found disks, will search its snapshots', { snapshots: xapiDisk.snapshots })
|
|
81
81
|
for (const snapshotRef of xapiDisk.snapshots) {
|
|
82
82
|
const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
|
|
83
83
|
|
|
@@ -96,7 +96,7 @@ export class ImportVmBackup {
|
|
|
96
96
|
// have a corresponding backup more recent than metadata ?
|
|
97
97
|
const pathToSnapshotData = await this.#getPathOfVdiSnapshot(snapshot.uuid)
|
|
98
98
|
if (pathToSnapshotData === undefined) {
|
|
99
|
-
debug('no backup linked to this
|
|
99
|
+
debug('no backup linked to this snapshot')
|
|
100
100
|
continue
|
|
101
101
|
}
|
|
102
102
|
if (snapshot.$SR.uuid !== (mapVdisSrs[vdi.$snapshot_of$uuid] ?? this._srUuid)) {
|
|
@@ -112,7 +112,7 @@ export class ImportVmBackup {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
let disk
|
|
115
|
-
const backupWithSnapshotPath = join(
|
|
115
|
+
const backupWithSnapshotPath = join(metadataDir, backupCandidate ?? '')
|
|
116
116
|
if (vhdPath === backupWithSnapshotPath) {
|
|
117
117
|
// all the data are already on the host
|
|
118
118
|
debug('direct reuse of a snapshot')
|
|
@@ -194,7 +194,7 @@ export class ImportVmBackup {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
async #decorateIncrementalVmMetadata() {
|
|
197
|
-
const {
|
|
197
|
+
const { additionalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
|
|
198
198
|
|
|
199
199
|
const ignoredVdis = new Set(
|
|
200
200
|
Object.entries(mapVdisSrs)
|
|
@@ -211,8 +211,8 @@ export class ImportVmBackup {
|
|
|
211
211
|
|
|
212
212
|
const cache = new Map()
|
|
213
213
|
const mapVdisSrRefs = {}
|
|
214
|
-
if (
|
|
215
|
-
backup.vm.tags.push(
|
|
214
|
+
if (additionalVmTag !== undefined) {
|
|
215
|
+
backup.vm.tags.push(additionalVmTag)
|
|
216
216
|
}
|
|
217
217
|
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
|
|
218
218
|
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
|
package/RemoteAdapter.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { formatFilenameDate } from './_filenameDate.mjs'
|
|
|
27
27
|
import { getTmpDir } from './_getTmpDir.mjs'
|
|
28
28
|
import { isMetadataFile } from './_backupType.mjs'
|
|
29
29
|
import { isValidXva } from './_isValidXva.mjs'
|
|
30
|
-
import { listPartitions,
|
|
30
|
+
import { listPartitions, LVM_PARTITION_TYPE_MBR, LVM_PARTITION_TYPE_GPT } from './_listPartitions.mjs'
|
|
31
31
|
import { lvs, pvs } from './_lvm.mjs'
|
|
32
32
|
import { watchStreamSize } from './_watchStreamSize.mjs'
|
|
33
33
|
|
|
@@ -40,7 +40,7 @@ export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
|
|
40
40
|
|
|
41
41
|
export const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
|
|
42
42
|
|
|
43
|
-
const
|
|
43
|
+
const IMMUTABILITY_METADATA_FILENAME = '/immutability.json'
|
|
44
44
|
|
|
45
45
|
const { debug, warn } = createLogger('xo:backups:RemoteAdapter')
|
|
46
46
|
|
|
@@ -312,8 +312,8 @@ export class RemoteAdapter {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
async deleteVmBackups(files) {
|
|
315
|
-
const
|
|
316
|
-
const { delta, full, ...others } = groupBy(
|
|
315
|
+
const metadata = await asyncMap(files, file => this.readVmBackupMetadata(file))
|
|
316
|
+
const { delta, full, ...others } = groupBy(metadata, 'mode')
|
|
317
317
|
|
|
318
318
|
const unsupportedModes = Object.keys(others)
|
|
319
319
|
if (unsupportedModes.length !== 0) {
|
|
@@ -498,7 +498,7 @@ export class RemoteAdapter {
|
|
|
498
498
|
|
|
499
499
|
const results = []
|
|
500
500
|
await asyncMapSettled(partitions, partition =>
|
|
501
|
-
partition.type ===
|
|
501
|
+
partition.type === LVM_PARTITION_TYPE_MBR || partition.type === LVM_PARTITION_TYPE_GPT
|
|
502
502
|
? this._listLvmLogicalVolumes(devicePath, partition, results)
|
|
503
503
|
: results.push(partition)
|
|
504
504
|
)
|
|
@@ -575,7 +575,7 @@ export class RemoteAdapter {
|
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
-
async #
|
|
578
|
+
async #getCacheableDataListVmBackups(dir) {
|
|
579
579
|
debug('generating cache', { path: dir })
|
|
580
580
|
|
|
581
581
|
const handler = this._handler
|
|
@@ -622,7 +622,7 @@ export class RemoteAdapter {
|
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
// nothing cached, or cache unreadable => regenerate it
|
|
625
|
-
const backups = await this.#
|
|
625
|
+
const backups = await this.#getCacheableDataListVmBackups(`${BACKUP_DIR}/${vmUuid}`)
|
|
626
626
|
if (backups === undefined) {
|
|
627
627
|
return
|
|
628
628
|
}
|
|
@@ -779,7 +779,7 @@ export class RemoteAdapter {
|
|
|
779
779
|
// if the remote is immutable, check if this metadata is also immutable
|
|
780
780
|
try {
|
|
781
781
|
// this file is not encrypted
|
|
782
|
-
await this._handler._readFile(
|
|
782
|
+
await this._handler._readFile(IMMUTABILITY_METADATA_FILENAME)
|
|
783
783
|
remoteIsImmutable = true
|
|
784
784
|
} catch (error) {
|
|
785
785
|
if (error.code !== 'ENOENT') {
|
package/_getOldEntries.mjs
CHANGED
|
@@ -97,40 +97,59 @@ export function getOldEntries(minRetentionCount, entries, { longTermRetention =
|
|
|
97
97
|
remaining: retention,
|
|
98
98
|
lastMatchingBucket: null,
|
|
99
99
|
formatter: LTR_DEFINITIONS[duration].makeDateFormatter({ ...settings, dateCreator }),
|
|
100
|
+
entries: {}, // bucket => entry id
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
const nb = entries.length
|
|
103
|
-
const
|
|
104
|
-
|
|
104
|
+
const minDurationEntries = []
|
|
105
|
+
let previousTimestamp = -1
|
|
105
106
|
for (let i = nb - 1; i >= 0; i--) {
|
|
106
107
|
const entry = entries[i]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
if (entry.timestamp !== undefined) {
|
|
109
|
+
// we go through the entries from the last (most recent) to the first (oldest)
|
|
110
|
+
assert.ok(
|
|
111
|
+
previousTimestamp === -1 || entry.timestamp < previousTimestamp,
|
|
112
|
+
`entries must be sorted in desc order ${new Date(entry.timestamp)} , previous : > ${new Date(previousTimestamp)} `
|
|
113
|
+
)
|
|
114
|
+
previousTimestamp = entry.timestamp
|
|
115
|
+
const entryDate = dateCreator(entry.timestamp)
|
|
116
|
+
for (const [duration, { remaining, lastMatchingBucket, formatter }] of Object.entries(dateBuckets)) {
|
|
117
|
+
const bucket = formatter(entryDate)
|
|
118
|
+
if (lastMatchingBucket !== bucket) {
|
|
119
|
+
if (remaining === 0) {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
dateBuckets[duration].lastMatchingBucket = bucket
|
|
123
|
+
dateBuckets[duration].remaining -= 1
|
|
121
124
|
}
|
|
122
|
-
|
|
123
|
-
dateBuckets[duration].remaining -= 1
|
|
124
|
-
dateBuckets[duration].lastMatchingBucket = bucket
|
|
125
|
+
dateBuckets[duration].entries[bucket] = entry
|
|
125
126
|
}
|
|
127
|
+
} else {
|
|
128
|
+
// replicated VM entries or snapshot retention don't have a timestamp
|
|
129
|
+
// but also can't have LTR
|
|
130
|
+
assert.deepStrictEqual(
|
|
131
|
+
longTermRetention,
|
|
132
|
+
{},
|
|
133
|
+
"Can't compute long term retention if entries don't have a timestamp"
|
|
134
|
+
)
|
|
126
135
|
}
|
|
136
|
+
|
|
137
|
+
// also keep it on retention
|
|
127
138
|
if (i >= nb - minRetentionCount) {
|
|
128
|
-
|
|
139
|
+
minDurationEntries.push(entry)
|
|
129
140
|
}
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
}
|
|
142
|
+
const kept = new Set(minDurationEntries)
|
|
143
|
+
for (const { entries } of Object.values(dateBuckets)) {
|
|
144
|
+
for (const entry of Object.values(entries)) {
|
|
145
|
+
kept.add(entry)
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
|
|
149
|
+
// ensure order is the same as the source
|
|
150
|
+
const oldEntries = entries.filter(entry => {
|
|
151
|
+
return !kept.has(entry)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return oldEntries
|
|
136
155
|
}
|
package/_listPartitions.mjs
CHANGED
|
@@ -20,13 +20,31 @@ const IGNORED_PARTITION_TYPES = {
|
|
|
20
20
|
0xd5: true,
|
|
21
21
|
|
|
22
22
|
0x82: true, // swap
|
|
23
|
+
// GPT Linux swap GUID
|
|
24
|
+
'0657fd6d-a4ab-43c4-84e5-0933c84b4f4f': true,
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
// MBR LVM type
|
|
28
|
+
export const LVM_PARTITION_TYPE_MBR = 0x8e
|
|
29
|
+
// GPT LVM type
|
|
30
|
+
export const LVM_PARTITION_TYPE_GPT = 'e6d6d379-f507-44c2-a23c-238f2a3df928'
|
|
26
31
|
|
|
27
32
|
const parsePartxLine = createParser({
|
|
28
33
|
keyTransform: key => (key === 'UUID' ? 'id' : key.toLowerCase()),
|
|
29
|
-
valueTransform: (value, key) =>
|
|
34
|
+
valueTransform: (value, key) => {
|
|
35
|
+
if (key === 'start' || key === 'size') {
|
|
36
|
+
return +value
|
|
37
|
+
}
|
|
38
|
+
// For GPT partitions, type is a UUID string
|
|
39
|
+
if (key === 'type' && !value.startsWith('0x')) {
|
|
40
|
+
return value.toLowerCase()
|
|
41
|
+
}
|
|
42
|
+
// For MBR partitions, type is a hex number as string (e.g., "0x8e")
|
|
43
|
+
if (key === 'type' && value.startsWith('0x')) {
|
|
44
|
+
return parseInt(value, 16)
|
|
45
|
+
}
|
|
46
|
+
return value
|
|
47
|
+
},
|
|
30
48
|
})
|
|
31
49
|
|
|
32
50
|
// returns an empty array in case of a non-partitioned disk
|
|
@@ -74,28 +74,28 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async #computeTransferListPerJob(sourceBackups, remotesBackups) {
|
|
77
|
-
const
|
|
77
|
+
const localMetadata = new Map()
|
|
78
78
|
sourceBackups.forEach(metadata => {
|
|
79
79
|
const timestamp = metadata.timestamp
|
|
80
|
-
|
|
80
|
+
localMetadata.set(timestamp, metadata)
|
|
81
81
|
})
|
|
82
82
|
const nbRemotes = remotesBackups.length
|
|
83
|
-
const
|
|
83
|
+
const remoteMetadata = {}
|
|
84
84
|
remotesBackups.forEach(async remoteBackups => {
|
|
85
85
|
remoteBackups.forEach(metadata => {
|
|
86
86
|
const timestamp = metadata.timestamp
|
|
87
|
-
|
|
87
|
+
remoteMetadata[timestamp] = (remoteMetadata[timestamp] ?? 0) + 1
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
let transferList = []
|
|
92
|
-
const timestamps = [...
|
|
92
|
+
const timestamps = [...localMetadata.keys()]
|
|
93
93
|
timestamps.sort()
|
|
94
94
|
for (const timestamp of timestamps) {
|
|
95
|
-
if (
|
|
95
|
+
if (remoteMetadata[timestamp] !== nbRemotes) {
|
|
96
96
|
// this backup is not present in all the remote
|
|
97
97
|
// should be retransferred if not found later
|
|
98
|
-
transferList.push(
|
|
98
|
+
transferList.push(localMetadata.get(timestamp))
|
|
99
99
|
} else {
|
|
100
100
|
// backup is present in local and remote : the chain has already been transferred
|
|
101
101
|
transferList = []
|
|
@@ -33,7 +33,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
|
33
33
|
let metadata = await this._isAlreadyTransferred(timestamp)
|
|
34
34
|
if (metadata !== undefined) {
|
|
35
35
|
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
|
36
|
-
Task.info('This backup has already been
|
|
36
|
+
Task.info('This backup has already been transferred')
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const oldBackups = getOldEntries(
|
|
@@ -29,7 +29,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
|
|
29
29
|
// the SR that the VM has been replicated on
|
|
30
30
|
const sr = this._sr
|
|
31
31
|
assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
|
|
32
|
-
assert.notEqual(this._targetVmRef, undefined, 'A vm should have been
|
|
32
|
+
assert.notEqual(this._targetVmRef, undefined, 'A vm should have been transferred to be health checked')
|
|
33
33
|
// copy VM
|
|
34
34
|
return Task.run(
|
|
35
35
|
{
|
package/formatVmBackups.mjs
CHANGED
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.67.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"tar": "^6.1.15",
|
|
52
52
|
"uuid": "^9.0.0",
|
|
53
53
|
"value-matcher": "^0.2.0",
|
|
54
|
-
"vhd-lib": "^4.14.
|
|
55
|
-
"xen-api": "^4.7.
|
|
54
|
+
"vhd-lib": "^4.14.4",
|
|
55
|
+
"xen-api": "^4.7.5",
|
|
56
56
|
"yazl": "^2.5.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tmp": "^0.2.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@xen-orchestra/xapi": "^8.
|
|
65
|
+
"@xen-orchestra/xapi": "^8.6.1"
|
|
66
66
|
},
|
|
67
67
|
"license": "AGPL-3.0-or-later",
|
|
68
68
|
"author": {
|