@xen-orchestra/backups 0.44.6 → 0.44.7

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.
@@ -160,10 +160,10 @@ export class ImportVmBackup {
160
160
  // update the stream with the negative vhd stream
161
161
  stream = await negativeVhd.stream()
162
162
  vdis[vdiRef].baseVdi = snapshotCandidate
163
- } catch (err) {
163
+ } catch (error) {
164
164
  // can be a broken VHD chain, a vhd chain with a key backup, ....
165
165
  // not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
166
- warn(`can't use differential restore`, err)
166
+ warn(`can't use differential restore`, { error })
167
167
  disposableDescendants?.dispose()
168
168
  }
169
169
  }
package/RemoteAdapter.mjs CHANGED
@@ -191,13 +191,14 @@ export class RemoteAdapter {
191
191
  // check if we will be allowed to merge a a vhd created in this adapter
192
192
  // with the vhd at path `path`
193
193
  async isMergeableParent(packedParentUid, path) {
194
- return await Disposable.use(openVhd(this.handler, path), vhd => {
194
+ return await Disposable.use(VhdSynthetic.fromVhdChain(this.handler, path), vhd => {
195
195
  // this baseUuid is not linked with this vhd
196
196
  if (!vhd.footer.uuid.equals(packedParentUid)) {
197
197
  return false
198
198
  }
199
199
 
200
- const isVhdDirectory = vhd instanceof VhdDirectory
200
+ // check if all the chain is composed of vhd directory
201
+ const isVhdDirectory = vhd.checkVhdsClass(VhdDirectory)
201
202
  return isVhdDirectory
202
203
  ? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
203
204
  : !this.useVhdDirectory()
package/_cleanVm.mjs CHANGED
@@ -437,7 +437,8 @@ export async function cleanVm(
437
437
  }
438
438
  }
439
439
 
440
- logWarn('unused VHD', { path: vhd })
440
+ // no warning because a VHD can be unused for perfectly good reasons,
441
+ // e.g. the corresponding backup (metadata file) has been deleted
441
442
  if (remove) {
442
443
  logInfo('deleting unused VHD', { path: vhd })
443
444
  unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
@@ -86,7 +86,12 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
86
86
  throw new Error(`Job mode ${job.mode} not implemented for mirror backup`)
87
87
  }
88
88
 
89
- return runTask(taskStart, () => vmBackup.run())
89
+ return sourceRemoteAdapter
90
+ .listVmBackups(vmUuid, ({ mode }) => mode === job.mode)
91
+ .then(vmBackups => {
92
+ // avoiding to create tasks for empty directories
93
+ if (vmBackups.length > 0) return runTask(taskStart, () => vmBackup.run())
94
+ })
90
95
  }
91
96
  const { concurrency } = settings
92
97
  await asyncMapSettled(vmsUuids, !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm))
@@ -2,6 +2,7 @@ import { asyncEach } from '@vates/async-each'
2
2
  import { decorateMethodsWith } from '@vates/decorate-with'
3
3
  import { defer } from 'golike-defer'
4
4
  import assert from 'node:assert'
5
+ import * as UUID from 'uuid'
5
6
  import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
6
7
  import mapValues from 'lodash/mapValues.js'
7
8
 
@@ -9,11 +10,48 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
9
10
  import { forkDeltaExport } from './_forkDeltaExport.mjs'
10
11
  import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
11
12
  import { Task } from '../../Task.mjs'
13
+ import { Disposable } from 'promise-toolbox'
14
+ import { openVhd } from 'vhd-lib'
15
+ import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
12
16
 
13
17
  class IncrementalRemoteVmBackupRunner extends AbstractRemote {
14
18
  _getRemoteWriter() {
15
19
  return IncrementalRemoteWriter
16
20
  }
21
+ async _selectBaseVm(metadata) {
22
+ // for each disk , get the parent
23
+ const baseUuidToSrcVdi = new Map()
24
+
25
+ // no previous backup for a base( =key) backup
26
+ if (metadata.isBase) {
27
+ return
28
+ }
29
+ await asyncEach(Object.entries(metadata.vdis), async ([id, vdi]) => {
30
+ const isDifferencing = metadata.isVhdDifferencing[`${id}.vhd`]
31
+ if (isDifferencing) {
32
+ const vmDir = getVmBackupDir(metadata.vm.uuid)
33
+ const path = `${vmDir}/${metadata.vhds[id]}`
34
+ // don't catch error : we can't recover if the source vhd are missing
35
+ await Disposable.use(openVhd(this._sourceRemoteAdapter._handler, path), vhd => {
36
+ baseUuidToSrcVdi.set(UUID.stringify(vhd.header.parentUuid), vdi.$snapshot_of$uuid)
37
+ })
38
+ }
39
+ })
40
+
41
+ const presentBaseVdis = new Map(baseUuidToSrcVdi)
42
+ await this._callWriters(
43
+ writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
44
+ 'writer.checkBaseVdis()',
45
+ false
46
+ )
47
+ // check if the parent vdi are present in all the remotes
48
+ baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
49
+ if (!presentBaseVdis.has(baseUuid)) {
50
+ throw new Error(`Missing vdi ${baseUuid} which is a base for a delta`)
51
+ }
52
+ })
53
+ // yeah , let's go
54
+ }
17
55
  async _run($defer) {
18
56
  const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
19
57
  await this._callWriters(async writer => {
@@ -26,7 +64,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
26
64
  if (transferList.length > 0) {
27
65
  for (const metadata of transferList) {
28
66
  assert.strictEqual(metadata.mode, 'delta')
29
-
67
+ await this._selectBaseVm(metadata)
30
68
  await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
31
69
  const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
32
70
  useChain: false,
@@ -50,6 +88,17 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
50
88
  }),
51
89
  'writer.transfer()'
52
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
+ )
101
+
53
102
  await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
54
103
  // for healthcheck
55
104
  this._tags = metadata.vm.tags
@@ -78,6 +78,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
78
78
  'writer.transfer()'
79
79
  )
80
80
 
81
+ // we want to control the uuid of the vhd in the chain
82
+ // and ensure they are correctly chained
83
+ await this._callWriters(
84
+ writer =>
85
+ writer.updateUuidAndChain({
86
+ isVhdDifferencing,
87
+ timestamp,
88
+ vdis: deltaExport.vdis,
89
+ }),
90
+ 'writer.updateUuidAndChain()'
91
+ )
92
+
81
93
  this._baseVm = exportedVm
82
94
 
83
95
  if (baseVm !== undefined) {
@@ -133,7 +145,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
133
145
  ])
134
146
  const srcVdi = srcVdis[snapshotOf]
135
147
  if (srcVdi !== undefined) {
136
- baseUuidToSrcVdi.set(baseUuid, srcVdi)
148
+ baseUuidToSrcVdi.set(baseUuid, srcVdi.uuid)
137
149
  } else {
138
150
  debug('ignore snapshot VDI because no longer present on VM', {
139
151
  vdi: baseUuid,
@@ -154,18 +166,18 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
154
166
  }
155
167
 
156
168
  const fullVdisRequired = new Set()
157
- baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
169
+ baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
158
170
  if (presentBaseVdis.has(baseUuid)) {
159
171
  debug('found base VDI', {
160
172
  base: baseUuid,
161
- vdi: srcVdi.uuid,
173
+ vdi: srcVdiUuid,
162
174
  })
163
175
  } else {
164
176
  debug('missing base VDI', {
165
177
  base: baseUuid,
166
- vdi: srcVdi.uuid,
178
+ vdi: srcVdiUuid,
167
179
  })
168
- fullVdisRequired.add(srcVdi.uuid)
180
+ fullVdisRequired.add(srcVdiUuid)
169
181
  }
170
182
  })
171
183
 
@@ -1,17 +1,15 @@
1
1
  import assert from 'node:assert'
2
2
  import mapValues from 'lodash/mapValues.js'
3
- import ignoreErrors from 'promise-toolbox/ignoreErrors'
4
3
  import { asyncEach } from '@vates/async-each'
5
4
  import { asyncMap } from '@xen-orchestra/async-map'
6
- import { chainVhd, checkVhdChain, openVhd, VhdAbstract } from 'vhd-lib'
5
+ import { chainVhd, openVhd } from 'vhd-lib'
7
6
  import { createLogger } from '@xen-orchestra/log'
8
7
  import { decorateClass } from '@vates/decorate-with'
9
8
  import { defer } from 'golike-defer'
10
- import { dirname } from 'node:path'
9
+ import { dirname, basename } from 'node:path'
11
10
 
12
11
  import { formatFilenameDate } from '../../_filenameDate.mjs'
13
12
  import { getOldEntries } from '../../_getOldEntries.mjs'
14
- import { TAG_BASE_DELTA } from '../../_incrementalVm.mjs'
15
13
  import { Task } from '../../Task.mjs'
16
14
 
17
15
  import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
@@ -23,42 +21,45 @@ import { Disposable } from 'promise-toolbox'
23
21
  const { warn } = createLogger('xo:backups:DeltaBackupWriter')
24
22
 
25
23
  export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
24
+ #parentVdiPaths
25
+ #vhds
26
26
  async checkBaseVdis(baseUuidToSrcVdi) {
27
+ this.#parentVdiPaths = {}
27
28
  const { handler } = this._adapter
28
29
  const adapter = this._adapter
29
30
 
30
31
  const vdisDir = `${this._vmBackupDir}/vdis/${this._job.id}`
31
32
 
32
- await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
33
- let found = false
33
+ await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdiUuid]) => {
34
+ let parentDestPath
35
+ const vhdDir = `${vdisDir}/${srcVdiUuid}`
34
36
  try {
35
- const vhds = await handler.list(`${vdisDir}/${srcVdi.uuid}`, {
37
+ const vhds = await handler.list(vhdDir, {
36
38
  filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
37
39
  ignoreMissing: true,
38
40
  prependDir: true,
39
41
  })
40
42
  const packedBaseUuid = packUuid(baseUuid)
41
- await asyncMap(vhds, async path => {
43
+ // the last one is probably the right one
44
+
45
+ for (let i = vhds.length - 1; i >= 0 && parentDestPath === undefined; i--) {
46
+ const path = vhds[i]
42
47
  try {
43
- await checkVhdChain(handler, path)
44
- // Warning, this should not be written as found = found || await adapter.isMergeableParent(packedBaseUuid, path)
45
- //
46
- // since all the checks of a path are done in parallel, found would be containing
47
- // only the last answer of isMergeableParent which is probably not the right one
48
- // this led to the support tickets https://help.vates.fr/#ticket/zoom/4751 , 4729, 4665 and 4300
49
-
50
- const isMergeable = await adapter.isMergeableParent(packedBaseUuid, path)
51
- found = found || isMergeable
48
+ if (await adapter.isMergeableParent(packedBaseUuid, path)) {
49
+ parentDestPath = path
50
+ }
52
51
  } catch (error) {
53
52
  warn('checkBaseVdis', { error })
54
- await ignoreErrors.call(VhdAbstract.unlink(handler, path))
55
53
  }
56
- })
54
+ }
57
55
  } catch (error) {
58
56
  warn('checkBaseVdis', { error })
59
57
  }
60
- if (!found) {
58
+ // no usable parent => the runner will have to decide to fall back to a full or stop backup
59
+ if (parentDestPath === undefined) {
61
60
  baseUuidToSrcVdi.delete(baseUuid)
61
+ } else {
62
+ this.#parentVdiPaths[vhdDir] = parentDestPath
62
63
  }
63
64
  })
64
65
  }
@@ -123,6 +124,44 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
123
124
  }
124
125
  }
125
126
 
127
+ async updateUuidAndChain({ isVhdDifferencing, vdis }) {
128
+ assert.notStrictEqual(
129
+ this.#vhds,
130
+ undefined,
131
+ '_transfer must be called before updateUuidAndChain for incremental backups'
132
+ )
133
+
134
+ const parentVdiPaths = this.#parentVdiPaths
135
+ const { handler } = this._adapter
136
+ const vhds = this.#vhds
137
+ await asyncEach(Object.entries(vdis), async ([id, vdi]) => {
138
+ const isDifferencing = isVhdDifferencing[`${id}.vhd`]
139
+ const path = `${this._vmBackupDir}/${vhds[id]}`
140
+ if (isDifferencing) {
141
+ assert.notStrictEqual(
142
+ parentVdiPaths,
143
+ 'checkbasevdi must be called before updateUuidAndChain for incremental backups'
144
+ )
145
+ const parentPath = parentVdiPaths[dirname(path)]
146
+ // we are in a incremental backup
147
+ // we already computed the chain in checkBaseVdis
148
+ assert.notStrictEqual(parentPath, undefined, 'A differential VHD must have a parent')
149
+ // forbid any kind of loop
150
+ assert.ok(basename(parentPath) < basename(path), `vhd must be sorted to be chained`)
151
+ await chainVhd(handler, parentPath, handler, path)
152
+ }
153
+
154
+ // set the correct UUID in the VHD if needed
155
+ await Disposable.use(openVhd(handler, path), async vhd => {
156
+ if (!vhd.footer.uuid.equals(packUuid(vdi.uuid))) {
157
+ vhd.footer.uuid = packUuid(vdi.uuid)
158
+ await vhd.readBlockAllocationTable() // required by writeFooter()
159
+ await vhd.writeFooter()
160
+ }
161
+ })
162
+ })
163
+ }
164
+
126
165
  async _deleteOldEntries() {
127
166
  const adapter = this._adapter
128
167
  const oldEntries = this._oldEntries
@@ -141,14 +180,10 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
141
180
  const jobId = job.id
142
181
  const handler = adapter.handler
143
182
 
144
- let metadataContent = await this._isAlreadyTransferred(timestamp)
145
- if (metadataContent !== undefined) {
146
- // @todo : should skip backup while being vigilant to not stuck the forked stream
147
- Task.info('This backup has already been transfered')
148
- }
149
-
150
183
  const basename = formatFilenameDate(timestamp)
151
- const vhds = mapValues(
184
+ // update this.#vhds before eventually skipping transfer, so that
185
+ // updateUuidAndChain has all the mandatory data
186
+ const vhds = (this.#vhds = mapValues(
152
187
  deltaExport.vdis,
153
188
  vdi =>
154
189
  `vdis/${jobId}/${
@@ -158,7 +193,15 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
158
193
  vdi.uuid
159
194
  : vdi.$snapshot_of$uuid
160
195
  }/${adapter.getVhdFileName(basename)}`
161
- )
196
+ ))
197
+
198
+ let metadataContent = await this._isAlreadyTransferred(timestamp)
199
+ if (metadataContent !== undefined) {
200
+ // skip backup while being vigilant to not stuck the forked stream
201
+ Task.info('This backup has already been transfered')
202
+ Object.values(deltaExport.streams).forEach(stream => stream.destroy())
203
+ return { size: 0 }
204
+ }
162
205
 
163
206
  metadataContent = {
164
207
  isVhdDifferencing,
@@ -174,38 +217,13 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
174
217
  vm,
175
218
  vmSnapshot,
176
219
  }
220
+
177
221
  const { size } = await Task.run({ name: 'transfer' }, async () => {
178
222
  let transferSize = 0
179
223
  await asyncEach(
180
- Object.entries(deltaExport.vdis),
181
- async ([id, vdi]) => {
224
+ Object.keys(deltaExport.vdis),
225
+ async id => {
182
226
  const path = `${this._vmBackupDir}/${vhds[id]}`
183
-
184
- const isDifferencing = isVhdDifferencing[`${id}.vhd`]
185
- let parentPath
186
- if (isDifferencing) {
187
- const vdiDir = dirname(path)
188
- parentPath = (
189
- await handler.list(vdiDir, {
190
- filter: filename => filename[0] !== '.' && filename.endsWith('.vhd'),
191
- prependDir: true,
192
- })
193
- )
194
- .sort()
195
- .pop()
196
-
197
- assert.notStrictEqual(
198
- parentPath,
199
- undefined,
200
- `missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config[TAG_BASE_DELTA]}`
201
- )
202
-
203
- parentPath = parentPath.slice(1) // remove leading slash
204
-
205
- // TODO remove when this has been done before the export
206
- await checkVhd(handler, parentPath)
207
- }
208
-
209
227
  // don't write it as transferSize += await async function
210
228
  // since i += await asyncFun lead to race condition
211
229
  // as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
@@ -217,17 +235,6 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
217
235
  writeBlockConcurrency: this._config.writeBlockConcurrency,
218
236
  })
219
237
  transferSize += transferSizeOneDisk
220
-
221
- if (isDifferencing) {
222
- await chainVhd(handler, parentPath, handler, path)
223
- }
224
-
225
- // set the correct UUID in the VHD
226
- await Disposable.use(openVhd(handler, path), async vhd => {
227
- vhd.footer.uuid = packUuid(vdi.uuid)
228
- await vhd.readBlockAllocationTable() // required by writeFooter()
229
- await vhd.writeFooter()
230
- })
231
238
  },
232
239
  {
233
240
  concurrency: settings.diskPerVmConcurrency,
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert'
1
2
  import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
2
3
  import ignoreErrors from 'promise-toolbox/ignoreErrors'
3
4
  import { formatDateTime } from '@xen-orchestra/xapi'
@@ -14,6 +15,7 @@ import find from 'lodash/find.js'
14
15
 
15
16
  export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
16
17
  async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
18
+ assert.notStrictEqual(baseVm, undefined)
17
19
  const sr = this._sr
18
20
  const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(
19
21
  vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
@@ -36,7 +38,9 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
36
38
  }
37
39
  }
38
40
  }
39
-
41
+ updateUuidAndChain() {
42
+ // nothing to do, the chaining is not modified in this case
43
+ }
40
44
  prepare({ isFull }) {
41
45
  // create the task related to this export and ensure all methods are called in this context
42
46
  const task = new Task({
@@ -5,6 +5,10 @@ export class AbstractIncrementalWriter extends AbstractWriter {
5
5
  throw new Error('Not implemented')
6
6
  }
7
7
 
8
+ updateUuidAndChain() {
9
+ throw new Error('Not implemented')
10
+ }
11
+
8
12
  cleanup() {
9
13
  throw new Error('Not implemented')
10
14
  }
@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
113
113
  )
114
114
  }
115
115
 
116
- _isAlreadyTransferred(timestamp) {
116
+ async _isAlreadyTransferred(timestamp) {
117
117
  const vmUuid = this._vmUuid
118
118
  const adapter = this._adapter
119
119
  const backupDir = getVmBackupDir(vmUuid)
120
120
  try {
121
121
  const actualMetadata = JSON.parse(
122
- adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
122
+ await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
123
123
  )
124
124
  return actualMetadata
125
125
  } catch (error) {}
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.44.6",
11
+ "version": "0.44.7",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -28,7 +28,7 @@
28
28
  "@vates/nbd-client": "^3.0.0",
29
29
  "@vates/parse-duration": "^0.1.1",
30
30
  "@xen-orchestra/async-map": "^0.1.2",
31
- "@xen-orchestra/fs": "^4.1.4",
31
+ "@xen-orchestra/fs": "^4.1.5",
32
32
  "@xen-orchestra/log": "^0.6.0",
33
33
  "@xen-orchestra/template": "^0.1.0",
34
34
  "app-conf": "^2.3.0",
@@ -44,7 +44,7 @@
44
44
  "proper-lockfile": "^4.1.2",
45
45
  "tar": "^6.1.15",
46
46
  "uuid": "^9.0.0",
47
- "vhd-lib": "^4.9.0",
47
+ "vhd-lib": "^4.9.1",
48
48
  "xen-api": "^2.0.1",
49
49
  "yazl": "^2.5.1"
50
50
  },
@@ -56,7 +56,7 @@
56
56
  "tmp": "^0.2.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "@xen-orchestra/xapi": "^4.2.0"
59
+ "@xen-orchestra/xapi": "^4.2.1"
60
60
  },
61
61
  "license": "AGPL-3.0-or-later",
62
62
  "author": {