Skip to content

Commit

Permalink
feat(xo-web/xostor): possibility to set preferred interface at creation
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieu authored and pdonias committed Apr 12, 2024
1 parent e3f31cc commit 2f962dd
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 27 deletions.
9 changes: 9 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2561,6 +2561,7 @@ const messages = {
xosanIssueHostNotInNetwork: 'Will configure the host xosan network device with a static IP address and plug it in.',
// ----- XOSTOR -----
approximateFinalSize: 'Approximate final size',
byDefaultManagementNetworkUsed: 'By default, the management network will be used',
cantFetchDisksFromNonXcpngHost: 'Unable to fetch physical disks from non-XCP-ng host',
diskAlreadyMounted: 'The disk is mounted on: {mountpoint}',
diskHasChildren: 'The disk has children',
Expand All @@ -2571,11 +2572,15 @@ const messages = {
fieldsMissing: 'Some fields are missing',
hostsNotSameNumberOfDisks: 'Hosts do not have the same number of disks',
ignoreFileSystems: 'Ignore file systems',
interfaceName: 'Interface name',
interfaceNameRequired: 'Interface name is required if a network is provided',
interfaceNameReserved: 'This interface name is reserved',
isTapdevsDisk: 'This is "tapdevs" disk',
licenseBoundUnknownXostor: 'License attached to an unknown XOSTOR',
licenseNotBoundXostor: 'No XOSTOR attached',
licenseExpiredXostorWarning:
'The license {licenseId} has expired. You can still use the SR but cannot administrate it anymore.',
networkNoPifs: 'The network does not have PIFs',
networks: 'Networks',
notXcpPool: 'Not an XCP-ng pool',
noXostorFound: 'No XOSTOR found',
Expand All @@ -2584,13 +2589,17 @@ const messages = {
onlyShowXostorRequirements: 'Only show {type} that meet XOSTOR requirements',
poolAlreadyHasXostor: 'Pool already has a XOSTOR',
poolNotRecentEnough: 'Not recent enough. Current version: {version}',
pifsNoIp: 'Not all PIFs have an IP',
pifsNotAttached: 'Not all PIFs are attached',
pifsNotStatic: 'Not all PIFs are static',
replication: 'Replication',
rpuNoLongerAvailableIfXostor:
'As long as a XOSTOR storage is present in the pool, Rolling Pool Update will not be available',
selectDisks: 'Select disk(s)…',
selectedDiskTypeIncompatibleXostor: 'Only disks of type "Disk" and "Raid" are accepted. Selected disk type: {type}.',
storage: 'Storage',
summary: 'Summary',
whiteSpaceNotAllowed: 'White space not allowed',
wrongNumberOfHosts: 'Wrong number of hosts',
xostor: 'XOSTOR',
xostorAvailableInXoa: 'XOSTOR is available in XOA',
Expand Down
133 changes: 106 additions & 27 deletions packages/xo-web/src/xo-app/xostor/new-xostor-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Card, CardBlock, CardHeader } from 'card'
import { connectStore, formatSize } from 'utils'
import { Container, Col, Row } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { find, first, map, mapValues, remove, size, some } from 'lodash'
import { first, map, mapValues, remove, size, some } from 'lodash'
import { createXostorSr, getBlockdevices } from 'xo'
import { injectState, provideState } from 'reaclette'
import { Input as DebounceInput } from 'debounce-input-decorator'
Expand Down Expand Up @@ -63,6 +63,10 @@ const xostorDiskPredicate = disk =>
!isDiskMounted(disk) &&
!diskHasChildren(disk) &&
!isTapdevsDisk(disk)
const arePifsAttached = pifs => pifs.every(pif => pif.attached)
const arePifsStatic = pifs => pifs.every(pif => pif.mode === 'Static' || pif.ipv6Mode === 'Static')
const doesNetworkHavePifs = network => network.PIFs.length > 0
const doPifsHaveIp = pifs => pifs.every(pif => pif.ip !== '' || (pif.ipv6.length > 0 && pif.ipv6.some(v6 => v6 !== '')))

// ===================================================================

Expand Down Expand Up @@ -241,19 +245,59 @@ const NetworkCard = decorate([
},
computed: {
networksPredicate: (state, props) => network => {
const isOnPool = network.$pool === state.poolId
const pifs = network.PIFs
return state.onlyShowXostorNetworks
? isOnPool && pifs.length > 0 && pifs.every(pifId => props.pifs[pifId].ip !== '')
: isOnPool
if (network.$pool !== state.poolId) {
return false
}
const pifs = props.pifsByNetwork[network.id]
return (
!state.onlyShowXostorNetworks ||
(doesNetworkHavePifs(network) && arePifsStatic(pifs) && doPifsHaveIp(pifs) && arePifsAttached(pifs))
)
},
networkHavePifs: (state, props) => doesNetworkHavePifs(props.networks[state.networkId]),
pifsAreAttached: (state, props) => arePifsAttached(props.pifsByNetwork[state.networkId]),
pifsAreStatic: (state, props) => arePifsStatic(props.pifsByNetwork[state.networkId]),
pifsHaveIp: (state, props) => doPifsHaveIp(props.pifsByNetwork[state.networkId]),
interfaceNameContainsWhiteSpace: state => state.interfaceName.includes(' '),
interfaceNameReserved: state => state.interfaceName.trim() === 'default',
networkCompatible: state =>
state.networkHavePifs && state.pifsAreStatic && state.pifsHaveIp && state.pifsAreAttached,
},
}),
injectState,
({ effects, state }) => (
<Card>
<CardHeader>{_('network')}</CardHeader>
<CardHeader>
{_('network')}
{_('optionalEntry')}
</CardHeader>
<CardBlock>
<i className='d-block'>
<Icon icon='info' /> {_('byDefaultManagementNetworkUsed')}
</i>
<Row className='mb-1'>
<Col>
{_('interfaceName')}
<DebounceInput
className='form-control'
name='interfaceName'
onChange={effects.onInterfaceNameChange}
value={state.interfaceName}
/>
<ul className='text-danger'>
{state.interfaceNameReserved && (
<li>
<Icon icon='alarm' /> {_('interfaceNameReserved')}
</li>
)}
{state.interfaceNameContainsWhiteSpace && (
<li>
<Icon icon='alarm' /> {_('whiteSpaceNotAllowed')}
</li>
)}
</ul>
</Col>
</Row>
<label>
<input
checked={state.onlyShowXostorNetworks}
Expand All @@ -269,6 +313,16 @@ const NetworkCard = decorate([
predicate={state.networksPredicate}
value={state.networkId}
/>
{state.networkId !== undefined && !state.networkCompatible && (
<div className='text-danger'>
<ul>
{!state.networkHavePifs && <li>{_('networkNoPifs')}</li>}
{!state.pifsHaveIp && <li>{_('pifsNoIp')}</li>}
{!state.pifsAreStatic && <li>{_('pifsNotStatic')}</li>}
{!state.pifsAreAttached && <li>{_('pifsNotAttached')}</li>}
</ul>
</div>
)}
</CardBlock>
</Card>
),
Expand Down Expand Up @@ -457,6 +511,7 @@ const SummaryCard = decorate([
{state.isProvisioningMissing && <li>{_('fieldRequired', { field: _('provisioning') })}</li>}
{state.isNameMissing && <li>{_('fieldRequired', { field: _('name') })}</li>}
{state.isDisksMissing && <li>{_('xostorDiskRequired')}</li>}
{state.isInterfaceNameMissing && <li>{_('interfaceNameRequired')}</li>}
</ul>
</div>
) : (
Expand All @@ -480,11 +535,13 @@ const SummaryCard = decorate([
<Col size={6}>{_('keyValue', { key: _('provisioning'), value: state.provisioning.label })}</Col>
</Row>
<Row>
<Col size={12}>{_('keyValue', { key: _('pool'), value: <PoolRenderItem id={state.poolId} /> })}</Col>
{/* FIXME: XOSTOR network management is not yet implemented at XOSTOR level */}
{/* <Col size={6}>
{_('keyValue', { key: _('network'), value: <NetworkRenderItem id={state.networkId} /> })}
</Col> */}
<Col size={6}>{_('keyValue', { key: _('pool'), value: <PoolRenderItem id={state.poolId} /> })}</Col>
<Col size={6}>
{_('keyValue', {
key: _('network'),
value: state.networkId && <NetworkRenderItem id={state.networkId} />,
})}
</Col>
</Row>
<Row>
<Col size={6}>{_('keyValue', { key: _('numberOfHosts'), value: state.numberOfHostsWithDisks })}</Col>
Expand All @@ -504,17 +561,18 @@ const NewXostorForm = decorate([
connectStore({
hostsByPoolId: createGetObjectsOfType('host').sort().groupBy('$pool'),
networks: createGetObjectsOfType('network'),
pifs: createGetObjectsOfType('PIF'),
pifsByNetwork: createGetObjectsOfType('PIF').groupBy('$network'),
}),
provideState({
initialState: () => ({
_networkId: undefined,
_createdSrUuid: undefined, // used for redirection when the storage has been created
disksByHost: {},
ignoreFileSystems: false,
interfaceName: '',
provisioning: PROVISIONING_OPTIONS[0], // default value 'thin'
poolId: undefined,
hostId: undefined,
networkId: undefined,
replication: REPLICATION_OPTIONS[1], // default value 2
srDescription: '',
srName: '',
Expand All @@ -527,6 +585,9 @@ const NewXostorForm = decorate([
onIgnoreFileSystemsChange(_, value) {
this.state.ignoreFileSystems = value
},
onInterfaceNameChange(_, ev) {
this.state.interfaceName = ev.target.value
},
onPoolChange(_, pool) {
this.state.disksByHost = {}
this.state.poolId = pool?.id
Expand All @@ -538,7 +599,7 @@ const NewXostorForm = decorate([
this.state.provisioning = provisioning
},
onNetworkChange(_, network) {
this.state._networkId = network?.id ?? null
this.state.networkId = network?.id
},
onDiskChange(_, disk, hostId) {
const { disksByHost } = this.state
Expand All @@ -557,7 +618,24 @@ const NewXostorForm = decorate([
}
},
async createXostorSr() {
const { disksByHost, ignoreFileSystems, srDescription, srName, provisioning, replication } = this.state
const {
disksByHost,
ignoreFileSystems,
interfaceName,
networkId,
srDescription,
srName,
provisioning,
replication,
} = this.state

const preferredInterface =
networkId !== undefined
? {
networkId,
name: interfaceName.trim(),
}
: undefined

this.state._createdSrUuid = await createXostorSr({
description: srDescription.trim() === '' ? undefined : srDescription.trim(),
Expand All @@ -566,14 +644,13 @@ const NewXostorForm = decorate([
name: srName.trim() === '' ? undefined : srName.trim(),
provisioning: provisioning.value,
replication: replication.value,
preferredInterface,
})
},
},
computed: {
// Private ==========
_disksByHostValues: state => Object.values(state.disksByHost).filter(disks => disks.length > 0),
_defaultNetworkId: (state, props) => props.networks?.[state._pifManagement?.$network]?.id,
_pifManagement: (state, props) => find(props.pifs, pif => pif.$pool === state.poolId && pif.management),
// Utils ============
poolHosts: (state, props) => props.hostsByPoolId?.[state.poolId],
isPoolSelected: state => state.poolId !== undefined,
Expand All @@ -582,16 +659,19 @@ const NewXostorForm = decorate([
isProvisioningMissing: state => state.provisioning === null,
isNameMissing: state => state.srName.trim() === '',
isDisksMissing: state => state.numberOfHostsWithDisks === 0,
isInterfaceNameMissing: state => state.networkId !== undefined && state.interfaceName.trim() === '',
isFormInvalid: state =>
state.isReplicationMissing || state.isProvisioningMissing || state.isNameMissing || state.isDisksMissing,
state.isReplicationMissing ||
state.isProvisioningMissing ||
state.isNameMissing ||
state.isDisksMissing ||
state.isInterfaceNameMissing,
isXcpngHost: state => isXcpngHost(first(state.poolHosts)),
getSrPath: state => () => `/srs/${state._createdSrUuid}`,
// State ============
networkId: state => (state._networkId === undefined ? state._defaultNetworkId : state._networkId),
},
}),
injectState,
({ effects, resetState, state, hostsByPoolId, networks, pifs }) => (
({ effects, resetState, state, hostsByPoolId, networks, pifsByNetwork }) => (
<Container>
<Row>
<Col size={6}>
Expand All @@ -602,13 +682,12 @@ const NewXostorForm = decorate([
</Col>
</Row>
<Row>
<Col size={12}>
<Col size={6}>
<PoolCard hostsByPoolId={hostsByPoolId} />
</Col>
{/* FIXME: XOSTOR network management is not yet implemented at XOSTOR level */}
{/* <Col size={6}>
<NetworkCard networks={networks} pifs={pifs} />
</Col> */}
<Col size={6}>
<NetworkCard networks={networks} pifsByNetwork={pifsByNetwork} />
</Col>
</Row>
<Row>
<DisksCard />
Expand Down

0 comments on commit 2f962dd

Please sign in to comment.