Compare commits

...

14 Commits

Author SHA1 Message Date
Rajaa
76bdc05320 vmId instead vmData 2023-02-21 19:28:12 +01:00
Rajaa
49da3481a8 fix ova/xva import 2023-02-21 19:28:12 +01:00
Rajaa
1c3e879da8 more fixes & remove unnecessary changes 2023-02-21 19:28:12 +01:00
Rajaa
d82d640c3d fixes from feedbacks 2023-02-21 19:28:12 +01:00
Rajaa
ffff7e7762 fixes from feedbacks 2023-02-21 19:28:11 +01:00
Rajaa
4b218e5ee2 handle ESXI import 2023-02-21 19:28:11 +01:00
Rajaa
4917223c2d minor changes 2023-02-21 19:28:11 +01:00
Rajaa
a8d37f1a43 handle when pool is undefined 2023-02-21 19:28:11 +01:00
Rajaa
88970f3d27 refactor duplicate code 2023-02-21 19:28:11 +01:00
Rajaa
6bd6045b6a fixes and remove unnecessary changes 2023-02-21 19:28:11 +01:00
Rajaa
7c3464edd0 improvement code 2023-02-21 19:28:11 +01:00
Rajaa
ee8699893e improved code to increase readability 2023-02-21 19:28:11 +01:00
Rajaa
c1aca3e37a support selecting ESXI 2023-02-21 19:28:11 +01:00
Rajaa
05cc7c64b4 feat(xo-web/import/esxi): import VM from ESXI 2023-02-21 19:28:11 +01:00
20 changed files with 807 additions and 404 deletions

View File

@@ -2598,8 +2598,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'al SR:',
// Original text: "VMs to import"
vmsToImport: 'VMs para importar',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'VM{nVms, plural, one {} other {s}} para importar',
// Original text: "Reset"
importVmsCleanList: 'Reiniciar',

View File

@@ -2657,8 +2657,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'Sur le SR:',
// Original text: "VMs to import"
vmsToImport: 'VMs à importer',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'VM{nVms, plural, one {} other {s}} à importer',
// Original text: "Reset"
importVmsCleanList: 'Réinitialiser',

View File

@@ -2276,7 +2276,7 @@ export default {
// Original text: 'To SR:'
vmImportToSr: undefined,
// Original text: 'VMs to import'
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
vmsToImport: undefined,
// Original text: 'Reset'

View File

@@ -2490,7 +2490,7 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'Adattárolóra:',
// Original text: "VMs to import"
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'Importálandó VPS-el',
// Original text: "Reset"

View File

@@ -3746,8 +3746,8 @@ export default {
// Original text: 'To SR:'
vmImportToSr: 'Per SR:',
// Original text: 'VMs to import'
vmsToImport: 'VMs da importare',
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
vmsToImport: 'VM{nVms, plural, one {} other {s}} da importare',
// Original text: 'Reset'
importVmsCleanList: 'Ripristina',

View File

@@ -2280,8 +2280,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'To SR:',
// Original text: "VMs to import"
vmsToImport: 'VMs to import',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
// Original text: "Reset"
importVmsCleanList: 'Reset',

View File

@@ -2279,8 +2279,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'Enviar para SR:',
// Original text: "VMs to import"
vmsToImport: 'Importar VMs',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'Importar VM{nVms, plural, one {} other {s}} ',
// Original text: "Reset"
importVmsCleanList: 'Reiniciar',

View File

@@ -2600,7 +2600,7 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'В SR:',
// Original text: "VMs to import"
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'ВМ для импорта',
// Original text: "Reset"

View File

@@ -3223,7 +3223,7 @@ export default {
// Original text: "To 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",
// Original text: "Reset"

View File

@@ -1739,7 +1739,7 @@ export default {
// Original text: "To SR:"
vmImportToSr: '到存储库',
// Original text: "VMs to import"
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: '导入虚拟机',
// Original text: "Reset"

View File

@@ -9,7 +9,10 @@ const messages = {
description: 'Description',
deleteSourceVm: 'Delete source VM',
expiration: 'Expiration',
hostIp: 'Host IP',
keyValue: '{key}: {value}',
sslCertificate: 'SSL certificate',
vmSrUsage: '{used} used of {total} ({free} free)',
notDefined: 'Not defined',
statusConnecting: 'Connecting',
@@ -1609,12 +1612,13 @@ const messages = {
// ---- VM import ---
fileType: 'File type:',
fromUrl: 'From URL',
fromVmware: 'From VMware',
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',
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
importVmsCleanList: 'Reset',
vmImportSuccess: 'VM import success',
vmImportFailed: 'VM import failed',

View File

@@ -3468,3 +3468,10 @@ export const synchronizeNetbox = pools =>
body: _('syncNetboxWarning'),
icon: 'refresh',
}).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)

View File

@@ -7,6 +7,7 @@ import { NavLink, NavTabs } from 'nav'
import { routes } from 'utils'
import DiskImport from '../disk-import'
import EsxiImport from '../vm-import/esxi-import'
import VmImport from '../vm-import'
const HEADER = (
@@ -25,6 +26,9 @@ const HEADER = (
<NavLink to='/import/disk'>
<Icon icon='disk' /> {_('labelDisk')}
</NavLink>
<NavLink to='/import/vmware'>
<Icon icon='vm' /> {_('fromVmware')}
</NavLink>
</NavTabs>
</Col>
</Row>
@@ -34,6 +38,7 @@ const HEADER = (
const Import = routes('vm', {
disk: DiskImport,
vm: VmImport,
vmware: EsxiImport,
})(({ children }) => (
<Page header={HEADER} title='newImport' formatTitle>
{children}

View File

@@ -494,6 +494,11 @@ export default class Menu extends Component {
icon: 'disk',
label: 'labelDisk',
},
{
to: '/import/vmware',
icon: 'vm',
label: 'fromVmware',
},
],
},
!(noOperatablePools && noResourceSets) && {

View 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

View File

@@ -1,302 +1,29 @@
import * as CM from 'complex-matcher'
import * as FormGrid from 'form-grid'
import _ from 'intl'
import ActionButton from 'action-button'
import Button from 'button'
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 { Container, Col, Row } from 'grid'
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 { Container } from 'grid'
import { Toggle } from 'form'
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
import parseOvaFile from './ova'
import styles from './index.css'
import XvaImport from './xva-import'
import UrlImport from './url-import'
// ===================================================================
const FILE_TYPES = [
{
label: 'XVA',
value: 'xva',
},
]
const FORMAT_TO_HANDLER = {
ova: parseOvaFile,
xva: noop,
const RENDER_BY_TYPE = {
xva: <XvaImport />,
url: <UrlImport />,
}
// ===================================================================
@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 {
constructor(props) {
super(props)
this.state = {
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() {
const { isFromUrl, pool, sr, srPredicate, type, url, vms } = this.state
const { isFromUrl } = this.state
return (
<Container>
@@ -304,113 +31,7 @@ export default class Import extends Component {
<p>
<Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}
</p>
<FormGrid.Row>
<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>
))}
{RENDER_BY_TYPE[isFromUrl ? 'url' : 'xva']}
</form>
</Container>
)

View 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

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

View 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

View 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