Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e6b0ae06d | ||
|
|
bcc0452646 | ||
|
|
9d9691c5a3 | ||
|
|
e56edc70d5 | ||
|
|
d7f4d0f5e0 | ||
|
|
8c24dd1732 | ||
|
|
575a423edf | ||
|
|
e311860bb5 | ||
|
|
e6289ebc16 | ||
|
|
013e20aa0f | ||
|
|
45a0a83fa4 | ||
|
|
ae518399fa | ||
|
|
d949112921 | ||
|
|
bb19afc45c | ||
|
|
7780cb176a | ||
|
|
74ff64dfb4 | ||
|
|
9be3c40ead | ||
|
|
0f00c7e393 | ||
|
|
95492f6f89 | ||
|
|
046fa7282b | ||
|
|
6cd99c39f4 | ||
|
|
48c3a65cc6 | ||
|
|
8b0b2d7c31 | ||
|
|
d8280087a4 | ||
|
|
c14261a0bc | ||
|
|
3d6defca37 | ||
|
|
d062a5175a | ||
|
|
f218874c4b | ||
|
|
b1e879ca2f | ||
|
|
c5010c2caa | ||
|
|
2c40b99d8b | ||
|
|
0d127f2b92 | ||
|
|
0464886e80 | ||
|
|
d655a3e222 | ||
|
|
579f0b91d5 | ||
|
|
72b1878254 | ||
|
|
74dd4c8db7 | ||
|
|
ef4ecce572 | ||
|
|
1becccffbc | ||
|
|
b95b1622b1 | ||
|
|
36d6e3779d | ||
|
|
b0e000328d | ||
|
|
cc080ec681 | ||
|
|
0d4cf48410 |
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
1. ⚠️ **If you don't follow this template, the issue will be closed**.
|
||||
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
|
||||
|
||||
Are you using XOA or XO from the sources?
|
||||
|
||||
If XOA:
|
||||
|
||||
- which release channel? (`stable` vs `latest`)
|
||||
- please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
|
||||
|
||||
If XO from the sources:
|
||||
|
||||
- Provide **your commit number**. If it's older than a week, we won't investigate
|
||||
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
|
||||
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please provide the following information):**
|
||||
|
||||
- Node: [e.g. 16.12.1]
|
||||
- hypervisor: [e.g. XCP-ng 8.2.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
119
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
119
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ['type: bug :bug:', 'status: triaging :triangular_flag_on_post:']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
1. ⚠️ **If you don't follow this template, the issue will be closed**.
|
||||
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Are you using XOA or XO from the sources?'
|
||||
- type: dropdown
|
||||
id: xo-origin
|
||||
attributes:
|
||||
label: Are you using XOA or XO from the sources?
|
||||
options:
|
||||
- XOA
|
||||
- XO from the sources
|
||||
- both
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '### If XOA:'
|
||||
- type: dropdown
|
||||
id: xoa-channel
|
||||
attributes:
|
||||
label: Which release channel?
|
||||
description: please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
|
||||
options:
|
||||
- stable
|
||||
- latest
|
||||
- both
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '### If XO from the sources:'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
|
||||
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
|
||||
- type: input
|
||||
id: xo-sources-commit-number
|
||||
attributes:
|
||||
label: Provide your commit number
|
||||
description: If it's older than a week, we won't investigate
|
||||
placeholder: e.g. 579f0
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Bug description:'
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: error-message
|
||||
attributes:
|
||||
label: Error message
|
||||
render: Markdown
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: 'Steps to reproduce the behavior:'
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Environment (please provide the following information):'
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node
|
||||
placeholder: e.g. 16.12.1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: hypervisor-version
|
||||
attributes:
|
||||
label: Hypervisor
|
||||
placeholder: e.g. XCP-ng 8.2.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here
|
||||
validations:
|
||||
required: false
|
||||
@@ -22,7 +22,7 @@
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.8.0"
|
||||
"vhd-lib": "^4.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
||||
)
|
||||
}
|
||||
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
|
||||
await healthCheckVm.add_tag('xo:no-bak=Health Check')
|
||||
await healthCheckVm.add_tags('xo:no-bak=Health Check')
|
||||
await new HealthCheckVmBackup({
|
||||
restoredVm: healthCheckVm,
|
||||
xapi,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"tar": "^6.1.15",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.8.0",
|
||||
"vhd-lib": "^4.9.0",
|
||||
"xen-api": "^2.0.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
## **next**
|
||||
|
||||
- Fix Typescript typings errors when running `yarn type-check` command (PR [#7278](https://github.com/vatesfr/xen-orchestra/pull/7278))
|
||||
|
||||
## **0.1.7** (2023-12-28)
|
||||
|
||||
- [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))
|
||||
- XOA quick deploy (PR [#7245](https://github.com/vatesfr/xen-orchestra/pull/7245))
|
||||
- Fix infinite loader when no stats on pool dashboard (PR [#7236](https://github.com/vatesfr/xen-orchestra/pull/7236))
|
||||
- [Tree view] Display VMs count (PR [#7185](https://github.com/vatesfr/xen-orchestra/pull/7185))
|
||||
|
||||
## **0.1.6** (2023-11-30)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/lite",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"scripts": {
|
||||
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
|
||||
"build": "run-p type-check build-only",
|
||||
|
||||
@@ -21,7 +21,8 @@ a {
|
||||
}
|
||||
|
||||
code,
|
||||
code * {
|
||||
code *,
|
||||
pre {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
1
@xen-orchestra/lite/src/assets/xo.svg
Normal file
1
@xen-orchestra/lite/src/assets/xo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 43 KiB |
@@ -13,6 +13,9 @@
|
||||
<slot />
|
||||
<div class="right">
|
||||
<PoolOverrideWarning as-tooltip />
|
||||
<UiButton v-if="isDesktop" :icon="faDownload" @click="openXoaDeploy">
|
||||
{{ $t("deploy-xoa") }}
|
||||
</UiButton>
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
@@ -22,14 +25,20 @@
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import TextLogo from "@/components/TextLogo.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { faBars } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBars, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const openXoaDeploy = () => router.push({ name: "xoa.deploy" });
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const { isMobile } = storeToRefs(uiStore);
|
||||
const { isMobile, isDesktop } = storeToRefs(uiStore);
|
||||
|
||||
const navigationStore = useNavigationStore();
|
||||
const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
@@ -62,5 +71,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
:icon="faStar"
|
||||
class="master-icon"
|
||||
/>
|
||||
<p
|
||||
class="vm-count"
|
||||
v-tooltip="$t('vm-running', { count: vmCount })"
|
||||
v-if="isReady"
|
||||
>
|
||||
{{ vmCount }}
|
||||
</p>
|
||||
<InfraAction
|
||||
:icon="isExpanded ? faAngleDown : faAngleUp"
|
||||
@click="toggle()"
|
||||
@@ -41,6 +48,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useToggle } from "@vueuse/core";
|
||||
import { computed } from "vue";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
|
||||
const props = defineProps<{
|
||||
hostOpaqueRef: XenApiHost["$ref"];
|
||||
@@ -58,6 +66,12 @@ const isCurrentHost = computed(
|
||||
() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef
|
||||
);
|
||||
const [isExpanded, toggle] = useToggle(true);
|
||||
|
||||
const { recordsByHostRef, isReady } = useVmCollection();
|
||||
|
||||
const vmCount = computed(
|
||||
() => recordsByHostRef.value.get(props.hostOpaqueRef)?.length ?? 0
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -74,4 +88,18 @@ const [isExpanded, toggle] = useToggle(true);
|
||||
.master-icon {
|
||||
color: var(--color-orange-world-base);
|
||||
}
|
||||
|
||||
.vm-count {
|
||||
font-size: smaller;
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
color: var(--color-blue-scale-500);
|
||||
border-radius: calc(var(--size) / 2);
|
||||
background-color: var(--color-extra-blue-base);
|
||||
--size: 2.3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<UiModal color="error" @submit="modal.approve()">
|
||||
<ConfirmModalLayout :icon="faExclamationCircle">
|
||||
<template #title>{{ $t("invalid-field") }}</template>
|
||||
|
||||
<template #default>
|
||||
{{ message }}
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<ModalApproveButton>
|
||||
{{ $t("ok") }}
|
||||
</ModalApproveButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
|
||||
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import { IK_MODAL } from "@/types/injection-keys";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { inject } from "vue";
|
||||
|
||||
defineProps<{
|
||||
message: string;
|
||||
}>();
|
||||
|
||||
const modal = inject(IK_MODAL)!;
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
18
@xen-orchestra/lite/src/components/ui/UiRaw.vue
Normal file
18
@xen-orchestra/lite/src/components/ui/UiRaw.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<pre class="ui-raw"><slot /></pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-raw {
|
||||
background-color: var(--color-blue-scale-400);
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
width: 48em;
|
||||
padding: 0.5em;
|
||||
border-radius: 8px;
|
||||
line-height: 150%;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@/libs/xapi-stats";
|
||||
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
import { type Pausable, promiseTimeout, useTimeoutPoll } from "@vueuse/core";
|
||||
import { computed, type ComputedRef, onUnmounted, ref } from "vue";
|
||||
import { computed, type ComputedRef, onUnmounted, ref, type Ref } from "vue";
|
||||
|
||||
export type Stat<T> = {
|
||||
canBeExpired: boolean;
|
||||
@@ -42,7 +42,7 @@ export default function useFetchStats<
|
||||
T extends XenApiHost | XenApiVm,
|
||||
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
|
||||
>(getStats: GetStats<T, S>, granularity: GRANULARITY): FetchedStats<T, S> {
|
||||
const stats = ref<Map<string, Stat<S>>>(new Map());
|
||||
const stats = ref(new Map()) as Ref<Map<string, Stat<S>>>;
|
||||
const timestamp = ref<number[]>([0, 0]);
|
||||
const abortController = new AbortController();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type HostConfig = {
|
||||
export const useHostPatches = (hosts: MaybeRefOrGetter<XenApiHost[]>) => {
|
||||
const hostStore = useHostStore();
|
||||
|
||||
const configByHost = reactive(new Map<string, HostConfig>());
|
||||
const configByHost = reactive(new Map()) as Map<string, HostConfig>;
|
||||
|
||||
const fetchHostPatches = async (hostRef: XenApiHost["$ref"]) => {
|
||||
if (!configByHost.has(hostRef)) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { computed, ref, unref } from "vue";
|
||||
import type { MaybeRef } from "@vueuse/core";
|
||||
import { computed, ref, type Ref, unref } from "vue";
|
||||
|
||||
export default function useMultiSelect<T>(
|
||||
usableIds: MaybeRef<T[]>,
|
||||
selectableIds?: MaybeRef<T[]>
|
||||
) {
|
||||
const $selected = ref<Set<T>>(new Set());
|
||||
const $selected = ref(new Set()) as Ref<Set<T>>;
|
||||
|
||||
const selected = computed({
|
||||
get() {
|
||||
|
||||
@@ -54,6 +54,7 @@ type ObjectTypeToRecordMapping = {
|
||||
host: XenApiHost;
|
||||
host_metrics: XenApiHostMetrics;
|
||||
message: XenApiMessage<any>;
|
||||
network: XenApiNetwork;
|
||||
pool: XenApiPool;
|
||||
sr: XenApiSr;
|
||||
vm: XenApiVm;
|
||||
@@ -113,9 +114,11 @@ export interface XenApiHost extends XenApiRecord<"host"> {
|
||||
}
|
||||
|
||||
export interface XenApiSr extends XenApiRecord<"sr"> {
|
||||
content_type: string;
|
||||
name_label: string;
|
||||
physical_size: number;
|
||||
physical_utilisation: number;
|
||||
shared: boolean;
|
||||
}
|
||||
|
||||
export interface XenApiVm extends XenApiRecord<"vm"> {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"about": "About",
|
||||
"access-xoa": "Access XOA",
|
||||
"add": "Add",
|
||||
"add-filter": "Add filter",
|
||||
"add-or": "+OR",
|
||||
"add-sort": "Add sort",
|
||||
"admin-login": "Admin login",
|
||||
"admin-password": "Admin password",
|
||||
"admin-password-confirm": "Confirm admin password",
|
||||
"alarm-type": {
|
||||
"cpu_usage": "CPU usage exceeds {n}%",
|
||||
"disk_usage": "Disk usage exceeds {n}%",
|
||||
@@ -26,12 +30,14 @@
|
||||
"backup": "Backup",
|
||||
"cancel": "Cancel",
|
||||
"change-state": "Change state",
|
||||
"check-errors": "Check out the errors:",
|
||||
"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",
|
||||
"community-name": "{name} community",
|
||||
"configuration": "Configuration",
|
||||
"confirm-cancel": "Are you sure you want to cancel?",
|
||||
"confirm-delete": "You're about to delete {0}",
|
||||
"console": "Console",
|
||||
@@ -43,14 +49,28 @@
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"delete-vms": "Delete 1 VM | Delete {n} VMs",
|
||||
"deploy": "Deploy",
|
||||
"deploy-xoa": "Deploy XOA",
|
||||
"deploy-xoa-available-on-desktop": "XOA deployment is available on your desktop interface",
|
||||
"deploy-xoa-status": {
|
||||
"configuring": "Configuring XOA…",
|
||||
"importing": "Importing XOA…",
|
||||
"not-responding": "XOA is not responding",
|
||||
"ready": "XOA is ready!",
|
||||
"starting": "Starting XOA…",
|
||||
"waiting": "Waiting for XOA to respond…"
|
||||
},
|
||||
"descending": "descending",
|
||||
"description": "Description",
|
||||
"dhcp": "DHCP",
|
||||
"disabled": "Disabled",
|
||||
"display": "Display",
|
||||
"dns": "DNS",
|
||||
"do-you-have-needs": "You have needs and/or expectations? Let us know",
|
||||
"documentation": "Documentation",
|
||||
"documentation-name": "{name} documentation",
|
||||
"edit-config": "Edit config",
|
||||
"enabled": "Enabled",
|
||||
"error-no-data": "Error, can't collect data.",
|
||||
"error-occurred": "An error has occurred",
|
||||
"export": "Export",
|
||||
@@ -84,11 +104,16 @@
|
||||
"force-shutdown": "Force shutdown",
|
||||
"fullscreen": "Fullscreen",
|
||||
"fullscreen-leave": "Leave fullscreen",
|
||||
"gateway": "Gateway",
|
||||
"n-gb-left": "{n} GB left",
|
||||
"n-gb-required": "{n} GB required",
|
||||
"go-back": "Go back",
|
||||
"gzip": "gzip",
|
||||
"here": "Here",
|
||||
"hosts": "Hosts",
|
||||
"invalid-field": "Invalid field",
|
||||
"keep-me-logged": "Keep me logged in",
|
||||
"keep-page-open": "Do not refresh or quit tab before end of deployment.",
|
||||
"language": "Language",
|
||||
"last-week": "Last week",
|
||||
"learn-more": "Learn more",
|
||||
@@ -104,6 +129,7 @@
|
||||
"n-missing": "{n} missing",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Name",
|
||||
"netmask": "Netmask",
|
||||
"network": "Network",
|
||||
"network-download": "Download",
|
||||
"network-throughput": "Network throughput",
|
||||
@@ -119,6 +145,7 @@
|
||||
"not-found": "Not found",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
"ok": "OK",
|
||||
"on-object": "on {object}",
|
||||
"open-console-in-new-tab": "Open console in new tab",
|
||||
"or": "Or",
|
||||
@@ -154,14 +181,23 @@
|
||||
"selected-vms-in-execution": "Some selected VMs are running",
|
||||
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"select": {
|
||||
"network": "Select a network",
|
||||
"storage": "Select a storage"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"shutdown": "Shutdown",
|
||||
"snapshot": "Snapshot",
|
||||
"sort-by": "Sort by",
|
||||
"ssh-account": "SSH account",
|
||||
"ssh-login": "SSH login",
|
||||
"ssh-password": "SSH password",
|
||||
"ssh-password-confirm": "Confirm SSH password",
|
||||
"stacked-cpu-usage": "Stacked CPU usage",
|
||||
"stacked-ram-usage": "Stacked RAM usage",
|
||||
"start": "Start",
|
||||
"start-on-host": "Start on specific host",
|
||||
"static-ip": "Static IP",
|
||||
"stats": "Stats",
|
||||
"status": "Status",
|
||||
"storage": "Storage",
|
||||
@@ -191,8 +227,18 @@
|
||||
"vcpus-used": "vCPUs used",
|
||||
"version": "Version",
|
||||
"vm-is-running": "The VM is running",
|
||||
"vm-running": "VM running | VMs running",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite is under construction",
|
||||
"xoa-admin-account": "XOA admin account",
|
||||
"xoa-deploy": "XOA deployment",
|
||||
"xoa-deploy-failed": "Sorry, deployment failed!",
|
||||
"xoa-deploy-retry": "Try again to deploy XOA",
|
||||
"xoa-deploy-successful": "XOA deployment successful!",
|
||||
"xoa-ip": "XOA IP address",
|
||||
"xoa-password-confirm-different": "XOA password confirmation is different",
|
||||
"xoa-ssh-account": "XOA SSH account",
|
||||
"xoa-ssh-password-confirm-different": "SSH password confirmation is different",
|
||||
"you-are-currently-on": "You are currently on: {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"about": "À propos",
|
||||
"access-xoa": "Accéder à la XOA",
|
||||
"add": "Ajouter",
|
||||
"add-filter": "Ajouter un filtre",
|
||||
"add-or": "+OU",
|
||||
"add-sort": "Ajouter un tri",
|
||||
"admin-login": "Nom d'utilisateur administrateur",
|
||||
"admin-password": "Mot de passe administrateur",
|
||||
"admin-password-confirm": "Confirmer le mot de passe administrateur",
|
||||
"alarm-type": {
|
||||
"cpu_usage": "L'utilisation du CPU dépasse {n}%",
|
||||
"disk_usage": "L'utilisation du disque dépasse {n}%",
|
||||
@@ -26,12 +30,14 @@
|
||||
"backup": "Sauvegarde",
|
||||
"cancel": "Annuler",
|
||||
"change-state": "Changer l'état",
|
||||
"check-errors": "Consultez les erreurs :",
|
||||
"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é",
|
||||
"community-name": "Communauté {name}",
|
||||
"configuration": "Configuration",
|
||||
"confirm-cancel": "Êtes-vous sûr de vouloir annuler ?",
|
||||
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
|
||||
"console": "Console",
|
||||
@@ -43,14 +49,28 @@
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"delete-vms": "Supprimer 1 VM | Supprimer {n} VMs",
|
||||
"deploy": "Déployer",
|
||||
"deploy-xoa": "Déployer XOA",
|
||||
"deploy-xoa-available-on-desktop": "Le déploiement de la XOA est disponible sur ordinateur",
|
||||
"deploy-xoa-status": {
|
||||
"configuring": "Configuration de la XOA…",
|
||||
"importing": "Importation de la XOA…",
|
||||
"not-responding": "La XOA ne répond pas",
|
||||
"ready": "La XOA est prête !",
|
||||
"starting": "Démarrage de la XOA…",
|
||||
"waiting": "En attente de réponse de la XOA…"
|
||||
},
|
||||
"descending": "descendant",
|
||||
"description": "Description",
|
||||
"dhcp": "DHCP",
|
||||
"dns": "DNS",
|
||||
"disabled": "Désactivé",
|
||||
"display": "Affichage",
|
||||
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
|
||||
"documentation": "Documentation",
|
||||
"documentation-name": "Documentation {name}",
|
||||
"edit-config": "Modifier config",
|
||||
"enabled": "Activé",
|
||||
"error-no-data": "Erreur, impossible de collecter les données.",
|
||||
"error-occurred": "Une erreur est survenue",
|
||||
"export": "Exporter",
|
||||
@@ -84,11 +104,16 @@
|
||||
"force-shutdown": "Forcer l'arrêt",
|
||||
"fullscreen": "Plein écran",
|
||||
"fullscreen-leave": "Quitter plein écran",
|
||||
"gateway": "Passerelle",
|
||||
"n-gb-left": "{n} Go libres",
|
||||
"n-gb-required": "{n} Go requis",
|
||||
"go-back": "Revenir en arrière",
|
||||
"gzip": "gzip",
|
||||
"here": "Ici",
|
||||
"hosts": "Hôtes",
|
||||
"invalid-field": "Champ invalide",
|
||||
"keep-me-logged": "Rester connecté",
|
||||
"keep-page-open": "Ne pas rafraichir ou quitter cette page avant la fin du déploiement.",
|
||||
"language": "Langue",
|
||||
"last-week": "Semaine dernière",
|
||||
"learn-more": "En savoir plus",
|
||||
@@ -104,6 +129,7 @@
|
||||
"n-missing": "{n} manquant | {n} manquants",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Nom",
|
||||
"netmask": "Masque réseau",
|
||||
"network": "Réseau",
|
||||
"network-download": "Descendant",
|
||||
"network-throughput": "Débit du réseau",
|
||||
@@ -119,6 +145,7 @@
|
||||
"not-found": "Non trouvé",
|
||||
"object": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
"ok": "OK",
|
||||
"on-object": "sur {object}",
|
||||
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
||||
"or": "Ou",
|
||||
@@ -154,14 +181,23 @@
|
||||
"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",
|
||||
"select": {
|
||||
"network": "Sélectionner un réseau",
|
||||
"storage": "Sélectionner un SR"
|
||||
},
|
||||
"settings": "Paramètres",
|
||||
"shutdown": "Arrêter",
|
||||
"snapshot": "Instantané",
|
||||
"sort-by": "Trier par",
|
||||
"ssh-account": "Compte SSH",
|
||||
"ssh-login": "Nom d'utilisateur SSH",
|
||||
"ssh-password": "Mot de passe SSH",
|
||||
"ssh-password-confirm": "Confirmer le mot de passe SSH",
|
||||
"stacked-cpu-usage": "Utilisation CPU empilée",
|
||||
"stacked-ram-usage": "Utilisation RAM empilée",
|
||||
"start": "Démarrer",
|
||||
"start-on-host": "Démarrer sur un hôte spécifique",
|
||||
"static-ip": "IP statique",
|
||||
"stats": "Stats",
|
||||
"status": "Statut",
|
||||
"storage": "Stockage",
|
||||
@@ -191,8 +227,18 @@
|
||||
"vcpus-used": "vCPUs utilisés",
|
||||
"version": "Version",
|
||||
"vm-is-running": "La VM est en cours d'exécution",
|
||||
"vm-running": "VM en cours d'exécution | VMs en cours d'exécution",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite est en construction",
|
||||
"xoa-admin-account": "Compte administrateur de la XOA",
|
||||
"xoa-deploy": "Déploiement de la XOA",
|
||||
"xoa-deploy-failed": "Erreur lors du déploiement de la XOA !",
|
||||
"xoa-deploy-retry": "Ré-essayer de déployer une XOA",
|
||||
"xoa-deploy-successful": "XOA deployée avec succès !",
|
||||
"xoa-ip": "XOA IP address",
|
||||
"xoa-password-confirm-different": "La confirmation du mot de passe XOA est différente",
|
||||
"xoa-ssh-account": "Compte SSH de la XOA",
|
||||
"xoa-ssh-password-confirm-different": "La confirmation du mot de passe SSH est différente",
|
||||
"you-are-currently-on": "Vous êtes actuellement sur : {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ const router = createRouter({
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/xoa-deploy",
|
||||
name: "xoa.deploy",
|
||||
component: () => import("@/views/xoa-deploy/XoaDeployView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
|
||||
9
@xen-orchestra/lite/src/stores/xen-api/network.store.ts
Normal file
9
@xen-orchestra/lite/src/stores/xen-api/network.store.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
|
||||
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useNetworkStore = defineStore("xen-api-network", () => {
|
||||
return useXenApiStoreSubscribableContext("network");
|
||||
});
|
||||
|
||||
export const useNetworkCollection = createUseCollection(useNetworkStore);
|
||||
674
@xen-orchestra/lite/src/views/xoa-deploy/XoaDeployView.vue
Normal file
674
@xen-orchestra/lite/src/views/xoa-deploy/XoaDeployView.vue
Normal file
@@ -0,0 +1,674 @@
|
||||
<template>
|
||||
<TitleBar :icon="faDownload">{{ $t("deploy-xoa") }}</TitleBar>
|
||||
<div v-if="deploying" class="status">
|
||||
<img src="@/assets/xo.svg" width="300" alt="Xen Orchestra" />
|
||||
|
||||
<!-- Error -->
|
||||
<template v-if="error !== undefined">
|
||||
<div>
|
||||
<h2>{{ $t("xoa-deploy-failed") }}</h2>
|
||||
<UiIcon :icon="faExclamationCircle" class="danger" />
|
||||
</div>
|
||||
<div class="error">
|
||||
<strong>{{ $t("check-errors") }}</strong>
|
||||
<UiRaw>{{ error }}</UiRaw>
|
||||
</div>
|
||||
<UiButton :icon="faDownload" @click="resetValues()">
|
||||
{{ $t("xoa-deploy-retry") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<!-- Success -->
|
||||
<template v-else-if="url !== undefined">
|
||||
<div>
|
||||
<h2>{{ $t("xoa-deploy-successful") }}</h2>
|
||||
<UiIcon :icon="faCircleCheck" class="success" />
|
||||
</div>
|
||||
<UiButton :icon="faArrowUpRightFromSquare" @click="openXoa">
|
||||
{{ $t("access-xoa") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<!-- Deploying -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<h2>{{ $t("xoa-deploy") }}</h2>
|
||||
<!-- TODO: add progress bar -->
|
||||
<p>{{ status }}</p>
|
||||
</div>
|
||||
<p class="warning">
|
||||
<UiIcon :icon="faExclamationCircle" />
|
||||
{{ $t("keep-page-open") }}
|
||||
</p>
|
||||
<UiButton
|
||||
:disabled="vmRef === undefined"
|
||||
color="error"
|
||||
outlined
|
||||
@click="cancel()"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isMobile" class="not-available">
|
||||
<p>{{ $t("deploy-xoa-available-on-desktop") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="card-view">
|
||||
<UiCard>
|
||||
<form @submit.prevent="deploy">
|
||||
<FormSection :label="$t('configuration')">
|
||||
<div class="row">
|
||||
<FormInputWrapper
|
||||
:label="$t('storage')"
|
||||
:help="$t('n-gb-required', { n: REQUIRED_GB })"
|
||||
>
|
||||
<FormSelect v-model="selectedSr" required>
|
||||
<option disabled :value="undefined">
|
||||
{{ $t("select.storage") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="sr in filteredSrs"
|
||||
:value="sr"
|
||||
:key="sr.uuid"
|
||||
:class="
|
||||
sr.physical_size - sr.physical_utilisation <
|
||||
REQUIRED_GB * 1024 ** 3
|
||||
? 'warning'
|
||||
: 'success'
|
||||
"
|
||||
>
|
||||
{{ sr.name_label }} -
|
||||
{{
|
||||
$t("n-gb-left", {
|
||||
n: Math.round(
|
||||
(sr.physical_size - sr.physical_utilisation) / 1024 ** 3
|
||||
),
|
||||
})
|
||||
}}
|
||||
<span
|
||||
v-if="
|
||||
sr.physical_size - sr.physical_utilisation <
|
||||
REQUIRED_GB * 1024 ** 3
|
||||
"
|
||||
>⚠️</span
|
||||
>
|
||||
</option>
|
||||
</FormSelect>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper :label="$t('network')" required>
|
||||
<FormSelect v-model="selectedNetwork" required>
|
||||
<option disabled :value="undefined">
|
||||
{{ $t("select.network") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="network in filteredNetworks"
|
||||
:value="network"
|
||||
:key="network.uuid"
|
||||
>
|
||||
{{ network.name_label }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper>
|
||||
<div class="radio-group">
|
||||
<label
|
||||
><FormRadio value="static" v-model="ipStrategy" />{{
|
||||
$t("static-ip")
|
||||
}}</label
|
||||
>
|
||||
<label
|
||||
><FormRadio value="dhcp" v-model="ipStrategy" />{{
|
||||
$t("dhcp")
|
||||
}}</label
|
||||
>
|
||||
</div>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper
|
||||
:label="$t('xoa-ip')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
|
||||
>
|
||||
<FormInput
|
||||
v-model="ip"
|
||||
:disabled="!requireIpConf"
|
||||
placeholder="xxx.xxx.xxx.xxx"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
<FormInputWrapper
|
||||
:label="$t('netmask')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
|
||||
>
|
||||
<FormInput
|
||||
v-model="netmask"
|
||||
:disabled="!requireIpConf"
|
||||
placeholder="255.255.255.0"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper
|
||||
:label="$t('dns')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
|
||||
>
|
||||
<FormInput
|
||||
v-model="dns"
|
||||
:disabled="!requireIpConf"
|
||||
placeholder="8.8.8.8"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
<FormInputWrapper
|
||||
:label="$t('gateway')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
|
||||
>
|
||||
<FormInput
|
||||
v-model="gateway"
|
||||
:disabled="!requireIpConf"
|
||||
placeholder="xxx.xxx.xxx.xxx"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection :label="$t('xoa-admin-account')">
|
||||
<div class="row">
|
||||
<FormInputWrapper
|
||||
:label="$t('admin-login')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#default-xo-account"
|
||||
>
|
||||
<FormInput
|
||||
v-model="xoaUser"
|
||||
required
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper
|
||||
:label="$t('admin-password')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#default-xo-account"
|
||||
>
|
||||
<FormInput
|
||||
type="password"
|
||||
v-model="xoaPwd"
|
||||
required
|
||||
:placeholder="$t('password')"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
<FormInputWrapper
|
||||
:label="$t('admin-password-confirm')"
|
||||
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#default-xo-account"
|
||||
>
|
||||
<FormInput
|
||||
type="password"
|
||||
v-model="xoaPwdConfirm"
|
||||
required
|
||||
:placeholder="$t('password')"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection :label="$t('xoa-ssh-account')">
|
||||
<div class="row">
|
||||
<FormInputWrapper :label="$t('ssh-account')">
|
||||
<label
|
||||
><span>{{ $t("disabled") }}</span
|
||||
><FormToggle v-model="enableSshAccount" /><span>{{
|
||||
$t("enabled")
|
||||
}}</span></label
|
||||
>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper :label="$t('ssh-login')">
|
||||
<FormInput value="xoa" placeholder="xoa" disabled />
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div class="row">
|
||||
<FormInputWrapper :label="$t('ssh-password')">
|
||||
<FormInput
|
||||
type="password"
|
||||
v-model="sshPwd"
|
||||
:placeholder="$t('password')"
|
||||
:disabled="!enableSshAccount"
|
||||
:required="enableSshAccount"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
<FormInputWrapper :label="$t('ssh-password-confirm')">
|
||||
<FormInput
|
||||
type="password"
|
||||
v-model="sshPwdConfirm"
|
||||
:placeholder="$t('password')"
|
||||
:disabled="!enableSshAccount"
|
||||
:required="enableSshAccount"
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<UiButtonGroup>
|
||||
<UiButton outlined @click="router.back()">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
<UiButton type="submit">
|
||||
{{ $t("deploy") }}
|
||||
</UiButton>
|
||||
</UiButtonGroup>
|
||||
</form>
|
||||
</UiCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faCircleCheck,
|
||||
faDownload,
|
||||
faExclamationCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useModal } from "@/composables/modal.composable";
|
||||
import { useNetworkCollection } from "@/stores/xen-api/network.store";
|
||||
import { usePageTitleStore } from "@/stores/page-title.store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useSrCollection } from "@/stores/xen-api/sr.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import type { XenApiNetwork, XenApiSr } from "@/libs/xen-api/xen-api.types";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
|
||||
import FormRadio from "@/components/form/FormRadio.vue";
|
||||
import FormSection from "@/components/form/FormSection.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import FormToggle from "@/components/form/FormToggle.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiRaw from "@/components/ui/UiRaw.vue";
|
||||
|
||||
const REQUIRED_GB = 20;
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
usePageTitleStore().setTitle(() => t("deploy-xoa"));
|
||||
|
||||
const invalidField = (message: string) =>
|
||||
useModal(() => import("@/components/modals/InvalidFieldModal.vue"), {
|
||||
message,
|
||||
});
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const { isMobile } = storeToRefs(uiStore);
|
||||
|
||||
const xapi = useXenApiStore().getXapi();
|
||||
|
||||
const { records: srs } = useSrCollection();
|
||||
const filteredSrs = computed(() =>
|
||||
srs.value
|
||||
.filter((sr) => sr.content_type !== "iso" && sr.physical_size > 0)
|
||||
// Sort: shared first then largest free space first
|
||||
.sort((sr1, sr2) => {
|
||||
if (sr1.shared === sr2.shared) {
|
||||
return (
|
||||
sr2.physical_size -
|
||||
sr2.physical_utilisation -
|
||||
(sr1.physical_size - sr1.physical_utilisation)
|
||||
);
|
||||
} else {
|
||||
return sr1.shared ? -1 : 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const { records: networks } = useNetworkCollection();
|
||||
const filteredNetworks = computed(() =>
|
||||
[...networks.value].sort((network1, network2) =>
|
||||
network1.name_label < network2.name_label ? -1 : 1
|
||||
)
|
||||
);
|
||||
|
||||
const deploying = ref(false);
|
||||
const status = ref<string | undefined>();
|
||||
const error = ref<string | undefined>();
|
||||
const url = ref<string | undefined>();
|
||||
const vmRef = ref<string | undefined>();
|
||||
|
||||
const resetValues = () => {
|
||||
deploying.value = false;
|
||||
status.value = undefined;
|
||||
error.value = undefined;
|
||||
url.value = undefined;
|
||||
vmRef.value = undefined;
|
||||
};
|
||||
|
||||
const openXoa = () => {
|
||||
window.open(url.value, "_blank", "noopener");
|
||||
};
|
||||
|
||||
const selectedSr = ref<XenApiSr>();
|
||||
const selectedNetwork = ref<XenApiNetwork>();
|
||||
const ipStrategy = ref<"static" | "dhcp">("dhcp");
|
||||
const requireIpConf = computed(() => ipStrategy.value === "static");
|
||||
|
||||
const ip = ref("");
|
||||
const netmask = ref("");
|
||||
const dns = ref("");
|
||||
const gateway = ref("");
|
||||
const xoaUser = ref("");
|
||||
const xoaPwd = ref("");
|
||||
const xoaPwdConfirm = ref("");
|
||||
const enableSshAccount = ref(true);
|
||||
const sshPwd = ref("");
|
||||
const sshPwdConfirm = ref("");
|
||||
|
||||
async function deploy() {
|
||||
if (selectedSr.value === undefined || selectedNetwork.value === undefined) {
|
||||
// Should not happen
|
||||
console.error("SR or network is undefined");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
ipStrategy.value === "static" &&
|
||||
(ip.value === "" ||
|
||||
netmask.value === "" ||
|
||||
dns.value === "" ||
|
||||
gateway.value === "")
|
||||
) {
|
||||
// Should not happen
|
||||
console.error("Missing IP config");
|
||||
return;
|
||||
}
|
||||
|
||||
if (xoaUser.value === "" || xoaPwd.value === "") {
|
||||
// Should not happen
|
||||
console.error("Missing XOA credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
if (xoaPwd.value !== xoaPwdConfirm.value) {
|
||||
// TODO: use formal validation system
|
||||
invalidField(t("xoa-password-confirm-different"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableSshAccount.value && sshPwd.value === "") {
|
||||
// Should not happen
|
||||
console.error("Missing XOA credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableSshAccount.value && sshPwd.value !== sshPwdConfirm.value) {
|
||||
// TODO: use form validation system
|
||||
invalidField(t("xoa-ssh-password-confirm-different"));
|
||||
return;
|
||||
}
|
||||
|
||||
deploying.value = true;
|
||||
|
||||
try {
|
||||
status.value = t("deploy-xoa-status.importing");
|
||||
|
||||
vmRef.value = (
|
||||
(await xapi.call("VM.import", [
|
||||
"http://xoa.io:8888/",
|
||||
selectedSr.value.$ref,
|
||||
false, // full_restore
|
||||
false, // force
|
||||
])) as string[]
|
||||
)[0];
|
||||
|
||||
status.value = t("deploy-xoa-status.configuring");
|
||||
|
||||
const [vifRef] = (await xapi.call("VM.get_VIFs", [
|
||||
vmRef.value,
|
||||
])) as string[];
|
||||
await xapi.call("VIF.destroy", [vifRef]);
|
||||
|
||||
if (!deploying.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [device] = (await xapi.call("VM.get_allowed_VIF_devices", [
|
||||
vmRef.value,
|
||||
])) as string[];
|
||||
await xapi.call("VIF.create", [
|
||||
{
|
||||
device,
|
||||
MAC: "",
|
||||
MTU: selectedNetwork.value.MTU,
|
||||
network: selectedNetwork.value.$ref,
|
||||
other_config: {},
|
||||
qos_algorithm_params: {},
|
||||
qos_algorithm_type: "",
|
||||
VM: vmRef.value,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!deploying.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = [
|
||||
xapi.call("VM.add_to_xenstore_data", [
|
||||
vmRef.value,
|
||||
"vm-data/admin-account",
|
||||
JSON.stringify({ email: xoaUser.value, password: xoaPwd.value }),
|
||||
]),
|
||||
];
|
||||
|
||||
// TODO: add host to servers with session token?
|
||||
|
||||
if (ipStrategy.value === "static") {
|
||||
promises.push(
|
||||
xapi.call("VM.add_to_xenstore_data", [
|
||||
vmRef.value,
|
||||
"vm-data/ip",
|
||||
ip.value,
|
||||
]),
|
||||
xapi.call("VM.add_to_xenstore_data", [
|
||||
vmRef.value,
|
||||
"vm-data/netmask",
|
||||
netmask.value,
|
||||
]),
|
||||
xapi.call("VM.add_to_xenstore_data", [
|
||||
vmRef.value,
|
||||
"vm-data/gateway",
|
||||
gateway.value,
|
||||
]),
|
||||
xapi.call("VM.add_to_xenstore_data", [
|
||||
vmRef.value,
|
||||
"vm-data/dns",
|
||||
dns.value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (enableSshAccount.value) {
|
||||
promises.push(
|
||||
xapi.call("VM.add_to_xenstore_data", [
|
||||
vmRef.value,
|
||||
"vm-data/system-account-xoa-password",
|
||||
sshPwd.value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (!deploying.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.value = t("deploy-xoa-status.starting");
|
||||
|
||||
await xapi.call("VM.start", [
|
||||
vmRef.value,
|
||||
false, // start_paused
|
||||
false, // force
|
||||
]);
|
||||
|
||||
if (!deploying.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.value = t("deploy-xoa-status.waiting");
|
||||
|
||||
const metricsRef = await xapi.call("VM.get_guest_metrics", [vmRef.value]);
|
||||
let attempts = 120;
|
||||
let networks: { "0/ip": string } | undefined;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10e3)); // Sleep 10s
|
||||
do {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1e3)); // Sleep 1s
|
||||
networks = await xapi.call("VM_guest_metrics.get_networks", [metricsRef]);
|
||||
if (!deploying.value) {
|
||||
return;
|
||||
}
|
||||
} while (--attempts > 0 && networks?.["0/ip"] === undefined);
|
||||
|
||||
if (attempts === 0 || networks === undefined) {
|
||||
status.value = t("deploy-xoa-status.not-responding");
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
"admin-account",
|
||||
"dns",
|
||||
"gateway",
|
||||
"ip",
|
||||
"netmask",
|
||||
"xoa-updater-credentials",
|
||||
].map((key) =>
|
||||
xapi.call("VM.remove_from_xenstore_data", [
|
||||
vmRef.value,
|
||||
`vm-data/${key}`,
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
status.value = t("deploy-xoa-status.ready");
|
||||
|
||||
// TODO: handle IPv6
|
||||
url.value = `https://${networks["0/ip"]}`;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
error.value = err?.message ?? err?.code ?? "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
const _vmRef = vmRef.value;
|
||||
console.log("_vmRef:", _vmRef);
|
||||
resetValues();
|
||||
if (_vmRef !== undefined) {
|
||||
try {
|
||||
await xapi.call("VM.destroy", [_vmRef]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.card-view {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 10rem;
|
||||
}
|
||||
|
||||
.form-toggle {
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
.form-input-wrapper {
|
||||
flex-grow: 1;
|
||||
min-width: 60rem;
|
||||
}
|
||||
|
||||
.input-container * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 1.67rem 0;
|
||||
& > * {
|
||||
min-width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-radio {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.not-available,
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 42px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 76.5vh;
|
||||
color: var(--color-extra-blue-base);
|
||||
text-align: center;
|
||||
padding: 5rem;
|
||||
margin: auto;
|
||||
h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.not-available {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.status {
|
||||
color: var(--color-blue-scale-100);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--color-green-infra-base);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--color-red-vates-base);
|
||||
}
|
||||
.success,
|
||||
.danger {
|
||||
&.ui-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.warning {
|
||||
color: var(--color-orange-world-base);
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^3.3.0",
|
||||
"vhd-lib": "^4.8.0"
|
||||
"vhd-lib": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@@ -3,7 +3,6 @@ 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'
|
||||
|
||||
@@ -11,7 +10,7 @@ const waitAgentRestart = (xapi, hostRef, prevAgentStartTime) =>
|
||||
new Promise(resolve => {
|
||||
// even though the ref could change in case of pool master restart, tests show it stays the same
|
||||
const stopWatch = xapi.watchObject(hostRef, host => {
|
||||
if (+host.other_config.agent_start_time > prevAgentStartTime) {
|
||||
if (+host.other_config.agent_start_time > prevAgentStartTime && host.enabled) {
|
||||
stopWatch()
|
||||
resolve()
|
||||
}
|
||||
@@ -35,6 +34,11 @@ class Host {
|
||||
* @param {string} ref - Opaque reference of the host
|
||||
*/
|
||||
async smartReboot($defer, ref, bypassBlockedSuspend = false, bypassCurrentVmCheck = false) {
|
||||
await this.callAsync('host.disable', ref)
|
||||
|
||||
// host may have been re-enabled already, this is not an problem
|
||||
$defer.onFailure(() => this.callAsync('host.enable', ref))
|
||||
|
||||
let currentVmRef
|
||||
try {
|
||||
currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid())
|
||||
@@ -67,19 +71,15 @@ class Host {
|
||||
})
|
||||
|
||||
const suspendedVms = []
|
||||
if (await this.getField('host', ref, 'enabled')) {
|
||||
await this.callAsync('host.disable', ref)
|
||||
$defer(async () => {
|
||||
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))
|
||||
})
|
||||
}
|
||||
// Resuming VMs should occur after host enabling to avoid triggering a 'NO_HOSTS_AVAILABLE' error
|
||||
//
|
||||
// The defers are running in reverse order.
|
||||
$defer(() => asyncEach(suspendedVms, vmRef => this.callAsync('VM.resume', vmRef, false, false)))
|
||||
$defer.onFailure(() =>
|
||||
// if the host has not been rebooted, it might still be disabled and need to be enabled manually
|
||||
this.callAsync('host.enable', ref)
|
||||
)
|
||||
|
||||
await asyncEach(
|
||||
residentVmRefs,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.8.0",
|
||||
"vhd-lib": "^4.9.0",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"private": false,
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -4,16 +4,36 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [xo-cli] Supports NDJSON response for the `rest get` command (it also respects the `--json` flag) [Forum#69326](https://xcp-ng.org/forum/post/69326)
|
||||
|
||||
## Released packages
|
||||
|
||||
- xo-cli 0.24.0
|
||||
|
||||
## **5.90.0** (2023-12-29)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [VDI] Create XAPI task during NBD export (PR [#7228](https://github.com/vatesfr/xen-orchestra/pull/7228))
|
||||
- [Backup] Use multiple link to speedup NBD backup (PR [#7216](https://github.com/vatesfr/xen-orchestra/pull/7216))
|
||||
- [VDI/Export] Expose NBD settings in the XO and REST APIs api (PR [#7251](https://github.com/vatesfr/xen-orchestra/pull/7251))
|
||||
- [Tags] Implement scoped tags (PR [#7270](https://github.com/vatesfr/xen-orchestra/pull/7270))
|
||||
- [HTTP] `http.useForwardedHeaders` setting can be enabled when XO is behind a reverse proxy to fetch clients IP addresses from `X-Forwarded-*` headers [Forum#67625](https://xcp-ng.org/forum/post/67625) (PR [#7233](https://github.com/vatesfr/xen-orchestra/pull/7233))
|
||||
- [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))
|
||||
- [VM] Trying to increase the memory of a running VM will now propose the option to automatically restart it and increasing its memory [#7069](https://github.com/vatesfr/xen-orchestra/issues/7069) (PR [#7244](https://github.com/vatesfr/xen-orchestra/pull/7244))
|
||||
- [xo-cli] Explicit error when attempting to use REST API before being registered
|
||||
- [REST API] _XO config & Pool metadata Backup_ jobs are available at `/backup/jobs/metadata`
|
||||
- [REST API] _Mirror Backup_ jobs are available at `/backup/jobs/mirror`
|
||||
- [Host/Network/PIF] Display and ability to edit IPv6 field [#5400](https://github.com/vatesfr/xen-orchestra/issues/5400) (PR [#7218](https://github.com/vatesfr/xen-orchestra/pull/7218))
|
||||
- [SR] show an icon on SR during VDI coalescing (with XCP-ng 8.3+) (PR [#7241](https://github.com/vatesfr/xen-orchestra/pull/7241))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [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/mirror`
|
||||
- [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))
|
||||
- [HTTP] `http.useForwardedHeaders` setting can be enabled when XO is behind a reverse proxy to fetch clients IP addresses from `X-Forwarded-*` headers [Forum#67625](https://xcp-ng.org/forum/post/67625) (PR [#7233](https://github.com/vatesfr/xen-orchestra/pull/7233))
|
||||
- [Backup]Use multiple link to speedup NBD backup (PR [#7216](https://github.com/vatesfr/xen-orchestra/pull/7216))
|
||||
- [Backup] Show if disk is differential or full in incremental backups (PR [#7222](https://github.com/vatesfr/xen-orchestra/pull/7222))
|
||||
- [VDI] Create XAPI task during NBD export (PR [#7228](https://github.com/vatesfr/xen-orchestra/pull/7228))
|
||||
- [Menu/Proxies] Added a warning icon if unable to check proxies upgrade (PR [#7237](https://github.com/vatesfr/xen-orchestra/pull/7237))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -24,19 +44,21 @@
|
||||
- [Backup] Reduce memory consumption when using NBD (PR [#7216](https://github.com/vatesfr/xen-orchestra/pull/7216))
|
||||
- [Mirror backup] Fix _Report when_ setting being reset to _Failure_ when editing backup job (PR [#7235](https://github.com/vatesfr/xen-orchestra/pull/7235))
|
||||
- [RPU] VMs are correctly migrated to their original host (PR [#7238](https://github.com/vatesfr/xen-orchestra/pull/7238))
|
||||
- [Backup/Report] Missing report for Mirror Backup (PR [#7254](https://github.com/vatesfr/xen-orchestra/pull/7254))
|
||||
|
||||
### Released packages
|
||||
|
||||
- vhd-lib 4.8.0
|
||||
- @vates/nbd-client 3.0.0
|
||||
- @xen-orchestra/xapi 4.1.0
|
||||
- @xen-orchestra/backups 0.44.3
|
||||
- @xen-orchestra/proxy 0.26.42
|
||||
- xo-server 5.130.0
|
||||
- xo-server-auth-saml 0.11.0
|
||||
- xo-server-transport-email 1.0.0
|
||||
- xo-server-transport-slack 0.0.1
|
||||
- xo-web 5.130.1
|
||||
- xo-cli 0.23.0
|
||||
- vhd-lib 4.9.0
|
||||
- xo-server 5.132.0
|
||||
- xo-web 5.133.0
|
||||
|
||||
## **5.89.0** (2023-11-30)
|
||||
|
||||
|
||||
@@ -7,17 +7,19 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [SR] show an icon on SR during VDI coalescing (with XCP-ng 8.3+) (PR [#7241](https://github.com/vatesfr/xen-orchestra/pull/7241))
|
||||
|
||||
- [VDI/Export] Expose NBD settings in the XO and REST APIs api (PR [#7251](https://github.com/vatesfr/xen-orchestra/pull/7251))
|
||||
- [Menu/Proxies] Added a warning icon if unable to check proxies upgrade (PR [#7237](https://github.com/vatesfr/xen-orchestra/pull/7237))
|
||||
- [Settings/Logs] Use GitHub issue form with pre-filled fields when reporting a bug [#7142](https://github.com/vatesfr/xen-orchestra/issues/7142) (PR [#7274](https://github.com/vatesfr/xen-orchestra/pull/7274))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup/Report] Missing report for Mirror Backup (PR [#7254](https://github.com/vatesfr/xen-orchestra/pull/7254))
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Proxies] Fix `this.getObject` is not a function during deployment
|
||||
- [Settings/Logs] Fix `sr.getAllUnhealthyVdiChainsLength: not enough permissions` error with non-admin users (PR [#7265](https://github.com/vatesfr/xen-orchestra/pull/7265))
|
||||
- [Settings/Logs] Fix `proxy.getAll: not enough permissions` error with non-admin users (PR [#7249](https://github.com/vatesfr/xen-orchestra/pull/7249))
|
||||
- [Replication/Health Check] Fix `healthCheckVm.add_tag is not a function` error [Forum#69156](https://xcp-ng.org/forum/post/69156)
|
||||
- [Plugin/load-balancer] Prevent unwanted migrations to hosts with low free memory (PR [#7288](https://github.com/vatesfr/xen-orchestra/pull/7288))
|
||||
- Avoid unnecessary `pool.add_to_other_config: Duplicate key` error in XAPI log [Forum#68761](https://xcp-ng.org/forum/post/68761)
|
||||
|
||||
### Packages to release
|
||||
|
||||
> When modifying a package, add it here with its release type.
|
||||
@@ -34,8 +36,11 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- vhd-lib patch
|
||||
- xo-server minor
|
||||
- @xen-orchestra/backups patch
|
||||
- @xen-orchestra/xapi patch
|
||||
- xen-api patch
|
||||
- xo-server patch
|
||||
- xo-server-load-balancer patch
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -1,46 +1,86 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# Code of Conduct - Xen Orchestra
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at julien.fontanet@vates.fr. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
julien.fontanet@isonoe.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
@@ -24,7 +24,7 @@ Xen Orchestra itself is built as a modular solution. Each part has its role.
|
||||
|
||||
## xo-server (server)
|
||||
|
||||
The core is "[xo-server](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server/)" - a daemon dealing directly with XenServer or XAPI capable hosts. This is where users are stored, and it's the center point for talking to your whole Xen infrastructure.
|
||||
The core is "[xo-server](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server/)" - a daemon dealing directly with XCP-ng/XenServer or XAPI capable hosts. This is where users are stored, and it's the center point for talking to your whole Xen infrastructure.
|
||||
|
||||
XO-Server is the core of Xen Orchestra. Its central role opens a lot of possibilities versus other solutions - let's see why.
|
||||
|
||||
|
||||
@@ -24,34 +24,34 @@ Nevertheless, there may be some reasons for XO to trigger a key (full) export in
|
||||
|
||||
## VDI chain protection
|
||||
|
||||
Backup jobs regularly delete snapshots. When a snapshot is deleted, either manually or via a backup job, it triggers the need for Xenserver to coalesce the VDI chain - to merge the remaining VDIs and base copies in the chain. This means generally we cannot take too many new snapshots on said VM until Xenserver has finished running a coalesce job on the VDI chain.
|
||||
Backup jobs regularly delete snapshots. When a snapshot is deleted, either manually or via a backup job, it triggers the need for XCP-ng/XenServer to coalesce the VDI chain - to merge the remaining VDIs and base copies in the chain. This means generally we cannot take too many new snapshots on said VM until XCP-ng/XenServer has finished running a coalesce job on the VDI chain.
|
||||
|
||||
This mechanism and scheduling is handled by XenServer itself, not Xen Orchestra. But we can check your existing VDI chain and avoid creating more snapshots than your storage can merge. If we don't, this will lead to catastrophic consequences. Xen Orchestra is the **only** XenServer/XCP backup product that takes this into account and offers protection.
|
||||
This mechanism and scheduling is handled by XCP-ng/XenServer itself, not Xen Orchestra. But we can check your existing VDI chain and avoid creating more snapshots than your storage can merge. If we don't, this will lead to catastrophic consequences. Xen Orchestra is the **only** XCP-ng/XenServer backup product that takes this into account and offers protection.
|
||||
|
||||
Without this detection, you could have 2 potential issues:
|
||||
|
||||
- `The Snapshot Chain is too Long`
|
||||
- `SR_BACKEND_FAILURE_44 (insufficient space)`
|
||||
|
||||
The first issue is a chain that contains more than 30 elements (fixed XenServer limit), and the other one means it's full because the "coalesce" process couldn't keep up the pace and the storage filled up.
|
||||
The first issue is a chain that contains more than 30 elements (fixed XCP-ng/XenServer limit), and the other one means it's full because the "coalesce" process couldn't keep up the pace and the storage filled up.
|
||||
|
||||
In the end, this message is a **protection mechanism preventing damage to your SR**. The backup job will fail, but XenServer itself should eventually automatically coalesce the snapshot chain, and the the next time the backup job should complete.
|
||||
In the end, this message is a **protection mechanism preventing damage to your SR**. The backup job will fail, but XCP-ng/XenServer itself should eventually automatically coalesce the snapshot chain, and the the next time the backup job should complete.
|
||||
|
||||
Just remember this: **a coalesce should happen every time a snapshot is removed**.
|
||||
|
||||
> You can read more on this on our dedicated blog post regarding [XenServer coalesce detection](https://xen-orchestra.com/blog/xenserver-coalesce-detection-in-xen-orchestra/).
|
||||
> You can read more on this on our dedicated blog post regarding [XCP-ng/XenServer coalesce detection](https://xen-orchestra.com/blog/xenserver-coalesce-detection-in-xen-orchestra/).
|
||||
|
||||
### Troubleshooting a constant VDI Chain Protection message (XenServer failure to coalesce)
|
||||
### Troubleshooting a constant VDI Chain Protection message (XCP-ng/XenServer failure to coalesce)
|
||||
|
||||
As previously mentioned, this message can be normal and it just means XenServer needs to perform a coalesce to merge old snapshots. However if you repeatedly get this message and it seems XenServer is not coalescing, You can take a few steps to determine why.
|
||||
As previously mentioned, this message can be normal and it just means XCP-ng/XenServer needs to perform a coalesce to merge old snapshots. However if you repeatedly get this message and it seems XCP-ng/XenServer is not coalescing, You can take a few steps to determine why.
|
||||
|
||||
First check SMlog on the XenServer host for messages relating to VDI corruption or coalesce job failure. For example, by running `cat /var/log/SMlog | grep -i exception` or `cat /var/log/SMlog | grep -i error` on the XenServer host with the affected storage.
|
||||
First check SMlog on the XCP-ng/XenServer host for messages relating to VDI corruption or coalesce job failure. For example, by running `cat /var/log/SMlog | grep -i exception` or `cat /var/log/SMlog | grep -i error` on the XCP-ng/XenServer host with the affected storage.
|
||||
|
||||
Coalesce jobs can also fail to run if the SR does not have enough free space. Check the problematic SR and make sure it has enough free space, generally 30% or more free is recommended depending on VM size. You can check if this is the issue by searching `SMlog` with `grep -i coales /var/log/SMlog` (you may have to look at previous logs such as `SMlog.1`).
|
||||
|
||||
You can check if a coalesce job is currently active by running `ps axf | grep vhd` on the XenServer host and looking for a VHD process in the results (one of the resulting processes will be the grep command you just ran, ignore that one).
|
||||
You can check if a coalesce job is currently active by running `ps axf | grep vhd` on the XCP-ng/XenServer host and looking for a VHD process in the results (one of the resulting processes will be the grep command you just ran, ignore that one).
|
||||
|
||||
If you don't see any running coalesce jobs, and can't find any other reason that XenServer has not started one, you can attempt to make it start a coalesce job by rescanning the SR. This is harmless to try, but will not always result in a coalesce. Visit the problematic SR in the XOA UI, then click the "Rescan All Disks" button towards the top right: it looks like a refresh circle icon. This should begin the coalesce process - if you click the Advanced tab in the SR view, the "disks needing to be coalesced" list should become smaller and smaller.
|
||||
If you don't see any running coalesce jobs, and can't find any other reason that XCP-ng/XenServer has not started one, you can attempt to make it start a coalesce job by rescanning the SR. This is harmless to try, but will not always result in a coalesce. Visit the problematic SR in the XOA UI, then click the "Rescan All Disks" button towards the top right: it looks like a refresh circle icon. This should begin the coalesce process - if you click the Advanced tab in the SR view, the "disks needing to be coalesced" list should become smaller and smaller.
|
||||
|
||||
As a last resort, migrating the VM (more specifically, its disks) to a new storage repository will also force a coalesce and solve this issue. That means migrating a VM to another host (with its own storage) and back will force the VDI chain for that VM to be coalesced, and get rid of the `VDI Chain Protection` message.
|
||||
|
||||
|
||||
@@ -192,9 +192,9 @@ Any Debian Linux mount point could be supported this way, until we add further o
|
||||
|
||||
All your scheduled backups are acccessible in the "Restore" view in the backup section of Xen Orchestra.
|
||||
|
||||
1. Select your remote and click on the eye icon to see the VMs available
|
||||
1. Search the VM Name and click on the blue button with a white arrow
|
||||
2. Choose the backup you want to restore
|
||||
3. Select the SR where you want to restore it
|
||||
3. Select the SR where you want to restore it and click "OK"
|
||||
|
||||
:::tip
|
||||
You can restore your backup even on a brand new host/pool and on brand new hardware.
|
||||
@@ -311,7 +311,7 @@ The first purely sequential strategy will lead to the fact that: **you can't pre
|
||||
If you need your backup to be done at a specific time you should consider creating a specific backup task for this VM.
|
||||
:::
|
||||
|
||||
Strategy number 2 is to parallelise: all the snapshots will be taken at 3 AM. However **it's risky without limits**: it means potentially doing 50 snapshots or more at once on the same storage. **Since XenServer doesn't have a queue**, it will try to do all of them at once. This is also prone to race conditions and could cause crashes on your storage.
|
||||
Strategy number 2 is to parallelise: all the snapshots will be taken at 3 AM. However **it's risky without limits**: it means potentially doing 50 snapshots or more at once on the same storage. **Since XCP-ng/XenServer doesn't have a queue**, it will try to do all of them at once. This is also prone to race conditions and could cause crashes on your storage.
|
||||
|
||||
By default the _parallel strategy_ is, on paper, the most logical one. But you need to be careful and give it some limits on concurrency.
|
||||
|
||||
|
||||
@@ -118,6 +118,22 @@ On XOA, the log file for XO-server is in `/var/log/syslog`. It contains all the
|
||||
|
||||
If you don't want to have Xen Orchestra exposed directly outside, or just integrating it with your existing infrastructure, you can use a Reverse Proxy.
|
||||
|
||||
First of all you need to allow Xen Orchestra to use `X-Forwarded-*` headers to determine the IP addresses of clients:
|
||||
|
||||
```toml
|
||||
[http]
|
||||
# Accepted values for this setting:
|
||||
# - false (default): do not use the headers
|
||||
# - true: always use the headers
|
||||
# - a list of trusted addresses: the headers will be used only if the connection
|
||||
# is coming from one of these addresses
|
||||
#
|
||||
# More info about the accepted values: https://www.npmjs.com/package/proxy-addr?activeTab=readme#proxyaddrreq-trust
|
||||
#
|
||||
# > Note: X-Forwarded-* headers are easily spoofed and the detected IP addresses are unreliable.
|
||||
useForwardedHeaders = ['127.0.0.1']
|
||||
```
|
||||
|
||||
### Apache
|
||||
|
||||
As `xo-web` and `xo-server` communicate with _WebSockets_, you need to have the [`mod_proxy`](http://httpd.apache.org/docs/2.4/mod/mod_proxy.html), [`mod_proxy_http`](http://httpd.apache.org/docs/2.4/mod/mod_proxy_http.html), [`mod_proxy_wstunnel`](http://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) and [`mod_rewrite`](http://httpd.apache.org/docs/2.4/mod/mod_rewrite.html) modules enabled.
|
||||
|
||||
@@ -12,7 +12,7 @@ If you don't have any servers connected, you'll see a panel telling you to add a
|
||||
|
||||
### Add a host
|
||||
|
||||
Just click on "Add server", enter the IP of a XenServer host (ideally the pool master if in a pool):
|
||||
Just click on "Add server", enter the IP of a XCP-ng/XenServer host (ideally the pool master if in a pool):
|
||||
|
||||

|
||||
|
||||
@@ -69,12 +69,12 @@ All your pools are displayed here:
|
||||
You can also see missing patches in red.
|
||||
|
||||
:::tip
|
||||
Did you know? Even a single XenServer host is inside a pool!
|
||||
Did you know? Even a single XCP-ng/XenServer host is inside a pool!
|
||||
:::
|
||||
|
||||
## Live filter search
|
||||
|
||||
The idea is not just to provide a good search engine, but also a complete solution for managing all your XenServer infrastructure. Ideally:
|
||||
The idea is not just to provide a good search engine, but also a complete solution for managing all your XCP-ng/XenServer infrastructure. Ideally:
|
||||
|
||||
- less clicks to see or do what you need
|
||||
- find a subset of interesting objects
|
||||
@@ -238,7 +238,7 @@ The next step is to select a template:
|
||||

|
||||
|
||||
:::tip
|
||||
What is a XenServer template? It can be 2 things: first an "empty" template, meaning it contains only the configuration for your future VM, such as example settings (minimum disk size, RAM and CPU, BIOS settings if HVM etc.) Or it could be a previous VM you converted into a template: in this case, creating a VM will clone the existing disks.
|
||||
What is a XCP-ng/XenServer template? It can be 2 things: first an "empty" template, meaning it contains only the configuration for your future VM, such as example settings (minimum disk size, RAM and CPU, BIOS settings if HVM etc.) Or it could be a previous VM you converted into a template: in this case, creating a VM will clone the existing disks.
|
||||
:::
|
||||
|
||||
##### Name and description
|
||||
@@ -289,7 +289,7 @@ Please refer to the [XCP-ng CloudInit section](advanced.md#cloud-init) for more.
|
||||
|
||||
#### Interfaces
|
||||
|
||||
This is the network section of the VM configuration: in general, MAC field is kept empty (autogenerated from XenServer). We also select the management network by default, but you can change it to reflect your own network configuration.
|
||||
This is the network section of the VM configuration: in general, MAC field is kept empty (autogenerated from XCP-ng/XenServer). We also select the management network by default, but you can change it to reflect your own network configuration.
|
||||
|
||||
#### Disks
|
||||
|
||||
@@ -331,7 +331,7 @@ To do so: Access the Xen Orchestra page for your running VM, then enter the Disk
|
||||
|
||||
#### Offline VDI migration
|
||||
|
||||
Even though it's not currently supported in XenServer, we can do it in Xen Orchestra. It's exactly the same process as a running VM.
|
||||
Even though it's not currently supported in XCP-ng/XenServer, we can do it in Xen Orchestra. It's exactly the same process as a running VM.
|
||||
|
||||
### VM recovery
|
||||
|
||||
@@ -347,7 +347,7 @@ Activating "Auto Power on" for a VM will also configure the pool accordingly. If
|
||||
|
||||
### VM high availability (HA)
|
||||
|
||||
If you pool supports HA (must have shared storage), you can activate "HA". Read our blog post for more details on [VM high availability with XenServer](https://xen-orchestra.com/blog/xenserver-and-vm-high-availability/).
|
||||
If you pool supports HA (must have shared storage), you can activate "HA". Read our blog post for more details on [VM high availability with XCP-ng/XenServer](https://xen-orchestra.com/blog/xenserver-and-vm-high-availability/).
|
||||
|
||||
#### Docker management
|
||||
|
||||
@@ -371,7 +371,7 @@ If one VM has for example, "Double", it will have double the priority on the Xen
|
||||
|
||||
### VM Copy
|
||||
|
||||
VM copy allows you to make an export and an import via streaming. You can target any SR in your whole XenServer infrastructure (even across different pools!)
|
||||
VM copy allows you to make an export and an import via streaming. You can target any SR in your whole XCP-ng/XenServer infrastructure (even across different pools!)
|
||||
|
||||
### Snapshot management
|
||||
|
||||
@@ -387,7 +387,7 @@ By default, XOA will try to make a snapshot with quiesce. If the VM does not sup
|
||||
|
||||
## VM import and export
|
||||
|
||||
Xen Orchestra can import and export VM's in XVA format (XenServer format) or import OVA files (OVF1 format).
|
||||
Xen Orchestra can import and export VM's in XVA format (XCP-ng/XenServer format) or import OVA files (OVF1 format).
|
||||
|
||||
:::tip
|
||||
We support OVA import from VirtualBox. Feel free to report issues with OVA from other virtualization platforms.
|
||||
@@ -590,7 +590,7 @@ To remove one host from a pool, you can go to the "Advanced" tab of the host pag
|
||||
|
||||
## Visualizations
|
||||
|
||||
Visualizations can help you to understand your XenServer infrastructure, as well as correlate events and detect bottlenecks.
|
||||
Visualizations can help you to understand your XCP-ng/XenServer infrastructure, as well as correlate events and detect bottlenecks.
|
||||
|
||||
:::tip
|
||||
:construction_worker: This section needs to be completed: screenshots and how-to :construction_worker:
|
||||
@@ -608,7 +608,7 @@ You can also update all your hosts (install missing patches) from this page.
|
||||
|
||||
### Parallel Coordinates
|
||||
|
||||
A Parallel Coordinates visualization helps to detect proportions in a hierarchical environment. In a XenServer environment, it's especially useful if you want to see useful information from a large amount of data.
|
||||
A Parallel Coordinates visualization helps to detect proportions in a hierarchical environment. In a XCP-ng/XenServer environment, it's especially useful if you want to see useful information from a large amount of data.
|
||||
|
||||

|
||||
|
||||
@@ -687,7 +687,7 @@ This allows you to enjoy Docker containers displayed directly in Xen Orchestra.
|
||||
|
||||
### Docker plugin installation
|
||||
|
||||
This first step is needed until Docker is supported natively in the XenServer API (XAPI).
|
||||
This first step is needed until Docker is supported natively in the XCP-ng/XenServer API (XAPI).
|
||||
|
||||
:::tip
|
||||
The plugin should be installed on every host you will be using, even if they are on the same pool.
|
||||
|
||||
@@ -6,7 +6,7 @@ Xen Orchestra is an Open Source project created by [Olivier Lambert](https://www
|
||||
|
||||
The idea of Xen Orchestra was origally born in 2009, see the original announcement on [Xen User mailing list](https://lists.xenproject.org/archives/html/xen-users/2009-09/msg00537.html). It worked on Xen and `xend` (now deprecated).
|
||||
|
||||
## XO reboot for XenServer/XCP
|
||||
## XO reboot for XCP-ng/XenServer
|
||||
|
||||
Project was rebooted in the end of 2012, and "pushed" thanks to Lars Kurth. It's also a commercial project since 2016, and now with a team of 6 people dedicated fulltime.
|
||||
|
||||
|
||||
@@ -121,6 +121,16 @@ Content-Type: application/x-ndjson
|
||||
{"name_label":"Debian 10 Cloudinit self-service","power_state":"Halted","url":"/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"}
|
||||
```
|
||||
|
||||
## Task monitoring
|
||||
|
||||
When fetching a task record, the special `wait` query string can be used. If its value is `result` it will wait for the task to be resolved (either success or failure) before returning, otherwise it will wait for the next change of state.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
'https://xo.example.org/rest/v0/tasks/0lr4zljbe?wait=result'
|
||||
```
|
||||
|
||||
## Properties update
|
||||
|
||||
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
|
||||
@@ -302,9 +312,7 @@ curl \
|
||||
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/snapshot'
|
||||
```
|
||||
|
||||
By default, actions are asynchronous and return the reference of the task associated with the request.
|
||||
|
||||
> Tasks monitoring is still under construcration and will come in a future release :)
|
||||
By default, actions are asynchronous and return the reference of the task associated with the request (see [_Task monitoring_](#task-monitoring)).
|
||||
|
||||
The `?sync` flag can be used to run the action synchronously without requiring task monitoring. The result of the action will be returned encoded as JSON:
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ It means you don't have a default SR set on the pool you are importing XOA on. T
|
||||
|
||||
## Unreachable after boot
|
||||
|
||||
XOA uses HVM mode. If your physical host doesn't support virtualization extensions, XOA won't work. To check if your XenServer supports hardware assisted virtualization (HVM), you can enter this command in your host: `grep --color vmx /proc/cpuinfo`. If you don't have any result, it means XOA won't work on this hardware.
|
||||
XOA uses HVM mode. If your physical host doesn't support virtualization extensions, XOA won't work. To check if your XCP-ng/XenServer supports hardware assisted virtualization (HVM), you can enter this command in your host: `grep --color vmx /proc/cpuinfo`. If you don't have any result, it means XOA won't work on this hardware.
|
||||
|
||||
## Set or recover XOA VM password
|
||||
|
||||
@@ -195,7 +195,7 @@ If you have ghost tasks accumulating in your Xen Orchestra you can try the follo
|
||||
|
||||
1. refresh the web page
|
||||
1. disconnect and reconnect the Xen pool/server owning the tasks
|
||||
1. restart the XenAPI Toolstack of the XenServer master
|
||||
1. restart the XenAPI Toolstack of the XCP-ng/XenServer master
|
||||
1. restart xo-server
|
||||
|
||||
### Redownload and rebuild
|
||||
|
||||
@@ -255,7 +255,7 @@ To create a new set of resources to delegate, go to the "Self Service" section i
|
||||
Only an admin can create a set of resources
|
||||
:::
|
||||
|
||||
To allow people to create VMs as they want, we need to give them a _part_ of your XenServer resources (disk space, CPUs, RAM). You can call this "general quotas" if you like. But you first need to decide which resources will be used.
|
||||
To allow people to create VMs as they want, we need to give them a _part_ of your XCP-ng/XenServer resources (disk space, CPUs, RAM). You can call this "general quotas" if you like. But you first need to decide which resources will be used.
|
||||
|
||||
In this example below, we'll create a set called **"sandbox"** with:
|
||||
|
||||
|
||||
@@ -58,18 +58,18 @@ Please only use this if you have issues with [the default way to deploy XOA](ins
|
||||
|
||||
### Via a bash script
|
||||
|
||||
Alternatively, you can deploy it by connecting to your XenServer host and executing the following:
|
||||
Alternatively, you can deploy it by connecting to your XCP-ng/XenServer host and executing the following:
|
||||
|
||||
```sh
|
||||
bash -c "$(wget -qO- https://xoa.io/deploy)"
|
||||
```
|
||||
|
||||
:::tip
|
||||
This won't write or modify anything on your XenServer host: it will just import the XOA VM into your default storage repository.
|
||||
This won't write or modify anything on your XCP-ng/XenServer host: it will just import the XOA VM into your default storage repository.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
If you are using an old XenServer version, you may get a `curl` error:
|
||||
If you are using an old XCP-ng/XenServer version, you may get a `curl` error:
|
||||
|
||||
```
|
||||
curl: (35) error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version
|
||||
|
||||
@@ -39,7 +39,7 @@ In order to work, XOSAN need a minimal set of requirements.
|
||||
|
||||
### Storage
|
||||
|
||||
XOSAN can be deployed on an existing **Local LVM storage**, that XenServer configure by default during its installation. You need 10GiB for XOSAN VM (one on each host) and the rest for XOSAN data, eg all the space left.
|
||||
XOSAN can be deployed on an existing **Local LVM storage**, that XCP-ng/XenServer configure by default during its installation. You need 10GiB for XOSAN VM (one on each host) and the rest for XOSAN data, eg all the space left.
|
||||
|
||||
However, if you have unused disks on your host, you can also create yourself a local LVM storage while using Xen Orchestra:
|
||||
|
||||
@@ -47,7 +47,7 @@ However, if you have unused disks on your host, you can also create yourself a l
|
||||
- Select the host having the disk you want to use for XOSAN
|
||||
- Select "Local LVM" and enter the path of this disk (e.g: `/dev/sdf`)
|
||||
|
||||
> You can discover disks names by issuing `fdisk -l` command on your XenServer host.
|
||||
> You can discover disks names by issuing `fdisk -l` command on your XCP-ng/XenServer host.
|
||||
|
||||
> **Recommended hardware:** we don't have specific hardware recommendation regarding hard disks. It could be directly a disk or even a disk exposed via a hardware RAID. Note that RAID mode will influence global speed of XOSAN.
|
||||
|
||||
@@ -183,13 +183,13 @@ It's very similar to **RAID 10**. In this example, you'll have 300GiB of data us
|
||||
|
||||
#### Examples
|
||||
|
||||
Here is some examples depending of the number of XenServer hosts.
|
||||
Here is some examples depending of the number of XCP-ng/XenServer hosts.
|
||||
|
||||
##### 2 hosts
|
||||
|
||||
This is a kind of special mode. On a 2 nodes setup, one node must know what's happening if it can't contact the other node. This is called a **split brain** scenario. To avoid data loss, it goes on read only. But there is a way to overcome this, with a special node, called **the arbiter**. It will only require an extra VM using only few disk space.
|
||||
|
||||
Thanks to this arbiter, you'll have 3 nodes running on 2 XenServer hosts:
|
||||
Thanks to this arbiter, you'll have 3 nodes running on 2 XCP-ng/XenServer hosts:
|
||||
|
||||
- if the host with 1 node is down, the other host will continue to provide a working XOSAN
|
||||
- if the host with 2 nodes (1 normal and 1 arbiter) id down, the other node will go into read only mode, to avoid split brain scenario.
|
||||
@@ -312,13 +312,13 @@ Once you are ready, you can click on `Create`. XOSAN will automatically deploy i
|
||||
|
||||
## Try it!
|
||||
|
||||
XOSAN is a 100% software defined solution for XenServer hyperconvergence. You can unlock a free 50GiB cluster to test the solution in your infrastructure and discover all the benefits you can get by using XOSAN.
|
||||
XOSAN is a 100% software defined solution for XCP-ng/XenServer hyperconvergence. You can unlock a free 50GiB cluster to test the solution in your infrastructure and discover all the benefits you can get by using XOSAN.
|
||||
|
||||
### Step 1
|
||||
|
||||
You will need to be registered on our website in order to use Xen Orchestra. If you are not yet registered, [here is the way](https://xen-orchestra.com/#!/signup)
|
||||
|
||||
SSH in your XenServer and use the command line `bash -c "$(wget -qO- https://xoa.io/deploy)"` - it will deploy Xen Orchestra Appliance on your XenServer infrastructure which is required to use XOSAN.
|
||||
SSH in your XCP-ng/XenServer and use the command line `bash -c "$(wget -qO- https://xoa.io/deploy)"` - it will deploy Xen Orchestra Appliance on your XCP-ng/XenServer infrastructure which is required to use XOSAN.
|
||||
|
||||
> Note: You can also download the XVA file and follow [these instructions](https://xen-orchestra.com/docs/xoa.html#the-alternative).
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.8.0"
|
||||
"vhd-lib": "^4.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish",
|
||||
|
||||
@@ -45,10 +45,12 @@ exports.createNbdVhdStream = async function createVhdStream(
|
||||
const bufFooter = await readChunkStrict(sourceStream, FOOTER_SIZE)
|
||||
|
||||
const header = unpackHeader(await readChunkStrict(sourceStream, HEADER_SIZE))
|
||||
header.tableOffset = FOOTER_SIZE + HEADER_SIZE
|
||||
// compute BAT in order
|
||||
const batSize = Math.ceil((header.maxTableEntries * 4) / SECTOR_SIZE) * SECTOR_SIZE
|
||||
// skip space between header and beginning of the table
|
||||
await skipStrict(sourceStream, header.tableOffset - (FOOTER_SIZE + HEADER_SIZE))
|
||||
// new table offset
|
||||
header.tableOffset = FOOTER_SIZE + HEADER_SIZE
|
||||
const streamBat = await readChunkStrict(sourceStream, batSize)
|
||||
let offset = FOOTER_SIZE + HEADER_SIZE + batSize
|
||||
// check if parentlocator are ordered
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vhd-lib",
|
||||
"version": "4.8.0",
|
||||
"version": "4.9.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"readable-stream": "^4.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"throttle": "^1.0.3",
|
||||
"vhd-lib": "^4.8.0"
|
||||
"vhd-lib": "^4.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +361,16 @@ export class Xapi extends EventEmitter {
|
||||
if (value === null) {
|
||||
return this.call(`${type}.remove_from_${field}`, ref, entry).then(noop)
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// First, remove any previous value to avoid triggering an unnecessary
|
||||
// `MAP_DUPLICATE_KEY` error which will appear in the XAPI logs
|
||||
//
|
||||
// This is safe because this method does not throw if the entry is missing.
|
||||
//
|
||||
// See https://xcp-ng.org/forum/post/68761
|
||||
await this.call(`${type}.remove_from_${field}`, ref, entry)
|
||||
|
||||
try {
|
||||
await this.call(`${type}.add_to_${field}`, ref, entry, value)
|
||||
return
|
||||
@@ -370,7 +379,6 @@ export class Xapi extends EventEmitter {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
await this.call(`${type}.remove_from_${field}`, ref, entry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,12 @@ async function connect() {
|
||||
|
||||
const xo = new Xo({ rejectUnauthorized: !allowUnauthorized, url: server })
|
||||
await xo.open()
|
||||
await xo.signIn({ token })
|
||||
try {
|
||||
await xo.signIn({ token })
|
||||
} catch (error) {
|
||||
await xo.close()
|
||||
throw error
|
||||
}
|
||||
return xo
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-cli",
|
||||
"version": "0.22.0",
|
||||
"version": "0.24.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Basic CLI for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -29,6 +29,7 @@
|
||||
"node": ">=15.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/read-chunk": "^1.2.0",
|
||||
"chalk": "^5.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
"fs-extra": "^11.1.0",
|
||||
@@ -41,6 +42,7 @@
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"pw": "^0.0.4",
|
||||
"split2": "^4.2.0",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xo-lib": "^0.11.1"
|
||||
},
|
||||
|
||||
@@ -2,10 +2,13 @@ import { basename, join } from 'node:path'
|
||||
import { createWriteStream } from 'node:fs'
|
||||
import { normalize } from 'node:path/posix'
|
||||
import { parse as parseContentType } from 'content-type'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { pipeline } from 'node:stream'
|
||||
import { pipeline as pPipeline } from 'node:stream/promises'
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
import getopts from 'getopts'
|
||||
import hrp from 'http-request-plus'
|
||||
import merge from 'lodash/merge.js'
|
||||
import split2 from 'split2'
|
||||
|
||||
import * as config from './config.mjs'
|
||||
|
||||
@@ -19,6 +22,8 @@ function addPrefix(suffix) {
|
||||
return path
|
||||
}
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
function parseParams(args) {
|
||||
const params = {}
|
||||
for (const arg of args) {
|
||||
@@ -60,7 +65,7 @@ const COMMANDS = {
|
||||
const response = await this.exec(path, { query: parseParams(rest) })
|
||||
|
||||
if (output !== '') {
|
||||
return pipeline(
|
||||
return pPipeline(
|
||||
response,
|
||||
output === '-'
|
||||
? process.stdout
|
||||
@@ -84,6 +89,13 @@ const COMMANDS = {
|
||||
}
|
||||
|
||||
return this.json ? JSON.stringify(result, null, 2) : result
|
||||
} else if (type === 'application/x-ndjson') {
|
||||
const lines = pipeline(response, split2(), noop)
|
||||
let line
|
||||
while ((line = await readChunk(lines)) !== null) {
|
||||
const data = JSON.parse(line)
|
||||
console.log(this.json ? JSON.stringify(data, null, 2) : data)
|
||||
}
|
||||
} else {
|
||||
throw new Error('unsupported content-type ' + type)
|
||||
}
|
||||
@@ -134,6 +146,12 @@ export async function rest(args) {
|
||||
|
||||
const { allowUnauthorized, server, token } = await config.load()
|
||||
|
||||
if (server === undefined) {
|
||||
const errorMessage =
|
||||
'Please use `xo-cli --register` to associate with an XO instance first.\n\nSee `xo-cli --help` for more info.'
|
||||
throw errorMessage
|
||||
}
|
||||
|
||||
const baseUrl = server
|
||||
const baseOpts = {
|
||||
headers: {
|
||||
|
||||
@@ -178,7 +178,7 @@ export default class PerformancePlan extends Plan {
|
||||
const state = this._getThresholdState(exceededAverages)
|
||||
if (
|
||||
destinationAverages.cpu + vmAverages.cpu >= this._thresholds.cpu.low ||
|
||||
destinationAverages.memoryFree - vmAverages.memory <= this._thresholds.cpu.high ||
|
||||
destinationAverages.memoryFree - vmAverages.memory <= this._thresholds.memory.high ||
|
||||
(!state.cpu &&
|
||||
!state.memory &&
|
||||
(exceededAverages.cpu - vmAverages.cpu < destinationAverages.cpu + vmAverages.cpu ||
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
- [Authentication](#authentication)
|
||||
- [Collections](#collections)
|
||||
- [Task monitoring](#task-monitoring)
|
||||
- [Properties update](#properties-update)
|
||||
- [Collections](#collections-1)
|
||||
- [VM destruction](#vm-destruction)
|
||||
- [VM Export](#vm-export)
|
||||
- [VM Import](#vm-import)
|
||||
- [VDI destruction](#vdi-destruction)
|
||||
- [VDI Export](#vdi-export)
|
||||
- [VDI Import](#vdi-import)
|
||||
- [Existing VDI](#existing-vdi)
|
||||
- [New VDI](#new-vdi)
|
||||
- [Actions](#actions)
|
||||
- [Available actions](#available-actions)
|
||||
- [Start an action](#start-an-action)
|
||||
@@ -117,6 +122,16 @@ Content-Type: application/x-ndjson
|
||||
{"name_label":"Debian 10 Cloudinit self-service","power_state":"Halted","url":"/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"}
|
||||
```
|
||||
|
||||
## Task monitoring
|
||||
|
||||
When fetching a task record, the special `wait` query string can be used. If its value is `result` it will wait for the task to be resolved (either success or failure) before returning, otherwise it will wait for the next change of state.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
'https://xo.company.lan/rest/v0/tasks/0lr4zljbe?wait=result'
|
||||
```
|
||||
|
||||
## Properties update
|
||||
|
||||
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
|
||||
@@ -303,9 +318,7 @@ curl \
|
||||
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/snapshot'
|
||||
```
|
||||
|
||||
By default, actions are asynchronous and return the reference of the task associated with the request.
|
||||
|
||||
> Tasks monitoring is still under construcration and will come in a future release :)
|
||||
By default, actions are asynchronous and return the reference of the task associated with the request (see [_Task monitoring_](#task-monitoring)).
|
||||
|
||||
The `?sync` flag can be used to run the action synchronously without requiring task monitoring. The result of the action will be returned encoded as JSON:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.130.0",
|
||||
"version": "5.132.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -128,7 +128,7 @@
|
||||
"unzipper": "^0.10.5",
|
||||
"uuid": "^9.0.0",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^4.8.0",
|
||||
"vhd-lib": "^4.9.0",
|
||||
"ws": "^8.2.3",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^2.0.0",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import filter from 'lodash/filter.js'
|
||||
import find from 'lodash/find.js'
|
||||
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
||||
|
||||
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi/index.mjs'
|
||||
|
||||
@@ -73,12 +74,34 @@ connect.resolve = {
|
||||
// ===================================================================
|
||||
// Reconfigure IP
|
||||
|
||||
export async function reconfigureIp({ pif, mode = 'DHCP', ip = '', netmask = '', gateway = '', dns = '' }) {
|
||||
const xapi = this.getXapi(pif)
|
||||
await xapi.call('PIF.reconfigure_ip', pif._xapiRef, mode, ip, netmask, gateway, dns)
|
||||
if (pif.management) {
|
||||
await xapi.call('host.management_reconfigure', pif._xapiRef)
|
||||
}
|
||||
export async function reconfigureIp({ pif, mode, ip = '', netmask = '', gateway = '', dns = '', ipv6, ipv6Mode }) {
|
||||
const task = this.tasks.create({
|
||||
name: `reconfigure ip of: ${pif.device}`,
|
||||
objectId: pif.uuid,
|
||||
type: 'xo:pif:reconfigureIp',
|
||||
})
|
||||
await task.run(async () => {
|
||||
const xapi = this.getXapi(pif)
|
||||
|
||||
if ((ipv6 !== '' && pif.ipv6?.[0] !== ipv6) || (ipv6Mode !== undefined && ipv6Mode !== pif.ipv6Mode)) {
|
||||
await Task.run(
|
||||
{ properties: { name: 'reconfigure IPv6', mode: ipv6Mode, ipv6, gateway, dns, objectId: pif.uuid } },
|
||||
() => xapi.call('PIF.reconfigure_ipv6', pif._xapiRef, ipv6Mode, ipv6, gateway, dns)
|
||||
)
|
||||
}
|
||||
|
||||
if (mode !== undefined && mode !== pif.mode) {
|
||||
await Task.run(
|
||||
{ properties: { name: 'reconfigure IPv4', mode, ip, netmask, gateway, dns, objectId: pif.uuid } },
|
||||
() => xapi.call('PIF.reconfigure_ip', pif._xapiRef, mode, ip, netmask, gateway, dns)
|
||||
)
|
||||
}
|
||||
if (pif.management) {
|
||||
await Task.run({ properties: { name: 'reconfigure PIF management', objectId: pif.uuid } }, () =>
|
||||
xapi.call('host.management_reconfigure', pif._xapiRef)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reconfigureIp.params = {
|
||||
@@ -88,6 +111,8 @@ reconfigureIp.params = {
|
||||
netmask: { type: 'string', minLength: 0, optional: true },
|
||||
gateway: { type: 'string', minLength: 0, optional: true },
|
||||
dns: { type: 'string', minLength: 0, optional: true },
|
||||
ipv6: { type: 'string', minLength: 0, default: '' },
|
||||
ipv6Mode: { enum: getIpv6ConfigurationModes(), optional: true },
|
||||
}
|
||||
|
||||
reconfigureIp.resolve = {
|
||||
|
||||
@@ -774,6 +774,29 @@ set.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const setAndRestart = defer(async function ($defer, params) {
|
||||
const vm = params.VM
|
||||
const force = extract(params, 'force')
|
||||
|
||||
await stop.bind(this)({ vm, force })
|
||||
|
||||
$defer(start.bind(this), { vm, force })
|
||||
|
||||
return set.bind(this)(params)
|
||||
})
|
||||
|
||||
setAndRestart.params = {
|
||||
// Restart options
|
||||
force: { type: 'boolean', optional: true },
|
||||
|
||||
// Set params
|
||||
...set.params,
|
||||
}
|
||||
|
||||
setAndRestart.resolve = set.resolve
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const restart = defer(async function ($defer, { vm, force = false, bypassBlockedOperation = force }) {
|
||||
const xapi = this.getXapi(vm)
|
||||
if (bypassBlockedOperation) {
|
||||
|
||||
@@ -965,7 +965,9 @@ async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
namespace: 'xosan',
|
||||
version: template.version,
|
||||
})
|
||||
const newVM = await xapi.VM_import(templateStream, this.getObject(lvmsrId, 'SR')._xapiRef)
|
||||
const newVM = await xapi._getOrWaitObject(
|
||||
await xapi.VM_import(templateStream, this.getObject(lvmsrId, 'SR')._xapiRef)
|
||||
)
|
||||
await xapi.editVm(newVM, {
|
||||
autoPoweron: true,
|
||||
name_label: 'XOSAN imported VM',
|
||||
|
||||
@@ -116,8 +116,7 @@ export default class Redis extends Collection {
|
||||
return Promise.all(
|
||||
map(ids, id => {
|
||||
return this.#get(prefix + id).then(model => {
|
||||
// If empty, consider it a no match.
|
||||
if (isEmpty(model)) {
|
||||
if (model === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,12 +196,21 @@ export default class Redis extends Collection {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the record in the database
|
||||
*
|
||||
* Returns undefined if not present.
|
||||
*/
|
||||
async #get(key) {
|
||||
const { redis } = this
|
||||
|
||||
let model
|
||||
try {
|
||||
model = await redis.get(key).then(JSON.parse)
|
||||
const json = await redis.get(key)
|
||||
|
||||
if (json !== null) {
|
||||
model = JSON.parse(json)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!error.message.startsWith('WRONGTYPE')) {
|
||||
throw error
|
||||
|
||||
@@ -562,15 +562,18 @@ const TRANSFORMS = {
|
||||
disallowUnplug: Boolean(obj.disallow_unplug),
|
||||
gateway: obj.gateway,
|
||||
ip: obj.IP,
|
||||
ipv6: obj.IPv6,
|
||||
mac: obj.MAC,
|
||||
management: Boolean(obj.management), // TODO: find a better name.
|
||||
carrier: Boolean(metrics && metrics.carrier),
|
||||
mode: obj.ip_configuration_mode,
|
||||
ipv6Mode: obj.ipv6_configuration_mode,
|
||||
mtu: +obj.MTU,
|
||||
netmask: obj.netmask,
|
||||
// A non physical PIF is a "copy" of an existing physical PIF (same device)
|
||||
// A physical PIF cannot be unplugged
|
||||
physical: Boolean(obj.physical),
|
||||
primaryAddressType: obj.primary_address_type,
|
||||
vlan: +obj.VLAN,
|
||||
speed: metrics && +metrics.speed,
|
||||
$host: link(obj, 'host'),
|
||||
|
||||
@@ -165,14 +165,16 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
async emergencyShutdownHost(hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
const vms = host.$resident_VMs
|
||||
log.debug(`Emergency shutdown: ${host.name_label}`)
|
||||
await asyncMap(vms, vm => {
|
||||
|
||||
await this.call('host.disable', host.$ref)
|
||||
|
||||
await asyncMap(host.$resident_VMs, vm => {
|
||||
if (!vm.is_control_domain) {
|
||||
return ignoreErrors.call(this.callAsync('VM.suspend', vm.$ref))
|
||||
}
|
||||
})
|
||||
await this.call('host.disable', host.$ref)
|
||||
|
||||
await this.callAsync('host.shutdown', host.$ref)
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,129 @@ export default class MigrateVm {
|
||||
return esxi.getAllVmMetadata()
|
||||
}
|
||||
|
||||
@decorateWith(deferrable)
|
||||
async _createVdis($defer, { diskChains, sr, xapi, vm }) {
|
||||
const vdis = {}
|
||||
for (const [node, chainByNode] of Object.entries(diskChains)) {
|
||||
const vdi = await xapi._getOrWaitObject(
|
||||
await xapi.VDI_create({
|
||||
name_description: 'fromESXI' + chainByNode[0].descriptionLabel,
|
||||
name_label: '[ESXI]' + chainByNode[0].nameLabel,
|
||||
SR: sr.$ref,
|
||||
virtual_size: chainByNode[0].capacity,
|
||||
})
|
||||
)
|
||||
// it can fail before the vdi is connected to the vm
|
||||
|
||||
$defer.onFailure.call(xapi, 'VDI_destroy', vdi.$ref)
|
||||
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
})
|
||||
vdis[node] = vdi
|
||||
}
|
||||
return vdis
|
||||
}
|
||||
|
||||
async #instantiateVhd({ esxi, disk, lookMissingBlockInParent = true, parentVhd, thin }) {
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
let vhd
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||
await vhd.readBlockAllocationTable()
|
||||
} else {
|
||||
if (parentVhd === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent VHD`)
|
||||
}
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, { lookMissingBlockInParent })
|
||||
}
|
||||
return vhd
|
||||
}
|
||||
|
||||
async #importDiskChain({ esxi, diskChain, lookMissingBlockInParent = true, parentVhd, thin, vdi }) {
|
||||
let vhd
|
||||
for (let diskIndex = 0; diskIndex < diskChain.length; diskIndex++) {
|
||||
const disk = diskChain[diskIndex]
|
||||
vhd = await this.#instantiateVhd({ esxi, disk, lookMissingBlockInParent, parentVhd, thin })
|
||||
}
|
||||
if (thin || parentVhd !== undefined) {
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
} else {
|
||||
// no transformation when there is no snapshot in thick mode
|
||||
const stream = await vhd.rawContent()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_RAW })
|
||||
}
|
||||
return vhd
|
||||
}
|
||||
|
||||
async #coldImportDiskChainFromEsxi({ esxi, diskChains, isRunning, stopSource, vdis, thin, vmId }) {
|
||||
if (isRunning) {
|
||||
if (stopSource) {
|
||||
// it the vm was running, we stop it and transfer the data in the active disk
|
||||
await Task.run({ properties: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
||||
} else {
|
||||
throw new Error(`can't cold import disk from VM ${vmId} with stopSource disabled `)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(diskChains).map(async ([node, diskChainByNode]) =>
|
||||
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
const vdi = vdis[node]
|
||||
return this.#importDiskChain({ esxi, diskChain: diskChainByNode, thin, vdi })
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async #warmImportDiskChainFromEsxi({ esxi, diskChains, isRunning, stopSource, thin, vdis, vmId }) {
|
||||
if (!isRunning) {
|
||||
return this.#coldImportDiskChainFromEsxi({ esxi, diskChains, isRunning, stopSource, vdis, vmId })
|
||||
}
|
||||
|
||||
const vhds = await Promise.all(
|
||||
// we need to to the cold import on all disks before stoppng the VM and starting to import the last delta
|
||||
Object.entries(diskChains).map(async ([node, chainByNode]) =>
|
||||
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
const vdi = vdis[node]
|
||||
|
||||
// it can be empty if the VM don't have a snapshot
|
||||
// nothing can be warm tranferred
|
||||
if (chainByNode.length === 1) {
|
||||
return
|
||||
}
|
||||
// if the VM is running we'll transfer everything before the last , which is an active disk
|
||||
// the esxi api does not allow us to read an active disk
|
||||
// later we'll stop the VM and transfer this snapshot
|
||||
return this.#importDiskChain({ esxi, diskChain: chainByNode.slice(0, -1), thin, vdi })
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
if (stopSource) {
|
||||
// The vm was running, we stop it and transfer the data in the active disk
|
||||
await Task.run({ properties: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(diskChains).map(async (node, index) => {
|
||||
await Task.run({ properties: { name: `Transfering deltas of ${index}` } }, async () => {
|
||||
const chainByNode = diskChains[node]
|
||||
const vdi = vdis[node]
|
||||
if (vdi === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
||||
}
|
||||
const vhd = vhds[index]
|
||||
return this.#importDiskChain({ esxi, diskChain: chainByNode.slice(-1), parentVhd: vhd, thin, vdi })
|
||||
})
|
||||
})
|
||||
)
|
||||
} else {
|
||||
Task.warning(`Import from VM ${vmId} with stopSource disabled won't contains the data of the mast snapshot`)
|
||||
}
|
||||
}
|
||||
|
||||
@decorateWith(deferrable)
|
||||
async migrationfromEsxi(
|
||||
$defer,
|
||||
@@ -231,96 +354,18 @@ export default class MigrateVm {
|
||||
)
|
||||
return vm
|
||||
})
|
||||
|
||||
$defer.onFailure.call(xapi, 'VM_destroy', vm.$ref)
|
||||
|
||||
const vhds = await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
||||
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const vdi = await xapi._getOrWaitObject(
|
||||
await xapi.VDI_create({
|
||||
name_description: 'fromESXI' + chainByNode[0].descriptionLabel,
|
||||
name_label: '[ESXI]' + chainByNode[0].nameLabel,
|
||||
SR: sr.$ref,
|
||||
virtual_size: chainByNode[0].capacity,
|
||||
})
|
||||
)
|
||||
// it can fail before the vdi is connected to the vm
|
||||
|
||||
$defer.onFailure.call(xapi, 'VDI_destroy', vdi.$ref)
|
||||
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
})
|
||||
let parentVhd, vhd
|
||||
// if the VM is running we'll transfer everything before the last , which is an active disk
|
||||
// the esxi api does not allow us to read an active disk
|
||||
// later we'll stop the VM and transfer this snapshot
|
||||
const nbColdDisks = isRunning ? chainByNode.length - 1 : chainByNode.length
|
||||
for (let diskIndex = 0; diskIndex < nbColdDisks; diskIndex++) {
|
||||
// the first one is a RAW disk ( full )
|
||||
const disk = chainByNode[diskIndex]
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||
await vhd.readBlockAllocationTable()
|
||||
} else {
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd)
|
||||
}
|
||||
parentVhd = vhd
|
||||
}
|
||||
// it can be empty if the VM don't have a snapshot and is running
|
||||
if (vhd !== undefined) {
|
||||
if (thin) {
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
} else {
|
||||
// no transformation when there is no snapshot in thick mode
|
||||
const stream = await vhd.rawContent()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_RAW })
|
||||
}
|
||||
}
|
||||
return { vdi, vhd }
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
if (isRunning && stopSource) {
|
||||
// it the vm was running, we stop it and transfer the data in the active disk
|
||||
await Task.run({ properties: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) => {
|
||||
await Task.run({ properties: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const disk = chainByNode[chainByNode.length - 1]
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
const { vdi, vhd: parentVhd } = vhds[userdevice]
|
||||
let vhd
|
||||
if (vdi === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
||||
}
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||
await vhd.readBlockAllocationTable()
|
||||
} else {
|
||||
if (parentVhd === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent VHD`)
|
||||
}
|
||||
// we only want to transfer blocks present in the delta vhd, not the full vhd chain
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
|
||||
lookMissingBlockInParent: false,
|
||||
})
|
||||
}
|
||||
const stream = vhd.stream()
|
||||
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
const vdis = await this._createVdis({ diskChains: chainsByNodes, sr, xapi, vm })
|
||||
$defer.onFailure.call(async () => Object.values(vdis).map(vdi => vdi && xapi.VDI_destroy(vdi.$ref)))
|
||||
await this.#coldImportDiskChainFromEsxi({
|
||||
esxi,
|
||||
diskChains: chainsByNodes,
|
||||
isRunning,
|
||||
stopSource,
|
||||
thin,
|
||||
vdis,
|
||||
vmId,
|
||||
})
|
||||
|
||||
await Task.run({ properties: { name: 'Finishing transfer' } }, async () => {
|
||||
// remove the importing in label
|
||||
|
||||
@@ -271,13 +271,15 @@ export default class Proxy {
|
||||
[namespace]: { xva },
|
||||
} = await app.getResourceCatalog()
|
||||
const xapi = app.getXapi(srId)
|
||||
const vm = await xapi.VM_import(
|
||||
await app.requestResource({
|
||||
id: xva.id,
|
||||
namespace,
|
||||
version: xva.version,
|
||||
}),
|
||||
srId && this.getObject(srId, 'SR')._xapiRef
|
||||
const vm = await xapi.getOrWaitObject(
|
||||
await xapi.VM_import(
|
||||
await app.requestResource({
|
||||
id: xva.id,
|
||||
namespace,
|
||||
version: xva.version,
|
||||
}),
|
||||
srId && app.getObject(srId, 'SR')._xapiRef
|
||||
)
|
||||
)
|
||||
$defer.onFailure(() => xapi.VM_destroy(vm.$ref))
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"pako": "^2.0.4",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"tar-stream": "^3.1.6",
|
||||
"vhd-lib": "^4.8.0",
|
||||
"vhd-lib": "^4.9.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.130.1",
|
||||
"version": "5.133.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -120,6 +120,7 @@
|
||||
"readable-stream": "^3.0.2",
|
||||
"redux": "^4.0.0",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"relative-luminance": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"rimraf": "^5.0.1",
|
||||
"sass": "^1.38.1",
|
||||
|
||||
@@ -1541,9 +1541,6 @@ export default {
|
||||
// Original text: 'Invalid parameters'
|
||||
configIpErrorTitle: undefined,
|
||||
|
||||
// Original text: 'IP address and netmask required'
|
||||
configIpErrorMessage: undefined,
|
||||
|
||||
// Original text: 'Static IP address'
|
||||
staticIp: undefined,
|
||||
|
||||
|
||||
@@ -1596,9 +1596,6 @@ export default {
|
||||
// Original text: "Invalid parameters"
|
||||
configIpErrorTitle: 'Paramètres invalides',
|
||||
|
||||
// Original text: "IP address and netmask required"
|
||||
configIpErrorMessage: 'Adresse IP et masque de réseau requis',
|
||||
|
||||
// Original text: "Static IP address"
|
||||
staticIp: 'Adresse IP statique',
|
||||
|
||||
|
||||
@@ -1295,9 +1295,6 @@ export default {
|
||||
// Original text: 'Invalid parameters'
|
||||
configIpErrorTitle: undefined,
|
||||
|
||||
// Original text: 'IP address and netmask required'
|
||||
configIpErrorMessage: undefined,
|
||||
|
||||
// Original text: 'Static IP address'
|
||||
staticIp: undefined,
|
||||
|
||||
|
||||
@@ -1491,9 +1491,6 @@ export default {
|
||||
// Original text: "Invalid parameters"
|
||||
configIpErrorTitle: 'Invalid parameters',
|
||||
|
||||
// Original text: "IP address and netmask required"
|
||||
configIpErrorMessage: 'IP cím and netmask required',
|
||||
|
||||
// Original text: "Static IP address"
|
||||
staticIp: 'Static IP cím',
|
||||
|
||||
|
||||
@@ -2387,9 +2387,6 @@ export default {
|
||||
// Original text: 'Invalid parameters'
|
||||
configIpErrorTitle: 'Parametri non validi',
|
||||
|
||||
// Original text: 'IP address and netmask required'
|
||||
configIpErrorMessage: 'Indirizzo IP e maschera di rete richiesti',
|
||||
|
||||
// Original text: 'Static IP address'
|
||||
staticIp: 'Indirizzo IP statico',
|
||||
|
||||
|
||||
@@ -1298,9 +1298,6 @@ export default {
|
||||
// Original text: 'Invalid parameters'
|
||||
configIpErrorTitle: undefined,
|
||||
|
||||
// Original text: 'IP address and netmask required'
|
||||
configIpErrorMessage: undefined,
|
||||
|
||||
// Original text: 'Static IP address'
|
||||
staticIp: undefined,
|
||||
|
||||
|
||||
@@ -1296,9 +1296,6 @@ export default {
|
||||
// Original text: 'Invalid parameters'
|
||||
configIpErrorTitle: undefined,
|
||||
|
||||
// Original text: 'IP address and netmask required'
|
||||
configIpErrorMessage: undefined,
|
||||
|
||||
// Original text: 'Static IP address'
|
||||
staticIp: undefined,
|
||||
|
||||
|
||||
@@ -1542,9 +1542,6 @@ export default {
|
||||
// Original text: 'Invalid parameters'
|
||||
configIpErrorTitle: undefined,
|
||||
|
||||
// Original text: 'IP address and netmask required'
|
||||
configIpErrorMessage: undefined,
|
||||
|
||||
// Original text: 'Static IP address'
|
||||
staticIp: undefined,
|
||||
|
||||
|
||||
@@ -1990,9 +1990,6 @@ export default {
|
||||
// Original text: "Invalid parameters"
|
||||
configIpErrorTitle: 'Geçersiz parametre',
|
||||
|
||||
// Original text: "IP address and netmask required"
|
||||
configIpErrorMessage: 'IP adresi ve ağ maskesi gerekli',
|
||||
|
||||
// Original text: "Static IP address"
|
||||
staticIp: 'Statik IP adresi',
|
||||
|
||||
|
||||
@@ -1072,11 +1072,13 @@ const messages = {
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
pifConfigureIp: 'Configure IP address',
|
||||
configIpErrorTitle: 'Invalid parameters',
|
||||
configIpErrorMessage: 'IP address and netmask required',
|
||||
staticIp: 'Static IP address',
|
||||
staticIpv6: 'Static IPv6 address',
|
||||
netmask: 'Netmask',
|
||||
dns: 'DNS',
|
||||
gateway: 'Gateway',
|
||||
ipRequired: 'An IP address is required',
|
||||
netmaskRequired: 'Netmask required',
|
||||
// ----- Host storage tabs -----
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srType: 'Type',
|
||||
@@ -1454,6 +1456,10 @@ const messages = {
|
||||
vmDefaultBootFirmwareLabel: 'default (bios)',
|
||||
vmBootFirmwareWarningMessage:
|
||||
"You're about to change your boot firmware. This is still experimental in CH/XCP-ng 8.0. Are you sure you want to continue?",
|
||||
setAndRestartVmFailed: 'Error on restarting and setting the VM: {vm}',
|
||||
vmEditAndRestartModalTitle: 'VM is currently running',
|
||||
vmEditAndRestartModalMessage:
|
||||
'This VM is currently running, and needs to be stopped to modify this value. Restart VM and modify this value?',
|
||||
|
||||
// ----- VM placeholders -----
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ const ADDITIONAL_FILES_FETCH_TIMEOUT = 5e3
|
||||
const jsonStringify = json => JSON.stringify(json, null, 2)
|
||||
const logger = createLogger('report-bug-button')
|
||||
|
||||
const GITHUB_URL = 'https://github.com/vatesfr/xen-orchestra/issues/new/choose'
|
||||
const GITHUB_URL = 'https://github.com/vatesfr/xen-orchestra/issues/new'
|
||||
const GITHUB_BUG_TEMPLATE = 'bug_report.yml'
|
||||
const XO_SUPPORT_URL = 'https://xen-orchestra.com/#!/member/support'
|
||||
const SUPPORT_PANEL_URL = './api/support/create/ticket'
|
||||
|
||||
@@ -42,12 +43,18 @@ const ADDITIONAL_FILES = [
|
||||
]
|
||||
|
||||
const reportInNewWindow = (url, { title, message, formatMessage = identity }) => {
|
||||
// message is ignored for the moment
|
||||
//
|
||||
// see https://github.com/vatesfr/xen-orchestra/issues/7142
|
||||
|
||||
const encodedTitle = encodeURIComponent(title == null ? '' : title)
|
||||
window.open(`${url}?title=${encodedTitle}`)
|
||||
|
||||
let _url = `${url}?title=${encodedTitle}`
|
||||
|
||||
if (url === GITHUB_URL) {
|
||||
const encodedErrorMessage = encodeURIComponent(jsonStringify(message.error))
|
||||
const encodedLabels = encodeURIComponent('type: bug :bug:,status: triaging :triangular_flag_on_post:')
|
||||
|
||||
_url += `&template=${GITHUB_BUG_TEMPLATE}&labels=${encodedLabels}&error-message=${encodedErrorMessage}`
|
||||
}
|
||||
|
||||
window.open(_url)
|
||||
}
|
||||
|
||||
export const reportOnSupportPanel = async ({ files = [], formatMessage = identity, message, title } = {}) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import map from 'lodash/map'
|
||||
import pFinally from 'promise-toolbox/finally'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import relativeLuminance from 'relative-luminance'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
@@ -16,26 +17,12 @@ import { SelectTag } from './select-objects'
|
||||
const INPUT_STYLE = {
|
||||
maxWidth: '8em',
|
||||
}
|
||||
const TAG_STYLE = {
|
||||
backgroundColor: '#2598d9',
|
||||
borderRadius: '0.5em',
|
||||
color: 'white',
|
||||
fontSize: '0.6em',
|
||||
margin: '0.2em',
|
||||
marginTop: '-0.1em',
|
||||
padding: '0.3em',
|
||||
verticalAlign: 'middle',
|
||||
}
|
||||
const LINK_STYLE = {
|
||||
cursor: 'pointer',
|
||||
}
|
||||
const ADD_TAG_STYLE = {
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
fontSize: '0.8em',
|
||||
marginLeft: '0.2em',
|
||||
}
|
||||
const REMOVE_TAG_STYLE = {
|
||||
cursor: 'pointer',
|
||||
verticalAlign: 'middle',
|
||||
}
|
||||
|
||||
class SelectExistingTag extends Component {
|
||||
@@ -128,19 +115,26 @@ export default class Tags extends Component {
|
||||
const deleteTag = (onDelete || onChange) && this._deleteTag
|
||||
|
||||
return (
|
||||
<span className='form-group' style={{ color: '#999' }}>
|
||||
<Icon icon='tags' />{' '}
|
||||
<span>
|
||||
<div style={{ color: '#999', display: 'inline-block' }}>
|
||||
<div style={{ display: 'inline-block', verticalAlign: 'middle' }}>
|
||||
<Icon icon='tags' />
|
||||
</div>
|
||||
<div style={{ display: 'inline-block', fontSize: '0.6em', verticalAlign: 'middle' }}>
|
||||
{map(labels.sort(), (label, index) => (
|
||||
<Tag label={label} onDelete={deleteTag} key={index} onClick={onClick} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
{(onAdd || onChange) && !this.state.editing ? (
|
||||
<span onClick={this._startEdit} style={ADD_TAG_STYLE}>
|
||||
<div onClick={this._startEdit} style={ADD_TAG_STYLE}>
|
||||
<Icon icon='add-tag' />
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className='form-inline' onBlur={this._closeEditionIfUnfocused} onFocus={this._focus}>
|
||||
<div
|
||||
style={{ display: 'inline-block', verticalAlign: 'middle' }}
|
||||
className='form-inline'
|
||||
onBlur={this._closeEditionIfUnfocused}
|
||||
onFocus={this._focus}
|
||||
>
|
||||
<span className='input-group'>
|
||||
<input autoFocus className='form-control' onKeyDown={this._onKeyDown} style={INPUT_STYLE} type='text' />
|
||||
<span className='input-group-btn'>
|
||||
@@ -149,27 +143,97 @@ export default class Tags extends Component {
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const Tag = ({ type, label, onDelete, onClick }) => (
|
||||
<span style={TAG_STYLE}>
|
||||
<span onClick={onClick && (() => onClick(label))} style={onClick && LINK_STYLE}>
|
||||
{label}
|
||||
</span>{' '}
|
||||
{onDelete ? (
|
||||
<span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
|
||||
<Icon icon='remove-tag' />
|
||||
</span>
|
||||
) : (
|
||||
[]
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
export const Tag = ({
|
||||
type,
|
||||
label,
|
||||
onDelete,
|
||||
onClick,
|
||||
|
||||
// must be in format #rrggbb for luminance parsing
|
||||
color = '#2598d9',
|
||||
}) => {
|
||||
const borderSize = '0.2em'
|
||||
const padding = '0.2em'
|
||||
|
||||
const isLight =
|
||||
relativeLuminance(
|
||||
Array.from({ length: 3 }, (_, i) => {
|
||||
const j = i * 2 + 1
|
||||
return parseInt(color.slice(j, j + 2), 16)
|
||||
})
|
||||
) > 0.5
|
||||
|
||||
const i = label.indexOf('=')
|
||||
const isScoped = i !== -1
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: color,
|
||||
border: borderSize + ' solid ' + color,
|
||||
borderRadius: '0.5em',
|
||||
color: isLight ? '#000' : '#fff',
|
||||
display: 'inline-block',
|
||||
margin: '0.2em',
|
||||
|
||||
// prevent value background from breaking border radius
|
||||
overflow: 'clip',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={onClick && (() => onClick(label))}
|
||||
style={{
|
||||
cursor: onClick && 'pointer',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding,
|
||||
}}
|
||||
>
|
||||
{isScoped ? label.slice(0, i) : label}
|
||||
</div>
|
||||
{isScoped && (
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
color: '#000',
|
||||
display: 'inline-block',
|
||||
padding,
|
||||
}}
|
||||
>
|
||||
{label.slice(i + 1) || <i>N/A</i>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{onDelete && (
|
||||
<div
|
||||
onClick={onDelete && (() => onDelete(label))}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
padding,
|
||||
|
||||
// if isScoped, the display is a bit different
|
||||
background: isScoped && '#fff',
|
||||
color: isScoped && (isLight ? '#000' : color),
|
||||
}}
|
||||
>
|
||||
<Icon icon='remove-tag' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Tag.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
noHostsAvailable,
|
||||
operationBlocked,
|
||||
operationFailed,
|
||||
vmBadPowerState,
|
||||
vmLacksFeature,
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
@@ -33,7 +34,7 @@ import store from 'store'
|
||||
import WarmMigrationModal from './warm-migration-modal'
|
||||
import { alert, chooseAction, confirm } from '../modal'
|
||||
import { error, info, success } from '../notification'
|
||||
import { getObject } from 'selectors'
|
||||
import { getObject, isAdmin } from 'selectors'
|
||||
import { getXoaPlan, SOURCES } from '../xoa-plans'
|
||||
import { noop, resolveId, resolveIds } from '../utils'
|
||||
import {
|
||||
@@ -359,7 +360,11 @@ export const subscribeRemotes = createSubscription(() => _call('remote.getAll'))
|
||||
|
||||
export const subscribeRemotesInfo = createSubscription(() => _call('remote.getAllInfo'))
|
||||
|
||||
export const subscribeProxies = createSubscription(() => _call('proxy.getAll'))
|
||||
export const subscribeProxies = createSubscription(() => {
|
||||
const _isAdmin = isAdmin(store.getState())
|
||||
|
||||
return _isAdmin ? _call('proxy.getAll') : undefined
|
||||
})
|
||||
|
||||
export const subscribeResourceSets = createSubscription(() => _call('resourceSet.getAll'))
|
||||
|
||||
@@ -563,7 +568,11 @@ subscribeVolumeInfo.forceRefresh = (() => {
|
||||
}
|
||||
})()
|
||||
|
||||
export const subscribeSrsUnhealthyVdiChainsLength = createSubscription(() => _call('sr.getAllUnhealthyVdiChainsLength'))
|
||||
export const subscribeSrsUnhealthyVdiChainsLength = createSubscription(() => {
|
||||
const _isAdmin = isAdmin(store.getState())
|
||||
|
||||
return _isAdmin ? _call('sr.getAllUnhealthyVdiChainsLength') : undefined
|
||||
})
|
||||
|
||||
const unhealthyVdiChainsLengthSubscriptionsBySr = {}
|
||||
export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
|
||||
@@ -1812,8 +1821,28 @@ export const editVm = async (vm, props) => {
|
||||
}
|
||||
}
|
||||
await _call('vm.set', { ...props, id: resolveId(vm) })
|
||||
.catch(err => {
|
||||
error(_('setVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }), err.message)
|
||||
.catch(async err => {
|
||||
if (vmBadPowerState.is(err, { actual: 'running' }) || err.message === 'Cannot change memory on running VM') {
|
||||
try {
|
||||
const force = await chooseAction({
|
||||
body: <p>{_('vmEditAndRestartModalMessage')}</p>,
|
||||
buttons: [
|
||||
{ label: _('rebootVmLabel'), value: false, btnStyle: 'success' },
|
||||
{ label: _('forceRebootVmLabel'), value: true, btnStyle: 'danger' },
|
||||
],
|
||||
icon: 'vm-reboot',
|
||||
title: _('vmEditAndRestartModalTitle'),
|
||||
})
|
||||
await _call('vm.setAndRestart', { ...props, id: resolveId(vm), force })
|
||||
} catch (err) {
|
||||
if (err !== undefined) {
|
||||
error(_('setAndRestartVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }), err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error(_('setVmFailed', { vm: renderXoItemFromId(resolveId(vm)) }), err.message)
|
||||
}
|
||||
})
|
||||
::tap(subscribeResourceSets.forceRefresh)
|
||||
}
|
||||
@@ -2244,11 +2273,13 @@ export const deletePifs = pifs =>
|
||||
body: _('deletePifsConfirm', { nPifs: pifs.length }),
|
||||
}).then(() => Promise.all(map(pifs, pif => _call('pif.delete', { pif: resolveId(pif) }))), noop)
|
||||
|
||||
export const reconfigurePifIp = (pif, { mode, ip, netmask, gateway, dns }) =>
|
||||
export const reconfigurePifIp = (pif, { mode, ip, ipv6, ipv6Mode, netmask, gateway, dns }) =>
|
||||
_call('pif.reconfigureIp', {
|
||||
pif: resolveId(pif),
|
||||
mode,
|
||||
ip,
|
||||
ipv6,
|
||||
ipv6Mode,
|
||||
netmask,
|
||||
gateway,
|
||||
dns,
|
||||
@@ -2256,6 +2287,8 @@ export const reconfigurePifIp = (pif, { mode, ip, netmask, gateway, dns }) =>
|
||||
|
||||
export const getIpv4ConfigModes = () => _call('pif.getIpv4ConfigurationModes')
|
||||
|
||||
export const getIpv6ConfigModes = () => _call('pif.getIpv6ConfigurationModes')
|
||||
|
||||
export const editPif = (pif, { vlan }) => _call('pif.editPif', { pif: resolveId(pif), vlan })
|
||||
|
||||
export const scanHostPifs = hostId => _call('host.scanPifs', { host: hostId })
|
||||
@@ -2596,7 +2629,7 @@ export const listVmBackups = remotes => _call('backupNg.listVmBackups', { remote
|
||||
export const restoreBackup = (
|
||||
backup,
|
||||
sr,
|
||||
{ generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false, useDifferentialRestore= false } = {}
|
||||
{ generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false, useDifferentialRestore = false } = {}
|
||||
) => {
|
||||
const promise = _call('backupNg.importVmBackup', {
|
||||
id: resolveId(backup),
|
||||
|
||||
@@ -335,9 +335,9 @@ export default class HostItem extends Component {
|
||||
{host.productBrand} {host.version}
|
||||
</Col>
|
||||
<Col mediumSize={3} className={styles.itemExpanded}>
|
||||
<span style={{ fontSize: '1.4em' }}>
|
||||
<div style={{ fontSize: '1.4em' }}>
|
||||
<HomeTags type='host' labels={host.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6} className={styles.itemExpanded}>
|
||||
<MiniStats fetch={this._fetchStats} />
|
||||
|
||||
@@ -265,9 +265,9 @@ export default class PoolItem extends Component {
|
||||
{master.productBrand} {master.version}
|
||||
</Col>
|
||||
<Col mediumSize={5}>
|
||||
<span style={{ fontSize: '1.4em' }}>
|
||||
<div style={{ fontSize: '1.4em' }}>
|
||||
<HomeTags type='pool' labels={pool.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={3} className={styles.itemExpanded}>
|
||||
<span>
|
||||
|
||||
@@ -192,9 +192,9 @@ export default class SrItem extends Component {
|
||||
{sr.VDIs.length}x <Icon icon='disk' />
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<span style={{ fontSize: '1.4em' }}>
|
||||
<div style={{ fontSize: '1.4em' }}>
|
||||
<HomeTags type='SR' labels={sr.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
)}
|
||||
|
||||
@@ -87,9 +87,9 @@ export default class TemplateItem extends Component {
|
||||
))}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<span style={{ fontSize: '1.4em' }}>
|
||||
<div style={{ fontSize: '1.4em' }}>
|
||||
<HomeTags type='VM-template' labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -209,9 +209,9 @@ export default class VmItem extends Component {
|
||||
))}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<span style={{ fontSize: '1.4em' }}>
|
||||
<div style={{ fontSize: '1.4em' }}>
|
||||
<HomeTags type='VM' labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6} className={styles.itemExpanded}>
|
||||
{this._isRunning && <MiniStats fetch={this._fetchStats} />}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
editNetwork,
|
||||
editPif,
|
||||
getIpv4ConfigModes,
|
||||
getIpv6ConfigModes,
|
||||
reconfigurePifIp,
|
||||
scanHostPifs,
|
||||
} from 'xo'
|
||||
@@ -41,7 +42,10 @@ class ConfigureIpModal extends Component {
|
||||
|
||||
const { pif } = props
|
||||
if (pif) {
|
||||
this.state = pick(pif, ['ip', 'netmask', 'dns', 'gateway'])
|
||||
this.state = {
|
||||
...pick(pif, ['ip', 'netmask', 'dns', 'gateway']),
|
||||
ipv6: pif.ipv6?.[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +54,7 @@ class ConfigureIpModal extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ip, netmask, dns, gateway } = this.state
|
||||
const { ip, ipv6, netmask, dns, gateway } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -61,6 +65,13 @@ class ConfigureIpModal extends Component {
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('staticIpv6')}</Col>
|
||||
<Col size={6}>
|
||||
<input className='form-control' onChange={this.linkState('ipv6')} value={ipv6} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('netmask')}</Col>
|
||||
<Col size={6}>
|
||||
@@ -111,14 +122,19 @@ const reconfigureIp = (pif, mode) => {
|
||||
title: _('pifConfigureIp'),
|
||||
body: <ConfigureIpModal pif={pif} />,
|
||||
}).then(params => {
|
||||
if (!params.ip || !params.netmask) {
|
||||
error(_('configIpErrorTitle'), _('configIpErrorMessage'))
|
||||
return
|
||||
if (params.ip === undefined && params.ipv6 === undefined) {
|
||||
return error(_('configIpErrorTitle'), _('ipRequired'))
|
||||
}
|
||||
if (params.ip !== undefined && params.netmask === undefined) {
|
||||
return error(_('configIpErrorTitle'), _('netmaskRequired'))
|
||||
}
|
||||
if (params.ipv6 !== undefined) {
|
||||
params.ipv6Mode = mode
|
||||
}
|
||||
return reconfigurePifIp(pif, { mode, ...params })
|
||||
}, noop)
|
||||
}
|
||||
return reconfigurePifIp(pif, { mode })
|
||||
return reconfigurePifIp(pif, { [pif.primaryAddressType === 'IPv6' ? 'ipv6Mode' : 'mode']: mode })
|
||||
}
|
||||
|
||||
class PifItemIp extends Component {
|
||||
@@ -126,7 +142,7 @@ class PifItemIp extends Component {
|
||||
|
||||
render() {
|
||||
const { pif } = this.props
|
||||
const pifIp = pif.ip
|
||||
const pifIp = pif.primaryAddressType === 'IPv6' ? pif.ipv6?.[0] : pif.ip
|
||||
return (
|
||||
<div>
|
||||
{pifIp}{' '}
|
||||
@@ -143,26 +159,41 @@ class PifItemIp extends Component {
|
||||
class PifItemMode extends Component {
|
||||
state = { configModes: [] }
|
||||
|
||||
componentDidMount() {
|
||||
getIpv4ConfigModes().then(configModes => this.setState({ configModes }))
|
||||
async componentDidMount() {
|
||||
const [ipv4ConfigModes, ipv6ConfigModes] = await Promise.all([getIpv4ConfigModes(), getIpv6ConfigModes()])
|
||||
this.setState({ ipv4ConfigModes, ipv6ConfigModes })
|
||||
}
|
||||
|
||||
_configIp = mode => mode != null && reconfigureIp(this.props.pif, mode.value)
|
||||
|
||||
_isIpv6 = createSelector(
|
||||
() => this.props.pif.primaryAddressType,
|
||||
primaryAddressType => primaryAddressType === 'IPv6'
|
||||
)
|
||||
|
||||
_getOptions = createSelector(
|
||||
() => this.state.configModes,
|
||||
configModes => configModes.map(mode => ({ label: mode, value: mode }))
|
||||
this._isIpv6,
|
||||
() => this.state.ipv4ConfigModes,
|
||||
() => this.state.ipv6ConfigModes,
|
||||
(isIpv6, ipv4ConfigModes, ipv6ConfigModes) =>
|
||||
(isIpv6 ? ipv6ConfigModes : ipv4ConfigModes).map(mode => ({ label: mode, value: mode }))
|
||||
)
|
||||
|
||||
_getValue = createSelector(
|
||||
this._isIpv6,
|
||||
() => this.props.pif.mode,
|
||||
mode => ({ label: mode, value: mode })
|
||||
() => this.props.pif.ipv6Mode,
|
||||
(isIpv6, mode, ipv6Mode) => {
|
||||
mode = isIpv6 ? ipv6Mode : mode
|
||||
return { label: mode, value: mode }
|
||||
}
|
||||
)
|
||||
|
||||
render() {
|
||||
const isIpv6 = this._isIpv6()
|
||||
return (
|
||||
<Select onChange={this._configIp} options={this._getOptions()} value={this._getValue()}>
|
||||
{this.props.pif.mode}
|
||||
{this.props.pif[isIpv6 ? 'ipv6Mode' : 'mode']}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -9654,6 +9654,11 @@ eslint@^8.7.0:
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
esm@^3.0.84:
|
||||
version "3.2.25"
|
||||
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
|
||||
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
|
||||
|
||||
espree@^9.0.0, espree@^9.3.1, espree@^9.6.0, espree@^9.6.1:
|
||||
version "9.6.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
|
||||
@@ -18394,6 +18399,13 @@ relateurl@0.2.x, relateurl@^0.2.7:
|
||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
|
||||
|
||||
relative-luminance@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/relative-luminance/-/relative-luminance-2.0.1.tgz#2babddf3a5a59673227d6f02e0f68e13989e3d13"
|
||||
integrity sha512-wFuITNthJilFPwkK7gNJcULxXBcfFZvZORsvdvxeOdO44wCeZnuQkf3nFFzOR/dpJNxYsdRZJLsepWbyKhnMww==
|
||||
dependencies:
|
||||
esm "^3.0.84"
|
||||
|
||||
release-zalgo@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730"
|
||||
@@ -19478,7 +19490,7 @@ split2@^3.0.0:
|
||||
dependencies:
|
||||
readable-stream "^3.0.0"
|
||||
|
||||
split2@^4.0.0, split2@^4.1.0:
|
||||
split2@^4.0.0, split2@^4.1.0, split2@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
|
||||
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
|
||||
|
||||
Reference in New Issue
Block a user