feat(xo-web,xo-server): create HBA SR (#2836)

Fixes #1992
This commit is contained in:
Olivier Lambert 2018-04-06 16:01:48 +02:00 committed by Pierre Donias
parent e6deb29070
commit 3cef668a75
4 changed files with 149 additions and 16 deletions

View File

@ -241,7 +241,7 @@ export async function createHba ({ host, nameLabel, nameDescription, scsiId }) {
const xapi = this.getXapi(host) const xapi = this.getXapi(host)
const deviceConfig = { const deviceConfig = {
scsiId, SCSIid: scsiId,
} }
const srRef = await xapi.call( const srRef = await xapi.call(
@ -251,7 +251,7 @@ export async function createHba ({ host, nameLabel, nameDescription, scsiId }) {
'0', '0',
nameLabel, nameLabel,
nameDescription, nameDescription,
'lvmoohba', // SR LVM over HBA 'lvmohba', // SR LVM over HBA
'user', // recommended by Citrix 'user', // recommended by Citrix
true, true,
{} {}
@ -366,7 +366,7 @@ export async function probeHba ({ host }) {
let xml let xml
try { try {
await xapi.call('SR.probe', host._xapiRef, 'type', {}) await xapi.call('SR.probe', host._xapiRef, {}, 'lvmohba', {})
throw new Error('the call above should have thrown an error') throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
@ -382,7 +382,7 @@ export async function probeHba ({ host }) {
hbaDevices.push({ hbaDevices.push({
hba: hbaDevice.hba.trim(), hba: hbaDevice.hba.trim(),
path: hbaDevice.path.trim(), path: hbaDevice.path.trim(),
scsciId: hbaDevice.SCSIid.trim(), scsiId: hbaDevice.SCSIid.trim(),
size: hbaDevice.size.trim(), size: hbaDevice.size.trim(),
vendor: hbaDevice.vendor.trim(), vendor: hbaDevice.vendor.trim(),
}) })
@ -668,6 +668,34 @@ probeIscsiExists.resolve = {
host: ['host', 'host', 'administrate'], host: ['host', 'host', 'administrate'],
} }
// -------------------------------------------------------------------
// This function helps to detect if this HBA already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections
export async function probeHbaExists ({ host, scsiId }) {
const xapi = this.getXapi(host)
const deviceConfig = {
SCSIid: scsiId,
}
const xml = parseXml(
await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmohba', {})
)
// get the UUID of SR connected to this LUN
return ensureArray(xml.SRlist.SR).map(sr => ({ uuid: sr.UUID.trim() }))
}
probeHbaExists.params = {
host: { type: 'string' },
scsiId: { type: 'string' },
}
probeHbaExists.resolve = {
host: ['host', 'host', 'administrate'],
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// This function helps to detect if this NFS SR already exists in XAPI // This function helps to detect if this NFS SR already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections // It returns a table of SR UUID, empty if no existing connections

View File

@ -429,6 +429,7 @@ const messages = {
newSrPath: 'Path', newSrPath: 'Path',
newSrIqn: 'IQN', newSrIqn: 'IQN',
newSrLun: 'LUN', newSrLun: 'LUN',
newSrNoHba: 'No HBA devices',
newSrAuth: 'with auth.', newSrAuth: 'with auth.',
newSrUsername: 'User Name', newSrUsername: 'User Name',
newSrPassword: 'Password', newSrPassword: 'Password',

View File

@ -1948,6 +1948,11 @@ export const probeSrIscsiExists = (
return _call('sr.probeIscsiExists', params) return _call('sr.probeIscsiExists', params)
} }
export const probeSrHba = host => _call('sr.probeHba', { host })
export const probeSrHbaExists = (host, scsiId) =>
_call('sr.probeHbaExists', { host, scsiId })
export const reattachSr = (host, uuid, nameLabel, nameDescription, type) => export const reattachSr = (host, uuid, nameLabel, nameDescription, type) =>
_call('sr.reattach', { host, uuid, nameLabel, nameDescription, type }) _call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
@ -1985,6 +1990,9 @@ export const createSrIscsi = (
return _call('sr.createIscsi', params) return _call('sr.createIscsi', params)
} }
export const createSrHba = (host, nameLabel, nameDescription, scsiId) =>
_call('sr.createHba', { host, nameLabel, nameDescription, scsiId })
export const createSrIso = ( export const createSrIso = (
host, host,
nameLabel, nameLabel,

View File

@ -16,6 +16,7 @@ import Wizard, { Section } from 'wizard'
import { confirm } from 'modal' import { confirm } from 'modal'
import { connectStore, formatSize } from 'utils' import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { ignoreErrors } from 'promise-toolbox'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
import { Password, Select } from 'form' import { Password, Select } from 'form'
import { SelectHost } from 'select-objects' import { SelectHost } from 'select-objects'
@ -30,11 +31,14 @@ import {
createSrIscsi, createSrIscsi,
createSrLvm, createSrLvm,
createSrNfs, createSrNfs,
createSrHba,
probeSrIscsiExists, probeSrIscsiExists,
probeSrIscsiIqns, probeSrIscsiIqns,
probeSrIscsiLuns, probeSrIscsiLuns,
probeSrNfs, probeSrNfs,
probeSrNfsExists, probeSrNfsExists,
probeSrHba,
probeSrHbaExists,
reattachSrIso, reattachSrIso,
reattachSr, reattachSr,
} from 'xo' } from 'xo'
@ -45,6 +49,50 @@ import {
onChange: propTypes.func.isRequired, onChange: propTypes.func.isRequired,
options: propTypes.array.isRequired, options: propTypes.array.isRequired,
}) })
class SelectScsiId extends Component {
_getOptions = createSelector(
() => this.props.options,
options =>
map(options, ({ vendor, path, size, scsiId }) => ({
label: `${vendor} - ${path} (${formatSize(size)})`,
value: scsiId,
}))
)
_handleChange = opt => {
const { props } = this
this.setState({ value: opt.value }, () => props.onChange(opt.value))
}
componentDidMount () {
return this.componentDidUpdate()
}
componentDidUpdate () {
let options
if (
this.state.value === null &&
(options = this._getOptions()).length === 1
) {
this._handleChange(options[0])
}
}
state = { value: null }
render () {
return (
<Select
clearable={false}
onChange={this._handleChange}
options={this._getOptions()}
value={this.state.value}
/>
)
}
}
class SelectIqn extends Component { class SelectIqn extends Component {
_getOptions = createSelector( _getOptions = createSelector(
() => this.props.options, () => this.props.options,
@ -141,10 +189,11 @@ class SelectLun extends Component {
// =================================================================== // ===================================================================
const SR_TYPE_TO_LABEL = { const SR_TYPE_TO_LABEL = {
nfs: 'NFS', hba: 'HBA',
iscsi: 'iSCSI', iscsi: 'iSCSI',
lvm: 'Local LVM',
local: 'Local', local: 'Local',
lvm: 'Local LVM',
nfs: 'NFS',
nfsiso: 'NFS ISO', nfsiso: 'NFS ISO',
smb: 'SMB', smb: 'SMB',
} }
@ -155,7 +204,7 @@ const SR_GROUP_TO_LABEL = {
} }
const typeGroups = { const typeGroups = {
vdisr: ['nfs', 'iscsi', 'lvm'], vdisr: ['hba', 'iscsi', 'lvm', 'nfs'],
isosr: ['local', 'nfsiso', 'smb'], isosr: ['local', 'nfsiso', 'smb'],
} }
@ -183,6 +232,7 @@ export default class New extends Component {
lockCreation: undefined, lockCreation: undefined,
lun: undefined, lun: undefined,
luns: undefined, luns: undefined,
hbaDevices: undefined,
name: undefined, name: undefined,
path: undefined, path: undefined,
paths: undefined, paths: undefined,
@ -212,7 +262,7 @@ export default class New extends Component {
server, server,
username, username,
} = this.refs } = this.refs
const { host, iqn, lun, path, type } = this.state const { host, iqn, lun, path, type, scsiId } = this.state
const createMethodFactories = { const createMethodFactories = {
nfs: async () => { nfs: async () => {
@ -235,6 +285,20 @@ export default class New extends Component {
path path
) )
}, },
hba: async () => {
const previous = await probeSrHbaExists(host.id, scsiId)
if (previous && previous.length > 0) {
try {
await confirm({
title: _('existingLunModalTitle'),
body: <p>{_('existingLunModalText')}</p>,
})
} catch (error) {
return
}
}
return createSrHba(host.id, name.value, description.value, scsiId)
},
iscsi: async () => { iscsi: async () => {
const previous = await probeSrIscsiExists( const previous = await probeSrIscsiExists(
host.id, host.id,
@ -311,16 +375,32 @@ export default class New extends Component {
_handleDescriptionChange = event => _handleDescriptionChange = event =>
this.setState({ description: event.target.value }) this.setState({ description: event.target.value })
_handleSrTypeSelection = event => { _handleSrTypeSelection = async event => {
const type = event.target.value const type = event.target.value
this.setState({ this.setState({
type, hbaDevices: undefined,
paths: undefined,
iqns: undefined, iqns: undefined,
paths: undefined,
summary: includes(['lvm', 'local', 'smb', 'hba']),
type,
unused: undefined,
usage: undefined, usage: undefined,
used: undefined, used: undefined,
unused: undefined, })
summary: type === 'lvm' || type === 'local' || type === 'smb', if (type === 'hba' && this.state.host !== undefined) {
this.setState(({ loading }) => ({ loading: loading + 1 }))
const hbaDevices = await probeSrHba(this.state.host.id)::ignoreErrors()
this.setState(({ loading }) => ({
hbaDevices,
loading: loading - 1,
}))
}
}
_handleSrHbaSelection = async scsiId => {
this.setState({
scsiId,
usage: true,
}) })
} }
@ -484,6 +564,7 @@ export default class New extends Component {
auth, auth,
host, host,
iqns, iqns,
hbaDevices,
loading, loading,
lockCreation, lockCreation,
lun, lun,
@ -579,6 +660,21 @@ export default class New extends Component {
</div> </div>
</fieldset> </fieldset>
)} )}
{type === 'hba' && (
<fieldset>
<label>{_('newSrLun')}</label>
<div>
{!isEmpty(hbaDevices) ? (
<SelectScsiId
options={hbaDevices}
onChange={this._handleSrHbaSelection}
/>
) : (
<em>{_('newSrNoHba')}</em>
)}
</div>
</fieldset>
)}
{paths && ( {paths && (
<fieldset> <fieldset>
<label htmlFor='selectSrPath'>{_('newSrPath')}</label> <label htmlFor='selectSrPath'>{_('newSrPath')}</label>
@ -774,8 +870,8 @@ export default class New extends Component {
<p key={key}> <p key={key}>
{sr.uuid} {sr.uuid}
<span className='pull-right'> <span className='pull-right'>
<a className='btn btn-warning'>{_('newSrInUse')}</a> // {/* FIXME Goes to sr view */}
FIXME Goes to sr view <a className='btn btn-warning'>{_('newSrInUse')}</a>
</span> </span>
</p> </p>
))} ))}
@ -801,7 +897,7 @@ export default class New extends Component {
<dd>{formatSize(+lun.size)}</dd> <dd>{formatSize(+lun.size)}</dd>
</dl> </dl>
)} )}
{type === 'nfs' && ( {includes(['nfs', 'hba'], type) && (
<dl className='dl-horizontal'> <dl className='dl-horizontal'>
<dt>{_('newSrPath')}</dt> <dt>{_('newSrPath')}</dt>
<dd>{path}</dd> <dd>{path}</dd>