Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-web/VM/import): ability to import XVA VM from URL #6130

Merged
merged 14 commits into from
Mar 29, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

> Users must be able to say: “Nice enhancement, I'm eager to test it”

- [Import VM] Add a way to import VM from an URL (PR [#6130](https://github.com/vatesfr/xen-orchestra/pull/6130))
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved

### Bug fixes

> Users must be able to say: “I had this issue, happy to know it's fixed”
Expand All @@ -31,3 +33,4 @@
> In case of conflict, the highest (lowest in previous list) `$version` wins.

- xo-server patch
- xo-web minor
4 changes: 4 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1545,8 +1545,12 @@ 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.',
tooltipsFileType: "In case the URL path isn't properly formatted.",
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
url: 'URL',
vmImportToPool: 'To Pool:',
vmImportToSr: 'To SR:',
vmsToImport: 'VMs to import',
Expand Down
9 changes: 7 additions & 2 deletions packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 of type string and corresponds to the vmId
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
success(_('vmImportSuccess'), name)
return [result]
}
formData.append('file', file)
const res = await post(result.$sendTo, formData)
const json = await res.json()
Expand Down
145 changes: 100 additions & 45 deletions packages/xo-web/src/xo-app/vm-import/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ 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 { 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'

import styles from './index.css'

import Tooltip from '../../common/tooltip'

// ===================================================================

const FORMAT_TO_HANDLER = {
Expand Down Expand Up @@ -198,7 +202,11 @@ const getRedirectionUrl = vms =>
export default class Import extends Component {
constructor(props) {
super(props)
this.state.vms = []
this.state = {
isFromUrl: false,
url: '',
vms: [],
}
}

_import = () => {
Expand All @@ -217,6 +225,15 @@ export default class Import extends Component {
)
}

_importVmFromUrl = () => {
const url = this.state.url
const file = {
name: url.split('/').pop(),
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
}
const type = this.state.type ?? file.name.split('.').pop().trim()
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
return importVm(file, type, undefined, this.state.sr, url)
}

_handleDrop = async files => {
this.setState({
vms: [],
Expand Down Expand Up @@ -270,11 +287,14 @@ export default class Import extends Component {
}

render() {
const { pool, sr, srPredicate, vms } = this.state
const { isFromUrl, pool, sr, srPredicate, vms, url } = 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>
Expand All @@ -293,62 +313,97 @@ 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>
pdonias marked this conversation as resolved.
Show resolved Hide resolved
<FormGrid.InputCol>
<Input
className='form-control'
onChange={this.linkState('url')}
pdonias marked this conversation as resolved.
Show resolved Hide resolved
placeholder='https://my-company.net/vm.xva'
type='url'
/>
</FormGrid.InputCol>
</FormGrid.Row>
<FormGrid.Row>
<Tooltip content={_('tooltipsFileType')}>
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
<FormGrid.LabelCol>{_('fileType')}:</FormGrid.LabelCol>
</Tooltip>
<FormGrid.InputCol>
<Input className='form-control' onChange={this.linkState('type')} placeholder='xva' />
pdonias marked this conversation as resolved.
Show resolved Hide resolved
</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>
)
Expand Down