@xen-orchestra/backups 0.53.1 → 0.54.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.
@@ -49,26 +49,25 @@ export class HealthCheckVmBackup {
49
49
  }
50
50
 
51
51
  // wait for the 'Running' event to be really stored in local xapi object cache
52
+
52
53
  restoredVm = await xapi.waitObjectState(restoredVm.$ref, vm => vm.power_state === 'Running', {
53
54
  timeout: remainingTimeout,
55
+ timeoutMessage: refOrUuid =>
56
+ `local xapi did not get Running state for VM ${refOrUuid} after ${timeout / 1000} second`,
54
57
  })
55
58
 
56
59
  const running = new Date()
57
60
  remainingTimeout -= running - started
58
61
 
59
- if (remainingTimeout < 0) {
60
- throw new Error(`local xapi did not get Running state for VM ${restoredId} after ${timeout / 1000} second`)
61
- }
62
62
  // wait for the guest tool version to be defined
63
63
  await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
64
64
  timeout: remainingTimeout,
65
+ timeoutMessage: refOrUuid =>
66
+ `timeout reached while waiting for ${refOrUuid} to report the driver version through the Xen tools. Please check or update the Xen tools.`,
65
67
  })
66
68
 
67
69
  const guestToolsReady = new Date()
68
70
  remainingTimeout -= guestToolsReady - running
69
- if (remainingTimeout < 0) {
70
- throw new Error(`local xapi did not get he guest tools check ${restoredId} after ${timeout / 1000} second`)
71
- }
72
71
 
73
72
  if (waitForScript) {
74
73
  const startedRestoredVm = await xapi.waitObjectState(
@@ -79,19 +78,10 @@ export class HealthCheckVmBackup {
79
78
  vm.xenstore_data['vm-data/xo-backup-health-check'] === 'failure'),
80
79
  {
81
80
  timeout: remainingTimeout,
81
+ timeoutMessage: refOrUuid =>
82
+ `timeout reached while waiting for ${refOrUuid} to report the startup script execution.`,
82
83
  }
83
84
  )
84
- const scriptOk = new Date()
85
- remainingTimeout -= scriptOk - guestToolsReady
86
- if (remainingTimeout < 0) {
87
- throw new Error(
88
- `Backup health check script did not update vm-data/xo-backup-health-check of ${restoredId} after ${
89
- timeout / 1000
90
- } second, got ${
91
- startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check']
92
- } instead of 'success' or 'failure'`
93
- )
94
- }
95
85
 
96
86
  if (startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check'] !== 'success') {
97
87
  const message = startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check-error']
package/RemoteAdapter.mjs CHANGED
@@ -18,6 +18,7 @@ import fromEvent from 'promise-toolbox/fromEvent'
18
18
  import groupBy from 'lodash/groupBy.js'
19
19
  import pDefer from 'promise-toolbox/defer'
20
20
  import pickBy from 'lodash/pickBy.js'
21
+ import reduce from 'lodash/reduce.js'
21
22
  import tar from 'tar'
22
23
  import zlib from 'zlib'
23
24
 
@@ -826,6 +827,29 @@ export class RemoteAdapter {
826
827
  }
827
828
  return metadata
828
829
  }
830
+
831
+ #computeTotalBackupSizeRecursively(backups) {
832
+ return reduce(
833
+ backups,
834
+ (prev, backup) => {
835
+ const _backup = Array.isArray(backup) ? this.#computeTotalBackupSizeRecursively(backup) : backup
836
+ return {
837
+ onDisk: prev.onDisk + (_backup.onDisk ?? _backup.size),
838
+ }
839
+ },
840
+ { onDisk: 0 }
841
+ )
842
+ }
843
+
844
+ async getTotalVmBackupSize() {
845
+ return this.#computeTotalBackupSizeRecursively(await this.listAllVmBackups())
846
+ }
847
+
848
+ async getTotalBackupSize() {
849
+ const vmBackupSize = await this.getTotalVmBackupSize()
850
+ // @TODO: add `getTotalXoBackupSize` and `getTotalPoolBackupSize` once `size` is implemented by fs
851
+ return vmBackupSize
852
+ }
829
853
  }
830
854
 
831
855
  Object.assign(RemoteAdapter.prototype, {
package/_cleanVm.mjs CHANGED
@@ -121,7 +121,19 @@ export async function checkAliases(
121
121
  ) {
122
122
  const aliasFound = []
123
123
  for (const alias of aliasPaths) {
124
- const target = await resolveVhdAlias(handler, alias)
124
+ let target
125
+ try {
126
+ target = await resolveVhdAlias(handler, alias)
127
+ } catch (err) {
128
+ if (err.code === 'ENOENT') {
129
+ logWarn('missing target of alias', { alias })
130
+ if (remove) {
131
+ logInfo('removing alias and non VHD target', { alias, target })
132
+ await handler.unlink(target)
133
+ await handler.unlink(alias)
134
+ }
135
+ }
136
+ }
125
137
 
126
138
  if (!isVhdFile(target)) {
127
139
  logWarn('alias references non VHD target', { alias, target })
@@ -201,9 +213,9 @@ export async function cleanVm(
201
213
 
202
214
  // remove broken VHDs
203
215
  await asyncMap(vhds, async path => {
204
- if(removeTmp && basename(path)[0] === '.'){
216
+ if (removeTmp && basename(path)[0] === '.') {
205
217
  logInfo('deleting temporary VHD', { path })
206
- return VhdAbstract.unlink(handler, path)
218
+ return VhdAbstract.unlink(handler, path)
207
219
  }
208
220
  try {
209
221
  await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
@@ -3,7 +3,6 @@ import ignoreErrors from 'promise-toolbox/ignoreErrors'
3
3
  import { asyncMap } from '@xen-orchestra/async-map'
4
4
  import { CancelToken } from 'promise-toolbox'
5
5
  import { compareVersions } from 'compare-versions'
6
- import { createVhdStreamWithLength } from 'vhd-lib'
7
6
  import { defer } from 'golike-defer'
8
7
 
9
8
  import { cancelableMap } from './_cancelableMap.mjs'
@@ -53,14 +52,32 @@ export async function exportIncrementalVm(
53
52
  $snapshot_of$uuid: vdi.$snapshot_of?.uuid,
54
53
  $SR$uuid: vdi.$SR.uuid,
55
54
  }
56
-
57
- streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
58
- baseRef: baseVdi?.$ref,
59
- cancelToken,
60
- format: 'vhd',
61
- nbdConcurrency,
62
- preferNbd,
63
- })
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
+ }
64
81
  })
65
82
 
66
83
  const suspendVdi = vm.$suspend_VDI
@@ -227,9 +244,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
227
244
  if (typeof stream === 'function') {
228
245
  stream = await stream()
229
246
  }
230
- if (stream.length === undefined) {
231
- stream = await createVhdStreamWithLength(stream)
232
- }
233
247
  await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
234
248
  await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
235
249
  await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label)
@@ -1,3 +1,5 @@
1
+ import { createLogger } from '@xen-orchestra/log'
2
+
1
3
  import { asyncEach } from '@vates/async-each'
2
4
  import assert from 'node:assert'
3
5
  import * as UUID from 'uuid'
@@ -11,6 +13,7 @@ import { Disposable } from 'promise-toolbox'
11
13
  import { openVhd } from 'vhd-lib'
12
14
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
13
15
 
16
+ const { warn } = createLogger('xo:backups:Incrementalremote')
14
17
  class IncrementalRemoteVmBackupRunner extends AbstractRemote {
15
18
  _getRemoteWriter() {
16
19
  return IncrementalRemoteWriter
@@ -46,11 +49,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
46
49
  })
47
50
 
48
51
  const presentBaseVdis = new Map(baseUuidToSrcVdi)
49
- await this._callWriters(
50
- writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
51
- 'writer.checkBaseVdis()',
52
- false
53
- )
52
+ await this._callWriters(writer => writer.checkBaseVdis(presentBaseVdis), 'writer.checkBaseVdis()', false)
54
53
  // check if the parent vdi are present in all the remotes
55
54
  baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
56
55
  if (!presentBaseVdis.has(baseUuid)) {
@@ -64,17 +63,29 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
64
63
 
65
64
  for (const metadata of transferList) {
66
65
  assert.strictEqual(metadata.mode, 'delta')
67
- await this._selectBaseVm(metadata)
68
- await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
69
66
  const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
70
67
  useChain: false,
71
68
  })
72
-
69
+ // don't trust metadata too much
70
+ // recompute if it's a base backup
71
+ // recompute if disks are differencing or not
73
72
  const isVhdDifferencing = {}
74
73
 
75
74
  await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
76
75
  isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
77
76
  })
77
+ const hasDifferencingDisk = Object.values(isVhdDifferencing).includes(true)
78
+ if (metadata.isBase === hasDifferencingDisk) {
79
+ warn(`Metadata isBase and real disk value are different`, {
80
+ metadataIsBase: metadata.isBase,
81
+ diskIsBase: !hasDifferencingDisk,
82
+ isVhdDifferencing,
83
+ })
84
+ }
85
+ metadata.isBase = !hasDifferencingDisk
86
+ metadata.isVhdDifferencing = isVhdDifferencing
87
+ await this._selectBaseVm(metadata)
88
+ await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
78
89
 
79
90
  incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
80
91
  await this._callWriters(
@@ -203,7 +203,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
203
203
  for (const srcVdi of srcVdis) {
204
204
  const snapshots = await xapi.getRecords('VDI', srcVdi.snapshots)
205
205
  for (const snapshot of snapshots) {
206
- if (snapshot.other_config[JOB_ID] === jobId) {
206
+ // only keep the snapshot related to this backup job
207
+ // and only if the job is still using purge snapshot data or if the disk
208
+ // is not a cbt metadata disk ( expect a type: user for normal disks)
209
+ if (
210
+ snapshot.other_config[JOB_ID] === jobId &&
211
+ (this._settings.cbtDestroySnapshotData || snapshot.type !== 'cbt_metadata')
212
+ ) {
207
213
  this._jobSnapshotVdis.push(snapshot)
208
214
  }
209
215
  }
@@ -219,6 +225,14 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
219
225
  await xapi.barrier()
220
226
  // ensure cached object are up to date
221
227
  this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
228
+
229
+ // get the datetime of the most recent snapshot
230
+ const lastSnapshotDateTime = this._jobSnapshotVdis
231
+ .map(({ other_config }) => other_config[DATETIME])
232
+ .sort()
233
+ .pop()
234
+
235
+ // remove older snapshot schedule per schedule
222
236
  const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
223
237
  await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
224
238
  const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
@@ -231,10 +245,13 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
231
245
  ...allSettings[scheduleId],
232
246
  ...allSettings[this._vm.uuid],
233
247
  }
234
- // ensure we never delete the last one for delta
235
- const minRetention = this.job.mode === 'delta' ? 1 : 0
236
- const retention = Math.max(settings.snapshotRetention ?? 0, minRetention)
248
+ const retention = settings.snapshotRetention ?? 0
237
249
  await asyncMap(getOldEntries(retention, datetimes), async datetime => {
250
+ // keep the last snapshot across all schedules for delta
251
+ // since we'll need it to compute delta for next backup
252
+ if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
253
+ return
254
+ }
238
255
  const vdis = snapshotPerDatetime[datetime]
239
256
  let vmRef
240
257
  // if there is an attached VM => destroy the VM (Non CBT backups)
@@ -293,7 +310,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
293
310
  await this._xapi.VDI_dataDestroy(vdiRef)
294
311
  Task.info(`Snapshot data has been deleted`, { vdiRef })
295
312
  } catch (error) {
296
- Task.warning(`Couldn't deleted snapshot data`, { error, vdiRef })
313
+ Task.warning(`Couldn't delete snapshot data`, { error, vdiRef })
297
314
  }
298
315
  }
299
316
  }
@@ -66,7 +66,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
66
66
 
67
67
  async beforeBackup() {
68
68
  await super.beforeBackup()
69
- return this._cleanVm({ merge: true })
69
+ return this._cleanVm({ merge: true, remove: true })
70
70
  }
71
71
 
72
72
  prepare({ isFull }) {
@@ -149,6 +149,9 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
149
149
  assert.notStrictEqual(parentPath, undefined, 'A differential VHD must have a parent')
150
150
  // forbid any kind of loop
151
151
  assert.ok(basename(parentPath) < basename(path), `vhd must be sorted to be chained`)
152
+ // re-chainVhd is mandatory
153
+ // since the parent may be a alias or not
154
+ // and the child may be the other
152
155
  await chainVhd(handler, parentPath, handler, path)
153
156
  }
154
157
 
@@ -15,11 +15,15 @@ import assert from 'node:assert'
15
15
  export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
16
16
  async checkBaseVdis(baseUuidToSrcVdi) {
17
17
  const sr = this._sr
18
+ if (baseUuidToSrcVdi.size === 0) {
19
+ // searching for the vdis is expensive
20
+ // don't do it if there is nothing to find
21
+ return
22
+ }
18
23
 
19
24
  // @todo use an index if possible
20
25
  // @todo : this seems similare to decorateVmMetadata
21
-
22
- const replicatedVdis = sr.$VDIs
26
+ const replicatedVdis = sr.$VDIs
23
27
  .filter(vdi => {
24
28
  // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
25
29
  return baseUuidToSrcVdi.has(vdi?.other_config[COPY_OF])
@@ -103,11 +107,10 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
103
107
  .filter(_ => !!_)
104
108
  // @todo use index ?
105
109
 
106
- const replicatedVdis = sr.$VDIs
107
- .filter(vdi => {
108
- // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
109
- return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
110
- })
110
+ const replicatedVdis = sr.$VDIs.filter(vdi => {
111
+ // REPLICATED_TO_SR_UUID is not used here since we are already filtering from sr.$VDIs
112
+ return sourceVdiUuids.includes(vdi?.other_config[COPY_OF])
113
+ })
111
114
 
112
115
  Object.values(backup.vdis).forEach(vdi => {
113
116
  vdi.other_config[COPY_OF] = vdi.uuid
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.53.1",
11
+ "version": "0.54.1",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -23,13 +23,13 @@
23
23
  "@vates/cached-dns.lookup": "^1.0.0",
24
24
  "@vates/compose": "^2.1.0",
25
25
  "@vates/decorate-with": "^2.1.0",
26
- "@vates/disposable": "^0.1.5",
27
- "@vates/fuse-vhd": "^2.1.1",
28
- "@vates/nbd-client": "^3.1.0",
26
+ "@vates/disposable": "^0.1.6",
27
+ "@vates/fuse-vhd": "^2.1.2",
28
+ "@vates/nbd-client": "^3.1.1",
29
29
  "@vates/parse-duration": "^0.1.1",
30
30
  "@xen-orchestra/async-map": "^0.1.2",
31
- "@xen-orchestra/fs": "^4.1.7",
32
- "@xen-orchestra/log": "^0.6.0",
31
+ "@xen-orchestra/fs": "^4.2.0",
32
+ "@xen-orchestra/log": "^0.7.0",
33
33
  "@xen-orchestra/template": "^0.1.0",
34
34
  "app-conf": "^3.0.0",
35
35
  "compare-versions": "^6.0.0",
@@ -47,8 +47,8 @@
47
47
  "tar": "^6.1.15",
48
48
  "uuid": "^9.0.0",
49
49
  "value-matcher": "^0.2.0",
50
- "vhd-lib": "^4.11.0",
51
- "xen-api": "^4.2.0",
50
+ "vhd-lib": "^4.11.1",
51
+ "xen-api": "^4.4.0",
52
52
  "yazl": "^2.5.1"
53
53
  },
54
54
  "devDependencies": {
@@ -59,7 +59,7 @@
59
59
  "tmp": "^0.2.1"
60
60
  },
61
61
  "peerDependencies": {
62
- "@xen-orchestra/xapi": "^7.4.0"
62
+ "@xen-orchestra/xapi": "^7.6.1"
63
63
  },
64
64
  "license": "AGPL-3.0-or-later",
65
65
  "author": {