feat(xo-web/VM/import): ability to import XVA VM from URL (#6130)
Follow-up of 86e390f70f
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
- [Rolling Pool Update] Don't update if some of the hosts are not running
|
||||
- [VM form] Add link to documentation on secure boot in the Advanced tab (PR [#6146](https://github.com/vatesfr/xen-orchestra/pull/6146))
|
||||
- [Install patches] Update confirmation messages for patch installation (PR [#6159](https://github.com/vatesfr/xen-orchestra/pull/6159))
|
||||
- [Import VM] Ability to import a VM from a URL (PR [#6130](https://github.com/vatesfr/xen-orchestra/pull/6130))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
||||
@@ -1550,8 +1550,11 @@ const messages = {
|
||||
resourceSetNew: 'New',
|
||||
|
||||
// ---- VM import ---
|
||||
fileType: 'File type:',
|
||||
fromUrl: 'From URL',
|
||||
importVmsList: 'Drop OVA or XVA files here to import Virtual Machines.',
|
||||
noSelectedVms: 'No selected VMs.',
|
||||
url: 'URL:',
|
||||
vmImportToPool: 'To Pool:',
|
||||
vmImportToSr: 'To SR:',
|
||||
vmsToImport: 'VMs to import',
|
||||
|
||||
@@ -1581,7 +1581,7 @@ export const fetchVmStats = (vm, granularity) => _call('vm.stats', { id: resolve
|
||||
|
||||
export const getVmsHaValues = () => _call('vm.getHaValues')
|
||||
|
||||
export const importVm = async (file, type = 'xva', data = undefined, sr) => {
|
||||
export const importVm = async (file, type = 'xva', data = undefined, sr, url = undefined) => {
|
||||
const { name } = file
|
||||
|
||||
info(_('startVmImport'), name)
|
||||
@@ -1598,7 +1598,12 @@ export const importVm = async (file, type = 'xva', data = undefined, sr) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await _call('vm.import', { type, data, sr: resolveId(sr) })
|
||||
const result = await _call('vm.import', { type, data, sr: resolveId(sr), url })
|
||||
if (url !== undefined) {
|
||||
// If imported from URL, result is the ID of the created VM
|
||||
success(_('vmImportSuccess'), name)
|
||||
return [result]
|
||||
}
|
||||
formData.append('file', file)
|
||||
const res = await post(result.$sendTo, formData)
|
||||
const json = await res.json()
|
||||
|
||||
@@ -10,10 +10,12 @@ import orderBy from 'lodash/orderBy'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { importVms, isSrWritable } from 'xo'
|
||||
import { SizeInput } from 'form'
|
||||
import { importVm, importVms, isSrWritable } from 'xo'
|
||||
import { Select, SizeInput, Toggle } from 'form'
|
||||
import { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { connectStore, formatSize, mapPlus, noop } from 'utils'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
|
||||
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import parseOvaFile from './ova'
|
||||
@@ -22,6 +24,13 @@ import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
]
|
||||
|
||||
const FORMAT_TO_HANDLER = {
|
||||
ova: parseOvaFile,
|
||||
xva: noop,
|
||||
@@ -198,7 +207,15 @@ const getRedirectionUrl = vms =>
|
||||
export default class Import extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state.vms = []
|
||||
this.state = {
|
||||
isFromUrl: false,
|
||||
type: {
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
url: '',
|
||||
vms: [],
|
||||
}
|
||||
}
|
||||
|
||||
_import = () => {
|
||||
@@ -217,6 +234,14 @@ export default class Import extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
_importVmFromUrl = () => {
|
||||
const { type, url } = this.state
|
||||
const file = {
|
||||
name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),
|
||||
}
|
||||
return importVm(file, type.value, undefined, this.state.sr, url)
|
||||
}
|
||||
|
||||
_handleDrop = async files => {
|
||||
this.setState({
|
||||
vms: [],
|
||||
@@ -270,11 +295,14 @@ export default class Import extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { pool, sr, srPredicate, vms } = this.state
|
||||
const { isFromUrl, pool, sr, srPredicate, type, url, vms } = this.state
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<form id='import-form'>
|
||||
<p>
|
||||
<Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}
|
||||
</p>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
@@ -293,62 +321,95 @@ export default class Import extends Component {
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
{sr && (
|
||||
<div>
|
||||
<Dropzone onDrop={this._handleDrop} message={_('importVmsList')} />
|
||||
<hr />
|
||||
<h5>{_('vmsToImport')}</h5>
|
||||
{vms.length > 0 ? (
|
||||
<div>
|
||||
{map(vms, ({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error ? (
|
||||
data && (
|
||||
{sr &&
|
||||
(!isFromUrl ? (
|
||||
<div>
|
||||
<Dropzone onDrop={this._handleDrop} message={_('importVmsList')} />
|
||||
<hr />
|
||||
<h5>{_('vmsToImport')}</h5>
|
||||
{vms.length > 0 ? (
|
||||
<div>
|
||||
{map(vms, ({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error ? (
|
||||
data && (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
</div>
|
||||
<VmData {...data} ref={`vm-data-${vmIndex}`} pool={pool} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong>{' '}
|
||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
<VmData {...data} ref={`vm-data-${vmIndex}`} pool={pool} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong>{' '}
|
||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{_('noSelectedVms')}</p>
|
||||
)}
|
||||
<hr />
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!vms.length}
|
||||
className='mr-1'
|
||||
form='import-form'
|
||||
handler={this._import}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={this._handleCleanSelectedVms}>{_('importVmsCleanList')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p>{_('noSelectedVms')}</p>
|
||||
)}
|
||||
<hr />
|
||||
<div className='form-group pull-right'>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('url')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
onChange={this.linkState('url')}
|
||||
placeholder='https://my-company.net/vm.xva'
|
||||
type='url'
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('fileType')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Select onChange={this.linkState('type')} options={FILE_TYPES} required value={type} />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!vms.length}
|
||||
className='mr-1'
|
||||
className='mr-1 mt-1'
|
||||
disabled={isEmpty(url)}
|
||||
form='import-form'
|
||||
handler={this._import}
|
||||
handler={this._importVmFromUrl}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={this._handleCleanSelectedVms}>{_('importVmsCleanList')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user