@xen-orchestra/backups 0.14.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.
Files changed (43) hide show
  1. package/Backup.js +263 -0
  2. package/DurablePartition.js +40 -0
  3. package/ImportVmBackup.js +66 -0
  4. package/README.md +28 -0
  5. package/RemoteAdapter.js +552 -0
  6. package/RestoreMetadataBackup.js +24 -0
  7. package/Task.js +151 -0
  8. package/_PoolMetadataBackup.js +75 -0
  9. package/_VmBackup.js +409 -0
  10. package/_XoMetadataBackup.js +62 -0
  11. package/_backupType.js +4 -0
  12. package/_backupWorker.js +155 -0
  13. package/_cancelableMap.js +20 -0
  14. package/_cleanVm.js +378 -0
  15. package/_deltaVm.js +347 -0
  16. package/_extractIdsFromSimplePattern.js +29 -0
  17. package/_filenameDate.js +6 -0
  18. package/_forkStreamUnpipe.js +28 -0
  19. package/_getOldEntries.js +4 -0
  20. package/_getTmpDir.js +20 -0
  21. package/_getVmBackupDir.js +6 -0
  22. package/_isValidXva.js +60 -0
  23. package/_listPartitions.js +52 -0
  24. package/_lvm.js +31 -0
  25. package/_watchStreamSize.js +7 -0
  26. package/formatVmBackups.js +34 -0
  27. package/merge-worker/cli.js +69 -0
  28. package/merge-worker/index.js +25 -0
  29. package/package.json +49 -0
  30. package/parseMetadataBackupId.js +23 -0
  31. package/runBackupWorker.js +38 -0
  32. package/writers/DeltaBackupWriter.js +221 -0
  33. package/writers/DeltaReplicationWriter.js +126 -0
  34. package/writers/FullBackupWriter.js +85 -0
  35. package/writers/FullReplicationWriter.js +88 -0
  36. package/writers/_AbstractDeltaWriter.js +26 -0
  37. package/writers/_AbstractFullWriter.js +12 -0
  38. package/writers/_AbstractWriter.js +10 -0
  39. package/writers/_MixinBackupWriter.js +51 -0
  40. package/writers/_MixinReplicationWriter.js +8 -0
  41. package/writers/_checkVhd.js +5 -0
  42. package/writers/_listReplicatedVms.js +30 -0
  43. package/writers/_packUuid.js +5 -0
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { catchGlobalErrors } = require('@xen-orchestra/log/configure.js')
4
+ const { createLogger } = require('@xen-orchestra/log')
5
+ const { getSyncedHandler } = require('@xen-orchestra/fs')
6
+ const { join } = require('path')
7
+ const Disposable = require('promise-toolbox/Disposable')
8
+ const min = require('lodash/min')
9
+
10
+ const { getVmBackupDir } = require('../_getVmBackupDir.js')
11
+ const { RemoteAdapter } = require('../RemoteAdapter.js')
12
+
13
+ const { CLEAN_VM_QUEUE } = require('./index.js')
14
+
15
+ // -------------------------------------------------------------------
16
+
17
+ catchGlobalErrors(createLogger('xo:backups:mergeWorker'))
18
+
19
+ const { fatal, info, warn } = createLogger('xo:backups:mergeWorker')
20
+
21
+ // -------------------------------------------------------------------
22
+
23
+ const main = Disposable.wrap(async function* main(args) {
24
+ const handler = yield getSyncedHandler({ url: 'file://' + process.cwd() })
25
+
26
+ yield handler.lock(CLEAN_VM_QUEUE)
27
+
28
+ const adapter = new RemoteAdapter(handler)
29
+
30
+ const listRetry = async () => {
31
+ const timeoutResolver = resolve => setTimeout(resolve, 10e3)
32
+ for (let i = 0; i < 10; ++i) {
33
+ const entries = await handler.list(CLEAN_VM_QUEUE)
34
+ if (entries.length !== 0) {
35
+ return entries
36
+ }
37
+ await new Promise(timeoutResolver)
38
+ }
39
+ }
40
+
41
+ let taskFiles
42
+ while ((taskFiles = await listRetry()) !== undefined) {
43
+ const taskFileBasename = min(taskFiles)
44
+ const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
45
+
46
+ // move this task to the end
47
+ await handler.rename(join(CLEAN_VM_QUEUE, taskFileBasename), taskFile)
48
+ try {
49
+ const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
50
+ await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
51
+
52
+ handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
53
+ } catch (error) {
54
+ warn('failure handling task', { error })
55
+ }
56
+ }
57
+ })
58
+
59
+ info('starting')
60
+ main(process.argv.slice(2)).then(
61
+ () => {
62
+ info('bye :-)')
63
+ },
64
+ error => {
65
+ fatal(error)
66
+
67
+ process.exit(1)
68
+ }
69
+ )
@@ -0,0 +1,25 @@
1
+ const { join, resolve } = require('path')
2
+ const { spawn } = require('child_process')
3
+ const { check } = require('proper-lockfile')
4
+
5
+ const CLEAN_VM_QUEUE = (exports.CLEAN_VM_QUEUE = '/xo-vm-backups/.queue/clean-vm/')
6
+
7
+ const CLI_PATH = resolve(__dirname, 'cli.js')
8
+ exports.run = async function runMergeWorker(remotePath) {
9
+ try {
10
+ // TODO: find a way to pass the acquire the lock and then pass it down the worker
11
+ if (await check(join(remotePath, CLEAN_VM_QUEUE))) {
12
+ // already locked, don't start another worker
13
+ return
14
+ }
15
+
16
+ spawn(CLI_PATH, {
17
+ cwd: remotePath,
18
+ detached: true,
19
+ stdio: 'inherit',
20
+ }).unref()
21
+ } catch (error) {
22
+ // we usually don't want to throw if the merge worker failed to start
23
+ return error
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "private": false,
3
+ "name": "@xen-orchestra/backups",
4
+ "homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups",
5
+ "bugs": "https://github.com/vatesfr/xen-orchestra/issues",
6
+ "repository": {
7
+ "directory": "@xen-orchestra/backups",
8
+ "type": "git",
9
+ "url": "https://github.com/vatesfr/xen-orchestra.git"
10
+ },
11
+ "version": "0.14.0",
12
+ "engines": {
13
+ "node": ">=14.6"
14
+ },
15
+ "scripts": {
16
+ "postversion": "npm publish --access public"
17
+ },
18
+ "dependencies": {
19
+ "@vates/compose": "^2.0.0",
20
+ "@vates/disposable": "^0.1.1",
21
+ "@vates/parse-duration": "^0.1.1",
22
+ "@xen-orchestra/async-map": "^0.1.2",
23
+ "@xen-orchestra/fs": "^0.18.0",
24
+ "@xen-orchestra/log": "^0.3.0",
25
+ "@xen-orchestra/template": "^0.1.0",
26
+ "compare-versions": "^3.6.0",
27
+ "d3-time-format": "^3.0.0",
28
+ "end-of-stream": "^1.4.4",
29
+ "fs-extra": "^10.0.0",
30
+ "golike-defer": "^0.5.1",
31
+ "limit-concurrency-decorator": "^0.5.0",
32
+ "lodash": "^4.17.20",
33
+ "node-zone": "^0.4.0",
34
+ "parse-pairs": "^1.1.0",
35
+ "promise-toolbox": "^0.20.0",
36
+ "proper-lockfile": "^4.1.2",
37
+ "pump": "^3.0.0",
38
+ "vhd-lib": "^1.2.0",
39
+ "yazl": "^2.5.1"
40
+ },
41
+ "peerDependencies": {
42
+ "@xen-orchestra/xapi": "^0.7.0"
43
+ },
44
+ "license": "AGPL-3.0-or-later",
45
+ "author": {
46
+ "name": "Vates SAS",
47
+ "url": "https://vates.fr"
48
+ }
49
+ }
@@ -0,0 +1,23 @@
1
+ const { DIR_XO_CONFIG_BACKUPS, DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
2
+
3
+ exports.parseMetadataBackupId = function parseMetadataBackupId(backupId) {
4
+ const [dir, ...rest] = backupId.split('/')
5
+ if (dir === DIR_XO_CONFIG_BACKUPS) {
6
+ const [scheduleId, timestamp] = rest
7
+ return {
8
+ type: 'xoConfig',
9
+ scheduleId,
10
+ timestamp,
11
+ }
12
+ } else if (dir === DIR_XO_POOL_METADATA_BACKUPS) {
13
+ const [scheduleId, poolUuid, timestamp] = rest
14
+ return {
15
+ type: 'pool',
16
+ poolUuid,
17
+ scheduleId,
18
+ timestamp,
19
+ }
20
+ }
21
+
22
+ throw new Error(`not supported backup dir (${dir})`)
23
+ }
@@ -0,0 +1,38 @@
1
+ const path = require('path')
2
+ const { createLogger } = require('@xen-orchestra/log')
3
+ const { fork } = require('child_process')
4
+
5
+ const { warn } = createLogger('xo:backups:backupWorker')
6
+
7
+ const PATH = path.resolve(__dirname, '_backupWorker.js')
8
+
9
+ exports.runBackupWorker = function runBackupWorker(params, onLog) {
10
+ return new Promise((resolve, reject) => {
11
+ const worker = fork(PATH)
12
+
13
+ worker.on('exit', code => reject(new Error(`worker exited with code ${code}`)))
14
+ worker.on('error', reject)
15
+
16
+ worker.on('message', message => {
17
+ try {
18
+ if (message.type === 'result') {
19
+ if (message.status === 'success') {
20
+ resolve(message.result)
21
+ } else {
22
+ reject(message.result)
23
+ }
24
+ } else if (message.type === 'log') {
25
+ onLog(message.data)
26
+ }
27
+ } catch (error) {
28
+ warn(error)
29
+ }
30
+ })
31
+
32
+ worker.send({
33
+ action: 'run',
34
+ data: params,
35
+ runWithLogs: onLog !== undefined,
36
+ })
37
+ })
38
+ }
@@ -0,0 +1,221 @@
1
+ const assert = require('assert')
2
+ const map = require('lodash/map.js')
3
+ const mapValues = require('lodash/mapValues.js')
4
+ const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
5
+ const { asyncMap } = require('@xen-orchestra/async-map')
6
+ const { chainVhd, checkVhdChain, default: Vhd } = require('vhd-lib')
7
+ const { createLogger } = require('@xen-orchestra/log')
8
+ const { dirname } = require('path')
9
+
10
+ const { formatFilenameDate } = require('../_filenameDate.js')
11
+ const { getOldEntries } = require('../_getOldEntries.js')
12
+ const { getVmBackupDir } = require('../_getVmBackupDir.js')
13
+ const { Task } = require('../Task.js')
14
+
15
+ const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
16
+ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
17
+ const { checkVhd } = require('./_checkVhd.js')
18
+ const { packUuid } = require('./_packUuid.js')
19
+
20
+ const { warn } = createLogger('xo:backups:DeltaBackupWriter')
21
+
22
+ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
23
+ async checkBaseVdis(baseUuidToSrcVdi) {
24
+ const { handler } = this._adapter
25
+ const backup = this._backup
26
+
27
+ const backupDir = getVmBackupDir(backup.vm.uuid)
28
+ const vdisDir = `${backupDir}/vdis/${backup.job.id}`
29
+
30
+ await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
31
+ let found = false
32
+ try {
33
+ const vhds = await handler.list(`${vdisDir}/${srcVdi.uuid}`, {
34
+ filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
35
+ prependDir: true,
36
+ })
37
+ await asyncMap(vhds, async path => {
38
+ try {
39
+ await checkVhdChain(handler, path)
40
+
41
+ const vhd = new Vhd(handler, path)
42
+ await vhd.readHeaderAndFooter()
43
+ found = found || vhd.footer.uuid.equals(packUuid(baseUuid))
44
+ } catch (error) {
45
+ warn('checkBaseVdis', { error })
46
+ await ignoreErrors.call(handler.unlink(path))
47
+ }
48
+ })
49
+ } catch (error) {
50
+ warn('checkBaseVdis', { error })
51
+ }
52
+ if (!found) {
53
+ baseUuidToSrcVdi.delete(baseUuid)
54
+ }
55
+ })
56
+ }
57
+
58
+ async beforeBackup() {
59
+ await super.beforeBackup()
60
+ return this._cleanVm({ merge: true })
61
+ }
62
+
63
+ prepare({ isFull }) {
64
+ // create the task related to this export and ensure all methods are called in this context
65
+ const task = new Task({
66
+ name: 'export',
67
+ data: {
68
+ id: this._remoteId,
69
+ isFull,
70
+ type: 'remote',
71
+ },
72
+ })
73
+ this.transfer = task.wrapFn(this.transfer)
74
+ this.cleanup = task.wrapFn(this.cleanup, true)
75
+
76
+ return task.run(() => this._prepare())
77
+ }
78
+
79
+ async _prepare() {
80
+ const adapter = this._adapter
81
+ const settings = this._settings
82
+ const { scheduleId, vm } = this._backup
83
+
84
+ const oldEntries = getOldEntries(
85
+ settings.exportRetention - 1,
86
+ await adapter.listVmBackups(vm.uuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
87
+ )
88
+ this._oldEntries = oldEntries
89
+
90
+ // FIXME: implement optimized multiple VHDs merging with synthetic
91
+ // delta
92
+ //
93
+ // For the time being, limit the number of deleted backups by run
94
+ // because it can take a very long time and can lead to
95
+ // interrupted backup with broken VHD chain.
96
+ //
97
+ // The old backups will be eventually merged in future runs of the
98
+ // job.
99
+ const { maxMergedDeltasPerRun } = this._settings
100
+ if (oldEntries.length > maxMergedDeltasPerRun) {
101
+ oldEntries.length = maxMergedDeltasPerRun
102
+ }
103
+
104
+ if (settings.deleteFirst) {
105
+ await this._deleteOldEntries()
106
+ }
107
+ }
108
+
109
+ async cleanup() {
110
+ if (!this._settings.deleteFirst) {
111
+ await this._deleteOldEntries()
112
+ }
113
+ }
114
+
115
+ async _deleteOldEntries() {
116
+ const adapter = this._adapter
117
+ const oldEntries = this._oldEntries
118
+
119
+ // delete sequentially from newest to oldest to avoid unnecessary merges
120
+ for (let i = oldEntries.length; i-- > 0; ) {
121
+ await adapter.deleteDeltaVmBackups([oldEntries[i]])
122
+ }
123
+ }
124
+
125
+ async _transfer({ timestamp, deltaExport, sizeContainers }) {
126
+ const adapter = this._adapter
127
+ const backup = this._backup
128
+
129
+ const { job, scheduleId, vm } = backup
130
+
131
+ const jobId = job.id
132
+ const handler = adapter.handler
133
+ const backupDir = getVmBackupDir(vm.uuid)
134
+
135
+ // TODO: clean VM backup directory
136
+
137
+ const basename = formatFilenameDate(timestamp)
138
+ const vhds = mapValues(
139
+ deltaExport.vdis,
140
+ vdi =>
141
+ `vdis/${jobId}/${
142
+ vdi.type === 'suspend'
143
+ ? // doesn't make sense to group by parent for memory because we
144
+ // don't do delta for it
145
+ vdi.uuid
146
+ : vdi.$snapshot_of$uuid
147
+ }/${basename}.vhd`
148
+ )
149
+
150
+ const metadataFilename = `${backupDir}/${basename}.json`
151
+ const metadataContent = {
152
+ jobId,
153
+ mode: job.mode,
154
+ scheduleId,
155
+ timestamp,
156
+ vbds: deltaExport.vbds,
157
+ vdis: deltaExport.vdis,
158
+ version: '2.0.0',
159
+ vifs: deltaExport.vifs,
160
+ vhds,
161
+ vm,
162
+ vmSnapshot: this._backup.exportedVm,
163
+ }
164
+
165
+ const { size } = await Task.run({ name: 'transfer' }, async () => {
166
+ await Promise.all(
167
+ map(deltaExport.vdis, async (vdi, id) => {
168
+ const path = `${backupDir}/${vhds[id]}`
169
+
170
+ const isDelta = vdi.other_config['xo:base_delta'] !== undefined
171
+ let parentPath
172
+ if (isDelta) {
173
+ const vdiDir = dirname(path)
174
+ parentPath = (
175
+ await handler.list(vdiDir, {
176
+ filter: filename => filename[0] !== '.' && filename.endsWith('.vhd'),
177
+ prependDir: true,
178
+ })
179
+ )
180
+ .sort()
181
+ .pop()
182
+
183
+ assert.notStrictEqual(parentPath, undefined, `missing parent of ${id}`)
184
+
185
+ parentPath = parentPath.slice(1) // remove leading slash
186
+
187
+ // TODO remove when this has been done before the export
188
+ await checkVhd(handler, parentPath)
189
+ }
190
+
191
+ await adapter.outputStream(path, deltaExport.streams[`${id}.vhd`], {
192
+ // no checksum for VHDs, because they will be invalidated by
193
+ // merges and chainings
194
+ checksum: false,
195
+ validator: tmpPath => checkVhd(handler, tmpPath),
196
+ })
197
+
198
+ if (isDelta) {
199
+ await chainVhd(handler, parentPath, handler, path)
200
+ }
201
+
202
+ // set the correct UUID in the VHD
203
+ const vhd = new Vhd(handler, path)
204
+ await vhd.readHeaderAndFooter()
205
+ vhd.footer.uuid = packUuid(vdi.uuid)
206
+ await vhd.readBlockAllocationTable() // required by writeFooter()
207
+ await vhd.writeFooter()
208
+ })
209
+ )
210
+ return {
211
+ size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
212
+ }
213
+ })
214
+ metadataContent.size = size
215
+ await handler.outputFile(metadataFilename, JSON.stringify(metadataContent), {
216
+ dirMode: backup.config.dirMode,
217
+ })
218
+
219
+ // TODO: run cleanup?
220
+ }
221
+ }
@@ -0,0 +1,126 @@
1
+ const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
2
+ const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
3
+ const { formatDateTime } = require('@xen-orchestra/xapi')
4
+
5
+ const { formatFilenameDate } = require('../_filenameDate.js')
6
+ const { getOldEntries } = require('../_getOldEntries.js')
7
+ const { importDeltaVm, TAG_COPY_SRC } = require('../_deltaVm.js')
8
+ const { Task } = require('../Task.js')
9
+
10
+ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
11
+ const { MixinReplicationWriter } = require('./_MixinReplicationWriter.js')
12
+ const { listReplicatedVms } = require('./_listReplicatedVms.js')
13
+
14
+ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinReplicationWriter(AbstractDeltaWriter) {
15
+ async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
16
+ const sr = this._sr
17
+ const replicatedVm = listReplicatedVms(sr.$xapi, this._backup.job.id, sr.uuid, this._backup.vm.uuid).find(
18
+ vm => vm.other_config[TAG_COPY_SRC] === baseVm.uuid
19
+ )
20
+ if (replicatedVm === undefined) {
21
+ return baseUuidToSrcVdi.clear()
22
+ }
23
+
24
+ const xapi = replicatedVm.$xapi
25
+ const replicatedVdis = new Set(
26
+ await asyncMap(await replicatedVm.$getDisks(), async vdiRef => {
27
+ const otherConfig = await xapi.getField('VDI', vdiRef, 'other_config')
28
+ return otherConfig[TAG_COPY_SRC]
29
+ })
30
+ )
31
+
32
+ for (const uuid of baseUuidToSrcVdi.keys()) {
33
+ if (!replicatedVdis.has(uuid)) {
34
+ baseUuidToSrcVdi.delete(uuid)
35
+ }
36
+ }
37
+ }
38
+
39
+ prepare({ isFull }) {
40
+ // create the task related to this export and ensure all methods are called in this context
41
+ const task = new Task({
42
+ name: 'export',
43
+ data: {
44
+ id: this._sr.uuid,
45
+ isFull,
46
+ type: 'SR',
47
+ },
48
+ })
49
+ this.transfer = task.wrapFn(this.transfer)
50
+ this.cleanup = task.wrapFn(this.cleanup, true)
51
+
52
+ return task.run(() => this._prepare())
53
+ }
54
+
55
+ async _prepare() {
56
+ const settings = this._settings
57
+ const { uuid: srUuid, $xapi: xapi } = this._sr
58
+ const { scheduleId, vm } = this._backup
59
+
60
+ // delete previous interrupted copies
61
+ ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vm.uuid), vm => vm.$destroy))
62
+
63
+ this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
64
+
65
+ if (settings.deleteFirst) {
66
+ await this._deleteOldEntries()
67
+ }
68
+ }
69
+
70
+ async cleanup() {
71
+ if (!this._settings.deleteFirst) {
72
+ await this._deleteOldEntries()
73
+ }
74
+ }
75
+
76
+ async _deleteOldEntries() {
77
+ return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
78
+ }
79
+
80
+ async _transfer({ timestamp, deltaExport, sizeContainers }) {
81
+ const sr = this._sr
82
+ const { job, scheduleId, vm } = this._backup
83
+
84
+ const { uuid: srUuid, $xapi: xapi } = sr
85
+
86
+ let targetVmRef
87
+ await Task.run({ name: 'transfer' }, async () => {
88
+ targetVmRef = await importDeltaVm(
89
+ {
90
+ __proto__: deltaExport,
91
+ vm: {
92
+ ...deltaExport.vm,
93
+ tags: [...deltaExport.vm.tags, 'Continuous Replication'],
94
+ },
95
+ },
96
+ sr
97
+ )
98
+ return {
99
+ size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
100
+ }
101
+ })
102
+
103
+ const targetVm = await xapi.getRecord('VM', targetVmRef)
104
+
105
+ await Promise.all([
106
+ targetVm.ha_restart_priority !== '' &&
107
+ Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
108
+ targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
109
+ asyncMap(['start', 'start_on'], op =>
110
+ targetVm.update_blocked_operations(
111
+ op,
112
+ 'Start operation for this vm is blocked, clone it if you want to use it.'
113
+ )
114
+ ),
115
+ targetVm.update_other_config({
116
+ 'xo:backup:sr': srUuid,
117
+
118
+ // these entries need to be added in case of offline backup
119
+ 'xo:backup:datetime': formatDateTime(timestamp),
120
+ 'xo:backup:job': job.id,
121
+ 'xo:backup:schedule': scheduleId,
122
+ 'xo:backup:vm': vm.uuid,
123
+ }),
124
+ ])
125
+ }
126
+ }
@@ -0,0 +1,85 @@
1
+ const { formatFilenameDate } = require('../_filenameDate.js')
2
+ const { getOldEntries } = require('../_getOldEntries.js')
3
+ const { getVmBackupDir } = require('../_getVmBackupDir.js')
4
+ const { Task } = require('../Task.js')
5
+
6
+ const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
7
+ const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
8
+
9
+ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(AbstractFullWriter) {
10
+ constructor(props) {
11
+ super(props)
12
+
13
+ this.run = Task.wrapFn(
14
+ {
15
+ name: 'export',
16
+ data: {
17
+ id: props.remoteId,
18
+ type: 'remote',
19
+
20
+ // necessary?
21
+ isFull: true,
22
+ },
23
+ },
24
+ this.run
25
+ )
26
+ }
27
+
28
+ async _run({ timestamp, sizeContainer, stream }) {
29
+ const backup = this._backup
30
+ const settings = this._settings
31
+
32
+ const { job, scheduleId, vm } = backup
33
+
34
+ const adapter = this._adapter
35
+ const handler = adapter.handler
36
+ const backupDir = getVmBackupDir(vm.uuid)
37
+
38
+ // TODO: clean VM backup directory
39
+
40
+ const oldBackups = getOldEntries(
41
+ settings.exportRetention - 1,
42
+ await adapter.listVmBackups(vm.uuid, _ => _.mode === 'full' && _.scheduleId === scheduleId)
43
+ )
44
+ const deleteOldBackups = () => adapter.deleteFullVmBackups(oldBackups)
45
+
46
+ const basename = formatFilenameDate(timestamp)
47
+
48
+ const dataBasename = basename + '.xva'
49
+ const dataFilename = backupDir + '/' + dataBasename
50
+
51
+ const metadataFilename = `${backupDir}/${basename}.json`
52
+ const metadata = {
53
+ jobId: job.id,
54
+ mode: job.mode,
55
+ scheduleId,
56
+ timestamp,
57
+ version: '2.0.0',
58
+ vm,
59
+ vmSnapshot: this._backup.exportedVm,
60
+ xva: './' + dataBasename,
61
+ }
62
+
63
+ const { deleteFirst } = settings
64
+ if (deleteFirst) {
65
+ await deleteOldBackups()
66
+ }
67
+
68
+ await Task.run({ name: 'transfer' }, async () => {
69
+ await adapter.outputStream(dataFilename, stream, {
70
+ validator: tmpPath => adapter.isValidXva(tmpPath),
71
+ })
72
+ return { size: sizeContainer.size }
73
+ })
74
+ metadata.size = sizeContainer.size
75
+ await handler.outputFile(metadataFilename, JSON.stringify(metadata), {
76
+ dirMode: backup.config.dirMode,
77
+ })
78
+
79
+ if (!deleteFirst) {
80
+ await deleteOldBackups()
81
+ }
82
+
83
+ // TODO: run cleanup?
84
+ }
85
+ }