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 deviceConfig = {
scsiId,
SCSIid: scsiId,
}
const srRef = await xapi.call(
@ -251,7 +251,7 @@ export async function createHba ({ host, nameLabel, nameDescription, scsiId }) {
'0',
nameLabel,
nameDescription,
'lvmoohba', // SR LVM over HBA
'lvmohba', // SR LVM over HBA
'user', // recommended by Citrix
true,
{}
@ -366,7 +366,7 @@ export async function probeHba ({ host }) {
let xml
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')
} catch (error) {
@ -382,7 +382,7 @@ export async function probeHba ({ host }) {
hbaDevices.push({
hba: hbaDevice.hba.trim(),
path: hbaDevice.path.trim(),
scsciId: hbaDevice.SCSIid.trim(),
scsiId: hbaDevice.SCSIid.trim(),
size: hbaDevice.size.trim(),
vendor: hbaDevice.vendor.trim(),
})
@ -668,6 +668,34 @@ probeIscsiExists.resolve = {
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
// It returns a table of SR UUID, empty if no existing connections

View File

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

View File

@ -1948,6 +1948,11 @@ export const probeSrIscsiExists = (
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) =>
_call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
@ -1985,6 +1990,9 @@ export const createSrIscsi = (
return _call('sr.createIscsi', params)
}
export const createSrHba = (host, nameLabel, nameDescription, scsiId) =>
_call('sr.createHba', { host, nameLabel, nameDescription, scsiId })
export const createSrIso = (
host,
nameLabel,

View File

@ -16,6 +16,7 @@ import Wizard, { Section } from 'wizard'
import { confirm } from 'modal'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { ignoreErrors } from 'promise-toolbox'
import { injectIntl } from 'react-intl'
import { Password, Select } from 'form'
import { SelectHost } from 'select-objects'
@ -30,11 +31,14 @@ import {
createSrIscsi,
createSrLvm,
createSrNfs,
createSrHba,
probeSrIscsiExists,
probeSrIscsiIqns,
probeSrIscsiLuns,
probeSrNfs,
probeSrNfsExists,
probeSrHba,
probeSrHbaExists,
reattachSrIso,
reattachSr,
} from 'xo'
@ -45,6 +49,50 @@ import {
onChange: propTypes.func.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 {
_getOptions = createSelector(
() => this.props.options,
@ -141,10 +189,11 @@ class SelectLun extends Component {
// ===================================================================
const SR_TYPE_TO_LABEL = {
nfs: 'NFS',
hba: 'HBA',
iscsi: 'iSCSI',
lvm: 'Local LVM',
local: 'Local',
lvm: 'Local LVM',
nfs: 'NFS',
nfsiso: 'NFS ISO',
smb: 'SMB',
}
@ -155,7 +204,7 @@ const SR_GROUP_TO_LABEL = {
}
const typeGroups = {
vdisr: ['nfs', 'iscsi', 'lvm'],
vdisr: ['hba', 'iscsi', 'lvm', 'nfs'],
isosr: ['local', 'nfsiso', 'smb'],
}
@ -183,6 +232,7 @@ export default class New extends Component {
lockCreation: undefined,
lun: undefined,
luns: undefined,
hbaDevices: undefined,
name: undefined,
path: undefined,
paths: undefined,
@ -212,7 +262,7 @@ export default class New extends Component {
server,
username,
} = this.refs
const { host, iqn, lun, path, type } = this.state
const { host, iqn, lun, path, type, scsiId } = this.state
const createMethodFactories = {
nfs: async () => {
@ -235,6 +285,20 @@ export default class New extends Component {
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 () => {
const previous = await probeSrIscsiExists(
host.id,
@ -311,16 +375,32 @@ export default class New extends Component {
_handleDescriptionChange = event =>
this.setState({ description: event.target.value })
_handleSrTypeSelection = event => {
_handleSrTypeSelection = async event => {
const type = event.target.value
this.setState({
type,
paths: undefined,
hbaDevices: undefined,
iqns: undefined,
paths: undefined,
summary: includes(['lvm', 'local', 'smb', 'hba']),
type,
unused: undefined,
usage: 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,
host,
iqns,
hbaDevices,
loading,
lockCreation,
lun,
@ -579,6 +660,21 @@ export default class New extends Component {
</div>
</fieldset>
)}
{type === 'hba' && (
<fieldset>
<label>{_('newSrLun')}</label>
<div>
{!isEmpty(hbaDevices) ? (
<SelectScsiId
options={hbaDevices}
onChange={this._handleSrHbaSelection}
/>
) : (
<em>{_('newSrNoHba')}</em>
)}
</div>
</fieldset>
)}
{paths && (
<fieldset>
<label htmlFor='selectSrPath'>{_('newSrPath')}</label>
@ -774,8 +870,8 @@ export default class New extends Component {
<p key={key}>
{sr.uuid}
<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>
</p>
))}
@ -801,7 +897,7 @@ export default class New extends Component {
<dd>{formatSize(+lun.size)}</dd>
</dl>
)}
{type === 'nfs' && (
{includes(['nfs', 'hba'], type) && (
<dl className='dl-horizontal'>
<dt>{_('newSrPath')}</dt>
<dd>{path}</dd>