Compare commits
14 Commits
feat_regis
...
fix-refs-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76bdc05320 | ||
|
|
49da3481a8 | ||
|
|
1c3e879da8 | ||
|
|
d82d640c3d | ||
|
|
ffff7e7762 | ||
|
|
4b218e5ee2 | ||
|
|
4917223c2d | ||
|
|
a8d37f1a43 | ||
|
|
88970f3d27 | ||
|
|
6bd6045b6a | ||
|
|
7c3464edd0 | ||
|
|
ee8699893e | ||
|
|
c1aca3e37a | ||
|
|
05cc7c64b4 |
@@ -2598,8 +2598,8 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'al SR:',
|
vmImportToSr: 'al SR:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: 'VMs para importar',
|
vmsToImport: 'VM{nVms, plural, one {} other {s}} para importar',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
importVmsCleanList: 'Reiniciar',
|
importVmsCleanList: 'Reiniciar',
|
||||||
|
|||||||
@@ -2657,8 +2657,8 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'Sur le SR:',
|
vmImportToSr: 'Sur le SR:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: 'VMs à importer',
|
vmsToImport: 'VM{nVms, plural, one {} other {s}} à importer',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
importVmsCleanList: 'Réinitialiser',
|
importVmsCleanList: 'Réinitialiser',
|
||||||
|
|||||||
@@ -2276,7 +2276,7 @@ export default {
|
|||||||
// Original text: 'To SR:'
|
// Original text: 'To SR:'
|
||||||
vmImportToSr: undefined,
|
vmImportToSr: undefined,
|
||||||
|
|
||||||
// Original text: 'VMs to import'
|
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
|
||||||
vmsToImport: undefined,
|
vmsToImport: undefined,
|
||||||
|
|
||||||
// Original text: 'Reset'
|
// Original text: 'Reset'
|
||||||
|
|||||||
@@ -2490,7 +2490,7 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'Adattárolóra:',
|
vmImportToSr: 'Adattárolóra:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: 'Importálandó VPS-el',
|
vmsToImport: 'Importálandó VPS-el',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
|
|||||||
@@ -3746,8 +3746,8 @@ export default {
|
|||||||
// Original text: 'To SR:'
|
// Original text: 'To SR:'
|
||||||
vmImportToSr: 'Per SR:',
|
vmImportToSr: 'Per SR:',
|
||||||
|
|
||||||
// Original text: 'VMs to import'
|
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
|
||||||
vmsToImport: 'VMs da importare',
|
vmsToImport: 'VM{nVms, plural, one {} other {s}} da importare',
|
||||||
|
|
||||||
// Original text: 'Reset'
|
// Original text: 'Reset'
|
||||||
importVmsCleanList: 'Ripristina',
|
importVmsCleanList: 'Ripristina',
|
||||||
|
|||||||
@@ -2280,8 +2280,8 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'To SR:',
|
vmImportToSr: 'To SR:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: 'VMs to import',
|
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
importVmsCleanList: 'Reset',
|
importVmsCleanList: 'Reset',
|
||||||
|
|||||||
@@ -2279,8 +2279,8 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'Enviar para SR:',
|
vmImportToSr: 'Enviar para SR:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: 'Importar VMs',
|
vmsToImport: 'Importar VM{nVms, plural, one {} other {s}} ',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
importVmsCleanList: 'Reiniciar',
|
importVmsCleanList: 'Reiniciar',
|
||||||
|
|||||||
@@ -2600,7 +2600,7 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'В SR:',
|
vmImportToSr: 'В SR:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: 'ВМ для импорта',
|
vmsToImport: 'ВМ для импорта',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
|
|||||||
@@ -3223,7 +3223,7 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: 'SR:',
|
vmImportToSr: 'SR:',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: "içe aktarılacak VM'ler",
|
vmsToImport: "içe aktarılacak VM'ler",
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
|
|||||||
@@ -1739,7 +1739,7 @@ export default {
|
|||||||
// Original text: "To SR:"
|
// Original text: "To SR:"
|
||||||
vmImportToSr: '到存储库',
|
vmImportToSr: '到存储库',
|
||||||
|
|
||||||
// Original text: "VMs to import"
|
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||||
vmsToImport: '导入虚拟机',
|
vmsToImport: '导入虚拟机',
|
||||||
|
|
||||||
// Original text: "Reset"
|
// Original text: "Reset"
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ const messages = {
|
|||||||
description: 'Description',
|
description: 'Description',
|
||||||
deleteSourceVm: 'Delete source VM',
|
deleteSourceVm: 'Delete source VM',
|
||||||
expiration: 'Expiration',
|
expiration: 'Expiration',
|
||||||
|
hostIp: 'Host IP',
|
||||||
keyValue: '{key}: {value}',
|
keyValue: '{key}: {value}',
|
||||||
|
sslCertificate: 'SSL certificate',
|
||||||
|
vmSrUsage: '{used} used of {total} ({free} free)',
|
||||||
|
|
||||||
notDefined: 'Not defined',
|
notDefined: 'Not defined',
|
||||||
statusConnecting: 'Connecting',
|
statusConnecting: 'Connecting',
|
||||||
@@ -1609,12 +1612,13 @@ const messages = {
|
|||||||
// ---- VM import ---
|
// ---- VM import ---
|
||||||
fileType: 'File type:',
|
fileType: 'File type:',
|
||||||
fromUrl: 'From URL',
|
fromUrl: 'From URL',
|
||||||
|
fromVmware: 'From VMware',
|
||||||
importVmsList: 'Drop OVA or XVA files here to import Virtual Machines.',
|
importVmsList: 'Drop OVA or XVA files here to import Virtual Machines.',
|
||||||
noSelectedVms: 'No selected VMs.',
|
noSelectedVms: 'No selected VMs.',
|
||||||
url: 'URL:',
|
url: 'URL:',
|
||||||
vmImportToPool: 'To Pool:',
|
vmImportToPool: 'To Pool:',
|
||||||
vmImportToSr: 'To SR:',
|
vmImportToSr: 'To SR:',
|
||||||
vmsToImport: 'VMs to import',
|
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
|
||||||
importVmsCleanList: 'Reset',
|
importVmsCleanList: 'Reset',
|
||||||
vmImportSuccess: 'VM import success',
|
vmImportSuccess: 'VM import success',
|
||||||
vmImportFailed: 'VM import failed',
|
vmImportFailed: 'VM import failed',
|
||||||
|
|||||||
@@ -3468,3 +3468,10 @@ export const synchronizeNetbox = pools =>
|
|||||||
body: _('syncNetboxWarning'),
|
body: _('syncNetboxWarning'),
|
||||||
icon: 'refresh',
|
icon: 'refresh',
|
||||||
}).then(() => _call('netbox.synchronize', { pools: resolveIds(pools) }))
|
}).then(() => _call('netbox.synchronize', { pools: resolveIds(pools) }))
|
||||||
|
|
||||||
|
// ESXi import ---------------------------------------------------------------
|
||||||
|
|
||||||
|
export const esxiConnect = (host, user, password, sslVerify) =>
|
||||||
|
_call('esxi.connect', { host, user, password, sslVerify })
|
||||||
|
|
||||||
|
export const importVmFromEsxi = params => _call('vm.importFromEsxi', params)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { NavLink, NavTabs } from 'nav'
|
|||||||
import { routes } from 'utils'
|
import { routes } from 'utils'
|
||||||
|
|
||||||
import DiskImport from '../disk-import'
|
import DiskImport from '../disk-import'
|
||||||
|
import EsxiImport from '../vm-import/esxi-import'
|
||||||
import VmImport from '../vm-import'
|
import VmImport from '../vm-import'
|
||||||
|
|
||||||
const HEADER = (
|
const HEADER = (
|
||||||
@@ -25,6 +26,9 @@ const HEADER = (
|
|||||||
<NavLink to='/import/disk'>
|
<NavLink to='/import/disk'>
|
||||||
<Icon icon='disk' /> {_('labelDisk')}
|
<Icon icon='disk' /> {_('labelDisk')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to='/import/vmware'>
|
||||||
|
<Icon icon='vm' /> {_('fromVmware')}
|
||||||
|
</NavLink>
|
||||||
</NavTabs>
|
</NavTabs>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -34,6 +38,7 @@ const HEADER = (
|
|||||||
const Import = routes('vm', {
|
const Import = routes('vm', {
|
||||||
disk: DiskImport,
|
disk: DiskImport,
|
||||||
vm: VmImport,
|
vm: VmImport,
|
||||||
|
vmware: EsxiImport,
|
||||||
})(({ children }) => (
|
})(({ children }) => (
|
||||||
<Page header={HEADER} title='newImport' formatTitle>
|
<Page header={HEADER} title='newImport' formatTitle>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -494,6 +494,11 @@ export default class Menu extends Component {
|
|||||||
icon: 'disk',
|
icon: 'disk',
|
||||||
label: 'labelDisk',
|
label: 'labelDisk',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: '/import/vmware',
|
||||||
|
icon: 'vm',
|
||||||
|
label: 'fromVmware',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
!(noOperatablePools && noResourceSets) && {
|
!(noOperatablePools && noResourceSets) && {
|
||||||
|
|||||||
251
packages/xo-web/src/xo-app/vm-import/esxi-import.js
Normal file
251
packages/xo-web/src/xo-app/vm-import/esxi-import.js
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import _, { messages } from 'intl'
|
||||||
|
import ActionButton from 'action-button'
|
||||||
|
import Button from 'button'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import React from 'react'
|
||||||
|
import { esxiConnect, importVmFromEsxi, isSrWritable } from 'xo'
|
||||||
|
import { injectIntl } from 'react-intl'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
import { Input } from 'debounce-input-decorator'
|
||||||
|
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||||
|
import { isEmpty, map } from 'lodash'
|
||||||
|
import { linkState } from 'reaclette-utils'
|
||||||
|
import { Password, Select } from 'form'
|
||||||
|
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||||
|
|
||||||
|
import VmData from './vm-data'
|
||||||
|
|
||||||
|
const getInitialState = () => ({
|
||||||
|
hasCertificate: true,
|
||||||
|
hostIp: '',
|
||||||
|
isConnected: false,
|
||||||
|
network: undefined,
|
||||||
|
password: '',
|
||||||
|
pool: undefined,
|
||||||
|
sr: undefined,
|
||||||
|
user: '',
|
||||||
|
vm: undefined,
|
||||||
|
vmData: undefined,
|
||||||
|
vmsById: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const EsxiImport = decorate([
|
||||||
|
provideState({
|
||||||
|
initialState: getInitialState,
|
||||||
|
effects: {
|
||||||
|
importVm:
|
||||||
|
() =>
|
||||||
|
({ hasCertificate, hostIp, network, password, sr, user, vmData }) => {
|
||||||
|
importVmFromEsxi({
|
||||||
|
host: hostIp,
|
||||||
|
network: network.id,
|
||||||
|
password,
|
||||||
|
sr,
|
||||||
|
sslVerify: hasCertificate,
|
||||||
|
user,
|
||||||
|
vm: vmData.id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
connect:
|
||||||
|
() =>
|
||||||
|
async ({ hostIp, hasCertificate, password, user }) => {
|
||||||
|
const vms = await esxiConnect(hostIp, user, password, hasCertificate)
|
||||||
|
return { isConnected: true, vmsById: vms.reduce((vms, vm) => ({ ...vms, [vm.id]: vm }), {}) }
|
||||||
|
},
|
||||||
|
linkState,
|
||||||
|
onChangeVm: (_, vm) => state => ({ vm, vmData: state.vmsById[vm.value] }),
|
||||||
|
onChangeVmData: (_, vmData) => ({ vmData }),
|
||||||
|
onChangeNetwork: (_, network) => ({ network }),
|
||||||
|
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||||
|
onChangeSr: (_, sr) => ({ sr }),
|
||||||
|
toggleCertificateCheck:
|
||||||
|
(_, { target: { checked, name } }) =>
|
||||||
|
state => ({
|
||||||
|
...state,
|
||||||
|
[name]: checked,
|
||||||
|
}),
|
||||||
|
reset: getInitialState,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectVmOptions: ({ vmsById }) =>
|
||||||
|
map(vmsById, vm => ({
|
||||||
|
label: vm.nameLabel,
|
||||||
|
value: vm.id,
|
||||||
|
})),
|
||||||
|
networkpredicate:
|
||||||
|
({ pool }) =>
|
||||||
|
network =>
|
||||||
|
network.$poolId === pool?.uuid,
|
||||||
|
srPredicate:
|
||||||
|
({ pool }) =>
|
||||||
|
sr =>
|
||||||
|
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectIntl,
|
||||||
|
injectState,
|
||||||
|
({
|
||||||
|
effects: {
|
||||||
|
connect,
|
||||||
|
importVm,
|
||||||
|
linkState,
|
||||||
|
onChangeVm,
|
||||||
|
onChangeVmData,
|
||||||
|
onChangeNetwork,
|
||||||
|
onChangePool,
|
||||||
|
onChangeSr,
|
||||||
|
reset,
|
||||||
|
srPredicate,
|
||||||
|
toggleCertificateCheck,
|
||||||
|
},
|
||||||
|
intl: { formatMessage },
|
||||||
|
state: {
|
||||||
|
hasCertificate,
|
||||||
|
hostIp,
|
||||||
|
isConnected,
|
||||||
|
network,
|
||||||
|
networkPredicate,
|
||||||
|
password,
|
||||||
|
pool,
|
||||||
|
selectVmOptions,
|
||||||
|
sr,
|
||||||
|
user,
|
||||||
|
vm,
|
||||||
|
vmsById,
|
||||||
|
vmData,
|
||||||
|
},
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
{!isConnected && (
|
||||||
|
<form id='esxi-connect-form'>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('hostIp')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<Input
|
||||||
|
className='form-control'
|
||||||
|
name='hostIp'
|
||||||
|
onChange={linkState}
|
||||||
|
placeholder='192.168.2.20'
|
||||||
|
required
|
||||||
|
value={hostIp}
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('user')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<Input
|
||||||
|
className='form-control'
|
||||||
|
name='user'
|
||||||
|
onChange={linkState}
|
||||||
|
placeholder={formatMessage(messages.user)}
|
||||||
|
required
|
||||||
|
value={user}
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('password')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<Password
|
||||||
|
name='password'
|
||||||
|
onChange={linkState}
|
||||||
|
placeholder={formatMessage(messages.password)}
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('sslCertificate')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<input
|
||||||
|
checked={hasCertificate}
|
||||||
|
name='hasCertificate'
|
||||||
|
onChange={toggleCertificateCheck}
|
||||||
|
type='checkbox'
|
||||||
|
value={hasCertificate}
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<div className='form-group pull-right'>
|
||||||
|
<ActionButton
|
||||||
|
btnStyle='primary'
|
||||||
|
className='mr-1'
|
||||||
|
form='esxi-connect-form'
|
||||||
|
handler={connect}
|
||||||
|
icon='connect'
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
{_('serverConnect')}
|
||||||
|
</ActionButton>
|
||||||
|
<Button onClick={reset}>{_('formReset')}</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{isConnected && (
|
||||||
|
<form id='esxi-migrate-form'>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vm')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<Select disabled={isEmpty(vmsById)} onChange={onChangeVm} options={selectVmOptions} required value={vm} />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectPool onChange={onChangePool} required value={pool} />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectSr
|
||||||
|
disabled={pool === undefined}
|
||||||
|
onChange={onChangeSr}
|
||||||
|
predicate={srPredicate}
|
||||||
|
required
|
||||||
|
value={sr}
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('network')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectNetwork
|
||||||
|
disabled={pool === undefined}
|
||||||
|
onChange={onChangeNetwork}
|
||||||
|
predicate={networkPredicate}
|
||||||
|
required
|
||||||
|
value={network}
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
{vm !== undefined && (
|
||||||
|
<div>
|
||||||
|
<hr />
|
||||||
|
<h5>{_('vmsToImport', { nVms: 1 })}</h5>
|
||||||
|
<VmData data={vmData} onChange={onChangeVmData} pool={pool} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='form-group pull-right'>
|
||||||
|
<ActionButton
|
||||||
|
btnStyle='primary'
|
||||||
|
className='mr-1'
|
||||||
|
disabled={vm === undefined}
|
||||||
|
form='esxi-migrate-form'
|
||||||
|
handler={importVm}
|
||||||
|
icon='import'
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
{_('newImport')}
|
||||||
|
</ActionButton>
|
||||||
|
<Button onClick={reset}>{_('formReset')}</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
export default EsxiImport
|
||||||
@@ -1,302 +1,29 @@
|
|||||||
import * as CM from 'complex-matcher'
|
|
||||||
import * as FormGrid from 'form-grid'
|
|
||||||
import _ from 'intl'
|
import _ from 'intl'
|
||||||
import ActionButton from 'action-button'
|
|
||||||
import Button from 'button'
|
|
||||||
import Component from 'base-component'
|
import Component from 'base-component'
|
||||||
import Dropzone from 'dropzone'
|
|
||||||
import isEmpty from 'lodash/isEmpty'
|
|
||||||
import map from 'lodash/map'
|
|
||||||
import orderBy from 'lodash/orderBy'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Container, Col, Row } from 'grid'
|
import { Container } from 'grid'
|
||||||
import { importVm, importVms, isSrWritable } from 'xo'
|
import { Toggle } from 'form'
|
||||||
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 XvaImport from './xva-import'
|
||||||
|
import UrlImport from './url-import'
|
||||||
import parseOvaFile from './ova'
|
|
||||||
|
|
||||||
import styles from './index.css'
|
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
const FILE_TYPES = [
|
const RENDER_BY_TYPE = {
|
||||||
{
|
xva: <XvaImport />,
|
||||||
label: 'XVA',
|
url: <UrlImport />,
|
||||||
value: 'xva',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const FORMAT_TO_HANDLER = {
|
|
||||||
ova: parseOvaFile,
|
|
||||||
xva: noop,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================================================================
|
|
||||||
|
|
||||||
@connectStore(
|
|
||||||
() => {
|
|
||||||
const getHostMaster = createGetObject((_, props) => props.pool.master)
|
|
||||||
const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)
|
|
||||||
const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)
|
|
||||||
|
|
||||||
return {
|
|
||||||
defaultNetwork: getDefaultNetworkId,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ withRef: true }
|
|
||||||
)
|
|
||||||
class VmData extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
descriptionLabel: PropTypes.string,
|
|
||||||
disks: PropTypes.objectOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
capacity: PropTypes.number.isRequired,
|
|
||||||
descriptionLabel: PropTypes.string.isRequired,
|
|
||||||
nameLabel: PropTypes.string.isRequired,
|
|
||||||
path: PropTypes.string.isRequired,
|
|
||||||
compression: PropTypes.string,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
memory: PropTypes.number,
|
|
||||||
nameLabel: PropTypes.string,
|
|
||||||
nCpus: PropTypes.number,
|
|
||||||
networks: PropTypes.array,
|
|
||||||
pool: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
const { props, refs } = this
|
|
||||||
return {
|
|
||||||
descriptionLabel: refs.descriptionLabel.value,
|
|
||||||
disks: map(props.disks, ({ capacity, path, compression, position }, diskId) => ({
|
|
||||||
capacity,
|
|
||||||
descriptionLabel: refs[`disk-description-${diskId}`].value,
|
|
||||||
nameLabel: refs[`disk-name-${diskId}`].value,
|
|
||||||
path,
|
|
||||||
position,
|
|
||||||
compression,
|
|
||||||
})),
|
|
||||||
memory: +refs.memory.value,
|
|
||||||
nameLabel: refs.nameLabel.value,
|
|
||||||
networks: map(props.networks, (_, networkId) => {
|
|
||||||
const network = refs[`network-${networkId}`].value
|
|
||||||
return network.id ? network.id : network
|
|
||||||
}),
|
|
||||||
nCpus: +refs.nCpus.value,
|
|
||||||
tables: props.tables,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getNetworkPredicate = createSelector(
|
|
||||||
() => this.props.pool.id,
|
|
||||||
id => network => network.$pool === id
|
|
||||||
)
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { descriptionLabel, defaultNetwork, disks, memory, nameLabel, nCpus, networks } = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Col mediumSize={6}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label>{_('vmNameLabel')}</label>
|
|
||||||
<input className='form-control' ref='nameLabel' defaultValue={nameLabel} type='text' required />
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label>{_('vmNameDescription')}</label>
|
|
||||||
<input className='form-control' ref='descriptionLabel' defaultValue={descriptionLabel} type='text' />
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col mediumSize={6}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label>{_('nCpus')}</label>
|
|
||||||
<input className='form-control' ref='nCpus' defaultValue={nCpus} type='number' required />
|
|
||||||
</div>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label>{_('vmMemory')}</label>
|
|
||||||
<SizeInput defaultValue={memory} ref='memory' required />
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col mediumSize={6}>
|
|
||||||
{!isEmpty(disks)
|
|
||||||
? map(disks, (disk, diskId) => (
|
|
||||||
<Row key={diskId}>
|
|
||||||
<Col mediumSize={6}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label>
|
|
||||||
{_('diskInfo', {
|
|
||||||
position: `${disk.position}`,
|
|
||||||
capacity: formatSize(disk.capacity),
|
|
||||||
})}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className='form-control'
|
|
||||||
ref={`disk-name-${diskId}`}
|
|
||||||
defaultValue={disk.nameLabel}
|
|
||||||
type='text'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col mediumSize={6}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label>{_('diskDescription')}</label>
|
|
||||||
<input
|
|
||||||
className='form-control'
|
|
||||||
ref={`disk-description-${diskId}`}
|
|
||||||
defaultValue={disk.descriptionLabel}
|
|
||||||
type='text'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
))
|
|
||||||
: _('noDisks')}
|
|
||||||
</Col>
|
|
||||||
<Col mediumSize={6}>
|
|
||||||
{networks.length > 0
|
|
||||||
? map(networks, (name, networkId) => (
|
|
||||||
<div className='form-group' key={networkId}>
|
|
||||||
<label>{_('networkInfo', { name })}</label>
|
|
||||||
<SelectNetwork
|
|
||||||
defaultValue={defaultNetwork}
|
|
||||||
ref={`network-${networkId}`}
|
|
||||||
predicate={this._getNetworkPredicate()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: _('noNetworks')}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================
|
|
||||||
|
|
||||||
const parseFile = async (file, type, func) => {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
data: await func(file),
|
|
||||||
file,
|
|
||||||
type,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return { error, file, type }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
export default class Import extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
isFromUrl: false,
|
isFromUrl: false,
|
||||||
type: {
|
|
||||||
label: 'XVA',
|
|
||||||
value: 'xva',
|
|
||||||
},
|
|
||||||
url: '',
|
|
||||||
vms: [],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_import = () => {
|
|
||||||
const { state } = this
|
|
||||||
return importVms(
|
|
||||||
mapPlus(state.vms, (vm, push, vmIndex) => {
|
|
||||||
if (!vm.error) {
|
|
||||||
const ref = this.refs[`vm-data-${vmIndex}`]
|
|
||||||
push({
|
|
||||||
...vm,
|
|
||||||
data: ref && ref.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
state.sr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_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: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const vms = await Promise.all(
|
|
||||||
mapPlus(files, (file, push) => {
|
|
||||||
const { name } = file
|
|
||||||
const extIndex = name.lastIndexOf('.')
|
|
||||||
|
|
||||||
let func
|
|
||||||
let type
|
|
||||||
|
|
||||||
if (extIndex >= 0 && (type = name.slice(extIndex + 1).toLowerCase()) && (func = FORMAT_TO_HANDLER[type])) {
|
|
||||||
push(parseFile(file, type, func))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleCleanSelectedVms = () => {
|
|
||||||
this.setState({
|
|
||||||
vms: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleSelectedPool = pool => {
|
|
||||||
if (pool === '') {
|
|
||||||
this.setState({
|
|
||||||
pool: undefined,
|
|
||||||
sr: undefined,
|
|
||||||
srPredicate: undefined,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
pool,
|
|
||||||
sr: pool.default_SR,
|
|
||||||
srPredicate: sr => sr.$pool === this.state.pool.id && isSrWritable(sr),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleSelectedSr = sr => {
|
|
||||||
this.setState({
|
|
||||||
sr: sr === '' ? undefined : sr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isFromUrl, pool, sr, srPredicate, type, url, vms } = this.state
|
const { isFromUrl } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -304,113 +31,7 @@ export default class Import extends Component {
|
|||||||
<p>
|
<p>
|
||||||
<Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}
|
<Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}
|
||||||
</p>
|
</p>
|
||||||
<FormGrid.Row>
|
{RENDER_BY_TYPE[isFromUrl ? 'url' : 'xva']}
|
||||||
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
|
|
||||||
<FormGrid.InputCol>
|
|
||||||
<SelectPool value={pool} onChange={this._handleSelectedPool} required />
|
|
||||||
</FormGrid.InputCol>
|
|
||||||
</FormGrid.Row>
|
|
||||||
<FormGrid.Row>
|
|
||||||
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
|
|
||||||
<FormGrid.InputCol>
|
|
||||||
<SelectSr
|
|
||||||
disabled={!pool}
|
|
||||||
onChange={this._handleSelectedSr}
|
|
||||||
predicate={srPredicate}
|
|
||||||
required
|
|
||||||
value={sr}
|
|
||||||
/>
|
|
||||||
</FormGrid.InputCol>
|
|
||||||
</FormGrid.Row>
|
|
||||||
{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-danger' role='alert'>
|
|
||||||
<strong>{_('vmImportError')}</strong>{' '}
|
|
||||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
|
||||||
</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>
|
|
||||||
</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'
|
|
||||||
className='mr-1 mt-1'
|
|
||||||
disabled={isEmpty(url)}
|
|
||||||
form='import-form'
|
|
||||||
handler={this._importVmFromUrl}
|
|
||||||
icon='import'
|
|
||||||
redirectOnSuccess={getRedirectionUrl}
|
|
||||||
type='submit'
|
|
||||||
>
|
|
||||||
{_('newImport')}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</form>
|
</form>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
113
packages/xo-web/src/xo-app/vm-import/url-import.js
Normal file
113
packages/xo-web/src/xo-app/vm-import/url-import.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import _ from 'intl'
|
||||||
|
import ActionButton from 'action-button'
|
||||||
|
import Button from 'button'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import React from 'react'
|
||||||
|
import { importVm, isSrWritable } from 'xo'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
import { Input } from 'debounce-input-decorator'
|
||||||
|
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { linkState } from 'reaclette-utils'
|
||||||
|
import { Select } from 'form'
|
||||||
|
import { SelectPool, SelectSr } from 'select-objects'
|
||||||
|
|
||||||
|
import { getRedirectionUrl } from './utils'
|
||||||
|
|
||||||
|
const FILE_TYPES = [
|
||||||
|
{
|
||||||
|
label: 'XVA',
|
||||||
|
value: 'xva',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const getInitialState = () => ({
|
||||||
|
pool: undefined,
|
||||||
|
sr: undefined,
|
||||||
|
type: {
|
||||||
|
label: 'XVA',
|
||||||
|
value: 'xva',
|
||||||
|
},
|
||||||
|
url: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const UrlImport = decorate([
|
||||||
|
provideState({
|
||||||
|
initialState: getInitialState,
|
||||||
|
effects: {
|
||||||
|
handleImport() {
|
||||||
|
const { type, url } = this.state
|
||||||
|
const file = {
|
||||||
|
name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),
|
||||||
|
}
|
||||||
|
importVm(file, type.value, undefined, this.state.sr, url)
|
||||||
|
},
|
||||||
|
linkState,
|
||||||
|
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||||
|
onChangeSr: (_, sr) => ({ sr }),
|
||||||
|
reset: getInitialState,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
srPredicate:
|
||||||
|
({ pool }) =>
|
||||||
|
sr =>
|
||||||
|
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectState,
|
||||||
|
({
|
||||||
|
effects: { handleImport, linkState, onChangePool, onChangeSr, reset },
|
||||||
|
state: { pool, sr, srPredicate, type, url },
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectPool value={pool} onChange={onChangePool} required />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectSr disabled={pool === undefined} onChange={onChangeSr} predicate={srPredicate} required value={sr} />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('url')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<Input
|
||||||
|
className='form-control'
|
||||||
|
name='url'
|
||||||
|
onChange={linkState}
|
||||||
|
placeholder='https://my-company.net/vm.xva'
|
||||||
|
type='url'
|
||||||
|
/>
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('fileType')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<Select name='type' onChange={linkState} options={FILE_TYPES} required value={type} />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className='form-group pull-right'>
|
||||||
|
<ActionButton
|
||||||
|
btnStyle='primary'
|
||||||
|
className='mr-1'
|
||||||
|
disabled={isEmpty(url)}
|
||||||
|
form='import-form'
|
||||||
|
handler={handleImport}
|
||||||
|
icon='import'
|
||||||
|
redirectOnSuccess={getRedirectionUrl}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
{_('newImport')}
|
||||||
|
</ActionButton>
|
||||||
|
<Button onClick={reset}>{_('formReset')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
export default UrlImport
|
||||||
8
packages/xo-web/src/xo-app/vm-import/utils.js
Normal file
8
packages/xo-web/src/xo-app/vm-import/utils.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as CM from 'complex-matcher'
|
||||||
|
|
||||||
|
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`
|
||||||
187
packages/xo-web/src/xo-app/vm-import/vm-data.js
Normal file
187
packages/xo-web/src/xo-app/vm-import/vm-data.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import _ from 'intl'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import React from 'react'
|
||||||
|
import { Col, Row } from 'grid'
|
||||||
|
import { connectStore, formatSize } from 'utils'
|
||||||
|
import { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
import { map } from 'lodash'
|
||||||
|
import { SelectNetwork } from 'select-objects'
|
||||||
|
import { SizeInput } from 'form'
|
||||||
|
|
||||||
|
const VmData = decorate([
|
||||||
|
connectStore(() => {
|
||||||
|
const getHostMaster = createGetObject((_, props) => props.pool.master)
|
||||||
|
const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)
|
||||||
|
const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultNetwork: getDefaultNetworkId,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
provideState({
|
||||||
|
effects: {
|
||||||
|
onChangeDisks(_, { target: { name, value } }) {
|
||||||
|
const { onChange, data: prevValue } = this.props
|
||||||
|
// name: nameLabel-index or descriptionLabel-index
|
||||||
|
const data = name.split('-')
|
||||||
|
const index = data[1]
|
||||||
|
onChange({
|
||||||
|
...prevValue,
|
||||||
|
disks: {
|
||||||
|
...prevValue.disks,
|
||||||
|
[index]: { ...prevValue.disks[index], [data[0]]: value },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChangeMemory(_, memory) {
|
||||||
|
const { onChange, data: prevData } = this.props
|
||||||
|
onChange({
|
||||||
|
...prevData,
|
||||||
|
memory,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChangeNCpus(_, { target: { value } }) {
|
||||||
|
const { onChange, data: prevData } = this.props
|
||||||
|
onChange({
|
||||||
|
...prevData,
|
||||||
|
nCpus: +value,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChangeNetworks(_, network, networkIndex) {
|
||||||
|
const { onChange, data } = this.props
|
||||||
|
onChange({
|
||||||
|
...data,
|
||||||
|
networks: data.networks.map((prevNetwork, index) => (index === networkIndex ? network.id : prevNetwork)),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChangeValue(_, { target: { name, value } }) {
|
||||||
|
const { onChange, data } = this.props
|
||||||
|
onChange({
|
||||||
|
...data,
|
||||||
|
[name]: value,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
networkPredicate:
|
||||||
|
(_, { pool }) =>
|
||||||
|
network =>
|
||||||
|
pool.id === network.$pool,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectState,
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
defaultNetwork,
|
||||||
|
effects: { onChangeDisks, onChangeMemory, onChangeNCpus, onChangeNetworks, onChangeValue },
|
||||||
|
state: { networkPredicate },
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col mediumSize={6}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>{_('vmNameLabel')}</label>
|
||||||
|
<input
|
||||||
|
className='form-control'
|
||||||
|
name='nameLabel'
|
||||||
|
onChange={onChangeValue}
|
||||||
|
type='text'
|
||||||
|
required
|
||||||
|
value={data.nameLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>{_('vmNameDescription')}</label>
|
||||||
|
<input
|
||||||
|
className='form-control'
|
||||||
|
name='descriptionLabel'
|
||||||
|
onChange={onChangeValue}
|
||||||
|
type='text'
|
||||||
|
value={data.descriptionLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col mediumSize={6}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>{_('nCpus')}</label>
|
||||||
|
<input className='form-control' onChange={onChangeNCpus} type='number' required value={data.nCpus} />
|
||||||
|
</div>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>{_('vmMemory')}</label>
|
||||||
|
<SizeInput onChange={onChangeMemory} required value={data.memory} />
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col mediumSize={6}>
|
||||||
|
{map(data.disks, (disk, diskId) => (
|
||||||
|
<Row key={diskId}>
|
||||||
|
<Col mediumSize={6}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>
|
||||||
|
{_('diskInfo', {
|
||||||
|
position: `${disk.position}`,
|
||||||
|
capacity: formatSize(disk.capacity),
|
||||||
|
})}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className='form-control'
|
||||||
|
name={`nameLabel-${diskId}`}
|
||||||
|
onChange={onChangeDisks}
|
||||||
|
type='text'
|
||||||
|
required
|
||||||
|
value={disk.nameLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col mediumSize={6}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>{_('diskDescription')}</label>
|
||||||
|
<input
|
||||||
|
className='form-control'
|
||||||
|
name={`descriptionLabel-${diskId}`}
|
||||||
|
onChange={onChangeDisks}
|
||||||
|
type='text'
|
||||||
|
required
|
||||||
|
value={disk.descriptionLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
<Col mediumSize={6}>
|
||||||
|
{map(data.networks, (networkId, index) => (
|
||||||
|
<div className='form-group' key={networkId}>
|
||||||
|
<label>{_('networkInfo', { name: index + 1 })}</label>
|
||||||
|
<SelectNetwork
|
||||||
|
onChange={network => onChangeNetworks(network, index)}
|
||||||
|
predicate={networkPredicate}
|
||||||
|
value={data.networks[index] ?? defaultNetwork}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{data.storage !== undefined && (
|
||||||
|
<Row className='mt-1'>
|
||||||
|
<Col mediumSize={12}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>{_('homeSrPage')}:</label>{' '}
|
||||||
|
<span>
|
||||||
|
{_('vmSrUsage', {
|
||||||
|
free: formatSize(data.storage.free),
|
||||||
|
total: formatSize(data.storage.used + data.storage.free),
|
||||||
|
used: formatSize(data.storage.used),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
export default VmData
|
||||||
202
packages/xo-web/src/xo-app/vm-import/xva-import.js
Normal file
202
packages/xo-web/src/xo-app/vm-import/xva-import.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import _ from 'intl'
|
||||||
|
import ActionButton from 'action-button'
|
||||||
|
import Button from 'button'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import Dropzone from 'dropzone'
|
||||||
|
import React from 'react'
|
||||||
|
import { createGetObjectsOfType } from 'selectors'
|
||||||
|
import { connectStore, formatSize, mapPlus, noop } from 'utils'
|
||||||
|
import { importVms, isSrWritable } from 'xo'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||||
|
import { orderBy } from 'lodash'
|
||||||
|
import { SelectPool, SelectSr } from 'select-objects'
|
||||||
|
|
||||||
|
import parseOvaFile from './ova'
|
||||||
|
import styles from './index.css'
|
||||||
|
import VmData from './vm-data'
|
||||||
|
import { getRedirectionUrl } from './utils'
|
||||||
|
|
||||||
|
const FORMAT_TO_HANDLER = {
|
||||||
|
ova: parseOvaFile,
|
||||||
|
xva: noop,
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseFile = async (file, type, func) => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
data: await func(file),
|
||||||
|
file,
|
||||||
|
type,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return { error, file, type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialState = () => ({
|
||||||
|
pool: undefined,
|
||||||
|
sr: undefined,
|
||||||
|
vms: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const XvaImport = decorate([
|
||||||
|
connectStore(() => ({
|
||||||
|
networksByName: createGetObjectsOfType('network').groupBy('name_label'),
|
||||||
|
})),
|
||||||
|
provideState({
|
||||||
|
initialState: getInitialState,
|
||||||
|
effects: {
|
||||||
|
handleImport:
|
||||||
|
() =>
|
||||||
|
({ sr, vms }) => {
|
||||||
|
importVms(
|
||||||
|
mapPlus(vms, (vm, push) => {
|
||||||
|
if (!vm.error) {
|
||||||
|
const { data } = vm
|
||||||
|
push(
|
||||||
|
data === undefined
|
||||||
|
? { ...vm }
|
||||||
|
: {
|
||||||
|
...vm,
|
||||||
|
data: {
|
||||||
|
...vm.data,
|
||||||
|
disks: Object.values(vm.data.disks),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
sr
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||||
|
onChangeSr: (_, sr) => ({ sr }),
|
||||||
|
onChangeVmData: (_, data, vmIndex) => state => {
|
||||||
|
const vms = [...state.vms]
|
||||||
|
vms[vmIndex].data = data
|
||||||
|
return { vms }
|
||||||
|
},
|
||||||
|
onDrop: (_, files) => async (_, props) => {
|
||||||
|
const vms = (
|
||||||
|
await Promise.all(
|
||||||
|
mapPlus(files, (file, push) => {
|
||||||
|
const { name } = file
|
||||||
|
const extIndex = name.lastIndexOf('.')
|
||||||
|
|
||||||
|
let func
|
||||||
|
let type
|
||||||
|
|
||||||
|
if (
|
||||||
|
extIndex >= 0 &&
|
||||||
|
(type = name.slice(extIndex + 1).toLowerCase()) &&
|
||||||
|
(func = FORMAT_TO_HANDLER[type])
|
||||||
|
) {
|
||||||
|
push(parseFile(file, type, func))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).map(vm => {
|
||||||
|
const { data } = vm
|
||||||
|
return data === undefined
|
||||||
|
? vm
|
||||||
|
: {
|
||||||
|
...vm,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
networks: data.networks.map(name => props.networksByName[name][0].id),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset: getInitialState,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
srPredicate:
|
||||||
|
({ pool }) =>
|
||||||
|
sr =>
|
||||||
|
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectState,
|
||||||
|
({
|
||||||
|
effects: { handleImport, onChangePool, onChangeSr, onChangeVmData, onDrop, reset },
|
||||||
|
state: { pool, sr, srPredicate, vms },
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectPool value={pool} onChange={onChangePool} required />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||||
|
<InputCol>
|
||||||
|
<SelectSr disabled={pool === undefined} onChange={onChangeSr} predicate={srPredicate} required value={sr} />
|
||||||
|
</InputCol>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<Dropzone onDrop={onDrop} message={_('importVmsList')} />
|
||||||
|
<hr />
|
||||||
|
<h5>{_('vmsToImport', { nVms: vms.length })}</h5>
|
||||||
|
{vms.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
{vms.map(({ 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={data} onChange={data => onChangeVmData(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>
|
||||||
|
) : (
|
||||||
|
<p>{_('noSelectedVms')}</p>
|
||||||
|
)}
|
||||||
|
<hr />
|
||||||
|
<div className='form-group pull-right'>
|
||||||
|
<ActionButton
|
||||||
|
btnStyle='primary'
|
||||||
|
disabled={vms.length === 0}
|
||||||
|
className='mr-1'
|
||||||
|
form='import-form'
|
||||||
|
handler={handleImport}
|
||||||
|
icon='import'
|
||||||
|
redirectOnSuccess={getRedirectionUrl}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
{_('newImport')}
|
||||||
|
</ActionButton>
|
||||||
|
<Button onClick={reset}>{_('importVmsCleanList')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
export default XvaImport
|
||||||
Reference in New Issue
Block a user