@xen-orchestra/backups 0.52.3 → 0.53.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.
@@ -2,39 +2,39 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
2
2
  import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
3
3
  import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
4
4
  import { watchStreamSize } from '../../_watchStreamSize.mjs'
5
- import { Task } from '../../Task.mjs'
6
5
 
7
6
  export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote {
8
7
  _getRemoteWriter() {
9
8
  return FullRemoteWriter
10
9
  }
10
+
11
+ _filterTransferList(transferList) {
12
+ return transferList.filter(this._filterPredicate)
13
+ }
14
+
11
15
  async _run() {
12
16
  const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
13
17
 
14
- if (transferList.length > 0) {
15
- for (const metadata of transferList) {
16
- const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
17
- const sizeContainer = watchStreamSize(stream)
18
+ for (const metadata of transferList) {
19
+ const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
20
+ const sizeContainer = watchStreamSize(stream)
18
21
 
19
- // @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
20
- await this._callWriters(
21
- writer =>
22
- writer.run({
23
- stream: forkStreamUnpipe(stream),
24
- // stream will be forked and transformed, it's not safe to attach additionnal properties to it
25
- streamLength: stream.length,
26
- timestamp: metadata.timestamp,
27
- vm: metadata.vm,
28
- vmSnapshot: metadata.vmSnapshot,
29
- sizeContainer,
30
- }),
31
- 'writer.run()'
32
- )
33
- // for healthcheck
34
- this._tags = metadata.vm.tags
35
- }
36
- } else {
37
- Task.info('No new data to upload for this VM')
22
+ // @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
23
+ await this._callWriters(
24
+ writer =>
25
+ writer.run({
26
+ stream: forkStreamUnpipe(stream),
27
+ // stream will be forked and transformed, it's not safe to attach additionnal properties to it
28
+ streamLength: stream.length,
29
+ timestamp: metadata.timestamp,
30
+ vm: metadata.vm,
31
+ vmSnapshot: metadata.vmSnapshot,
32
+ sizeContainer,
33
+ }),
34
+ 'writer.run()'
35
+ )
36
+ // for healthcheck
37
+ this._tags = metadata.vm.tags
38
38
  }
39
39
  }
40
40
  }
@@ -7,7 +7,6 @@ import mapValues from 'lodash/mapValues.js'
7
7
  import { AbstractRemote } from './_AbstractRemote.mjs'
8
8
  import { forkDeltaExport } from './_forkDeltaExport.mjs'
9
9
  import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
10
- import { Task } from '../../Task.mjs'
11
10
  import { Disposable } from 'promise-toolbox'
12
11
  import { openVhd } from 'vhd-lib'
13
12
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
@@ -16,6 +15,16 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
16
15
  _getRemoteWriter() {
17
16
  return IncrementalRemoteWriter
18
17
  }
18
+
19
+ // we'll transfer the full list if at least one backup should be transfered
20
+ // to ensure we don't cut the delta chain
21
+ _filterTransferList(transferList) {
22
+ if (transferList.some(vmBackupMetadata => this._filterPredicate(vmBackupMetadata))) {
23
+ return transferList
24
+ }
25
+ return []
26
+ }
27
+
19
28
  async _selectBaseVm(metadata) {
20
29
  // for each disk , get the parent
21
30
  const baseUuidToSrcVdi = new Map()
@@ -53,50 +62,46 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
53
62
  async _run() {
54
63
  const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
55
64
 
56
- if (transferList.length > 0) {
57
- for (const metadata of transferList) {
58
- assert.strictEqual(metadata.mode, 'delta')
59
- await this._selectBaseVm(metadata)
60
- await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
61
- const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
62
- useChain: false,
63
- })
65
+ for (const metadata of transferList) {
66
+ assert.strictEqual(metadata.mode, 'delta')
67
+ await this._selectBaseVm(metadata)
68
+ await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
69
+ const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
70
+ useChain: false,
71
+ })
64
72
 
65
- const isVhdDifferencing = {}
73
+ const isVhdDifferencing = {}
66
74
 
67
- await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
68
- isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
69
- })
75
+ await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
76
+ isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
77
+ })
70
78
 
71
- incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
72
- await this._callWriters(
73
- writer =>
74
- writer.transfer({
75
- deltaExport: forkDeltaExport(incrementalExport),
76
- isVhdDifferencing,
77
- timestamp: metadata.timestamp,
78
- vm: metadata.vm,
79
- vmSnapshot: metadata.vmSnapshot,
80
- }),
81
- 'writer.transfer()'
82
- )
83
- // this will update parent name with the needed alias
84
- await this._callWriters(
85
- writer =>
86
- writer.updateUuidAndChain({
87
- isVhdDifferencing,
88
- timestamp: metadata.timestamp,
89
- vdis: incrementalExport.vdis,
90
- }),
91
- 'writer.updateUuidAndChain()'
92
- )
79
+ incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
80
+ await this._callWriters(
81
+ writer =>
82
+ writer.transfer({
83
+ deltaExport: forkDeltaExport(incrementalExport),
84
+ isVhdDifferencing,
85
+ timestamp: metadata.timestamp,
86
+ vm: metadata.vm,
87
+ vmSnapshot: metadata.vmSnapshot,
88
+ }),
89
+ 'writer.transfer()'
90
+ )
91
+ // this will update parent name with the needed alias
92
+ await this._callWriters(
93
+ writer =>
94
+ writer.updateUuidAndChain({
95
+ isVhdDifferencing,
96
+ timestamp: metadata.timestamp,
97
+ vdis: incrementalExport.vdis,
98
+ }),
99
+ 'writer.updateUuidAndChain()'
100
+ )
93
101
 
94
- await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
95
- // for healthcheck
96
- this._tags = metadata.vm.tags
97
- }
98
- } else {
99
- Task.info('No new data to upload for this VM')
102
+ await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
103
+ // for healthcheck
104
+ this._tags = metadata.vm.tags
100
105
  }
101
106
  }
102
107
  }
@@ -1,14 +1,18 @@
1
- import { asyncEach } from '@vates/async-each'
1
+ import groupBy from 'lodash/groupBy.js'
2
+
2
3
  import { decorateMethodsWith } from '@vates/decorate-with'
3
4
  import { defer } from 'golike-defer'
4
5
  import { Disposable } from 'promise-toolbox'
6
+ import { createPredicate } from 'value-matcher'
5
7
 
6
8
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
7
9
 
8
10
  import { Abstract } from './_Abstract.mjs'
9
11
  import { extractIdsFromSimplePattern } from '../../extractIdsFromSimplePattern.mjs'
12
+ import { Task } from '../../Task.mjs'
10
13
 
11
14
  export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
15
+ _filterPredicate
12
16
  constructor({
13
17
  config,
14
18
  job,
@@ -57,39 +61,74 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
57
61
  })
58
62
  )
59
63
  })
64
+ const { filter } = job
65
+ if (filter === undefined) {
66
+ this._filterPredicate = () => true
67
+ } else {
68
+ this._filterPredicate = createPredicate(filter)
69
+ }
60
70
  }
61
71
 
62
- async _computeTransferList(predicate) {
63
- const vmBackups = await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, predicate)
72
+ async #computeTransferListPerJob(sourceBackups, remotesBackups) {
64
73
  const localMetada = new Map()
65
- Object.values(vmBackups).forEach(metadata => {
74
+ sourceBackups.forEach(metadata => {
66
75
  const timestamp = metadata.timestamp
67
76
  localMetada.set(timestamp, metadata)
68
77
  })
69
- const nbRemotes = Object.keys(this.remoteAdapters).length
78
+ const nbRemotes = remotesBackups.length
70
79
  const remoteMetadatas = {}
71
- await asyncEach(Object.values(this.remoteAdapters), async remoteAdapter => {
72
- const remoteMetadata = await remoteAdapter.listVmBackups(this._vmUuid, predicate)
73
- remoteMetadata.forEach(metadata => {
80
+ remotesBackups.forEach(async remoteBackups => {
81
+ remoteBackups.forEach(metadata => {
74
82
  const timestamp = metadata.timestamp
75
83
  remoteMetadatas[timestamp] = (remoteMetadatas[timestamp] ?? 0) + 1
76
84
  })
77
85
  })
78
86
 
79
- let chain = []
87
+ let transferList = []
80
88
  const timestamps = [...localMetada.keys()]
81
89
  timestamps.sort()
82
90
  for (const timestamp of timestamps) {
83
91
  if (remoteMetadatas[timestamp] !== nbRemotes) {
84
92
  // this backup is not present in all the remote
85
93
  // should be retransfered if not found later
86
- chain.push(localMetada.get(timestamp))
94
+ transferList.push(localMetada.get(timestamp))
87
95
  } else {
88
96
  // backup is present in local and remote : the chain has already been transferred
89
- chain = []
97
+ transferList = []
98
+ }
99
+ }
100
+ if (transferList.length > 0) {
101
+ const filteredTransferList = this._filterTransferList(transferList)
102
+ if (filteredTransferList.length > 0) {
103
+ return filteredTransferList
104
+ } else {
105
+ Task.info('This VM is excluded by the job filter')
106
+ return []
90
107
  }
108
+ } else {
109
+ Task.info('No new data to upload for this VM')
91
110
  }
92
- return chain
111
+
112
+ return []
113
+ }
114
+
115
+ /**
116
+ *
117
+ * @param {*} vmPredicate a callback checking if backup is eligible for transfer. This filter MUST NOT cut delta chains
118
+ * @returns
119
+ */
120
+ async _computeTransferList(vmPredicate) {
121
+ const sourceBackups = Object.values(await this._sourceRemoteAdapter.listVmBackups(this._vmUuid, vmPredicate))
122
+ const remotesBackups = await Promise.all(
123
+ Object.values(this.remoteAdapters).map(remoteAdapter => remoteAdapter.listVmBackups(this._vmUuid, vmPredicate))
124
+ )
125
+ const sourceBackupByJobId = groupBy(sourceBackups, 'jobId')
126
+ const transferByJobs = await Promise.all(
127
+ Object.values(sourceBackupByJobId).map(vmBackupsByJob =>
128
+ this.#computeTransferListPerJob(vmBackupsByJob, remotesBackups)
129
+ )
130
+ )
131
+ return transferByJobs.flat(1)
93
132
  }
94
133
 
95
134
  async run($defer) {
@@ -214,8 +214,12 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
214
214
  const allSettings = this.job.settings
215
215
  const baseSettings = this._baseSettings
216
216
 
217
- const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
218
217
  const xapi = this._xapi
218
+ // ensure all the event has been processed by the xapi
219
+ await xapi.barrier()
220
+ // ensure cached object are up to date
221
+ this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
222
+ const snapshotsPerSchedule = groupBy(this._jobSnapshotVdis, _ => _.other_config[SCHEDULE_ID])
219
223
  await asyncMap(Object.entries(snapshotsPerSchedule), async ([scheduleId, snapshots]) => {
220
224
  const snapshotPerDatetime = groupBy(snapshots, _ => _.other_config[DATETIME])
221
225
 
@@ -235,13 +239,12 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
235
239
  let vmRef
236
240
  // if there is an attached VM => destroy the VM (Non CBT backups)
237
241
  for (const vdi of vdis) {
238
- if (vdi.$VBDs.length > 0) {
239
- const vbds = vdi.$VBDs
242
+ const vbds = vdi.$VBDs.filter(({ $VM }) => $VM.is_control_domain === false)
243
+ if (vbds.length > 0) {
240
244
  // only one VM linked to this vdi
241
245
  // this will throw error for VDI still attached to control domain
242
246
  assert.strictEqual(vbds.length, 1, 'VDI must be free or attached to exactly one VM')
243
247
  const vm = vbds[0].$VM
244
- assert.strictEqual(vm.is_control_domain, false, `Disk is still attached to DOM0 VM`) // don't delete a VM (especially a control domain)
245
248
  assert.strictEqual(vm.is_a_snapshot, true, `VM must be a snapshot`) // don't delete a VM (especially a control domain)
246
249
 
247
250
  const vmRefVdi = vm.$ref
@@ -287,7 +290,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
287
290
  for (const vdiRef of vdiRefs) {
288
291
  try {
289
292
  // data_destroy will fail with a VDI_NO_CBT_METADATA error if CBT is not enabled on this VDI
290
- await this._xapi.call('VDI.data_destroy', vdiRef)
293
+ await this._xapi.VDI_dataDestroy(vdiRef)
291
294
  Task.info(`Snapshot data has been deleted`, { vdiRef })
292
295
  } catch (error) {
293
296
  Task.warning(`Couldn't deleted snapshot data`, { error, vdiRef })
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.52.3",
11
+ "version": "0.53.1",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -46,6 +46,7 @@
46
46
  "proper-lockfile": "^4.1.2",
47
47
  "tar": "^6.1.15",
48
48
  "uuid": "^9.0.0",
49
+ "value-matcher": "^0.2.0",
49
50
  "vhd-lib": "^4.11.0",
50
51
  "xen-api": "^4.2.0",
51
52
  "yazl": "^2.5.1"
@@ -58,7 +59,7 @@
58
59
  "tmp": "^0.2.1"
59
60
  },
60
61
  "peerDependencies": {
61
- "@xen-orchestra/xapi": "^7.3.0"
62
+ "@xen-orchestra/xapi": "^7.4.0"
62
63
  },
63
64
  "license": "AGPL-3.0-or-later",
64
65
  "author": {