Compare commits
17 Commits
test
...
updateChan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969b64d575 | ||
|
|
fa56e8453a | ||
|
|
eb64937bc6 | ||
|
|
1502ac317d | ||
|
|
8bfe293414 | ||
|
|
2e634a9d1c | ||
|
|
bea771ca90 | ||
|
|
99e3622f31 | ||
|
|
a16522241e | ||
|
|
b86cb12649 | ||
|
|
2af74008b2 | ||
|
|
2e689592f1 | ||
|
|
3f8436b58b | ||
|
|
e3dd59d684 | ||
|
|
549d9b70a9 | ||
|
|
3bf6aae103 | ||
|
|
afb110c473 |
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
37
@xen-orchestra/xapi/vtpm.mjs
Normal file
37
@xen-orchestra/xapi/vtpm.mjs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
29
packages/xo-server/src/api/vtpm.mjs
Normal file
29
packages/xo-server/src/api/vtpm.mjs
Normal 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'],
|
||||
}
|
||||
@@ -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'),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -251,6 +251,7 @@ class GenericSelect extends React.Component {
|
||||
? `${option.xoItem.type}-resourceSet`
|
||||
: undefined,
|
||||
memoryFree: option.xoItem.type === 'host' || undefined,
|
||||
showNetwork: true,
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user