Files

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,
})