Compare commits

...

31 Commits

Author SHA1 Message Date
Florent BEAUCHAMP
21e22626b6 wip 2023-10-30 14:39:23 +01:00
Florent BEAUCHAMP
06cddc516d refactor: pass baseVdi to importIncrementalVm 2023-10-24 17:38:46 +02:00
Florent BEAUCHAMP
d7f8d411b5 disc-oriented approach 2023-10-24 14:20:13 +02:00
Florent BEAUCHAMP
16570c6c66 reset 2023-10-24 13:18:49 +02:00
Florent BEAUCHAMP
b0789a7e31 implement same sr and vdi delta check 2023-10-24 13:17:10 +02:00
Florent BEAUCHAMP
5e71b63333 feat(@xen-orchestra/backups):ImportVmBackup 2023-10-24 13:17:10 +02:00
Florent BEAUCHAMP
3d616fde43 wip 2023-10-24 13:17:10 +02:00
Pierre Donias
8752487280 docs(installation): add nfs-common dependency for Debian/Ubuntu (#7108) 2023-10-18 22:50:29 +02:00
Pierre Donias
4b12a6d31d fix(xo-server-usage-report): handle null and nested stats (#7092)
Introduced by 083483645e

Fixes Zammad#18120
Fixes Zammad#18266

- Always assume that data can be `null`
- Handle edge cases where all values are `null`
- Properly handle nested RRD collections: collections have different depths (`memory`: 1, `cpus[0]`: 2, `pifs.rx[0]`: 3, ...). This PR replaces `getLastDays` which wouldn't handle those depths properly, with `getDeepLastValues` which is run on the whole stat object and doesn't assume the depth of the collections. It finds any Array at any depth and slices it to only keep the last N values.
2023-10-18 22:50:08 +02:00
Julien Fontanet
2924f82754 fix(xo-web): don't sign out on connection error (#7103)
May fix zammad#17717

Introduced by 005ab47d9
2023-10-18 18:07:16 +02:00
Pierre Donias
9b236a6191 fix(netbox/test): test custom fields first (#7104)
More atomic and it makes more sense for users to check that the Netbox
configuration is correct before doing any write operations
2023-10-18 11:56:10 +02:00
Julien Fontanet
a3b8553cec fix(xo-server,xo-web): fix total number of VDIs to coalesce (#7098)
Fixes #7016

Summing all chains does take not common chains into account, the total must be computed on the server side.
2023-10-18 11:52:43 +02:00
Pierre Donias
00a1778a6d feat(lite): set color-scheme CSS property to "dark" in dark mode (#7101) 2023-10-17 16:50:13 +02:00
MlssFrncJrg
3b6bc629bc fix(xo-web/home): fix misaligned descriptions (#7090) 2023-10-16 15:53:35 +02:00
Pierre Donias
04dfd9a02c fix(xo-server-usage-report): use @xen-orchestra/log to log errors (#7096)
Fixes Zammad#14579
Fixes Zammad#18183

Better handles error objects with a circular structure and avoids "Converting
circular structure to JSON" error on stringify
2023-10-16 10:07:57 +02:00
Pierre Donias
fb52868074 fix(xo-server/patching): always check that XS credentials are configured on XS (#7093)
Introduced by a30d962b1d
2023-10-13 16:49:04 +02:00
Pierre Donias
77d53d2abf fix(xo-server/patching): always pass xsCredentials to installPatches on XS (#7089)
Fixes Zammad#18284

Introduced by a30d962b1d
2023-10-13 11:45:17 +02:00
Julien Fontanet
6afb87def1 feat(xo-server/vm.set): support xenStoreData
Fixes #7055
2023-10-13 11:26:48 +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
38 changed files with 1442 additions and 945 deletions

View File

@@ -14,7 +14,26 @@ export class ImportVmBackup {
this._xapi = xapi
}
async #detectBaseVdis(){
const vmUuid = this._metadata.vm.uuid
const vm = await this._xapi.getRecordByUuid('VM', vmUuid)
const disks = vm.$getDisks()
const snapshots = {}
console.log({disks})
for (const disk of Object.values(disks)){
console.log({snapshots: disk.snapshots})
for(const snapshotRef of disk.snapshots){
const snapshot = await this._xapi.getRecordByUuid('VDI', snapshotRef)
snapshots[snapshot.uuid] = disk.uuid
}
}
console.log({snapshots})
return snapshots
}
async run() {
console.log('RUN')
const adapter = this._adapter
const metadata = this._metadata
const isFull = metadata.mode === 'full'
@@ -22,18 +41,23 @@ export class ImportVmBackup {
const sizeContainer = { size: 0 }
let backup
if (isFull) {
backup = await adapter.readFullVmBackup(metadata)
watchStreamSize(backup, sizeContainer)
} else {
console.log('restore delta')
assert.strictEqual(metadata.mode, 'delta')
const ignoredVdis = new Set(
Object.entries(this._importIncrementalVmSettings.mapVdisSrs)
.filter(([_, srUuid]) => srUuid === null)
.map(([vdiUuid]) => vdiUuid)
)
backup = await adapter.readIncrementalVmBackup(metadata, ignoredVdis)
//const vdiSnap = await this._xapi.getRecord('VDI-snapshot','83c96977-9bc5-483d-b816-4c96622fb5e6')
//console.log({vdiSnap})
const baseVdis = this.#detectBaseVdis()
backup = await adapter.readIncrementalVmBackup(metadata, ignoredVdis, { baseVdis })
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
}
@@ -49,7 +73,7 @@ export class ImportVmBackup {
? await xapi.VM_import(backup, srRef)
: await importIncrementalVm(backup, await xapi.getRecord('SR', srRef), {
...this._importIncrementalVmSettings,
detectBase: false,
baseVdis
})
await Promise.all([

View File

@@ -2,7 +2,7 @@ import { asyncEach } from '@vates/async-each'
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
import { compose } from '@vates/compose'
import { createLogger } from '@xen-orchestra/log'
import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } from 'vhd-lib'
import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic ,Constants} from 'vhd-lib'
import { decorateMethodsWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped.js'
import { dirname, join, resolve } from 'node:path'
@@ -694,8 +694,8 @@ export class RemoteAdapter {
return container.size
}
// open the hierarchy of ancestors until we find a full one
async _createVhdStream(handler, path, { useChain }) {
// open the hierarchy of ancestors until we find a usable one
async _createVhdStream(handler, path, { useChain, snapshotedVdis }) {
const disposableSynthetic = useChain ? await VhdSynthetic.fromVhdChain(handler, path) : await openVhd(handler, path)
// I don't want the vhds to be disposed on return
// but only when the stream is done ( or failed )
@@ -713,7 +713,67 @@ export class RemoteAdapter {
}
const synthetic = disposableSynthetic.value
await synthetic.readBlockAllocationTable()
const stream = await synthetic.stream()
let stream
// try to create a stream that will reuse any data already present on the host storage
// by looking for an existing snapshot matching one of the vhd in the chain
// and transfer only the differential
if (snapshotedVdis) {
try{
let vhdPaths = await handler.list(dirname(path), {filter: path=>path.endsWith('.vhd')})
stream = await Disposable.use(async function *(){
const vhdChilds = {}
const vhds = yield Disposable.all(vhdPaths.map(path => openVhd(handler, path, opts)))
for(const vhd of vhds){
vhdChilds[vhd.header.parentUuid] = vhdChilds[vhd.header.parentUuid] ?? []
vhdChilds[vhd.header.parentUuid].push(vhd)
}
let chain = []
let current = synthetic
// @todo : special case : we want to restore a vdi
// that still have its snapshot => nothing to transfer
while(current != undefined){
// find the child VDI of path
const childs = vhdChilds[current.footer.uuid]
// more than one => break
// inexistant => break
if(childs.length !== 1){
break
}
const child = childs[0]
// not a differential => we won't have a list of block changed
// no need to continue looking
if(child.footer.diskType !== Constants.DISK_TYPES.DIFFERENCING){
break
}
// we have a snapshot
if(snapshotedVdis[current.footer.uuid] !== undefined){
const descendants = VhdSynthetic.open(handler)
negativeVhd = new NegativeVhd(synthetic, descendants)
return negative.stream()
} else {
// continue to look into the chain
// hoping we'll found a match deeper
current = child
chain.unshift(current)
}
}
})
}catch(error){
warn("error while trying to reuse a snapshot, fallback to legacy restore", {error})
}
}
// fallback
if (stream === undefined) {
stream = await synthetic.stream()
}
stream.on('end', disposeOnce)
stream.on('close', disposeOnce)
@@ -721,7 +781,7 @@ export class RemoteAdapter {
return stream
}
async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true, snapshotedVdis } = {}) {
const handler = this._handler
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const dir = dirname(metadata._filename)
@@ -729,7 +789,7 @@ export class RemoteAdapter {
const streams = {}
await asyncMapSettled(Object.keys(vdis), async ref => {
streams[`${ref}.vhd`] = await this._createVhdStream(handler, join(dir, vhds[ref]), { useChain })
streams[`${ref}.vhd`] = await this._createVhdStream(handler, join(dir, vhds[ref]), { useChain, snapshotedVdis })
})
return {

View File

@@ -143,22 +143,11 @@ export async function exportIncrementalVm(
)
}
export const importIncrementalVm = defer(async function importIncrementalVm(
$defer,
incrementalVm,
sr,
{ cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
) {
const { version } = incrementalVm
if (compareVersions(version, '1.0.0') < 0) {
throw new Error(`Unsupported delta backup version: ${version}`)
}
const vmRecord = incrementalVm.vm
const xapi = sr.$xapi
// @todo movve this to incremental replication
async function detectBaseVdis(vmRecord, sr) {
let baseVm
if (detectBase) {
const xapi = sr.$xapi
const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
if (remoteBaseVmUuid) {
baseVm = find(
@@ -170,7 +159,30 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
}
}
const baseVdis = {}
baseVm &&
baseVm.$VBDs.forEach(vbd => {
const vdi = vbd.$VDI
if (vdi !== undefined) {
baseVdis[vdi.other_config[TAG_COPY_SRC]] = vbd.$VDI
}
})
return baseVdis
}
export const importIncrementalVm = defer(async function importIncrementalVm(
$defer,
incrementalVm,
sr,
{ baseVdis = {}, cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
) {
const { version } = incrementalVm
if (compareVersions(version, '1.0.0') < 0) {
throw new Error(`Unsupported delta backup version: ${version}`)
}
const vmRecord = incrementalVm.vm
const xapi = sr.$xapi
const cache = new Map()
const mapVdisSrRefs = {}
@@ -178,14 +190,9 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
}
const baseVdis = {}
baseVm &&
baseVm.$VBDs.forEach(vbd => {
const vdi = vbd.$VDI
if (vdi !== undefined) {
baseVdis[vbd.VDI] = vbd.$VDI
if (detectBase) {
baseVdis = await detectBaseVdis(vmRecord, sr)
}
})
const vdiRecords = incrementalVm.vdis
// 0. Create suspend_VDI
@@ -249,10 +256,11 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
await asyncMap(Object.keys(vdiRecords), async vdiRef => {
const vdi = vdiRecords[vdiRef]
let newVdi
// @todo how to rewrite this condition when giving directly a baseVdi ?
const remoteBaseVdiUuid = detectBase && vdi.other_config[TAG_BASE_DELTA]
if (remoteBaseVdiUuid) {
const baseVdi = find(baseVdis, vdi => vdi.other_config[TAG_COPY_SRC] === remoteBaseVdiUuid)
const baseVdi = baseVdis[vdi.other_config[TAG_COPY_SRC]]
// @todo : should be an error only for detectBase
if (!baseVdi) {
throw new Error(`missing base VDI (copy of ${remoteBaseVdiUuid})`)
}

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 => {
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

@@ -59,6 +59,8 @@
}
:root.dark {
color-scheme: dark;
--color-blue-scale-000: #ffffff;
--color-blue-scale-100: #e5e5e7;
--color-blue-scale-200: #9899a5;

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,23 @@
> 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))
- [RPU] Fix "XenServer credentials not found" when running a Rolling Pool Update on a XenServer pool (PR [#7089](https://github.com/vatesfr/xen-orchestra/pull/7089))
- [Usage report] Fix "Converting circular structure to JSON" error
- [Home] Fix OS icons alignment (PR [#7090](https://github.com/vatesfr/xen-orchestra/pull/7090))
- [SR/Advanced] Fix the total number of VDIs to coalesce by taking into account common chains [#7016](https://github.com/vatesfr/xen-orchestra/issues/7016) (PR [#7098](https://github.com/vatesfr/xen-orchestra/pull/7098))
- Don't require to sign in again in XO after losing connection to XO Server (e.g. when restarting or upgrading XO) (PR [#7103](https://github.com/vatesfr/xen-orchestra/pull/7103))
- [Usage report] Fix "Converting circular structure to JSON" error (PR [#7096](https://github.com/vatesfr/xen-orchestra/pull/7096))
- [Usage report] Fix "Cannot convert undefined or null to object" error (PR [#7092](https://github.com/vatesfr/xen-orchestra/pull/7092))
### Packages to release
> When modifying a package, add it here with its release type.
@@ -27,4 +40,12 @@
<!--packages-start-->
- @xen-orchestra/mixins minor
- @xen-orchestra/xapi minor
- xo-server minor
- xo-server-backup-reports minor
- xo-server-netbox patch
- xo-server-usage-report patch
- 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

@@ -106,7 +106,7 @@ XO needs the following packages to be installed. Redis is used as a database by
For example, on Debian/Ubuntu:
```sh
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils nfs-common
```
On Fedora/CentOS like:

View File

@@ -37,7 +37,7 @@ function mapProperties(object, mapping) {
}
async function showDetails(handler, path) {
const vhd = new VhdFile(handler, resolve(path))
const {value: vhd} = await openVhd(handler, resolve(path))
try {
await vhd.readHeaderAndFooter()

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({
const promises = [
mailReceivers !== undefined &&
(xo.sendEmail === undefined
? Promise.reject(new Error('transport-email plugin not enabled'))
: xo.sendEmail({
to: mailReceivers,
subject,
markdown,
}),
xo.sendToXmppClient !== undefined &&
xo.sendToXmppClient({
})),
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

@@ -103,6 +103,8 @@ class Netbox {
}
async test() {
await this.#checkCustomFields()
const randomSuffix = Math.random().toString(36).slice(2, 11)
const name = '[TMP] Xen Orchestra Netbox plugin test - ' + randomSuffix
await this.#request('/virtualization/cluster-types/', 'POST', {
@@ -113,8 +115,6 @@ class Netbox {
})
const nbClusterTypes = await this.#request(`/virtualization/cluster-types/?name=${encodeURIComponent(name)}`)
await this.#checkCustomFields()
if (nbClusterTypes.length !== 1) {
throw new Error('Could not properly write and read Netbox')
}

View File

@@ -12,9 +12,9 @@ import {
filter,
find,
forEach,
get,
isFinite,
map,
mapValues,
orderBy,
round,
values,
@@ -204,6 +204,11 @@ function computeMean(values) {
}
})
// No values to work with, return null
if (n === 0) {
return null
}
return sum / n
}
@@ -226,7 +231,7 @@ function getTop(objects, options) {
object => {
const value = object[opt]
return isNaN(value) ? -Infinity : value
return isNaN(value) || value === null ? -Infinity : value
},
'desc'
).slice(0, 3),
@@ -244,7 +249,9 @@ function computePercentage(curr, prev, options) {
return zipObject(
options,
map(options, opt =>
prev[opt] === 0 || prev[opt] === null ? 'NONE' : `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
prev[opt] === 0 || prev[opt] === null || curr[opt] === null
? 'NONE'
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
)
)
}
@@ -257,7 +264,15 @@ function getDiff(oldElements, newElements) {
}
function getMemoryUsedMetric({ memory, memoryFree = memory }) {
return map(memory, (value, key) => value - memoryFree[key])
return map(memory, (value, key) => {
const tMemory = value
const tMemoryFree = memoryFree[key]
if (tMemory == null || tMemoryFree == null) {
return null
}
return tMemory - tMemoryFree
})
}
const METRICS_MEAN = {
@@ -274,27 +289,34 @@ const DAYS_TO_KEEP = {
weekly: 7,
monthly: 30,
}
function getLastDays(data, periodicity) {
const daysToKeep = DAYS_TO_KEEP[periodicity]
const expectedData = {}
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value)) {
// slice only applies to array
expectedData[key] = value.slice(-daysToKeep)
} else {
expectedData[key] = value
function getDeepLastValues(data, nValues) {
if (data == null) {
return {}
}
if (Array.isArray(data)) {
return data.slice(-nValues)
}
return expectedData
if (typeof data !== 'object') {
throw new Error('data must be an object or an array')
}
return mapValues(data, value => getDeepLastValues(value, nValues))
}
// ===================================================================
async function getVmsStats({ runningVms, periodicity, xo }) {
const lastNValues = DAYS_TO_KEEP[periodicity]
return orderBy(
await Promise.all(
map(runningVms, async vm => {
const { stats } = await xo.getXapiVmStats(vm, GRANULARITY).catch(error => {
const stats = getDeepLastValues(
(
await xo.getXapiVmStats(vm, GRANULARITY).catch(error => {
log.warn('Error on fetching VM stats', {
error,
vmId: vm.id,
@@ -303,22 +325,25 @@ async function getVmsStats({ runningVms, periodicity, xo }) {
stats: {},
}
})
).stats,
lastNValues
)
const iopsRead = METRICS_MEAN.iops(getLastDays(get(stats.iops, 'r'), periodicity))
const iopsWrite = METRICS_MEAN.iops(getLastDays(get(stats.iops, 'w'), periodicity))
const iopsRead = METRICS_MEAN.iops(stats.iops?.r)
const iopsWrite = METRICS_MEAN.iops(stats.iops?.w)
return {
uuid: vm.uuid,
name: vm.name_label,
addresses: Object.values(vm.addresses),
cpu: METRICS_MEAN.cpu(getLastDays(stats.cpus, periodicity)),
ram: METRICS_MEAN.ram(getLastDays(getMemoryUsedMetric(stats), periodicity)),
diskRead: METRICS_MEAN.disk(getLastDays(get(stats.xvds, 'r'), periodicity)),
diskWrite: METRICS_MEAN.disk(getLastDays(get(stats.xvds, 'w'), periodicity)),
cpu: METRICS_MEAN.cpu(stats.cpus),
ram: METRICS_MEAN.ram(getMemoryUsedMetric(stats)),
diskRead: METRICS_MEAN.disk(stats.xvds?.r),
diskWrite: METRICS_MEAN.disk(stats.xvds?.w),
iopsRead,
iopsWrite,
iopsTotal: iopsRead + iopsWrite,
netReception: METRICS_MEAN.net(getLastDays(get(stats.vifs, 'rx'), periodicity)),
netTransmission: METRICS_MEAN.net(getLastDays(get(stats.vifs, 'tx'), periodicity)),
netReception: METRICS_MEAN.net(stats.vifs?.rx),
netTransmission: METRICS_MEAN.net(stats.vifs?.tx),
}
})
),
@@ -328,10 +353,14 @@ async function getVmsStats({ runningVms, periodicity, xo }) {
}
async function getHostsStats({ runningHosts, periodicity, xo }) {
const lastNValues = DAYS_TO_KEEP[periodicity]
return orderBy(
await Promise.all(
map(runningHosts, async host => {
const { stats } = await xo.getXapiHostStats(host, GRANULARITY).catch(error => {
const stats = getDeepLastValues(
(
await xo.getXapiHostStats(host, GRANULARITY).catch(error => {
log.warn('Error on fetching host stats', {
error,
hostId: host.id,
@@ -340,15 +369,18 @@ async function getHostsStats({ runningHosts, periodicity, xo }) {
stats: {},
}
})
).stats,
lastNValues
)
return {
uuid: host.uuid,
name: host.name_label,
cpu: METRICS_MEAN.cpu(getLastDays(stats.cpus, periodicity)),
ram: METRICS_MEAN.ram(getLastDays(getMemoryUsedMetric(stats), periodicity)),
load: METRICS_MEAN.load(getLastDays(stats.load, periodicity)),
netReception: METRICS_MEAN.net(getLastDays(get(stats.pifs, 'rx'), periodicity)),
netTransmission: METRICS_MEAN.net(getLastDays(get(stats.pifs, 'tx'), periodicity)),
cpu: METRICS_MEAN.cpu(stats.cpus),
ram: METRICS_MEAN.ram(getMemoryUsedMetric(stats)),
load: METRICS_MEAN.load(stats.load),
netReception: METRICS_MEAN.net(stats.pifs?.rx),
netTransmission: METRICS_MEAN.net(stats.pifs?.tx),
}
})
),
@@ -358,6 +390,8 @@ async function getHostsStats({ runningHosts, periodicity, xo }) {
}
async function getSrsStats({ periodicity, xo, xoObjects }) {
const lastNValues = DAYS_TO_KEEP[periodicity]
return orderBy(
await asyncMapSettled(
filter(xoObjects, obj => obj.type === 'SR' && obj.size > 0 && obj.$PBDs.length > 0),
@@ -371,7 +405,9 @@ async function getSrsStats({ periodicity, xo, xoObjects }) {
name += ` (${container.name_label})`
}
const { stats } = await xo.getXapiSrStats(sr.id, GRANULARITY).catch(error => {
const stats = getDeepLastValues(
(
await xo.getXapiSrStats(sr.id, GRANULARITY).catch(error => {
log.warn('Error on fetching SR stats', {
error,
srId: sr.id,
@@ -380,9 +416,12 @@ async function getSrsStats({ periodicity, xo, xoObjects }) {
stats: {},
}
})
).stats,
lastNValues
)
const iopsRead = computeMean(getLastDays(get(stats.iops, 'r'), periodicity))
const iopsWrite = computeMean(getLastDays(get(stats.iops, 'w'), periodicity))
const iopsRead = computeMean(stats.iops?.r)
const iopsWrite = computeMean(stats.iops?.w)
return {
uuid: sr.uuid,
@@ -477,7 +516,7 @@ async function getHostsMissingPatches({ runningHosts, xo }) {
.getXapi(host)
.listMissingPatches(host._xapiId)
.catch(error => {
console.error('[WARN] error on fetching hosts missing patches:', JSON.stringify(error))
log.warn('Error on fetching hosts missing patches', { error })
return []
})
@@ -741,7 +780,7 @@ class UsageReportPlugin {
try {
await this._sendReport(true)
} catch (error) {
console.error('[WARN] scheduled function:', (error && error.stack) || error)
log.warn('Scheduled usage report error', { error })
}
})

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

@@ -5,6 +5,7 @@ import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
import concat from 'lodash/concat.js'
import hrp from 'http-request-plus'
import mapKeys from 'lodash/mapKeys.js'
import { createLogger } from '@xen-orchestra/log'
import { defer } from 'golike-defer'
import { format } from 'json-rpc-peer'
@@ -622,6 +623,8 @@ warmMigration.params = {
// -------------------------------------------------------------------
const autoPrefix = (pfx, str) => (str.startsWith(pfx) ? str : pfx + str)
export const set = defer(async function ($defer, params) {
const VM = extract(params, 'VM')
const xapi = this.getXapi(VM)
@@ -646,6 +649,11 @@ export const set = defer(async function ($defer, params) {
await xapi.call('VM.set_suspend_SR', VM._xapiRef, suspendSr === null ? Ref.EMPTY : suspendSr._xapiRef)
}
const xenStoreData = extract(params, 'xenStoreData')
if (xenStoreData !== undefined) {
await this.getXapiObject(VM).update_xenstore_data(mapKeys(xenStoreData, (v, k) => autoPrefix('vm-data/', k)))
}
return xapi.editVm(vmId, params, async (limits, vm) => {
const resourceSet = xapi.xo.getData(vm, 'resourceSet')
@@ -747,6 +755,15 @@ set.params = {
blockedOperations: { type: 'object', optional: true, properties: { '*': { type: ['boolean', 'null', 'string'] } } },
suspendSr: { type: ['string', 'null'], optional: true },
xenStoreData: {
description: 'properties that should be set or deleted (if null) in the VM XenStore',
optional: true,
type: 'object',
additionalProperties: {
type: ['null', 'string'],
},
},
}
set.resolve = {

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

@@ -405,6 +405,11 @@ export default {
},
_poolWideInstall: deferrable(async function ($defer, patches, xsCredentials) {
// New XS patching system: https://support.citrix.com/article/CTX473972/upcoming-changes-in-xencenter
if (xsCredentials?.username === undefined || xsCredentials?.apikey === undefined) {
throw new Error('XenServer credentials not found. See https://xen-orchestra.com/docs/updater.html#xenserver-updates')
}
// Legacy XS patches
if (!useUpdateSystem(this.pool.$master)) {
// for each patch: pool_patch.pool_apply
@@ -420,11 +425,6 @@ export default {
}
// ----------
// New XS patching system: https://support.citrix.com/article/CTX473972/upcoming-changes-in-xencenter
if (xsCredentials?.username === undefined || xsCredentials?.apikey === undefined) {
throw new Error('XenServer credentials not found. See https://xen-orchestra.com/docs/updater.html#xenserver-updates')
}
// for each patch: pool_update.introduce → pool_update.pool_apply
for (const p of patches) {
const [vdi] = await Promise.all([this._uploadPatch($defer, p.uuid, xsCredentials), this._ejectToolsIsos()])
@@ -493,7 +493,7 @@ export default {
},
@decorateWith(deferrable)
async rollingPoolUpdate($defer) {
async rollingPoolUpdate($defer, { xsCredentials } = {}) {
const isXcp = _isXcp(this.pool.$master)
if (this.pool.ha_enabled) {
@@ -530,7 +530,7 @@ export default {
// On XS/CH, start by installing patches on all hosts
if (!isXcp) {
log.debug('Install patches')
await this.installPatches()
await this.installPatches({ xsCredentials })
}
// Remember on which hosts the running VMs are
@@ -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

@@ -49,18 +49,19 @@ export default {
await this._unplugPbd(this.getObject(id))
},
_getVdiChainsInfo(uuid, childrenMap, cache) {
_getVdiChainsInfo(uuid, childrenMap, cache, resultContainer) {
let info = cache[uuid]
if (info === undefined) {
const children = childrenMap[uuid]
const unhealthyLength = children !== undefined && children.length === 1 ? 1 : 0
resultContainer.nUnhealthyVdis += unhealthyLength
const vdi = this.getObjectByUuid(uuid, undefined)
if (vdi === undefined) {
info = { unhealthyLength, missingParent: uuid }
} else {
const parent = vdi.sm_config['vhd-parent']
if (parent !== undefined) {
info = this._getVdiChainsInfo(parent, childrenMap, cache)
info = this._getVdiChainsInfo(parent, childrenMap, cache, resultContainer)
info.unhealthyLength += unhealthyLength
} else {
info = { unhealthyLength }
@@ -76,12 +77,13 @@ export default {
const unhealthyVdis = { __proto__: null }
const children = groupBy(vdis, 'sm_config.vhd-parent')
const vdisWithUnknownVhdParent = { __proto__: null }
const resultContainer = { nUnhealthyVdis: 0 }
const cache = { __proto__: null }
forEach(vdis, vdi => {
if (vdi.managed && !vdi.is_a_snapshot) {
const { uuid } = vdi
const { unhealthyLength, missingParent } = this._getVdiChainsInfo(uuid, children, cache)
const { unhealthyLength, missingParent } = this._getVdiChainsInfo(uuid, children, cache, resultContainer)
if (unhealthyLength !== 0) {
unhealthyVdis[uuid] = unhealthyLength
@@ -95,6 +97,7 @@ export default {
return {
vdisWithUnknownVhdParent,
unhealthyVdis,
...resultContainer,
}
},

View File

@@ -138,14 +138,20 @@ export default class {
async authenticateUser(credentials, userData) {
const { tasks } = this._app
const task = await tasks.create({
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

@@ -62,11 +62,14 @@ export default class Pools {
}
const patchesName = await Promise.all([targetXapi.findPatches(targetRequiredPatches), ...findPatchesPromises])
const { xsCredentials } = _app.apiContext.user.preferences
// Install patches in parallel.
const installPatchesPromises = []
installPatchesPromises.push(
targetXapi.installPatches({
patches: patchesName[0],
xsCredentials,
})
)
let i = 1
@@ -74,6 +77,7 @@ export default class Pools {
installPatchesPromises.push(
sourceXapis[sourceId].installPatches({
patches: patchesName[i++],
xsCredentials,
})
)
}

View File

@@ -686,7 +686,7 @@ export default class XenServers {
$defer(() => app.loadPlugin('load-balancer'))
}
await this.getXapi(pool).rollingPoolUpdate()
await this.getXapi(pool).rollingPoolUpdate({ xsCredentials: app.apiContext.user.preferences.xsCredentials })
}
}

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'
@@ -108,7 +109,13 @@ const xo = invoke(() => {
credentials: { token },
})
xo.on('authenticationFailure', signOut)
xo.on('authenticationFailure', error => {
console.warn('authenticationFailure', error)
if (error.name !== 'ConnectionError') {
signOut(error)
}
})
xo.on('scheduledAttempt', ({ delay }) => {
console.warn('next attempt in %s ms', delay)
})
@@ -821,22 +828,51 @@ 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}`,
})
) {
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'>
@@ -845,18 +881,36 @@ export const restartHost = (host, force = false, suspendResidentVms = false) =>
),
title: _('restartHostModalTitle'),
})
return _call('host.restart', { id: resolveId(host), force, suspendResidentVms, bypassBackupCheck: true })
return _restartHost({ ...opts, host, bypassBackupCheck: true })
}
throw error
})
.catch(error => {
if (noHostsAvailable.is(error)) {
if (noHostsAvailableErrCheck(error)) {
alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage'))
}
throw error
}),
noop
)
}
}
// ---- 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) => (

View File

@@ -10,7 +10,7 @@ import { CustomFields } from 'custom-fields'
import { createGetObjectsOfType } from 'selectors'
import { createSelector } from 'reselect'
import { createSrUnhealthyVdiChainsLengthSubscription, deleteSr, reclaimSrSpace, toggleSrMaintenanceMode } from 'xo'
import { flowRight, isEmpty, keys, sum, values } from 'lodash'
import { flowRight, isEmpty, keys } from 'lodash'
// ===================================================================
@@ -44,11 +44,11 @@ const UnhealthyVdiChains = flowRight(
connectStore(() => ({
vdis: createGetObjectsOfType('VDI').pick(createSelector((_, props) => props.chains?.unhealthyVdis, keys)),
}))
)(({ chains: { unhealthyVdis } = {}, vdis }) =>
)(({ chains: { nUnhealthyVdis, unhealthyVdis } = {}, vdis }) =>
isEmpty(vdis) ? null : (
<div>
<hr />
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(unhealthyVdis)) })}</h3>
<h3>{_('srUnhealthyVdiTitle', { total: nUnhealthyVdis })}</h3>
<SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={unhealthyVdis} />
</div>
)

1449
yarn.lock

File diff suppressed because it is too large Load Diff