Compare commits

..

4 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
105 changed files with 1911 additions and 3747 deletions

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.43.2",
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/fs": "^4.1.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",

View File

@@ -681,13 +681,11 @@ export class RemoteAdapter {
}
}
async outputStream(path, input, { checksum = true, maxStreamLength, streamLength, validator = noop } = {}) {
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
const container = watchStreamSize(input)
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
maxStreamLength,
streamLength,
async validator() {
await input.task
return validator.apply(this, arguments)

View File

@@ -29,8 +29,6 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
writer =>
writer.run({
stream: forkStreamUnpipe(stream),
// stream will be forked and transformed, it's not safe to attach additionnal properties to it
streamLength: stream.length,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,

View File

@@ -35,25 +35,13 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
useSnapshot: false,
})
)
const vdis = await exportedVm.$getDisks()
let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
for (const vdiRef of vdis) {
const vdi = await this._xapi.getRecord('VDI', vdiRef)
// the size a of fully allocated vdi will be virtual_size exaclty, it's a gross over evaluation
// of the real stream size in general, since a disk is never completly full
// vdi.physical_size seems to underevaluate a lot the real disk usage of a VDI, as of 2023-10-30
maxStreamLength += vdi.virtual_size
}
const sizeContainer = watchStreamSize(stream)
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.run({
maxStreamLength,
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp,

View File

@@ -24,7 +24,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
)
}
async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
const settings = this._settings
const job = this._job
const scheduleId = this._scheduleId
@@ -65,8 +65,6 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
await Task.run({ name: 'transfer' }, async () => {
await adapter.outputStream(dataFilename, stream, {
maxStreamLength,
streamLength,
validator: tmpPath => adapter.isValidXva(tmpPath),
})
return { size: sizeContainer.size }

View File

@@ -1,9 +1,9 @@
import { AbstractWriter } from './_AbstractWriter.mjs'
export class AbstractFullWriter extends AbstractWriter {
async run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
try {
return await this._run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot })
return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })
} finally {
// ensure stream is properly closed
stream.destroy()

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.43.2",
"version": "0.43.0",
"engines": {
"node": ">=14.18"
},
@@ -28,7 +28,7 @@
"@vates/nbd-client": "^2.0.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/fs": "^4.1.0",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"app-conf": "^2.3.0",
@@ -56,7 +56,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^3.3.0"
"@xen-orchestra/xapi": "^3.2.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.1.1",
"version": "4.1.0",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",

View File

@@ -189,7 +189,7 @@ export default class RemoteHandlerAbstract {
* @param {number} [options.dirMode]
* @param {(this: RemoteHandlerAbstract, path: string) => Promise<undefined>} [options.validator] Function that will be called before the data is commited to the remote, if it fails, file should not exist
*/
async outputStream(path, input, { checksum = true, dirMode, maxStreamLength, streamLength, validator } = {}) {
async outputStream(path, input, { checksum = true, dirMode, validator } = {}) {
path = normalizePath(path)
let checksumStream
@@ -201,8 +201,6 @@ export default class RemoteHandlerAbstract {
}
await this._outputStream(path, input, {
dirMode,
maxStreamLength,
streamLength,
validator,
})
if (checksum) {
@@ -635,7 +633,7 @@ export default class RemoteHandlerAbstract {
}
throw error
},
// real unlink concurrency will be 2**max directory depth
// real unlink concurrency will be 2**max directory depth
{ concurrency: 2 }
)
)

View File

@@ -5,7 +5,6 @@ import {
CreateMultipartUploadCommand,
DeleteObjectCommand,
GetObjectCommand,
GetObjectLockConfigurationCommand,
HeadObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
@@ -18,7 +17,7 @@ import { getApplyMd5BodyChecksumPlugin } from '@aws-sdk/middleware-apply-body-ch
import { Agent as HttpAgent } from 'http'
import { Agent as HttpsAgent } from 'https'
import { createLogger } from '@xen-orchestra/log'
import { PassThrough, Transform, pipeline } from 'stream'
import { PassThrough, pipeline } from 'stream'
import { parse } from 'xo-remote-parser'
import copyStreamToBuffer from './_copyStreamToBuffer.js'
import guessAwsRegion from './_guessAwsRegion.js'
@@ -31,8 +30,6 @@ import { pRetry } from 'promise-toolbox'
// limits: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
const MAX_PART_NUMBER = 10000
const MIN_PART_SIZE = 5 * 1024 * 1024
const { warn } = createLogger('xo:fs:s3')
export default class S3Handler extends RemoteHandlerAbstract {
@@ -74,6 +71,9 @@ export default class S3Handler extends RemoteHandlerAbstract {
}),
})
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
const parts = split(path)
this.#bucket = parts.shift()
this.#dir = join(...parts)
@@ -223,35 +223,11 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
}
async _outputStream(path, input, { streamLength, maxStreamLength = streamLength, validator }) {
// S3 storage is limited to 10K part, each part is limited to 5GB. And the total upload must be smaller than 5TB
// a bigger partSize increase the memory consumption of aws/lib-storage exponentially
let partSize
if (maxStreamLength === undefined) {
warn(`Writing ${path} to a S3 remote without a max size set will cut it to 50GB`, { path })
partSize = MIN_PART_SIZE // min size for S3
} else {
partSize = Math.min(Math.max(Math.ceil(maxStreamLength / MAX_PART_NUMBER), MIN_PART_SIZE), MAX_PART_SIZE)
}
// ensure we don't try to upload a stream to big for this partSize
let readCounter = 0
const MAX_SIZE = MAX_PART_NUMBER * partSize
const streamCutter = new Transform({
transform(chunk, encoding, callback) {
readCounter += chunk.length
if (readCounter > MAX_SIZE) {
callback(new Error(`read ${readCounter} bytes, maximum size allowed is ${MAX_SIZE} `))
} else {
callback(null, chunk)
}
},
})
async _outputStream(path, input, { validator }) {
// Workaround for "ReferenceError: ReadableStream is not defined"
// https://github.com/aws/aws-sdk-js-v3/issues/2522
const Body = new PassThrough()
pipeline(input, streamCutter, Body, () => {})
pipeline(input, Body, () => {})
const upload = new Upload({
client: this.#s3,
@@ -259,8 +235,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
...this.#createParams(path),
Body,
},
partSize,
leavePartsOnError: false,
})
await upload.done()
@@ -444,24 +418,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
async _closeFile(fd) {}
async _sync() {
await super._sync()
try {
// if Object Lock is enabled, each upload must come with a contentMD5 header
// the computation of this md5 is memory-intensive, especially when uploading a stream
const res = await this.#s3.send(new GetObjectLockConfigurationCommand({ Bucket: this.#bucket }))
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
// will automatically add the contentMD5 header to any upload to S3
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
}
} catch (error) {
if (error.Code !== 'ObjectLockConfigurationNotFoundError') {
throw error
}
}
}
useVhdDirectory() {
return true
}

View File

@@ -3,8 +3,6 @@
## **next**
- Ability to snapshot/copy a VM from its view (PR [#7087](https://github.com/vatesfr/xen-orchestra/pull/7087))
- [Header] Replace logo with "XO LITE" (PR [#7118](https://github.com/vatesfr/xen-orchestra/pull/7118))
- New VM console toolbar + Ability to send Ctrl+Alt+Del (PR [#7088](https://github.com/vatesfr/xen-orchestra/pull/7088))
## **0.1.4** (2023-10-03)

View File

@@ -11,7 +11,6 @@
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@fontsource/poppins": "^5.0.8",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",

View File

@@ -1,11 +1,7 @@
@import "reset.css";
@import "theme.css";
@import "@fontsource/poppins/400.css";
@import "@fontsource/poppins/500.css";
@import "@fontsource/poppins/600.css";
@import "@fontsource/poppins/700.css";
@import "@fontsource/poppins/900.css";
@import "@fontsource/poppins/400-italic.css";
/* TODO Serve fonts locally */
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;0,900;1,400;1,500;1,600;1,700;1,900&display=swap");
body {
min-height: 100vh;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,6 +1,4 @@
:root {
--color-logo: #282467;
--color-blue-scale-000: #000000;
--color-blue-scale-100: #1a1b38;
--color-blue-scale-200: #595a6f;
@@ -61,10 +59,6 @@
}
:root.dark {
color-scheme: dark;
--color-logo: #e5e5e7;
--color-blue-scale-000: #ffffff;
--color-blue-scale-100: #e5e5e7;
--color-blue-scale-200: #9899a5;

View File

@@ -7,8 +7,7 @@
class="toggle-navigation"
/>
<RouterLink :to="{ name: 'home' }">
<img v-if="isMobile" alt="XO Lite" src="../assets/logo.svg" />
<TextLogo v-else />
<img alt="XO Lite" src="../assets/logo.svg" />
</RouterLink>
<slot />
<div class="right">
@@ -19,7 +18,6 @@
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import TextLogo from "@/components/TextLogo.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
@@ -46,10 +44,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
img {
width: 4rem;
}
.text-logo {
margin: 1rem;
}
}
.right {

View File

@@ -105,10 +105,6 @@ watchEffect(() => {
onBeforeUnmount(() => {
clearVncClient();
});
defineExpose({
sendCtrlAltDel: () => vncClient?.sendCtrlAltDel(),
});
</script>
<style lang="postcss" scoped>

View File

@@ -1,37 +0,0 @@
<template>
<svg
class="text-logo"
viewBox="300.85 622.73 318.32 63.27"
xmlns="http://www.w3.org/2000/svg"
width="100"
height="22"
>
<g>
<polygon
points="355.94 684.92 341.54 684.92 327.84 664.14 315.68 684.92 301.81 684.92 317.59 659.25 338.96 659.25 355.94 684.92"
/>
<path
d="M406.2,627.17c4.62,2.64,8.27,6.33,10.94,11.07,2.67,4.74,4.01,10.1,4.01,16.07s-1.34,11.35-4.01,16.12c-2.67,4.77-6.32,8.48-10.94,11.12-4.63,2.64-9.78,3.97-15.47,3.97s-10.85-1.32-15.47-3.97c-4.63-2.64-8.27-6.35-10.95-11.12-2.67-4.77-4.01-10.14-4.01-16.12s1.34-11.33,4.01-16.07c2.67-4.74,6.32-8.43,10.95-11.07,4.62-2.64,9.78-3.97,15.47-3.97s10.84,1.32,15.47,3.97Zm-24.86,9.65c-2.7,1.61-4.81,3.92-6.33,6.94-1.52,3.02-2.28,6.54-2.28,10.56s.76,7.54,2.28,10.56c1.52,3.02,3.63,5.33,6.33,6.94,2.7,1.61,5.83,2.41,9.39,2.41s6.69-.8,9.39-2.41c2.7-1.61,4.81-3.92,6.33-6.94,1.52-3.02,2.28-6.53,2.28-10.56s-.76-7.54-2.28-10.56-3.63-5.33-6.33-6.94c-2.7-1.61-5.83-2.41-9.39-2.41s-6.69,.8-9.39,2.41Z"
/>
<polygon
points="354.99 624.06 339.53 649.22 317.49 649.22 300.86 624.06 315.26 624.06 328.96 644.84 341.12 624.06 354.99 624.06"
/>
<g>
<path d="M476.32,675.94h20.81v10.04h-33.47v-63.14h12.66v53.1Z" />
<path d="M517.84,622.84v63.14h-12.66v-63.14h12.66Z" />
<path
d="M573.29,622.84v10.22h-16.82v52.92h-12.66v-52.92h-16.83v-10.22h46.31Z"
/>
<path
d="M595.18,633.06v15.83h21.26v10.04h-21.26v16.73h23.97v10.31h-36.64v-63.23h36.64v10.31h-23.97Z"
/>
</g>
</g>
</svg>
</template>
<style lang="postcss" scoped>
.text-logo {
fill: var(--color-logo);
}
</style>

View File

@@ -25,11 +25,10 @@ defineProps<{
align-items: center;
height: 4.4rem;
padding-right: 1.5rem;
padding-left: 1.5rem;
padding-left: 1rem;
white-space: nowrap;
border-radius: 0.8rem;
gap: 1rem;
background-color: var(--color-blue-scale-500);
&.disabled {
color: var(--color-blue-scale-400);

View File

@@ -75,8 +75,6 @@
"following-hosts-unreachable": "The following hosts are unreachable",
"force-reboot": "Force reboot",
"force-shutdown": "Force shutdown",
"fullscreen": "Fullscreen",
"fullscreen-leave": "Leave fullscreen",
"go-back": "Go back",
"here": "Here",
"hosts": "Hosts",
@@ -108,7 +106,7 @@
"object": "Object",
"object-not-found": "Object {id} can't be found…",
"on-object": "on {object}",
"open-console-in-new-tab": "Open console in new tab",
"open-in-new-window": "Open in new window",
"or": "Or",
"page-not-found": "This page is not to be found…",
"password": "Password",
@@ -139,7 +137,6 @@
"save": "Save",
"select-destination-host": "Select a destination host",
"selected-vms-in-execution": "Some selected VMs are running",
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"shutdown": "Shutdown",

View File

@@ -75,8 +75,6 @@
"following-hosts-unreachable": "Les hôtes suivants sont inaccessibles",
"force-reboot": "Forcer le redémarrage",
"force-shutdown": "Forcer l'arrêt",
"fullscreen": "Plein écran",
"fullscreen-leave": "Quitter plein écran",
"go-back": "Revenir en arrière",
"here": "Ici",
"hosts": "Hôtes",
@@ -108,7 +106,7 @@
"object": "Objet",
"object-not-found": "L'objet {id} est introuvable…",
"on-object": "sur {object}",
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
"open-in-new-window": "Ouvrir dans une nouvelle fenêtre",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
@@ -139,7 +137,6 @@
"save": "Enregistrer",
"select-destination-host": "Sélectionnez un hôte de destination",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"shutdown": "Arrêter",

View File

@@ -1,7 +1,7 @@
import { useBreakpoints, useColorMode } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useRoute } from "vue-router";
export const useUiStore = defineStore("ui", () => {
const currentHostOpaqueRef = ref();
@@ -14,15 +14,8 @@ export const useUiStore = defineStore("ui", () => {
const isMobile = computed(() => !isDesktop.value);
const router = useRouter();
const route = useRoute();
const hasUi = computed<boolean>({
get: () => route.query.ui !== "0",
set: (value: boolean) => {
void router.replace({ query: { ui: value ? undefined : "0" } });
},
});
const hasUi = computed(() => route.query.ui !== "0");
return {
colorMode,

View File

@@ -7,62 +7,40 @@
{{ $t("power-on-for-console") }}
</div>
<template v-else-if="vm && vmConsole">
<AppMenu horizontal>
<MenuItem
:icon="faArrowUpRightFromSquare"
@click="openInNewTab"
v-if="uiStore.hasUi"
>
{{ $t("open-console-in-new-tab") }}
</MenuItem>
<MenuItem
:icon="
uiStore.hasUi
? faUpRightAndDownLeftFromCenter
: faDownLeftAndUpRightToCenter
"
@click="toggleFullScreen"
>
{{ $t(uiStore.hasUi ? "fullscreen" : "fullscreen-leave") }}
</MenuItem>
<MenuItem
:disabled="!consoleElement"
:icon="faKeyboard"
@click="sendCtrlAltDel"
>
{{ $t("send-ctrl-alt-del") }}
</MenuItem>
</AppMenu>
<RemoteConsole
ref="consoleElement"
:is-console-available="isConsoleAvailable"
:location="vmConsole.location"
class="remote-console"
/>
<div class="open-in-new-window">
<RouterLink
v-if="uiStore.hasUi"
:to="{ query: { ui: '0' } }"
class="link"
target="_blank"
>
<UiIcon :icon="faArrowUpRightFromSquare" />
{{ $t("open-in-new-window") }}
</RouterLink>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import RemoteConsole from "@/components/RemoteConsole.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { VM_OPERATION, VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useConsoleCollection } from "@/stores/xen-api/console.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import {
faArrowUpRightFromSquare,
faDownLeftAndUpRightToCenter,
faKeyboard,
faUpRightAndDownLeftFromCenter,
} from "@fortawesome/free-solid-svg-icons";
import { computed, ref } from "vue";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useRoute } from "vue-router";
const STOP_OPERATIONS = [
VM_OPERATION.SHUTDOWN,
@@ -76,7 +54,6 @@ const STOP_OPERATIONS = [
usePageTitleStore().setTitle(useI18n().t("console"));
const router = useRouter();
const route = useRoute();
const uiStore = useUiStore();
@@ -118,26 +95,14 @@ const isConsoleAvailable = computed(() =>
? !isOperationPending(vm.value, STOP_OPERATIONS)
: false
);
const consoleElement = ref();
const sendCtrlAltDel = () => consoleElement.value?.sendCtrlAltDel();
const toggleFullScreen = () => {
uiStore.hasUi = !uiStore.hasUi;
};
const openInNewTab = () => {
const routeData = router.resolve({ query: { ui: "0" } });
window.open(routeData.href, "_blank");
};
</script>
<style lang="postcss" scoped>
.vm-console-view {
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 14.5rem);
flex-direction: column;
&.no-ui {
height: 100%;
@@ -195,9 +160,4 @@ const openInNewTab = () => {
}
}
}
.vm-console-view:deep(.app-menu) {
background-color: transparent;
align-self: center;
}
</style>

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.14.0",
"version": "0.13.0",
"engines": {
"node": ">=15.6"
},

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.37",
"version": "0.26.35",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,13 +32,13 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.43.2",
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/fs": "^4.1.0",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.14.0",
"@xen-orchestra/mixins": "^0.13.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^3.3.0",
"@xen-orchestra/xapi": "^3.2.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "3.3.0",
"version": "3.2.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {

View File

@@ -1,62 +1,8 @@
# ChangeLog
## **5.88.0** (2023-10-31)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [About] For source users, display if their XO is up to date [#5934](https://github.com/vatesfr/xen-orchestra/issues/5934) (PR [#7091](https://github.com/vatesfr/xen-orchestra/pull/7091))
- [Self] Show number of VMs that belong to each Resource Set (PR [#7114](https://github.com/vatesfr/xen-orchestra/pull/7114))
- [VM/New] Possibility to create and attach a _VTPM_ to a VM [#7066](https://github.com/vatesfr/xen-orchestra/issues/7066) [Forum#6578](https://xcp-ng.org/forum/topic/6578/xcp-ng-8-3-public-alpha/109) (PR [#7077](https://github.com/vatesfr/xen-orchestra/pull/7077))
- [XOSTOR] Ability to create a XOSTOR storage (PR [#6983](https://github.com/vatesfr/xen-orchestra/pull/6983))
### Enhancements
- [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))
- [VM/Advanced] Possibility to create/delete VTPM [#7066](https://github.com/vatesfr/xen-orchestra/issues/7066) [Forum#6578](https://xcp-ng.org/forum/topic/6578/xcp-ng-8-3-public-alpha/109) (PR [#7085](https://github.com/vatesfr/xen-orchestra/pull/7085))
- [Dashboard/Health] Displays number of VDIs to coalesce (PR [#7111](https://github.com/vatesfr/xen-orchestra/pull/7111))
- [Proxy] Ability to open support tunnel on XO Proxy (PRs [#7126](https://github.com/vatesfr/xen-orchestra/pull/7126) [#7127](https://github.com/vatesfr/xen-orchestra/pull/7127))
- [New network] Remove bonded PIFs from selector when creating network (PR [#7136](https://github.com/vatesfr/xen-orchestra/pull/7136))
- Try to preserve current page across reauthentication (PR [#7013](https://github.com/vatesfr/xen-orchestra/pull/7013))
- [XO-WEB/Forget SR] Changed the modal message and added a confirmation text to be sure the action is understood by the user (PR [#7154](https://github.com/vatesfr/xen-orchestra/pull/7154))
### Bug fixes
- [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))
- [Plugin/transport-xmpp] Fix plugin load
- [Self Service] Fix Self users not being able to snapshot VMs when they're members of a user group (PR [#7129](https://github.com/vatesfr/xen-orchestra/pull/7129))
- [Netbox] Fix "The selected cluster is not assigned to this site" error [Forum#7887](https://xcp-ng.org/forum/topic/7887) (PR [#7124](https://github.com/vatesfr/xen-orchestra/pull/7124))
- [Backups] Fix `MESSAGE_METHOD_UNKNOWN` during full backup [Forum#7894](https://xcp-ng.org/forum/topic/7894)(PR [#7139](https://github.com/vatesfr/xen-orchestra/pull/7139))
- [Resource Set] Fix error displayed after successful VM addition to resource set PR ([#7144](https://github.com/vatesfr/xen-orchestra/pull/7144))
### Released packages
- @xen-orchestra/fs 4.1.1
- @xen-orchestra/xapi 3.3.0
- @xen-orchestra/mixins 0.14.0
- xo-server-backup-reports 0.18.0
- xo-server-transport-xmpp 0.1.3
- xo-server-usage-report 0.10.5
- @xen-orchestra/backups 0.43.2
- @xen-orchestra/proxy 0.26.37
- xo-cli 0.21.0
- xo-server 5.125.1
- xo-server-netbox 1.3.2
- xo-web 5.127.1
## **5.87.0** (2023-09-29)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
@@ -107,6 +53,8 @@
## **5.86.1** (2023-09-07)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Bug fixes
- [User] _Forget all connection tokens_ button should not delete other users' tokens, even when current user is an administrator (PR [#7014](https://github.com/vatesfr/xen-orchestra/pull/7014))

View File

@@ -7,11 +7,16 @@
> 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”
- [Netbox] Fix VMs' `site` property being unnecessarily updated on some versions of Netbox (PR [#7145](https://github.com/vatesfr/xen-orchestra/pull/7145))
- [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
@@ -29,6 +34,10 @@
<!--packages-start-->
- xo-server-netbox patch
- @xen-orchestra/mixins minor
- @xen-orchestra/xapi minor
- xo-server minor
- xo-server-backup-reports minor
- xo-web minor
<!--packages-end-->

View File

@@ -1,35 +1,11 @@
<h3 align="center"><b>Xen Orchestra</b></h3>
<p align="center"><b>Manage, Backup and Cloudify your XCP-ng/XenServer infrastructure</b></p>
# Xen Orchestra [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
![](https://repository-images.githubusercontent.com/8077957/6dcf71fd-bad9-4bfa-933f-b466c52d513d)
![](http://i.imgur.com/tRffA5y.png)
XO (Xen Orchestra) is a complete solution to visualize, manage, backup and delegate your XCP-ng (or XenServer) infrastructure. **No agent** is required for it to work.
## Installation
It provides a web UI, a CLI and a REST API, while also getting a Terraform provider among other connectors/plugins.
## ⚡️ Quick start
Log in to your account and use the deploy form available from the [Vates website](https://vates.tech/deploy/).
## 📚 Documentation
The official documentation is available at https://xen-orchestra.com/docs
## 🚀 Features
- **Centralized interface**: one Xen Orchestra to rule your entire infrastructure, even across datacenters at various locations
- **Administration and management:** VM creation, management, migration, metrics and statistics, XO proxies for remote sites… XO will become your best friend!
- **Backup & Disaster Recovery:** The backup is an essential component for the security of your infrastructure. With Xen Orchestra, select the backup mode that suits you best and protect your VMs and your business. Rolling snapshot, Full backup & replication, incremental backup & replication, mirror backup, S3 support among many other possibilities!
- **Cloud Enabler:** Xen Orchestra is your cloud initiator for XCP-ng (and XenServer). Group management, resource delegation and easy group administration. The Cloud is yours!
## 📸 Screenshots
![](https://vates.tech/assets/img/illustrations/xen-orchestra-screen-1.png.avif)
![](https://vates.tech/assets/img/illustrations/xen-orchestra-screen-3.png.avif)
![](https://vates.tech/assets/img/illustrations/xen-orchestra-screen-4.png.avif)
XOA or manual install procedure is [available here](https://xen-orchestra.com/docs/installation.html)
## License
AGPL3 © [Vates](http://vates.tech)
AGPL3 © [Vates SAS](http://vates.fr)

View File

@@ -1,13 +1,17 @@
# Xen Orchestra
![](https://repository-images.githubusercontent.com/8077957/6dcf71fd-bad9-4bfa-933f-b466c52d513d)
## Introduction
XO (Xen Orchestra) is a complete solution to visualize, manage, backup and delegate your XCP-ng (or XenServer) infrastructure. **No agent** is required for it to work.
Welcome to the official Xen Orchestra (XO) documentation.
It provides a web UI, a CLI and a REST API, while also getting a Terraform provider among other connectors/plugins.
XO is a web interface to visualize and administer your XenServer (or XAPI enabled) hosts. **No agent** is required for it to work.
It aims to be easy to use on any device supporting modern web technologies (HTML 5, CSS 3, JavaScript), such as your desktop computer or your smartphone.
## Quick start
Log in to your account and use the deploy form available from [Vates website](https://vates.tech/deploy/)
Log in to your account and use the deploy form available on [Xen Orchestra website](https://xen-orchestra.com/#!/xoa).
More details available on the [installation section](installation.md#xoa).
![Xen Orchestra logo](./assets/logo.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 nfs-common
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils
```
On Fedora/CentOS like:

View File

@@ -23,7 +23,7 @@
"node": ">=10"
},
"dependencies": {
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/fs": "^4.1.0",
"cli-progress": "^3.1.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",

View File

@@ -20,7 +20,7 @@
"@vates/read-chunk": "^1.2.0",
"@vates/stream-reader": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/fs": "^4.1.0",
"@xen-orchestra/log": "^0.6.0",
"async-iterator-to-stream": "^1.0.2",
"decorator-synchronized": "^0.6.0",
@@ -33,7 +33,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/fs": "^4.1.0",
"execa": "^5.0.0",
"get-stream": "^6.0.0",
"rimraf": "^5.0.1",

View File

@@ -13,7 +13,6 @@ import humanFormat from 'human-format'
import identity from 'lodash/identity.js'
import isObject from 'lodash/isObject.js'
import micromatch from 'micromatch'
import os from 'os'
import pairs from 'lodash/toPairs.js'
import pick from 'lodash/pick.js'
import prettyMs from 'pretty-ms'
@@ -48,7 +47,7 @@ async function connect() {
return xo
}
async function parseRegisterArgs(args, tokenDescription, client, acceptToken = false) {
async function parseRegisterArgs(args, tokenDescription, acceptToken = false) {
const {
allowUnauthorized,
expiresIn,
@@ -85,21 +84,21 @@ async function parseRegisterArgs(args, tokenDescription, client, acceptToken = f
pw(resolve)
}),
] = opts
result.token = await _createToken({ ...result, client, description: tokenDescription, email, password })
result.token = await _createToken({ ...result, description: tokenDescription, email, password })
}
return result
}
async function _createToken({ allowUnauthorized, client, description, email, expiresIn, password, url }) {
async function _createToken({ allowUnauthorized, description, email, expiresIn, password, url }) {
const xo = new Xo({ rejectUnauthorized: !allowUnauthorized, url })
await xo.open()
try {
await xo.signIn({ email, password })
console.warn('Successfully logged with', xo.user.email)
return await xo.call('token.create', { client, description, expiresIn }).catch(error => {
// if invalid parameter error, retry without client and description for backward compatibility
return await xo.call('token.create', { description, expiresIn }).catch(error => {
// if invalid parameter error, retry without description for backward compatibility
if (error.code === 10) {
return xo.call('token.create', { expiresIn })
}
@@ -220,8 +219,6 @@ function wrap(val) {
// ===================================================================
const PACKAGE_JSON = JSON.parse(readFileSync(new URL('package.json', import.meta.url)))
const help = wrap(
(function (pkg) {
return `Usage:
@@ -358,7 +355,7 @@ $name v$version`.replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
return pkg[key]
})
})(PACKAGE_JSON)
})(JSON.parse(readFileSync(new URL('package.json', import.meta.url))))
)
// -------------------------------------------------------------------
@@ -425,18 +422,9 @@ async function createToken(args) {
COMMANDS.createToken = createToken
async function register(args) {
let { clientId } = await config.load()
if (clientId === undefined) {
clientId = Math.random().toString(36).slice(2)
}
const { name, version } = PACKAGE_JSON
const label = `${name}@${version} - ${os.hostname()} - ${os.type()} ${os.machine()}`
const opts = await parseRegisterArgs(args, label, { id: clientId }, true)
const opts = await parseRegisterArgs(args, 'xo-cli --register', true)
await config.set({
allowUnauthorized: opts.allowUnauthorized,
clientId,
server: opts.url,
token: opts.token,
})

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-cli",
"version": "0.21.0",
"version": "0.20.0",
"license": "AGPL-3.0-or-later",
"description": "Basic CLI for Xen-Orchestra",
"keywords": [

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.18.0",
"version": "0.17.4",
"license": "AGPL-3.0-or-later",
"description": "Backup reports plugin for XO-Server",
"keywords": [

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-netbox",
"version": "1.3.2",
"version": "1.3.0",
"license": "AGPL-3.0-or-later",
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
"keywords": [

View File

@@ -103,8 +103,6 @@ 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', {
@@ -115,6 +113,8 @@ 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')
}
@@ -144,9 +144,7 @@ class Netbox {
const httpRequest = async () => {
try {
const response = await this.#xo.httpRequest(url, options)
// API version only follows minor version, which is less precise and is not semver-valid
// See https://github.com/netbox-community/netbox/issues/12879#issuecomment-1589190236
this.#netboxApiVersion = semver.coerce(response.headers['api-version'])?.version ?? undefined
this.#netboxApiVersion = response.headers['api-version']
const body = await response.text()
if (body.length > 0) {
return JSON.parse(body)
@@ -338,14 +336,6 @@ class Netbox {
tags: [],
}
// Prior to Netbox v3.3.0: no "site" field on VMs
// v3.3.0: "site" is REQUIRED and MUST be the same as cluster's site
// v3.3.5: "site" is OPTIONAL (auto-assigned in UI, not in API). `null` and cluster's site are accepted.
// v3.4.8: "site" is OPTIONAL and AUTO-ASSIGNED with cluster's site. If passed: ignored except if site is different from cluster's, then error.
if (this.#netboxApiVersion === undefined || semver.satisfies(this.#netboxApiVersion, '3.3.0 - 3.4.7')) {
nbVm.site = find(nbClusters, { id: nbCluster.id })?.site?.id ?? null
}
const distro = xoVm.os_version?.distro
if (distro != null) {
const slug = slugify(distro)
@@ -389,7 +379,10 @@ class Netbox {
nbVm.tags = nbVmTags.sort(({ id: id1 }, { id: id2 }) => (id1 < id2 ? -1 : 1))
// https://netbox.readthedocs.io/en/stable/release-notes/version-2.7/#api-choice-fields-now-use-string-values-3569
if (this.#netboxApiVersion !== undefined && !semver.satisfies(this.#netboxApiVersion, '>=2.7.0')) {
if (
this.#netboxApiVersion !== undefined &&
!semver.satisfies(semver.coerce(this.#netboxApiVersion).version, '>=2.7.0')
) {
nbVm.status = xoVm.power_state === 'Running' ? 1 : 0
}
@@ -402,9 +395,6 @@ class Netbox {
cluster: nbVm.cluster?.id ?? null,
status: nbVm.status?.value ?? null,
platform: nbVm.platform?.id ?? null,
// If site is not supported by Netbox, its value is undefined
// If site is supported by Netbox but empty, its value is null
site: nbVm.site == null ? nbVm.site : nbVm.site.id,
// Sort them so that they can be compared by diff()
tags: nbVm.tags.map(nbTag => ({ id: nbTag.id })).sort(({ id: id1 }, { id: id2 }) => (id1 < id2 ? -1 : 1)),
})

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-transport-xmpp",
"version": "0.1.3",
"version": "0.1.2",
"license": "AGPL-3.0-or-later",
"description": "Transport Xmpp plugin for XO-Server",
"keywords": [
@@ -29,7 +29,8 @@
"node": ">=10"
},
"dependencies": {
"@xmpp/client": "^0.13.1"
"@xmpp/client": "^0.13.1",
"promise-toolbox": "^0.21.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -1,3 +1,4 @@
import fromEvent from 'promise-toolbox/fromEvent'
import { client, xml } from '@xmpp/client'
// ===================================================================
@@ -55,7 +56,10 @@ class TransportXmppPlugin {
async load() {
this._client = client(this._conf)
await this._client.start()
this._client.on('error', () => {})
await fromEvent(this._client.connection.socket, 'data')
await fromEvent(this._client, 'online')
this._unset = this._set('sendToXmppClient', this._sendToXmppClient)
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-usage-report",
"version": "0.10.5",
"version": "0.10.4",
"license": "AGPL-3.0-or-later",
"description": "Report resources usage with their evolution",
"keywords": [

View File

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

View File

@@ -172,7 +172,6 @@ ignoreVmSnapshotResources = false
restartHostTimeout = '20 minutes'
maxUncoalescedVdis = 1
vdiExportConcurrency = 12
vmEvacuationConcurrency = 3
vmExportConcurrency = 2
vmSnapshotConcurrency = 2

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.125.1",
"version": "5.124.0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -41,18 +41,18 @@
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.43.2",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
"@xen-orchestra/fs": "^4.1.1",
"@xen-orchestra/fs": "^4.1.0",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.14.0",
"@xen-orchestra/mixins": "^0.13.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/template": "^0.1.0",
"@xen-orchestra/vmware-explorer": "^0.3.0",
"@xen-orchestra/xapi": "^3.3.0",
"@xen-orchestra/xapi": "^3.2.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.0.1",
@@ -68,6 +68,7 @@
"cookie-parser": "^1.4.3",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"deptree": "^1.0.0",
"exec-promise": "^0.7.0",
"execa": "^7.0.0",
"express": "^4.16.2",

View File

@@ -1,7 +1,6 @@
import { createLogger } from '@xen-orchestra/log'
import assert from 'assert'
import { format } from 'json-rpc-peer'
import { incorrectState } from 'xo-common/api-errors.js'
import backupGuard from './_backupGuard.mjs'
@@ -524,24 +523,3 @@ getSmartctlInformation.params = {
getSmartctlInformation.resolve = {
host: ['id', 'host', 'view'],
}
export async function getBlockdevices({ host }) {
const xapi = this.getXapi(host)
if (host.productBrand !== 'XCP-ng') {
throw incorrectState({
actual: host.productBrand,
expected: 'XCP-ng',
object: host.id,
property: 'productBrand',
})
}
return JSON.parse(await xapi.call('host.call_plugin', host._xapiRef, 'lsblk.py', 'list_block_devices', {}))
}
getBlockdevices.params = {
id: { type: 'string' },
}
getBlockdevices.resolve = {
host: ['id', 'host', 'administrate'],
}

View File

@@ -202,26 +202,6 @@ checkHealth.params = {
},
}
export async function openSupportTunnel({ id }) {
await this.callProxyMethod(id, 'appliance.supportTunnel.open')
for (let i = 0; i < 10; ++i) {
const { open, stdout } = await this.callProxyMethod(id, 'appliance.supportTunnel.getState')
if (open && stdout.length !== 0) {
return stdout
}
await new Promise(resolve => setTimeout(resolve, 1e3))
}
throw new Error('could not open support tunnel')
}
openSupportTunnel.permission = 'admin'
openSupportTunnel.params = {
id: { type: 'string' },
}
export function updateApplianceSettings({ id, ...props }) {
return this.updateProxyAppliance(id, props)
}

View File

@@ -6,7 +6,6 @@ import some from 'lodash/some.js'
import ensureArray from '../_ensureArray.mjs'
import { asInteger } from '../xapi/utils.mjs'
import { debounceWithKey } from '../_pDebounceWithKey.mjs'
import { destroy as destroyXostor } from './xostor.mjs'
import { forEach, parseXml } from '../utils.mjs'
// ===================================================================
@@ -57,10 +56,6 @@ const srIsBackingHa = sr => sr.$pool.ha_enabled && some(sr.$pool.$ha_statefiles,
// TODO: find a way to call this "delete" and not destroy
export async function destroy({ sr }) {
const xapi = this.getXapi(sr)
if (sr.SR_type === 'linstor') {
await destroyXostor.call(this, { sr })
return
}
if (sr.SR_type !== 'xosan') {
await xapi.destroySr(sr._xapiId)
return

View File

@@ -1,9 +1,8 @@
// TODO: Prevent token connections from creating tokens.
// TODO: Token permission.
export async function create({ client, description, expiresIn }) {
export async function create({ description, expiresIn }) {
return (
await this.createAuthenticationToken({
client,
description,
expiresIn,
userId: this.apiContext.user.id,
@@ -18,15 +17,6 @@ create.params = {
optional: true,
type: 'string',
},
client: {
description:
'client this authentication token belongs to, if a previous token exists, it will be updated and returned',
optional: true,
type: 'object',
properties: {
id: { description: 'unique identifier of this client', type: 'string' },
},
},
expiresIn: {
optional: true,
type: ['number', 'string'],

View File

@@ -5,7 +5,6 @@ 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'
@@ -238,11 +237,6 @@ export const create = defer(async function ($defer, params) {
await this.allocIpAddresses(vif.$id, concat(vif.ipv4_allowed, vif.ipv6_allowed)).catch(() => xapi.deleteVif(vif))
}
if (params.createVtpm) {
const vtpmRef = await xapi.VTPM_create({ VM: xapiVm.$ref })
$defer.onFailure(() => xapi.call('VTPM.destroy', vtpmRef))
}
if (params.bootAfterCreate) {
startVmAndDestroyCloudConfigVdi(xapi, xapiVm, cloudConfigVdiUuid, params)
}
@@ -263,11 +257,6 @@ create.params = {
optional: true,
},
createVtpm: {
type: 'boolean',
default: false,
},
networkConfig: {
type: 'string',
optional: true,
@@ -633,8 +622,6 @@ 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)
@@ -659,11 +646,6 @@ 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')
@@ -765,15 +747,6 @@ 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 = {
@@ -973,12 +946,7 @@ export const snapshot = defer(async function (
}
}
// Workaround: allow Resource Set members to snapshot a VM even though they
// don't have operate permissions on the SR(s)
if (
resourceSet === undefined ||
(!resourceSet.subjects.includes(user.id) && !user.groups.some(groupId => resourceSet.subjects.includes(groupId)))
) {
if (resourceSet === undefined || !resourceSet.subjects.includes(user.id)) {
await checkPermissionOnSrs.call(this, vm)
}

View File

@@ -1,248 +0,0 @@
import { asyncEach } from '@vates/async-each'
import { createLogger } from '@xen-orchestra/log'
import { defer } from 'golike-defer'
const ENUM_PROVISIONING = {
Thin: 'thin',
Thick: 'thick',
}
const LV_NAME = 'thin_device'
const PROVISIONING = Object.values(ENUM_PROVISIONING)
const VG_NAME = 'linstor_group'
const _XOSTOR_DEPENDENCIES = ['xcp-ng-release-linstor', 'xcp-ng-linstor']
const XOSTOR_DEPENDENCIES = _XOSTOR_DEPENDENCIES.join(',')
const log = createLogger('xo:api:pool')
function pluginCall(xapi, host, plugin, fnName, args) {
return xapi.call('host.call_plugin', host._xapiRef, plugin, fnName, args)
}
async function destroyVolumeGroup(xapi, host, force) {
log.info(`Trying to delete the ${VG_NAME} volume group.`, { hostId: host.id })
return pluginCall(xapi, host, 'lvm.py', 'destroy_volume_group', {
vg_name: VG_NAME,
force: String(force),
})
}
async function installOrUpdateDependencies(host, method = 'install') {
if (method !== 'install' && method !== 'update') {
throw new Error('Invalid method')
}
const xapi = this.getXapi(host)
log.info(`Trying to ${method} XOSTOR dependencies (${XOSTOR_DEPENDENCIES})`, { hostId: host.id })
for (const _package of _XOSTOR_DEPENDENCIES) {
await pluginCall(xapi, host, 'updater.py', method, {
packages: _package,
})
}
}
export function installDependencies({ host }) {
return installOrUpdateDependencies.call(this, host)
}
installDependencies.description = 'Install XOSTOR dependencies'
installDependencies.permission = 'admin'
installDependencies.params = {
host: { type: 'string' },
}
installDependencies.resolve = {
host: ['host', 'host', 'administrate'],
}
export function updateDependencies({ host }) {
return installOrUpdateDependencies.call(this, host, 'update')
}
updateDependencies.description = 'Update XOSTOR dependencies'
updateDependencies.permission = 'admin'
updateDependencies.params = {
host: { type: 'string' },
}
updateDependencies.resolve = {
host: ['host', 'host', 'administrate'],
}
export async function formatDisks({ disks, force, host, ignoreFileSystems, provisioning }) {
const rawDisks = disks.join(',')
const xapi = this.getXapi(host)
const lvmPlugin = (fnName, args) => pluginCall(xapi, host, 'lvm.py', fnName, args)
log.info(`Format disks (${rawDisks}) with force: ${force}`, { hostId: host.id })
if (force) {
await destroyVolumeGroup(xapi, host, force)
}
// ATM we are unable to correctly identify errors (error.code can be used for multiple errors.)
// so we are just adding some suggestion of "why there is this error"
// Error handling will be improved as errors are discovered and understood
try {
await lvmPlugin('create_physical_volume', {
devices: rawDisks,
ignore_existing_filesystems: String(ignoreFileSystems),
force: String(force),
})
} catch (error) {
if (error.code === 'LVM_ERROR(5)') {
error.params = error.params.concat([
"[XO] This error can be triggered if one of the disks is a 'tapdevs' disk.",
'[XO] This error can be triggered if one of the disks have children',
])
}
throw error
}
try {
await lvmPlugin('create_volume_group', {
devices: rawDisks,
vg_name: VG_NAME,
})
} catch (error) {
if (error.code === 'LVM_ERROR(5)') {
error.params = error.params.concat([
"[XO] This error can be triggered if a VG 'linstor_group' is already present on the host.",
])
}
throw error
}
if (provisioning === ENUM_PROVISIONING.Thin) {
await lvmPlugin('create_thin_pool', {
lv_name: LV_NAME,
vg_name: VG_NAME,
})
}
}
formatDisks.description = 'Format disks for a XOSTOR use'
formatDisks.permission = 'admin'
formatDisks.params = {
disks: { type: 'array', items: { type: 'string' } },
force: { type: 'boolean', optional: true, default: false },
host: { type: 'string' },
ignoreFileSystems: { type: 'boolean', optional: true, default: false },
provisioning: { enum: PROVISIONING },
}
formatDisks.resolve = {
host: ['host', 'host', 'administrate'],
}
export const create = defer(async function (
$defer,
{ description, disksByHost, force, ignoreFileSystems, name, provisioning, replication }
) {
const hostIds = Object.keys(disksByHost)
const tmpBoundObjectId = `tmp_${hostIds.join(',')}_${Math.random().toString(32).slice(2)}`
const xostorLicenses = await this.getLicenses({ productType: 'xostor' })
const now = Date.now()
const availableLicenses = xostorLicenses.filter(
({ boundObjectId, expires }) => boundObjectId === undefined && (expires === undefined || expires > now)
)
let license = availableLicenses.find(license => license.productId === 'xostor')
if (license === undefined) {
license = availableLicenses.find(license => license.productId === 'xostor.trial')
}
if (license === undefined) {
license = await this.createBoundXostorTrialLicense({
boundObjectId: tmpBoundObjectId,
})
} else {
await this.bindLicense({
licenseId: license.id,
boundObjectId: tmpBoundObjectId,
})
}
$defer.onFailure(() =>
this.unbindLicense({
licenseId: license.id,
productId: license.productId,
boundObjectId: tmpBoundObjectId,
})
)
const hosts = hostIds.map(hostId => this.getObject(hostId, 'host'))
if (!hosts.every(host => host.$pool === hosts[0].$pool)) {
// we need to do this test to ensure it won't create a partial LV group with only the host of the pool of the first master
throw new Error('All hosts must be in the same pool')
}
const boundInstallDependencies = installDependencies.bind(this)
await asyncEach(hosts, host => boundInstallDependencies({ host }), { stopOnError: false })
const boundFormatDisks = formatDisks.bind(this)
await asyncEach(
hosts,
host => boundFormatDisks({ disks: disksByHost[host.id], host, force, ignoreFileSystems, provisioning }),
{
stopOnError: false,
}
)
const host = hosts[0]
const xapi = this.getXapi(host)
log.info(`Create XOSTOR (${name}) with provisioning: ${provisioning}`)
const srRef = await xapi.SR_create({
device_config: {
'group-name': 'linstor_group/' + LV_NAME,
redundancy: String(replication),
provisioning,
},
host: host.id,
name_description: description,
name_label: name,
shared: true,
type: 'linstor',
})
const srUuid = await xapi.getField('SR', srRef, 'uuid')
await this.rebindLicense({
licenseId: license.id,
oldBoundObjectId: tmpBoundObjectId,
newBoundObjectId: srUuid,
})
return srUuid
})
create.description = 'Create a XOSTOR storage'
create.permission = 'admin'
create.params = {
description: { type: 'string', optional: true, default: 'From XO-server' },
disksByHost: { type: 'object' },
force: { type: 'boolean', optional: true, default: false },
ignoreFileSystems: { type: 'boolean', optional: true, default: false },
name: { type: 'string' },
provisioning: { enum: PROVISIONING },
replication: { type: 'number' },
}
// Also called by sr.destroy if sr.SR_type === 'linstor'
export async function destroy({ sr }) {
if (sr.SR_type !== 'linstor') {
throw new Error('Not a XOSTOR storage')
}
const xapi = this.getXapi(sr)
const hosts = Object.values(xapi.objects.indexes.type.host).map(host => this.getObject(host.uuid, 'host'))
await xapi.destroySr(sr._xapiId)
const license = (await this.getLicenses({ productType: 'xostor' })).find(license => license.boundObjectId === sr.uuid)
await this.unbindLicense({
boundObjectId: license.boundObjectId,
productId: license.productId,
})
return asyncEach(hosts, host => destroyVolumeGroup(xapi, host, true), { stopOnError: false })
}
destroy.description = 'Destroy a XOSTOR storage'
destroy.permission = 'admin'
destroy.params = {
sr: { type: 'string' },
}
destroy.resolve = {
sr: ['sr', 'SR', 'administrate'],
}

View File

@@ -35,16 +35,6 @@ import Collection, { ModelAlreadyExists } from '../collection.mjs'
const VERSION = '20170905'
export default class Redis extends Collection {
// Prepare a record before storing in the database
//
// Input object can be mutated or a new one returned
_serialize(record) {}
// Clean a record after being fetched from the database
//
// Input object can be mutated or a new one returned
_unserialize(record) {}
constructor({ connection, indexes = [], namespace }) {
super()
@@ -95,8 +85,8 @@ export default class Redis extends Collection {
)
const idsIndex = `${prefix}_ids`
await asyncMapSettled(redis.sMembers(idsIndex), id => {
return this.#get(`${prefix}:${id}`).then(values =>
await asyncMapSettled(redis.sMembers(idsIndex), id =>
redis.hGetAll(`${prefix}:${id}`).then(values =>
values == null
? redis.sRem(idsIndex, id) // entry no longer exists
: asyncMapSettled(indexes, index => {
@@ -106,23 +96,22 @@ export default class Redis extends Collection {
}
})
)
})
)
}
_extract(ids) {
const prefix = this.prefix + ':'
const { redis } = this
const models = []
return Promise.all(
map(ids, id => {
return this.#get(prefix + id).then(model => {
return redis.hGetAll(prefix + id).then(model => {
// If empty, consider it a no match.
if (isEmpty(model)) {
return
}
model = this._unserialize(model) ?? model
// Mix the identifier in.
model.id = id
@@ -140,12 +129,6 @@ export default class Redis extends Collection {
return Promise.all(
map(models, async model => {
// don't mutate param
model = JSON.parse(JSON.stringify(model))
// allow specific serialization
model = this._serialize(model) ?? model
// Generate a new identifier if necessary.
if (model.id === undefined) {
model.id = generateUuid()
@@ -161,7 +144,7 @@ export default class Redis extends Collection {
// remove the previous values from indexes
if (indexes.length !== 0) {
const previous = await this.#get(`${prefix}:${id}`)
const previous = await redis.hGetAll(`${prefix}:${id}`)
await asyncMapSettled(indexes, index => {
const value = previous[index]
if (value !== undefined) {
@@ -201,22 +184,6 @@ export default class Redis extends Collection {
)
}
async #get(key) {
const { redis } = this
let model
try {
model = await redis.hGetAll(key)
} catch (error) {
if (!error.message.startsWith('WRONGTYPE')) {
throw error
}
model = await redis.get(key).then(JSON.parse)
}
return model
}
_get(properties) {
const { prefix, redis } = this
@@ -260,7 +227,7 @@ export default class Redis extends Collection {
promise = Promise.all([
promise,
asyncMapSettled(ids, id =>
this.#get(`${prefix}:${id}`).then(
redis.hGetAll(`${prefix}:${id}`).then(
values =>
values != null &&
asyncMapSettled(indexes, index => {

View File

@@ -2,21 +2,33 @@ import isEmpty from 'lodash/isEmpty.js'
import Collection from '../collection/redis.mjs'
import { forEach } from '../utils.mjs'
import { parseProp } from './utils.mjs'
// ===================================================================
export class Groups extends Collection {
_serialize(group) {
let tmp
group.users = isEmpty((tmp = group.users)) ? undefined : JSON.stringify(tmp)
}
_unserialize(group) {
group.users = parseProp('group', group, 'users', [])
}
create(name, provider, providerGroupId) {
return this.add({ name, provider, providerGroupId })
}
async save(group) {
// Serializes.
let tmp
group.users = isEmpty((tmp = group.users)) ? undefined : JSON.stringify(tmp)
return /* await */ this.update(group)
}
async get(properties) {
const groups = await super.get(properties)
// Deserializes.
forEach(groups, group => {
group.users = parseProp('group', group, 'users', [])
})
return groups
}
}

View File

@@ -1,26 +1,18 @@
import Collection from '../collection/redis.mjs'
import { createLogger } from '@xen-orchestra/log'
import { forEach } from '../utils.mjs'
const log = createLogger('xo:plugin-metadata')
// ===================================================================
export class PluginsMetadata extends Collection {
_serialize(metadata) {
const { autoload, configuration } = metadata
metadata.autoload = JSON.stringify(autoload)
metadata.configuration = JSON.stringify(configuration)
}
_unserialize(metadata) {
const { autoload, configuration } = metadata
metadata.autoload = autoload === 'true'
try {
metadata.configuration = configuration && JSON.parse(configuration)
} catch (error) {
log.warn(`cannot parse pluginMetadata.configuration: ${configuration}`)
metadata.configuration = []
}
async save({ id, autoload, configuration }) {
return /* await */ this.update({
id,
autoload: autoload ? 'true' : 'false',
configuration: configuration && JSON.stringify(configuration),
})
}
async merge(id, data) {
@@ -29,9 +21,27 @@ export class PluginsMetadata extends Collection {
throw new Error('no such plugin metadata')
}
return /* await */ this.update({
return /* await */ this.save({
...pluginMetadata,
...data,
})
}
async get(properties) {
const pluginsMetadata = await super.get(properties)
// Deserializes.
forEach(pluginsMetadata, pluginMetadata => {
const { autoload, configuration } = pluginMetadata
pluginMetadata.autoload = autoload === 'true'
try {
pluginMetadata.configuration = configuration && JSON.parse(configuration)
} catch (error) {
log.warn(`cannot parse pluginMetadata.configuration: ${configuration}`)
pluginMetadata.configuration = []
}
})
return pluginsMetadata
}
}

View File

@@ -1,26 +1,36 @@
import Collection from '../collection/redis.mjs'
import { serializeError } from '../utils.mjs'
import { forEach, serializeError } from '../utils.mjs'
import { parseProp } from './utils.mjs'
// ===================================================================
export class Remotes extends Collection {
_serialize(remote) {
const { benchmarks } = remote
if (benchmarks !== undefined) {
remote.benchmarks = JSON.stringify(benchmarks)
}
const { error } = remote
if (error !== undefined) {
remote.error = JSON.stringify(typeof error === 'object' ? serializeError(error) : error)
}
async get(properties) {
const remotes = await super.get(properties)
forEach(remotes, remote => {
remote.benchmarks = parseProp('remote', remote, 'benchmarks')
remote.enabled = remote.enabled === 'true'
remote.error = parseProp('remote', remote, 'error', remote.error)
})
return remotes
}
_unserialize(remote) {
remote.benchmarks = parseProp('remote', remote, 'benchmarks')
remote.enabled = remote.enabled === 'true'
remote.error = parseProp('remote', remote, 'error', remote.error)
_update(remotes) {
return super._update(
remotes.map(remote => {
const { benchmarks } = remote
if (benchmarks !== undefined) {
remote.benchmarks = JSON.stringify(benchmarks)
}
const { error } = remote
if (error !== undefined) {
remote.error = JSON.stringify(typeof error === 'object' ? serializeError(error) : error)
}
return remote
})
)
}
}

View File

@@ -1,35 +1,11 @@
import Collection from '../collection/redis.mjs'
import { serializeError } from '../utils.mjs'
import { forEach, serializeError } from '../utils.mjs'
import { parseProp } from './utils.mjs'
// ===================================================================
export class Servers extends Collection {
_serialize(server) {
server.allowUnauthorized = server.allowUnauthorized ? 'true' : undefined
server.enabled = server.enabled ? 'true' : undefined
const { error } = server
server.error = error != null ? JSON.stringify(serializeError(error)) : undefined
server.readOnly = server.readOnly ? 'true' : undefined
}
_unserialize(server) {
server.allowUnauthorized = server.allowUnauthorized === 'true'
server.enabled = server.enabled === 'true'
if (server.error) {
server.error = parseProp('server', server, 'error', '')
} else {
delete server.error
}
server.readOnly = server.readOnly === 'true'
// see https://github.com/vatesfr/xen-orchestra/issues/6656
if (server.httpProxy === '') {
delete server.httpProxy
}
}
async create(params) {
const { host } = params
@@ -39,4 +15,38 @@ export class Servers extends Collection {
return /* await */ this.add(params)
}
async get(properties) {
const servers = await super.get(properties)
// Deserializes
forEach(servers, server => {
server.allowUnauthorized = server.allowUnauthorized === 'true'
server.enabled = server.enabled === 'true'
if (server.error) {
server.error = parseProp('server', server, 'error', '')
} else {
delete server.error
}
server.readOnly = server.readOnly === 'true'
// see https://github.com/vatesfr/xen-orchestra/issues/6656
if (server.httpProxy === '') {
delete server.httpProxy
}
})
return servers
}
_update(servers) {
servers.forEach(server => {
server.allowUnauthorized = server.allowUnauthorized ? 'true' : undefined
server.enabled = server.enabled ? 'true' : undefined
const { error } = server
server.error = error != null ? JSON.stringify(serializeError(error)) : undefined
server.readOnly = server.readOnly ? 'true' : undefined
})
return super._update(servers)
}
}

View File

@@ -2,29 +2,4 @@ import Collection from '../collection/redis.mjs'
// ===================================================================
export class Tokens extends Collection {
_serialize(token) {
const { client } = token
if (client !== undefined) {
const { id, ...rest } = client
token.client_id = id
token.client = JSON.stringify(rest)
}
}
_unserialize(token) {
const { client, client_id } = token
if (client !== undefined) {
token.client = {
...JSON.parse(client),
id: client_id,
}
delete token.client_id
}
if (token.created_at !== undefined) {
token.created_at = +token.created_at
}
token.expiration = +token.expiration
}
}
export class Tokens extends Collection {}

View File

@@ -6,23 +6,25 @@ import { parseProp } from './utils.mjs'
// ===================================================================
const serialize = user => {
let tmp
return {
...user,
authProviders: isEmpty((tmp = user.authProviders)) ? undefined : JSON.stringify(tmp),
groups: isEmpty((tmp = user.groups)) ? undefined : JSON.stringify(tmp),
preferences: isEmpty((tmp = user.preferences)) ? undefined : JSON.stringify(tmp),
}
}
const deserialize = user => ({
permission: 'none',
...user,
authProviders: parseProp('user', user, 'authProviders', undefined),
groups: parseProp('user', user, 'groups', []),
preferences: parseProp('user', user, 'preferences', {}),
})
export class Users extends Collection {
_serialize(user) {
let tmp
user.authProviders = isEmpty((tmp = user.authProviders)) ? undefined : JSON.stringify(tmp)
user.groups = isEmpty((tmp = user.groups)) ? undefined : JSON.stringify(tmp)
user.preferences = isEmpty((tmp = user.preferences)) ? undefined : JSON.stringify(tmp)
}
_unserialize(user) {
if (user.permission === undefined) {
user.permission = 'none'
}
user.authProviders = parseProp('user', user, 'authProviders', undefined)
user.groups = parseProp('user', user, 'groups', [])
user.preferences = parseProp('user', user, 'preferences', {})
}
async create(properties) {
const { email } = properties
@@ -32,6 +34,14 @@ export class Users extends Collection {
}
// Adds the user to the collection.
return /* await */ this.add(properties)
return /* await */ this.add(serialize(properties))
}
async save(user) {
return /* await */ this.update(serialize(user))
}
async get(properties) {
return (await super.get(properties)).map(deserialize)
}
}

View File

@@ -511,8 +511,7 @@ const TRANSFORMS = {
// TODO: Should it replace usage?
physical_usage: +obj.physical_utilisation,
allocationStrategy:
srType === 'linstor' ? obj.$PBDs[0]?.device_config.provisioning ?? 'unknown' : ALLOCATION_BY_TYPE[srType],
allocationStrategy: ALLOCATION_BY_TYPE[srType],
current_operations: obj.current_operations,
inMaintenanceMode: obj.other_config['xo:maintenanceState'] !== undefined,
name_description: obj.name_description,

View File

@@ -66,7 +66,6 @@ export default class Xapi extends XapiBase {
maxUncoalescedVdis,
restartHostTimeout,
vdiExportConcurrency,
vmEvacuationConcurrency,
vmExportConcurrency,
vmMigrationConcurrency = 3,
vmSnapshotConcurrency,
@@ -77,7 +76,6 @@ export default class Xapi extends XapiBase {
this._guessVhdSizeOnImport = guessVhdSizeOnImport
this._maxUncoalescedVdis = maxUncoalescedVdis
this._restartHostTimeout = parseDuration(restartHostTimeout)
this._vmEvacuationConcurrency = vmEvacuationConcurrency
// close event is emitted when the export is canceled via browser. See https://github.com/vatesfr/xen-orchestra/issues/5535
const waitStreamEnd = async stream => fromEvents(await stream, ['end', 'close'])
@@ -194,36 +192,22 @@ export default class Xapi extends XapiBase {
return network.$ref
}
})(pool.other_config['xo:migrationNetwork'])
// host ref
// migration network: optional and might not be supported
// batch size: optional and might not be supported
const params = [hostRef, migrationNetworkRef ?? Ref.EMPTY, this._vmEvacuationConcurrency]
// Removes n params from the end and keeps removing until a non-empty param is found
const popParamsAndTrim = (n = 0) => {
let last
let i = 0
while (i < n || (last = params[params.length - 1]) === undefined || last === Ref.EMPTY) {
if (params.length <= 1) {
throw new Error('not enough params left')
}
params.pop()
i++
}
}
popParamsAndTrim()
try {
await pRetry(() => this.callAsync('host.evacuate', ...params), {
delay: 0,
when: { code: 'MESSAGE_PARAMETER_COUNT_MISMATCH' },
onRetry: error => {
log.warn(error)
popParamsAndTrim(1)
},
})
try {
await (migrationNetworkRef === undefined
? this.callAsync('host.evacuate', hostRef)
: this.callAsync('host.evacuate', hostRef, migrationNetworkRef))
} catch (error) {
if (error.code === 'MESSAGE_PARAMETER_COUNT_MISMATCH') {
log.warn(
'host.evacuate with a migration network is not supported on this host, falling back to evacuating without the migration network',
{ error }
)
await this.callAsync('host.evacuate', hostRef)
} else {
throw error
}
}
} catch (error) {
if (!force) {
await this.call('host.enable', hostRef)

View File

@@ -405,11 +405,6 @@ 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
@@ -425,6 +420,11 @@ 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, { xsCredentials } = {}) {
async rollingPoolUpdate($defer) {
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({ xsCredentials })
await this.installPatches()
}
// Remember on which hosts the running VMs are

View File

@@ -49,19 +49,18 @@ export default {
await this._unplugPbd(this.getObject(id))
},
_getVdiChainsInfo(uuid, childrenMap, cache, resultContainer) {
_getVdiChainsInfo(uuid, childrenMap, cache) {
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, resultContainer)
info = this._getVdiChainsInfo(parent, childrenMap, cache)
info.unhealthyLength += unhealthyLength
} else {
info = { unhealthyLength }
@@ -77,13 +76,12 @@ 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, resultContainer)
const { unhealthyLength, missingParent } = this._getVdiChainsInfo(uuid, children, cache)
if (unhealthyLength !== 0) {
unhealthyVdis[uuid] = unhealthyLength
@@ -97,7 +95,6 @@ export default {
return {
vdisWithUnknownVhdParent,
unhealthyVdis,
...resultContainer,
}
},

View File

@@ -59,38 +59,10 @@ const hasPermission = (actual, expected) => PERMISSIONS[actual] >= PERMISSIONS[e
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, useDefaults: true })
function checkParams(method, params) {
// Parameters suffixed by `?` are marked as ignorable by the client and
// ignored if unsupported by this version of the API
//
// This simplifies compatibility with older version of the API if support
// of the parameter is preferable but not necessary
const ignorableParams = new Set()
for (const key of Object.keys(params)) {
if (key.endsWith('?')) {
const rawKey = key.slice(0, -1)
if (Object.hasOwn(params, rawKey)) {
throw new Error(`conflicting keys: ${rawKey} and ${key}`)
}
params[rawKey] = params[key]
delete params[key]
ignorableParams.add(rawKey)
}
}
const { validate } = method
if (validate !== undefined) {
if (!validate(params)) {
const vErrors = new Set(validate.errors)
for (const error of vErrors) {
if (error.schemaPath === '#/additionalProperties' && ignorableParams.has(error.params.additionalProperty)) {
delete params[error.params.additionalProperty]
vErrors.delete(error)
}
}
if (vErrors.size !== 0) {
throw errors.invalidParameters(Array.from(vErrors))
}
throw errors.invalidParameters(validate.errors)
}
}
}

View File

@@ -15,6 +15,13 @@ const log = createLogger('xo:authentification')
const noSuchAuthenticationToken = id => noSuchObject(id, 'authenticationToken')
const unserialize = token => {
if (token.created_at !== undefined) {
token.created_at = +token.created_at
}
token.expiration = +token.expiration
}
export default class {
constructor(app) {
app.config.watch('authentication', config => {
@@ -79,7 +86,7 @@ export default class {
const tokensDb = (this._tokens = new Tokens({
connection: app._redis,
namespace: 'token',
indexes: ['client_id', 'user_id'],
indexes: ['user_id'],
}))
app.addConfigManager(
@@ -180,7 +187,7 @@ export default class {
// -----------------------------------------------------------------
async createAuthenticationToken({ client, description, expiresIn, userId }) {
async createAuthenticationToken({ description, expiresIn, userId }) {
let duration = this._defaultTokenValidity
if (expiresIn !== undefined) {
duration = parseDuration(expiresIn)
@@ -191,27 +198,8 @@ export default class {
}
}
const tokens = this._tokens
const now = Date.now()
const clientId = client?.id
if (clientId !== undefined) {
const token = await tokens.first({ client_id: clientId, user_id: userId })
if (token !== undefined) {
if (token.expiration > now) {
token.description = description
token.expiration = now + duration
tokens.update(token)::ignoreErrors()
return token
}
tokens.remove(token.id)::ignoreErrors()
}
}
const token = {
client,
created_at: now,
description,
id: await generateToken(),
@@ -246,6 +234,8 @@ export default class {
async _getAuthenticationToken(id, properties) {
const token = await this._tokens.first(properties ?? id)
if (token !== undefined) {
unserialize(token)
if (token.expiration > Date.now()) {
return token
}
@@ -271,6 +261,8 @@ export default class {
const tokensDb = this._tokens
const toRemove = []
for (const token of await tokensDb.get({ user_id: userId })) {
unserialize(token)
const { expiration } = token
if (expiration < now) {
toRemove.push(token.id)

View File

@@ -1,4 +1,5 @@
import * as openpgp from 'openpgp'
import DepTree from 'deptree'
import fromCallback from 'promise-toolbox/fromCallback'
import { createLogger } from '@xen-orchestra/log'
import { gunzip, gzip } from 'node:zlib'
@@ -10,6 +11,7 @@ const log = createLogger('xo:config-management')
export default class ConfigManagement {
constructor(app) {
this._app = app
this._depTree = new DepTree()
this._managers = { __proto__: null }
}
@@ -19,6 +21,7 @@ export default class ConfigManagement {
throw new Error(`${id} is already taken`)
}
this._depTree.add(id, dependencies)
this._managers[id] = { dependencies, exporter, importer }
}
@@ -73,27 +76,15 @@ export default class ConfigManagement {
config = JSON.parse(config)
const managers = this._managers
const imported = new Set()
async function importEntry(id) {
if (!imported.has(id)) {
imported.add(id)
for (const key of this._depTree.resolve()) {
const manager = managers[key]
await importEntries(managers[id].dependencies)
const data = config[id]
if (data !== undefined) {
log.debug(`importing ${id}`)
await managers[id].importer(data)
}
const data = config[key]
if (data !== undefined) {
log.debug(`importing ${key}`)
await manager.importer(data)
}
}
async function importEntries(ids) {
for (const id of ids) {
await importEntry(id)
}
}
await importEntries(Object.keys(config))
await this._app.hooks.clean()
}
}

View File

@@ -33,7 +33,7 @@ export default class {
plugins =>
Promise.all(
plugins.map(async plugin => {
await this._pluginsMetadata.update(plugin)
await this._pluginsMetadata.save(plugin)
if (plugin.configuration !== undefined && this._plugins[plugin.id] !== undefined) {
await this.configurePlugin(plugin.id, plugin.configuration)
}
@@ -88,7 +88,7 @@ export default class {
;({ autoload, configuration } = metadata)
} else {
log.info(`[NOTICE] register plugin ${name} for the first time`)
await this._pluginsMetadata.update({
await this._pluginsMetadata.save({
id,
autoload,
})

View File

@@ -62,14 +62,11 @@ 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
@@ -77,7 +74,6 @@ export default class Pools {
installPatchesPromises.push(
sourceXapis[sourceId].installPatches({
patches: patchesName[i++],
xsCredentials,
})
)
}

View File

@@ -444,7 +444,6 @@ export default class {
async shareVmResourceSet(vmId) {
const xapi = this._app.getXapi(vmId)
await xapi.barrier(xapi.getObject(vmId).$ref)
const resourceSetId = xapi.xo.getData(vmId, 'resourceSet')
if (resourceSetId === undefined) {
throw new Error('the vm is not in a resource set')

View File

@@ -39,7 +39,7 @@ export default class {
app.addConfigManager(
'groups',
() => groupsDb.get(),
groups => Promise.all(groups.map(group => groupsDb.update(group))),
groups => Promise.all(groups.map(group => groupsDb.save(group))),
['users']
)
app.addConfigManager(
@@ -53,7 +53,7 @@ export default class {
if (!isEmpty(conflictUsers)) {
await Promise.all(conflictUsers.map(({ id }) => id !== userId && this.deleteUser(id)))
}
return usersDb.update(user)
return usersDb.save(user)
})
)
)
@@ -196,7 +196,7 @@ export default class {
user.email = user.name
delete user.name
await this._users.update(user)
await this._users.save(user)
}
// Merge this method in getUser() when plain objects.
@@ -368,7 +368,7 @@ export default class {
if (name) group.name = name
await this._groups.update(group)
await this._groups.save(group)
}
async getGroup(id) {
@@ -390,17 +390,17 @@ export default class {
user.groups = addToArraySet(user.groups, groupId)
group.users = addToArraySet(group.users, userId)
await Promise.all([this._users.update(user), this._groups.update(group)])
await Promise.all([this._users.save(user), this._groups.save(group)])
}
async _removeUserFromGroup(userId, group) {
group.users = removeFromArraySet(group.users, userId)
return this._groups.update(group)
return this._groups.save(group)
}
async _removeGroupFromUser(groupId, user) {
user.groups = removeFromArraySet(user.groups, groupId)
return this._users.update(user)
return this._users.save(user)
}
async removeUserFromGroup(userId, groupId) {
@@ -438,11 +438,11 @@ export default class {
group.users = userIds
const updateUser = ::this._users.update
const saveUser = ::this._users.save
await Promise.all([
Promise.all(newUsers.map(updateUser)),
Promise.all(oldUsers.map(updateUser)),
this._groups.update(group),
Promise.all(newUsers.map(saveUser)),
Promise.all(oldUsers.map(saveUser)),
this._groups.save(group),
])
}
}

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.127.1",
"version": "5.126.0",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

View File

@@ -3184,6 +3184,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "Xen Orchestra server"
xenOrchestraServer: 'servidor',
// Original text: "Xen Orchestra web client"
xenOrchestraWeb: 'cliente web',
// Original text: "No pro support provided!"
noProSupport: '¡Sin soporte Pro!',

View File

@@ -3261,6 +3261,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "Xen Orchestra server"
xenOrchestraServer: 'Serveur Xen Orchestra',
// Original text: "Xen Orchestra web client"
xenOrchestraWeb: 'Client web Xen Orchestra',
// Original text: "No pro support provided!"
noProSupport: 'Pas de support professionel fourni !',

View File

@@ -2720,6 +2720,12 @@ export default {
// Original text: 'Xen Orchestra'
xenOrchestra: undefined,
// Original text: 'server'
xenOrchestraServer: undefined,
// Original text: 'web client'
xenOrchestraWeb: undefined,
// Original text: 'No pro support provided!'
noProSupport: undefined,

View File

@@ -3003,6 +3003,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'CLOUDXO',
// Original text: "Xen Orchestra server"
xenOrchestraServer: 'Cloudxo szerver',
// Original text: "Xen Orchestra web client"
xenOrchestraWeb: 'Cloudxo web kliens',
// Original text: "No pro support provided!"
noProSupport: 'Nincsen pro-szupport!',

View File

@@ -4697,6 +4697,12 @@ export default {
// Original text: 'No host selected to be added'
addHostNoHostMessage: 'Nessun host selezionato da aggiungere',
// Original text: 'Xen Orchestra server'
xenOrchestraServer: 'Server Xen Orchestra',
// Original text: 'Xen Orchestra web client'
xenOrchestraWeb: 'Client web Xen Orchestra',
// Original text: 'Professional support missing!'
noProSupport: 'Manca il supporto professionale!',

View File

@@ -2734,6 +2734,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "server"
xenOrchestraServer: 'serwer',
// Original text: "web client"
xenOrchestraWeb: 'web klient',
// Original text: "No pro support provided!"
noProSupport: 'No pro support provided!',

View File

@@ -2729,6 +2729,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "server"
xenOrchestraServer: 'servidor',
// Original text: "web client"
xenOrchestraWeb: 'cliente web',
// Original text: "No pro support provided!"
noProSupport: 'Nenhum suporte pro fornecido!',

View File

@@ -3187,6 +3187,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "Xen Orchestra server"
xenOrchestraServer: 'Сервер Xen Orchestra',
// Original text: "Xen Orchestra web client"
xenOrchestraWeb: 'WEB-клиент Xen Orchestra',
// Original text: "No pro support provided!"
noProSupport: '"PRO" поддержка не предоставляется!',

View File

@@ -4011,6 +4011,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "Xen Orchestra server"
xenOrchestraServer: 'Xen Orchestra sunucusu',
// Original text: "Xen Orchestra web client"
xenOrchestraWeb: 'Xen Orchestra web istemcisi',
// Original text: "No pro support provided!"
noProSupport: 'Hiçbir profesyonel destek verilmez!',

View File

@@ -2064,6 +2064,12 @@ export default {
// Original text: "Xen Orchestra"
xenOrchestra: 'Xen Orchestra',
// Original text: "server"
xenOrchestraServer: '服务器',
// Original text: "web client"
xenOrchestraWeb: 'Web客户端',
// Original text: "No pro support provided!"
noProSupport: '不提供专业支持!',

View File

@@ -25,10 +25,8 @@ const messages = {
esxiImportStopOnErrorDescription: 'Stop on the first error when importing VMs',
nImportVmsInParallel: 'Number of VMs to import in parallel',
stopOnError: 'Stop on error',
uuid: 'UUID',
vmSrUsage: 'Storage: {used} used of {total} ({free} free)',
new: 'New',
notDefined: 'Not defined',
status: 'Status',
statusConnecting: 'Connecting',
@@ -119,8 +117,6 @@ const messages = {
advancedSettings: 'Advanced settings',
forceUpgrade: 'Force upgrade',
txChecksumming: 'TX checksumming',
thick: 'Thick',
thin: 'Thin',
unknownSize: 'Unknown size',
installedCertificates: 'Installed certificates',
expiry: 'Expiry',
@@ -1103,7 +1099,7 @@ const messages = {
installAllPatchesContent: 'To install all patches go to pool.',
installAllPatchesRedirect: 'Go to pool',
installAllPatchesOnHostContent:
'The pool master must always be updated FIRST. Updating will automatically restart the toolstack. Running VMs will not be affected. Are you sure you want to continue and install all patches on this host?',
'This will automatically restart the toolstack. Running VMs will not be affected. Are you sure you want to continue and install all patches on this host?',
patchRelease: 'Release',
updatePluginNotInstalled:
'An error occurred while fetching the patches. Please make sure the updater plugin is installed by running `yum install xcp-ng-updater` on the host.',
@@ -1371,10 +1367,6 @@ const messages = {
logAction: 'Action',
// ----- VM advanced tab -----
createVtpm: 'Create a VTPM',
deleteVtpm: 'Delete the VTPM',
deleteVtpmWarning:
'If the VTPM is in use, removing it will result in a dangerous data loss. Are you sure you want to remove the VTPM?',
vmRemoveButton: 'Remove',
vmConvertToTemplateButton: 'Convert to template',
vmSwitchVirtualizationMode: 'Convert to {mode}',
@@ -1404,12 +1396,9 @@ const messages = {
srHaTooltip: 'SR used for High Availability',
nestedVirt: 'Nested virtualization',
vmAffinityHost: 'Affinity host',
vmNeedToBeHalted: 'The VM needs to be halted',
vmVga: 'VGA',
vmVideoram: 'Video RAM',
vmNicType: 'NIC type',
vtpm: 'VTPM',
vtpmRequireUefi: 'A UEFI boot firmware is necessary to use a VTPM',
noAffinityHost: 'None',
originalTemplate: 'Original template',
unknownOsName: 'Unknown',
@@ -1579,14 +1568,13 @@ const messages = {
unhealthyVdis: 'Unhealthy VDIs',
vdisToCoalesce: 'VDIs to coalesce',
vdisWithInvalidVhdParent: 'VDIs with invalid parent VHD',
srVdisToCoalesceWarning: 'This SR has {nVdis, number} VDI{nVdis, plural, one {} other {s}} to coalesce',
srVdisToCoalesceWarning: 'This SR has more than {limitVdis, number} VDIs to coalesce',
// ----- New VM -----
createVmModalTitle: 'Create VM',
createVmModalWarningMessage:
"You're about to use a large amount of resources available on the resource set. Are you sure you want to continue?",
copyHostBiosStrings: 'Copy host BIOS strings to VM',
enableVtpm: 'Enable VTPM',
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Info',
@@ -1656,11 +1644,8 @@ const messages = {
newVmNetworkConfigDoc: 'Network config documentation',
templateHasBiosStrings: 'The template already contains the BIOS strings',
secureBootLinkToDocumentationMessage: 'Click for more information about Guest UEFI Secure Boot.',
seeVtpmDocumentation: 'See VTPM documentation',
vmBootFirmwareIsUefi: 'The boot firmware is UEFI',
destroyCloudConfigVdiAfterBoot: 'Destroy cloud config drive after first boot',
vtpmNotSupported: 'VTPM is only supported on pools running XCP-ng/XS 8.3 or later.',
warningVtpmRequired: 'This template requires a VTPM, if you proceed, the VM will likely not be able to boot.',
// ----- Self -----
resourceSets: 'Resource sets',
@@ -1695,8 +1680,6 @@ const messages = {
resourceSetQuota: 'Used: {usage} (Total: {total})',
resourceSetNew: 'New',
shareVmsByDefault: 'Share VMs by default',
nVmsInResourceSet:
'{nVms, number} VM{nVms, plural, one {} other {s}} belong{nVms, plural, one {s} other {}} to this Resource Set',
// ---- VM import ---
fileType: 'File type:',
@@ -2109,7 +2092,8 @@ const messages = {
addHostNoHostMessage: 'No host selected to be added',
// ----- About View -----
failedToFetchLatestMasterCommit: 'Failed to fetch latest master commit',
xenOrchestraServer: 'Xen Orchestra server',
xenOrchestraWeb: 'Xen Orchestra web client',
noProSupport: 'Professional support missing!',
productionUse: 'Want to use in production?',
getSupport: 'Get pro support with the Xen Orchestra Appliance at {website}',
@@ -2127,9 +2111,6 @@ const messages = {
xoAccount: 'Access your XO Account',
openTicket: 'Report a problem',
openTicketText: 'Problem? Open a ticket!',
xoUpToDate: 'Your Xen Orchestra is up to date',
xoFromSourceNotUpToDate:
'You are not up to date with master. {nBehind} commit{nBehind, plural, one {} other {s}} behind {nAhead, plural, =0 {} other {and {nAhead, number} commit{nAhead, plural, one {} other {s}} ahead}}',
// ----- Upgrade Panel -----
upgradeNeeded: 'Upgrade needed',
@@ -2372,9 +2353,9 @@ const messages = {
'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
srForgetModalTitle: 'Forget SR',
srsForgetModalTitle: 'Forget selected SRs',
srForgetModalMessage: "Are you sure you want to forget this SR? You will lose all the metadata for it, meaning all the links between the VDIs (disks) and their respective VMs. This operation cannot be undone.",
srForgetModalMessage: "Are you sure you want to forget this SR? VDIs on this storage won't be removed.",
srsForgetModalMessage:
"Are you sure you want to forget {nPbds, number} SR{nPbds, plural, one {} other {s}}? You will lose all the metadata for it, meaning all the links between the VDIs (disks) and their respective VMs. This operation cannot be undone.",
"Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.",
srAllDisconnected: 'Disconnected',
srSomeConnected: 'Partially connected',
srAllConnected: 'Connected',
@@ -2492,44 +2473,6 @@ const messages = {
xosanUnderlyingStorageUsage: 'Using {usage}',
xosanCustomIpNetwork: 'Custom IP network (/24)',
xosanIssueHostNotInNetwork: 'Will configure the host xosan network device with a static IP address and plug it in.',
// ----- XOSTOR -----
approximateFinalSize: 'Approximate final size',
cantFetchDisksFromNonXcpngHost: 'Unable to fetch physical disks from non-XCP-ng host',
diskAlreadyMounted: 'The disk is mounted on: {mountpoint}',
diskHasChildren: 'The disk has children',
diskIncompatibleXostor: 'Disk incompatible with XOSTOR',
diskIsReadOnly: 'The disk is Read-Only',
disks: 'Disks',
fieldRequired: '{field} is required',
fieldsMissing: 'Some fields are missing',
hostsNotSameNumberOfDisks: 'Hosts do not have the same number of disks',
isTapdevsDisk: 'This is "tapdevs" disk',
licenseBoundUnknownXostor: 'License attached to an unknown XOSTOR',
licenseNotBoundXostor: 'No XOSTOR attached',
licenseExpiredXostorWarning:
'The license {licenseId} has expired. You can still use the SR but cannot administrate it anymore.',
networks: 'Networks',
notXcpPool: 'Not an XCP-ng pool',
noXostorFound: 'No XOSTOR found',
numberOfHosts: 'Number of hosts',
objectDoesNotMeetXostorRequirements: '{object} does not meet XOSTOR requirements. Refer to the documentation.',
onlyShowXostorRequirements: 'Only show {type} that meet XOSTOR requirements',
poolAlreadyHasXostor: 'Pool already has a XOSTOR',
poolNotRecentEnough: 'Not recent enough. Current version: {version}',
replication: 'Replication',
selectDisks: 'Select disk(s)…',
selectedDiskTypeIncompatibleXostor: 'Only disks of type "Disk" and "Raid" are accepted. Selected disk type: {type}.',
storage: 'Storage',
summary: 'Summary',
wrongNumberOfHosts: 'Wrong number of hosts',
xostor: 'XOSTOR',
xostorAvailableInXoa: 'XOSTOR is available in XOA',
xostorIsInBetaStage: 'XOSTOR is currently in its BETA stage. Do not use it in a production environment!',
xostorDiskRequired: 'At least one disk is required',
xostorDisksDropdownLabel: '({nDisks, number} disk{nDisks, plural, one {} other {s}}) {hostname}',
xostorMultipleLicenses: 'This XOSTOR has more than 1 license!',
xostorPackagesWillBeInstalled: '"xcp-ng-release-linstor" and "xcp-ng-linstor" will be installed on each host',
xostorReplicationWarning: 'If a disk dies, you will lose data',
// Hub
hubPage: 'Hub',

View File

@@ -9,15 +9,7 @@ import map from 'lodash/map.js'
import { renderXoItemFromId } from './render-xo-item'
const LicenseOptions = ({ license, formatDate }) => {
/**
* license.productId can be:
* - xcpng-enterprise
* - xcpng-standard
* - xo-proxy
* - xostor
* - xostor.trial
*/
const productId = license.productId.startsWith('xostor') ? license.productId : license.productId.split('-')[1]
const productId = license.productId.split('-')[1]
return (
<option value={license.id}>
<span>

View File

@@ -109,13 +109,7 @@ const xo = invoke(() => {
credentials: { token },
})
xo.on('authenticationFailure', error => {
console.warn('authenticationFailure', error)
if (error.name !== 'ConnectionError') {
signOut(error)
}
})
xo.on('authenticationFailure', signOut)
xo.on('scheduledAttempt', ({ delay }) => {
console.warn('next attempt in %s ms', delay)
})
@@ -1667,22 +1661,14 @@ export const migrateVms = vms =>
)
}, noop)
export const createVm = async args => {
try {
return await _call('vm.create', args)
} catch (err) {
handlePoolDoesNotSupportVtpmError(err)
throw error
}
}
export const createVm = args => _call('vm.create', args)
export const createVms = async (args, nameLabels, cloudConfigs) => {
await confirm({
export const createVms = (args, nameLabels, cloudConfigs) =>
confirm({
title: _('newVmCreateVms'),
body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length }),
})
try {
return await Promise.all(
}).then(() =>
Promise.all(
map(
nameLabels,
(
@@ -1696,11 +1682,7 @@ export const createVms = async (args, nameLabels, cloudConfigs) => {
})
)
)
} catch (error) {
handlePoolDoesNotSupportVtpmError(error)
throw error
}
}
)
export const getCloudInitConfig = template => _call('vm.getCloudInitConfig', { template })
@@ -1940,8 +1922,6 @@ export const importDisks = (disks, sr) =>
)
)
export const getBlockdevices = host => _call('host.getBlockdevices', { id: resolveId(host) })
import ExportVmModalBody from './export-vm-modal' // eslint-disable-line import/first
export const exportVm = async vm => {
const { compression, format } = await confirm({
@@ -2169,29 +2149,6 @@ export const deleteAclRule = ({ protocol = undefined, port = undefined, ipRange
vifId: resolveId(vif),
})
// VTPM -----------------------------------------------------------
const handlePoolDoesNotSupportVtpmError = err => {
if (
incorrectState.is(err, {
property: 'restrictions.restrict_vtpm',
expected: 'false',
})
) {
console.error(err)
throw new Error('This pool does not support VTPM')
}
}
export const createVtpm = async vm => {
try {
return await _call('vtpm.create', { id: resolveId(vm) })
} catch (err) {
handlePoolDoesNotSupportVtpmError(err)
throw err
}
}
export const deleteVtpm = vtpm => _call('vtpm.destroy', { id: resolveId(vtpm) })
// Network -----------------------------------------------------------
export const editNetwork = (network, props) => _call('network.set', { ...props, id: resolveId(network) })
@@ -2268,31 +2225,15 @@ export const deleteSr = sr =>
export const fetchSrStats = (sr, granularity) => _call('sr.stats', { id: resolveId(sr), granularity })
export const forgetSr = sr => {
export const forgetSr = sr =>
confirm({
title: _('srForgetModalTitle'),
body: (
<div className='text-warning'>
<p className='font-weight-bold'>{_('srForgetModalMessage')}</p>
</div>
),
strongConfirm: {
messageId: 'srForget',
},
}).then(() => _call('sr.forget', { id: resolveId(sr) }), noop);
};
body: _('srForgetModalMessage'),
}).then(() => _call('sr.forget', { id: resolveId(sr) }), noop)
export const forgetSrs = srs =>
confirm({
title: _('srsForgetModalTitle'),
body: (
<div className='text-warning'>
<p className='font-weight-bold'>{_('srsForgetModalMessage')}</p>
</div>
),
strongConfirm: {
messageId: 'srsForget',
},
body: _('srsForgetModalMessage'),
}).then(() => Promise.all(map(resolveIds(srs), id => _call('sr.forget', { id }))), noop)
export const reconnectAllHostsSr = sr =>
@@ -3503,10 +3444,6 @@ export const updateXosanPacks = pool =>
return downloadAndInstallXosanPack(pack, pool, { version: pack.version })
})
// XOSTOR --------------------------------------------------------------------
export const createXostorSr = params => _call('xostor.create', params)
// Licenses --------------------------------------------------------------------
export const getLicenses = ({ productType } = {}) => _call('xoa.licenses.getAll', { productType })
@@ -3653,11 +3590,6 @@ export const destroyProxyAppliances = proxies =>
export const upgradeProxyAppliance = (proxy, props) =>
_call('proxy.upgradeAppliance', { id: resolveId(proxy), ...props })
export const openTunnelOnProxy = async proxy => {
const result = await _call('proxy.openSupportTunnel', { id: resolveId(proxy) }).catch(err => err.message)
await alert(_('supportTunnel'), <pre>{result}</pre>)
}
export const getProxyApplianceUpdaterState = id => _call('proxy.getApplianceUpdaterState', { id })
export const updateProxyApplianceSettings = (id, props) => _call('proxy.updateApplianceSettings', { id, ...props })
@@ -3750,20 +3682,3 @@ export const esxiListVms = (host, user, password, sslVerify) =>
_call('esxi.listVms', { host, user, password, sslVerify })
export const importVmsFromEsxi = params => _call('vm.importMultipleFromEsxi', params)
// Github API ---------------------------------------------------------------
const _callGithubApi = async (endpoint = '') => {
const url = new URL('https://api.github.com/repos/vatesfr/xen-orchestra')
url.pathname += endpoint
const resp = await fetch(url.toString())
const json = await resp.json()
if (resp.ok) {
return json
} else {
throw new Error(json.message)
}
}
export const getMasterCommit = () => _callGithubApi('/commits/master')
export const compareCommits = (base, head) => _callGithubApi(`/compare/${base}...${head}`)

View File

@@ -1019,7 +1019,7 @@
@extend .fa-file-archive-o;
}
}
&-menu-xostor {
&-menu-xosan {
@extend .fa;
@extend .fa-database;
}

View File

@@ -100,14 +100,6 @@ $select-input-height: 40px; // Bootstrap input height
color: #333;
}
.d-inline-flex {
display: inline-flex;
}
.align-self-center {
align-self: center;
}
// COLORS ======================================================================
.xo-status-running {

View File

@@ -1,14 +1,17 @@
import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import Icon from 'icon'
import Link from 'link'
import Page from '../page'
import React from 'react'
import { getUser } from 'selectors'
import { compareCommits, getMasterCommit, serverVersion } from 'xo'
import { serverVersion } from 'xo'
import { Container, Row, Col } from 'grid'
import { connectStore, getXoaPlan } from 'utils'
import pkg from '../../../package'
const COMMIT_ID = process.env.GIT_HEAD
const HEADER = (
@@ -27,51 +30,13 @@ const HEADER = (
user: getUser,
}))
export default class About extends Component {
async componentWillMount() {
componentWillMount() {
serverVersion.then(serverVersion => {
this.setState({ serverVersion })
})
if (process.env.XOA_PLAN > 4 && COMMIT_ID !== '') {
try {
const commit = await getMasterCommit()
const isOnLatest = commit.sha === COMMIT_ID
const diff = {
nAhead: 0,
nBehind: 0,
}
if (!isOnLatest) {
try {
const { ahead_by, behind_by } = await compareCommits(commit.sha, COMMIT_ID)
diff.nAhead = ahead_by
diff.nBehind = behind_by
} catch (err) {
console.error(err)
diff.nBehind = 'unknown'
}
}
this.setState({
commit: {
isOnLatest,
master: commit,
diffWithMaster: diff,
fetched: true,
},
})
} catch (err) {
console.error(err)
this.setState({
commit: {
fetched: false,
},
})
}
}
}
render() {
const { user } = this.props
const { commit } = this.state
const isAdmin = user && user.permission === 'admin'
return (
@@ -79,53 +44,32 @@ export default class About extends Component {
<Container className='text-xs-center'>
{isAdmin && [
process.env.XOA_PLAN > 4 && COMMIT_ID !== '' && (
<Col>
<Row key='0'>
<Col mediumSize={6}>
<Icon icon='git' size={4} />
<h4>
Xen Orchestra, commit{' '}
<a href={'https://github.com/vatesfr/xen-orchestra/commit/' + COMMIT_ID}>
{COMMIT_ID.slice(0, 5)}
</a>
</h4>
</Col>
<Col mediumSize={6} className={commit?.fetched === false ? 'text-warning' : ''}>
<Icon icon='git' size={4} />
<h4>
{commit === undefined ? (
_('statusLoading')
) : commit.fetched ? (
<span>
Master, commit <a href={commit.master.html_url}>{commit.master.sha.slice(0, 5)}</a>
</span>
) : (
_('failedToFetchLatestMasterCommit')
)}
</h4>
</Col>
</Row>
{commit?.fetched && (
<Row className={`mt-1 ${commit.isOnLatest ? '' : 'text-warning '}`}>
<h4>
{commit.isOnLatest ? (
<span>
{_('xoUpToDate')} <Icon icon='check' color='text-success' />
</span>
) : (
<span>
{_('xoFromSourceNotUpToDate', {
nBehind: commit.diffWithMaster.nBehind,
nAhead: commit.diffWithMaster.nAhead,
})}{' '}
<Icon icon='alarm' color='text-warning' />
</span>
)}
</h4>
</Row>
)}
</Col>
<Row key='0'>
<Col>
<Icon icon='git' size={4} />
<h4>
Xen Orchestra, commit{' '}
<a href={'https://github.com/vatesfr/xen-orchestra/commit/' + COMMIT_ID}>{COMMIT_ID.slice(0, 5)}</a>
</h4>
</Col>
</Row>
),
<Row key='1'>
<Col mediumSize={6}>
<Icon icon='host' size={4} />
<Copiable tagName='h4' data={`xo-server ${this.state.serverVersion}`}>
xo-server {this.state.serverVersion || 'unknown'}
</Copiable>
<p className='text-muted'>{_('xenOrchestraServer')}</p>
</Col>
<Col mediumSize={6}>
<Icon icon='vm' size={4} />
<Copiable tagName='h4' data={`xo-web ${pkg.version}`}>
xo-web {pkg.version}
</Copiable>
<p className='text-muted'>{_('xenOrchestraWeb')}</p>
</Col>
</Row>,
]}
{process.env.XOA_PLAN > 4 ? (
<div>

View File

@@ -11,7 +11,7 @@ import { connectStore } from 'utils'
import { Col, Row } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { injectState, provideState } from 'reaclette'
import { forEach, isEmpty, map } from 'lodash'
import { forEach, isEmpty, map, size } from 'lodash'
import { Sr, Vdi } from 'render-xo-item'
import { subscribeSrsUnhealthyVdiChainsLength, VDIS_TO_COALESCE_LIMIT } from 'xo'
@@ -20,8 +20,8 @@ const COLUMNS = [
itemRenderer: (srId, { vdisHealthBySr }) => (
<div>
<Sr id={srId} link />{' '}
{vdisHealthBySr[srId].nUnhealthyVdis >= VDIS_TO_COALESCE_LIMIT && (
<Tooltip content={_('srVdisToCoalesceWarning', { nVdis: vdisHealthBySr[srId].nUnhealthyVdis })}>
{size(vdisHealthBySr[srId].unhealthyVdis) >= VDIS_TO_COALESCE_LIMIT && (
<Tooltip content={_('srVdisToCoalesceWarning', { limitVdis: VDIS_TO_COALESCE_LIMIT })}>
<span className='text-warning'>
<Icon icon='alarm' />
</span>

View File

@@ -46,7 +46,7 @@ import User from './user'
import Vm from './vm'
import Xoa from './xoa'
import XoaUpdates from './xoa/update'
import Xostor from './xostor'
import Xosan from './xosan'
import Import from './import'
import keymap, { help } from '../keymap'
@@ -128,7 +128,7 @@ export const ICON_POOL_LICENSE = {
'vms/new': NewVm,
'vms/:id': Vm,
xoa: Xoa,
xostor: Xostor,
xosan: Xosan,
import: Import,
hub: Hub,
proxies: Proxies,

View File

@@ -478,11 +478,7 @@ export default class Menu extends Component {
label: 'taskMenu',
pill: nResolvedTasks,
},
isAdmin && {
to: '/xostor',
label: 'xostor',
icon: 'menu-xostor',
},
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
!noOperatablePools && {
to: '/import/vm',
icon: 'menu-new-import',

View File

@@ -346,7 +346,6 @@ export default class NewVm extends BaseComponent {
seqStart: 1,
share: this._getResourceSet()?.shareByDefault ?? false,
tags: [],
createVtpm: this._templateNeedsVtpm(),
},
callback
)
@@ -494,7 +493,6 @@ export default class NewVm extends BaseComponent {
bootAfterCreate: state.bootAfterCreate,
copyHostBiosStrings:
state.hvmBootFirmware !== 'uefi' && !this._templateHasBiosStrings() && state.copyHostBiosStrings,
createVtpm: state.createVtpm,
destroyCloudConfigVdiAfterBoot: state.destroyCloudConfigVdiAfterBoot,
secureBoot: state.secureBoot,
share: state.share,
@@ -601,7 +599,6 @@ export default class NewVm extends BaseComponent {
}),
// settings
secureBoot: template.secureBoot,
createVtpm: this._templateNeedsVtpm(),
})
if (this._isCoreOs()) {
@@ -751,8 +748,6 @@ export default class NewVm extends BaseComponent {
template => template && template.virtualizationMode === 'hvm'
)
_templateNeedsVtpm = () => this.props.template?.platform?.vtpm === 'true'
// On change -------------------------------------------------------------------
_onChangeSshKeys = keys => this._setState({ sshKeys: map(keys, key => key.id) })
@@ -886,12 +881,7 @@ export default class NewVm extends BaseComponent {
_getRedirectionUrl = id => (this.state.state.multipleVms ? '/home' : `/vms/${id}`)
_handleBootFirmware = value =>
this._setState({
hvmBootFirmware: value,
secureBoot: false,
createVtpm: value === 'uefi' ? this._templateNeedsVtpm() : false,
})
_handleBootFirmware = value => this._setState({ hvmBootFirmware: value, secureBoot: false })
// MAIN ------------------------------------------------------------------------
@@ -1541,7 +1531,6 @@ export default class NewVm extends BaseComponent {
cpuCap,
cpusMax,
cpuWeight,
createVtpm,
destroyCloudConfigVdiAfterBoot,
hvmBootFirmware,
installMethod,
@@ -1576,8 +1565,6 @@ export default class NewVm extends BaseComponent {
</label>
) : null
const isVtpmSupported = pool?.vtpmSupported ?? true
return (
<Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>
<SectionContent column>
@@ -1782,23 +1769,6 @@ export default class NewVm extends BaseComponent {
<Item label={_('secureBoot')}>
<Toggle onChange={this._toggleState('secureBoot')} value={secureBoot} />
</Item>
<Item label={_('enableVtpm')} className='d-inline-flex'>
<Tooltip content={!isVtpmSupported ? _('vtpmNotSupported') : undefined}>
<Toggle onChange={this._toggleState('createVtpm')} value={createVtpm} disabled={!isVtpmSupported} />
</Tooltip>
{/* FIXME: link to VTPM documentation when ready */}
{/* &nbsp;
<Tooltip content={_('seeVtpmDocumentation')}>
<a className='text-info align-self-center' style={{ cursor: 'pointer' }} href='#'>
<Icon icon='info' />
</a>
</Tooltip> */}
{!createVtpm && this._templateNeedsVtpm() && (
<span className='align-self-center text-warning ml-1'>
<Icon icon='alarm' /> {_('warningVtpmRequired')}
</span>
)}
</Item>
</SectionContent>
),
isAdmin && isHvm && (

View File

@@ -156,7 +156,7 @@ const NewNetwork = decorate([
pifPredicate:
(_, { pool }) =>
pif =>
!pif.isBondSlave && !pif.isBondMaster && pif.vlan === -1 && pif.$host === (pool && pool.master),
!pif.isBondSlave && pif.vlan === -1 && pif.$host === (pool && pool.master),
pifPredicateSdnController:
(_, { pool }) =>
pif =>

View File

@@ -23,7 +23,6 @@ import {
forgetProxyAppliances,
getLicenses,
getProxyApplianceUpdaterState,
openTunnelOnProxy,
registerProxy,
subscribeProxies,
upgradeProxyAppliance,
@@ -113,13 +112,6 @@ const INDIVIDUAL_ACTIONS = [
label: _('forceUpgrade'),
level: 'primary',
},
{
collapsed: true,
handler: proxy => openTunnelOnProxy(proxy),
icon: 'open-tunnel',
label: _('openTunnel'),
level: 'primary',
},
{
handler: ({ id }, { router }) =>
router.push({

View File

@@ -13,7 +13,6 @@ import intersection from 'lodash/intersection'
import isEmpty from 'lodash/isEmpty'
import keyBy from 'lodash/keyBy'
import keys from 'lodash/keys'
import Link from 'link'
import map from 'lodash/map'
import mapKeys from 'lodash/mapKeys'
import PropTypes from 'prop-types'
@@ -21,7 +20,6 @@ import React from 'react'
import remove from 'lodash/remove'
import renderXoItem from 'render-xo-item'
import ResourceSetQuotas from 'resource-set-quotas'
import size from 'lodash/size'
import some from 'lodash/some'
import Tags from 'tags'
import Upgrade from 'xoa-upgrade'
@@ -572,13 +570,10 @@ export class Edit extends Component {
@addSubscriptions({
ipPools: subscribeIpPools,
})
@connectStore({
vms: createGetObjectsOfType('VM').filter((state, props) => vm => vm.resourceSet === props.resourceSet.id),
})
@injectIntl
class ResourceSet extends Component {
_renderDisplay = () => {
const { resourceSet, vms } = this.props
const { resourceSet } = this.props
const resolvedIpPools = mapKeys(this.props.ipPools, 'id')
const { limits, ipPools, subjects, objectsByType, tags } = resourceSet
@@ -620,9 +615,6 @@ class ResourceSet extends Component {
</li>,
<li key='graphs' className='list-group-item'>
<ResourceSetQuotas limits={limits} />
<Link to={`/home?s=resourceSet:${resourceSet.id}&t=VM`}>
<Icon icon='preview' /> {_('nVmsInResourceSet', { nVms: size(vms) })}
</Link>
</li>,
<li key='actions' className='list-group-item text-xs-center'>
<div className='btn-toolbar'>

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 } from 'lodash'
import { flowRight, isEmpty, keys, sum, values } from 'lodash'
// ===================================================================
@@ -44,11 +44,11 @@ const UnhealthyVdiChains = flowRight(
connectStore(() => ({
vdis: createGetObjectsOfType('VDI').pick(createSelector((_, props) => props.chains?.unhealthyVdis, keys)),
}))
)(({ chains: { nUnhealthyVdis, unhealthyVdis } = {}, vdis }) =>
)(({ chains: { unhealthyVdis } = {}, vdis }) =>
isEmpty(vdis) ? null : (
<div>
<hr />
<h3>{_('srUnhealthyVdiTitle', { total: nUnhealthyVdis })}</h3>
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(unhealthyVdis)) })}</h3>
<SortedTable collection={vdis} columns={COLUMNS} stateUrlParam='s_unhealthy_vdis' userData={unhealthyVdis} />
</div>
)

View File

@@ -2,7 +2,6 @@ import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import decorate from 'apply-decorators'
import Copiable from 'copiable'
import defined, { get } from '@xen-orchestra/defined'
import getEventValue from 'get-event-value'
import Icon from 'icon'
@@ -29,10 +28,8 @@ import {
cloneVm,
convertVmToTemplate,
createVgpu,
createVtpm,
deleteVgpu,
deleteVm,
deleteVtpm,
editVm,
getVmsHaValues,
isVmRunning,
@@ -453,48 +450,9 @@ export default class TabAdvanced extends Component {
_onNicTypeChange = value => editVm(this.props.vm, { nicType: value === '' ? null : value })
_getDisabledAddVtpmReason = createSelector(
() => this.props.vm,
() => this.props.pool,
(vm, pool) => {
if (pool?.vtpmSupported === false) {
return _('vtpmNotSupported')
}
if (vm.boot.firmware !== 'uefi') {
return _('vtpmRequireUefi')
}
if (vm.power_state !== 'Halted') {
return _('vmNeedToBeHalted')
}
}
)
_getDisabledDeleteVtpmReason = () => {
if (this.props.vm.power_state !== 'Halted') {
return _('vmNeedToBeHalted')
}
}
_handleDeleteVtpm = async vtpm => {
await confirm({
icon: 'delete',
title: _('deleteVtpm'),
body: <p>{_('deleteVtpmWarning')}</p>,
strongConfirm: {
messageId: 'deleteVtpm',
},
})
return deleteVtpm(vtpm)
}
render() {
const { container, isAdmin, vgpus, vm, vmPool } = this.props
const isWarmMigrationAvailable = getXoaPlan().value >= PREMIUM.value
const addVtpmTooltip = this._getDisabledAddVtpmReason()
const deleteVtpmTooltip = this._getDisabledDeleteVtpmReason()
const isAddVtpmAvailable = addVtpmTooltip === undefined
const isDeleteVtpmAvailable = deleteVtpmTooltip === undefined
const vtpmId = vm.VTPMs[0]
return (
<Container>
<Row>
@@ -840,59 +798,6 @@ export default class TabAdvanced extends Component {
</td>
</tr>
)}
<tr>
<th>{_('vtpm')}</th>
<td>
{/*
FIXME: add documentation link
<a
className='text-muted'
href='#'
rel='noopener noreferrer'
style={{ display: 'block' }}
target='_blank'
>
<Icon icon='info' /> {_('seeVtpmDocumentation')}
</a> */}
{vtpmId === undefined ? (
<Tooltip content={addVtpmTooltip}>
<ActionButton
btnStyle='primary'
disabled={!isAddVtpmAvailable}
handler={createVtpm}
handlerParam={vm}
icon='add'
>
{_('createVtpm')}
</ActionButton>
</Tooltip>
) : (
<div>
<Tooltip content={deleteVtpmTooltip}>
<ActionButton
btnStyle='danger'
disabled={!isDeleteVtpmAvailable}
handler={this._handleDeleteVtpm}
handlerParam={vtpmId}
icon='delete'
>
{_('deleteVtpm')}
</ActionButton>
</Tooltip>
<table className='table mt-1'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td' data={vtpmId}>
{vtpmId.slice(0, 4)}
</Copiable>
</tr>
</tbody>
</table>
</div>
)}
</td>
</tr>
{vm.boot.firmware === 'uefi' && (
<tr>
<th>{_('secureBoot')}</th>

View File

@@ -18,7 +18,7 @@ import { get } from '@xen-orchestra/defined'
import { getLicenses, selfBindLicense, subscribePlugins, subscribeProxies, subscribeSelfLicenses } from 'xo'
import Proxies from './proxies'
import Xostor from './xostor'
import Xosan from './xosan'
// -----------------------------------------------------------------------------
@@ -128,22 +128,6 @@ const LicenseManager = ({ item, userData }) => {
}
}
if (type === 'xostor') {
const { srId } = item
if (srId === undefined) {
return _('licenseNotBoundXostor')
}
const sr = userData.xostorSrs[srId]
return (
<span>
{sr === undefined ? _('licenseBoundUnknownXostor') : <Link to={`srs/${sr.id}`}>{renderXoItem(sr)}</Link>}{' '}
<CopyToClipboardButton value={srId} />
</span>
)
}
console.warn('encountered unsupported license type')
return null
}
@@ -190,15 +174,11 @@ const PRODUCTS_COLUMNS = [
// -----------------------------------------------------------------------------
@adminOnly
@connectStore(() => {
const getSrs = createGetObjectsOfType('SR')
return {
xosanSrs: getSrs.filter([
({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
]),
xoaRegistration: state => state.xoaRegisterState,
xostorSrs: getSrs.filter([({ SR_type }) => SR_type === 'linstor']),
}
@connectStore({
xosanSrs: createGetObjectsOfType('SR').filter([
({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
]),
xoaRegistration: state => state.xoaRegisterState,
})
@addSubscriptions(() => ({
plugins: subscribePlugins,
@@ -216,7 +196,7 @@ export default class Licenses extends Component {
return getLicenses()
.then(licenses => {
const { proxy, xcpng, xoa, xosan, xostor } = groupBy(licenses, license => {
const { proxy, xcpng, xoa, xosan } = groupBy(licenses, license => {
for (const productType of license.productTypes) {
if (productType === 'xo') {
return 'xoa'
@@ -230,9 +210,6 @@ export default class Licenses extends Component {
if (productType === 'xcpng') {
return 'xcpng'
}
if (productType === 'xostor') {
return 'xostor'
}
}
return 'other'
})
@@ -242,7 +219,6 @@ export default class Licenses extends Component {
xcpng,
xoa,
xosan,
xostor,
},
})
})
@@ -324,21 +300,6 @@ export default class Licenses extends Component {
}
})
// --- XOSTOR ---
forEach(licenses.xostor, license => {
// When `expires` is undefined, the license isn't expired
if (!(license.expires < now)) {
products.push({
buyer: license.buyer,
expires: license.expires,
id: license.id,
product: 'XOSTOR',
type: 'xostor',
srId: license.boundObjectId,
})
}
})
return products
}
)
@@ -383,7 +344,7 @@ export default class Licenses extends Component {
return <em>{_('statusLoading')}</em>
}
const { xoaRegistration, selfLicenses, xosanSrs, xostorSrs } = this.props
const { xoaRegistration, selfLicenses, xosanSrs } = this.props
return (
<Container>
@@ -410,15 +371,24 @@ export default class Licenses extends Component {
data-registeredEmail={xoaRegistration.email}
data-selfLicenses={selfLicenses}
data-xosanSrs={xosanSrs}
data-xostorSrs={xostorSrs}
stateUrlParam='s'
/>
</Col>
</Row>
<Row>
<Col>
<h2>{_('xostor')}</h2>
<Xostor xostorLicenses={this.state.licenses.xostor} updateLicenses={this._updateLicenses} />
<h2>
XOSAN
<a
className='btn btn-secondary ml-1'
href='https://xen-orchestra.com/#!/xosan-home'
target='_blank'
rel='noopener noreferrer'
>
<Icon icon='bug' /> {_('productSupport')}
</a>
</h2>
<Xosan xosanLicenses={this.state.licenses.xosan} updateLicenses={this._updateLicenses} />
</Col>
</Row>
<Row>

View File

@@ -0,0 +1,134 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Link from 'link'
import React from 'react'
import renderXoItem, { Pool } from 'render-xo-item'
import SelectLicense from 'select-license'
import SortedTable from 'sorted-table'
import { connectStore } from 'utils'
import { createSelector, createGetObjectsOfType } from 'selectors'
import { filter, forEach, includes, map } from 'lodash'
import { unlockXosan } from 'xo'
class XosanLicensesForm extends Component {
state = {
licenseId: 'none',
}
onChangeLicense = event => {
this.setState({ licenseId: event.target.value })
}
unlockXosan = () => {
const { item, userData } = this.props
return unlockXosan(this.state.licenseId, item.id).then(userData.updateLicenses)
}
render() {
const { item, userData } = this.props
const { licenseId } = this.state
const license = userData.licensesByXosan[item.id]
if (license === null) {
return (
<span className='text-danger'>
{_('xosanMultipleLicenses')} <a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
</span>
)
}
return license?.productId === 'xosan' ? (
<span>{license.id.slice(-4)}</span>
) : (
<form className='form-inline'>
<SelectLicense onChange={this.onChangeLicense} productType='xosan' />
<ActionButton
btnStyle='primary'
className='ml-1'
disabled={licenseId === 'none'}
handler={this.unlockXosan}
handlerParam={licenseId}
icon='connect'
>
{_('bindLicense')}
</ActionButton>
</form>
)
}
}
const XOSAN_COLUMNS = [
{
name: _('xosanName'),
itemRenderer: sr => <Link to={`srs/${sr.id}`}>{renderXoItem(sr)}</Link>,
sortCriteria: 'name_label',
},
{
name: _('xosanPool'),
itemRenderer: sr => <Pool id={sr.$pool} link />,
},
{
name: _('license'),
component: XosanLicensesForm,
},
]
const XOSAN_INDIVIDUAL_ACTIONS = [
{
label: _('productSupport'),
icon: 'support',
handler: () => window.open('https://xen-orchestra.com'),
},
]
@connectStore(() => ({
xosanSrs: createGetObjectsOfType('SR').filter([
({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
]),
}))
export default class Xosan extends Component {
_getLicensesByXosan = createSelector(
() => this.props.xosanLicenses,
licenses => {
const licensesByXosan = {}
forEach(licenses, license => {
let xosanId
if ((xosanId = license.boundObjectId) === undefined) {
return
}
licensesByXosan[xosanId] =
licensesByXosan[xosanId] !== undefined
? null // XOSAN bound to multiple licenses!
: license
})
return licensesByXosan
}
)
_getKnownXosans = createSelector(
createSelector(
() => this.props.xosanLicenses,
(licenses = []) => filter(map(licenses, 'boundObjectId'))
),
() => this.props.xosanSrs,
(knownXosanIds, xosanSrs) => filter(xosanSrs, ({ id }) => includes(knownXosanIds, id))
)
render() {
return (
<SortedTable
collection={this._getKnownXosans()}
columns={XOSAN_COLUMNS}
individualActions={XOSAN_INDIVIDUAL_ACTIONS}
stateUrlParam='s_xosan'
userData={{
licensesByXosan: this._getLicensesByXosan(),
xosanSrs: this.props.xosanSrs,
updateLicenses: this.props.updateLicenses,
}}
/>
)
}
}

View File

@@ -1,137 +0,0 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import SelectLicense from 'select-license'
import SortedTable from 'sorted-table'
import { bindLicense } from 'xo'
import { connectStore } from 'utils'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { groupBy } from 'lodash'
import { injectState, provideState } from 'reaclette'
import { Pool, Sr } from 'render-xo-item'
import BulkIcons from '../../../common/bulk-icons'
class XostorLicensesForm extends Component {
state = {
licenseId: 'none',
}
bind = () => {
const { item, userData } = this.props
return bindLicense(this.state.licenseId, item.uuid).then(userData.updateLicenses)
}
getAlerts = createSelector(
() => this.props.item,
() => this.props.userData,
(sr, userData) => {
const alerts = []
const licenses = userData.licensesByXostorUuid[sr.id]
// Xostor bound to multiple licenses
if (licenses?.length > 1) {
alerts.push({
level: 'danger',
render: (
<p>
{_('xostorMultipleLicenses')}
<br />
{licenses.map(license => license.id.slice(-4)).join(',')}
</p>
),
})
}
const license = licenses?.[0]
if (license?.expires < Date.now()) {
alerts.push({
level: 'danger',
render: _('licenseExpiredXostorWarning', { licenseId: license?.id.slice(-4) }),
})
}
return alerts
}
)
render() {
const alerts = this.getAlerts()
if (alerts.length > 0) {
return <BulkIcons alerts={alerts} />
}
const { item, userData } = this.props
const { licenseId } = this.state
const licenses = userData.licensesByXostorUuid[item.id]
const license = licenses?.[0]
return license !== undefined ? (
<span>{license?.id.slice(-4)}</span>
) : (
<div>
{license !== undefined && (
<div className='text-danger mb-1'>
<Icon icon='alarm' /> {_('licenseHasExpired')}
</div>
)}
<form className='form-inline'>
<SelectLicense onChange={this.linkState('licenseId')} productType='xostor' />
<ActionButton
btnStyle='primary'
className='ml-1'
disabled={licenseId === 'none'}
handler={this.bind}
handlerParam={licenseId}
icon='connect'
>
{_('bindLicense')}
</ActionButton>
</form>
</div>
)
}
}
const INDIVIDUAL_ACTIONS = [
{
label: _('productSupport'),
icon: 'support',
handler: () => window.open('https://xen-orchestra.com'),
},
]
const COLUMNS = [
{
default: true,
name: _('name'),
itemRenderer: sr => <Sr id={sr.id} link container={false} />,
sortCriteria: 'name_label',
},
{ name: _('pool'), itemRenderer: sr => <Pool id={sr.$pool} link /> },
{ name: _('license'), component: XostorLicensesForm },
]
const Xostor = decorate([
connectStore(() => ({
xostorSrs: createGetObjectsOfType('SR').filter([({ SR_type }) => SR_type === 'linstor']),
})),
provideState({
computed: {
licensesByXostorUuid: (state, { xostorLicenses }) => groupBy(xostorLicenses, 'boundObjectId'),
},
}),
injectState,
({ state, xostorSrs, updateLicenses }) => (
<SortedTable
collection={xostorSrs}
columns={COLUMNS}
data-updateLicenses={updateLicenses}
data-licensesByXostorUuid={state.licensesByXostorUuid}
individualActions={INDIVIDUAL_ACTIONS}
/>
),
])
export default Xostor

View File

@@ -43,13 +43,7 @@ const Support = decorate([
<ActionButton btnStyle='primary' disabled={COMMUNITY} handler={reportOnSupportPanel} icon='ticket'>
{_('createSupportTicket')}
</ActionButton>
<ActionButton
btnStyle='danger'
disabled={COMMUNITY}
handler={restartXoServer}
icon='restart'
className='ml-1'
>
<ActionButton btnStyle='danger' disabled={COMMUNITY} handler={restartXoServer} icon='restart' className='ml-1'>
{_('restartXoServer')}
</ActionButton>
</Col>

Some files were not shown because too many files have changed in this diff Show More