feat(xo-web/import): ability to import multiple VMs from VMware (#6718)

See #6708
This commit is contained in:
rajaa-b
2023-03-29 14:35:45 +02:00
committed by GitHub
parent 8a99326a76
commit f237101b4a
16 changed files with 102 additions and 56 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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',

View File

@@ -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'

View File

@@ -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',

View File

@@ -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',

View File

@@ -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ć',

View File

@@ -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'

View File

@@ -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'

View File

@@ -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',

View File

@@ -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',

View File

@@ -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)

View File

@@ -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')}

View File

@@ -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),

View File

@@ -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)

View 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`
}