From a776eaf61a769010e9cf263c039358cc9020893e Mon Sep 17 00:00:00 2001 From: badrAZ Date: Thu, 26 Nov 2020 17:14:06 +0100 Subject: [PATCH] feat(xo-server,xo-web): file restore via proxies (#5359) --- CHANGELOG.unreleased.md | 1 + .../src/xo-mixins/backups-ng/index.js | 2 +- .../src/xo-mixins/file-restore-ng.js | 69 ++++++++++++++++--- packages/xo-server/src/xo-mixins/proxies.js | 15 ++-- .../src/xo-app/backup/file-restore/index.js | 5 +- .../backup/file-restore/restore-file-modal.js | 10 ++- 6 files changed, 81 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 863434436..0fbd18d39 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -12,6 +12,7 @@ - [SR] Use SR type `zfs` instead of `file` for ZFS storage repositories (PR [5302](https://github.com/vatesfr/xen-orchestra/pull/5330)) - [Dashboard/Health] List VMs with missing or outdated guest tools (PR [#5376](https://github.com/vatesfr/xen-orchestra/pull/5376)) - [VIF] Ability for admins to set any allowed IPs, including IPv6 and IPs that are not in an IP pool [#2535](https://github.com/vatesfr/xen-orchestra/issues/2535) [#1872](https://github.com/vatesfr/xen-orchestra/issues/1872) (PR [#5367](https://github.com/vatesfr/xen-orchestra/pull/5367)) +- [Proxy] Ability to restore a file from VM backup (PR [#5359](https://github.com/vatesfr/xen-orchestra/pull/5359)) ### Bug fixes diff --git a/packages/xo-server/src/xo-mixins/backups-ng/index.js b/packages/xo-server/src/xo-mixins/backups-ng/index.js index bc6f3db86..04df217d4 100644 --- a/packages/xo-server/src/xo-mixins/backups-ng/index.js +++ b/packages/xo-server/src/xo-mixins/backups-ng/index.js @@ -591,7 +591,7 @@ export default class BackupNg { try { const logsStream = await app.callProxyMethod(proxyId, 'backup.run', params, { - expectStream: true, + assertType: 'iterator', }) const localTaskIds = { __proto__: null } diff --git a/packages/xo-server/src/xo-mixins/file-restore-ng.js b/packages/xo-server/src/xo-mixins/file-restore-ng.js index 33cd04165..6b473d929 100644 --- a/packages/xo-server/src/xo-mixins/file-restore-ng.js +++ b/packages/xo-server/src/xo-mixins/file-restore-ng.js @@ -32,12 +32,6 @@ const IGNORED_PARTITION_TYPES = { 0x82: true, // swap } -const PARTITION_TYPE_NAMES = { - 0x07: 'NTFS', - 0x0c: 'FAT', - 0x83: 'linux', -} - const RE_VHDI = /^vhdi(\d+)$/ async function addDirectory(zip, realPath, metadataPath) { @@ -54,8 +48,7 @@ async function addDirectory(zip, realPath, metadataPath) { const parsePartxLine = createPairsParser({ keyTransform: key => (key === 'UUID' ? 'id' : key.toLowerCase()), - valueTransform: (value, key) => - key === 'start' || key === 'size' ? +value : key === 'type' ? PARTITION_TYPE_NAMES[+value] || value : value, + valueTransform: (value, key) => (key === 'start' || key === 'size' || key === 'type' ? +value : value), }) const listLvmLogicalVolumes = compose( @@ -172,6 +165,25 @@ export default class BackupNgFileRestore { @defer async fetchBackupNgPartitionFiles($defer, remoteId, diskId, partitionId, paths) { + const app = this._app + const { proxy, url, options } = await app.getRemoteWithCredentials(remoteId) + if (proxy !== undefined) { + return app.callProxyMethod( + proxy, + 'backup.fetchPartitionFiles', + { + disk: diskId, + remote: { + url, + options, + }, + partition: partitionId, + paths, + }, + { assertType: 'stream' } + ) + } + const disk = await this._mountDisk(remoteId, diskId) $defer.onFailure(disk.unmount) @@ -190,6 +202,29 @@ export default class BackupNgFileRestore { @defer async listBackupNgDiskPartitions($defer, remoteId, diskId) { + const app = this._app + const { proxy, url, options } = await app.getRemoteWithCredentials(remoteId) + if (proxy !== undefined) { + const stream = await app.callProxyMethod( + proxy, + 'backup.listDiskPartitions', + { + disk: diskId, + remote: { + url, + options, + }, + }, + { assertType: 'iterator' } + ) + + const partitions = [] + for await (const partition of stream) { + partitions.push(partition) + } + return partitions + } + const disk = await this._mountDisk(remoteId, diskId) $defer(disk.unmount) return this._listPartitions(disk.path) @@ -197,6 +232,20 @@ export default class BackupNgFileRestore { @defer async listBackupNgPartitionFiles($defer, remoteId, diskId, partitionId, path) { + const app = this._app + const { proxy, url, options } = await app.getRemoteWithCredentials(remoteId) + if (proxy !== undefined) { + return app.callProxyMethod(proxy, 'backup.listPartitionFiles', { + disk: diskId, + remote: { + url, + options, + }, + partition: partitionId, + path, + }) + } + const disk = await this._mountDisk(remoteId, diskId) $defer(disk.unmount) @@ -237,8 +286,8 @@ export default class BackupNgFileRestore { const partitions = [] splitLines(stdout).forEach(line => { const partition = parsePartxLine(line) - let { type } = partition - if (type == null || (type = +type) in IGNORED_PARTITION_TYPES) { + const { type } = partition + if (type == null || type in IGNORED_PARTITION_TYPES) { return } diff --git a/packages/xo-server/src/xo-mixins/proxies.js b/packages/xo-server/src/xo-mixins/proxies.js index 101cba8da..1b9f93332 100644 --- a/packages/xo-server/src/xo-mixins/proxies.js +++ b/packages/xo-server/src/xo-mixins/proxies.js @@ -321,7 +321,8 @@ export default class Proxy { await this.callProxyMethod(id, 'system.getServerVersion') } - async callProxyMethod(id, method, params, { expectStream = false } = {}) { + // enum assertType {iterator, scalar, stream} + async callProxyMethod(id, method, params, { assertType = 'scalar' } = {}) { const proxy = await this._getProxy(id) const request = { @@ -358,6 +359,10 @@ export default class Proxy { const responseType = contentType.parse(response).type if (responseType === 'application/octet-stream') { + if (assertType !== 'stream') { + response.destroy() + throw new Error(`expect the result to be ${assertType}`) + } return response } @@ -367,13 +372,13 @@ export default class Proxy { const firstLine = await readChunk(lines) const result = parse.result(firstLine) - const isStream = result.$responseType === 'ndjson' - if (isStream !== expectStream) { + const isIterator = result.$responseType === 'ndjson' + if (assertType !== (isIterator ? 'iterator' : 'scalar')) { lines.destroy() - throw new Error(`expect the result ${expectStream ? '' : 'not'} to be a stream`) + throw new Error(`expect the result to be ${assertType}`) } - if (isStream) { + if (isIterator) { return lines } lines.destroy() diff --git a/packages/xo-web/src/xo-app/backup/file-restore/index.js b/packages/xo-web/src/xo-app/backup/file-restore/index.js index 9e1c28a40..1ea33d890 100644 --- a/packages/xo-web/src/xo-app/backup/file-restore/index.js +++ b/packages/xo-web/src/xo-app/backup/file-restore/index.js @@ -88,10 +88,7 @@ export default class Restore extends Component { } _refreshBackupList = async (_remotes = this.props.remotes, jobs = this.props.jobs) => { - const remotes = keyBy( - filter(_remotes, ({ enabled, proxy }) => enabled && proxy === undefined), - 'id' - ) + const remotes = keyBy(filter(_remotes, 'enabled'), 'id') const backupsByRemote = await listVmBackups(toArray(remotes)) const backupDataByVm = {} diff --git a/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js b/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js index 6b0f59286..ee0f7715e 100644 --- a/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js +++ b/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js @@ -2,6 +2,7 @@ import _ from 'intl' import ActionButton from 'action-button' import ButtonGroup from 'button-group' import Component from 'base-component' +import defined from '@xen-orchestra/defined' import Icon from 'icon' import React from 'react' import Select from 'form/select' @@ -16,6 +17,12 @@ import { listPartitions, listFiles } from 'xo' // ----------------------------------------------------------------------------- +const PARTITION_TYPE_NAMES = { + 0x07: 'NTFS', + 0x0c: 'FAT', + 0x83: 'LINUX', +} + const BACKUP_RENDERER = getRenderXoItemOfType('backup') const diskOptionRenderer = disk => ( @@ -26,7 +33,8 @@ const diskOptionRenderer = disk => ( const partitionOptionRenderer = partition => ( - {partition.name} {partition.type} {partition.size && `(${formatSize(+partition.size)})`} + {partition.name} {defined(PARTITION_TYPE_NAMES[partition.type], partition.type)}{' '} + {partition.size && `(${formatSize(+partition.size)})`} )