@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.
- package/Backup.js +263 -0
- package/DurablePartition.js +40 -0
- package/ImportVmBackup.js +66 -0
- package/README.md +28 -0
- package/RemoteAdapter.js +552 -0
- package/RestoreMetadataBackup.js +24 -0
- package/Task.js +151 -0
- package/_PoolMetadataBackup.js +75 -0
- package/_VmBackup.js +409 -0
- package/_XoMetadataBackup.js +62 -0
- package/_backupType.js +4 -0
- package/_backupWorker.js +155 -0
- package/_cancelableMap.js +20 -0
- package/_cleanVm.js +378 -0
- package/_deltaVm.js +347 -0
- package/_extractIdsFromSimplePattern.js +29 -0
- package/_filenameDate.js +6 -0
- package/_forkStreamUnpipe.js +28 -0
- package/_getOldEntries.js +4 -0
- package/_getTmpDir.js +20 -0
- package/_getVmBackupDir.js +6 -0
- package/_isValidXva.js +60 -0
- package/_listPartitions.js +52 -0
- package/_lvm.js +31 -0
- package/_watchStreamSize.js +7 -0
- package/formatVmBackups.js +34 -0
- package/merge-worker/cli.js +69 -0
- package/merge-worker/index.js +25 -0
- package/package.json +49 -0
- package/parseMetadataBackupId.js +23 -0
- package/runBackupWorker.js +38 -0
- package/writers/DeltaBackupWriter.js +221 -0
- package/writers/DeltaReplicationWriter.js +126 -0
- package/writers/FullBackupWriter.js +85 -0
- package/writers/FullReplicationWriter.js +88 -0
- package/writers/_AbstractDeltaWriter.js +26 -0
- package/writers/_AbstractFullWriter.js +12 -0
- package/writers/_AbstractWriter.js +10 -0
- package/writers/_MixinBackupWriter.js +51 -0
- package/writers/_MixinReplicationWriter.js +8 -0
- package/writers/_checkVhd.js +5 -0
- package/writers/_listReplicatedVms.js +30 -0
- 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
|
+
}
|