Compare commits

...

17 Commits

Author SHA1 Message Date
Pizzosaure
969b64d575 fix(home): upadted Changelog 2023-10-16 15:40:43 +02:00
Pizzosaure
fa56e8453a unrelated change fixed 2023-10-16 10:02:16 +02:00
Pizzosaure
eb64937bc6 feedback PR: condition fixed 2023-10-16 09:43:59 +02:00
Pizzosaure
1502ac317d fix(home):fix misaligned descriptions 2023-10-13 10:56:28 +02:00
Mathieu
8bfe293414 feat(lite/VM): add copy, snapshot single action (#7087) 2023-10-12 11:09:11 +02:00
Mathieu
2e634a9d1c feat(xapi/VTPM): ability to create, destroy VTPM (#7074) 2023-10-12 09:19:38 +02:00
Pierre Donias
bea771ca90 fix(xo-server/RPU): do not migrate VM back if already on host (#7071)
See https://xcp-ng.org/forum/topic/7802
2023-10-11 16:16:44 +02:00
Pierre Donias
99e3622f31 feat(xo-web/SelectPif): show network name (#7081)
See Zammad#17381
2023-10-10 15:59:24 +02:00
Pizzosaure
a16522241e docs(netbox): remove extra backtick (#7083)
Introduced by 3b3f927e4b
2023-10-10 14:14:15 +02:00
Julien Fontanet
b86cb12649 chore(yarn.lock): update dev deps 2023-10-09 17:06:54 +02:00
Julien Fontanet
2af74008b2 feat(xo-server-backup-reports): errors are logged as XO tasks 2023-10-09 09:35:24 +02:00
Julien Fontanet
2e689592f1 feat(xo-server-backup-reports): error when transports not enabled 2023-10-09 09:35:24 +02:00
Julien Fontanet
3f8436b58b fix(xo-server/authenticateUser): use clearLogOnSuccess
This fixes success logs not deleted due to race conditions.
2023-10-09 09:35:24 +02:00
Julien Fontanet
e3dd59d684 feat(mixins/Tasks#create): clearLogOnSuccess option 2023-10-09 09:35:24 +02:00
mathieuRA
549d9b70a9 feat(xo-web/host): allow to force smartReboot 2023-10-06 16:52:26 +02:00
mathieuRA
3bf6aae103 feat(xapi/host_smartReboot): ability to bypass blocked operations 2023-10-06 16:52:26 +02:00
Julien Fontanet
afb110c473 fix(fs/rmtree): fix huge memory usage (#7073)
Fixes zammad#15258

This adds a sane concurrency limit of 2 per depth level.

Co-authored-by: Florent BEAUCHAMP <florent.beauchamp@vates.fr>
2023-10-06 09:52:11 +02:00
25 changed files with 1158 additions and 832 deletions

View File

@@ -624,14 +624,18 @@ export default class RemoteHandlerAbstract {
const files = await this._list(dir)
await asyncEach(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
this._unlink(`${dir}/${file}`).catch(
error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
},
// real unlink concurrency will be 2**max directory depth
{ concurrency: 2 }
)
)
return this._rmtree(dir)
}

View File

@@ -2,6 +2,8 @@
## **next**
- Ability to snapshot/copy a VM from its view (PR [#7087](https://github.com/vatesfr/xen-orchestra/pull/7087))
## **0.1.4** (2023-10-03)
- Ability to migrate selected VMs to another host (PR [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040))

View File

@@ -1,6 +1,9 @@
<template>
<MenuItem
v-tooltip="!areAllSelectedVmsHalted && $t('selected-vms-in-execution')"
v-tooltip="
!areAllSelectedVmsHalted &&
$t(isSingleAction ? 'vm-is-running' : 'selected-vms-in-execution')
"
:busy="areSomeSelectedVmsCloning"
:disabled="isDisabled"
:icon="faCopy"
@@ -22,6 +25,7 @@ import { computed } from "vue";
const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
isSingleAction?: boolean;
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();

View File

@@ -11,6 +11,23 @@
</template>
<VmActionPowerStateItems :vm-refs="[vm.$ref]" />
</AppMenu>
<AppMenu v-if="vm !== undefined" placement="bottom-end" shadow>
<template #trigger="{ open, isOpen }">
<UiButton
:active="isOpen"
:icon="faEllipsisVertical"
@click="open"
transparent
class="more-actions-button"
v-tooltip="{
placement: 'left',
content: $t('more-actions'),
}"
/>
</template>
<VmActionCopyItem :selected-refs="[vm.$ref]" is-single-action />
<VmActionSnapshotItem :vm-refs="[vm.$ref]" />
</AppMenu>
</template>
</TitleBar>
</template>
@@ -21,11 +38,15 @@ import TitleBar from "@/components/TitleBar.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import {
faAngleDown,
faDisplay,
faEllipsisVertical,
faPowerOff,
} from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -40,3 +61,9 @@ const vm = computed(() =>
const name = computed(() => vm.value?.name_label);
</script>
<style lang="postcss">
.more-actions-button {
font-size: 1.2em;
}
</style>

View File

@@ -86,6 +86,7 @@
"loading-hosts": "Loading hosts…",
"log-out": "Log out",
"login": "Login",
"more-actions": "More actions",
"migrate": "Migrate",
"migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
"n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch",
@@ -173,6 +174,7 @@
"vcpus": "vCPUs",
"vcpus-used": "vCPUs used",
"version": "Version",
"vm-is-running": "The VM is running",
"vms": "VMs",
"xo-lite-under-construction": "XOLite is under construction"
}

View File

@@ -86,6 +86,7 @@
"loading-hosts": "Chargement des hôtes…",
"log-out": "Se déconnecter",
"login": "Connexion",
"more-actions": "Plus d'actions",
"migrate": "Migrer",
"migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
"n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch",
@@ -173,6 +174,7 @@
"vcpus": "vCPUs",
"vcpus-used": "vCPUs utilisés",
"version": "Version",
"vm-is-running": "La VM est en cours d'exécution",
"vms": "VMs",
"xo-lite-under-construction": "XOLite est en construction"
}

View File

@@ -24,6 +24,8 @@ const serializeError = error => ({
})
export default class Tasks extends EventEmitter {
#logsToClearOnSuccess = new Set()
// contains consolidated logs of all live and finished tasks
#store
@@ -36,6 +38,22 @@ export default class Tasks extends EventEmitter {
this.#tasks.delete(id)
},
onTaskUpdate: async taskLog => {
const { id, status } = taskLog
if (status !== 'pending') {
if (this.#logsToClearOnSuccess.has(id)) {
this.#logsToClearOnSuccess.delete(id)
if (status === 'success') {
try {
await this.#store.del(id)
} catch (error) {
warn('failure on deleting task log from store', { error, taskLog })
}
return
}
}
}
// Error objects are not JSON-ifiable by default
const { result } = taskLog
if (result instanceof Error && result.toJSON === undefined) {
@@ -135,7 +153,10 @@ export default class Tasks extends EventEmitter {
*
* @returns {Task}
*/
create({ name, objectId, userId = this.#app.apiContext?.user?.id, type, ...props }) {
create(
{ name, objectId, userId = this.#app.apiContext?.user?.id, type, ...props },
{ clearLogOnSuccess = false } = {}
) {
const tasks = this.#tasks
const task = new Task({ properties: { ...props, name, objectId, userId, type }, onProgress: this.#onProgress })
@@ -152,6 +173,9 @@ export default class Tasks extends EventEmitter {
task.id = id
tasks.set(id, task)
if (clearLogOnSuccess) {
this.#logsToClearOnSuccess.add(id)
}
return task
}

View File

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

View File

@@ -1,6 +1,8 @@
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'
@@ -31,7 +33,38 @@ class Host {
*
* @param {string} ref - Opaque reference of the host
*/
async smartReboot($defer, ref) {
async smartReboot($defer, ref, bypassBlockedSuspend = false, bypassCurrentVmCheck = false) {
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 = []
if (await this.getField('host', ref, 'enabled')) {
await this.callAsync('host.disable', ref)
@@ -42,13 +75,8 @@ class Host {
})
}
let currentVmRef
try {
currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid())
} catch (error) {}
await asyncEach(
await this.getField('host', ref, 'resident_VMs'),
residentVmRefs,
async vmRef => {
if (vmRef === currentVmRef) {
return

View File

@@ -0,0 +1,37 @@
import upperFirst from 'lodash/upperFirst.js'
import { incorrectState } from 'xo-common/api-errors.js'
export default class Vtpm {
async create({ is_unique = false, VM }) {
const pool = this.pool
// If VTPM.create is called on a pool that doesn't support VTPM, the errors aren't explicit.
// See https://github.com/xapi-project/xen-api/issues/5186
if (pool.restrictions.restrict_vtpm !== 'false') {
throw incorrectState({
actual: pool.restrictions.restrict_vtpm,
expected: 'false',
object: pool.uuid,
property: 'restrictions.restrict_vtpm',
})
}
try {
return await this.call('VTPM.create', VM, is_unique)
} catch (error) {
const { code, params } = error
if (code === 'VM_BAD_POWER_STATE') {
const [, expected, actual] = params
// In `VM_BAD_POWER_STATE` errors, the power state is lowercased
throw incorrectState({
actual: upperFirst(actual),
expected: upperFirst(expected),
object: await this.getField('VM', VM, 'uuid'),
property: 'power_state',
})
}
throw error
}
}
}

View File

@@ -7,10 +7,17 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Host/Advanced] Allow to force _Smart reboot_ if some resident VMs have the suspend operation blocked [Forum#7136](https://xcp-ng.org/forum/topic/7136/suspending-vms-during-host-reboot/23) (PR [#7025](https://github.com/vatesfr/xen-orchestra/pull/7025))
- [Plugin/backup-report] Errors are now listed in XO tasks
- [PIF] Show network name in PIF selectors (PR [#7081](https://github.com/vatesfr/xen-orchestra/pull/7081))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Rolling Pool Update] After the update, when migrating VMs back to their host, do not migrate VMs that are already on the right host [Forum#7802](https://xcp-ng.org/forum/topic/7802) (PR [#7071](https://github.com/vatesfr/xen-orchestra/pull/7071))
- [Home] Fix OS icons alignment (PR [#7090](https://github.com/vatesfr/xen-orchestra/pull/7090))
### Packages to release
> When modifying a package, add it here with its release type.
@@ -27,4 +34,10 @@
<!--packages-start-->
- @xen-orchestra/mixins minor
- @xen-orchestra/xapi minor
- xo-server minor
- xo-server-backup-reports minor
- xo-web minor
<!--packages-end-->

View File

@@ -362,7 +362,7 @@ XO will try to find the right prefix for each IP address. If it can't find a pre
- Assign it to object types:
- Virtualization > cluster
- Virtualization > virtual machine
- Virtualization > interface`
- Virtualization > interface
![](./assets/customfield.png)

View File

@@ -90,6 +90,8 @@ const formatSpeed = (bytes, milliseconds) =>
})
: 'N/A'
const noop = Function.prototype
const NO_VMS_MATCH_THIS_PATTERN = 'no VMs match this pattern'
const NO_SUCH_OBJECT_ERROR = 'no such object'
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
@@ -193,13 +195,17 @@ const toMarkdown = parts => {
class BackupReportsXoPlugin {
constructor(xo) {
this._xo = xo
this._eventListener = async (...args) => {
try {
await this._report(...args)
} catch (error) {
logger.warn(error)
}
}
const report = this._report
this._report = (...args) =>
xo.tasks
.create(
{ type: 'xo:xo-server-backup-reports:sendReport', name: 'Sending backup report', runId: args[0] },
{ clearLogOnSuccess: true }
)
.run(() => report.call(this, ...args))
this._eventListener = (...args) => this._report(...args).catch(noop)
}
configure({ toMails, toXmpp }) {
@@ -595,24 +601,28 @@ class BackupReportsXoPlugin {
})
}
_sendReport({ mailReceivers, markdown, subject, success }) {
async _sendReport({ mailReceivers, markdown, subject, success }) {
if (mailReceivers === undefined || mailReceivers.length === 0) {
mailReceivers = this._mailsReceivers
}
const xo = this._xo
return Promise.all([
xo.sendEmail !== undefined &&
xo.sendEmail({
to: mailReceivers,
subject,
markdown,
}),
xo.sendToXmppClient !== undefined &&
xo.sendToXmppClient({
to: this._xmppReceivers,
message: markdown,
}),
const promises = [
mailReceivers !== undefined &&
(xo.sendEmail === undefined
? Promise.reject(new Error('transport-email plugin not enabled'))
: xo.sendEmail({
to: mailReceivers,
subject,
markdown,
})),
this._xmppReceivers !== undefined &&
(xo.sendEmail === undefined
? Promise.reject(new Error('transport-xmpp plugin not enabled'))
: xo.sendToXmppClient({
to: this._xmppReceivers,
message: markdown,
})),
xo.sendSlackMessage !== undefined &&
xo.sendSlackMessage({
message: markdown,
@@ -622,7 +632,22 @@ class BackupReportsXoPlugin {
status: success ? 'OK' : 'CRITICAL',
message: markdown,
}),
])
]
const errors = []
const pushError = errors.push.bind(errors)
await Promise.all(promises.filter(Boolean).map(_ => _.catch(pushError)))
if (errors.length !== 0) {
throw new AggregateError(
errors,
errors
.map(_ => _.message)
.filter(_ => _ != null && _.length !== 0)
.join(', ')
)
}
}
_legacyVmHandler(status) {

View File

@@ -119,7 +119,15 @@ set.resolve = {
// FIXME: set force to false per default when correctly implemented in
// UI.
export async function restart({ bypassBackupCheck = false, host, force = false, suspendResidentVms }) {
export async function restart({
bypassBackupCheck = false,
host,
force = false,
suspendResidentVms,
bypassBlockedSuspend = force,
bypassCurrentVmCheck = force,
}) {
if (bypassBackupCheck) {
log.warn('host.restart with argument "bypassBackupCheck" set to true', { hostId: host.id })
} else {
@@ -127,7 +135,9 @@ export async function restart({ bypassBackupCheck = false, host, force = false,
}
const xapi = this.getXapi(host)
return suspendResidentVms ? xapi.host_smartReboot(host._xapiRef) : xapi.rebootHost(host._xapiId, force)
return suspendResidentVms
? xapi.host_smartReboot(host._xapiRef, bypassBlockedSuspend, bypassCurrentVmCheck)
: xapi.rebootHost(host._xapiId, force)
}
restart.description = 'restart the host'
@@ -137,6 +147,14 @@ restart.params = {
type: 'boolean',
optional: true,
},
bypassBlockedSuspend: {
type: 'boolean',
optional: true,
},
bypassCurrentVmCheck: {
type: 'boolean',
optional: true,
},
id: { type: 'string' },
force: {
type: 'boolean',

View File

@@ -0,0 +1,29 @@
export async function create({ vm }) {
const xapi = this.getXapi(vm)
const vtpmRef = await xapi.VTPM_create({ VM: vm._xapiRef })
return xapi.getField('VTPM', vtpmRef, 'uuid')
}
create.description = 'create a VTPM'
create.params = {
id: { type: 'string' },
}
create.resolve = {
vm: ['id', 'VM', 'administrate'],
}
export async function destroy({ vtpm }) {
await this.getXapi(vtpm).call('VTPM.destroy', vtpm._xapiRef)
}
destroy.description = 'destroy a VTPM'
destroy.params = {
id: { type: 'string' },
}
destroy.resolve = {
vtpm: ['id', 'VTPM', 'administrate'],
}

View File

@@ -118,6 +118,7 @@ const TRANSFORMS = {
},
suspendSr: link(obj, 'suspend_image_SR'),
zstdSupported: obj.restrictions.restrict_zstd_export === 'false',
vtpmSupported: obj.restrictions.restrict_vtpm === 'false',
// TODO
// - ? networks = networksByPool.items[pool.id] (network.$pool.id)
@@ -413,6 +414,7 @@ const TRANSFORMS = {
suspendSr: link(obj, 'suspend_SR'),
tags: obj.tags,
VIFs: link(obj, 'VIFs'),
VTPMs: link(obj, 'VTPMs'),
virtualizationMode: domainType,
// deprecated, use pvDriversVersion instead
@@ -841,6 +843,14 @@ const TRANSFORMS = {
vgpus: link(obj, 'VGPUs'),
}
},
vtpm(obj) {
return {
type: 'VTPM',
vm: link(obj, 'VM'),
}
},
}
// ===================================================================

View File

@@ -629,7 +629,13 @@ export default {
continue
}
const residentVms = host.$resident_VMs.map(vm => vm.uuid)
for (const vmId of vmIds) {
if (residentVms.includes(vmId)) {
continue
}
try {
await this.migrateVm(vmId, this, hostId)
} catch (err) {

View File

@@ -138,14 +138,20 @@ export default class {
async authenticateUser(credentials, userData) {
const { tasks } = this._app
const task = await tasks.create({
type: 'xo:authentication:authenticateUser',
name: 'XO user authentication',
credentials: replace(credentials),
userData,
})
const task = await tasks.create(
{
type: 'xo:authentication:authenticateUser',
name: 'XO user authentication',
credentials: replace(credentials),
userData,
},
{
// only keep trace of failed attempts
clearLogOnSuccess: true,
}
)
const result = await task.run(async () => {
return task.run(async () => {
// don't even attempt to authenticate with empty password
const { password } = credentials
if (password === '') {
@@ -177,11 +183,6 @@ export default class {
delete failures[username]
return result
})
// only keep trace of failed attempts
await tasks.deleteLog(task.id)
return result
}
// -----------------------------------------------------------------

View File

@@ -6,7 +6,7 @@ import React from 'react'
const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
props.className = classNames(
props.className,
icon !== undefined ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
icon != null ? `xo-icon-${icon}` : 'fa', // Misaligned problem modification: if no icon or null, apply 'fa'
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
color,
fixedWidth && 'fa-fw'

View File

@@ -963,9 +963,13 @@ const messages = {
enableHostLabel: 'Enable',
disableHostLabel: 'Disable',
restartHostAgent: 'Restart toolstack',
smartRebootBypassCurrentVmCheck:
'As the XOA is hosted on the host that is scheduled for a reboot, it will also be restarted. Consequently, XO won\'t be able to resume VMs, and VMs with the "Protect from accidental shutdown" option enabled will not have this option reactivated automatically.',
smartRebootHostLabel: 'Smart reboot',
smartRebootHostTooltip: 'Suspend resident VMs, reboot host and resume VMs automatically',
forceRebootHostLabel: 'Force reboot',
forceSmartRebootHost:
'Smart Reboot failed because {nVms, number} VM{nVms, plural, one {} other {s}} ha{nVms, plural, one {s} other {ve}} {nVms, plural, one {its} other {their}} Suspend operation blocked. Would you like to force?',
rebootHostLabel: 'Reboot',
noHostsAvailableErrorTitle: 'Error while restarting host',
noHostsAvailableErrorMessage:

View File

@@ -299,6 +299,63 @@ Vdi.defaultProps = {
// ===================================================================
export const Pif = decorate([
connectStore(() => {
const getObject = createGetObject()
const getNetwork = createGetObject(createSelector(getObject, pif => get(() => pif.$network)))
// FIXME: props.self ugly workaround to get object as a self user
return (state, props) => ({
pif: getObject(state, props, props.self),
network: getNetwork(state, props),
})
}),
({ id, showNetwork, pif, network }) => {
if (pif === undefined) {
return unknowItem(id, 'PIF')
}
const { carrier, device, deviceName, vlan } = pif
const showExtraInfo = deviceName || vlan !== -1 || (showNetwork && network !== undefined)
return (
<span>
<Icon icon='network' color={carrier ? 'text-success' : 'text-danger'} /> {device}
{showExtraInfo && (
<span>
{' '}
({deviceName}
{vlan !== -1 && (
<span>
{' '}
-{' '}
{_('keyValue', {
key: _('pifVlanLabel'),
value: vlan,
})}
</span>
)}
{showNetwork && network !== undefined && <span> - {network.name_label}</span>})
</span>
)}
</span>
)
},
])
Pif.propTypes = {
id: PropTypes.string.isRequired,
self: PropTypes.bool,
showNetwork: PropTypes.bool,
}
Pif.defaultProps = {
self: false,
showNetwork: false,
}
// ===================================================================
export const Network = decorate([
connectStore(() => {
const getObject = createGetObject()
@@ -561,24 +618,8 @@ const xoItemToRender = {
),
// PIF.
PIF: ({ carrier, device, deviceName, vlan }) => (
<span>
<Icon icon='network' color={carrier ? 'text-success' : 'text-danger'} /> {device}
{(deviceName !== '' || vlan !== -1) && (
<span>
{' '}
({deviceName}
{deviceName !== '' && vlan !== -1 && ' - '}
{vlan !== -1 &&
_('keyValue', {
key: _('pifVlanLabel'),
value: vlan,
})}
)
</span>
)}
</span>
),
PIF: props => <Pif {...props} />,
// Tags.
tag: tag => (
<span>

View File

@@ -251,6 +251,7 @@ class GenericSelect extends React.Component {
? `${option.xoItem.type}-resourceSet`
: undefined,
memoryFree: option.xoItem.type === 'host' || undefined,
showNetwork: true,
})}
</span>
)

View File

@@ -16,6 +16,7 @@ import {
incorrectState,
noHostsAvailable,
operationBlocked,
operationFailed,
vmLacksFeature,
} from 'xo-common/api-errors'
@@ -821,42 +822,89 @@ export const setRemoteSyslogHost = (host, syslogDestination) =>
export const setRemoteSyslogHosts = (hosts, syslogDestination) =>
Promise.all(map(hosts, host => setRemoteSyslogHost(host, syslogDestination)))
export const restartHost = (host, force = false, suspendResidentVms = false) =>
confirm({
export const restartHost = async (
host,
force = false,
suspendResidentVms = false,
bypassBlockedSuspend = false,
bypassCurrentVmCheck = false
) => {
await confirm({
title: _('restartHostModalTitle'),
body: _('restartHostModalMessage'),
}).then(
() =>
_call('host.restart', { id: resolveId(host), force, suspendResidentVms })
.catch(async error => {
if (
forbiddenOperation.is(error, {
reason: `A backup may run on the pool: ${host.$poolId}`,
}) ||
forbiddenOperation.is(error, {
reason: `A backup is running on the pool: ${host.$poolId}`,
})
) {
await confirm({
body: (
<p className='text-warning'>
<Icon icon='alarm' /> {_('bypassBackupHostModalMessage')}
</p>
),
title: _('restartHostModalTitle'),
})
return _call('host.restart', { id: resolveId(host), force, suspendResidentVms, bypassBackupCheck: true })
}
throw error
})
.catch(error => {
if (noHostsAvailable.is(error)) {
alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage'))
}
throw error
}),
noop
)
})
return _restartHost({ host, force, suspendResidentVms, bypassBlockedSuspend, bypassCurrentVmCheck })
}
const _restartHost = async ({ host, ...opts }) => {
opts = { ...opts, id: resolveId(host) }
try {
await _call('host.restart', opts)
} catch (error) {
if (cantSuspend(error)) {
await confirm({
body: (
<p>
<Icon icon='alarm' /> {_('forceSmartRebootHost', { nVms: error.data.actual.length })}
</p>
),
title: _('restartHostModalTitle'),
})
return _restartHost({ ...opts, host, bypassBlockedSuspend: true })
}
if (xoaOnHost(error)) {
await confirm({
body: (
<p>
<Icon icon='alarm' /> {_('smartRebootBypassCurrentVmCheck')}
</p>
),
title: _('restartHostModalTitle'),
})
return _restartHost({ ...opts, host, bypassCurrentVmCheck: true })
}
if (backupIsRunning(error, host.$poolId)) {
await confirm({
body: (
<p className='text-warning'>
<Icon icon='alarm' /> {_('bypassBackupHostModalMessage')}
</p>
),
title: _('restartHostModalTitle'),
})
return _restartHost({ ...opts, host, bypassBackupCheck: true })
}
if (noHostsAvailableErrCheck(error)) {
alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage'))
}
throw error
}
}
// ---- Restart Host errors
const cantSuspend = err =>
err !== undefined &&
incorrectState.is(err, {
object: 'suspendBlocked',
})
const xoaOnHost = err =>
err !== undefined &&
operationFailed.is(err, {
code: 'xoaOnHost',
})
const backupIsRunning = (err, poolId) =>
err !== undefined &&
(forbiddenOperation.is(err, {
reason: `A backup may run on the pool: ${poolId}`,
}) ||
forbiddenOperation.is(err, {
reason: `A backup is running on the pool: ${poolId}`,
}))
const noHostsAvailableErrCheck = err => err !== undefined && noHostsAvailable.is(err)
export const restartHosts = (hosts, force = false) => {
const nHosts = size(hosts)

View File

@@ -76,7 +76,7 @@ const downloadLogs = async uuid => {
const forceReboot = host => restartHost(host, true)
const smartReboot = ALLOW_SMART_REBOOT
? host => restartHost(host, false, true) // don't force, suspend resident VMs
? host => restartHost(host, false, true, false, false) // don't force, suspend resident VMs, don't bypass blocked suspend, don't bypass current VM check
: () => {}
const formatPack = ({ name, author, description, version }, key) => (

1449
yarn.lock

File diff suppressed because it is too large Load Diff