@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.
@@ -29,10 +29,10 @@ export class ImportVmBackup {
29
29
  metadata,
30
30
  srUuid,
31
31
  xapi,
32
- settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
32
+ settings: { additionalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
33
33
  }) {
34
34
  this._adapter = adapter
35
- this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
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 metdataDir = dirname(metadata._filename)
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(metdataDir, vhds[vdiRef])
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, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
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 snaphot')
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(metdataDir, backupCandidate ?? '')
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 { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
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 (additionnalVmTag !== undefined) {
215
- backup.vm.tags.push(additionnalVmTag)
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, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
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 IMMUTABILTY_METADATA_FILENAME = '/immutability.json'
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 metadatas = await asyncMap(files, file => this.readVmBackupMetadata(file))
316
- const { delta, full, ...others } = groupBy(metadatas, 'mode')
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 === LVM_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 #getCachabledDataListVmBackups(dir) {
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.#getCachabledDataListVmBackups(`${BACKUP_DIR}/${vmUuid}`)
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(IMMUTABILTY_METADATA_FILENAME)
782
+ await this._handler._readFile(IMMUTABILITY_METADATA_FILENAME)
783
783
  remoteIsImmutable = true
784
784
  } catch (error) {
785
785
  if (error.code !== 'ENOENT') {
@@ -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 oldEntries = []
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
- const entryDate = dateCreator(entry.timestamp)
108
- let shouldBeKept = false
109
- for (const [duration, { remaining, lastMatchingBucket, formatter }] of Object.entries(dateBuckets)) {
110
- if (remaining === 0) {
111
- continue
112
- }
113
- const bucket = formatter(entryDate)
114
- if (lastMatchingBucket !== bucket) {
115
- if (lastMatchingBucket !== null) {
116
- assert.strictEqual(
117
- lastMatchingBucket > bucket,
118
- true,
119
- `entries must be sorted in asc order ${lastMatchingBucket} ${bucket}`
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
- shouldBeKept = true
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
- shouldBeKept = true
139
+ minDurationEntries.push(entry)
129
140
  }
130
- if (!shouldBeKept) {
131
- oldEntries.push(entry)
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
- // we expect the entries to be in the right order
135
- return oldEntries.reverse()
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
  }
@@ -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
- export const LVM_PARTITION_TYPE = 0x8e
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) => (key === 'start' || key === 'size' || key === 'type' ? +value : value),
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 localMetada = new Map()
77
+ const localMetadata = new Map()
78
78
  sourceBackups.forEach(metadata => {
79
79
  const timestamp = metadata.timestamp
80
- localMetada.set(timestamp, metadata)
80
+ localMetadata.set(timestamp, metadata)
81
81
  })
82
82
  const nbRemotes = remotesBackups.length
83
- const remoteMetadatas = {}
83
+ const remoteMetadata = {}
84
84
  remotesBackups.forEach(async remoteBackups => {
85
85
  remoteBackups.forEach(metadata => {
86
86
  const timestamp = metadata.timestamp
87
- remoteMetadatas[timestamp] = (remoteMetadatas[timestamp] ?? 0) + 1
87
+ remoteMetadata[timestamp] = (remoteMetadata[timestamp] ?? 0) + 1
88
88
  })
89
89
  })
90
90
 
91
91
  let transferList = []
92
- const timestamps = [...localMetada.keys()]
92
+ const timestamps = [...localMetadata.keys()]
93
93
  timestamps.sort()
94
94
  for (const timestamp of timestamps) {
95
- if (remoteMetadatas[timestamp] !== nbRemotes) {
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(localMetada.get(timestamp))
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 transfered')
36
+ Task.info('This backup has already been transferred')
37
37
  }
38
38
 
39
39
  const oldBackups = getOldEntries(
@@ -99,7 +99,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
99
99
  srUuid,
100
100
  xapi,
101
101
  settings: {
102
- additionnalVmTag: 'xo:no-bak=Health Check',
102
+ additionalVmTag: 'xo:no-bak=Health Check',
103
103
  },
104
104
  }).run()
105
105
  let restoredVm
@@ -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 transfered to be health checked')
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
  {
@@ -43,6 +43,7 @@ function formatVmBackup(backup) {
43
43
  uuid: backup.vm.uuid,
44
44
  name_description: backup.vm.name_description,
45
45
  name_label: backup.vm.name_label,
46
+ tags: backup.vm.tags,
46
47
  },
47
48
 
48
49
  differencingVhds,
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.66.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.3",
55
- "xen-api": "^4.7.4",
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.5.0"
65
+ "@xen-orchestra/xapi": "^8.6.1"
66
66
  },
67
67
  "license": "AGPL-3.0-or-later",
68
68
  "author": {