Compare commits

..

10 Commits

Author SHA1 Message Date
Florent BEAUCHAMP
d5853645bc fixes: plugin now work with vdi-tools plugin 2023-10-05 14:43:04 +02:00
Florent BEAUCHAMP
b80a3449e5 wip 2023-09-27 15:44:13 +02:00
Florent BEAUCHAMP
f5a2cfebf1 code cleanup 2023-09-25 15:26:41 +02:00
Florent BEAUCHAMP
b195e1e854 rounding is hard 2023-09-25 15:26:41 +02:00
Florent BEAUCHAMP
3c8bc08681 fix parent locator and bat length 2023-09-25 15:26:41 +02:00
Florent BEAUCHAMP
d201c26a68 more fixes 2023-09-25 15:26:41 +02:00
Florent BEAUCHAMP
5008a9c822 fixes 2023-09-25 15:26:41 +02:00
Florent Beauchamp
75dcea9e86 feat(vhd-cli): implement deeper checks for vhd 2023-09-25 15:26:41 +02:00
Florent BEAUCHAMP
767ac4e738 feat: vhd synthetic 2023-09-25 15:26:41 +02:00
Florent BEAUCHAMP
1066cdea32 feat(backup): use tcp direct transfer - DO NOT MERGE 2023-09-25 15:26:41 +02:00
140 changed files with 1192 additions and 3390 deletions

View File

@@ -13,15 +13,12 @@ describe('decorateWith', () => {
const expectedFn = Function.prototype
const newFn = () => {}
const decorator = decorateWith(
function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
const decorator = decorateWith(function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
return newFn
},
...expectedArgs
)
return newFn
}, ...expectedArgs)
const descriptor = {
configurable: true,

View File

@@ -22,7 +22,7 @@
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.6.1"
"vhd-lib": "^4.5.0"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -7,7 +7,7 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/backups": "^0.42.0",
"@xen-orchestra/fs": "^4.1.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.13",
"version": "1.0.12",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -31,11 +31,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
throw new Error('cannot backup a VM created by this very job')
}
const currentOperations = Object.values(vm.current_operations)
if (currentOperations.some(_ => _ === 'migrate_send' || _ === 'pool_migrate')) {
throw new Error('cannot backup a VM currently being migrated')
}
this.config = config
this.job = job
this.remoteAdapters = remoteAdapters
@@ -261,15 +256,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
if (this._writers.size !== 0) {
const { pool_migrate = null, migrate_send = null } = this._exportedVm.blocked_operations
const reason = 'VM migration is blocked during backup'
await this._exportedVm.update_blocked_operations({ pool_migrate: reason, migrate_send: reason })
try {
await this._copy()
} finally {
await this._exportedVm.update_blocked_operations({ pool_migrate, migrate_send })
}
await this._copy()
}
} finally {
if (startAfter) {

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.43.0",
"version": "0.42.0",
"engines": {
"node": ">=14.18"
},
@@ -44,7 +44,7 @@
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.6.1",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.6",
"yazl": "^2.5.1"
},
@@ -56,7 +56,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^3.2.0"
"@xen-orchestra/xapi": "^3.1.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -17,7 +17,7 @@
"xo-fs": "./cli.js"
},
"engines": {
"node": ">=15"
"node": ">=14.13"
},
"dependencies": {
"@aws-sdk/abort-controller": "^3.272.0",

View File

@@ -1,74 +0,0 @@
const noop = Function.prototype
const BARRIER = Symbol('runAsync.BARRIER')
export default function runAsync(stack, worker, { concurrency = 10, signal, stopOnError = true } = {}) {
return new Promise((resolve, reject) => {
const errors = []
let onAbort
let working = 0
if (signal !== undefined) {
onAbort = () => {
stop(reject, signal.reason)
}
signal.addEventListener('abort', onAbort)
}
function stop(cb, arg) {
if (run !== noop) {
run = noop
if (signal !== undefined) {
signal.removeEventListener('abort', onAbort)
}
cb(arg)
}
}
function push(...args) {
stack.push(...args)
run()
}
async function runOne(entry) {
++working
try {
await worker(entry, push)
} catch (error) {
if (stopOnError) {
stop(reject, error)
} else {
errors.push(error)
}
}
--working
run()
}
let run = function () {
if (stack.length === 0) {
if (working === 0) {
if (errors.length !== 0) {
stop(reject, new AggregateError(errors))
} else {
stop(resolve)
}
}
return
}
while (working < concurrency && stack.length !== 0) {
const entry = stack.pop()
if (entry === BARRIER) {
if (working === 0) {
continue
} else {
stack.push(entry)
return
}
}
runOne(entry)
}
}
run()
})
}
runAsync.BARRIER = BARRIER

View File

@@ -1,5 +1,6 @@
import assert from 'assert'
import getStream from 'get-stream'
import { asyncEach } from '@vates/async-each'
import { coalesceCalls } from '@vates/coalesce-calls'
import { createLogger } from '@xen-orchestra/log'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
@@ -12,7 +13,6 @@ import { synchronized } from 'decorator-synchronized'
import { basename, dirname, normalize as normalizePath } from './path'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
import { DEFAULT_ENCRYPTION_ALGORITHM, _getEncryptor } from './_encryptor'
import runAsync from './_runAsync.js'
const { info, warn } = createLogger('xo:fs:abstract')
@@ -98,6 +98,7 @@ export default class RemoteHandlerAbstract {
const sharedLimit = limitConcurrency(options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS)
this.closeFile = sharedLimit(this.closeFile)
this.copy = sharedLimit(this.copy)
this.exists = sharedLimit(this.exists)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
@@ -315,7 +316,7 @@ export default class RemoteHandlerAbstract {
return p
}
async __rmdir(dir) {
async rmdir(dir) {
await timeout.call(this._rmdir(normalizePath(dir)).catch(ignoreEnoent), this._timeout)
}
@@ -323,6 +324,14 @@ export default class RemoteHandlerAbstract {
await this._rmtree(normalizePath(dir))
}
async _exists(file){
throw new Error('not implemented')
}
async exists(file){
return this._exists(normalizePath(file))
}
// Asks the handler to sync the state of the effective remote with its'
// metadata
//
@@ -614,29 +623,26 @@ export default class RemoteHandlerAbstract {
}
async _rmtree(dir) {
await runAsync([dir], async (entry, push) => {
try {
await this.__unlink(entry)
} catch (error) {
const { code } = error
try {
return await this._rmdir(dir)
} catch (error) {
if (error.code !== 'ENOTEMPTY') {
throw error
}
}
const files = await this._list(dir)
await asyncEach(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (code !== 'EISDIR' && code === 'EPERM') {
throw error
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
try {
await this.__rmdir(entry)
} catch (error) {
if (error.code !== 'ENOTEMPTY') {
throw error
}
push(entry, runAsync.BARRIER, ...(await this.__list(entry, { prependDir: true })))
}
}
})
throw error
})
)
return this._rmtree(dir)
}
// called to initialize the remote

View File

@@ -206,4 +206,9 @@ export default class LocalHandler extends RemoteHandlerAbstract {
_writeFile(file, data, { flags }) {
return this.#addSyncStackTrace(fs.writeFile, this.getFilePath(file), data, { flag: flags })
}
async _exists(file){
const exists = await fs.pathExists(this._getFilePath(file))
return exists
}
}

View File

@@ -421,4 +421,17 @@ export default class S3Handler extends RemoteHandlerAbstract {
useVhdDirectory() {
return true
}
async _exists(file){
try{
await this._s3.send(new HeadObjectCommand(this._createParams(file)))
return true
}catch(error){
// normalize this error code
if (error.name === 'NoSuchKey') {
return false
}
throw error
}
}
}

View File

@@ -2,10 +2,6 @@
## **next**
- Ability to migrate selected VMs to another host (PR [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040))
- Ability to snapshot selected VMs (PR [#7021](https://github.com/vatesfr/xen-orchestra/pull/7021))
- Add Patches to Pool Dashboard (PR [#6709](https://github.com/vatesfr/xen-orchestra/pull/6709))
## **0.1.3** (2023-09-01)
- Add Alarms to Pool Dashboard (PR [#6976](https://github.com/vatesfr/xen-orchestra/pull/6976))

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@@ -14,66 +14,66 @@
</UiActionButton>
</UiFilterGroup>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<UiModal
v-if="isOpen"
:icon="faFilter"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
>
{{ property }}
</UiBadge>
</div>
</div>
</template>
{{ property }}
</UiBadge>
</div>
</div>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
</UiModal>
</template>
<script lang="ts" setup>
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
import type { Filters, NewFilter } from "@/types/filter";
import { faFilter, faPlus } from "@fortawesome/free-solid-svg-icons";
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiBadge from "@/components/ui/UiBadge.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { getFilterIcon } from "@/libs/utils";
import type { Filters, NewFilter } from "@/types/filter";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
defineProps<{
activeFilters: string[];
@@ -85,7 +85,7 @@ const emit = defineEmits<{
(event: "removeFilter", filter: string): void;
}>();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const { isOpen, open, close } = useModal();
const newFilters = ref<NewFilter[]>([]);
let newFilterId = 0;
@@ -156,6 +156,11 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>
@@ -185,10 +190,4 @@ const handleSubmit = () => {
margin-top: 0.6rem;
gap: 0.5rem;
}
.rows {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -219,6 +219,7 @@ const valueInputAfter = computed(() =>
.collection-filter-row {
display: flex;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid var(--background-color-secondary);
gap: 1rem;
@@ -241,8 +242,4 @@ const valueInputAfter = computed(() =>
.form-widget-advanced {
flex: 1;
}
.ui-action-button:first-of-type {
margin-left: auto;
}
</style>

View File

@@ -17,56 +17,56 @@
</UiActionButton>
</UiFilterGroup>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
</template>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
<UiModal
v-if="isOpen"
:icon="faSort"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
</UiModal>
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import type { ActiveSorts, Sorts } from "@/types/sort";
import {
faCaretDown,
faCaretUp,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { ref } from "vue";
@@ -81,7 +81,7 @@ const emit = defineEmits<{
(event: "removeSort", property: string): void;
}>();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const { isOpen, open, close } = useModal();
const newSortProperty = ref();
const newSortIsAscending = ref<boolean>(true);
@@ -96,6 +96,11 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>

View File

@@ -28,9 +28,13 @@
</tr>
</thead>
<tbody>
<tr v-for="item in filteredAndSortedCollection" :key="item.$ref">
<tr v-for="item in filteredAndSortedCollection" :key="item[idProperty]">
<td v-if="isSelectable">
<input v-model="selected" :value="item.$ref" type="checkbox" />
<input
v-model="selected"
:value="item[props.idProperty]"
type="checkbox"
/>
</td>
<slot :item="item" name="body-row" />
</tr>
@@ -38,7 +42,10 @@
</UiTable>
</template>
<script generic="T extends XenApiRecord<any>" lang="ts" setup>
<script lang="ts" setup>
import { computed, toRef, watch } from "vue";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
import CollectionFilter from "@/components/CollectionFilter.vue";
import CollectionSorter from "@/components/CollectionSorter.vue";
import UiTable from "@/components/ui/UiTable.vue";
@@ -47,20 +54,17 @@ import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useMultiSelect from "@/composables/multi-select.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiRecord } from "@/libs/xen-api/xen-api.types";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
import { computed, toRef, watch } from "vue";
const props = defineProps<{
modelValue?: T["$ref"][];
modelValue?: string[];
availableFilters?: Filters;
availableSorts?: Sorts;
collection: T[];
collection: Record<string, any>[];
idProperty: string;
}>();
const emit = defineEmits<{
(event: "update:modelValue", selectedRefs: T["$ref"][]): void;
(event: "update:modelValue", selectedRefs: string[]): void;
}>();
const isSelectable = computed(() => props.modelValue !== undefined);
@@ -81,10 +85,12 @@ const filteredAndSortedCollection = useSortedCollection(
compareFn
);
const usableRefs = computed(() => props.collection.map((item) => item["$ref"]));
const usableRefs = computed(() =>
props.collection.map((item) => item[props.idProperty])
);
const selectableRefs = computed(() =>
filteredAndSortedCollection.value.map((item) => item["$ref"])
filteredAndSortedCollection.value.map((item) => item[props.idProperty])
);
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);

View File

@@ -1,71 +0,0 @@
<template>
<UiCardSpinner v-if="!areSomeLoaded" />
<UiTable v-else class="hosts-patches-table" :class="{ desktop: isDesktop }">
<tr v-for="patch in sortedPatches" :key="patch.$id">
<th>{{ patch.name }}</th>
<td>
<div class="version">
{{ patch.version }}
<template v-if="hasMultipleHosts">
<UiSpinner v-if="!areAllLoaded" />
<UiCounter
v-else
v-tooltip="{
placement: 'left',
content: $t('n-hosts-awaiting-patch', {
n: patch.$hostRefs.size,
}),
}"
:value="patch.$hostRefs.size"
class="counter"
color="error"
/>
</template>
</div>
</td>
</tr>
</UiTable>
</template>
<script lang="ts" setup>
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTable from "@/components/ui/UiTable.vue";
import type { XenApiPatchWithHostRefs } from "@/composables/host-patches.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import { useUiStore } from "@/stores/ui.store";
import { computed } from "vue";
const props = defineProps<{
patches: XenApiPatchWithHostRefs[];
hasMultipleHosts: boolean;
areAllLoaded: boolean;
areSomeLoaded: boolean;
}>();
const sortedPatches = computed(() =>
[...props.patches].sort(
(patch1, patch2) => patch1.changelog.date - patch2.changelog.date
)
);
const { isDesktop } = useUiStore();
</script>
<style lang="postcss" scoped>
.hosts-patches-table.desktop {
max-width: 45rem;
}
.version {
display: flex;
gap: 1rem;
justify-content: flex-end;
align-items: center;
}
.counter {
font-size: 1rem;
}
</style>

View File

@@ -66,8 +66,8 @@ onUnmounted(() => {
store.value?.unsubscribe(subscriptionId);
});
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(
() => store.value?.getByUuid(props.uuid as any)
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(() =>
store.value?.getByUuid(props.uuid as any)
);
const isReady = computed(() => {

View File

@@ -4,7 +4,7 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import {
faMoon,
faPause,
@@ -15,14 +15,14 @@ import {
import { computed } from "vue";
const props = defineProps<{
state: VM_POWER_STATE;
state: POWER_STATE;
}>();
const icons = {
[VM_POWER_STATE.RUNNING]: faPlay,
[VM_POWER_STATE.PAUSED]: faPause,
[VM_POWER_STATE.SUSPENDED]: faMoon,
[VM_POWER_STATE.HALTED]: faStop,
[POWER_STATE.RUNNING]: faPlay,
[POWER_STATE.PAUSED]: faPause,
[POWER_STATE.SUSPENDED]: faMoon,
[POWER_STATE.HALTED]: faStop,
};
const icon = computed(() => icons[props.state] ?? faQuestion);

View File

@@ -1,58 +1,45 @@
<template>
<UiModal v-model="isSslModalOpen" color="error">
<ConfirmModalLayout :icon="faServer">
<template #title>{{ $t("unreachable-hosts") }}</template>
<template #default>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
</template>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="closeSslModal">{{ $t("cancel") }}</UiButton>
</template>
</ConfirmModalLayout>
<UiModal
v-if="isSslModalOpen"
:icon="faServer"
color="error"
@close="clearUnreachableHostsUrls"
>
<template #title>{{ $t("unreachable-hosts") }}</template>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
</template>
</UiModal>
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import useModal from "@/composables/modal.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import UiModal from "@/components/ui/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { computed, ref, watch } from "vue";
import { difference } from "lodash-es";
import { ref, watch } from "vue";
const { records: hosts } = useHostCollection();
const unreachableHostsUrls = ref<Set<string>>(new Set());
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
const reload = () => window.location.reload();
const { isOpen: isSslModalOpen, close: closeSslModal } = useModal({
onClose: () => unreachableHostsUrls.value.clear(),
});
watch(
() => unreachableHostsUrls.value.size,
(size) => {
isSslModalOpen.value = size > 0;
},
{ immediate: true }
);
watch(hosts, (nextHosts, previousHosts) => {
difference(nextHosts, previousHosts).forEach((host) => {
const url = new URL("http://localhost");
@@ -66,11 +53,7 @@ watch(hosts, (nextHosts, previousHosts) => {
</script>
<style lang="postcss" scoped>
.description {
text-align: center;
p {
margin: 1rem 0;
}
.description p {
margin: 1rem 0;
}
</style>

View File

@@ -58,7 +58,7 @@ const getDefaultOpenedDirectories = (): Set<string> => {
}
const openedDirectories = new Set<string>();
const parts = currentRoute.path.split("/").slice(2);
const parts = currentRoute.path.split("/");
let currentPath = "";
for (const part of parts) {

View File

@@ -1,8 +1,6 @@
<template>
<UiModal v-model="isRawValueModalOpen">
<BasicModalLayout>
<CodeHighlight :code="rawValueModalPayload" />
</BasicModalLayout>
<UiModal v-if="isRawValueModalOpen" @close="closeRawValueModal">
<CodeHighlight :code="rawValueModalPayload" />
</UiModal>
<StoryParamsTable>
<thead>
@@ -101,8 +99,7 @@ import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import StoryWidget from "@/components/component-story/StoryWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
@@ -133,6 +130,7 @@ const model = useVModel(props, "modelValue", emit);
const {
open: openRawValueModal,
close: closeRawValueModal,
isOpen: isRawValueModalOpen,
payload: rawValueModalPayload,
} = useModal<string>();

View File

@@ -4,7 +4,7 @@
v-if="label !== undefined || learnMoreUrl !== undefined"
class="label-container"
>
<label :class="{ light }" :for="id" class="label">
<label :for="id" class="label">
<UiIcon :icon="icon" />
{{ label }}
</label>
@@ -58,7 +58,6 @@ const props = withDefaults(
error?: string;
help?: string;
disabled?: boolean;
light?: boolean;
}>(),
{ disabled: undefined }
);
@@ -96,24 +95,14 @@ useContext(DisabledContext, () => props.disabled);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.label {
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
font-size: 1.4rem;
padding: 1rem 0;
&.light {
font-size: 1.6rem;
color: var(--color-blue-scale-300);
font-weight: 400;
}
&:not(.light) {
font-size: 1.4rem;
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
}
}
.messages-container {

View File

@@ -1,28 +1,20 @@
<template>
<UiModal
v-model="isCodeModalOpen"
@submit.prevent="saveJson"
:color="isJsonValid ? 'success' : 'error'"
closable
v-if="isCodeModalOpen"
:icon="faCode"
@close="closeCodeModal"
>
<FormModalLayout @submit.prevent="saveJson" :icon="faCode">
<template #default>
<FormTextarea class="modal-textarea" v-model="editedJson" />
</template>
<template #buttons>
<UiButton transparent @click="formatJson">
{{ $t("reformat") }}
</UiButton>
<UiButton outlined @click="closeCodeModal">
{{ $t("cancel") }}
</UiButton>
<UiButton :disabled="!isJsonValid" type="submit">
{{ $t("save") }}
</UiButton>
</template>
</FormModalLayout>
<FormTextarea class="modal-textarea" v-model="editedJson" />
<template #buttons>
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
<UiButton :disabled="!isJsonValid" type="submit"
>{{ $t("save") }}
</UiButton>
</template>
</UiModal>
<FormInput
@click="openCodeModal"
:model-value="jsonValue"
@@ -34,9 +26,8 @@
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import FormTextarea from "@/components/form/FormTextarea.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { useVModel, whenever } from "@vueuse/core";

View File

@@ -41,11 +41,11 @@ import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { percent } from "@/libs/utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
const ACTIVE_STATES = new Set([VM_POWER_STATE.RUNNING, VM_POWER_STATE.PAUSED]);
const ACTIVE_STATES = new Set([POWER_STATE.RUNNING, POWER_STATE.PAUSED]);
const {
hasError: hostStoreHasError,

View File

@@ -1,41 +0,0 @@
<template>
<UiCard>
<UiCardTitle class="patches-title">
{{ $t("patches") }}
<template v-if="areAllLoaded" #right>
{{ $t("n-missing", { n: count }) }}
</template>
</UiCardTitle>
<div class="table-container">
<HostPatches
:are-all-loaded="areAllLoaded"
:are-some-loaded="areSomeLoaded"
:has-multiple-hosts="hosts.length > 1"
:patches="patches"
/>
</div>
</UiCard>
</template>
<script lang="ts" setup>
import HostPatches from "@/components/HostPatchesTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostPatches } from "@/composables/host-patches.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { records: hosts } = useHostCollection();
const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts);
</script>
<style lang="postcss" scoped>
.patches-title {
--section-title-right-color: var(--color-red-vates-base);
}
.table-container {
max-height: 40rem;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<Teleport to="body">
<form
:class="className"
class="ui-modal"
v-bind="$attrs"
@click.self="emit('close')"
>
<div class="container">
<span v-if="onClose" class="close-icon" @click="emit('close')">
<UiIcon :icon="faXmark" />
</span>
<div v-if="icon || $slots.icon" class="modal-icon">
<slot name="icon">
<UiIcon :icon="icon" />
</slot>
</div>
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
<div v-if="$slots.default" class="content">
<slot />
</div>
<UiButtonGroup :color="color">
<slot name="buttons" />
</UiButtonGroup>
</div>
</form>
</Teleport>
</template>
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useMagicKeys, whenever } from "@vueuse/core";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
color?: "info" | "warning" | "error" | "success";
onClose?: () => void;
}>(),
{ color: "info" }
);
const emit = defineEmits<{
(event: "close"): void;
}>();
const { escape } = useMagicKeys();
whenever(escape, () => emit("close"));
const className = computed(() => {
return [`color-${props.color}`, { "has-icon": props.icon !== undefined }];
});
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background-color: #00000080;
}
.color-success {
--modal-color: var(--color-green-infra-base);
--modal-background-color: var(--background-color-green-infra);
}
.color-info {
--modal-color: var(--color-extra-blue-base);
--modal-background-color: var(--background-color-extra-blue);
}
.color-warning {
--modal-color: var(--color-orange-world-base);
--modal-background-color: var(--background-color-orange-world);
}
.color-error {
--modal-color: var(--color-red-vates-base);
--modal-background-color: var(--background-color-red-vates);
}
.container {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
min-width: 40rem;
padding: 4.2rem;
text-align: center;
border-radius: 1rem;
background-color: var(--modal-background-color);
box-shadow: var(--shadow-400);
}
.close-icon {
font-size: 2rem;
position: absolute;
top: 1.5rem;
right: 2rem;
padding: 0.2rem 0.5rem;
cursor: pointer;
color: var(--modal-color);
}
.container :deep(.accent) {
color: var(--modal-color);
}
.modal-icon {
font-size: 4.8rem;
margin: 2rem 0;
color: var(--modal-color);
}
.ui-title {
margin-top: 4rem;
.has-icon & {
margin-top: 0;
}
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
.content {
overflow: auto;
font-size: 1.6rem;
max-height: calc(100vh - 40rem);
margin-top: 2rem;
}
.ui-button-group {
margin-top: 4rem;
}
</style>

View File

@@ -1,28 +0,0 @@
<template>
<UiIcon
:class="textClass"
:icon="faXmark"
class="modal-close-icon"
@click="close"
/>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
const { textClass } = useContext(ColorContext);
const close = inject(IK_MODAL_CLOSE, undefined);
</script>
<style lang="postcss" scoped>
.modal-close-icon {
font-size: 2rem;
cursor: pointer;
}
</style>

View File

@@ -1,83 +0,0 @@
<template>
<component
:is="tag"
:class="[backgroundClass, { nested: isNested }]"
class="modal-container"
>
<header v-if="$slots.header" class="modal-header">
<slot name="header" />
</header>
<main v-if="$slots.default" class="modal-content">
<slot name="default" />
</main>
<footer v-if="$slots.footer" class="modal-footer">
<slot name="footer" />
</footer>
</component>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_NESTED } from "@/types/injection-keys";
import { inject, provide } from "vue";
const props = withDefaults(
defineProps<{
tag?: string;
color?: Color;
}>(),
{ tag: "div" }
);
defineSlots<{
header: () => any;
default: () => any;
footer: () => any;
}>();
const { backgroundClass } = useContext(ColorContext, () => props.color);
const isNested = inject(IK_MODAL_NESTED, false);
provide(IK_MODAL_NESTED, true);
</script>
<style lang="postcss" scoped>
.modal-container {
display: grid;
grid-template-rows: 1fr auto 1fr;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 20rem);
padding: 2rem;
gap: 1rem;
border-radius: 1rem;
font-size: 1.6rem;
&:not(.nested) {
min-width: 40rem;
box-shadow: var(--shadow-400);
}
&.nested {
margin-top: 2rem;
}
}
.modal-header {
grid-row: 1;
}
.modal-content {
text-align: center;
grid-row: 2;
padding: 2rem;
max-height: 75vh;
overflow: auto;
}
.modal-footer {
grid-row: 3;
align-self: end;
}
</style>

View File

@@ -1,57 +0,0 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="ui-modal" @click.self="close">
<slot />
</div>
</Teleport>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { useMagicKeys, useVModel, whenever } from "@vueuse/core/index";
import { provide } from "vue";
const props = defineProps<{
modelValue: boolean;
color?: Color;
}>();
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
}>();
const isOpen = useVModel(props, "modelValue", emit);
const close = () => (isOpen.value = false);
provide(IK_MODAL_CLOSE, close);
useContext(ColorContext, () => props.color);
const { escape } = useMagicKeys();
whenever(escape, () => close());
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background: rgba(26, 27, 56, 0.25);
flex-direction: column;
gap: 2rem;
font-size: 1.6rem;
font-weight: 400;
}
</style>

View File

@@ -1,26 +0,0 @@
<template>
<ModalContainer>
<template #header>
<ModalCloseIcon class="close-icon" />
</template>
<template #default>
<slot />
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
defineSlots<{
default: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-icon {
float: right;
}
</style>

View File

@@ -1,77 +0,0 @@
<template>
<ModalContainer tag="form">
<template #header>
<div class="close-bar">
<ModalCloseIcon />
</div>
</template>
<template #default>
<UiIcon :class="textClass" :icon="icon" class="main-icon" />
<div v-if="$slots.title || $slots.subtitle" class="titles">
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
</div>
<div v-if="$slots.default">
<slot name="default" />
</div>
</template>
<template #footer>
<UiButtonGroup>
<slot name="buttons" />
</UiButtonGroup>
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{
icon?: IconDefinition;
}>();
const { textClass } = useContext(ColorContext);
defineSlots<{
title: () => void;
subtitle: () => void;
default: () => void;
buttons: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-bar {
text-align: right;
}
.main-icon {
font-size: 4.8rem;
margin-bottom: 2rem;
}
.titles {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
</style>

View File

@@ -1,71 +0,0 @@
<template>
<ModalContainer tag="form">
<template #header>
<div :class="borderClass" class="title-bar">
<UiIcon :class="textClass" :icon="icon" />
<slot name="title" />
<ModalCloseIcon class="close-icon" />
</div>
</template>
<template #default>
<slot />
</template>
<template #footer>
<UiButtonGroup class="footer-buttons">
<slot name="buttons" />
</UiButtonGroup>
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
disabled?: boolean;
}>(),
{ disabled: undefined }
);
defineSlots<{
title: () => void;
default: () => void;
buttons: () => void;
}>();
const { textClass, borderClass } = useContext(ColorContext);
useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>
.title-bar {
display: flex;
border-bottom-width: 1px;
border-bottom-style: solid;
font-size: 2.4rem;
gap: 1rem;
padding-bottom: 1rem;
font-weight: 500;
align-items: center;
}
.close-icon {
margin-left: auto;
align-self: flex-start;
}
.footer-buttons {
justify-content: flex-end;
}
</style>

View File

@@ -14,7 +14,7 @@
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCopy } from "@fortawesome/free-solid-svg-icons";
@@ -36,7 +36,7 @@ const areAllSelectedVmsHalted = computed(
() =>
selectedVms.value.length > 0 &&
selectedVms.value.every(
(selectedVm) => selectedVm.power_state === VM_POWER_STATE.HALTED
(selectedVm) => selectedVm.power_state === POWER_STATE.HALTED
)
);

View File

@@ -1,49 +1,48 @@
<template>
<MenuItem
v-tooltip="areSomeVmsInExecution && $t('selected-vms-in-execution')"
:disabled="isDisabled"
:disabled="areSomeVmsInExecution"
:icon="faTrashCan"
@click="openDeleteModal"
>
{{ $t("delete") }}
</MenuItem>
<UiModal v-model="isDeleteModalOpen">
<ConfirmModalLayout :icon="faSatellite">
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
</ConfirmModalLayout>
<UiModal
v-if="isDeleteModalOpen"
:icon="faSatellite"
@close="closeDeleteModal"
>
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
</UiModal>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import { useContext } from "@/composables/context.composable";
import useModal from "@/composables/modal.composable";
import { ColorContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { faSatellite, faTrashCan } from "@fortawesome/free-solid-svg-icons";
@@ -66,11 +65,7 @@ const vms = computed<XenApiVm[]>(() =>
);
const areSomeVmsInExecution = computed(() =>
vms.value.some((vm) => vm.power_state !== VM_POWER_STATE.HALTED)
);
const isDisabled = computed(
() => vms.value.length === 0 || areSomeVmsInExecution.value
vms.value.some((vm) => vm.power_state !== POWER_STATE.HALTED)
);
const deleteVms = async () => {

View File

@@ -1,95 +0,0 @@
<template>
<MenuItem
v-tooltip="
!areAllVmsMigratable && $t('some-selected-vms-can-not-be-migrated')
"
:busy="isMigrating"
:disabled="isParentDisabled || !areAllVmsMigratable"
:icon="faRoute"
@click="openModal"
>
{{ $t("migrate") }}
</MenuItem>
<UiModal v-model="isModalOpen">
<FormModalLayout :disabled="isMigrating" @submit.prevent="handleMigrate">
<template #title>
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</template>
<div>
<FormInputWrapper :label="$t('select-destination-host')" light>
<FormSelect v-model="selectedHost">
<option :value="undefined">
{{ $t("select-destination-host") }}
</option>
<option
v-for="host in availableHosts"
:key="host.$ref"
:value="host"
>
{{ host.name_label }}
</option>
</FormSelect>
</FormInputWrapper>
</div>
<template #buttons>
<UiButton outlined @click="closeModal">
{{ isMigrating ? $t("close") : $t("cancel") }}
</UiButton>
<UiButton :busy="isMigrating" :disabled="!isValid" type="submit">
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</UiButton>
</template>
</FormModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useContext } from "@/composables/context.composable";
import useModal from "@/composables/modal.composable";
import { useVmMigration } from "@/composables/vm-migration.composable";
import { DisabledContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { faRoute } from "@fortawesome/free-solid-svg-icons";
const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
}>();
const isParentDisabled = useContext(DisabledContext);
const {
open: openModal,
isOpen: isModalOpen,
close: closeModal,
} = useModal({
onClose: () => (selectedHost.value = undefined),
});
const {
selectedHost,
availableHosts,
isValid,
migrate,
isMigrating,
areAllVmsMigratable,
} = useVmMigration(() => props.selectedRefs);
const handleMigrate = async () => {
try {
await migrate();
closeModal();
} catch (e) {
console.error("Error while migrating", e);
}
};
</script>

View File

@@ -100,7 +100,7 @@ import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { useXenApiStore } from "@/stores/xen-api.store";
import {
faCirclePlay,
@@ -136,16 +136,16 @@ const vmRefsWithPowerState = computed(() =>
const xenApi = useXenApiStore().getXapi();
const areVmsRunning = computed(() =>
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
vms.value.every((vm) => vm.power_state === POWER_STATE.RUNNING)
);
const areVmsHalted = computed(() =>
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.HALTED)
vms.value.every((vm) => vm.power_state === POWER_STATE.HALTED)
);
const areVmsSuspended = computed(() =>
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.SUSPENDED)
vms.value.every((vm) => vm.power_state === POWER_STATE.SUSPENDED)
);
const areVmsPaused = computed(() =>
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.PAUSED)
vms.value.every((vm) => vm.power_state === POWER_STATE.PAUSED)
);
const areOperationsPending = (operation: VM_OPERATION | VM_OPERATION[]) =>
@@ -179,7 +179,7 @@ const areVmsBusyToForceShutdown = computed(() =>
areOperationsPending(VM_OPERATION.HARD_SHUTDOWN)
);
const getHostState = (host: XenApiHost) =>
isHostRunning(host) ? VM_POWER_STATE.RUNNING : VM_POWER_STATE.HALTED;
isHostRunning(host) ? POWER_STATE.RUNNING : POWER_STATE.HALTED;
</script>
<style lang="postcss" scoped>

View File

@@ -1,52 +0,0 @@
<template>
<MenuItem
:busy="areSomeVmsSnapshoting"
:disabled="isDisabled"
:icon="faCamera"
@click="handleSnapshot"
>
{{ $t("snapshot") }}
</MenuItem>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCamera } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();
const vms = computed(() =>
props.vmRefs
.map((vmRef) => getByOpaqueRef(vmRef))
.filter((vm): vm is XenApiVm => vm !== undefined)
);
const areSomeVmsSnapshoting = computed(() =>
vms.value.some((vm) => isOperationPending(vm, VM_OPERATION.SNAPSHOT))
);
const isDisabled = computed(
() => vms.value.length === 0 || areSomeVmsSnapshoting.value
);
const handleSnapshot = () => {
const vmRefsToSnapshot = Object.fromEntries(
vms.value.map((vm) => [
vm.$ref,
`${vm.name_label}_${new Date().toISOString()}`,
])
);
return useXenApiStore().getXapi().vm.snapshot(vmRefsToSnapshot);
};
</script>
<style lang="postcss" scoped></style>

View File

@@ -15,12 +15,16 @@
<VmActionPowerStateItems :vm-refs="selectedRefs" />
</template>
</MenuItem>
<VmActionMigrateItem :selected-refs="selectedRefs" />
<MenuItem v-tooltip="$t('coming-soon')" :icon="faRoute">
{{ $t("migrate") }}
</MenuItem>
<VmActionCopyItem :selected-refs="selectedRefs" />
<MenuItem v-tooltip="$t('coming-soon')" :icon="faEdit">
{{ $t("edit-config") }}
</MenuItem>
<VmActionSnapshotItem :vm-refs="selectedRefs" />
<MenuItem v-tooltip="$t('coming-soon')" :icon="faCamera">
{{ $t("snapshot") }}
</MenuItem>
<VmActionExportItem :vm-refs="selectedRefs" />
<VmActionDeleteItem :vm-refs="selectedRefs" />
</AppMenu>
@@ -31,18 +35,18 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useUiStore } from "@/stores/ui.store";
import {
faCamera,
faEdit,
faEllipsis,
faPowerOff,
faRoute,
} from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";

View File

@@ -1,95 +0,0 @@
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useHostStore } from "@/stores/xen-api/host.store";
import type { XenApiPatch } from "@/types/xen-api";
import { type Pausable, useTimeoutPoll, watchArray } from "@vueuse/core";
import { computed, type MaybeRefOrGetter, reactive, toValue } from "vue";
export type XenApiPatchWithHostRefs = XenApiPatch & { $hostRefs: Set<string> };
type HostConfig = {
timeoutPoll: Pausable;
patches: XenApiPatch[];
isLoaded: boolean;
};
export const useHostPatches = (hosts: MaybeRefOrGetter<XenApiHost[]>) => {
const hostStore = useHostStore();
const configByHost = reactive(new Map<string, HostConfig>());
const fetchHostPatches = async (hostRef: XenApiHost["$ref"]) => {
if (!configByHost.has(hostRef)) {
return;
}
const config = configByHost.get(hostRef)!;
config.patches = await hostStore.fetchMissingPatches(hostRef);
config.isLoaded = true;
};
const registerHost = (hostRef: XenApiHost["$ref"]) => {
if (configByHost.has(hostRef)) {
return;
}
const timeoutPoll = useTimeoutPoll(() => fetchHostPatches(hostRef), 10000, {
immediate: true,
});
configByHost.set(hostRef, {
timeoutPoll,
patches: [],
isLoaded: false,
});
};
const unregisterHost = (hostRef: string) => {
configByHost.get(hostRef)?.timeoutPoll.pause();
configByHost.delete(hostRef);
};
watchArray(
() => toValue(hosts).map((host) => host.$ref),
(_n, _p, addedRefs, removedRefs) => {
addedRefs.forEach((ref) => registerHost(ref));
removedRefs?.forEach((ref) => unregisterHost(ref));
},
{ immediate: true }
);
const patches = computed(() => {
const records = new Map<string, XenApiPatchWithHostRefs>();
configByHost.forEach(({ patches }, hostRef) => {
patches.forEach((patch) => {
const record = records.get(patch.$id);
if (record !== undefined) {
return record.$hostRefs.add(hostRef);
}
records.set(patch.$id, {
...patch,
$hostRefs: new Set([hostRef]),
});
});
});
return Array.from(records.values());
});
const count = computed(() => patches.value.length);
const areAllLoaded = computed(() =>
Array.from(configByHost.values()).every((config) => config.isLoaded)
);
const areSomeLoaded = computed(
() =>
areAllLoaded.value ||
Array.from(configByHost.values()).some((config) => config.isLoaded)
);
return { patches, count, areAllLoaded, areSomeLoaded };
};

View File

@@ -1,57 +1,16 @@
# useModal composable
### Usage
#### API
`useModal<T>(options: ModalOptions)`
Type parameter:
- `T`: The type for the modal's payload.
Parameters:
- `options`: An optional object of type `ModalOptions`.
Returns an object with:
- `payload: ReadOnly<Ref<T | undefined>>`: The payload data of the modal. Mainly used if a single modal is used for
multiple items (typically with `v-for`)
- `isOpen: WritableComputedRef<boolean>`: A writable computed indicating if the modal is open or not.
- `open(currentPayload?: T)`: A function to open the modal and optionally set its payload.
- `close(force = false)`: A function to close the modal. If force is set to `true`, the modal will be closed without
calling the `confirmClose` callback.
#### Types
`ModalOptions`
An object type that accepts:
- `confirmClose?: () => boolean`: An optional callback that is called before the modal is closed. If this function
returns `false`, the modal will not be closed.
- `onClose?: () => void`: An optional callback that is called after the modal is closed.
### Example
```vue
<template>
<div v-for="item in items">
{{ item.name }}
<button @click="openRemoveModal(item)">Delete</button>
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
</div>
<UiModal v-model="isRemoveModalOpen">
<ModalContainer>
<template #header>
Are you sure you want to delete {{ removeModalPayload.name }}?
</template>
<template #footer>
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
</template>
</ModalContainer>
<UiModal v-if="isRemoveModalOpen">
Are you sure you want to delete {{ removeModalPayload.name }}
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
</UiModal>
</template>
@@ -63,11 +22,7 @@ const {
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal({
confirmClose: () =>
window.confirm("Are you sure you want to close this modal?"),
onClose: () => console.log("Modal closed"),
});
} = useModal();
async function handleRemove() {
await removeItem(removeModalPayload.id);

View File

@@ -1,11 +1,6 @@
import { computed, readonly, ref } from "vue";
import { ref } from "vue";
type ModalOptions = {
confirmClose?: () => boolean;
onClose?: () => void;
};
export default function useModal<T>(options: ModalOptions = {}) {
export default function useModal<T>() {
const $payload = ref<T>();
const $isOpen = ref(false);
@@ -13,35 +8,15 @@ export default function useModal<T>(options: ModalOptions = {}) {
$isOpen.value = true;
$payload.value = payload;
};
const close = (force = false) => {
if (!force && options.confirmClose?.() === false) {
return;
}
if (options.onClose) {
options.onClose();
}
const close = () => {
$isOpen.value = false;
$payload.value = undefined;
};
const isOpen = computed({
get() {
return $isOpen.value;
},
set(value) {
if (value) {
open();
} else {
close();
}
},
});
return {
payload: readonly($payload),
isOpen,
payload: $payload,
isOpen: $isOpen,
open,
close,
};

View File

@@ -1,82 +0,0 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { castArray } from "lodash-es";
import type { MaybeRefOrGetter } from "vue";
import { computed, ref, toValue } from "vue";
export const useVmMigration = (
vmRefs: MaybeRefOrGetter<XenApiVm["$ref"] | XenApiVm["$ref"][]>
) => {
const $isMigrating = ref(false);
const selectedHost = ref<XenApiHost>();
const { getByOpaqueRef: getVm } = useVmCollection();
const { records: hosts } = useHostCollection();
const vms = computed(
() =>
castArray(toValue(vmRefs))
.map((vmRef) => getVm(vmRef))
.filter((vm) => vm !== undefined) as XenApiVm[]
);
const isMigrating = computed(
() =>
$isMigrating.value ||
vms.value.some((vm) =>
Object.values(vm.current_operations).some(
(operation) => operation === VM_OPERATION.POOL_MIGRATE
)
)
);
const availableHosts = computed(() => {
return hosts.value
.filter((host) => vms.value.some((vm) => vm.resident_on !== host.$ref))
.sort(sortRecordsByNameLabel);
});
const areAllVmsMigratable = computed(() =>
vms.value.every((vm) =>
vm.allowed_operations.includes(VM_OPERATION.POOL_MIGRATE)
)
);
const isValid = computed(
() =>
!isMigrating.value &&
vms.value.length > 0 &&
selectedHost.value !== undefined
);
const migrate = async () => {
if (!isValid.value) {
return;
}
try {
$isMigrating.value = true;
const hostRef = selectedHost.value!.$ref;
const xapi = useXenApiStore().getXapi();
await xapi.vm.migrate(
vms.value.map((vm) => vm.$ref),
hostRef
);
} finally {
$isMigrating.value = false;
}
};
return {
isMigrating,
availableHosts,
selectedHost,
areAllVmsMigratable,
isValid,
migrate,
};
};

View File

@@ -12,12 +12,12 @@ export type MixinAbstractConstructor<T = unknown> = abstract new (
export type MixinFunction<
T extends MixinConstructor | MixinAbstractConstructor = MixinConstructor,
R extends T = T & MixinConstructor,
R extends T = T & MixinConstructor
> = (Base: T) => R;
export type MixinReturnValue<
T extends MixinConstructor | MixinAbstractConstructor,
M extends MixinFunction<T, any>[],
M extends MixinFunction<T, any>[]
> = UnionToIntersection<
| T
| {
@@ -27,7 +27,7 @@ export type MixinReturnValue<
export default function mixin<
T extends MixinConstructor | MixinAbstractConstructor,
M extends MixinFunction<T, any>[],
M extends MixinFunction<T, any>[]
>(Base: T, ...mixins: M): MixinReturnValue<T, M> {
return mixins.reduce(
(mix, applyMixin) => applyMixin(mix),

View File

@@ -1,493 +0,0 @@
export enum TASK_ALLOWED_OPERATION {
CANCEL = "cancel",
DESTROY = "destroy",
}
export enum TASK_STATUS_TYPE {
CANCELLED = "cancelled",
CANCELLING = "cancelling",
FAILURE = "failure",
PENDING = "pending",
SUCCESS = "success",
}
export enum EVENT_OPERATION {
ADD = "add",
DEL = "del",
MOD = "mod",
}
export enum POOL_ALLOWED_OPERATION {
APPLY_UPDATES = "apply_updates",
CERT_REFRESH = "cert_refresh",
CLUSTER_CREATE = "cluster_create",
CONFIGURE_REPOSITORIES = "configure_repositories",
COPY_PRIMARY_HOST_CERTS = "copy_primary_host_certs",
DESIGNATE_NEW_MASTER = "designate_new_master",
EXCHANGE_CA_CERTIFICATES_ON_JOIN = "exchange_ca_certificates_on_join",
EXCHANGE_CERTIFICATES_ON_JOIN = "exchange_certificates_on_join",
GET_UPDATES = "get_updates",
HA_DISABLE = "ha_disable",
HA_ENABLE = "ha_enable",
SYNC_UPDATES = "sync_updates",
TLS_VERIFICATION_ENABLE = "tls_verification_enable",
}
export enum TELEMETRY_FREQUENCY {
DAILY = "daily",
MONTHLY = "monthly",
WEEKLY = "weekly",
}
export enum UPDATE_SYNC_FREQUENCY {
DAILY = "daily",
WEEKLY = "weekly",
}
export enum AFTER_APPLY_GUIDANCE {
RESTART_HOST = "restartHost",
RESTART_HVM = "restartHVM",
RESTART_PV = "restartPV",
RESTART_XAPI = "restartXAPI",
}
export enum UPDATE_AFTER_APPLY_GUIDANCE {
RESTART_HOST = "restartHost",
RESTART_HVM = "restartHVM",
RESTART_PV = "restartPV",
RESTART_XAPI = "restartXAPI",
}
export enum LIVEPATCH_STATUS {
OK = "ok",
OK_LIVEPATCH_COMPLETE = "ok_livepatch_complete",
OK_LIVEPATCH_INCOMPLETE = "ok_livepatch_incomplete",
}
export enum VM_POWER_STATE {
HALTED = "Halted",
PAUSED = "Paused",
RUNNING = "Running",
SUSPENDED = "Suspended",
}
export enum UPDATE_GUIDANCE {
REBOOT_HOST = "reboot_host",
REBOOT_HOST_ON_LIVEPATCH_FAILURE = "reboot_host_on_livepatch_failure",
RESTART_DEVICE_MODEL = "restart_device_model",
RESTART_TOOLSTACK = "restart_toolstack",
}
export enum ON_SOFTREBOOT_BEHAVIOR {
DESTROY = "destroy",
PRESERVE = "preserve",
RESTART = "restart",
SOFT_REBOOT = "soft_reboot",
}
export enum ON_NORMAL_EXIT {
DESTROY = "destroy",
RESTART = "restart",
}
export enum VM_OPERATION {
ASSERT_OPERATION_VALID = "assert_operation_valid",
AWAITING_MEMORY_LIVE = "awaiting_memory_live",
CALL_PLUGIN = "call_plugin",
CHANGING_DYNAMIC_RANGE = "changing_dynamic_range",
CHANGING_MEMORY_LIMITS = "changing_memory_limits",
CHANGING_MEMORY_LIVE = "changing_memory_live",
CHANGING_NVRAM = "changing_NVRAM",
CHANGING_SHADOW_MEMORY = "changing_shadow_memory",
CHANGING_SHADOW_MEMORY_LIVE = "changing_shadow_memory_live",
CHANGING_STATIC_RANGE = "changing_static_range",
CHANGING_VCPUS = "changing_VCPUs",
CHANGING_VCPUS_LIVE = "changing_VCPUs_live",
CHECKPOINT = "checkpoint",
CLEAN_REBOOT = "clean_reboot",
CLEAN_SHUTDOWN = "clean_shutdown",
CLONE = "clone",
COPY = "copy",
CREATE_TEMPLATE = "create_template",
CREATE_VTPM = "create_vtpm",
CSVM = "csvm",
DATA_SOURCE_OP = "data_source_op",
DESTROY = "destroy",
EXPORT = "export",
GET_BOOT_RECORD = "get_boot_record",
HARD_REBOOT = "hard_reboot",
HARD_SHUTDOWN = "hard_shutdown",
IMPORT = "import",
MAKE_INTO_TEMPLATE = "make_into_template",
METADATA_EXPORT = "metadata_export",
MIGRATE_SEND = "migrate_send",
PAUSE = "pause",
POOL_MIGRATE = "pool_migrate",
POWER_STATE_RESET = "power_state_reset",
PROVISION = "provision",
QUERY_SERVICES = "query_services",
RESUME = "resume",
RESUME_ON = "resume_on",
REVERT = "revert",
REVERTING = "reverting",
SEND_SYSRQ = "send_sysrq",
SEND_TRIGGER = "send_trigger",
SHUTDOWN = "shutdown",
SNAPSHOT = "snapshot",
SNAPSHOT_WITH_QUIESCE = "snapshot_with_quiesce",
START = "start",
START_ON = "start_on",
SUSPEND = "suspend",
UNPAUSE = "unpause",
UPDATE_ALLOWED_OPERATIONS = "update_allowed_operations",
}
export enum ON_CRASH_BEHAVIOUR {
COREDUMP_AND_DESTROY = "coredump_and_destroy",
COREDUMP_AND_RESTART = "coredump_and_restart",
DESTROY = "destroy",
PRESERVE = "preserve",
RENAME_RESTART = "rename_restart",
RESTART = "restart",
}
export enum DOMAIN_TYPE {
HVM = "hvm",
PV = "pv",
PVH = "pvh",
PV_IN_PVH = "pv_in_pvh",
UNSPECIFIED = "unspecified",
}
export enum TRISTATE_TYPE {
NO = "no",
UNSPECIFIED = "unspecified",
YES = "yes",
}
export enum VMPP_BACKUP_TYPE {
CHECKPOINT = "checkpoint",
SNAPSHOT = "snapshot",
}
export enum VMPP_BACKUP_FREQUENCY {
DAILY = "daily",
HOURLY = "hourly",
WEEKLY = "weekly",
}
export enum VMPP_ARCHIVE_FREQUENCY {
ALWAYS_AFTER_BACKUP = "always_after_backup",
DAILY = "daily",
NEVER = "never",
WEEKLY = "weekly",
}
export enum VMPP_ARCHIVE_TARGET_TYPE {
CIFS = "cifs",
NFS = "nfs",
NONE = "none",
}
export enum VMSS_FREQUENCY {
DAILY = "daily",
HOURLY = "hourly",
WEEKLY = "weekly",
}
export enum VMSS_TYPE {
CHECKPOINT = "checkpoint",
SNAPSHOT = "snapshot",
SNAPSHOT_WITH_QUIESCE = "snapshot_with_quiesce",
}
export enum VM_APPLIANCE_OPERATION {
CLEAN_SHUTDOWN = "clean_shutdown",
HARD_SHUTDOWN = "hard_shutdown",
SHUTDOWN = "shutdown",
START = "start",
}
export enum HOST_ALLOWED_OPERATION {
APPLY_UPDATES = "apply_updates",
EVACUATE = "evacuate",
POWER_ON = "power_on",
PROVISION = "provision",
REBOOT = "reboot",
SHUTDOWN = "shutdown",
VM_MIGRATE = "vm_migrate",
VM_RESUME = "vm_resume",
VM_START = "vm_start",
}
export enum LATEST_SYNCED_UPDATES_APPLIED_STATE {
NO = "no",
UNKNOWN = "unknown",
YES = "yes",
}
export enum HOST_DISPLAY {
DISABLED = "disabled",
DISABLE_ON_REBOOT = "disable_on_reboot",
ENABLED = "enabled",
ENABLE_ON_REBOOT = "enable_on_reboot",
}
export enum HOST_SCHED_GRAN {
CORE = "core",
CPU = "cpu",
SOCKET = "socket",
}
export enum NETWORK_OPERATION {
ATTACHING = "attaching",
}
export enum NETWORK_DEFAULT_LOCKING_MODE {
DISABLED = "disabled",
UNLOCKED = "unlocked",
}
export enum NETWORK_PURPOSE {
INSECURE_NBD = "insecure_nbd",
NBD = "nbd",
}
export enum VIF_OPERATION {
ATTACH = "attach",
PLUG = "plug",
UNPLUG = "unplug",
}
export enum VIF_LOCKING_MODE {
DISABLED = "disabled",
LOCKED = "locked",
NETWORK_DEFAULT = "network_default",
UNLOCKED = "unlocked",
}
export enum VIF_IPV4_CONFIGURATION_MODE {
NONE = "None",
STATIC = "Static",
}
export enum VIF_IPV6_CONFIGURATION_MODE {
NONE = "None",
STATIC = "Static",
}
export enum PIF_IGMP_STATUS {
DISABLED = "disabled",
ENABLED = "enabled",
UNKNOWN = "unknown",
}
export enum IP_CONFIGURATION_MODE {
DHCP = "DHCP",
NONE = "None",
STATIC = "Static",
}
export enum IPV6_CONFIGURATION_MODE {
AUTOCONF = "Autoconf",
DHCP = "DHCP",
NONE = "None",
STATIC = "Static",
}
export enum PRIMARY_ADDRESS_TYPE {
IPV4 = "IPv4",
IPV6 = "IPv6",
}
export enum BOND_MODE {
ACTIVE_BACKUP = "active-backup",
BALANCE_SLB = "balance-slb",
LACP = "lacp",
}
export enum STORAGE_OPERATION {
DESTROY = "destroy",
FORGET = "forget",
PBD_CREATE = "pbd_create",
PBD_DESTROY = "pbd_destroy",
PLUG = "plug",
SCAN = "scan",
UNPLUG = "unplug",
UPDATE = "update",
VDI_CLONE = "vdi_clone",
VDI_CREATE = "vdi_create",
VDI_DATA_DESTROY = "vdi_data_destroy",
VDI_DESTROY = "vdi_destroy",
VDI_DISABLE_CBT = "vdi_disable_cbt",
VDI_ENABLE_CBT = "vdi_enable_cbt",
VDI_INTRODUCE = "vdi_introduce",
VDI_LIST_CHANGED_BLOCKS = "vdi_list_changed_blocks",
VDI_MIRROR = "vdi_mirror",
VDI_RESIZE = "vdi_resize",
VDI_SET_ON_BOOT = "vdi_set_on_boot",
VDI_SNAPSHOT = "vdi_snapshot",
}
export enum SR_HEALTH {
HEALTHY = "healthy",
RECOVERING = "recovering",
}
export enum VDI_OPERATION {
BLOCKED = "blocked",
CLONE = "clone",
COPY = "copy",
DATA_DESTROY = "data_destroy",
DESTROY = "destroy",
DISABLE_CBT = "disable_cbt",
ENABLE_CBT = "enable_cbt",
FORCE_UNLOCK = "force_unlock",
FORGET = "forget",
GENERATE_CONFIG = "generate_config",
LIST_CHANGED_BLOCKS = "list_changed_blocks",
MIRROR = "mirror",
RESIZE = "resize",
RESIZE_ONLINE = "resize_online",
SET_ON_BOOT = "set_on_boot",
SNAPSHOT = "snapshot",
UPDATE = "update",
}
export enum VDI_TYPE {
CBT_METADATA = "cbt_metadata",
CRASHDUMP = "crashdump",
EPHEMERAL = "ephemeral",
HA_STATEFILE = "ha_statefile",
METADATA = "metadata",
PVS_CACHE = "pvs_cache",
REDO_LOG = "redo_log",
RRD = "rrd",
SUSPEND = "suspend",
SYSTEM = "system",
USER = "user",
}
export enum ON_BOOT {
PERSIST = "persist",
RESET = "reset",
}
export enum VBD_OPERATION {
ATTACH = "attach",
EJECT = "eject",
INSERT = "insert",
PAUSE = "pause",
PLUG = "plug",
UNPAUSE = "unpause",
UNPLUG = "unplug",
UNPLUG_FORCE = "unplug_force",
}
export enum VBD_TYPE {
CD = "CD",
DISK = "Disk",
FLOPPY = "Floppy",
}
export enum VBD_MODE {
RO = "RO",
RW = "RW",
}
export enum VTPM_OPERATION {
DESTROY = "destroy",
}
export enum PERSISTENCE_BACKEND {
XAPI = "xapi",
}
export enum CONSOLE_PROTOCOL {
RDP = "rdp",
RFB = "rfb",
VT100 = "vt100",
}
export enum CLS {
CERTIFICATE = "Certificate",
HOST = "Host",
POOL = "Pool",
PVS_PROXY = "PVS_proxy",
SR = "SR",
VDI = "VDI",
VM = "VM",
VMPP = "VMPP",
VMSS = "VMSS",
}
export enum TUNNEL_PROTOCOL {
GRE = "gre",
VXLAN = "vxlan",
}
export enum SRIOV_CONFIGURATION_MODE {
MANUAL = "manual",
MODPROBE = "modprobe",
SYSFS = "sysfs",
UNKNOWN = "unknown",
}
export enum PGPU_DOM0_ACCESS {
DISABLED = "disabled",
DISABLE_ON_REBOOT = "disable_on_reboot",
ENABLED = "enabled",
ENABLE_ON_REBOOT = "enable_on_reboot",
}
export enum ALLOCATION_ALGORITHM {
BREADTH_FIRST = "breadth_first",
DEPTH_FIRST = "depth_first",
}
export enum VGPU_TYPE_IMPLEMENTATION {
GVT_G = "gvt_g",
MXGPU = "mxgpu",
NVIDIA = "nvidia",
NVIDIA_SRIOV = "nvidia_sriov",
PASSTHROUGH = "passthrough",
}
export enum PVS_PROXY_STATUS {
CACHING = "caching",
INCOMPATIBLE_PROTOCOL_VERSION = "incompatible_protocol_version",
INCOMPATIBLE_WRITE_CACHE_MODE = "incompatible_write_cache_mode",
INITIALISED = "initialised",
STOPPED = "stopped",
}
export enum SDN_CONTROLLER_PROTOCOL {
PSSL = "pssl",
SSL = "ssl",
}
export enum VUSB_OPERATION {
ATTACH = "attach",
PLUG = "plug",
UNPLUG = "unplug",
}
export enum CLUSTER_OPERATION {
ADD = "add",
DESTROY = "destroy",
DISABLE = "disable",
ENABLE = "enable",
REMOVE = "remove",
}
export enum CLUSTER_HOST_OPERATION {
DESTROY = "destroy",
DISABLE = "disable",
ENABLE = "enable",
}
export enum CERTIFICATE_TYPE {
CA = "ca",
HOST = "host",
HOST_INTERNAL = "host_internal",
}

View File

@@ -296,7 +296,7 @@ export default class XenApi {
XenApiVm["$ref"],
XenApiVm["power_state"]
>;
type VmRefsWithNameLabel = Record<XenApiVm["$ref"], string>;
type VmRefsToClone = Record<XenApiVm["$ref"], /* Cloned VM name */ string>;
return {
delete: (vmRefs: VmRefs) =>
@@ -351,7 +351,7 @@ export default class XenApi {
)
);
},
clone: (vmRefsToClone: VmRefsWithNameLabel) => {
clone: (vmRefsToClone: VmRefsToClone) => {
const vmRefs = Object.keys(vmRefsToClone) as XenApiVm["$ref"][];
return Promise.all(
@@ -360,26 +360,6 @@ export default class XenApi {
)
);
},
migrate: (vmRefs: VmRefs, destinationHostRef: XenApiHost["$ref"]) => {
return Promise.all(
castArray(vmRefs).map((vmRef) =>
this.call("VM.pool_migrate", [
vmRef,
destinationHostRef,
{ force: "false" },
])
)
);
},
snapshot: (vmRefsToSnapshot: VmRefsWithNameLabel) => {
const vmRefs = Object.keys(vmRefsToSnapshot) as XenApiVm["$ref"][];
return Promise.all(
vmRefs.map((vmRef) =>
this.call("VM.snapshot", [vmRef, vmRefsToSnapshot[vmRef]])
)
);
},
};
}
}

View File

@@ -1,46 +1,8 @@
import type {
ALLOCATION_ALGORITHM,
BOND_MODE,
DOMAIN_TYPE,
IP_CONFIGURATION_MODE,
IPV6_CONFIGURATION_MODE,
NETWORK_DEFAULT_LOCKING_MODE,
NETWORK_OPERATION,
NETWORK_PURPOSE,
ON_BOOT,
ON_CRASH_BEHAVIOUR,
ON_NORMAL_EXIT,
ON_SOFTREBOOT_BEHAVIOR,
PERSISTENCE_BACKEND,
PGPU_DOM0_ACCESS,
PIF_IGMP_STATUS,
PRIMARY_ADDRESS_TYPE,
SRIOV_CONFIGURATION_MODE,
TUNNEL_PROTOCOL,
UPDATE_GUIDANCE,
VBD_MODE,
VBD_OPERATION,
VBD_TYPE,
VDI_OPERATION,
VDI_TYPE,
VGPU_TYPE_IMPLEMENTATION,
VIF_IPV4_CONFIGURATION_MODE,
VIF_IPV6_CONFIGURATION_MODE,
VIF_LOCKING_MODE,
VIF_OPERATION,
VM_APPLIANCE_OPERATION,
XEN_API_OBJECT_TYPES,
POWER_STATE,
VM_OPERATION,
VM_POWER_STATE,
VMPP_ARCHIVE_FREQUENCY,
VMPP_ARCHIVE_TARGET_TYPE,
VMPP_BACKUP_FREQUENCY,
VMPP_BACKUP_TYPE,
VMSS_FREQUENCY,
VMSS_TYPE,
VTPM_OPERATION,
VUSB_OPERATION,
} from "@/libs/xen-api/xen-api.enums";
import type { XEN_API_OBJECT_TYPES } from "@/libs/xen-api/xen-api.utils";
} from "@/libs/xen-api/xen-api.utils";
type TypeMapping = typeof XEN_API_OBJECT_TYPES;
export type ObjectType = keyof TypeMapping;
@@ -119,236 +81,18 @@ export interface XenApiSr extends XenApiRecord<"sr"> {
}
export interface XenApiVm extends XenApiRecord<"vm"> {
HVM_boot_params: Record<string, string>;
HVM_boot_policy: string;
HVM_shadow_multiplier: number;
NVRAM: Record<string, string>;
PCI_bus: string;
PV_args: string;
PV_bootloader: string;
PV_bootloader_args: string;
PV_kernel: string;
PV_legacy_args: string;
PV_ramdisk: string;
VBDs: XenApiVbd["$ref"][];
VCPUs_at_startup: number;
VCPUs_max: number;
VCPUs_params: Record<string, string>;
VGPUs: XenApiVgpu["$ref"][];
VIFs: XenApiVif["$ref"][];
VTPMs: XenApiVtpm["$ref"][];
VUSBs: XenApiVusb["$ref"][];
actions_after_crash: ON_CRASH_BEHAVIOUR;
actions_after_reboot: ON_NORMAL_EXIT;
actions_after_shutdown: ON_NORMAL_EXIT;
actions_after_softreboot: ON_SOFTREBOOT_BEHAVIOR;
affinity: XenApiHost["$ref"];
allowed_operations: VM_OPERATION[];
appliance: XenApiVmAppliance["$ref"];
attached_PCIs: XenApiPci["$ref"][];
bios_strings: Record<string, string>;
blobs: Record<string, XenApiBlob["$ref"]>;
blocked_operations: Record<VM_OPERATION, string>;
children: XenApiVm["$ref"][];
consoles: XenApiConsole["$ref"][];
crash_dumps: XenApiCrashdump["$ref"][];
current_operations: Record<string, VM_OPERATION>;
domain_type: DOMAIN_TYPE;
domarch: string;
domid: number;
generation_id: string;
guest_metrics: XenApiVmGuestMetrics["$ref"];
ha_always_run: boolean;
ha_restart_priority: string;
hardware_platform_version: number;
has_vendor_device: boolean;
guest_metrics: string;
metrics: XenApiVmMetrics["$ref"];
name_label: string;
name_description: string;
power_state: POWER_STATE;
resident_on: XenApiHost["$ref"];
consoles: XenApiConsole["$ref"][];
is_control_domain: boolean;
is_a_snapshot: boolean;
is_a_template: boolean;
is_control_domain: boolean;
is_default_template: boolean;
is_snapshot_from_vmpp: boolean;
is_vmss_snapshot: boolean;
last_boot_CPU_flags: Record<string, string>;
last_booted_record: string;
memory_dynamic_max: number;
memory_dynamic_min: number;
memory_overhead: number;
memory_static_max: number;
memory_static_min: number;
memory_target: number;
metrics: XenApiVmMetrics["$ref"];
name_description: string;
name_label: string;
order: number;
other_config: Record<string, string>;
parent: XenApiVm["$ref"];
pending_guidances: UPDATE_GUIDANCE[];
platform: Record<string, string>;
power_state: VM_POWER_STATE;
protection_policy: XenApiVmpp["$ref"];
recommendations: string;
reference_label: string;
requires_reboot: boolean;
resident_on: XenApiHost["$ref"];
scheduled_to_be_resident_on: XenApiHost["$ref"];
shutdown_delay: number;
snapshot_info: Record<string, string>;
snapshot_metadata: string;
snapshot_of: XenApiVm["$ref"];
snapshot_schedule: XenApiVmss["$ref"];
snapshot_time: string;
snapshots: XenApiVm["$ref"][];
start_delay: number;
suspend_SR: XenApiSr["$ref"];
suspend_VDI: XenApiVdi["$ref"];
tags: string[];
transportable_snapshot_id: string;
user_version: number;
version: number;
xenstore_data: Record<string, string>;
}
export interface XenApiVtpm extends XenApiRecord<"vtpm"> {
VM: XenApiVm["$ref"];
allowed_operations: VTPM_OPERATION[];
backend: XenApiVm["$ref"];
current_operations: Record<string, VTPM_OPERATION>;
is_protected: boolean;
is_unique: boolean;
persistence_backend: PERSISTENCE_BACKEND;
}
export interface XenApiVusb extends XenApiRecord<"vusb"> {
USB_group: XenApiUsbGroup["$ref"];
VM: XenApiVm["$ref"];
allowed_operations: VUSB_OPERATION[];
current_operations: Record<string, VUSB_OPERATION>;
currently_attached: boolean;
other_config: Record<string, string>;
}
export interface XenApiUsbGroup extends XenApiRecord<"usb_group"> {
PUSBs: XenApiPusb["$ref"][];
VUSBs: XenApiVusb["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
}
export interface XenApiPusb extends XenApiRecord<"pusb"> {
USB_group: XenApiUsbGroup["$ref"];
description: string;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
passthrough_enabled: boolean;
path: string;
product_desc: string;
product_id: string;
serial: string;
speed: number;
vendor_desc: string;
vendor_id: string;
version: string;
}
export interface XenApiVgpu extends XenApiRecord<"vgpu"> {
GPU_group: XenApiGpuGroup["$ref"];
PCI: XenApiPci["$ref"];
VM: XenApiVm["$ref"];
compatibility_metadata: Record<string, string>;
currently_attached: boolean;
device: string;
extra_args: string;
other_config: Record<string, string>;
resident_on: XenApiPgpu["$ref"];
scheduled_to_be_resident_on: XenApiPgpu["$ref"];
type: XenApiVgpuType["$ref"];
}
export interface XenApiGpuGroup extends XenApiRecord<"gpu_group"> {
GPU_types: string[];
PGPUs: XenApiPgpu["$ref"][];
VGPUs: XenApiVgpu["$ref"][];
allocation_algorithm: ALLOCATION_ALGORITHM;
enabled_VGPU_types: XenApiVgpuType["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
supported_VGPU_types: XenApiVgpuType["$ref"][];
}
export interface XenApiPgpu extends XenApiRecord<"pgpu"> {
GPU_group: XenApiGpuGroup["$ref"];
PCI: XenApiPci["$ref"];
compatibility_metadata: Record<string, string>;
dom0_access: PGPU_DOM0_ACCESS;
enabled_VGPU_types: XenApiVgpuType["$ref"][];
host: XenApiHost["$ref"];
is_system_display_device: boolean;
other_config: Record<string, string>;
resident_VGPUs: XenApiVgpu["$ref"][];
supported_VGPU_max_capacities: Record<XenApiVgpuType["$ref"], number>;
supported_VGPU_types: XenApiVgpuType["$ref"][];
}
export interface XenApiVgpuType extends XenApiRecord<"vgpu_type"> {
VGPUs: XenApiVgpu["$ref"][];
compatible_types_in_vm: XenApiVgpuType["$ref"][];
enabled_on_GPU_groups: XenApiGpuGroup["$ref"][];
enabled_on_PGPUs: XenApiPgpu["$ref"][];
experimental: boolean;
framebuffer_size: number;
identifier: string;
implementation: VGPU_TYPE_IMPLEMENTATION;
max_heads: number;
max_resolution_x: number;
max_resolution_y: number;
model_name: string;
supported_on_GPU_groups: XenApiGpuGroup["$ref"][];
supported_on_PGPUs: XenApiPgpu["$ref"][];
vendor_name: string;
}
export interface XenApiVmAppliance extends XenApiRecord<"vm_appliance"> {
VMs: XenApiVm["$ref"][];
allowed_operations: VM_APPLIANCE_OPERATION[];
current_operations: Record<string, VM_APPLIANCE_OPERATION>;
name_description: string;
name_label: string;
}
export interface XenApiVmpp extends XenApiRecord<"vmpp"> {
VMs: XenApiVm["$ref"][];
alarm_config: Record<string, string>;
archive_frequency: VMPP_ARCHIVE_FREQUENCY;
archive_last_run_time: string;
archive_schedule: Record<string, string>;
archive_target_config: Record<string, string>;
archive_target_type: VMPP_ARCHIVE_TARGET_TYPE;
backup_frequency: VMPP_BACKUP_FREQUENCY;
backup_last_run_time: string;
backup_retention_value: number;
backup_schedule: Record<string, string>;
backup_type: VMPP_BACKUP_TYPE;
is_alarm_enabled: boolean;
is_archive_running: boolean;
is_backup_running: boolean;
is_policy_enabled: boolean;
name_description: string;
name_label: string;
recent_alerts: string[];
}
export interface XenApiVmss extends XenApiRecord<"vmss"> {
VMs: XenApiVm["$ref"][];
enabled: boolean;
frequency: VMSS_FREQUENCY;
last_run_time: string;
name_description: string;
name_label: string;
retained_snapshots: number;
schedule: Record<string, string>;
type: VMSS_TYPE;
VCPUs_at_startup: number;
}
export interface XenApiConsole extends XenApiRecord<"console"> {
@@ -387,238 +131,6 @@ export interface XenApiMessage<RelationType extends RawObjectType>
timestamp: string;
}
export interface XenApiVbd extends XenApiRecord<"vbd"> {
VDI: XenApiVdi["$ref"];
VM: XenApiVm["$ref"];
allowed_operations: VBD_OPERATION[];
bootable: boolean;
current_operations: Record<string, VBD_OPERATION>;
currently_attached: boolean;
device: string;
empty: boolean;
metrics: XenApiVbdMetrics["$ref"];
mode: VBD_MODE;
other_config: Record<string, string>;
qos_algorithm_params: Record<string, string>;
qos_algorithm_type: string;
qos_supported_algorithms: string[];
runtime_properties: Record<string, string>;
status_code: number;
status_detail: string;
storage_lock: boolean;
type: VBD_TYPE;
unpluggable: boolean;
userdevice: string;
}
export interface XenApiVbdMetrics extends XenApiRecord<"vbd_metrics"> {
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
}
export interface XenApiVdi extends XenApiRecord<"vdi"> {
SR: XenApiSr["$ref"];
VBDs: XenApiVbd["$ref"][];
allow_caching: boolean;
allowed_operations: VDI_OPERATION[];
cbt_enabled: boolean;
crash_dumps: XenApiCrashdump["$ref"][];
current_operations: Record<string, VDI_OPERATION>;
is_a_snapshot: boolean;
is_tools_iso: boolean;
location: string;
managed: boolean;
metadata_latest: boolean;
metadata_of_pool: XenApiPool["$ref"];
missing: boolean;
name_description: string;
name_label: string;
on_boot: ON_BOOT;
other_config: Record<string, string>;
parent: XenApiVdi["$ref"];
physical_utilisation: number;
read_only: boolean;
sharable: boolean;
sm_config: Record<string, string>;
snapshot_of: XenApiVdi["$ref"];
snapshot_time: string;
snapshots: XenApiVdi["$ref"][];
storage_lock: boolean;
tags: string[];
type: VDI_TYPE;
virtual_size: number;
xenstore_data: Record<string, string>;
}
export interface XenApiCrashdump extends XenApiRecord<"crashdump"> {
VDI: XenApiVdi["$ref"];
VM: XenApiVm["$ref"];
other_config: Record<string, string>;
}
export interface XenApiNetwork extends XenApiRecord<"network"> {
MTU: number;
PIFs: XenApiPif["$ref"][];
VIFs: XenApiVif["$ref"][];
allowed_operations: NETWORK_OPERATION[];
assigned_ips: Record<XenApiVif["$ref"], string>;
blobs: Record<string, XenApiBlob["$ref"]>;
bridge: string;
current_operations: Record<string, NETWORK_OPERATION>;
default_locking_mode: NETWORK_DEFAULT_LOCKING_MODE;
managed: boolean;
name_description: string;
name_label: string;
other_config: Record<string, string>;
purpose: NETWORK_PURPOSE[];
tags: string[];
}
export interface XenApiBlob extends XenApiRecord<"blob"> {
last_updated: string;
mime_type: string;
name_description: string;
name_label: string;
public: boolean;
size: number;
}
export interface XenApiVif extends XenApiRecord<"vif"> {
MAC: string;
MAC_autogenerated: boolean;
MTU: number;
VM: XenApiVm["$ref"];
allowed_operations: VIF_OPERATION[];
current_operations: Record<string, VIF_OPERATION>;
currently_attached: boolean;
device: string;
ipv4_addresses: string[];
ipv4_allowed: string[];
ipv4_configuration_mode: VIF_IPV4_CONFIGURATION_MODE;
ipv4_gateway: string;
ipv6_addresses: string[];
ipv6_allowed: string[];
ipv6_configuration_mode: VIF_IPV6_CONFIGURATION_MODE;
ipv6_gateway: string;
locking_mode: VIF_LOCKING_MODE;
metrics: XenApiVifMetrics["$ref"];
network: XenApiNetwork["$ref"];
other_config: Record<string, string>;
qos_algorithm_params: Record<string, string>;
qos_algorithm_type: string;
qos_supported_algorithms: string[];
runtime_properties: Record<string, string>;
status_code: number;
status_detail: string;
}
export interface XenApiVifMetrics extends XenApiRecord<"vif_metrics"> {
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
}
export interface XenApiPif extends XenApiRecord<"pif"> {
DNS: string;
IP: string;
IPv6: string[];
MAC: string;
MTU: number;
PCI: XenApiPci["$ref"];
VLAN: number;
VLAN_master_of: XenApiVlan["$ref"];
VLAN_slave_of: XenApiVlan["$ref"][];
bond_master_of: XenApiBond["$ref"][];
bond_slave_of: XenApiBond["$ref"];
capabilities: string[];
currently_attached: boolean;
device: string;
disallow_unplug: boolean;
gateway: string;
host: XenApiHost["$ref"];
igmp_snooping_status: PIF_IGMP_STATUS;
ip_configuration_mode: IP_CONFIGURATION_MODE;
ipv6_configuration_mode: IPV6_CONFIGURATION_MODE;
ipv6_gateway: string;
managed: boolean;
management: boolean;
metrics: XenApiPifMetrics["$ref"];
netmask: string;
network: XenApiNetwork["$ref"];
other_config: Record<string, string>;
physical: boolean;
primary_address_type: PRIMARY_ADDRESS_TYPE;
properties: Record<string, string>;
sriov_logical_PIF_of: XenApiNetworkSriov["$ref"][];
sriov_physical_PIF_of: XenApiNetworkSriov["$ref"][];
tunnel_access_PIF_of: XenApiTunnel["$ref"][];
tunnel_transport_PIF_of: XenApiTunnel["$ref"][];
}
export interface XenApiNetworkSriov extends XenApiRecord<"network_sriov"> {
configuration_mode: SRIOV_CONFIGURATION_MODE;
logical_PIF: XenApiPif["$ref"];
physical_PIF: XenApiPif["$ref"];
requires_reboot: boolean;
}
export interface XenApiVlan extends XenApiRecord<"vlan"> {
other_config: Record<string, string>;
tag: number;
tagged_PIF: XenApiPif["$ref"];
untagged_PIF: XenApiPif["$ref"];
}
export interface XenApiTunnel extends XenApiRecord<"tunnel"> {
access_PIF: XenApiPif["$ref"];
other_config: Record<string, string>;
protocol: TUNNEL_PROTOCOL;
status: Record<string, string>;
transport_PIF: XenApiPif["$ref"];
}
export interface XenApiPci extends XenApiRecord<"pci"> {
class_name: string;
dependencies: XenApiPci["$ref"][];
device_name: string;
driver_name: string;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
pci_id: string;
subsystem_device_name: string;
subsystem_vendor_name: string;
vendor_name: string;
}
export interface XenApiPifMetrics extends XenApiRecord<"pif_metrics"> {
carrier: boolean;
device_id: string;
device_name: string;
duplex: boolean;
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
pci_bus_path: string;
speed: number;
vendor_id: string;
vendor_name: string;
}
export interface XenApiBond extends XenApiRecord<"bond"> {
auto_update_mac: boolean;
links_up: number;
master: XenApiPif["$ref"];
mode: BOND_MODE;
other_config: Record<string, string>;
primary_slave: XenApiPif["$ref"];
properties: Record<string, string>;
slaves: XenApiPif["$ref"][];
}
export type XenApiEvent<
RelationType extends ObjectType,
XRecord extends ObjectTypeToRecord<RelationType>,

View File

@@ -40,7 +40,6 @@ export const XEN_API_OBJECT_TYPES = {
vm: "VM",
vmpp: "VMPP",
vmss: "VMSS",
vm_appliance: "VM_appliance",
vm_guest_metrics: "VM_guest_metrics",
vm_metrics: "VM_metrics",
vusb: "VUSB",
@@ -63,7 +62,6 @@ export const XEN_API_OBJECT_TYPES = {
subject: "subject",
task: "task",
tunnel: "tunnel",
vtpm: "VTPM",
} as const;
export const rawTypeToType = <RawType extends RawObjectType>(
@@ -74,6 +72,28 @@ export const typeToRawType = <Type extends ObjectType>(
type: Type
): TypeToRawType<Type> => XEN_API_OBJECT_TYPES[type];
export enum POWER_STATE {
RUNNING = "Running",
PAUSED = "Paused",
HALTED = "Halted",
SUSPENDED = "Suspended",
}
export enum VM_OPERATION {
START = "start",
START_ON = "start_on",
RESUME = "resume",
UNPAUSE = "unpause",
CLONE = "clone",
SHUTDOWN = "shutdown",
CLEAN_SHUTDOWN = "clean_shutdown",
HARD_SHUTDOWN = "hard_shutdown",
CLEAN_REBOOT = "clean_reboot",
HARD_REBOOT = "hard_reboot",
PAUSE = "pause",
SUSPEND = "suspend",
}
export const buildXoObject = <T extends XenApiRecord<ObjectType>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: T["$ref"] }

View File

@@ -27,12 +27,10 @@
"cancel": "Cancel",
"change-state": "Change state",
"click-to-display-alarms": "Click to display alarms:",
"close": "Close",
"confirm-delete": "You're about to delete {0}",
"coming-soon": "Coming soon!",
"community": "Community",
"community-name": "{name} community",
"confirm-cancel": "Are you sure you want to cancel?",
"confirm-delete": "You're about to delete {0}",
"console": "Console",
"console-unavailable": "Console unavailable",
"copy": "Copy",
@@ -44,9 +42,9 @@
"descending": "descending",
"description": "Description",
"display": "Display",
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"documentation": "Documentation",
"documentation-name": "{name} documentation",
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"edit-config": "Edit config",
"error-no-data": "Error, can't collect data.",
"error-occurred": "An error has occurred",
@@ -86,18 +84,15 @@
"log-out": "Log out",
"login": "Login",
"migrate": "Migrate",
"migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
"n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch",
"n-missing": "{n} missing",
"n-vms": "1 VM | {n} VMs",
"name": "Name",
"network": "Network",
"network-download": "Download",
"network-throughput": "Network throughput",
"network-upload": "Upload",
"new-features-are-coming": "New features are coming soon!",
"news": "News",
"news-name": "{name} news",
"new-features-are-coming": "New features are coming soon!",
"no-alarm-triggered": "No alarm triggered",
"no-tasks": "No tasks",
"not-found": "Not found",
@@ -109,7 +104,6 @@
"page-not-found": "This page is not to be found…",
"password": "Password",
"password-invalid": "Password invalid",
"patches": "Patches",
"pause": "Pause",
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
@@ -133,13 +127,11 @@
},
"resume": "Resume",
"save": "Save",
"select-destination-host": "Select a destination host",
"selected-vms-in-execution": "Some selected VMs are running",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"shutdown": "Shutdown",
"snapshot": "Snapshot",
"some-selected-vms-can-not-be-migrated": "Some selected VMs can't be migrated",
"sort-by": "Sort by",
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",

View File

@@ -25,14 +25,12 @@
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"backup": "Sauvegarde",
"cancel": "Annuler",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"change-state": "Changer l'état",
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
"close": "Fermer",
"coming-soon": "Bientôt disponible !",
"community": "Communauté",
"community-name": "Communauté {name}",
"confirm-cancel": "Êtes-vous sûr de vouloir annuler ?",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"console": "Console",
"console-unavailable": "Console indisponible",
"copy": "Copier",
@@ -44,9 +42,9 @@
"descending": "descendant",
"description": "Description",
"display": "Affichage",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"documentation": "Documentation",
"documentation-name": "Documentation {name}",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"edit-config": "Modifier config",
"error-no-data": "Erreur, impossible de collecter les données.",
"error-occurred": "Une erreur est survenue",
@@ -86,18 +84,15 @@
"log-out": "Se déconnecter",
"login": "Connexion",
"migrate": "Migrer",
"migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
"n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch",
"n-missing": "{n} manquant | {n} manquants",
"n-vms": "1 VM | {n} VMs",
"name": "Nom",
"network": "Réseau",
"network-download": "Descendant",
"network-throughput": "Débit du réseau",
"network-upload": "Montant",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"news": "Actualités",
"news-name": "Actualités {name}",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"no-alarm-triggered": "Aucune alarme déclenchée",
"no-tasks": "Aucune tâche",
"not-found": "Non trouvé",
@@ -109,7 +104,6 @@
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"patches": "Patches",
"pause": "Pause",
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
@@ -133,13 +127,11 @@
},
"resume": "Reprendre",
"save": "Enregistrer",
"select-destination-host": "Sélectionnez un hôte de destination",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"shutdown": "Arrêter",
"snapshot": "Instantané",
"some-selected-vms-can-not-be-migrated": "Certaines VMs sélectionnées ne peuvent pas être migrées",
"sort-by": "Trier par",
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",

View File

@@ -38,7 +38,7 @@ type StoreToRefs<SS extends Store<any, any, any, any>> = ToRefs<
type Output<
S extends StoreDefinition<any, any, any, any>,
Defer extends boolean,
Defer extends boolean
> = Omit<S, keyof StoreToRefs<S> | IgnoredProperties> &
StoreToRefs<S> &
(Defer extends true
@@ -54,7 +54,7 @@ export const createUseCollection = <
infer A
>
? Store<Id, S, G, A>
: never,
: never
>(
useStore: SD
) => {

View File

@@ -4,7 +4,6 @@ import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostMetricsStore } from "@/stores/xen-api/host-metrics.store";
import type { XenApiPatch } from "@/types/xen-api";
import { defineStore } from "pinia";
import { computed } from "vue";
@@ -43,36 +42,10 @@ export const useHostStore = defineStore("xen-api-host", () => {
});
}) as GetStats<XenApiHost>;
const fetchMissingPatches = async (
hostRef: XenApiHost["$ref"]
): Promise<XenApiPatch[]> => {
const xenApiStore = useXenApiStore();
const rawPatchesAsString = await xenApiStore
.getXapi()
.call<string>("host.call_plugin", [
hostRef,
"updater.py",
"check_update",
{},
]);
const rawPatches = JSON.parse(rawPatchesAsString) as Omit<
XenApiPatch,
"$id"
>[];
return rawPatches.map((rawPatch) => ({
...rawPatch,
$id: `${rawPatch.name}-${rawPatch.version}`,
}));
};
return {
...context,
runningHosts,
getStats,
fetchMissingPatches,
};
});

View File

@@ -3,10 +3,8 @@ import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-s
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import {
type VM_OPERATION,
VM_POWER_STATE,
} from "@/libs/xen-api/xen-api.enums";
import type { VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostStore } from "@/stores/xen-api/host.store";
@@ -37,7 +35,7 @@ export const useVmStore = defineStore("xen-api-vm", () => {
};
const runningVms = computed(() =>
records.value.filter((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
records.value.filter((vm) => vm.power_state === POWER_STATE.RUNNING)
);
const recordsByHostRef = computed(() => {

View File

@@ -9,7 +9,6 @@
prop('error').type('string').widget(),
prop('help').type('string').widget().preset('256 by default'),
prop('disabled').type('boolean').widget().ctx(),
prop('light').bool().widget(),
slot().help('Contains the input'),
]"
>

View File

@@ -1,11 +0,0 @@
```vue-template
<UiModal v-model="isOpen">
<BasicModalLayout>
Here is a basic modal...
</BasicModalLayout>
</UiModal>
```
```vue-script
const { isOpen } = useModal();
```

View File

@@ -1,22 +0,0 @@
<template>
<ComponentStory
v-slot="{ settings }"
:params="[
slot(),
setting('defaultSlotContent').preset('Modal content').widget(text()),
]"
>
<BasicModalLayout>
{{ settings.defaultSlotContent }}
</BasicModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import { setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,21 +0,0 @@
```vue-template
<UiModal v-model="isOpen">
<ConfirmModalLayout :icon="faShip">
<template #title>Do you confirm?</template>
<template #subtitle>You should be sure about this</template>
<template #buttons>
<UiButton outlined @click="close">I prefer not</UiButton>
<UiButton @click="accept">Yes, I'm sure!</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
```
```vue-script
const { isOpen, close } = useModal();
const accept = async () => {
// do something
close();
}
```

View File

@@ -1,25 +0,0 @@
```vue-template
<UiModal v-model="isOpen">
<FormModalLayout :icon="faShip" @submit.prevent="handleSubmit">
<template #title>Migrate 3 VMs/template>
<template #default>
<!-- Form content goes here... -->
</template>
<template #buttons>
<UiButton outlined @click="close">Cancel</UiButton>
<UiButton type="submit">Migrate 3 VMs</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
```
```vue-script
const { isOpen, close } = useModal();
const handleSubmit = async () => {
// Handling form submission...
close();
}
```

View File

@@ -1,54 +0,0 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[iconProp(), slot('title'), slot('default'), slot('buttons')]"
>
<FormModalLayout :icon="faRoute" v-bind="properties">
<template #title>Migrate 3 VMs</template>
<div>
<FormInputWrapper
label="Select a destination host"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
<FormInputWrapper
label="Select a migration network (optional)"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
<FormInputWrapper
help="Individual selection for each VDI is not available on multiple VMs migration."
label="Select a destination SR"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
</div>
<template #buttons>
<UiButton outlined>Cancel</UiButton>
<UiButton>Migrate 3 VMs</UiButton>
</template>
</FormModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { iconProp, slot } from "@/libs/story/story-param";
import { faRoute } from "@fortawesome/free-solid-svg-icons";
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,19 +0,0 @@
A basic modal container containing 3 slots: `header`, `default` and `footer`.
Tag will be `div` by default but can be changed with the `tag` prop.
Color can be changed with the `color` prop.
To keep the content centered vertically, header and footer will always have the same height.
Modal content has an max height + overflow to prevent the modal growing out of the screen.
Modal containers can be nested.
```vue-template
<ModalContainer>
<template #header>Header</template>
<template #default>Content</template>
<template #header>Footer</template>
</ModalContainer>
```

View File

@@ -1,52 +0,0 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
prop('tag').str().default('div').widget(),
colorProp(),
slot('header'),
slot(),
slot('footer'),
setting('headerSlotContent')
.preset('Header')
.widget(text())
.help('Content for default slot'),
setting('defaultSlotContent')
.preset('Content')
.widget(text())
.help('Content for default slot'),
setting('footerSlotContent')
.preset('Footer')
.widget(text())
.help('Content for default slot'),
setting('showNested')
.preset(false)
.widget(boolean())
.help('Show nested modal'),
]"
>
<ModalContainer v-bind="properties">
<template #header>
{{ settings.headerSlotContent }}
</template>
<template #default>
{{ settings.defaultSlotContent }}
<ModalContainer v-if="settings.showNested" color="error">
Nested modal
</ModalContainer>
</template>
<template #footer>
{{ settings.footerSlotContent }}
</template>
</ModalContainer>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import { colorProp, prop, setting, slot } from "@/libs/story/story-param";
import { boolean, text } from "@/libs/story/story-widget";
</script>

View File

@@ -1,21 +0,0 @@
This component only handle the modal backdrop and content positioning.
You can use any pre-made layouts, create your own or use the `ModalContainer` component.
It is meant to be used with `useModal` composable.
```vue-template
<button @click="open">Delete all items</button>
<UiModal v-model="isOpen">
<ModalContainer...>
<!-- <ConfirmModalLayout ...> (Or you can use a pre-made layout) -->
</UiModal>
```
```vue-script
import { faRemove } from "@fortawesome/free-solid-svg-icons";
import { useModal } from "@composable/modal.composable";
const { open, close, isOpen } = useModal().
```

View File

@@ -1,24 +0,0 @@
<template>
<ComponentStory
:params="[
model()
.required()
.type('boolean')
.help('Whether the modal is opened or not'),
colorProp().ctx(),
slot().help('Place your ModalContainer here'),
]"
>
<button type="button" @click="open">Open modal</button>
<UiModal v-model="isOpen" />
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { colorProp, model, slot } from "@/libs/story/story-param";
const { isOpen, open } = useModal();
</script>

View File

@@ -2,9 +2,9 @@
<ComponentStory
:params="[
prop('state')
.enum(...Object.values(VM_POWER_STATE))
.enum(...Object.values(POWER_STATE))
.required()
.preset(VM_POWER_STATE.RUNNING)
.preset(POWER_STATE.RUNNING)
.widget(),
]"
v-slot="{ properties }"
@@ -17,7 +17,7 @@
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import { prop } from "@/libs/story/story-param";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,19 @@
```vue-template
<button @click="open">Delete all items</button>
<UiModal v-if="isOpen" @close="close" :icon="faRemove">
<template #title>You are about to delete 12 items</template>
<template #subtitle>They'll be gone forever</template>
<template #buttons>
<UiButton @click="delete" color="error">Yes, delete</UiButton>
<UiButton @click="close">Cancel</UiButton>
</template>
</UiModal>
```
```vue-script
import { faRemove } from "@fortawesome/free-solid-svg-icons";
import { useModal } from "@composable/modal.composable";
const { open, close, isOpen } = useModal().
```

View File

@@ -1,32 +1,45 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
colorProp(),
iconProp(),
event('close').preset(close),
slot('default'),
slot('title'),
slot('subtitle'),
slot('default'),
slot('icon'),
slot('buttons').help('Meant to receive UiButton components'),
setting('title').preset('Modal Title').widget(),
setting('subtitle').preset('Modal Subtitle').widget(),
]"
v-slot="{ properties, settings }"
>
<ConfirmModalLayout v-bind="properties">
<UiButton type="button" @click="open">Open Modal</UiButton>
<UiModal v-bind="properties" v-if="isOpen">
<template #title>{{ settings.title }}</template>
<template #subtitle>{{ settings.subtitle }}</template>
<template #buttons>
<UiButton outlined>Discard</UiButton>
<UiButton>Go</UiButton>
<UiButton @click="close">Discard</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { iconProp, setting, slot } from "@/libs/story/story-param";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import {
colorProp,
event,
iconProp,
setting,
slot,
} from "@/libs/story/story-param";
const { open, close, isOpen } = useModal();
</script>
<style lang="postcss" scoped></style>

View File

@@ -47,7 +47,3 @@ export const IK_BUTTON_GROUP_TRANSPARENT = Symbol() as InjectionKey<
export const IK_CARD_GROUP_VERTICAL = Symbol() as InjectionKey<boolean>;
export const IK_INPUT_ID = Symbol() as InjectionKey<ComputedRef<string>>;
export const IK_MODAL_CLOSE = Symbol() as InjectionKey<() => void>;
export const IK_MODAL_NESTED = Symbol() as InjectionKey<boolean>;

View File

@@ -21,19 +21,3 @@ export interface XenApiAlarm<RelationType extends RawObjectType>
triggerLevel: number;
type: XenApiAlarmType;
}
export type XenApiPatch = {
$id: string;
name: string;
description: string;
license: string;
release: string;
size: number;
url: string;
version: string;
changelog: {
date: number;
description: string;
author: string;
};
};

View File

@@ -3,7 +3,7 @@
<UiCardGroup>
<PoolDashboardStatus />
<PoolDashboardAlarms class="alarms" />
<PoolDashboardHostsPatches />
<UiCardComingSoon title="Patches" />
</UiCardGroup>
<UiCardGroup>
<UiCardGroup>
@@ -36,12 +36,12 @@ import PoolDashboardTasks from "@/components/pool/dashboard/PoolDashboardTasks.v
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardHostsPatches from "@/components/pool/dashboard/PoolDashboardHostsPatches.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import UiCardComingSoon from "@/components/ui/UiCardComingSoon.vue";
import UiCardGroup from "@/components/ui/UiCardGroup.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";

View File

@@ -12,6 +12,7 @@
:available-filters="filters"
:available-sorts="filters"
:collection="vms"
id-property="$ref"
>
<template #head-row>
<ColumnHeader :icon="faPowerOff" />
@@ -22,15 +23,7 @@
<td>
<PowerStateIcon :state="vm.power_state" />
</td>
<td>
<div class="vm-name">
<UiSpinner
v-if="isMigrating(vm)"
v-tooltip="'This VM is being migrated'"
/>
{{ vm.name_label }}
</div>
</td>
<td>{{ vm.name_label }}</td>
<td>{{ vm.name_description }}</td>
</template>
</CollectionTable>
@@ -43,14 +36,11 @@ import ColumnHeader from "@/components/ColumnHeader.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { VM_OPERATION, VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { Filters } from "@/types/filter";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
@@ -62,7 +52,7 @@ const { t } = useI18n();
const titleStore = usePageTitleStore();
titleStore.setTitle(t("vms"));
const { records: vms, isOperationPending } = useVmCollection();
const { records: vms } = useVmCollection();
const { isMobile, isDesktop } = storeToRefs(useUiStore());
const filters: Filters = {
@@ -72,26 +62,17 @@ const filters: Filters = {
label: t("power-state"),
icon: faPowerOff,
type: "enum",
choices: Object.values(VM_POWER_STATE),
choices: Object.values(POWER_STATE),
},
};
const selectedVmsRefs = ref([]);
titleStore.setCount(() => selectedVmsRefs.value.length);
const isMigrating = (vm: XenApiVm) =>
isOperationPending(vm, VM_OPERATION.POOL_MIGRATE);
</script>
<style lang="postcss" scoped>
.pool-vms-view {
overflow: auto;
}
.vm-name {
display: inline-flex;
align-items: center;
gap: 1rem;
}
</style>

View File

@@ -110,19 +110,19 @@ const template = computed(() => {
]"
>
<${componentName} v-bind="properties"${
slotsNames.length > 0
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
)
.join("\n ")}
slotsNames.length > 0
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
)
.join("\n ")}
</${componentName}>`
: ` />`
}
: ` />`
}
</ComponentStory>
</template>

View File

@@ -34,7 +34,7 @@ import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useConsoleCollection } from "@/stores/xen-api/console.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
@@ -77,7 +77,7 @@ const hasError = computed(() => hasVmError.value || hasConsoleError.value);
const vm = computed(() => getVmByUuid(route.params.uuid as XenApiVm["uuid"]));
const isVmRunning = computed(
() => vm.value?.power_state === VM_POWER_STATE.RUNNING
() => vm.value?.power_state === POWER_STATE.RUNNING
);
const vmConsole = computed(() => {

View File

@@ -135,10 +135,10 @@ export default class Tasks extends EventEmitter {
*
* @returns {Task}
*/
create({ name, objectId, userId = this.#app.apiContext?.user?.id, type, ...props }) {
create({ name, objectId, userId = this.#app.apiContext?.user?.id, type }) {
const tasks = this.#tasks
const task = new Task({ properties: { ...props, name, objectId, userId, type }, onProgress: this.#onProgress })
const task = new Task({ properties: { name, objectId, userId, type }, onProgress: this.#onProgress })
// Use a compact, sortable, string representation of the creation date
//

View File

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

View File

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

View File

@@ -43,7 +43,7 @@
"pw": "^0.0.4",
"xdg-basedir": "^4.0.0",
"xo-lib": "^0.11.1",
"xo-vmdk-to-vhd": "^2.5.6"
"xo-vmdk-to-vhd": "^2.5.5"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -10,7 +10,7 @@
"@xen-orchestra/log": "^0.6.0",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"vhd-lib": "^4.6.1"
"vhd-lib": "^4.5.0"
},
"engines": {
"node": ">=14"

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "3.2.0",
"version": "3.1.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -33,8 +33,9 @@
"http-request-plus": "^1.0.0",
"json-rpc-protocol": "^0.13.2",
"lodash": "^4.17.15",
"node-ssh": "^13.1.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.6.1",
"vhd-lib": "^4.5.0",
"xo-common": "^0.8.0"
},
"private": false,

View File

@@ -9,7 +9,7 @@ import peekFooterFromStream from 'vhd-lib/peekFooterFromVhdStream.js'
import AggregateError from './_AggregateError.mjs'
const { error, warn } = createLogger('xo:xapi:sr')
const { warn } = createLogger('xo:xapi:sr')
const OC_MAINTENANCE = 'xo:maintenanceState'
@@ -146,22 +146,6 @@ class Sr {
}
}
async reclaimSpace(srRef) {
const result = await this.call('host.call_plugin', this.pool.master, 'trim', 'do_trim', {
sr_uuid: await this.getField('SR', srRef, 'uuid'),
})
// Error example:
// <?xml version="1.0" ?><trim_response><key_value_pair><key>errcode</key><value>TrimException</value></key_value_pair><key_value_pair><key>errmsg</key><value>blkdiscard: /dev/VG_XenStorage-f5775872-b5e7-98e5-488a-7194efdaf8f6/f5775872-b5e7-98e5-488a-7194efdaf8f6_trim_lv: BLKDISCARD ioctl failed: Operation not supported</value></key_value_pair></trim_response>
const errMatch = result?.match(/<key>errcode<\/key><value>(.*?)<\/value>.*<key>errmsg<\/key><value>(.*?)<\/value>/)
if (errMatch) {
error(result)
const err = new Error(errMatch[2])
err.code = errMatch[1]
throw err
}
}
async importVdi(
$defer,
ref,

View File

@@ -1,6 +1,8 @@
import CancelToken from 'promise-toolbox/CancelToken'
import pCatch from 'promise-toolbox/catch'
import pRetry from 'promise-toolbox/retry'
import net from 'node:net'
import { createLogger } from '@xen-orchestra/log'
import { decorateClass } from '@vates/decorate-with'
import { strict as assert } from 'node:assert'
@@ -13,7 +15,46 @@ import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from './index.mjs'
const { warn } = createLogger('xo:xapi:vdi')
const noop = Function.prototype
async function getTcpStream(host, xapi, vhdUuid) {
console.log({ vhdUuid })
// Host.call_plugin avec plugin=vdi-tools fn=expoty_vdi et argument uuid=<vdi-uuid> hostname=<hostname or ip> port=<port>
const XO_ADDRESS = '10.200.200.32'
// create tcp server
const server = net.createServer()
await new Promise(resolve => {
server.listen(0, () => {
resolve()
})
})
try {
const promise = new Promise((resolve, reject) => {
server.on('connection', clientSocket => {
console.log('client connected')
resolve(clientSocket)
clientSocket.on('end', () => {
console.log('client disconnected');
server.close()
});
clientSocket.on('error', err => {
console.log('client error', err)
server.close()
})
});
})
xapi.call('host.call_plugin', host.$ref, 'vdi-tools', 'export_vdi', { uuid: vhdUuid, hostname: XO_ADDRESS, port: '' + server.address().port })
.then(res => console.log({ res }))
.catch(err => console.error(err))
const stream = await promise
return stream
} catch (error) {
console.error(error)
console.log(error.call.params)
}
}
class Vdi {
async clone(vdiRef) {
return extractOpaqueRef(await this.callAsync('VDI.clone', vdiRef))
@@ -64,6 +105,8 @@ class Vdi {
})
}
async _getNbdClient(ref) {
const nbdInfos = await this.call('VDI.get_nbd_info', ref)
if (nbdInfos.length > 0) {
@@ -93,6 +136,19 @@ class Vdi {
assert.equal(format, 'vhd')
query.base = baseRef
} else {
// for now the direct export plugin does not support differential disks
try{
const vdi = this.getObject(ref)
const sr = this.getObject(vdi.SR)
const pbds = sr.PBDs.map(pbdUuid => this.getObject(pbdUuid))
const hosts = pbds.map(pbd => this.getObject(pbd.host))
return getTcpStream(hosts[0], this, vdi.uuid)
}catch(err){
// @todo : fall back to xapi export if plugin is not installed
throw err
}
}
let nbdClient, stream
try {
@@ -134,23 +190,22 @@ class Vdi {
if (stream.length === undefined) {
throw new Error('Trying to import a VDI without a length field. Please report this error to Xen Orchestra.')
}
const vdi = await this.getRecord('VDI', ref)
const sr = await this.getRecord('SR', vdi.SR)
try {
await this.putResource(cancelToken, stream, '/import_raw_vdi/', {
query: {
format,
vdi: ref,
},
task: await this.task_create(`Importing content into VDI ${vdi.name_label} on SR ${sr.name_label}`),
task: await this.task_create(`Importing content into VDI ${await this.getField('VDI', ref, 'name_label')}`),
})
} catch (error) {
// augment the error with as much relevant info as possible
const poolMaster = await this.getRecord('host', this.pool.master)
const [poolMaster, vdi] = await Promise.all([
this.getRecord('host', this.pool.master),
this.getRecord('VDI', ref),
])
error.pool_master = poolMaster
error.SR = sr
error.SR = await this.getRecord('SR', vdi.SR)
error.VDI = vdi
throw error
}

View File

@@ -1,59 +1,8 @@
# ChangeLog
## **5.87.0** (2023-09-29)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [Patches] Support new XenServer Updates system. See [our documentation](https://xen-orchestra.com/docs/updater.html#xenserver-updates). (PR [#7044](https://github.com/vatesfr/xen-orchestra/pull/7044))
- [Host/Advanced] New button to download system logs [#3968](https://github.com/vatesfr/xen-orchestra/issues/3968) (PR [#7048](https://github.com/vatesfr/xen-orchestra/pull/7048))
- [Home/Hosts, Pools] Display host brand and version (PR [#7027](https://github.com/vatesfr/xen-orchestra/pull/7027))
- [SR] Ability to reclaim space [#1204](https://github.com/vatesfr/xen-orchestra/issues/1204) (PR [#7054](https://github.com/vatesfr/xen-orchestra/pull/7054))
- [XOA] New button to restart XO Server directly from the UI (PR [#7056](https://github.com/vatesfr/xen-orchestra/pull/7056))
- [Host/Advanced] Display system disks health based on the _smartctl_ plugin. [#4458](https://github.com/vatesfr/xen-orchestra/issues/4458) (PR [#7060](https://github.com/vatesfr/xen-orchestra/pull/7060))
- [Authentication] Failed attempts are now logged as XO tasks (PR [#7061](https://github.com/vatesfr/xen-orchestra/pull/7061))
- [Backup] Prevent VMs from being migrated while they are backed up (PR [#7024](https://github.com/vatesfr/xen-orchestra/pull/7024))
- [Backup] Prevent VMs from being backed up while they are migrated (PR [#7024](https://github.com/vatesfr/xen-orchestra/pull/7024))
### Enhancements
- [Netbox] Don't delete VMs that have been created manually in XO-synced cluster [Forum#7639](https://xcp-ng.org/forum/topic/7639) (PR [#7008](https://github.com/vatesfr/xen-orchestra/pull/7008))
- [Kubernetes] _Search domains_ field is now optional [#7028](https://github.com/vatesfr/xen-orchestra/pull/7028)
- [REST API] Hosts' audit and system logs can be downloaded [#3968](https://github.com/vatesfr/xen-orchestra/issues/3968) (PR [#7048](https://github.com/vatesfr/xen-orchestra/pull/7048))
### Bug fixes
- [Backup/Restore] Fix `Cannot read properties of undefined (reading 'id')` error when restoring via an XO Proxy (PR [#7026](https://github.com/vatesfr/xen-orchestra/pull/7026))
- [Google/GitHub Auth] Fix `Internal Server Error` (xo-server: `Cannot read properties of undefined (reading 'id')`) when logging in with Google or GitHub [Forum#7729](https://xcp-ng.org/forum/topic/7729) (PRs [#7031](https://github.com/vatesfr/xen-orchestra/pull/7031) [#7032](https://github.com/vatesfr/xen-orchestra/pull/7032))
- [Jobs] Fix schedules not being displayed on first load [#6968](https://github.com/vatesfr/xen-orchestra/issues/6968) (PR [#7034](https://github.com/vatesfr/xen-orchestra/pull/7034))
- [OVA Export] Fix support of disks with more than 8.2GiB of content (PR [#7047](https://github.com/vatesfr/xen-orchestra/pull/7047))
- [Backup] Fix `VHDFile implementation is not compatible with encrypted remote` when using VHD directory with encryption (PR [#7045](https://github.com/vatesfr/xen-orchestra/pull/7045))
- [Backup/Mirror] Fix `xo:fs:local WARN lock compromised` when mirroring a Backup Repository to a local/NFS/SMB repository ([#7043](https://github.com/vatesfr/xen-orchestra/pull/7043))
- [Ova import] Fix importing VM with collision in disk position (PR [#7051](https://github.com/vatesfr/xen-orchestra/pull/7051)) (issue [7046](https://github.com/vatesfr/xen-orchestra/issues/7046))
- [Backup/Mirror] Fix backup report not being sent (PR [#7049](https://github.com/vatesfr/xen-orchestra/pull/7049))
- [New VM] Only add MBR to cloud-init drive on Windows VMs to avoid booting issues (e.g. with Talos) (PR [#7050](https://github.com/vatesfr/xen-orchestra/pull/7050))
- [VDI Import] Add the SR name to the corresponding XAPI task (PR [#6979](https://github.com/vatesfr/xen-orchestra/pull/6979))
### Released packages
- xo-vmdk-to-vhd 2.5.6
- xo-server-auth-github 0.3.1
- xo-server-auth-google 0.3.1
- xo-server-netbox 1.3.0
- vhd-lib 4.6.1
- @xen-orchestra/xapi 3.2.0
- @xen-orchestra/backups 0.43.0
- @xen-orchestra/backups-cli 1.0.13
- @xen-orchestra/mixins 0.13.0
- @xen-orchestra/proxy 0.26.35
- xo-server 5.124.0
- xo-server-backup-reports 0.17.4
- xo-web 5.126.0
## **5.86.1** (2023-09-07)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Bug fixes
@@ -116,6 +65,8 @@
## **5.85.0** (2023-07-31)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Highlights
- [Import/From VMWare] Support ESXi 6.5+ with snapshot (PR [#6909](https://github.com/vatesfr/xen-orchestra/pull/6909))

View File

@@ -7,10 +7,19 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Netbox] Don't delete VMs that have been created manually in XO-synced cluster [Forum#7639](https://xcp-ng.org/forum/topic/7639) (PR [#7008](https://github.com/vatesfr/xen-orchestra/pull/7008))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Backup/Restore] Fix `Cannot read properties of undefined (reading 'id')` error when restoring via an XO Proxy (PR [#7026](https://github.com/vatesfr/xen-orchestra/pull/7026))
- [Google/GitHub Auth] Fix `Internal Server Error` (xo-server: `Cannot read properties of undefined (reading 'id')`) when logging in with Google or GitHub [Forum#7729](https://xcp-ng.org/forum/topic/7729) (PRs [#7031](https://github.com/vatesfr/xen-orchestra/pull/7031) [#7032](https://github.com/vatesfr/xen-orchestra/pull/7032))
- [Jobs] Fix schedules not being displayed on first load [#6968](https://github.com/vatesfr/xen-orchestra/issues/6968) (PR [#7034](https://github.com/vatesfr/xen-orchestra/pull/7034))
- [OVA Export] Fix support of disks with more than 8.2GiB of content (PR [#7047](https://github.com/vatesfr/xen-orchestra/pull/7047))
- [Backup] Fix `VHDFile implementation is not compatible with encrypted remote` when using VHD directory with encryption (PR [#7045](https://github.com/vatesfr/xen-orchestra/pull/7045))
- [Backup/Mirror] Fix `xo:fs:local WARN lock compromised` when mirroring a Backup Repository to a local/NFS/SMB repository ([#7043](https://github.com/vatesfr/xen-orchestra/pull/7043))
### Packages to release
> When modifying a package, add it here with its release type.
@@ -27,4 +36,12 @@
<!--packages-start-->
- @xen-orchestra/backups patch
- vhd-lib minor
- xo-server patch
- xo-server-auth-github patch
- xo-server-auth-google patch
- xo-server-netbox minor
- xo-web patch
<!--packages-end-->

View File

@@ -6,6 +6,7 @@ ISC License may be found here: https://www.isc.org/licenses/
The texts of the two licenses are inserted at the root of this repo.
Below is the list of the various components and their corresponding licenses, AGPL or ISC.
- @xen-orchestra/audit-core - AGPL-3.0-or-later
- @xen-orchestra/babel-config - AGPL-3.0-or-later
- @xen-orchestra/backups - AGPL-3.0-or-later
@@ -50,6 +51,7 @@ Below is the list of the various components and their corresponding licenses, AG
- xo-vmdk-to-vhd - AGPL-3.0-or-later
- xo-web - AGPL-3.0-or-later
- @vates/async-each - ISC
- @vates/cached-dns.lookup - ISC
- @vates/coalesce-calls - ISC

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -654,7 +654,7 @@ List of the VMs with more than the recommended amount of snapshots (3). There is
### Duplicated MAC addresses
Machines with the same MAC addresses on a network will result in unexpected behavior if they run simultaneously.
Machines with the same MAC addresses on a network will result in unexpected behavior if they run simultaneously.
### Guest Tools status

View File

@@ -125,21 +125,3 @@ If you can't fetch updates, perform a few checks from your XOA:
- if not, check your `/etc/resolv.conf` file and modify it if necessary (give a correct DNS server)
- use `ifconfig` to check your network configuration
- check your firewall(s) and allow XOA to reach xen-orchestra.com (port 443)
## XenServer Updates
Starting September 2023, XenServer Updates require authentication:
1. Make sure your XenServer hosts have [the proper licenses](https://docs.xenserver.com/en-us/citrix-hypervisor/overview-licensing.html)
2. Go to any XenServer Update URL like [this one](https://support.citrix.com/article/CTX277443/hotfix-xs81e006-for-citrix-hypervisor-81) and log in to check that your account has permissions to download updates. You should see a "Download" button.
3. Go to this URL: [https://support.citrix.com/xencenterclientiddownload](https://support.citrix.com/xencenterclientiddownload) and click "Download Client ID"
![Download XenServer Client ID](./assets/xs-client-id-download.png)
4. In Xen Orchestra, go to your User Settings page (bottom left-hand corner) and upload the file `xencenter_client_id.json` you just downloaded from the "XenServer Client ID" section
![Upload XenServer Client ID](./assets/xs-client-id-upload.png)
5. Go to a pool's "Patches" page. You can now install XenServer Updates. If you get a `LICENCE_RESTRICTION` error, it means that [you're missing XenServer licenses on your hosts](https://docs.xenserver.com/en-us/citrix-hypervisor/overview-licensing.html).

View File

@@ -111,21 +111,21 @@ Save the configuration and then activate the plugin (button on top).
### GitHub
This plugin allows any GitHub user to authenticate to Xen Orchestra.
This plugin allows any GitHub user to authenticate to Xen-Orchestra.
The first time a user signs in, XO will create a new XO user with the same identifier (i.e. GitHub name), with _user_ permissions. An existing admin will need to apply the appropriate permissions for your environment.
First you need to configure a new app in your GitHub account. Go to your Github settings > "Developer Settings" > "OAuth Apps" > "New OAuth App".
First you need to configure a new app in your GitHub account:
![](https://raw.githubusercontent.com/vatesfr/xen-orchestra/master/packages/xo-server-auth-github/github.png)
Go to your Github settings > "Developer Settings" > "OAuth Apps" > "New OAuth App".
1. Name your GitHub application under "Application Name".
2. Enter your Xen Orchestra URL (or IP) under "Homepage URL"
3. Add your "Authorization callback URL" (for example, https://homepageUrl/signin/github/callback)
![](./assets/auth-github-form.png)
When you get your Client ID and your Client secret, you can configure them in the GitHub Plugin inside the "Settings/Plugins" view of Xen Orchestra.
![](./assets/auth-github-secret.png)
When you get your `clientID` and your `clientSecret`, you can configure them in the GitHub Plugin inside the "Settings/Plugins" view of Xen Orchestra.
Be sure to activate the plugin after you save the configuration (button on top). When it's done, you'll see a link in the login view, this is where you'll go to authenticate:

View File

@@ -1,30 +1,46 @@
'use strict'
const { VhdFile, checkVhdChain } = require('vhd-lib')
const { openVhd, VhdSynthetic } = require('vhd-lib')
const getopts = require('getopts')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { resolve } = require('path')
const { Disposable } = require('promise-toolbox')
const checkVhd = (handler, path) => new VhdFile(handler, path).readHeaderAndFooter()
module.exports = async function check(rawArgs) {
const { chain, _: args } = getopts(rawArgs, {
boolean: ['chain'],
const { chain, bat, blocks, remote, _: args } = getopts(rawArgs, {
boolean: ['chain', 'bat', 'blocks'],
default: {
chain: false,
bat: false,
blocks: false
},
})
const check = chain ? checkVhdChain : checkVhd
await Disposable.use(getSyncedHandler({ url: 'file:///' }), async handler => {
for (const vhd of args) {
try {
await check(handler, resolve(vhd))
console.log('ok:', vhd)
} catch (error) {
console.error('nok:', vhd, error)
const vhdPath = args[0]
await Disposable.factory( async function * open(remote, vhdPath) {
const handler = yield getSyncedHandler({url : remote ?? 'file:///'})
const vhd = chain? yield VhdSynthetic.fromVhdChain(handler, vhdPath) : yield openVhd(handler, vhdPath)
await vhd.readBlockAllocationTable()
if(bat){
const nBlocks = vhd.header.maxTableEntries
let nbErrors = 0
for (let blockId = 0; blockId < nBlocks; ++blockId) {
if(!vhd.containsBlock(blockId)){
continue
}
const ok = await vhd.checkBlock(blockId)
if(!ok){
console.warn(`block ${blockId} is invalid`)
nbErrors ++
}
}
console.log('BAT check done ', nbErrors === 0 ? 'OK': `${nbErrors} block(s) faileds`)
}
})
if(blocks){
for await(const _ of vhd.blocks()){
}
console.log('Blocks check done')
}
})(remote, vhdPath)
}

View File

@@ -31,7 +31,7 @@
"lodash": "^4.17.21",
"promise-toolbox": "^0.21.0",
"uuid": "^9.0.0",
"vhd-lib": "^4.6.1"
"vhd-lib": "^4.5.0"
},
"scripts": {
"postversion": "npm publish",

View File

@@ -407,4 +407,14 @@ exports.VhdAbstract = class VhdAbstract {
assert.strictEqual(copied, length, 'invalid length')
return copied
}
_checkBlock() {
throw new Error('not implemented')
}
// check if a block is ok without reading it
// there still can be error when reading the block later (if it's deleted, if right are incorrects,...)
async checkBlock(blockId) {
return this._checkBlock(blockId)
}
}

View File

@@ -96,11 +96,8 @@ describe('VhdDirectory', async () => {
for await (const block of vhd.blocks()) {
await compressedVhd.writeEntireBlock(block)
}
await Promise.all[
(await compressedVhd.writeHeader(),
await compressedVhd.writeFooter(),
await compressedVhd.writeBlockAllocationTable())
]
await Promise
.all[(await compressedVhd.writeHeader(), await compressedVhd.writeFooter(), await compressedVhd.writeBlockAllocationTable())]
// compressed vhd have a metadata file
assert.equal(await fs.exists(`${tempDir}/compressed.vhd/chunk-filters.json`), true)

View File

@@ -317,4 +317,9 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
})
this.#compressor = getCompressor(chunkFilters[0])
}
async _checkBlock(blockId){
const path = this._getFullBlockPath(blockId)
return this._handler.exists(path)
}
}

View File

@@ -469,4 +469,8 @@ exports.VhdFile = class VhdFile extends VhdAbstract {
async getSize() {
return await this._handler.getSize(this._path)
}
_checkBlock(blockId){
return true
}
}

View File

@@ -113,6 +113,10 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
return vhd?._getFullBlockPath(blockId)
}
_checkBlock(blockId) {
const vhd = this.#getVhdWithBlock(blockId)
return vhd?._checkBlock(blockId) ?? false
}
// return true if all the vhds ar an instance of cls
checkVhdsClass(cls) {
return this.#vhds.every(vhd => vhd instanceof cls)

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vhd-lib",
"version": "4.6.1",
"version": "4.5.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",

View File

@@ -25,16 +25,8 @@ async function checkFile(vhdName) {
// Since the qemu-img check command isn't compatible with vhd format, we use
// the convert command to do a check by conversion. Indeed, the conversion will
// fail if the source file isn't a proper vhd format.
const target = vhdName + '.qcow2'
try {
await execa('qemu-img', ['convert', '-fvpc', '-Oqcow2', vhdName, target])
} finally {
try {
await fsPromise.unlink(target)
} catch (err) {
console.warn(err)
}
}
await execa('qemu-img', ['convert', '-fvpc', '-Oqcow2', vhdName, 'outputFile.qcow2'])
await fsPromise.unlink('./outputFile.qcow2')
}
exports.checkFile = checkFile

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