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()) && (
)}
)
}
// 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 (
)
}
_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 (
)
}
_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 */