import _, { messages } from 'intl' import ActionButton from 'action-button' import BaseComponent from 'base-component' import Button from 'button' import classNames from 'classnames' import defined, { get } from '@xen-orchestra/defined' import Icon from 'icon' import isIp from 'is-ip' import Link from 'link' import Page from '../page' import PropTypes from 'prop-types' import React from 'react' import renderXoItem from 'render-xo-item' import SelectBootFirmware from 'select-boot-firmware' import SelectCoresPerSocket from 'select-cores-per-socket' import store from 'store' import Tags from 'tags' import Tooltip from 'tooltip' import Wizard, { Section } from 'wizard' import { compileTemplate } from '@xen-orchestra/template' import { confirm } from 'modal' import { Container, Row, Col } from 'grid' import { injectIntl } from 'react-intl' import { AvailableTemplateVars, DEFAULT_CLOUD_CONFIG_TEMPLATE, DEFAULT_NETWORK_CONFIG_TEMPLATE, NetworkConfigInfo, } from 'cloud-config' import { Input as DebounceInput, Textarea as DebounceTextarea } from 'debounce-input-decorator' import { Limits } from 'usage' import { clamp, every, filter, find, forEach, includes, isEmpty, isEqual, join, map, size, slice, sum, sumBy, } from 'lodash' import { addSshKey, createVm, createVms, getCloudInitConfig, getPoolGuestSecureBootReadiness, isSrShared, subscribeCurrentUser, subscribeIpPools, subscribeResourceSets, XEN_DEFAULT_CPU_CAP, XEN_DEFAULT_CPU_WEIGHT, } from 'xo' import { SelectCloudConfig, SelectHost, SelectIp, SelectNetwork, SelectNetworkConfig, SelectPool, SelectResourceSet, SelectResourceSetIp, SelectResourceSetsNetwork, SelectResourceSetsSr, SelectResourceSetsVdi, SelectResourceSetsVmTemplate, SelectRole, SelectSr, SelectSshKey, SelectSubject, SelectVdi, SelectVgpuType, SelectVmTemplate, } from 'select-objects' import { SizeInput, Toggle } from 'form' import { addSubscriptions, connectStore, formatSize, generateReadableRandomString, resolveIds } from 'utils' import { createFilter, createFinder, createGetObject, createGetObjectsOfType, createSelector, getIsPoolAdmin, getResolvedResourceSets, getUser, } from 'selectors' import { CURRENT as XOA_PLAN, ENTERPRISE } from 'xoa-plans' import styles from './index.css' const MULTIPLICAND = 2 const NB_VMS_MIN = 2 const NB_VMS_MAX = 100 const ACL_LEVELS = { admin: 'danger', operator: 'primary', viewer: 'success', } /* eslint-disable camelcase */ const getObject = createGetObject((_, id) => id) // Sub-components const SectionContent = ({ column, children }) => (
{children}
) const LineItem = ({ children }) =>
{children}
const Item = ({ label, children, className }) => ( {label && ( {label}   )} {children} ) @addSubscriptions({ // eslint-disable-next-line standard/no-callback-literal ipPoolsConfigured: cb => subscribeIpPools(ipPools => cb(ipPools.length > 0)), }) @injectIntl class Vif extends BaseComponent { _getIpPoolPredicate = createSelector( () => this.props.vif, vif => ipPool => includes(ipPool.networks, vif.network) ) render() { const { intl: { formatMessage }, ipPoolsConfigured, networkPredicate, onChangeAddresses, onChangeMac, onChangeNetwork, onDelete, pool, resourceSet, vif, } = this.props return ( {pool ? ( ) : ( )} {ipPoolsConfigured && ( {pool ? ( ) : ( )} )} ) } } class AddAclsModal extends BaseComponent { get value() { return this.state } render() { const { action, subjects } = this.state return (
) } } // ============================================================================= const isVdiPresent = vdi => !vdi.missing @addSubscriptions({ resourceSets: subscribeResourceSets, user: subscribeCurrentUser, }) @connectStore(() => { const getIsAdmin = createSelector(getUser, user => user && user.permission === 'admin') const getNetworks = createGetObjectsOfType('network').sort() const getPool = createGetObject((_, props) => props.location.query.pool) const getPools = createGetObjectsOfType('pool') const getSrs = createGetObjectsOfType('SR') const getTemplate = createGetObject((_, props) => props.location.query.template) const getTemplates = createGetObjectsOfType('VM-template').sort() const getUserSshKeys = createSelector( (_, props) => { const user = props.user return user && user.preferences && user.preferences.sshKeys }, keys => keys ) const getHosts = createGetObjectsOfType('host') return (state, props) => ({ isAdmin: getIsAdmin(state, props), isPoolAdmin: getIsPoolAdmin(state, props), networks: getNetworks(state, props), pool: getPool(state, props), pools: getPools(state, props), resolvedResourceSets: getResolvedResourceSets( state, props, props.pool === undefined // to get objects as a self user ), srs: getSrs(state, props), template: getTemplate(state, props, props.pool === undefined), templates: getTemplates(state, props), userSshKeys: getUserSshKeys(state, props), hosts: getHosts(state, props), }) }) @injectIntl export default class NewVm extends BaseComponent { static contextTypes = { router: PropTypes.object, } constructor() { super() this._uniqueId = 0 // NewVm's form's state is stored in this.state.state instead of this.state // so it can be emptied easily with this.setState({ state: {} }) this.state = { state: {} } } componentDidMount() { this._reset(() => { const { template } = this.props if (template !== undefined) { this._initTemplate(this.props.template) } }) } async componentDidUpdate(prevProps) { const template = this.props.template if (get(() => prevProps.template.id) !== get(() => template.id)) { this._initTemplate(template) } // as an admin, the resource set is selected in the advanced settings and // `share` is initialized by `_setResourceSet`, it must not be overridden here if ( this.state.state.resourceSet === undefined && (!isEqual(prevProps.resourceSets, this.props.resourceSets) || prevProps.location.query.resourceSet !== this.props.location.query.resourceSet) ) { this._setState({ share: this._getResourceSet()?.shareByDefault ?? false, }) } const pool = this.props.pool if ( get(() => prevProps.pool.id) !== get(() => pool.id) || (pool === undefined && get(() => template.id) !== get(() => prevProps.template.id)) ) { const poolId = pool?.id ?? template?.$pool this.setState({ poolGuestSecurebootReadiness: poolId === undefined ? undefined : await getPoolGuestSecureBootReadiness(poolId), }) } } _getResourceSet = createFinder( () => this.props.resourceSets, createSelector( () => this.props.location.query.resourceSet, resourceSetId => resourceSet => (resourceSet !== undefined ? resourceSetId === resourceSet.id : undefined) ) ) _getResolvedResourceSet = createFinder( () => this.props.resolvedResourceSets, createSelector(this._getResourceSet, resourceSet => resourceSet !== undefined ? resolvedResourceSet => resolvedResourceSet.id === resourceSet.id : false ) ) // Utils ----------------------------------------------------------------------- get _isDiskTemplate() { const { template } = this.props return template && template.$VBDs.length !== 0 && template.name_label !== 'Other install media' } _setState = (newValues, callback) => { this.setState( { state: { ...this.state.state, ...newValues, }, }, callback ) } _replaceState = (state, callback) => this.setState({ state }, callback) _linkState = (path, targetPath) => this.linkState(`state.${path}`, targetPath) _toggleState = path => this.toggleState(`state.${path}`) // Actions --------------------------------------------------------------------- _reset = callback => { this._replaceState( { acls: [], bootAfterCreate: true, copyHostBiosStrings: this._templateHasBiosStrings(), coresPerSocket: undefined, CPUs: '', cpuCap: '', cpusMax: '', cpuWeight: '', destroyCloudConfigVdiAfterBoot: false, existingDisks: {}, fastClone: true, hvmBootFirmware: '', installMethod: 'noConfigDrive', multipleVms: false, name_label: '', name_description: '', nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`), namePattern: '{name}%', nbVms: NB_VMS_MIN, resourceSet: undefined, VDIs: [], VIFs: [], secureBoot: false, seqStart: 1, share: this._getResourceSet()?.shareByDefault ?? false, tags: [], createVtpm: this._templateNeedsVtpm(), }, callback ) } _selfCreate = () => { const { VDIs, existingDisks, memoryDynamicMax } = this.state.state const { template } = this.props const disksSize = sumBy(VDIs, 'size') + sumBy(existingDisks, 'size') const templateDisksSize = sumBy(template.template_info.disks, 'size') const templateMemoryDynamicMax = template.memory.dynamic[1] const templateVcpusMax = template.CPUs.max return this._getCpusMax() > MULTIPLICAND * templateVcpusMax || memoryDynamicMax > MULTIPLICAND * templateMemoryDynamicMax || disksSize > MULTIPLICAND * templateDisksSize ? confirm({ title: _('createVmModalTitle'), body: _('createVmModalWarningMessage'), }).then(this._create) : this._create() } _create = () => { const { state } = this.state let installation switch (state.installMethod) { case 'ISO': installation = { method: 'cdrom', repository: state.installIso.id, } break case 'network': const matches = /^(http|ftp|nfs)/i.exec(state.installNetwork) if (!matches) { throw new Error('invalid network URL') } installation = { method: matches[1].toLowerCase(), repository: state.installNetwork, } break case 'PXE': installation = { method: 'network', repository: 'pxe', } } let cloudConfig let cloudConfigs let networkConfig if (state.installMethod !== 'noConfigDrive') { if (state.installMethod === 'SSH') { const format = hostname => hostname.replace(/^\s+|\s+$/g, '').replace(/\s+/g, '-') const stringifiedKeys = join( map(state.sshKeys, keyId => { return this.props.userSshKeys[keyId] ? ` - ${this.props.userSshKeys[keyId].key}\n` : '' }), '' ) cloudConfig = `#cloud-config\nhostname: ${format(state.name_label)}\nssh_authorized_keys:\n${stringifiedKeys}` if (state.multipleVms) { cloudConfigs = map( state.nameLabels, nameLabel => `#cloud-config\nhostname: ${format(nameLabel)}\nssh_authorized_keys:\n${stringifiedKeys}` ) } } else if (state.installMethod === 'customConfig') { const replacer = this._buildTemplate(defined(state.customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE)) cloudConfig = replacer(this.state.state, 0) if (state.multipleVms) { const seqStart = state.seqStart cloudConfigs = map(state.nameLabels, (_, i) => replacer(state, i + +seqStart)) } networkConfig = state.networkConfig } } else if (this._isCoreOs()) { cloudConfig = state.cloudConfig if (state.multipleVms) { cloudConfigs = new Array(state.nbVms).fill(state.cloudConfig) } } // Split allowed IPs into IPv4 and IPv6 const { VIFs } = state const _VIFs = map(VIFs, vif => { const _vif = { ...vif } if (_vif.mac?.trim() === '') { delete _vif.mac } delete _vif.addresses _vif.allowedIpv4Addresses = [] _vif.allowedIpv6Addresses = [] forEach(vif.addresses, ip => { if (!isIp(ip)) { return } if (isIp.v4(ip)) { _vif.allowedIpv4Addresses.push(ip) } else { _vif.allowedIpv6Addresses.push(ip) } }) return _vif }) // - self user: resource set ID in the URL, // - admin: resource set in state const resourceSet = this._getResourceSet() ?? state.resourceSet const { template } = this.props // Either use `memory` OR `memory*` params let { memory, memoryStaticMax, memoryDynamicMin, memoryDynamicMax } = state if ((memoryStaticMax != null || memoryDynamicMin != null) && memoryDynamicMax == null) { memoryDynamicMax = memory } if (memoryDynamicMax != null) { memory = undefined } const data = { acls: state.acls.map(acl => ({ subject: acl.subject.id, action: acl.action.id })), affinityHost: state.affinityHost && state.affinityHost.id, clone: this._isDiskTemplate && state.fastClone, existingDisks: state.existingDisks, installation, name_label: state.name_label, template: template.id, VDIs: state.VDIs, VIFs: _VIFs, resourceSet: resourceSet && resourceSet.id, // vm.set parameters coresPerSocket: state.coresPerSocket === null ? undefined : state.coresPerSocket, CPUs: state.CPUs, cpusMax: this._getCpusMax(), cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight, cpuCap: state.cpuCap === '' ? null : state.cpuCap, name_description: state.name_description, memory, memoryMax: memoryDynamicMax, memoryMin: memoryDynamicMin, memoryStaticMax, pv_args: state.pv_args, autoPoweron: state.autoPoweron, bootAfterCreate: state.bootAfterCreate, copyHostBiosStrings: state.hvmBootFirmware !== 'uefi' && !this._templateHasBiosStrings() && state.copyHostBiosStrings, createVtpm: state.createVtpm, destroyCloudConfigVdiAfterBoot: state.destroyCloudConfigVdiAfterBoot, secureBoot: state.secureBoot, share: state.share, cloudConfig, networkConfig: this._isCoreOs() ? undefined : networkConfig, coreOs: this._isCoreOs(), tags: state.tags, vgpuType: get(() => state.vgpuType.id), gpuGroup: get(() => state.vgpuType.gpuGroup), hvmBootFirmware: state.hvmBootFirmware === '' ? undefined : state.hvmBootFirmware, } return state.multipleVms ? createVms(data, state.nameLabels, cloudConfigs) : createVm(data) } _onChangeTemplate = template => { const { pathname, query } = this.props.location this.context.router.push({ pathname, query: { ...query, template: template && template.id }, }) } _initTemplate = template => { if (!template) { return this._reset() } const storeState = store.getState() const isInResourceSet = this._getIsInResourceSet() const { state } = this.state const { pool } = this.props const resourceSet = this._getResolvedResourceSet() const existingDisks = {} forEach(template.$VBDs, vbdId => { const vbd = getObject(storeState, vbdId, resourceSet) if (!vbd || vbd.is_cd_drive) { return } const vdi = getObject(storeState, vbd.VDI, resourceSet) if (vdi) { existingDisks[vbd.position] = { name_label: vdi.name_label, name_description: vdi.name_description, size: vdi.size, $SR: pool || isInResourceSet(vdi.$SR) ? vdi.$SR : this._getDefaultSr(template), } } }) let VIFs = [] const defaultNetworkIds = this._getDefaultNetworkIds(template) forEach( // iterate template VIFs in device order template.VIFs.map(id => getObject(storeState, id, resourceSet)).sort((a, b) => a.device - b.device), vif => { VIFs.push({ network: pool || isInResourceSet(vif.$network) ? vif.$network : defaultNetworkIds[0], }) } ) if (VIFs.length === 0) { VIFs = defaultNetworkIds.map(id => ({ network: id })) } const name_label = state.name_label === '' || !state.name_labelHasChanged ? template.name_label : state.name_label const name_description = state.name_description === '' || !state.name_descriptionHasChanged ? template.other.default_template === 'true' || template.name_description === undefined ? '' : template.name_description : state.name_description const replacer = this._buildVmsNameTemplate() this._setState({ // infos name_label, name_description, nameLabels: map(Array(+state.nbVms), (_, index) => replacer({ name_label, name_description, template }, index + 1) ), copyHostBiosStrings: !isEmpty(template.bios_strings), // performances CPUs: template.CPUs.number, cpusMax: template.CPUs.max, cpuCap: '', cpuWeight: '', hvmBootFirmware: defined(() => template.boot.firmware, ''), memory: template.memory.dynamic[1], // installation installMethod: (template.install_methods != null && template.install_methods[0]) || 'noConfigDrive', sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [0], // interfaces VIFs, // disks existingDisks, VDIs: map(template.template_info.disks, disk => { return { ...disk, name_description: disk.name_description || 'Created by XO', name_label: (name_label || 'disk') + '_' + generateReadableRandomString(5), SR: this._getDefaultSr(template), } }), // settings secureBoot: template.secureBoot, createVtpm: this._templateNeedsVtpm(), }) if (this._isCoreOs()) { getCloudInitConfig(template.id).then( cloudConfig => this._setState({ cloudConfig, coreOsDefaultTemplateError: false }), () => this._setState({ coreOsDefaultTemplateError: true }) ) } } // Selectors ------------------------------------------------------------------- _getIsInPool = createSelector( () => { const { pool } = this.props return pool && pool.id }, poolId => ({ $pool }) => $pool === poolId ) _getIsInResourceSet = createSelector( () => { const resourceSet = this._getResourceSet() return resourceSet && resourceSet.objects }, objectsIds => id => includes(objectsIds, id) ) _getVmPredicate = createSelector( this._getIsInPool, this._getIsInResourceSet, (isInPool, isInResourceSet) => vm => isInResourceSet(vm.id) || isInPool(vm) ) _getSrPredicate = createSelector( this._getIsInPool, this._getIsInResourceSet, () => this.props.template, () => this.props.pool === undefined, (isInPool, isInResourceSet, template, self) => disk => (self ? isInResourceSet(disk.id) : isInPool(disk)) && disk.content_type !== 'iso' && disk.size > 0 && template !== undefined && template.$pool === disk.$pool ) _getIsoPredicate = createSelector( () => this.props.pool && this.props.pool.id, poolId => sr => (poolId == null || poolId === sr.$pool) && sr.SR_type === 'iso' ) _getNetworkPredicate = createSelector( this._getIsInPool, this._getIsInResourceSet, () => this.props.pool === undefined, () => this.props.template, (isInPool, isInResourceSet, self, template) => network => (self ? isInResourceSet(network.id) : isInPool(network)) && template !== undefined && template.$pool === network.$pool ) _getPoolNetworks = createSelector( () => this.props.networks, () => { const { pool } = this.props return pool && pool.id }, (networks, poolId) => filter(networks, network => network.$pool === poolId) ) _getAffinityHostPredicate = createSelector( () => this.props.pool, () => this.state.state.existingDisks, () => this.state.state.VDIs, () => this.props.srs, (pool, existingDisks, VDIs, srs) => { if (!srs) { return false } const containers = [ ...map(existingDisks, disk => get(() => srs[disk.$SR].$container)), ...map(VDIs, disk => get(() => srs[disk.SR].$container)), ] return host => host.$pool === pool.id && every(containers, container => container === pool.id || container === host.id) } ) _getAutomaticNetworks = createSelector( createFilter(this._getPoolNetworks, [network => network.automatic]), networks => networks.map(_ => _.id) ) _getDefaultNetworkIds = template => { if (template === undefined) { return [] } if (this.props.pool === undefined) { const network = find(this._getResolvedResourceSet().objectsByType.network, { $pool: template.$pool, }) return network !== undefined ? [network.id] : [] } const automaticNetworks = this._getAutomaticNetworks() if (automaticNetworks.length !== 0) { return automaticNetworks } const network = find(this._getPoolNetworks(), network => { const pif = getObject(store.getState(), network.PIFs[0]) return pif && pif.management }) return network !== undefined ? [network.id] : [] } _buildVmsNameTemplate = createSelector( () => this.state.state.namePattern, namePattern => this._buildTemplate(namePattern) ) _buildTemplate = pattern => { const rules = { '{index}': (_, i) => i, '{name}': state => state.name_label || '', '%': (state, i) => (state.multipleVms ? i : '%'), } this.props.userSshKeys?.forEach(sshKey => { rules[`{sshKey:${sshKey.title}}`] = sshKey.key }) return compileTemplate(pattern, rules) } _templateHasBiosStrings = createSelector( () => this.props.template, template => template !== undefined && !isEmpty(template.bios_strings) ) _getVgpuTypePredicate = createSelector( () => this.props.pool, pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool ) _isCoreOs = createSelector( () => this.props.template, template => template && template.name_label === 'CoreOS' ) _isHvm = createSelector( () => this.props.template, template => template && template.virtualizationMode === 'hvm' ) _templateNeedsVtpm = () => this.props.template?.needsVtpm // On change ------------------------------------------------------------------- _onChangeSshKeys = keys => this._setState({ sshKeys: map(keys, key => key.id) }) _updateNbVms = () => { const { nbVms, nameLabels, seqStart } = this.state.state const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX) const newNameLabels = [...nameLabels] if (nbVmsClamped < nameLabels.length) { this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) }) } else { const replacer = this._buildVmsNameTemplate() for (let i = +seqStart + nameLabels.length; i <= +seqStart + nbVmsClamped - 1; i++) { newNameLabels.push(replacer(this.state.state, i)) } this._setState({ nameLabels: newNameLabels }) } } _updateNameLabels = () => { const { nameLabels, seqStart } = this.state.state const nbVms = nameLabels.length const newNameLabels = [] const replacer = this._buildVmsNameTemplate() for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) { newNameLabels.push(replacer(this.state.state, i)) } this._setState({ nameLabels: newNameLabels }) } _selectResourceSet = resourceSet => { const { pathname } = this.props.location this.context.router.push({ pathname, query: resourceSet && { resourceSet: resourceSet.id }, }) this._reset() } // admin only _setResourceSet = resourceSet => { this._setState({ // the select gives `null` when cleared, `undefined` is expected by `vm.create` resourceSet: resourceSet ?? undefined, share: resourceSet?.shareByDefault ?? false, }) } _selectPool = pool => { const { pathname } = this.props.location this.context.router.push({ pathname, query: pool && { pool: pool.id }, }) this._reset() } _getDefaultSr = template => { const { pool } = this.props if (pool !== undefined) { return pool.default_SR } if (template === undefined) { return } const defaultSr = getObject(store.getState(), template.$pool, true).default_SR return includes( resolveIds( filter( this._getResolvedResourceSet().objectsByType.SR, sr => sr.$pool === template.$pool && sr.content_type !== 'iso' && sr.size > 0 ) ), defaultSr ) ? defaultSr : undefined } _addVdi = () => { const { state } = this.state const { template } = this.props this._setState({ VDIs: [ ...state.VDIs, { name_description: 'Created by XO', name_label: (state.name_label || 'disk') + '_' + generateReadableRandomString(5), SR: this._getDefaultSr(template), type: 'system', }, ], }) } _removeVdi = index => { const { VDIs } = this.state.state this._setState({ VDIs: [...VDIs.slice(0, index), ...VDIs.slice(index + 1)], }) } _addInterface = () => { const { template } = this.props const { state } = this.state this._setState({ VIFs: [...state.VIFs, { network: this._getDefaultNetworkIds(template)[0] }], }) } _removeInterface = index => { const { VIFs } = this.state.state this._setState({ VIFs: [...VIFs.slice(0, index), ...VIFs.slice(index + 1)], }) } _addNewSshKey = () => { const { newSshKey, sshKeys } = this.state.state const { userSshKeys } = this.props const splitKey = newSshKey.split(' ') const title = splitKey.length === 3 ? splitKey[2].split('\n')[0] : newSshKey.slice(-10) // save key addSshKey({ title, key: newSshKey, }).then(() => { // select key this._setState({ sshKeys: [...(sshKeys || []), userSshKeys ? userSshKeys.length : 0], newSshKey: '', }) }) } _getRedirectionUrl = id => (this.state.state.multipleVms ? '/home' : `/vms/${id}`) _handleBootFirmware = value => this._setState({ hvmBootFirmware: value, secureBoot: false, createVtpm: value === 'uefi' ? this._templateNeedsVtpm() : false, }) _addAcls = async () => { const { action, subjects } = await confirm({ title: _('vmAddAcls'), icon: 'menu-settings-acls', body: , }) if (action == null) { return } // Remove ACLs that are being re-assigned const subjectIds = subjects.map(subject => subject.id) const acls = this.state.state.acls.filter(acl => !subjectIds.includes(acl.subject.id)) if (isEmpty(subjects)) { return } this._setState({ acls: [...acls, ...subjects.map(subject => ({ action, subject }))] }) } _removeAcl = event => { const { action, subject } = event.currentTarget.dataset this._setState({ acls: this.state.state.acls.filter(acl => acl.action.id !== action || acl.subject.id !== subject), }) } // MAIN ------------------------------------------------------------------------ _renderHeader = () => { const { isAdmin, isPoolAdmin, pool, resourceSets } = this.props const selectPool = ( ) const selectResourceSet = ( ) return (

{isAdmin || (isPoolAdmin && process.env.XOA_PLAN > 3) || !isEmpty(resourceSets) ? _('newVmCreateNewVmOn', { select: isAdmin || isPoolAdmin ? selectPool : selectResourceSet, }) : _('newVmCreateNewVmNoPermission')}

) } render() { const { pool } = this.props return ( {(pool || this._getResourceSet()) && (
{this._renderInfo()} {this._renderPerformances()} {this._renderInstallSettings()} {this._renderInterfaces()} {this._renderDisks()} {this._renderAdvanced()} {this._renderSummary()}
{_('newVmReset')} {_('newVmCreate')}
)}
) } // INFO ------------------------------------------------------------------------ _renderInfo = () => { const { name_description, name_label } = this.state.state const { template } = this.props return (
{this.props.pool ? ( ) : ( )}
) } _isInfoDone = () => { const { name_label } = this.state.state const { template } = this.props return name_label && template } _getCpusMax = createSelector( () => this.state.state.CPUs, () => this.state.state.cpusMax, Math.max ) _renderPerformances = () => { const { coresPerSocket, CPUs, memory, memoryDynamicMax } = this.state.state const { template } = this.props const { pool } = this.props const memoryThreshold = get(() => template.memory.static[0]) const selectCoresPerSocket = ( pool.cpus.cores)} maxVcpus={this._getCpusMax()} onChange={this._linkState('coresPerSocket')} value={coresPerSocket} /> ) return (
{' '} {memoryDynamicMax == null && memory != null && memory < memoryThreshold && ( )} {pool !== undefined ? ( selectCoresPerSocket ) : ( {selectCoresPerSocket} )}
) } _isPerformancesDone = () => { const { CPUs, memory, memoryDynamicMax } = this.state.state return CPUs && (memory != null || memoryDynamicMax != null) } // INSTALL SETTINGS ------------------------------------------------------------ _onChangeCloudConfig = cloudConfig => { this._setState({ customConfig: get(() => cloudConfig.template), }) } _onChangeNetworkConfig = networkConfig => this._setState({ networkConfig: get(() => networkConfig.template), }) _renderInstallSettings = () => { const { coreOsDefaultTemplateError } = this.state.state const { template } = this.props if (!template) { return } const { cloudConfig, customConfig, networkConfig, installIso, installMethod, installNetwork, newSshKey, pv_args, sshKeys, } = this.state.state const { formatMessage } = this.props.intl return (
{this._isDiskTemplate ? (
  {this.props.userSshKeys && this.props.userSshKeys.length > 0 && ( )}
    {!this._isCoreOs() && ( )}
) : ( {template.virtualizationMode === 'pv' ? ( {' '} {_('newVmNetworkLabel')}{' '} ) : ( {' '} {_('newVmPxeLabel')} )} )}   {_('newVmIsoDvdLabel')}   {this.props.pool ? ( ) : ( )} {this._isCoreOs() && (
{' '} {!coreOsDefaultTemplateError ? ( ) : ( {_('coreOsDefaultTemplateError')} )}
)}
) } _isInstallSettingsDone = () => { const { customConfig, installIso, installMethod, installNetwork, sshKeys } = this.state.state const { template } = this.props switch (installMethod) { case 'customConfig': return customConfig === undefined || customConfig.trim() !== '' || installMethod === 'noConfigDrive' case 'ISO': return installIso case 'network': return /^(http|ftp|nfs)/i.exec(installNetwork) case 'PXE': return true case 'SSH': return !isEmpty(sshKeys) || installMethod === 'noConfigDrive' default: return template && this._isDiskTemplate && installMethod === 'noConfigDrive' } } // INTERFACES ------------------------------------------------------------------ _renderInterfaces = () => { const { state: { VIFs }, } = this.state return (
{map(VIFs, (vif, index) => (
this._removeInterface(index)} pool={this.props.pool} resourceSet={this._getResolvedResourceSet()} vif={vif} /> {index < VIFs.length - 1 &&
}
))}
) } _isInterfacesDone = () => every(this.state.state.VIFs, vif => vif.network) // DISKS ----------------------------------------------------------------------- _getDiskSrs = createSelector( () => this.state.state.existingDisks, () => this.state.state.VDIs, (existingDisks, vdis) => { const diskSrs = new Set() forEach(existingDisks, disk => diskSrs.add(disk.$SR)) vdis.forEach(disk => diskSrs.add(disk.SR)) return [...diskSrs] } ) _srsNotOnSameHost = createSelector( this._getDiskSrs, () => this.props.srs, (diskSrs, srs) => { let container let sr return diskSrs.some(srId => { sr = srs[srId] return ( sr !== undefined && !isSrShared(sr) && (container !== undefined ? container !== sr.$container : ((container = sr.$container), false)) ) }) } ) _renderDisks = () => { const { state: { installMethod, existingDisks, VDIs }, } = this.state const { pool } = this.props let i = 0 const resourceSet = this._getResolvedResourceSet() return (
{/* Existing disks */} {map(existingDisks, (disk, index) => (
{pool ? ( ) : ( )} {' '} {i++ < size(existingDisks) + VDIs.length - 1 &&
}
))} {/* VDIs */} {map(VDIs, (vdi, index) => (
{pool ? ( ) : ( )} {index < VDIs.length - 1 &&
}
))} {this._srsNotOnSameHost() && ( {_('newVmSrsNotOnSameHost')} )}
) } _isDisksDone = () => every(this.state.state.VDIs, vdi => vdi.SR && vdi.name_label && vdi.size !== undefined) && every(this.state.state.existingDisks, (vdi, index) => vdi.$SR && vdi.name_label && vdi.size !== undefined) // ADVANCED -------------------------------------------------------------------- _renderAdvanced = () => { const { acls, affinityHost, autoPoweron, bootAfterCreate, copyHostBiosStrings, cpuCap, cpusMax, cpuWeight, createVtpm, destroyCloudConfigVdiAfterBoot, hvmBootFirmware, installMethod, memoryDynamicMin, memoryDynamicMax, memoryStaticMax, multipleVms, nameLabels, namePattern, nbVms, resourceSet, secureBoot, seqStart, share, showAdvanced, tags, } = this.state.state const { isAdmin, pool } = this.props const { formatMessage } = this.props.intl const isHvm = this._isHvm() const _copyHostBiosStrings = isAdmin && isHvm ? ( ) : null const isVtpmSupported = pool?.vtpmSupported ?? true return (
{showAdvanced && [
,   {_('newVmBootAfterCreate')}   {_('autoPowerOn')} , , this._getResourceSet() !== undefined && (   {_('newVmShare')} ), , ,   {multipleVms && ( {map(nameLabels, (nameLabel, index) => ( ))} )} , isAdmin && ( ), isHvm && ( ), isHvm && ( pool.master) : affinityHost.id} onChange={this._handleBootFirmware} value={hvmBootFirmware} /> ), hvmBootFirmware === 'uefi' && [ {secureBoot && this.state.poolGuestSecurebootReadiness === 'not_ready' && ( {_('secureBootNotSetup')} )} , {/* FIXME: link to VTPM documentation when ready */} {/*   */} {!createVtpm && this._templateNeedsVtpm() && ( {_('warningVtpmRequired')} )} , ], isAdmin && isHvm && ( {hvmBootFirmware === 'uefi' || this._templateHasBiosStrings() ? ( {_copyHostBiosStrings} ) : ( _copyHostBiosStrings )} ), isAdmin && ( {_('vmAcls')} {acls.map(({ subject, action }) => ( {renderXoItem(subject)}{' '} {action.name}{' '} ))} ), isAdmin && this._getResourceSet() === undefined && ( ), ]}
) } _isAdvancedDone = () => { const lowerThan = (small, big) => small == null || big == null || small <= big const { memoryDynamicMin, memoryDynamicMax, memoryStaticMax } = this.state.state return lowerThan(memoryDynamicMin, memoryDynamicMax) && lowerThan(memoryDynamicMax, memoryStaticMax) } // SUMMARY --------------------------------------------------------------------- _renderSummary = () => { const { CPUs, existingDisks, fastClone, memory, memoryDynamicMax, multipleVms, nameLabels, VDIs, VIFs } = this.state.state const factor = multipleVms ? nameLabels.length : 1 const resourceSet = this._getResourceSet() const limits = resourceSet && resourceSet.limits const cpusLimits = limits && limits.cpus const memoryLimits = limits && limits.memory const diskLimits = limits && limits.disk const _memory = memoryDynamicMax || memory || 0 return (

{CPUs || 0}x

{_memory ? formatSize(_memory) : '0 B'}

{size(existingDisks) + VDIs.length || 0}x

{VIFs.length}x

{limits && ( {cpusLimits?.total !== undefined && ( )} {memoryLimits?.total !== undefined && ( )} {diskLimits?.total !== undefined && ( disk.size))) * factor} used={diskLimits.usage} /> )} )}
{this._isDiskTemplate && (
{' '} {_('fastCloneVmLabel')}
)}
) } _availableResources = () => { const resourceSet = this._getResourceSet() if (!resourceSet) { return true } const { CPUs, existingDisks, memory, memoryDynamicMax, VDIs, multipleVms, nameLabels } = this.state.state const _memory = memoryDynamicMax || memory || 0 const factor = multipleVms ? nameLabels.length : 1 return !( CPUs * factor > get(() => resourceSet.limits.cpus.total - resourceSet.limits.cpus.usage) || _memory * factor > get(() => resourceSet.limits.memory.total - resourceSet.limits.memory.usage) || (sumBy(VDIs, 'size') + sum(map(existingDisks, disk => disk.size))) * factor > get(() => resourceSet.limits.disk.total - resourceSet.limits.disk.usage) ) } } /* eslint-enable camelcase */