feat(xo-server,xo-web): file restore via proxies (#5359)
This commit is contained in:
parent
ae2a92d229
commit
a776eaf61a
@ -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
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 = {}
|
||||
|
@ -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 => (
|
||||
<span>
|
||||
{partition.name} {partition.type} {partition.size && `(${formatSize(+partition.size)})`}
|
||||
{partition.name} {defined(PARTITION_TYPE_NAMES[partition.type], partition.type)}{' '}
|
||||
{partition.size && `(${formatSize(+partition.size)})`}
|
||||
</span>
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user