Some corrections for delta integration in xo-web.

- List delta backups in subfolders.
- Fix unhandled exception. (ENOENT)
- ...
This commit is contained in:
wescoeur 2015-12-18 15:32:56 +01:00
parent b290520951
commit 412a1bd62a
3 changed files with 87 additions and 50 deletions

View File

@ -26,11 +26,8 @@ exports.delete = delete_
disconnect = $coroutine ({vbd}) -> disconnect = $coroutine ({vbd}) ->
xapi = @getXAPI vbd xapi = @getXAPI vbd
yield xapi.disconnectVBD(vbd._xapiRef)
# TODO: check if VBD is attached before return
yield xapi.call 'VBD.unplug_force', vbd._xapiRef
return true
disconnect.params = { disconnect.params = {
id: { type: 'string' } id: { type: 'string' }
@ -46,11 +43,8 @@ exports.disconnect = disconnect
connect = $coroutine ({vbd}) -> connect = $coroutine ({vbd}) ->
xapi = @getXAPI vbd xapi = @getXAPI vbd
yield xapi.connectVBD(vbd._xapiRef)
# TODO: check if VBD is attached before return
yield xapi.call 'VBD.plug', vbd._xapiRef
return true
connect.params = { connect.params = {
id: { type: 'string' } id: { type: 'string' }

View File

@ -1026,7 +1026,7 @@ export default class Xapi extends XapiBase {
let host let host
let snapshotRef let snapshotRef
if (isVmRunning(vm)) { if (isVmRunning(vm) && !onlyMetadata) {
host = vm.$resident_on host = vm.$resident_on
snapshotRef = await this._snapshotVm(vm) snapshotRef = await this._snapshotVm(vm)
} else { } 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) { async destroyVbdsFromVm (vmId) {
await Promise.all( 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
View File

@ -766,14 +766,35 @@ export default class Xo extends EventEmitter {
return this._listRemote(remote) 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) { async _listRemote (remote) {
const fsRemotes = { const fsRemotes = {
nfs: true, nfs: true,
local: true local: true
} }
if (remote.type in fsRemotes) { if (remote.type in fsRemotes) {
const files = await fs.readdir(remote.path) return this._listRemoteBackups(remote)
return filter(files, file => endsWith(file, '.xva'))
} }
throw new Error('Unhandled remote type') throw new Error('Unhandled remote type')
} }
@ -827,24 +848,32 @@ export default class Xo extends EventEmitter {
} }
} }
async importVmBackup (remoteId, file, sr) { async _openAndwaitReadableFile (path, errorMessage) {
const remote = await this.getRemote(remoteId)
const path = `${remote.path}/${file}`
const stream = fs.createReadStream(path) const stream = fs.createReadStream(path)
try { try {
await eventToPromise(stream, 'readable') await eventToPromise(stream, 'readable')
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
throw new Error('VM to import not found in this remote') throw new Error(errorMessage)
} }
throw error throw error
} }
const xapi = this.getXAPI(sr)
const stats = await fs.stat(path) 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. // Count delta backups.
const nDelta = this._countDeltaVdiBackups(backups) const nDelta = this._countDeltaVdiBackups(backups)
const isFull = (nDelta + 1 >= depth || !backups.length)
// Make snapshot. // Make snapshot.
const date = safeDateFormat(new Date()) const date = safeDateFormat(new Date())
const base = find(vdi.$snapshots, { name_label: 'BASE_VDI_SNAPSHOT' }) const base = find(vdi.$snapshots, { name_label: 'XO_DELTA_BASE_VDI_SNAPSHOT' })
const currentSnapshot = await xapi.snapshotVdi(vdi.$id, '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. // Export full or delta backup.
const vdiFilename = `${date}_${isFull ? 'full' : 'delta'}.vhd` const vdiFilename = `${date}_${isFull ? 'full' : 'delta'}.vhd`
const backupFullPath = `${path}/${vdiFilename}` const backupFullPath = `${path}/${vdiFilename}`
const sourceStream = await xapi.exportVdi(currentSnapshot.$id, {
baseId: isFull ? undefined : base.$id,
format: Xapi.VDI_FORMAT_VHD
})
try { try {
await eventToPromise( const sourceStream = await xapi.exportVdi(currentSnapshot.$id, {
sourceStream.pipe( baseId: isFull ? undefined : base.$id,
fs.createWriteStream(backupFullPath, { flags: 'wx' }) format: Xapi.VDI_FORMAT_VHD
), })
'finish'
) const targetStream = fs.createWriteStream(backupFullPath, { flags: 'wx' })
sourceStream.on('error', () => targetStream.emit('error'))
await eventToPromise(sourceStream.pipe(targetStream), 'finish')
} catch (e) { } catch (e) {
// Remove backup. (corrupt) // Remove backup. (corrupt)
await xapi.deleteVdi(currentSnapshot.$id)
fs.unlink(backupFullPath).catch(noop) fs.unlink(backupFullPath).catch(noop)
throw e throw e
} }
// Remove last snapshot from last retention or previous snapshot.
if (base) { if (base) {
await xapi.deleteVdi(base.$id) await xapi.deleteVdi(base.$id)
} }
// Remove last snapshot from last retention or previous snapshot.
backups.push(vdiFilename) backups.push(vdiFilename)
await this._removeOldVdiBackups(backups, path, depth) await this._removeOldVdiBackups(backups, path, depth)
@ -941,11 +973,11 @@ export default class Xo extends EventEmitter {
} }
async _importVdiBackupContent (xapi, file, vdiId) { async _importVdiBackupContent (xapi, file, vdiId) {
const stream = fs.createReadStream(file) const [ stream, length ] = await this._openAndwaitReadableFile(
const stats = await fs.stat(file) file, 'VDI to import not found in this remote')
await xapi.importVdiContent(vdiId, stream, { await xapi.importVdiContent(vdiId, stream, {
length: stats.size, length,
format: Xapi.VDI_FORMAT_VHD 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) 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) // 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. // The files are all vhd.
async rollingDeltaVmBackup ({vm, remoteId, tag, depth}) { async rollingDeltaVmBackup ({vm, remoteId, tag, depth}) {
const remote = await this.getRemote(remoteId) const remote = await this.getRemote(remoteId)
const directory = `vm_${tag}_${vm.uuid}` const directory = `vm_delta_${tag}_${vm.uuid}`
const path = `${remote.path}/${directory}` const path = `${remote.path}/${directory}`
await fs.ensureDir(path) await fs.ensureDir(path)
const info = { const info = {
vbds: [], vbds: [],
vdis: {}, vdis: {}
name_label: vm.name_label
} }
const promises = [] const promises = []
@ -1040,11 +1071,12 @@ export default class Xo extends EventEmitter {
await Promise.all(promises) 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 date = safeDateFormat(new Date())
const backupFormat = `${date}_${vm.name_label}`
const xvaPath = `${path}/${date}.xva` const xvaPath = `${path}/${backupFormat}.xva`
const infoPath = `${path}/${date}.json` const infoPath = `${path}/${backupFormat}.json`
try { try {
await Promise.all([ await Promise.all([
@ -1060,14 +1092,13 @@ export default class Xo extends EventEmitter {
await this._removeOldBackups(backups, path, backups.length - (depth - 1) * 2) await this._removeOldBackups(backups, path, backups.length - (depth - 1) * 2)
// Returns relative path. // Returns relative path.
return `${directory}/${date}` return `${directory}/${backupFormat}`
} }
async _importVmMetadata (xapi, file) { async _importVmMetadata (xapi, file) {
const stream = fs.createReadStream(file) const [ stream, length ] = await this._openAndwaitReadableFile(
const stats = await fs.stat(file) file, 'VM metadata to import not found in this remote')
return await xapi.importVm(stream, length, { onlyMetadata: true })
return await xapi.importVm(stream, stats.size, { onlyMetadata: true })
} }
async _importDeltaVdiBackupFromVm (xapi, vmId, remoteId, directory, vdiInfo) { async _importDeltaVdiBackupFromVm (xapi, vmId, remoteId, directory, vdiInfo) {