feat(xo-web/import): ability to import multiple VMs from VMware (#6718)
See #6708
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
- [VM] Show distro icon for opensuse-microos [Forum#6965](https://xcp-ng.org/forum/topic/6965) (PR [#6746](https://github.com/vatesfr/xen-orchestra/pull/6746))
|
||||
- [Backup] Display the VM name label in the log even if the VM is not currently connected
|
||||
- [Backup] Display the SR name label in the log even if the SR is not currently connected
|
||||
- [Import VM] Ability to import multiple VMs from ESXi (PR [#6718](https://github.com/vatesfr/xen-orchestra/pull/6718))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
||||
@@ -2616,7 +2616,7 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Comenzando export…',
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
// Original text: 'Number of CPUs'
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: 'Memory'
|
||||
|
||||
@@ -2675,8 +2675,8 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: "L'export commence…",
|
||||
|
||||
// Original text: "N CPUs"
|
||||
nCpus: 'N CPUs',
|
||||
// Original text: "Number of CPUs"
|
||||
nCpus: 'Nombre de CPUs',
|
||||
|
||||
// Original text: "Memory"
|
||||
vmMemory: 'Mémoire',
|
||||
|
||||
@@ -2294,7 +2294,7 @@ export default {
|
||||
// Original text: 'Export starting…'
|
||||
startVmExport: undefined,
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
// Original text: 'Number of CPUs'
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: 'Memory'
|
||||
|
||||
@@ -2508,8 +2508,8 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Exportálás indul…',
|
||||
|
||||
// Original text: "N CPUs"
|
||||
nCpus: 'N CPUs',
|
||||
// Original text: "Number of CPUs"
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: "Memory"
|
||||
vmMemory: 'Memória',
|
||||
|
||||
@@ -3779,8 +3779,8 @@ export default {
|
||||
// Original text: 'VDI export starting…'
|
||||
startVdiExport: "Inizio dell'esportazione VDI…",
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
nCpus: 'N CPUs',
|
||||
// Original text: 'Number of CPUs'
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: 'Memory'
|
||||
vmMemory: 'Memoria',
|
||||
|
||||
@@ -2298,8 +2298,8 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Eksport rozpoczęty…',
|
||||
|
||||
// Original text: "N CPUs"
|
||||
nCpus: 'N CPUs',
|
||||
// Original text: "Number of CPUs"
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: "Memory"
|
||||
vmMemory: 'Pamieć',
|
||||
|
||||
@@ -2297,7 +2297,7 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Iniciando exportação…',
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
// Original text: 'Number of CPUs'
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: 'Memory'
|
||||
|
||||
@@ -2618,7 +2618,7 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Начало экспорта…',
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
// Original text: 'Number of CPUs'
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: 'Memory'
|
||||
|
||||
@@ -3244,8 +3244,8 @@ export default {
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Dışa aktarma başlatılıyor...',
|
||||
|
||||
// Original text: "N CPUs"
|
||||
nCpus: 'N CPU',
|
||||
// Original text: 'Number of CPUs'
|
||||
nCpus: undefined,
|
||||
|
||||
// Original text: "Memory"
|
||||
vmMemory: 'Bellek',
|
||||
|
||||
@@ -19,7 +19,9 @@ const messages = {
|
||||
esxiImportStopSource: 'Stop the source VM',
|
||||
esxiImportStopSourceDescription:
|
||||
'Source VM stopped before the last delta transfer (after final snapshot). Needed to fully transfer a running VM',
|
||||
|
||||
esxiImportStopOnErrorDescription: 'Stop on the first error when importing VMs',
|
||||
nImportVmsInParallel: 'Number of VMs to import in parallel',
|
||||
stopOnError: 'Stop on error',
|
||||
vmSrUsage: 'Storage: {used} used of {total} ({free} free)',
|
||||
|
||||
notDefined: 'Not defined',
|
||||
@@ -1645,7 +1647,7 @@ const messages = {
|
||||
startVdiImport: 'VDI import starting…',
|
||||
startVmExport: 'Export starting…',
|
||||
startVdiExport: 'VDI export starting…',
|
||||
nCpus: 'N CPUs',
|
||||
nCpus: 'Number of CPUs',
|
||||
vmMemory: 'Memory',
|
||||
diskInfo: 'Disk {position} ({capacity})',
|
||||
diskDescription: 'Disk description',
|
||||
|
||||
@@ -3498,4 +3498,4 @@ export const synchronizeNetbox = pools =>
|
||||
export const esxiListVms = (host, user, password, sslVerify) =>
|
||||
_call('esxi.listVms', { host, user, password, sslVerify })
|
||||
|
||||
export const importVmFromEsxi = params => _call('vm.importFromEsxi', params)
|
||||
export const importVmsFromEsxi = params => _call('vm.importMultipleFromEsxi', params)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import Collapse from 'collapse'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { connectStore, resolveId } from 'utils'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { esxiListVms, importVmFromEsxi, isSrWritable } from 'xo'
|
||||
import { esxiListVms, importVmsFromEsxi, isSrWritable } from 'xo'
|
||||
import { find, isEmpty, keyBy, map, pick } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
@@ -14,8 +15,9 @@ import { Password, Select, Toggle } from 'form'
|
||||
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import VmData from './vm-data'
|
||||
import { getRedirectionUrl } from '../utils'
|
||||
|
||||
const getRedirectUrl = vmId => `/vms/${vmId}`
|
||||
const N_IMPORT_VMS_IN_PARALLEL = 2
|
||||
|
||||
@injectIntl
|
||||
@connectStore({
|
||||
@@ -24,12 +26,14 @@ const getRedirectUrl = vmId => `/vms/${vmId}`
|
||||
})
|
||||
class EsxiImport extends Component {
|
||||
state = {
|
||||
skipSslVerify: false,
|
||||
thin: false,
|
||||
stopSource: false,
|
||||
concurrency: N_IMPORT_VMS_IN_PARALLEL,
|
||||
hostIp: '',
|
||||
isConnected: false,
|
||||
password: '',
|
||||
thin: false,
|
||||
skipSslVerify: false,
|
||||
stopSource: false,
|
||||
stopOnError: true,
|
||||
user: '',
|
||||
}
|
||||
|
||||
@@ -60,18 +64,21 @@ class EsxiImport extends Component {
|
||||
poolId => (poolId === undefined ? undefined : sr => isSrWritable(sr) && sr.$poolId === poolId)
|
||||
)
|
||||
|
||||
_importVm = () => {
|
||||
const { hostIp, network, password, skipSslVerify, sr, stopSource, thin, user, vm } = this.state
|
||||
return importVmFromEsxi({
|
||||
_importVms = () => {
|
||||
const { concurrency, hostIp, network, password, skipSslVerify, sr, stopSource, stopOnError, thin, user, vms } =
|
||||
this.state
|
||||
return importVmsFromEsxi({
|
||||
concurrency: +concurrency,
|
||||
host: hostIp,
|
||||
network: network?.id ?? this._getDefaultNetwork(),
|
||||
password,
|
||||
sr: resolveId(sr),
|
||||
sslVerify: !skipSslVerify,
|
||||
stopOnError,
|
||||
stopSource,
|
||||
thin,
|
||||
user,
|
||||
vm: vm.value,
|
||||
vms: vms.map(vm => vm.value),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,8 +99,6 @@ class EsxiImport extends Component {
|
||||
_resetConnectForm = () => {
|
||||
this.setState({
|
||||
skipSslVerify: false,
|
||||
thin: false,
|
||||
stopSource: false,
|
||||
hostIp: '',
|
||||
isConnected: false,
|
||||
password: '',
|
||||
@@ -103,18 +108,21 @@ class EsxiImport extends Component {
|
||||
|
||||
_resetImportForm = () => {
|
||||
this.setState({
|
||||
concurrency: N_IMPORT_VMS_IN_PARALLEL,
|
||||
network: undefined,
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
vm: undefined,
|
||||
stopSource: false,
|
||||
stopOnError: true,
|
||||
thin: false,
|
||||
vms: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props
|
||||
const {
|
||||
thin,
|
||||
stopSource,
|
||||
concurrency,
|
||||
hostIp,
|
||||
isConnected,
|
||||
network = this._getDefaultNetwork(),
|
||||
@@ -122,8 +130,11 @@ class EsxiImport extends Component {
|
||||
pool,
|
||||
skipSslVerify,
|
||||
sr,
|
||||
stopSource,
|
||||
stopOnError,
|
||||
thin,
|
||||
user,
|
||||
vm,
|
||||
vms,
|
||||
vmsById,
|
||||
} = this.state
|
||||
|
||||
@@ -184,14 +195,26 @@ class EsxiImport extends Component {
|
||||
return (
|
||||
<form>
|
||||
<Row>
|
||||
<LabelCol>{_('vm')}</LabelCol>
|
||||
<LabelCol>{_('nImportVmsInParallel')}</LabelCol>
|
||||
<InputCol>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('concurrency')}
|
||||
type='number'
|
||||
value={concurrency}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vms')}</LabelCol>
|
||||
<InputCol>
|
||||
<Select
|
||||
disabled={isEmpty(vmsById)}
|
||||
onChange={this.linkState('vm')}
|
||||
multi
|
||||
onChange={this.linkState('vms')}
|
||||
options={this._getSelectVmOptions()}
|
||||
required
|
||||
value={vm}
|
||||
value={vms}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
@@ -239,21 +262,35 @@ class EsxiImport extends Component {
|
||||
<small className='form-text text-muted'>{_('esxiImportStopSourceDescription')}</small>
|
||||
</InputCol>
|
||||
</Row>
|
||||
{vm !== undefined && (
|
||||
<Row>
|
||||
<LabelCol>{_('stopOnError')}</LabelCol>
|
||||
<InputCol>
|
||||
<Toggle onChange={this.toggleState('stopOnError')} value={stopOnError} />
|
||||
<small className='form-text text-muted'>{_('esxiImportStopOnErrorDescription')}</small>
|
||||
</InputCol>
|
||||
</Row>
|
||||
|
||||
{!isEmpty(vms) && (
|
||||
<div>
|
||||
<hr />
|
||||
<h5>{_('vmsToImport', { nVms: 1 })}</h5>
|
||||
<VmData data={vmsById[vm.value]} />
|
||||
<h5>{_('vmsToImport', { nVms: vms.length })}</h5>
|
||||
{vms.map(vm => (
|
||||
<Collapse className='mt-1 mb-1' buttonText={vm.label} key={vm.value} size='small'>
|
||||
<div className='mt-1'>
|
||||
<VmData data={vmsById[vm.value]} />
|
||||
</div>
|
||||
</Collapse>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={vm === undefined}
|
||||
handler={this._importVm}
|
||||
disabled={isEmpty(vms)}
|
||||
handler={this._importVms}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectUrl}
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
|
||||
@@ -7,8 +7,8 @@ const VmData = ({ data }) => (
|
||||
<div>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>{_('keyValue', { key: _('vmNameLabel'), value: data.nameLabel })}</div>
|
||||
<div className='form-group'>
|
||||
<div>{_('keyValue', { key: _('vmNameLabel'), value: data.nameLabel })}</div>
|
||||
<div>
|
||||
{_('keyValue', {
|
||||
key: _('powerState'),
|
||||
value: data.powerState === 'poweredOn' ? _('powerStateRunning') : _('powerStateHalted'),
|
||||
@@ -16,16 +16,16 @@ const VmData = ({ data }) => (
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>{_('keyValue', { key: _('nCpus'), value: data.nCpus })}</div>
|
||||
<div className='form-group'>{_('keyValue', { key: _('vmMemory'), value: formatSize(data.memory) })}</div>
|
||||
<div>{_('keyValue', { key: _('nCpus'), value: data.nCpus })}</div>
|
||||
<div>{_('keyValue', { key: _('vmMemory'), value: formatSize(data.memory) })}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>{_('keyValue', { key: _('firmware'), value: data.firmware })}</div>
|
||||
<div>{_('keyValue', { key: _('firmware'), value: data.firmware })}</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<div>
|
||||
{_('keyValue', {
|
||||
key: _('guestToolStatus'),
|
||||
value: data.guestToolsInstalled ? _('noToolsInstalled') : _('toolsInstalled'),
|
||||
@@ -35,7 +35,7 @@ const VmData = ({ data }) => (
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={12}>
|
||||
<div className='form-group'>
|
||||
<div>
|
||||
<span>
|
||||
{_('vmSrUsage', {
|
||||
free: formatSize(data.storage.free),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
@@ -22,6 +21,7 @@ import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
import parseOvaFile from './ova'
|
||||
|
||||
import styles from './index.css'
|
||||
import { getRedirectionUrl } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -198,13 +198,6 @@ const parseFile = async (file, type, func) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getRedirectionUrl = vms =>
|
||||
vms.length === 0
|
||||
? undefined // no redirect
|
||||
: vms.length === 1
|
||||
? `/vms/${vms[0]}`
|
||||
: `/home?s=${encodeURIComponent(new CM.Property('id', new CM.Or(vms.map(_ => new CM.String(_)))).toString())}&t=VM`
|
||||
|
||||
export default class Import extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
13
packages/xo-web/src/xo-app/vm-import/utils.js
Normal file
13
packages/xo-web/src/xo-app/vm-import/utils.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import { resolveIds } from 'utils'
|
||||
|
||||
export const getRedirectionUrl = (vms = []) => {
|
||||
const vmIds = resolveIds(typeof vms === 'object' ? Object.values(vms) : vms)
|
||||
return vmIds.length === 0
|
||||
? undefined // no redirect
|
||||
: vmIds.length === 1
|
||||
? `/vms/${vmIds[0]}`
|
||||
: `/home?s=${encodeURIComponent(
|
||||
new CM.Property('id', new CM.Or(vmIds.map(vm => new CM.String(vm)))).toString()
|
||||
)}&t=VM`
|
||||
}
|
||||
Reference in New Issue
Block a user