Compare commits

..

1 Commits

Author SHA1 Message Date
b-Nollet
de4efdaf7a feat(xo-server): implement VUSB API 2024-02-21 17:19:41 +01:00
12 changed files with 96 additions and 81 deletions

View File

@@ -437,8 +437,7 @@ export async function cleanVm(
} }
} }
// no warning because a VHD can be unused for perfectly good reasons, logWarn('unused VHD', { path: vhd })
// e.g. the corresponding backup (metadata file) has been deleted
if (remove) { if (remove) {
logInfo('deleting unused VHD', { path: vhd }) logInfo('deleting unused VHD', { path: vhd })
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd)) unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))

View File

@@ -7,3 +7,4 @@ export { default as VDI } from './vdi.mjs'
export { default as VIF } from './vif.mjs' export { default as VIF } from './vif.mjs'
export { default as VM } from './vm.mjs' export { default as VM } from './vm.mjs'
export { default as VTPM } from './vtpm.mjs' export { default as VTPM } from './vtpm.mjs'
export { default as VUSB } from './vusb.mjs'

View File

@@ -0,0 +1,16 @@
import ignoreErrors from 'promise-toolbox/ignoreErrors'
export default class Vusb {
async create(VM, USB_group) {
return this.call('VUSB.create', VM, USB_group)
}
async unplug(ref) {
await this.call('VUSB.unplug', ref)
}
async destroy(ref) {
await ignoreErrors.call(this.VUSB_unplug(ref))
await this.call('VUSB.destroy', ref)
}
}

View File

@@ -13,6 +13,7 @@
- [Home & REST API] `$container` field of an halted VM now points to a host if a VDI is on a local storage [Forum#71769](https://xcp-ng.org/forum/post/71769) - [Home & REST API] `$container` field of an halted VM now points to a host if a VDI is on a local storage [Forum#71769](https://xcp-ng.org/forum/post/71769)
- [Size Input] Ability to select two new units in the dropdown (`TiB`, `PiB`) (PR [#7382](https://github.com/vatesfr/xen-orchestra/pull/7382)) - [Size Input] Ability to select two new units in the dropdown (`TiB`, `PiB`) (PR [#7382](https://github.com/vatesfr/xen-orchestra/pull/7382))
### Bug fixes ### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed” > Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -22,8 +23,6 @@
- [Remotes] Correctly clear error when the remote is tested with success - [Remotes] Correctly clear error when the remote is tested with success
- [Import/VMWare] Fix importing last snapshot (PR [#7370](https://github.com/vatesfr/xen-orchestra/pull/7370)) - [Import/VMWare] Fix importing last snapshot (PR [#7370](https://github.com/vatesfr/xen-orchestra/pull/7370))
- [Host/Reboot] Fix false positive warning when restarting an host after updates (PR [#7366](https://github.com/vatesfr/xen-orchestra/pull/7366)) - [Host/Reboot] Fix false positive warning when restarting an host after updates (PR [#7366](https://github.com/vatesfr/xen-orchestra/pull/7366))
- [New/VM] Respect _Fast clone_ setting broken since 5.91.0 (PR [#7388](https://github.com/vatesfr/xen-orchestra/issues/7388))
- [Backup] Remove incorrect _unused VHD_ warning because the situation is normal (PR [#7406](https://github.com/vatesfr/xen-orchestra/issues/7406))
### Packages to release ### Packages to release

View File

@@ -425,32 +425,6 @@ It works even if the VM is running, because we'll automatically export a snapsho
In the VM "Snapshots" tab, you can also export a snapshot like you export a VM. In the VM "Snapshots" tab, you can also export a snapshot like you export a VM.
## VM migration
### Simple VM Migration (VM.pool_migrate)
In simple migration, the VM's active state is transferred from host A to host B while its disks remains in its original location. This feature is only possible when the VM's disks are on a shared SR by both hosts and if the VM is running.
#### Use Case
- Migrate a VM within the same pool from host A to host B without moving the VM's VDIs.
### VM Migration with Storage Motion (VM.migrate_send)
VM migration with storage motion allows you to migrate a VM from one host to another when the VM's disks are not on a shared SR between the two hosts or if a specific network is chosen for the migration. VDIs will be migrated to the destination SR if one is provided.
#### Use Cases
- Migrate a VM to another pool.
- Migrate a VM within the same pool from host A to host B by selecting a network for the migration.
- Migrate a VM within the same pool from host A to host B by moving the VM's VDIs to another storage.
### Expected Behavior
- Migrating a VM that has VDIs on a shared SR from host A to host B must trigger a "Simple VM Migration".
- Migrating a VM that has VDIs on a shared SR from host A to host B using a particular network must trigger a "VM Migration with Storage Motion" without moving its VDIs.
- Migrating a VM from host A to host B with a destination SR must trigger a "VM Migration with Storage Motion" and move VDIs to the destination SR, regardless of where the VDIs were stored.
## Hosts management ## Hosts management
Outside updates (see next section), you can also do host management via Xen Orchestra. Basic operations are supported, like reboot, shutdown and so on. Outside updates (see next section), you can also do host management via Xen Orchestra. Basic operations are supported, like reboot, shutdown and so on.

View File

@@ -1,27 +0,0 @@
export async function scan({ host }) {
await this.getXapi(host).call('PUSB.scan', host._xapiRef)
}
scan.params = {
host: { type: 'string' },
}
scan.resolve = {
host: ['host', 'host', 'operate'],
}
export async function set({ pusb, enabled }) {
const xapi = this.getXapi(pusb)
if (enabled !== undefined && enabled !== pusb.passthroughEnabled) {
await xapi.call('PUSB.set_passthrough_enabled', pusb._xapiRef, enabled)
}
}
set.params = {
id: { type: 'string' },
enabled: { type: 'boolean', optional: true },
}
set.resolve = {
pusb: ['id', 'PUSB', 'administrate'],
}

View File

@@ -0,0 +1,42 @@
// Creates a VUSB which will be plugged to the VM at its next restart
// Only one VUSB can be attached to a given USB_group, and up to six VUSB can be attached to a VM.
export async function create({ vm, usbGroup }) {
const xapi = this.getXapi(vm)
const vusbRef = await xapi.VUSB_create(vm._xapiRef, usbGroup._xapiRef)
return xapi.getField('VUSB', vusbRef, 'uuid')
}
create.params = {
vmId: { type: 'string' },
usbGroupId: { type: 'string' },
}
create.resolve = {
vm: ['vmId', 'VM', 'administrate'],
usbGroup: ['usbGroupId', 'USB_group', 'administrate'],
}
// Unplug VUSB until next VM restart
export async function unplug({ vusb }) {
await this.getXapi(vusb).VUSB_unplug(vusb._xapiRef)
}
unplug.params = {
id: { type: 'string' },
}
unplug.resolve = {
vusb: ['id', 'VUSB', 'administrate'],
}
export async function destroy({ vusb }) {
await this.getXapi(vusb).VUSB_destroy(vusb._xapiRef)
}
destroy.params = {
id: { type: 'string' },
}
destroy.resolve = {
vusb: ['id', 'VUSB', 'administrate'],
}

View File

@@ -882,6 +882,8 @@ const TRANSFORMS = {
} }
}, },
// -----------------------------------------------------------------
vtpm(obj) { vtpm(obj) {
return { return {
type: 'VTPM', type: 'VTPM',
@@ -890,16 +892,31 @@ const TRANSFORMS = {
} }
}, },
pusb(obj) { // -----------------------------------------------------------------
return {
type: 'PUSB',
description: obj.description, vusb(obj) {
host: link(obj, 'host'), return {
passthroughEnabled: obj.passthrough_enabled, type: 'VUSB',
vm: link(obj, 'VM'),
currentlyAttached: obj.currently_attached,
usbGroup: link(obj, 'USB_group'), usbGroup: link(obj, 'USB_group'),
} }
}, },
// -----------------------------------------------------------------
usb_group(obj) {
return {
type: 'USB_group',
PUSB: link(obj, 'PUSBs'),
VUSB: link(obj, 'VUSBs'),
nameDescription: obj.name_description,
nameLabel: obj.name_label,
otherConfig: obj.other_config,
}
},
} }
// =================================================================== // ===================================================================

View File

@@ -54,9 +54,13 @@ export default class IsoDevice extends Component {
() => this.props.vm.$pool, () => this.props.vm.$pool,
() => this.props.vm.$container, () => this.props.vm.$container,
(vmPool, vmContainer) => sr => { (vmPool, vmContainer) => sr => {
const vmRunning = vmContainer !== vmPool
const sameHost = vmContainer === sr.$container
const samePool = vmPool === sr.$pool
return ( return (
vmPool === sr.$pool && samePool &&
(sr.shared || vmContainer === sr.$container) && (vmRunning ? sr.shared || sameHost : true) &&
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size)) (sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
) )
} }

View File

@@ -460,7 +460,7 @@ export const SelectHostVm = makeStoreSelect(
export const SelectVmTemplate = makeStoreSelect( export const SelectVmTemplate = makeStoreSelect(
() => { () => {
const getVmTemplatesByPool = createGetObjectsOfType('VM-template').filter(getPredicate).sort().groupBy('$pool') const getVmTemplatesByPool = createGetObjectsOfType('VM-template').filter(getPredicate).sort().groupBy('$container')
const getPools = createGetObjectsOfType('pool') const getPools = createGetObjectsOfType('pool')
.pick(createSelector(getVmTemplatesByPool, vmTemplatesByPool => keys(vmTemplatesByPool))) .pick(createSelector(getVmTemplatesByPool, vmTemplatesByPool => keys(vmTemplatesByPool)))
.sort() .sort()

View File

@@ -3,6 +3,7 @@ import _ from 'intl'
import Copiable from 'copiable' import Copiable from 'copiable'
import decorate from 'apply-decorators' import decorate from 'apply-decorators'
import Icon from 'icon' import Icon from 'icon'
import map from 'lodash/map'
import React from 'react' import React from 'react'
import store from 'store' import store from 'store'
import HomeTags from 'home-tags' import HomeTags from 'home-tags'
@@ -23,21 +24,10 @@ export default decorate([
provideState({ provideState({
computed: { computed: {
areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { host }) => areHostsVersionsEqualByPool[host.$pool], areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { host }) => areHostsVersionsEqualByPool[host.$pool],
inMemoryVms: (_, { vms }) => {
const result = []
for (const key of Object.keys(vms)) {
const vm = vms[key]
const { power_state } = vm
if (power_state === 'Running' || power_state === 'Paused') {
result.push(vm)
}
}
return result
},
}, },
}), }),
injectState, injectState,
({ statsOverview, host, nVms, vmController, state: { areHostsVersionsEqual, inMemoryVms } }) => { ({ statsOverview, host, nVms, vmController, vms, state: { areHostsVersionsEqual } }) => {
const pool = getObject(store.getState(), host.$pool) const pool = getObject(store.getState(), host.$pool)
const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString()) const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString())
return ( return (
@@ -130,7 +120,7 @@ export default decorate([
tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`} tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`}
value={vmController.memory.size} value={vmController.memory.size}
/> />
{inMemoryVms.map(vm => ( {map(vms, vm => (
<UsageElement <UsageElement
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`} tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
key={vm.id} key={vm.id}

View File

@@ -300,7 +300,7 @@ export default class NewVm extends BaseComponent {
get _isDiskTemplate() { get _isDiskTemplate() {
const { template } = this.props const { template } = this.props
return template && template.$VBDs.length !== 0 && template.name_label !== 'Other install media' return template && template.template_info.disks.length === 0 && template.name_label !== 'Other install media'
} }
_setState = (newValues, callback) => { _setState = (newValues, callback) => {
this.setState( this.setState(
@@ -470,7 +470,7 @@ export default class NewVm extends BaseComponent {
const data = { const data = {
affinityHost: state.affinityHost && state.affinityHost.id, affinityHost: state.affinityHost && state.affinityHost.id,
clone: this._isDiskTemplate && state.fastClone, clone: !this._isDiskTemplate && state.fastClone,
existingDisks: state.existingDisks, existingDisks: state.existingDisks,
installation, installation,
name_label: state.name_label, name_label: state.name_label,