diff --git a/src/api/vm.coffee b/src/api/vm.coffee index 98bb370fe..a5d29ee8f 100644 --- a/src/api/vm.coffee +++ b/src/api/vm.coffee @@ -755,12 +755,13 @@ exports.importDeltaBackup = importDeltaBackup #--------------------------------------------------------------------- -deltaCopy = ({ force, vm, sr }) -> @deltaCopyVm(vm, sr, force) +deltaCopy = ({ force, vm, retention, sr }) -> @deltaCopyVm(vm, sr, force, retention) deltaCopy.params = { + force: { type: 'boolean', optional: true }, id: { type: 'string' }, - sr: { type: 'string' }, - force: { type: 'boolean', optional: true } + retention: { type: 'number', optional: true }, + sr: { type: 'string' } } deltaCopy.resolve = { diff --git a/src/xapi/index.js b/src/xapi/index.js index f06114809..abd6bb1b0 100644 --- a/src/xapi/index.js +++ b/src/xapi/index.js @@ -894,18 +894,15 @@ export default class Xapi extends XapiBase { vbds, vdis, vifs, - vm: baseVm && !disableBaseTags - ? { - ...vm, - other_config: { + vm: { + ...vm, + other_config: baseVm && !disableBaseTags + ? { ...vm.other_config, [TAG_BASE_DELTA]: baseVm.uuid } - } - : { - ...vm, - other_config: omit(vm.other_config, TAG_BASE_DELTA) - } + : omit(vm.other_config, TAG_BASE_DELTA) + } }, 'streams', { value: await streams::pAll() }) diff --git a/src/xo-mixins/backups.js b/src/xo-mixins/backups.js index b404273b2..f189edcd2 100644 --- a/src/xo-mixins/backups.js +++ b/src/xo-mixins/backups.js @@ -51,6 +51,8 @@ import { const DELTA_BACKUP_EXT = '.json' const DELTA_BACKUP_EXT_LENGTH = DELTA_BACKUP_EXT.length +const TAG_SOURCE_VM = 'xo:source_vm' +const TAG_EXPORT_TIME = 'xo:export_time' const shortDate = utcFormat('%Y-%m-%d') @@ -398,12 +400,12 @@ export default class { // ----------------------------------------------------------------- @deferrable.onFailure - async deltaCopyVm ($onFailure, srcVm, targetSr, force = false) { + async deltaCopyVm ($onFailure, srcVm, targetSr, force = false, retention = 1) { const srcXapi = this._xo.getXapi(srcVm) const targetXapi = this._xo.getXapi(targetSr) // Get Xen objects from XO objects. - srcVm = srcXapi.getObject(srcVm._xapiId) + const { uuid } = srcVm = srcXapi.getObject(srcVm._xapiId) targetSr = targetXapi.getObject(targetSr._xapiId) // 1. Find the local base for this SR (if any). @@ -426,7 +428,10 @@ export default class { $onFailure(() => srcXapi.deleteVm(delta.vm.uuid)) $onFailure(cancel) - delta.vm.name_label += ` (${shortDate(Date.now())})` + const now = Date.now() + delta.vm.name_label += ` (${shortDate(now)})` + delta.vm.other_config[TAG_SOURCE_VM] = uuid + delta.vm.other_config[TAG_EXPORT_TIME] = safeDateFormat(now) forEach(delta.vdis, (vdi, key) => { const id = `${key}.vhd` @@ -436,10 +441,19 @@ export default class { delta.streams[id] = delta.streams[id].pipe(sizeStream) }) + let toRemove = filter(targetXapi.objects.all, obj => + obj.$type === 'vm' && + obj.other_config[TAG_SOURCE_VM] === uuid + ) + const n = toRemove.length - retention + 1 // take into account the future copy + toRemove = n > 0 + ? sortBy(toRemove, _ => _.other_config[TAG_EXPORT_TIME]).slice(0, n) + : undefined + const promise = targetXapi.importDeltaVm( delta, { - deleteBase: true, // Remove the remote base. + deleteBase: toRemove.length === 0, // old replications are not captured in toRemove srId: targetSr.$id } ) @@ -450,6 +464,12 @@ export default class { promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors() } + if (toRemove !== undefined) { + promise.then(() => asyncMap(toRemove, _ => + targetXapi.deleteVm(_.$id)) + )::ignoreErrors() + } + // (Asynchronously) Identify snapshot as future base. promise.then(() => { return srcXapi._updateObjectMapProperty(srcVm, 'other_config', {