mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
176 lines
5.6 KiB
JavaScript
176 lines
5.6 KiB
JavaScript
import { asyncEach } from '@vates/async-each'
|
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
import { decorateClass } from '@vates/decorate-with'
|
|
import { defer } from 'golike-defer'
|
|
import { incorrectState, operationFailed } from 'xo-common/api-errors.js'
|
|
|
|
import { getCurrentVmUuid } from './_XenStore.mjs'
|
|
|
|
const waitAgentRestart = (xapi, hostRef, prevAgentStartTime) =>
|
|
new Promise(resolve => {
|
|
// even though the ref could change in case of pool master restart, tests show it stays the same
|
|
const stopWatch = xapi.watchObject(hostRef, host => {
|
|
if (+host.other_config.agent_start_time > prevAgentStartTime && host.enabled) {
|
|
stopWatch()
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
|
|
class Host {
|
|
async restartAgent(ref) {
|
|
const agentStartTime = +(await this.getField('host', ref, 'other_config')).agent_start_time
|
|
const host = await this.getRecord('host', ref)
|
|
if (host.$pool.ha_enabled) {
|
|
throw incorrectState({
|
|
actual: host.$pool.ha_enabled,
|
|
expected: false,
|
|
object: host.$pool.uuid,
|
|
property: 'HA_enabled',
|
|
})
|
|
}
|
|
await this.call('host.restart_agent', ref)
|
|
|
|
await waitAgentRestart(this, ref, agentStartTime)
|
|
}
|
|
|
|
/**
|
|
* Suspend all resident VMS, reboot the host and resume the VMs
|
|
*
|
|
* The current VM is not suspended as to not interrupt the process.
|
|
*
|
|
* @param {string} ref - Opaque reference of the host
|
|
*/
|
|
async smartReboot($defer, ref, bypassBlockedSuspend = false, bypassCurrentVmCheck = false) {
|
|
await this.callAsync('host.disable', ref)
|
|
|
|
// host may have been re-enabled already, this is not a problem
|
|
$defer.onFailure(() => this.callAsync('host.enable', ref))
|
|
|
|
let currentVmRef
|
|
try {
|
|
currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid())
|
|
} catch (error) {}
|
|
|
|
const residentVmRefs = await this.getField('host', ref, 'resident_VMs')
|
|
|
|
// check the VMs with PCI passthrough
|
|
const vms = await asyncMap(residentVmRefs, ref => this.getRecord('VM', ref))
|
|
const vmsNotSuspendable = []
|
|
|
|
await asyncEach(vms, async vm => {
|
|
try {
|
|
await this.call('VM.assert_operation_valid', vm.$ref, 'suspend')
|
|
} catch (err) {
|
|
vmsNotSuspendable.push(vm)
|
|
}
|
|
})
|
|
|
|
const vmsNotSuspendableRefs = new Set(vmsNotSuspendable.map(vm => vm.$ref))
|
|
|
|
const vmsWithSuspendBlocked = await asyncMap(residentVmRefs, ref => this.getRecord('VM', ref)).filter(
|
|
vm =>
|
|
vm.$ref !== currentVmRef &&
|
|
!vm.is_control_domain &&
|
|
vm.power_state !== 'Halted' &&
|
|
vm.power_state !== 'Suspended' &&
|
|
(vm.blocked_operations.suspend !== undefined || vmsNotSuspendableRefs.has(vm.$ref))
|
|
)
|
|
|
|
if (!bypassBlockedSuspend && vmsWithSuspendBlocked.length > 0) {
|
|
throw incorrectState({ actual: vmsWithSuspendBlocked.map(vm => vm.uuid), expected: [], object: 'suspendBlocked' })
|
|
}
|
|
|
|
if (!bypassCurrentVmCheck && residentVmRefs.includes(currentVmRef)) {
|
|
throw operationFailed({
|
|
objectId: await this.getField('VM', currentVmRef, 'uuid'),
|
|
code: 'xoaOnHost',
|
|
})
|
|
}
|
|
|
|
await asyncEach(vmsWithSuspendBlocked, vm => {
|
|
$defer(() => vm.update_blocked_operations('suspend', vm.blocked_operations.suspend ?? null))
|
|
return vm.update_blocked_operations('suspend', null)
|
|
})
|
|
|
|
const suspendedVms = []
|
|
|
|
// Resuming VMs should occur after host enabling to avoid triggering a 'NO_HOSTS_AVAILABLE' error
|
|
//
|
|
// The defers are running in reverse order.
|
|
$defer(() => asyncEach(suspendedVms, vmRef => this.callAsync('VM.resume', vmRef, false, false)))
|
|
$defer.onFailure(() =>
|
|
// if the host has not been rebooted, it might still be disabled and need to be enabled manually
|
|
this.callAsync('host.enable', ref)
|
|
)
|
|
|
|
await asyncEach(
|
|
residentVmRefs,
|
|
async vmRef => {
|
|
if (vmRef === currentVmRef) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this.callAsync('VM.suspend', vmRef)
|
|
suspendedVms.push(vmRef)
|
|
} catch (error) {
|
|
const { code } = error
|
|
|
|
// operation is not allowed on a control domain, ignore
|
|
if (code === 'OPERATION_NOT_ALLOWED') {
|
|
return
|
|
}
|
|
|
|
// ignore if the VM is already halted or suspended
|
|
if (code === 'VM_BAD_POWER_STATE') {
|
|
// power state is usually capitalized in XAPI but is lowercased in this error
|
|
//
|
|
// don't rely on it to be future proof
|
|
const powerState = error.params[2].toLowerCase()
|
|
if (powerState === 'halted' || powerState === 'suspended') {
|
|
return
|
|
}
|
|
}
|
|
|
|
// fallback on suspend error
|
|
try {
|
|
await this.callAsync('VM.clean_shutdown', vmRef)
|
|
} catch (error) {
|
|
await this.callAsync('VM.hard_shutdown', vmRef)
|
|
}
|
|
}
|
|
},
|
|
{ stopOnError: false }
|
|
)
|
|
|
|
const agentStartTime = +(await this.getField('host', ref, 'other_config')).agent_start_time
|
|
await this.callAsync('host.reboot', ref)
|
|
await waitAgentRestart(this, ref, agentStartTime)
|
|
}
|
|
|
|
async getMdadmHealth(ref) {
|
|
try {
|
|
const result = await this.callAsync('host.call_plugin', ref, 'raid.py', 'check_raid_pool', {})
|
|
const parsedResult = JSON.parse(result)
|
|
|
|
return parsedResult
|
|
} catch (error) {
|
|
if (
|
|
error.code === 'XENAPI_MISSING_PLUGIN' ||
|
|
error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION' ||
|
|
error.code === 'HOST_OFFLINE'
|
|
) {
|
|
return null
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
}
|
|
export default Host
|
|
|
|
decorateClass(Host, {
|
|
smartReboot: defer,
|
|
})
|