Otherwise it might leads to race condition where new VMs appear on the host but are ignored by this method.
129 lines
4.2 KiB
JavaScript
129 lines
4.2 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
|
|
|
|
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 an 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')
|
|
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
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
throw error
|
|
}
|
|
},
|
|
{ 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)
|
|
}
|
|
}
|
|
export default Host
|
|
|
|
decorateClass(Host, {
|
|
smartReboot: defer,
|
|
})
|