Some corrections for delta integration in xo-web.
- List delta backups in subfolders. - Fix unhandled exception. (ENOENT) - ...
This commit is contained in:
parent
b290520951
commit
412a1bd62a
@ -26,11 +26,8 @@ exports.delete = delete_
|
||||
|
||||
disconnect = $coroutine ({vbd}) ->
|
||||
xapi = @getXAPI vbd
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
yield xapi.call 'VBD.unplug_force', vbd._xapiRef
|
||||
|
||||
return true
|
||||
yield xapi.disconnectVBD(vbd._xapiRef)
|
||||
return
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
@ -46,11 +43,8 @@ exports.disconnect = disconnect
|
||||
|
||||
connect = $coroutine ({vbd}) ->
|
||||
xapi = @getXAPI vbd
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
yield xapi.call 'VBD.plug', vbd._xapiRef
|
||||
|
||||
return true
|
||||
yield xapi.connectVBD(vbd._xapiRef)
|
||||
return
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
|
16
src/xapi.js
16
src/xapi.js
@ -1026,7 +1026,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
let host
|
||||
let snapshotRef
|
||||
if (isVmRunning(vm)) {
|
||||
if (isVmRunning(vm) && !onlyMetadata) {
|
||||
host = vm.$resident_on
|
||||
snapshotRef = await this._snapshotVm(vm)
|
||||
} else {
|
||||
@ -1514,9 +1514,21 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async connectVBD (vbdId) {
|
||||
await this.call('VBD.plug', vbdId)
|
||||
}
|
||||
|
||||
async disconnectVBD (vbdId) {
|
||||
// TODO: check if VBD is attached before
|
||||
await this.call('VBD.unplug_force', vbdId)
|
||||
}
|
||||
|
||||
async destroyVbdsFromVm (vmId) {
|
||||
await Promise.all(
|
||||
mapToArray(this.getObject(vmId).$VBDs, vbd => this.call('VBD.destroy', vbd.$ref))
|
||||
mapToArray(this.getObject(vmId).$VBDs, async vbd => {
|
||||
await this.disconnectVBD(vbd.$ref).catch(noop)
|
||||
return this.call('VBD.destroy', vbd.$ref)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
107
src/xo.js
107
src/xo.js
@ -766,14 +766,35 @@ export default class Xo extends EventEmitter {
|
||||
return this._listRemote(remote)
|
||||
}
|
||||
|
||||
async _listRemoteBackups (remote) {
|
||||
const path = remote.path
|
||||
|
||||
// List backups. (Except delta backups)
|
||||
const xvaFilter = file => endsWith(file, '.xva')
|
||||
|
||||
const files = await fs.readdir(path)
|
||||
const backups = filter(files, xvaFilter)
|
||||
|
||||
// List delta backups.
|
||||
const deltaDirs = filter(files, file => startsWith(file, 'vm_delta_'))
|
||||
|
||||
for (const deltaDir of deltaDirs) {
|
||||
const files = await fs.readdir(`${path}/${deltaDir}`)
|
||||
const deltaBackups = filter(files, xvaFilter)
|
||||
|
||||
Array.prototype.push.apply(backups, mapToArray(deltaBackups, deltaBackup => `${deltaDir}/${deltaBackup}`))
|
||||
}
|
||||
|
||||
return backups
|
||||
}
|
||||
|
||||
async _listRemote (remote) {
|
||||
const fsRemotes = {
|
||||
nfs: true,
|
||||
local: true
|
||||
}
|
||||
if (remote.type in fsRemotes) {
|
||||
const files = await fs.readdir(remote.path)
|
||||
return filter(files, file => endsWith(file, '.xva'))
|
||||
return this._listRemoteBackups(remote)
|
||||
}
|
||||
throw new Error('Unhandled remote type')
|
||||
}
|
||||
@ -827,24 +848,32 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async importVmBackup (remoteId, file, sr) {
|
||||
const remote = await this.getRemote(remoteId)
|
||||
const path = `${remote.path}/${file}`
|
||||
async _openAndwaitReadableFile (path, errorMessage) {
|
||||
const stream = fs.createReadStream(path)
|
||||
|
||||
try {
|
||||
await eventToPromise(stream, 'readable')
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new Error('VM to import not found in this remote')
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const xapi = this.getXAPI(sr)
|
||||
const stats = await fs.stat(path)
|
||||
|
||||
await xapi.importVm(stream, stats.size, { srId: sr._xapiId })
|
||||
return [ stream, stats.size ]
|
||||
}
|
||||
|
||||
async importVmBackup (remoteId, file, sr) {
|
||||
const remote = await this.getRemote(remoteId)
|
||||
const path = `${remote.path}/${file}`
|
||||
const [ stream, length ] = await this._openAndwaitReadableFile(
|
||||
path, 'VM to import not found in this remote')
|
||||
|
||||
const xapi = this.getXAPI(sr)
|
||||
|
||||
await xapi.importVm(stream, length, { srId: sr._xapiId })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@ -900,39 +929,42 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
// Count delta backups.
|
||||
const nDelta = this._countDeltaVdiBackups(backups)
|
||||
const isFull = (nDelta + 1 >= depth || !backups.length)
|
||||
|
||||
// Make snapshot.
|
||||
const date = safeDateFormat(new Date())
|
||||
const base = find(vdi.$snapshots, { name_label: 'BASE_VDI_SNAPSHOT' })
|
||||
const currentSnapshot = await xapi.snapshotVdi(vdi.$id, 'BASE_VDI_SNAPSHOT')
|
||||
const base = find(vdi.$snapshots, { name_label: 'XO_DELTA_BASE_VDI_SNAPSHOT' })
|
||||
const currentSnapshot = await xapi.snapshotVdi(vdi.$id, 'XO_DELTA_BASE_VDI_SNAPSHOT')
|
||||
|
||||
// It is strange to have no base but a full backup !
|
||||
// A full is necessary if it not exists backups or
|
||||
// the number of delta backups is sufficient.
|
||||
const isFull = (nDelta + 1 >= depth || !backups.length || !base)
|
||||
|
||||
// Export full or delta backup.
|
||||
const vdiFilename = `${date}_${isFull ? 'full' : 'delta'}.vhd`
|
||||
const backupFullPath = `${path}/${vdiFilename}`
|
||||
const sourceStream = await xapi.exportVdi(currentSnapshot.$id, {
|
||||
baseId: isFull ? undefined : base.$id,
|
||||
format: Xapi.VDI_FORMAT_VHD
|
||||
})
|
||||
|
||||
try {
|
||||
await eventToPromise(
|
||||
sourceStream.pipe(
|
||||
fs.createWriteStream(backupFullPath, { flags: 'wx' })
|
||||
),
|
||||
'finish'
|
||||
)
|
||||
const sourceStream = await xapi.exportVdi(currentSnapshot.$id, {
|
||||
baseId: isFull ? undefined : base.$id,
|
||||
format: Xapi.VDI_FORMAT_VHD
|
||||
})
|
||||
|
||||
const targetStream = fs.createWriteStream(backupFullPath, { flags: 'wx' })
|
||||
sourceStream.on('error', () => targetStream.emit('error'))
|
||||
await eventToPromise(sourceStream.pipe(targetStream), 'finish')
|
||||
} catch (e) {
|
||||
// Remove backup. (corrupt)
|
||||
await xapi.deleteVdi(currentSnapshot.$id)
|
||||
fs.unlink(backupFullPath).catch(noop)
|
||||
throw e
|
||||
}
|
||||
|
||||
// Remove last snapshot from last retention or previous snapshot.
|
||||
if (base) {
|
||||
await xapi.deleteVdi(base.$id)
|
||||
}
|
||||
|
||||
// Remove last snapshot from last retention or previous snapshot.
|
||||
backups.push(vdiFilename)
|
||||
await this._removeOldVdiBackups(backups, path, depth)
|
||||
|
||||
@ -941,11 +973,11 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
|
||||
async _importVdiBackupContent (xapi, file, vdiId) {
|
||||
const stream = fs.createReadStream(file)
|
||||
const stats = await fs.stat(file)
|
||||
const [ stream, length ] = await this._openAndwaitReadableFile(
|
||||
file, 'VDI to import not found in this remote')
|
||||
|
||||
await xapi.importVdiContent(vdiId, stream, {
|
||||
length: stats.size,
|
||||
length,
|
||||
format: Xapi.VDI_FORMAT_VHD
|
||||
})
|
||||
}
|
||||
@ -983,9 +1015,9 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async _listDeltaVmBackups (path, tag, name_label) {
|
||||
async _listDeltaVmBackups (path) {
|
||||
const files = await fs.readdir(path)
|
||||
return await sortBy(filter(files, (fileName) => /^\d+T\d+Z\.(?:xva|json)$/.test(fileName)))
|
||||
return await sortBy(filter(files, (fileName) => /^\d+T\d+Z_.*\.(?:xva|json)$/.test(fileName)))
|
||||
}
|
||||
|
||||
// FIXME: Avoid bad files creation. (For example, exception during backup)
|
||||
@ -993,15 +1025,14 @@ export default class Xo extends EventEmitter {
|
||||
// The files are all vhd.
|
||||
async rollingDeltaVmBackup ({vm, remoteId, tag, depth}) {
|
||||
const remote = await this.getRemote(remoteId)
|
||||
const directory = `vm_${tag}_${vm.uuid}`
|
||||
const directory = `vm_delta_${tag}_${vm.uuid}`
|
||||
const path = `${remote.path}/${directory}`
|
||||
|
||||
await fs.ensureDir(path)
|
||||
|
||||
const info = {
|
||||
vbds: [],
|
||||
vdis: {},
|
||||
name_label: vm.name_label
|
||||
vdis: {}
|
||||
}
|
||||
|
||||
const promises = []
|
||||
@ -1040,11 +1071,12 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
const backups = await this._listDeltaVmBackups(path, tag, vm.name_label)
|
||||
const backups = await this._listDeltaVmBackups(path)
|
||||
const date = safeDateFormat(new Date())
|
||||
const backupFormat = `${date}_${vm.name_label}`
|
||||
|
||||
const xvaPath = `${path}/${date}.xva`
|
||||
const infoPath = `${path}/${date}.json`
|
||||
const xvaPath = `${path}/${backupFormat}.xva`
|
||||
const infoPath = `${path}/${backupFormat}.json`
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
@ -1060,14 +1092,13 @@ export default class Xo extends EventEmitter {
|
||||
await this._removeOldBackups(backups, path, backups.length - (depth - 1) * 2)
|
||||
|
||||
// Returns relative path.
|
||||
return `${directory}/${date}`
|
||||
return `${directory}/${backupFormat}`
|
||||
}
|
||||
|
||||
async _importVmMetadata (xapi, file) {
|
||||
const stream = fs.createReadStream(file)
|
||||
const stats = await fs.stat(file)
|
||||
|
||||
return await xapi.importVm(stream, stats.size, { onlyMetadata: true })
|
||||
const [ stream, length ] = await this._openAndwaitReadableFile(
|
||||
file, 'VM metadata to import not found in this remote')
|
||||
return await xapi.importVm(stream, length, { onlyMetadata: true })
|
||||
}
|
||||
|
||||
async _importDeltaVdiBackupFromVm (xapi, vmId, remoteId, directory, vdiInfo) {
|
||||
|
Loading…
Reference in New Issue
Block a user