Compare commits
20 Commits
xo-lite-v0
...
ManonMerci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50afcdab3b | ||
|
|
59a9a63971 | ||
|
|
a2e8b999da | ||
|
|
489ad51b4d | ||
|
|
7db2516a38 | ||
|
|
1141ef524f | ||
|
|
f449258ed3 | ||
|
|
bb3b83c690 | ||
|
|
2b973275c0 | ||
|
|
037e1c1dfa | ||
|
|
f0da94081b | ||
|
|
cd44a6e28c | ||
|
|
70b09839c7 | ||
|
|
12140143d2 | ||
|
|
e68236c9f2 | ||
|
|
8a1a0d76f7 | ||
|
|
4a5bc5dccc | ||
|
|
0ccdfbd6f4 | ||
|
|
75af7668b5 | ||
|
|
0b454fa670 |
@@ -68,6 +68,11 @@ module.exports = {
|
||||
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
|
||||
// this rule can prevent race condition bugs like parallel `a += await foo()`
|
||||
//
|
||||
// as it has a lots of false positive, it is only enabled as a warning for now
|
||||
'require-atomic-updates': 'warn',
|
||||
|
||||
strict: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## **next**
|
||||
|
||||
- [VM/Action] Ability to migrate a VM from its view (PR [#7164](https://github.com/vatesfr/xen-orchestra/pull/7164))
|
||||
- Ability to override host address with `master` URL query param (PR [#7187](https://github.com/vatesfr/xen-orchestra/pull/7187))
|
||||
- Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223))
|
||||
- Add indeterminate state on FormToggle component (PR [#7230](https://github.com/vatesfr/xen-orchestra/pull/7230))
|
||||
- Add new UiStatusPanel component (PR [#7227](https://github.com/vatesfr/xen-orchestra/pull/7227))
|
||||
|
||||
## **0.1.6** (2023-11-30)
|
||||
|
||||
- Explicit error if users attempt to connect from a slave host (PR [#7110](https://github.com/vatesfr/xen-orchestra/pull/7110))
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</RouterLink>
|
||||
<slot />
|
||||
<div class="right">
|
||||
<PoolOverrideWarning as-tooltip />
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import TextLogo from "@/components/TextLogo.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
@@ -51,6 +53,10 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.warning-not-current-pool {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="app-login form-container">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<img alt="XO Lite" src="../assets/logo-title.svg" />
|
||||
<PoolOverrideWarning />
|
||||
<p v-if="isHostIsSlaveErr(error)" class="error">
|
||||
<UiIcon :icon="faExclamationCircle" />
|
||||
{{ $t("login-only-on-master") }}
|
||||
@@ -45,6 +46,7 @@ import FormCheckbox from "@/components/form/FormCheckbox.vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
|
||||
import LoginError from "@/components/LoginError.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { XenApiError } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
@@ -1,49 +1,28 @@
|
||||
<template>
|
||||
<div class="page-under-construction">
|
||||
<img alt="Under construction" src="@/assets/under-construction.svg" />
|
||||
<p class="title">{{ $t("xo-lite-under-construction") }}</p>
|
||||
<p class="subtitle">{{ $t("new-features-are-coming") }}</p>
|
||||
<UiStatusPanel
|
||||
:image-source="underConstruction"
|
||||
:subtitle="$t('new-features-are-coming')"
|
||||
:title="$t('xo-lite-under-construction')"
|
||||
>
|
||||
<p class="contact">
|
||||
{{ $t("do-you-have-needs") }}
|
||||
<a
|
||||
href="https://xcp-ng.org/forum/topic/5018/xo-lite-building-an-embedded-ui-in-xcp-ng"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("here") }} →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</UiStatusPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import underConstruction from "@/assets/under-construction.svg";
|
||||
import UiStatusPanel from "@/components/ui/UiStatusPanel.vue";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.page-under-construction {
|
||||
width: 100%;
|
||||
min-height: 76.5vh;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 40px;
|
||||
width: 30%;
|
||||
}
|
||||
.title {
|
||||
font-weight: 400;
|
||||
font-size: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 21px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.contact {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
|
||||
59
@xen-orchestra/lite/src/components/PoolOverrideWarning.vue
Normal file
59
@xen-orchestra/lite/src/components/PoolOverrideWarning.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="xenApi.isPoolOverridden"
|
||||
class="warning-not-current-pool"
|
||||
@click="xenApi.resetPoolMasterIp"
|
||||
v-tooltip="
|
||||
asTooltip && {
|
||||
placement: 'right',
|
||||
content: `
|
||||
${$t('you-are-currently-on', [masterSessionStorage])}.
|
||||
${$t('click-to-return-default-pool')}
|
||||
`,
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="wrapper">
|
||||
<UiIcon :icon="faWarning" />
|
||||
<p v-if="!asTooltip">
|
||||
<i18n-t keypath="you-are-currently-on">
|
||||
<strong>{{ masterSessionStorage }}</strong>
|
||||
</i18n-t>
|
||||
<br />
|
||||
{{ $t("click-to-return-default-pool") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useSessionStorage } from "@vueuse/core";
|
||||
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
|
||||
defineProps<{
|
||||
asTooltip?: boolean;
|
||||
}>();
|
||||
|
||||
const xenApi = useXenApiStore();
|
||||
const masterSessionStorage = useSessionStorage("master", null);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.warning-not-current-pool {
|
||||
color: var(--color-orange-world-base);
|
||||
cursor: pointer;
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
margin: auto 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<input
|
||||
v-model="value"
|
||||
:class="{ indeterminate: type === 'checkbox' && value === undefined }"
|
||||
:class="{ indeterminate: isIndeterminate }"
|
||||
:disabled="isDisabled"
|
||||
:type="type === 'radio' ? 'radio' : 'checkbox'"
|
||||
class="input"
|
||||
@@ -60,6 +60,10 @@ const icon = computed(() => {
|
||||
|
||||
return faCheck;
|
||||
});
|
||||
|
||||
const isIndeterminate = computed(
|
||||
() => (type === "checkbox" || type === "toggle") && value.value === undefined
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -127,6 +131,12 @@ const icon = computed(() => {
|
||||
.input:checked + .fake-checkbox > .icon {
|
||||
transform: translateX(0.7em);
|
||||
}
|
||||
|
||||
.input.indeterminate + .fake-checkbox > .icon {
|
||||
opacity: 1;
|
||||
color: var(--color-blue-scale-300);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
<UiCardTitle>
|
||||
{{ $t("cpu-provisioning") }}
|
||||
<template v-if="!hasError" #right>
|
||||
<!-- TODO: add a tooltip for the warning icon -->
|
||||
<UiStatusIcon v-if="state !== 'success'" :state="state" />
|
||||
<UiStatusIcon
|
||||
v-if="state !== 'success'"
|
||||
v-tooltip="{
|
||||
content: $t('cpu-provisioning-warning'),
|
||||
placement: 'left',
|
||||
}"
|
||||
:state="state"
|
||||
/>
|
||||
</template>
|
||||
</UiCardTitle>
|
||||
<NoDataError v-if="hasError" />
|
||||
@@ -37,11 +43,12 @@ import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
|
||||
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
|
||||
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
|
||||
import { useHostCollection } from "@/stores/xen-api/host.store";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import { percent } from "@/libs/utils";
|
||||
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
|
||||
import { useHostCollection } from "@/stores/xen-api/host.store";
|
||||
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { logicAnd } from "@vueuse/math";
|
||||
import { computed } from "vue";
|
||||
|
||||
|
||||
47
@xen-orchestra/lite/src/components/ui/UiStatusPanel.vue
Normal file
47
@xen-orchestra/lite/src/components/ui/UiStatusPanel.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="ui-status-panel">
|
||||
<img :src="imageSource" alt="" class="image" />
|
||||
<p v-if="title !== undefined" class="title">{{ title }}</p>
|
||||
<p v-if="subtitle !== undefined" class="subtitle">{{ subtitle }}</p>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
imageSource: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-status-panel {
|
||||
width: 100%;
|
||||
min-height: 76.5vh;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 400;
|
||||
font-size: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 21px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
margin-bottom: 40px;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,11 @@
|
||||
v-tooltip="
|
||||
selectedRefs.length > 0 &&
|
||||
!isMigratable &&
|
||||
$t('no-selected-vm-can-be-migrated')
|
||||
$t(
|
||||
isSingleAction
|
||||
? 'this-vm-cant-be-migrated'
|
||||
: 'no-selected-vm-can-be-migrated'
|
||||
)
|
||||
"
|
||||
:busy="isMigrating"
|
||||
:disabled="isParentDisabled || !isMigratable"
|
||||
@@ -28,6 +32,7 @@ import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
selectedRefs: XenApiVm["$ref"][];
|
||||
isSingleAction?: boolean;
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRefs, isOperationPending, areSomeOperationAllowed } =
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<VmActionCopyItem :selected-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionExportItem :vm-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionSnapshotItem :vm-refs="[vm.$ref]" />
|
||||
<VmActionMigrateItem :selected-refs="[vm.$ref]" is-single-action />
|
||||
</AppMenu>
|
||||
</template>
|
||||
</TitleBar>
|
||||
@@ -38,6 +39,7 @@ import AppMenu from "@/components/menu/AppMenu.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
|
||||
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
|
||||
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
|
||||
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"cancel": "Cancel",
|
||||
"change-state": "Change state",
|
||||
"click-to-display-alarms": "Click to display alarms:",
|
||||
"click-to-return-default-pool": "Click here to return to the default pool",
|
||||
"close": "Close",
|
||||
"coming-soon": "Coming soon!",
|
||||
"community": "Community",
|
||||
@@ -37,6 +38,7 @@
|
||||
"console-unavailable": "Console unavailable",
|
||||
"copy": "Copy",
|
||||
"cpu-provisioning": "CPU provisioning",
|
||||
"cpu-provisioning-warning": "The number of vCPUs allocated exceeds the number of physical CPUs available. System performance could be affected",
|
||||
"cpu-usage": "CPU usage",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
@@ -177,6 +179,7 @@
|
||||
"theme-auto": "Auto",
|
||||
"theme-dark": "Dark",
|
||||
"theme-light": "Light",
|
||||
"this-vm-cant-be-migrated": "This VM can't be migrated",
|
||||
"top-#": "Top {n}",
|
||||
"total-cpus": "Total CPUs",
|
||||
"total-free": "Total free",
|
||||
@@ -189,5 +192,6 @@
|
||||
"vm-is-running": "The VM is running",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite is under construction",
|
||||
"you-are-currently-on": "You are currently on: {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"cancel": "Annuler",
|
||||
"change-state": "Changer l'état",
|
||||
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
|
||||
"click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut",
|
||||
"close": "Fermer",
|
||||
"coming-soon": "Bientôt disponible !",
|
||||
"community": "Communauté",
|
||||
@@ -37,6 +38,7 @@
|
||||
"console-unavailable": "Console indisponible",
|
||||
"copy": "Copier",
|
||||
"cpu-provisioning": "Provisionnement CPU",
|
||||
"cpu-provisioning-warning": "Le nombre de vCPU alloués dépasse le nombre de CPU physique disponible. Les performances du système pourraient être affectées",
|
||||
"cpu-usage": "Utilisation CPU",
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
@@ -177,6 +179,7 @@
|
||||
"theme-auto": "Auto",
|
||||
"theme-dark": "Sombre",
|
||||
"theme-light": "Clair",
|
||||
"this-vm-cant-be-migrated": "Cette VM ne peut pas être migrée",
|
||||
"top-#": "Top {n}",
|
||||
"total-cpus": "Total CPUs",
|
||||
"total-free": "Total libre",
|
||||
@@ -189,5 +192,6 @@
|
||||
"vm-is-running": "La VM est en cours d'exécution",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite est en construction",
|
||||
"you-are-currently-on": "Vous êtes actuellement sur : {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import XapiStats from "@/libs/xapi-stats";
|
||||
import XenApi from "@/libs/xen-api/xen-api";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { useLocalStorage, useSessionStorage, whenever } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const HOST_URL = import.meta.env.PROD
|
||||
? window.origin
|
||||
@@ -15,7 +17,27 @@ enum STATUS {
|
||||
}
|
||||
|
||||
export const useXenApiStore = defineStore("xen-api", () => {
|
||||
const xenApi = new XenApi(HOST_URL);
|
||||
// undefined not correctly handled. See https://github.com/vueuse/vueuse/issues/3595
|
||||
const masterSessionStorage = useSessionStorage<null | string>("master", null);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
whenever(
|
||||
() => route.query.master,
|
||||
async (newMaster) => {
|
||||
masterSessionStorage.value = newMaster as string;
|
||||
await router.replace({ query: { ...route.query, master: undefined } });
|
||||
window.location.reload();
|
||||
}
|
||||
);
|
||||
|
||||
const hostUrl = new URL(HOST_URL);
|
||||
if (masterSessionStorage.value !== null) {
|
||||
hostUrl.hostname = masterSessionStorage.value;
|
||||
}
|
||||
|
||||
const isPoolOverridden = hostUrl.origin !== new URL(HOST_URL).origin;
|
||||
const xenApi = new XenApi(hostUrl.origin);
|
||||
const xapiStats = new XapiStats(xenApi);
|
||||
const storedSessionId = useLocalStorage<string | undefined>(
|
||||
"sessionId",
|
||||
@@ -75,14 +97,21 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
||||
status.value = STATUS.DISCONNECTED;
|
||||
}
|
||||
|
||||
function resetPoolMasterIp() {
|
||||
masterSessionStorage.value = null;
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
isPoolOverridden,
|
||||
connect,
|
||||
reconnect,
|
||||
disconnect,
|
||||
getXapi,
|
||||
getXapiStats,
|
||||
currentSessionId,
|
||||
resetPoolMasterIp,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
<div :class="{ 'no-ui': !uiStore.hasUi }" class="vm-console-view">
|
||||
<div v-if="hasError">{{ $t("error-occurred") }}</div>
|
||||
<UiSpinner v-else-if="!isReady" class="spinner" />
|
||||
<div v-else-if="!isVmRunning" class="not-running">
|
||||
<div><img alt="" src="@/assets/monitor.svg" /></div>
|
||||
{{ $t("power-on-for-console") }}
|
||||
</div>
|
||||
<UiStatusPanel
|
||||
v-else-if="!isVmRunning"
|
||||
:image-source="monitor"
|
||||
:title="$t('power-on-for-console')"
|
||||
/>
|
||||
<template v-else-if="vm && vmConsole">
|
||||
<AppMenu horizontal>
|
||||
<MenuItem
|
||||
v-if="uiStore.hasUi"
|
||||
:icon="faArrowUpRightFromSquare"
|
||||
@click="openInNewTab"
|
||||
v-if="uiStore.hasUi"
|
||||
>
|
||||
{{ $t("open-console-in-new-tab") }}
|
||||
</MenuItem>
|
||||
@@ -44,10 +45,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import monitor from "@/assets/monitor.svg";
|
||||
import AppMenu from "@/components/menu/AppMenu.vue";
|
||||
import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import RemoteConsole from "@/components/RemoteConsole.vue";
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import UiStatusPanel from "@/components/ui/UiStatusPanel.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";
|
||||
@@ -158,7 +161,6 @@ const openInNewTab = () => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.not-running,
|
||||
.not-available {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { asyncMap } from '@xen-orchestra/async-map'
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
import { defer } from 'golike-defer'
|
||||
import { incorrectState, operationFailed } from 'xo-common/api-errors.js'
|
||||
import pRetry from 'promise-toolbox/retry'
|
||||
|
||||
import { getCurrentVmUuid } from './_XenStore.mjs'
|
||||
|
||||
@@ -69,7 +70,12 @@ class Host {
|
||||
if (await this.getField('host', ref, 'enabled')) {
|
||||
await this.callAsync('host.disable', ref)
|
||||
$defer(async () => {
|
||||
await this.callAsync('host.enable', ref)
|
||||
await pRetry(() => this.callAsync('host.enable', ref), {
|
||||
delay: 10e3,
|
||||
retries: 6,
|
||||
when: { code: 'HOST_STILL_BOOTING' },
|
||||
})
|
||||
|
||||
// Resuming VMs should occur after host enabling to avoid triggering a 'NO_HOSTS_AVAILABLE' error
|
||||
return asyncEach(suspendedVms, vmRef => this.callAsync('VM.resume', vmRef, false, false))
|
||||
})
|
||||
|
||||
@@ -7,10 +7,20 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Forget SR] Changed the modal message and added a confirmation text to be sure the action is understood by the user [#7148](https://github.com/vatesfr/xen-orchestra/issues/7148) (PR [#7155](https://github.com/vatesfr/xen-orchestra/pull/7155))
|
||||
- [REST API] `/backups` has been renamed to `/backup` (redirections are in place for compatibility)
|
||||
- [REST API] _VM backup & Replication_ jobs have been moved from `/backup/jobs/:id` to `/backup/jobs/vm/:id` (redirections are in place for compatibility)
|
||||
- [REST API] _XO config & Pool metadata Backup_ jobs are available at `/backup/jobs/metadata`
|
||||
- [REST API] _Mirror Backup_ jobs are available at `/backup/jobs/metadata`
|
||||
- [Plugin/auth-saml] Add _Force re-authentication_ setting [Forum#67764](https://xcp-ng.org/forum/post/67764) (PR [#7232](https://github.com/vatesfr/xen-orchestra/pull/7232))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [REST API] Returns a proper 404 _Not Found_ error when a job does not exist instead of _Internal Server Error_
|
||||
- [Host/Smart reboot] Automatically retries up to a minute when `HOST_STILL_BOOTING` [#7194](https://github.com/vatesfr/xen-orchestra/issues/7194) (PR [#7231](https://github.com/vatesfr/xen-orchestra/pull/7231))
|
||||
|
||||
### Packages to release
|
||||
|
||||
> When modifying a package, add it here with its release type.
|
||||
@@ -27,4 +37,8 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @xen-orchestra/xapi patch
|
||||
- xo-server minor
|
||||
- xo-server-auth-saml minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
BIN
docs/assets/backuplog.png
Normal file
BIN
docs/assets/backuplog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/assets/enablenbd.png
Normal file
BIN
docs/assets/enablenbd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/assets/nbdconnection.png
Normal file
BIN
docs/assets/nbdconnection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -106,13 +106,13 @@ 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 nfs-common ntfs-3g
|
||||
```
|
||||
|
||||
On Fedora/CentOS like:
|
||||
|
||||
```sh
|
||||
dnf install redis libpng-devel git libvhdi-tools lvm2 cifs-utils make automake gcc gcc-c++
|
||||
dnf install redis libpng-devel git lvm2 cifs-utils make automake gcc gcc-c++ nfs-utils ntfs-3g
|
||||
```
|
||||
|
||||
### Make sure Redis is running
|
||||
|
||||
@@ -46,6 +46,12 @@ You should try \`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddr
|
||||
default: DEFAULTS.disableRequestedAuthnContext,
|
||||
type: 'boolean',
|
||||
},
|
||||
forceAuthn: {
|
||||
title: 'Force re-authentication',
|
||||
description: 'Request the identity provider to authenticate the user, even if they possess a valid session.',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
required: ['cert', 'entryPoint', 'issuer', 'usernameField'],
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function copyVm({ vm, sr }) {
|
||||
const input = await srcXapi.VM_export(vm._xapiRef)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('import full VM...')
|
||||
await tgtXapi.VM_destroy((await tgtXapi.importVm(input, { srId: sr })).$ref)
|
||||
await tgtXapi.VM_destroy(await tgtXapi.VM_import(input, sr._xapiRef))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1297,7 +1297,8 @@ async function import_({ data, sr, type = 'xva', url }) {
|
||||
throw invalidParameters('URL import is only compatible with XVA')
|
||||
}
|
||||
|
||||
return (await xapi.importVm(await hrp(url), { srId, type })).$id
|
||||
const ref = await xapi.VM_import(await hrp(url), sr._xapiRef)
|
||||
return xapi.call('VM.get_by_uuid', ref)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -965,10 +965,7 @@ async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
namespace: 'xosan',
|
||||
version: template.version,
|
||||
})
|
||||
const newVM = await xapi.importVm(templateStream, {
|
||||
srId: lvmsrId,
|
||||
type: 'xva',
|
||||
})
|
||||
const newVM = await xapi.VM_import(templateStream, this.getObject(lvmsrId, 'SR')._xapiRef)
|
||||
await xapi.editVm(newVM, {
|
||||
autoPoweron: true,
|
||||
name_label: 'XOSAN imported VM',
|
||||
|
||||
@@ -389,7 +389,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
const onVmCreation = nameLabel !== undefined ? vm => vm.set_name_label(nameLabel) : null
|
||||
|
||||
const vm = await targetXapi._getOrWaitObject(await targetXapi._importVm(stream, sr, onVmCreation))
|
||||
const vm = await targetXapi._getOrWaitObject(await targetXapi.VM_import(stream, sr.$ref, onVmCreation))
|
||||
|
||||
return {
|
||||
vm,
|
||||
@@ -674,36 +674,6 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
@cancelable
|
||||
async _importVm($cancelToken, stream, sr, onVmCreation = undefined) {
|
||||
const taskRef = await this.task_create('VM import')
|
||||
const query = {}
|
||||
|
||||
if (sr != null) {
|
||||
query.sr_id = sr.$ref
|
||||
}
|
||||
|
||||
if (onVmCreation != null) {
|
||||
this.waitObject(
|
||||
obj => obj != null && obj.current_operations != null && taskRef in obj.current_operations,
|
||||
onVmCreation
|
||||
)
|
||||
}
|
||||
|
||||
const vmRef = await this.putResource($cancelToken, stream, '/import/', {
|
||||
query,
|
||||
task: taskRef,
|
||||
}).then(extractOpaqueRef, error => {
|
||||
// augment the error with as much relevant info as possible
|
||||
error.pool_master = this.pool.$master
|
||||
error.SR = sr
|
||||
|
||||
throw error
|
||||
})
|
||||
|
||||
return vmRef
|
||||
}
|
||||
|
||||
@decorateWith(deferrable)
|
||||
async _importOvaVm($defer, stream, { descriptionLabel, disks, memory, nameLabel, networks, nCpus, tables }, sr) {
|
||||
// 1. Create VM.
|
||||
@@ -812,7 +782,7 @@ export default class Xapi extends XapiBase {
|
||||
const sr = srId && this.getObject(srId)
|
||||
|
||||
if (type === 'xva') {
|
||||
return /* await */ this._getOrWaitObject(await this._importVm(stream, sr))
|
||||
return /* await */ this._getOrWaitObject(await this.VM_import(stream, sr?.$ref))
|
||||
}
|
||||
|
||||
if (type === 'ova') {
|
||||
|
||||
@@ -271,13 +271,13 @@ export default class Proxy {
|
||||
[namespace]: { xva },
|
||||
} = await app.getResourceCatalog()
|
||||
const xapi = app.getXapi(srId)
|
||||
const vm = await xapi.importVm(
|
||||
const vm = await xapi.VM_import(
|
||||
await app.requestResource({
|
||||
id: xva.id,
|
||||
namespace,
|
||||
version: xva.version,
|
||||
}),
|
||||
{ srId }
|
||||
srId && this.getObject(srId, 'SR')._xapiRef
|
||||
)
|
||||
$defer.onFailure(() => xapi.VM_destroy(vm.$ref))
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ export default class RestApi {
|
||||
})
|
||||
)
|
||||
|
||||
collections.backups = { id: 'backups' }
|
||||
collections.backup = { id: 'backup' }
|
||||
collections.restore = { id: 'restore' }
|
||||
collections.tasks = { id: 'tasks' }
|
||||
collections.users = { id: 'users' }
|
||||
@@ -280,23 +280,26 @@ export default class RestApi {
|
||||
wrap((req, res) => sendObjects(collections, req, res))
|
||||
)
|
||||
|
||||
// For compatibility redirect from /backups* to /backup
|
||||
api.get('/backups*', (req, res) => {
|
||||
res.redirect(308, req.baseUrl + '/backup' + req.params[0])
|
||||
})
|
||||
|
||||
const backupTypes = {
|
||||
__proto__: null,
|
||||
|
||||
metadata: 'metadataBackup',
|
||||
mirror: 'mirrorBackup',
|
||||
vm: 'backup',
|
||||
}
|
||||
|
||||
api
|
||||
.get(
|
||||
'/backups',
|
||||
'/backup',
|
||||
wrap((req, res) => sendObjects([{ id: 'jobs' }, { id: 'logs' }], req, res))
|
||||
)
|
||||
.get(
|
||||
'/backups/jobs',
|
||||
wrap(async (req, res) => sendObjects(await app.getAllJobs('backup'), req, res))
|
||||
)
|
||||
.get(
|
||||
'/backups/jobs/:id',
|
||||
wrap(async (req, res) => {
|
||||
res.json(await app.getJob(req.params.id, 'backup'))
|
||||
})
|
||||
)
|
||||
.get(
|
||||
'/backups/logs',
|
||||
'/backup/logs',
|
||||
wrap(async (req, res) => {
|
||||
const { filter, limit } = req.query
|
||||
const logs = await app.getBackupNgLogsSorted({
|
||||
@@ -306,6 +309,37 @@ export default class RestApi {
|
||||
await sendObjects(logs, req, res)
|
||||
})
|
||||
)
|
||||
.get(
|
||||
'/backup/jobs',
|
||||
wrap((req, res) =>
|
||||
sendObjects(
|
||||
Object.keys(backupTypes).map(id => ({ id })),
|
||||
req,
|
||||
res
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for (const [collection, type] of Object.entries(backupTypes)) {
|
||||
api
|
||||
.get(
|
||||
'/backup/jobs/' + collection,
|
||||
wrap(async (req, res) => sendObjects(await app.getAllJobs(type), req, res))
|
||||
)
|
||||
.get(
|
||||
`/backup/jobs/${collection}/:id`,
|
||||
wrap(async (req, res) => {
|
||||
res.json(await app.getJob(req.params.id, type))
|
||||
}, true)
|
||||
)
|
||||
}
|
||||
|
||||
// For compatibility, redirect /backup/jobs/:id to /backup/jobs/vm/:id
|
||||
api.get('/backup/jobs/:id', (req, res) => {
|
||||
res.redirect(308, req.baseUrl + '/backup/jobs/vm/' + req.params.id)
|
||||
})
|
||||
|
||||
api
|
||||
.get(
|
||||
'/restore',
|
||||
wrap((req, res) => sendObjects([{ id: 'logs' }], req, res))
|
||||
|
||||
@@ -53,6 +53,7 @@ export default class {
|
||||
this._db = (async () => {
|
||||
await fse.ensureDir(dir)
|
||||
await fse.access(dir, fse.constants.R_OK | fse.constants.W_OK)
|
||||
await fse.chmod(dir, 0o700)
|
||||
return levelup(dir)
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -864,6 +864,7 @@ const messages = {
|
||||
srDisconnectAll: 'Disconnect from all hosts',
|
||||
srForget: 'Forget this SR',
|
||||
srsForget: 'Forget SRs',
|
||||
nSrsForget: 'Forget {nSrs, number} SR{nSrs, plural, one {} other{s}}',
|
||||
srRemoveButton: 'Remove this SR',
|
||||
srNoVdis: 'No VDIs in this storage',
|
||||
srReclaimSpace: 'Reclaim freed space',
|
||||
@@ -2375,11 +2376,9 @@ const messages = {
|
||||
srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',
|
||||
srsDisconnectAllModalMessage:
|
||||
'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? VDIs on this storage won't be removed.",
|
||||
srsForgetModalMessage:
|
||||
"Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.",
|
||||
forgetNSrsModalMessage: 'Are you sure you want to forget {nSrs, number} SR{nSrs, plural, one {} other{s}}?',
|
||||
srForgetModalWarning:
|
||||
'You will lose all the metadata, meaning all the links between the VDIs (disks) and their respective VMs. This operation cannot be undone.',
|
||||
srAllDisconnected: 'Disconnected',
|
||||
srSomeConnected: 'Partially connected',
|
||||
srAllConnected: 'Connected',
|
||||
|
||||
@@ -2268,15 +2268,20 @@ export const deleteSr = sr =>
|
||||
|
||||
export const fetchSrStats = (sr, granularity) => _call('sr.stats', { id: resolveId(sr), granularity })
|
||||
|
||||
export const forgetSr = sr =>
|
||||
confirm({
|
||||
title: _('srForgetModalTitle'),
|
||||
body: _('srForgetModalMessage'),
|
||||
}).then(() => _call('sr.forget', { id: resolveId(sr) }), noop)
|
||||
export const forgetSr = sr => forgetSrs([sr])
|
||||
|
||||
export const forgetSrs = srs =>
|
||||
confirm({
|
||||
title: _('srsForgetModalTitle'),
|
||||
body: _('srsForgetModalMessage'),
|
||||
title: _('nSrsForget', { nSrs: srs.length }),
|
||||
body: (
|
||||
<p className='text-warning font-weight-bold'>
|
||||
{_('forgetNSrsModalMessage', { nSrs: srs.length })} {_('srForgetModalWarning')}
|
||||
</p>
|
||||
),
|
||||
strongConfirm: {
|
||||
messageId: 'nSrsForget',
|
||||
values: { nSrs: srs.length },
|
||||
},
|
||||
}).then(() => Promise.all(map(resolveIds(srs), id => _call('sr.forget', { id }))), noop)
|
||||
|
||||
export const reconnectAllHostsSr = sr =>
|
||||
|
||||
@@ -123,6 +123,10 @@ async function readPackagesFromChangelog(toRelease) {
|
||||
}
|
||||
|
||||
const { name, releaseType } = match.groups
|
||||
if (name in toRelease) {
|
||||
throw new Error('duplicate package to release in CHANGELOG.unreleased.md: ' + name)
|
||||
}
|
||||
|
||||
toRelease[name] = releaseType
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user