feat: share a vm in a resource set (#2611)

Fixes #2589
This commit is contained in:
badrAZ 2018-02-13 15:24:20 +01:00 committed by Julien Fontanet
parent 8b05486945
commit 8971d218bb
6 changed files with 392 additions and 318 deletions

View File

@ -500,7 +500,6 @@ export async function set (params) {
const vmId = VM._xapiId
const resourceSetId = extract(params, 'resourceSet')
if (resourceSetId !== undefined) {
if (this.user.permission !== 'admin') {
throw unauthorized()
@ -509,6 +508,11 @@ export async function set (params) {
await this.setVmResourceSet(vmId, resourceSetId)
}
const share = extract(params, 'share')
if (share) {
await this.shareVmResourceSet(vmId)
}
return xapi.editVm(vmId, params, async (limits, vm) => {
const resourceSet = xapi.xo.getData(vm, 'resourceSet')
@ -580,6 +584,8 @@ set.params = {
// Move the vm In to/Out of Self Service
resourceSet: { type: ['string', 'null'], optional: true },
share: { type: 'boolean', optional: true },
}
set.resolve = {

View File

@ -391,10 +391,18 @@ export default class {
await this._xo.removeAclsForObject(vmId)
}
if (resourceSetId != null) {
const { subjects } = await this.getResourceSet(resourceSetId)
await asyncMap(subjects, subject =>
this._xo.addAcl(subject, vmId, 'admin')
)
await this.shareVmResourceSet(vmId)
}
}
async shareVmResourceSet (vmId) {
const xapi = this._xo.getXapi(vmId)
const resourceSetId = xapi.xo.getData(vmId, 'resourceSet')
if (resourceSetId === undefined) {
throw new Error('the vm is not in a resource set')
}
const { subjects } = await this.getResourceSet(resourceSetId)
await asyncMap(subjects, subject => this._xo.addAcl(subject, vmId, 'admin'))
}
}

View File

@ -844,6 +844,7 @@ const messages = {
// ----- VM advanced tab -----
vmRemoveButton: 'Remove',
vmConvertButton: 'Convert',
vmShareButton: 'Share',
xenSettingsLabel: 'Xen settings',
guestOsLabel: 'Guest OS',
miscLabel: 'Misc',
@ -1252,6 +1253,9 @@ const messages = {
deleteRemotesModalMessage:
'Are you sure you want to delete {nRemotes, number} remote{nRemotes, plural, one {} other {s}}?',
revertVmModalTitle: 'Revert your VM',
shareVmInResourceSetModalTitle: 'Share your VM',
shareVmInResourceSetModalMessage:
'This VM will be shared with all the members of the self-service {self}. Are you sure?',
deleteVifsModalTitle: 'Delete VIF{nVifs, plural, one {} other {s}}',
deleteVifsModalMessage:
'Are you sure you want to delete {nVifs, number} VIF{nVifs, plural, one {} other {s}}?',

View File

@ -24,6 +24,7 @@ import { forbiddenOperation, noHostsAvailable } from 'xo-common/api-errors'
import _ from '../intl'
import invoke from '../invoke'
import logError from '../log-error'
import renderXoItem from '../render-xo-item'
import store from 'store'
import { alert, chooseAction, confirm } from '../modal'
import { error, info, success } from '../notification'
@ -1171,6 +1172,14 @@ export const createVgpu = (vm, { gpuGroup, vgpuType }) =>
export const deleteVgpu = vgpu => _call('vm.deleteVgpu', resolveIds({ vgpu }))
export const shareVm = (vm, resourceSet) =>
confirm({
title: _('shareVmInResourceSetModalTitle'),
body: _('shareVmInResourceSetModalMessage', {
self: renderXoItem(resourceSet),
}),
}).then(() => editVm(vm, { share: true }), noop)
// DISK ---------------------------------------------------------------
export const createDisk = (name, size, sr, { vm, bootable, mode, position }) =>

View File

@ -356,6 +356,10 @@
@extend .text-primary;
@extend .fa-ship;
}
&-share {
@extend .fa;
@extend .fa-list-alt;
}
}
// Generic states

View File

@ -24,9 +24,9 @@ import {
osFamily,
} from 'utils'
import {
createVgpu,
cloneVm,
convertVmToTemplate,
createVgpu,
deleteVgpu,
deleteVm,
editVm,
@ -34,6 +34,7 @@ import {
recoveryStartVm,
restartVm,
resumeVm,
shareVm,
stopVm,
subscribeResourceSets,
suspendVm,
@ -41,7 +42,12 @@ import {
XEN_DEFAULT_CPU_WEIGHT,
XEN_VIDEORAM_VALUES,
} from 'xo'
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import {
createGetObjectsOfType,
createSelector,
getCheckPermissions,
isAdmin,
} from 'selectors'
const forceReboot = vm => restartVm(vm, true)
const forceShutdown = vm => stopVm(vm, true)
@ -122,6 +128,31 @@ class ResourceSetItem extends Component {
}
}
@addSubscriptions({
resourceSets: subscribeResourceSets,
})
class ShareVmButton extends Component {
_shareVm = () => {
const { resourceSets, vm } = this.props
return shareVm(vm, {
...find(resourceSets, { id: vm.resourceSet }),
type: 'resourceSet',
})
}
render () {
return (
<TabButton
btnStyle='primary'
handler={this._shareVm}
icon='vm-share'
labelId='vmShareButton'
/>
)
}
}
class NewVgpu extends Component {
get value () {
return this.state
@ -284,340 +315,352 @@ export default connectStore(() => {
createSelector(getVgpus, vgpus => map(vgpus, 'gpuGroup'))
)
const getCanAdministrate = createSelector(
getCheckPermissions,
(_, props) => props.vm.id,
(check, id) => check(id, 'administrate')
)
return {
canAdministrate: getCanAdministrate,
gpuGroup: getGpuGroup,
isAdmin,
vgpus: getVgpus,
vgpuTypes: getVgpuTypes,
}
})(({ container, gpuGroup, isAdmin, vgpus, vgpuTypes, vm }) => (
<Container>
<Row>
<Col className='text-xs-right'>
{vm.power_state === 'Running' && (
<span>
<TabButton
btnStyle='primary'
handler={suspendVm}
handlerParam={vm}
icon='vm-suspend'
labelId='suspendVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={vm}
icon='vm-force-reboot'
labelId='forceRebootVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
{vm.power_state === 'Halted' && (
<span>
<TabButton
btnStyle='primary'
handler={recoveryStartVm}
handlerParam={vm}
icon='vm-recovery-mode'
labelId='recoveryModeLabel'
/>
<TabButton
btnStyle='primary'
handler={fullCopy}
handlerParam={vm}
icon='vm-clone'
labelId='cloneVmLabel'
/>
<TabButton
btnStyle='danger'
handler={convertVmToTemplate}
handlerParam={vm}
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
/>
</span>
)}
{vm.power_state === 'Suspended' && (
<span>
<TabButton
btnStyle='primary'
handler={resumeVm}
handlerParam={vm}
icon='vm-start'
labelId='resumeVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
<TabButton
btnStyle='danger'
handler={deleteVm}
handlerParam={vm}
icon='vm-delete'
labelId='vmRemoveButton'
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{vm.uuid}</Copiable>
</tr>
<tr>
<th>{_('virtualizationMode')}</th>
<td>
{vm.virtualizationMode === 'pv'
? _('paraVirtualizedMode')
: _('hardwareVirtualizedMode')}
</td>
</tr>
{vm.virtualizationMode === 'pv' && (
})(
({ canAdministrate, container, gpuGroup, isAdmin, vgpus, vgpuTypes, vm }) => (
<Container>
<Row>
<Col className='text-xs-right'>
{(isAdmin || canAdministrate) &&
vm.resourceSet != null && <ShareVmButton vm={vm} />}
{vm.power_state === 'Running' && (
<span>
<TabButton
btnStyle='primary'
handler={suspendVm}
handlerParam={vm}
icon='vm-suspend'
labelId='suspendVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={vm}
icon='vm-force-reboot'
labelId='forceRebootVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
{vm.power_state === 'Halted' && (
<span>
<TabButton
btnStyle='primary'
handler={recoveryStartVm}
handlerParam={vm}
icon='vm-recovery-mode'
labelId='recoveryModeLabel'
/>
<TabButton
btnStyle='primary'
handler={fullCopy}
handlerParam={vm}
icon='vm-clone'
labelId='cloneVmLabel'
/>
<TabButton
btnStyle='danger'
handler={convertVmToTemplate}
handlerParam={vm}
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
/>
</span>
)}
{vm.power_state === 'Suspended' && (
<span>
<TabButton
btnStyle='primary'
handler={resumeVm}
handlerParam={vm}
icon='vm-start'
labelId='resumeVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
<TabButton
btnStyle='danger'
handler={deleteVm}
handlerParam={vm}
icon='vm-delete'
labelId='vmRemoveButton'
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('pvArgsLabel')}</th>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{vm.uuid}</Copiable>
</tr>
<tr>
<th>{_('virtualizationMode')}</th>
<td>
<Text
value={vm.PV_args}
onChange={value => editVm(vm, { PV_args: value })}
/>
{vm.virtualizationMode === 'pv'
? _('paraVirtualizedMode')
: _('hardwareVirtualizedMode')}
</td>
</tr>
)}
<tr>
<th>{_('cpuWeightLabel')}</th>
<td>
<Number
value={vm.cpuWeight == null ? null : vm.cpuWeight}
onChange={value => editVm(vm, { cpuWeight: value })}
nullable
>
{vm.cpuWeight == null
? _('defaultCpuWeight', { value: XEN_DEFAULT_CPU_WEIGHT })
: vm.cpuWeight}
</Number>
</td>
</tr>
<tr>
<th>{_('cpuCapLabel')}</th>
<td>
<Number
value={vm.cpuCap == null ? null : vm.cpuCap}
onChange={value => editVm(vm, { cpuCap: value })}
nullable
>
{vm.cpuCap == null
? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
: vm.cpuCap}
</Number>
</td>
</tr>
<tr>
<th>{_('autoPowerOn')}</th>
<td>
<Toggle
value={Boolean(vm.auto_poweron)}
onChange={value => editVm(vm, { auto_poweron: value })}
/>
</td>
</tr>
<tr>
<th>{_('ha')}</th>
<td>
<Toggle
value={vm.high_availability}
onChange={value => editVm(vm, { high_availability: value })}
/>
</td>
</tr>
<tr>
<th>{_('vmAffinityHost')}</th>
<td>
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && (
{vm.virtualizationMode === 'pv' && (
<tr>
<th>{_('pvArgsLabel')}</th>
<td>
<Text
value={vm.PV_args}
onChange={value => editVm(vm, { PV_args: value })}
/>
</td>
</tr>
)}
<tr>
<th>{_('vmVgpus')}</th>
<th>{_('cpuWeightLabel')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
<Number
value={vm.cpuWeight == null ? null : vm.cpuWeight}
onChange={value => editVm(vm, { cpuWeight: value })}
nullable
>
{vm.cpuWeight == null
? _('defaultCpuWeight', { value: XEN_DEFAULT_CPU_WEIGHT })
: vm.cpuWeight}
</Number>
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVga')}</th>
<th>{_('cpuCapLabel')}</th>
<td>
<Number
value={vm.cpuCap == null ? null : vm.cpuCap}
onChange={value => editVm(vm, { cpuCap: value })}
nullable
>
{vm.cpuCap == null
? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
: vm.cpuCap}
</Number>
</td>
</tr>
<tr>
<th>{_('autoPowerOn')}</th>
<td>
<Toggle
value={vm.vga === 'std'}
onChange={value =>
editVm(vm, { vga: value ? 'std' : 'cirrus' })
}
value={Boolean(vm.auto_poweron)}
onChange={value => editVm(vm, { auto_poweron: value })}
/>
</td>
</tr>
)}
{vm.vga === 'std' && (
<tr>
<th>{_('vmVideoram')}</th>
<th>{_('ha')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { videoram: +getEventValue(event) })
}
value={vm.videoram}
>
{map(XEN_VIDEORAM_VALUES, val => (
<option key={val} value={val}>
{formatSize(val * 1048576)}
</option>
))}
</select>
<Toggle
value={vm.high_availability}
onChange={value => editVm(vm, { high_availability: value })}
/>
</td>
</tr>
)}
</tbody>
</table>
<br />
<h3>{_('vmLimitsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('vmCpuLimitsLabel')}</th>
<td>
<Number
value={vm.CPUs.number}
onChange={cpus => editVm(vm, { cpus })}
/>
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<tr>
<th>{_('vmAffinityHost')}</th>
<td>
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVgpus')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVga')}</th>
<td>
<Toggle
value={vm.vga === 'std'}
onChange={value =>
editVm(vm, { vga: value ? 'std' : 'cirrus' })
}
/>
</td>
</tr>
)}
{vm.vga === 'std' && (
<tr>
<th>{_('vmVideoram')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { videoram: +getEventValue(event) })
}
value={vm.videoram}
>
{map(XEN_VIDEORAM_VALUES, val => (
<option key={val} value={val}>
{formatSize(val * 1048576)}
</option>
))}
</select>
</td>
</tr>
)}
</tbody>
</table>
<br />
<h3>{_('vmLimitsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('vmCpuLimitsLabel')}</th>
<td>
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax => editVm(vm, { cpusStaticMax })}
value={vm.CPUs.number}
onChange={cpus => editVm(vm, { cpus })}
/>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{_('xenToolsStatusValue', {
status: normalizeXenToolsStatus(vm.xenTools),
})}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null ? resourceSet.id : resourceSet,
})
}
value={vm.resourceSet}
/>
) : vm.resourceSet !== undefined ? (
<ResourceSetItem id={vm.resourceSet} />
) : (
_('resourceSetNone')
)}
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
))
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax => editVm(vm, { cpusStaticMax })}
/>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{_('xenToolsStatusValue', {
status: normalizeXenToolsStatus(vm.xenTools),
})}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) ||
_('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null ? resourceSet.id : resourceSet,
})
}
value={vm.resourceSet}
/>
) : vm.resourceSet !== undefined ? (
<ResourceSetItem id={vm.resourceSet} />
) : (
_('resourceSetNone')
)}
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
)
)