Compare commits

..

1 Commits

Author SHA1 Message Date
Thierry
57a57f4391 feat(lite): rework subscriptions 2023-08-03 16:21:49 +02:00
123 changed files with 2850 additions and 2892 deletions

View File

@@ -1,11 +1,8 @@
'use strict'
module.exports = {
arrowParens: 'avoid',
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
//

View File

@@ -9,7 +9,7 @@
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.40.0",
"@xen-orchestra/fs": "^4.0.1",
"filenamify": "^6.0.0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0"

View File

@@ -683,17 +683,11 @@ export class RemoteAdapter {
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
const container = watchStreamSize(input)
const handler = this._handler
await handler.outputStream(path, input, {
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
async validator(tmpPath) {
async validator() {
await input.task
// size on file system can be bigger when encrypted ( IV + alignment padding)
const size = await handler.getSize(tmpPath, { exact: false })
if (Math.abs(size - container.size) > handler.getSizeApproximationMargin()) {
return false
}
return validator.apply(this, arguments)
},
})

View File

@@ -21,12 +21,7 @@ export class RestoreMetadataBackup {
})
} else {
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
const dataFileName = resolve(backupId, metadata.data ?? 'data.json')
const data = await handler.readFile(dataFileName)
// if data is JSON, sent it as a plain string, otherwise, consider the data as binary and encode it
const isJson = dataFileName.endsWith('.json')
return isJson ? data.toString() : { encoding: 'base64', data: data.toString('base64') }
return String(await handler.readFile(resolve(backupId, metadata.data ?? 'data.json')))
}
}
}

View File

@@ -22,13 +22,7 @@ export class XoMetadataBackup {
const dir = `${scheduleDir}/${formatFilenameDate(timestamp)}`
const data = job.xoMetadata
let dataBaseName = './data'
// JSON data is sent as plain string, binary data is sent as an object with `data` and `encoding properties
const isJson = typeof data === 'string'
if (isJson) {
dataBaseName += '.json'
}
const dataBaseName = './data.json'
const metadata = JSON.stringify(
{
@@ -60,7 +54,7 @@ export class XoMetadataBackup {
async () => {
const handler = adapter.handler
const dirMode = this._config.dirMode
await handler.outputFile(dataFileName, isJson ? data : Buffer.from(data.data, data.encoding), { dirMode })
await handler.outputFile(dataFileName, data, { dirMode })
await handler.outputFile(metaDataFileName, metadata, {
dirMode,
})

View File

@@ -18,7 +18,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
const vdiRefs = await xapi.VM_getDisks(baseVm.$ref)
for (const vdiRef of vdiRefs) {
const vdi = xapi.getObject(vdiRef)
if (vdi.$SR.uuid !== this._healthCheckSr.uuid) {
if (vdi.$SR.uuid !== this._heathCheckSr.uuid) {
return false
}
}

View File

@@ -30,8 +30,8 @@
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^6.0.0",
"d3-time-format": "^4.1.0",
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"decorator-synchronized": "^0.6.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",

View File

@@ -227,19 +227,11 @@ export default class RemoteHandlerAbstract {
// when using encryption, the file size is aligned with the encryption block size ( 16 bytes )
// that means that the size will be 1 to 16 bytes more than the content size + the initialized vector length (16 bytes)
async getSize(file, { exact = true } = {}) {
exact && assert.strictEqual(this.isEncrypted, false, `Can't compute size of an encrypted file ${file}`)
async getSize(file) {
assert.strictEqual(this.isEncrypted, false, `Can't compute size of an encrypted file ${file}`)
const size = await timeout.call(this._getSize(typeof file === 'string' ? normalizePath(file) : file), this._timeout)
return size
}
getSizeApproximationMargin() {
if (this.isEncrypted) {
// on block for initialization vector + at most 1 bloc - 1 byte for aligment padding
return this.#encryptor.ivLength * 2 - 1
}
return 0
return size - this.#encryptor.ivLength
}
async __list(dir, { filter, ignoreMissing = false, prependDir = false } = {}) {

View File

@@ -1,4 +1,2 @@
// Keeping this file to prevent applying the global monorepo config for now
module.exports = {
trailingComma: "es5",
};
module.exports = {};

View File

@@ -1,144 +0,0 @@
<!-- TOC -->
- [XenApiCollection](#xenapicollection)
- [Get the collection](#get-the-collection)
- [Defer the subscription](#defer-the-subscription)
- [Create a dedicated collection](#create-a-dedicated-collection)
- [Alter the collection](#alter-the-collection)
_ [Example 1: Adding props to records](#example-1-adding-props-to-records)
_ [Example 2: Adding props to the collection](#example-2-adding-props-to-the-collection) \* [Example 3, filtering and sorting the collection](#example-3-filtering-and-sorting-the-collection)
<!-- TOC -->
# XenApiCollection
## Get the collection
To retrieve a collection, invoke `useXenApiCollection("VM")`.
By doing this, the current component will be automatically subscribed to the collection and will be updated when the
collection changes.
When the component is unmounted, the subscription will be automatically stopped.
## Defer the subscription
If you don't want to fetch the data of the collection when the component is mounted, you can pass `{ immediate: false }`
as options: `const { start, isStarted } = useXenApiCollection("VM", { immediate: false })`.
Then you subscribe to the collection by calling `start()`.
## Create a dedicated collection
It is recommended to create a dedicated collection composable for each type of record you want to use.
They are stored in `src/composables/xen-api-collection/*-collection.composable.ts`.
```typescript
// src/composables/xen-api-collection/console-collection.composable.ts
export const useConsoleCollection = () => useXenApiCollection("console");
```
If you want to allow the user to defer the subscription, you can propagate the options to `useXenApiCollection`.
```typescript
// console-collection.composable.ts
export const useConsoleCollection = <
Immediate extends boolean = true,
>(options?: {
immediate?: Immediate;
}) => useXenApiCollection("console", options);
```
```typescript
// MyComponent.vue
const collection = useConsoleCollection({ immediate: false });
setTimeout(() => collection.start(), 10000);
```
## Alter the collection
You can alter the collection by overriding parts of it.
### Example 1: Adding props to records
```typescript
// xen-api.ts
export interface XenApiConsole extends XenApiRecord<"console"> {
// ... existing props
someProp: string;
someOtherProp: number;
}
```
```typescript
// console-collection.composable.ts
export const useConsoleCollection = () => {
const collection = useXenApiCollection("console");
const records = computed(() => {
return collection.records.value.map((console) => ({
...console,
someProp: "Some value",
someOtherProp: 42,
}));
});
return {
...collection,
records,
};
};
```
```typescript
const consoleCollection = useConsoleCollection();
consoleCollection.getByUuid("...").someProp; // "Some value"
```
### Example 2: Adding props to the collection
```typescript
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
return {
...collection,
runningVms: computed(() =>
collection.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
};
};
```
### Example 3, filtering and sorting the collection
```typescript
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
return {
...collection,
records: computed(() =>
collection.records.value
.filter(
(vm) =>
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
)
.sort((vm1, vm2) => vm1.name_label.localeCompare(vm2.name_label))
),
};
};
```

View File

@@ -0,0 +1,220 @@
# Stores for XenApiRecord collections
All collections of `XenApiRecord` are stored inside the `xapiCollectionStore`.
To retrieve a collection, invoke `useXapiCollectionStore().get(type)`.
## TL;DR - How to extend a subscription
_**Note:** Once the extension grows in complexity, it's recommended to create a dedicated file for it (e.g. `host.extension.ts` for `host.store.ts`)._
```typescript
type MyExtension1 = Extension<{ propA: string }>;
type MyExtension2 = Extension<{ propB: string }, { withB: true }>;
type Extensions = [
XenApiRecordExtension<XenApiHost>, // If needed
DeferExtension, // If needed
MyExtension1,
MyExtension2
];
export const useHostStore = defineStore("host", () => {
const hostCollection = useXapiCollectionStore().get("console");
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const originalSubscription = hostCollection.subscribe(options);
const myExtension1: PartialSubscription<MyExtension1> = {
propA: "Hello",
};
const myExtension2: PartialSubscription<MyExtension2> | undefined =
options?.withB
? {
propB: "World",
}
: undefined;
return {
...originalSubscription,
...myExtension1,
...myExtension2,
};
};
return {
...hostCollection,
subscribe,
};
});
```
## Accessing a collection
In order to use a collection, you'll need to subscribe to it.
```typescript
const consoleStore = useXapiCollectionStore().get("console");
const { records, getByUuid /* ... */ } = consoleStore.subscribe();
```
## Deferred subscription
If you wish to initialize the subscription on demand, you can pass `{ immediate: false }` as options to `subscribe()`.
```typescript
const consoleStore = useXapiCollectionStore().get("console");
const { records, start, isStarted /* ... */ } = consoleStore.subscribe({
immediate: false,
});
// Later, you can then use start() to initialize the subscription.
```
## Create a dedicated store for a collection
To create a dedicated store for a specific `XenApiRecord`, simply return the collection from the XAPI Collection Store:
```typescript
export const useConsoleStore = defineStore("console", () =>
useXapiCollectionStore().get("console")
);
```
## Extending the base Subscription
To extend the base Subscription, you'll need to override the `subscribe` method.
### Define the extensions
Subscription extensions are defined as a simple extension (`Extension<object>`) or as a conditional
extension (`Extension<object, object>`).
When using a conditional extension, the corresponding `object` type will be added to the subscription only if
the the options passed to `subscribe(options)` do match the second argument or `Extension`.
There is two existing extensions:
- `XenApiRecordExtension<T extends XenApiRecord>`: a simple extension which defined all the base
properties and methods (`records`, `getByOpaqueRef`, `getByUuid`, etc.)
- `DeferExtension`: a conditional extension which add the `start` and `isStarted` properties if the
`immediate` option is set to `false`.
```typescript
// Always present extension
type PropABExtension = Extension<{
propA: string;
propB: ComputedRef<number>;
}>;
// Conditional extension 1
type PropCExtension = Extension<
{ propC: ComputedRef<string> }, // <- This signature will be added
{ optC: string } // <- if this condition is met
>;
// Conditional extension 2
type PropDExtension = Extension<
{ propD: () => void }, // <- This signature will be added
{ optD: number } // <- if this condition is met
>;
// Create the extensions array
type Extensions = [
XenApiRecordExtension<XenApiHost>,
DeferExtension,
PropABExtension,
PropCExtension,
PropDExtension
];
```
### Define the `subscribe` method
You can then create the `subscribe` function with the help of `Options` and `Subscription` helper types.
This will allow to get correct completion and type checking for the `options` argument, and to get the correct return
type based on passed options.
```typescript
const subscribe = <O extends Options<Extensions>>(options?: O) => {
return {
// ...
} as Subscription<Extensions, O>;
};
```
### Extend the subscription
The `PartialSubscription` type will help to define and check the data to add to subscription for each extension.
```typescript
export const useConsoleStore = defineStore("console", () => {
const consoleCollection = useXapiCollectionStore().get("console");
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const originalSubscription = consoleCollection.subscribe(options);
const propABSubscription: PartialSubscription<PropABExtension> = {
propA: "Some string",
propB: computed(() => 42),
};
const propCSubscription: PartialSubscription<PropCExtension> | undefined =
options?.optC !== undefined
? {
propC: computed(() => "Some other string"),
}
: undefined;
const propDSubscription: PartialSubscription<PropDExtension> | undefined =
options?.optD !== undefined
? {
propD: () => console.log("Hello"),
}
: undefined;
return {
...originalSubscription,
...propABSubscription,
...propCSubscription,
...propDSubscription,
};
};
return {
...consoleCollection,
subscribe,
};
});
```
The generated `subscribe` method will then automatically have the following `options` signature:
```typescript
type Options = {
immediate?: false;
optC?: string;
optD?: number;
};
```
### Use the subscription
```typescript
const store = useConsoleStore();
// No options (Contains common properties: `propA`, `propB`, `records`, `getByUuid`, etc.)
const subscription1 = store.subscribe();
// optC option (Contains common properties + `propC`)
const subscription2 = store.subscribe({ optC: "Hello" });
// optD option (Contains common properties + `propD`)
const subscription3 = store.subscribe({ optD: 12 });
// optC and optD options (Contains common properties + `propC` + `propD`)
const subscription4 = store.subscribe({ optC: "Hello", optD: 12 });
```

View File

@@ -49,7 +49,7 @@
"@rushstack/eslint-patch": "^1.1.0",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",

View File

@@ -23,7 +23,7 @@ import AppNavigation from "@/components/AppNavigation.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import UnreachableHostsModal from "@/components/UnreachableHostsModal.vue";
import { useChartTheme } from "@/composables/chart-theme.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
@@ -42,9 +42,7 @@ if (link == null) {
link.href = favicon;
const xenApiStore = useXenApiStore();
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
useChartTheme();
const uiStore = useUiStore();

View File

@@ -27,14 +27,14 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
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 { useHostStore } from "@/stores/host.store";
const { records: hosts } = useHostCollection();
const { records: hosts } = useHostStore().subscribe();
const unreachableHostsUrls = ref<Set<string>>(new Set());
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);

View File

@@ -2,80 +2,32 @@
<RouterLink :to="{ name: 'story' }">
<UiTitle type="h4">Stories</UiTitle>
</RouterLink>
<StoryMenuTree
:tree="tree"
@toggle-directory="toggleDirectory"
:opened-directories="openedDirectories"
/>
<ul class="links">
<li v-for="route in routes" :key="route.name">
<RouterLink class="link" :to="route">
{{ route.meta.storyTitle }}
</RouterLink>
</li>
</ul>
</template>
<script lang="ts" setup>
import StoryMenuTree from "@/components/component-story/StoryMenuTree.vue";
import { useRouter } from "vue-router";
import UiTitle from "@/components/ui/UiTitle.vue";
import { type RouteRecordNormalized, useRoute, useRouter } from "vue-router";
import { ref } from "vue";
const { getRoutes } = useRouter();
const routes = getRoutes().filter((route) => route.meta.isStory);
export type StoryTree = Map<
string,
{ path: string; directory: string; children: StoryTree }
>;
function createTree(routes: RouteRecordNormalized[]) {
const tree: StoryTree = new Map();
for (const route of routes) {
const parts = route.path.slice(7).split("/");
let currentNode = tree;
let currentPath = "";
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
if (!currentNode.has(part)) {
currentNode.set(part, {
children: new Map(),
path: route.path,
directory: currentPath,
});
}
currentNode = currentNode.get(part)!.children;
}
}
return tree;
}
const tree = createTree(routes);
const currentRoute = useRoute();
const getDefaultOpenedDirectories = (): Set<string> => {
if (!currentRoute.meta.isStory) {
return new Set<string>();
}
const openedDirectories = new Set<string>();
const parts = currentRoute.path.split("/");
let currentPath = "";
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
openedDirectories.add(currentPath);
}
return openedDirectories;
};
const openedDirectories = ref(getDefaultOpenedDirectories());
const toggleDirectory = (directory: string) => {
if (openedDirectories.value.has(directory)) {
openedDirectories.value.delete(directory);
} else {
openedDirectories.value.add(directory);
}
};
</script>
<style lang="postcss" scoped>
.links {
padding: 1rem;
}
.link {
display: inline-block;
padding: 0.5rem 1rem;
text-decoration: none;
font-size: 1.6rem;
}
</style>

View File

@@ -1,83 +0,0 @@
<template>
<ul class="story-menu-tree">
<li v-for="[key, node] in tree" :key="key">
<span
v-if="node.children.size > 0"
class="directory"
@click="emit('toggle-directory', node.directory)"
>
<UiIcon
:icon="isOpen(node.directory) ? faFolderOpen : faFolderClosed"
/>
{{ formatName(key) }}
</span>
<RouterLink v-else :to="node.path" class="link">
<UiIcon :icon="faFile" />
{{ formatName(key) }}
</RouterLink>
<StoryMenuTree
v-if="isOpen(node.directory)"
:tree="node.children"
@toggle-directory="emit('toggle-directory', $event)"
:opened-directories="openedDirectories"
/>
</li>
</ul>
</template>
<script lang="ts" setup>
import type { StoryTree } from "@/components/component-story/StoryMenu.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import {
faFile,
faFolderClosed,
faFolderOpen,
} from "@fortawesome/free-regular-svg-icons";
const props = defineProps<{
tree: StoryTree;
openedDirectories: Set<string>;
}>();
const emit = defineEmits<{
(event: "toggle-directory", directory: string): void;
}>();
const isOpen = (directory: string) => props.openedDirectories.has(directory);
const formatName = (name: string) => {
const parts = name.split("-");
return parts.map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
};
</script>
<style lang="postcss" scoped>
.story-menu-tree {
padding-left: 1rem;
.story-menu-tree {
padding-left: 2.2rem;
}
}
.directory {
font-weight: 500;
}
.link {
padding: 0.5rem 0;
}
.directory {
padding: 0.5rem 0;
}
.link,
.directory {
cursor: pointer;
text-decoration: none;
font-size: 1.6rem;
display: inline-block;
}
</style>

View File

@@ -28,10 +28,10 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
import {
faAngleDown,
@@ -46,10 +46,11 @@ const props = defineProps<{
hostOpaqueRef: XenApiHost["$ref"];
}>();
const { getByOpaqueRef } = useHostCollection();
const { getByOpaqueRef } = useHostStore().subscribe();
const host = computed(() => getByOpaqueRef(props.hostOpaqueRef));
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
const isPoolMaster = computed(() => pool.value?.master === props.hostOpaqueRef);
const uiStore = useUiStore();

View File

@@ -16,9 +16,9 @@
<script lang="ts" setup>
import InfraHostItem from "@/components/infra/InfraHostItem.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useHostStore } from "@/stores/host.store";
const { records: hosts, isReady, hasError } = useHostCollection();
const { records: hosts, isReady, hasError } = useHostStore().subscribe();
</script>
<style lang="postcss" scoped>

View File

@@ -28,10 +28,10 @@ import InfraHostList from "@/components/infra/InfraHostList.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
const { isReady, hasError, pool } = usePoolCollection();
const { isReady, hasError, pool } = usePoolStore().subscribe();
</script>
<style lang="postcss" scoped>

View File

@@ -19,8 +19,8 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiVm } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { useIntersectionObserver } from "@vueuse/core";
import { computed, ref } from "vue";
@@ -29,7 +29,7 @@ const props = defineProps<{
vmOpaqueRef: XenApiVm["$ref"];
}>();
const { getByOpaqueRef } = useVmCollection();
const { getByOpaqueRef } = useVmStore().subscribe();
const vm = computed(() => getByOpaqueRef(props.vmOpaqueRef));
const rootElement = ref();
const isVisible = ref(false);

View File

@@ -11,8 +11,8 @@
<script lang="ts" setup>
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -20,7 +20,7 @@ const props = defineProps<{
hostOpaqueRef?: XenApiHost["$ref"];
}>();
const { isReady, recordsByHostRef, hasError } = useVmCollection();
const { isReady, recordsByHostRef, hasError } = useVmStore().subscribe();
const vms = computed(() =>
recordsByHostRef.value.get(

View File

@@ -5,12 +5,12 @@
</template>
<script lang="ts" setup>
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { computed } from "vue";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
import TitleBar from "@/components/TitleBar.vue";
import { usePoolStore } from "@/stores/pool.store";
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
const name = computed(() => pool.value?.name_label ?? "...");
</script>

View File

@@ -33,7 +33,7 @@
<script lang="ts" setup>
import RouterTab from "@/components/RouterTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
const { pool, isReady } = usePoolCollection();
const { pool, isReady } = usePoolStore().subscribe();
</script>

View File

@@ -37,11 +37,12 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { useVmMetricsCollection } from "@/composables/xen-api-collection/vm-metrics-collection.composable";
import { percent } from "@/libs/utils";
import { POWER_STATE } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { useVmMetricsStore } from "@/stores/vm-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
@@ -51,16 +52,18 @@ const {
hasError: hostStoreHasError,
isReady: isHostStoreReady,
runningHosts,
} = useHostCollection();
} = useHostStore().subscribe({
hostMetricsSubscription: useHostMetricsStore().subscribe(),
});
const {
hasError: vmStoreHasError,
isReady: isVmStoreReady,
records: vms,
} = useVmCollection();
} = useVmStore().subscribe();
const { getByOpaqueRef: getVmMetrics, isReady: isVmMetricsStoreReady } =
useVmMetricsCollection();
useVmMetricsStore().subscribe();
const nPCpu = computed(() =>
runningHosts.value.reduce(

View File

@@ -11,20 +11,20 @@
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import UiSpinner from "@/components/ui/UiSpinner.vue";
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -12,21 +12,21 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject } from "vue";
import type { ComputedRef } from "vue";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import type { Stat } from "@/composables/fetch-stats.composable";
import UiSpinner from "@/components/ui/UiSpinner.vue";
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -26,21 +26,21 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSeparator from "@/components/ui/UiSeparator.vue";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
const {
isReady: isVmReady,
records: vms,
hasError: hasVmError,
} = useVmCollection();
} = useVmStore().subscribe();
const {
isReady: isHostMetricsReady,
records: hostMetrics,
hasError: hasHostMetricsError,
} = useHostMetricsCollection();
} = useHostMetricsStore().subscribe();
const hasError = computed(() => hasVmError.value || hasHostMetricsError.value);

View File

@@ -23,11 +23,11 @@ import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useSrCollection } from "@/composables/xen-api-collection/sr-collection.composable";
import { useSrStore } from "@/stores/storage.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed } from "vue";
const { records: srs, isReady, hasError } = useSrCollection();
const { records: srs, isReady, hasError } = useSrStore().subscribe();
const data = computed<{
result: { id: string; label: string; value: number }[];

View File

@@ -9,9 +9,9 @@
import TasksTable from "@/components/tasks/TasksTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useTaskCollection } from "@/composables/xen-api-collection/task-collection.composable";
import { useTaskStore } from "@/stores/task.store";
const { pendingTasks } = useTaskCollection();
const { pendingTasks } = useTaskStore().subscribe();
</script>
<style lang="postcss" scoped></style>

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useHostCollection();
const { hasError } = useHostStore().subscribe();
const stats = inject(
IK_HOST_STATS,

View File

@@ -12,9 +12,9 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
@@ -29,7 +29,7 @@ const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { records: hosts } = useHostCollection();
const { records: hosts } = useHostStore().subscribe();
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useVmCollection();
const { hasError } = useVmStore().subscribe();
const stats = inject(
IK_VM_STATS,

View File

@@ -10,15 +10,15 @@
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import NoDataError from "@/components/NoDataError.vue";
import { useHostStore } from "@/stores/host.store";
const { hasError } = useHostCollection();
const { hasError } = useHostStore().subscribe();
const stats = inject(
IK_HOST_STATS,

View File

@@ -17,10 +17,10 @@
<script lang="ts" setup>
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import { formatSize } from "@/libs/utils";
import { formatSize, getHostMemory } from "@/libs/utils";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
@@ -31,22 +31,27 @@ const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { runningHosts } = useHostCollection();
const { getHostMemory } = useHostMetricsCollection();
const hostMetricsSubscription = useHostMetricsStore().subscribe();
const hostStore = useHostStore();
const { runningHosts } = hostStore.subscribe({ hostMetricsSubscription });
const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const customMaxValue = computed(() =>
sumBy(runningHosts.value, (host) => getHostMemory(host)?.size ?? 0)
sumBy(
runningHosts.value,
(host) => getHostMemory(host, hostMetricsSubscription)?.size ?? 0
)
);
const currentData = computed(() => {
let size = 0,
usage = 0;
runningHosts.value.forEach((host) => {
const hostMemory = getHostMemory(host);
const hostMemory = getHostMemory(host, hostMetricsSubscription);
size += hostMemory?.size ?? 0;
usage += hostMemory?.usage ?? 0;
});

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useVmCollection();
const { hasError } = useVmStore().subscribe();
const stats = inject(
IK_VM_STATS,

View File

@@ -34,9 +34,9 @@
<script lang="ts" setup>
import RelativeTime from "@/components/RelativeTime.vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { parseDateTime } from "@/libs/utils";
import type { XenApiTask } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { computed } from "vue";
const props = defineProps<{
@@ -44,7 +44,7 @@ const props = defineProps<{
task: XenApiTask;
}>();
const { getByOpaqueRef: getHost } = useHostCollection();
const { getByOpaqueRef: getHost } = useHostStore().subscribe();
const createdAt = computed(() => parseDateTime(props.task.created));

View File

@@ -40,8 +40,8 @@
import TaskRow from "@/components/tasks/TaskRow.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTable from "@/components/ui/UiTable.vue";
import { useTaskCollection } from "@/composables/xen-api-collection/task-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
import { useTaskStore } from "@/stores/task.store";
import { computed } from "vue";
const props = defineProps<{
@@ -49,7 +49,7 @@ const props = defineProps<{
finishedTasks?: XenApiTask[];
}>();
const { hasError, isFetching } = useTaskCollection();
const { hasError, isFetching } = useTaskStore().subscribe();
const hasTasks = computed(
() => props.pendingTasks.length > 0 || (props.finishedTasks?.length ?? 0) > 0

View File

@@ -12,9 +12,10 @@
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import { isOperationsPending } from "@/libs/utils";
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCopy } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -23,7 +24,7 @@ const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();
const { getByOpaqueRef } = useVmStore().subscribe();
const selectedVms = computed(() =>
props.selectedRefs
@@ -38,7 +39,7 @@ const areAllSelectedVmsHalted = computed(() =>
);
const areSomeSelectedVmsCloning = computed(() =>
selectedVms.value.some((vm) => isOperationPending(vm, VM_OPERATION.CLONE))
selectedVms.value.some((vm) => isOperationsPending(vm, VM_OPERATION.CLONE))
);
const handleCopy = async () => {

View File

@@ -35,11 +35,11 @@
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { POWER_STATE } from "@/libs/xen-api";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faSatellite, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -51,7 +51,7 @@ const props = defineProps<{
}>();
const xenApi = useXenApiStore().getXapi();
const { getByOpaqueRef: getVm } = useVmCollection();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const {
open: openDeleteModal,
close: closeDeleteModal,

View File

@@ -27,7 +27,6 @@
</template>
<script lang="ts" setup>
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { computed } from "vue";
import { exportVmsAsCsvFile, exportVmsAsJsonFile } from "@/libs/vm";
import MenuItem from "@/components/menu/MenuItem.vue";
@@ -37,6 +36,7 @@ import {
faFileCsv,
faFileExport,
} from "@fortawesome/free-solid-svg-icons";
import { useVmStore } from "@/stores/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api";
@@ -44,7 +44,7 @@ const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef: getVm } = useVmCollection();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const vms = computed(() =>
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
);

View File

@@ -95,12 +95,13 @@
import MenuItem from "@/components/menu/MenuItem.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { isHostRunning, isOperationsPending } from "@/libs/utils";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import {
faCirclePlay,
@@ -120,12 +121,12 @@ const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef: getVm, isOperationPending } = useVmCollection();
const { records: hosts } = useHostCollection();
const { pool } = usePoolCollection();
const { isHostRunning } = useHostMetricsCollection();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const { records: hosts } = useHostStore().subscribe();
const { pool } = usePoolStore().subscribe();
const hostMetricsSubscription = useHostMetricsStore().subscribe();
const vms = computed(() =>
const vms = computed<XenApiVm[]>(() =>
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
);
@@ -149,7 +150,7 @@ const areVmsPaused = computed(() =>
);
const areOperationsPending = (operation: VM_OPERATION | VM_OPERATION[]) =>
vms.value.some((vm) => isOperationPending(vm, operation));
vms.value.some((vm) => isOperationsPending(vm, operation));
const areVmsBusyToStart = computed(() =>
areOperationsPending(VM_OPERATION.START)
@@ -179,7 +180,9 @@ const areVmsBusyToForceShutdown = computed(() =>
areOperationsPending(VM_OPERATION.HARD_SHUTDOWN)
);
const getHostState = (host: XenApiHost) =>
isHostRunning(host) ? POWER_STATE.RUNNING : POWER_STATE.HALTED;
isHostRunning(host, hostMetricsSubscription)
? POWER_STATE.RUNNING
: POWER_STATE.HALTED;
</script>
<style lang="postcss" scoped>

View File

@@ -20,8 +20,8 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useVmStore } from "@/stores/vm.store";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiVm } from "@/libs/xen-api";
import {
faAngleDown,
@@ -31,7 +31,7 @@ import {
import { computed } from "vue";
import { useRouter } from "vue-router";
const { getByUuid: getVmByUuid } = useVmCollection();
const { getByUuid: getVmByUuid } = useVmStore().subscribe();
const { currentRoute } = useRouter();
const vm = computed(() =>

View File

@@ -17,9 +17,9 @@ export type Stat<T> = {
pausable: Pausable;
};
export type GetStats<
type GetStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
S extends HostStats | VmStats
> = (
uuid: T["uuid"],
granularity: GRANULARITY,
@@ -29,7 +29,7 @@ export type GetStats<
export type FetchedStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
S extends HostStats | VmStats
> = {
register: (object: T) => void;
unregister: (object: T) => void;
@@ -40,7 +40,7 @@ export type FetchedStats<
export default function useFetchStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
S extends HostStats | VmStats
>(getStats: GetStats<T, S>, granularity: GRANULARITY): FetchedStats<T, S> {
const stats = ref<Map<string, Stat<S>>>(new Map());
const timestamp = ref<number[]>([0, 0]);
@@ -108,7 +108,7 @@ export default function useFetchStats<
return {
register,
unregister,
stats: computed(() => Array.from(stats.value.values()) as Stat<S>[]),
stats: computed<Stat<S>[]>(() => Array.from(stats.value.values())),
timestampStart: computed(() => timestamp.value[0]),
timestampEnd: computed(() => timestamp.value[1]),
};

View File

@@ -1,63 +0,0 @@
import type { RawObjectType } from "@/libs/xen-api";
import { getXenApiCollection } from "@/libs/xen-api-collection";
import type {
RawTypeToRecord,
XenApiBaseCollection,
XenApiCollectionManager,
} from "@/types/xen-api-collection";
import { tryOnUnmounted } from "@vueuse/core";
import { computed } from "vue";
export const useXenApiCollection = <
ObjectType extends RawObjectType,
Record extends RawTypeToRecord<ObjectType>,
Immediate extends boolean,
>(
type: ObjectType,
options?: { immediate?: Immediate }
): XenApiBaseCollection<Record, Immediate> => {
const baseCollection = getXenApiCollection(type);
const isDeferred = options?.immediate === false;
const id = Symbol();
const collection = {
records: baseCollection.records,
isFetching: baseCollection.isFetching,
isReloading: baseCollection.isReloading,
isReady: baseCollection.isReady,
hasError: baseCollection.hasError,
hasUuid: baseCollection.hasUuid.bind(baseCollection),
getByUuid: baseCollection.getByUuid.bind(baseCollection),
getByOpaqueRef: baseCollection.getByOpaqueRef.bind(baseCollection),
};
tryOnUnmounted(() => baseCollection.unsubscribe(id));
if (isDeferred) {
return {
...collection,
start: () => baseCollection.subscribe(id),
isStarted: computed(() => baseCollection.hasSubscriptions.value),
} as XenApiBaseCollection<Record, false>;
}
baseCollection.subscribe(id);
return collection as XenApiBaseCollection<Record, Immediate>;
};
export const useXenApiCollectionManager = <
ObjectType extends RawObjectType,
Record extends RawTypeToRecord<ObjectType>,
>(
type: ObjectType
): XenApiCollectionManager<Record> => {
const collection = getXenApiCollection(type);
return {
hasSubscriptions: collection.hasSubscriptions,
add: collection.add.bind(collection),
remove: collection.remove.bind(collection),
update: collection.update.bind(collection),
};
};

View File

@@ -1,3 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
export const useConsoleCollection = () => useXenApiCollection("console");

View File

@@ -1,45 +0,0 @@
import type { GetStats } from "@/composables/fetch-stats.composable";
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import { computed } from "vue";
export const useHostCollection = () => {
const collection = useXenApiCollection("host");
const hostMetricsCollection = useHostMetricsCollection();
return {
...collection,
runningHosts: computed(() =>
collection.records.value.filter((host) =>
hostMetricsCollection.isHostRunning(host)
)
),
getStats: ((
hostUuid,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const xenApiStore = useXenApiStore();
const host = collection.getByUuid(hostUuid);
if (host === undefined) {
throw new Error(`Host ${hostUuid} could not be found.`);
}
const xapiStats = xenApiStore.isConnected
? xenApiStore.getXapiStats()
: undefined;
return xapiStats?._getAndUpdateStats({
abortSignal,
host,
ignoreExpired,
uuid: host.uuid,
granularity,
});
}) as GetStats<XenApiHost>,
};
};

View File

@@ -1,24 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
export const useHostMetricsCollection = () => {
const collection = useXenApiCollection("host_metrics");
return {
...collection,
getHostMemory: (host: XenApiHost) => {
const hostMetrics = collection.getByOpaqueRef(host.metrics);
if (hostMetrics !== undefined) {
const total = +hostMetrics.memory_total;
return {
usage: total - +hostMetrics.memory_free,
size: total,
};
}
},
isHostRunning: (host: XenApiHost) => {
return collection.getByOpaqueRef(host.metrics)?.live === true;
},
};
};

View File

@@ -1,14 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import type { XenApiPool } from "@/libs/xen-api";
import { computed } from "vue";
export const usePoolCollection = () => {
const poolCollection = useXenApiCollection("pool");
return {
...poolCollection,
pool: computed<XenApiPool | undefined>(
() => poolCollection.records.value[0]
),
};
};

View File

@@ -1,3 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
export const useSrCollection = () => useXenApiCollection("SR");

View File

@@ -1,83 +0,0 @@
import type { GetStats } from "@/composables/fetch-stats.composable";
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import {
POWER_STATE,
VM_OPERATION,
type XenApiHost,
type XenApiVm,
} from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import { castArray } from "lodash-es";
import { computed } from "vue";
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
const hostCollection = useHostCollection();
const xenApiStore = useXenApiStore();
const records = computed(() =>
collection.records.value
.filter(
(vm) => !vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
)
.sort(sortRecordsByNameLabel)
);
return {
...collection,
records,
isOperationPending: (
vm: XenApiVm,
operations: VM_OPERATION[] | VM_OPERATION
) => {
const currentOperations = Object.values(vm.current_operations);
return castArray(operations).some((operation) =>
currentOperations.includes(operation)
);
},
runningVms: computed(() =>
records.value.filter((vm) => vm.power_state === POWER_STATE.RUNNING)
),
recordsByHostRef: computed(() => {
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
records.value.forEach((vm) => {
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
vmsByHostOpaqueRef.set(vm.resident_on, []);
}
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
});
return vmsByHostOpaqueRef;
}),
getStats: ((id, granularity, ignoreExpired = false, { abortSignal }) => {
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = collection.getByUuid(id);
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
}
const host = hostCollection.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return xenApiStore.getXapiStats()._getAndUpdateStats<VmStats>({
abortSignal,
host,
ignoreExpired,
uuid: vm.uuid,
granularity,
});
}) as GetStats<XenApiVm>,
};
};

View File

@@ -1,3 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
export const useVmMetricsCollection = () => useXenApiCollection("VM_metrics");

View File

@@ -1,14 +1,19 @@
import type {
RawXenApiRecord,
XenApiHost,
XenApiRecord,
XenApiVm,
VM_OPERATION,
RawObjectType,
XenApiHostMetrics,
} from "@/libs/xen-api";
import type { Filter } from "@/types/filter";
import type { XenApiRecordSubscription } from "@/types/subscription";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons";
import { utcParse } from "d3-time-format";
import humanFormat from "human-format";
import { find, forEach, round, size, sum } from "lodash-es";
import { castArray, find, forEach, round, size, sum } from "lodash-es";
export function sortRecordsByNameLabel(
record1: { name_label: string },
@@ -17,7 +22,14 @@ export function sortRecordsByNameLabel(
const label1 = record1.name_label.toLocaleLowerCase();
const label2 = record2.name_label.toLocaleLowerCase();
return label1.localeCompare(label2);
switch (true) {
case label1 < label2:
return -1;
case label1 > label2:
return 1;
default:
return 0;
}
}
export function escapeRegExp(string: string) {
@@ -103,6 +115,28 @@ export function getStatsLength(stats?: object | any[]) {
return size(find(stats, (stat) => stat != null));
}
export function isHostRunning(
host: XenApiHost,
hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics>
) {
return hostMetricsSubscription.getByOpaqueRef(host.metrics)?.live === true;
}
export function getHostMemory(
host: XenApiHost,
hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics>
) {
const hostMetrics = hostMetricsSubscription.getByOpaqueRef(host.metrics);
if (hostMetrics !== undefined) {
const total = +hostMetrics.memory_total;
return {
usage: total - +hostMetrics.memory_free,
size: total,
};
}
}
export const buildXoObject = <T extends XenApiRecord<RawObjectType>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: T["$ref"] }
@@ -149,3 +183,13 @@ export function parseRamUsage(
export const getFirst = <T>(value: T | T[]): T | undefined =>
Array.isArray(value) ? value[0] : value;
export const isOperationsPending = (
obj: XenApiVm,
operations: VM_OPERATION[] | VM_OPERATION
) => {
const currentOperations = Object.values(obj.current_operations);
return castArray(operations).some((operation) =>
currentOperations.includes(operation)
);
};

View File

@@ -1,112 +0,0 @@
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type { RawTypeToRecord } from "@/types/xen-api-collection";
import { whenever } from "@vueuse/core";
import { computed, reactive } from "vue";
const collections = new Map<RawObjectType, XenApiCollection<any>>();
export const getXenApiCollection = <
ObjectType extends RawObjectType,
Record extends RawTypeToRecord<ObjectType> = RawTypeToRecord<ObjectType>,
>(
type: ObjectType
) => {
if (!collections.has(type)) {
collections.set(type, new XenApiCollection(type));
}
return collections.get(type)! as XenApiCollection<Record>;
};
export class XenApiCollection<Record extends XenApiRecord<any>> {
private state = reactive({
isReady: false,
isFetching: false,
lastError: undefined as string | undefined,
subscriptions: new Set<symbol>(),
recordsByOpaqueRef: new Map<Record["$ref"], Record>(),
recordsByUuid: new Map<Record["uuid"], Record>(),
});
isReady = computed(() => this.state.isReady);
isFetching = computed(() => this.state.isFetching);
isReloading = computed(() => this.state.isReady && this.state.isFetching);
lastError = computed(() => this.state.lastError);
hasError = computed(() => this.state.lastError !== undefined);
hasSubscriptions = computed(() => this.state.subscriptions.size > 0);
records = computed(() => Array.from(this.state.recordsByOpaqueRef.values()));
subscribe(id: symbol) {
this.state.subscriptions.add(id);
}
unsubscribe(id: symbol) {
this.state.subscriptions.delete(id);
}
constructor(private type: RawObjectType) {
const xenApiStore = useXenApiStore();
whenever(
() => xenApiStore.isConnected && this.hasSubscriptions.value,
() => this.fetchAll(xenApiStore)
);
}
getByOpaqueRef(opaqueRef: Record["$ref"]) {
return this.state.recordsByOpaqueRef.get(opaqueRef);
}
getByUuid(uuid: Record["uuid"]) {
return this.state.recordsByUuid.get(uuid);
}
hasUuid(uuid: Record["uuid"]) {
return this.state.recordsByUuid.has(uuid);
}
add(record: Record) {
this.state.recordsByOpaqueRef.set(record.$ref, record);
this.state.recordsByUuid.set(record.uuid, record);
}
update(record: Record) {
this.state.recordsByOpaqueRef.set(record.$ref, record);
this.state.recordsByUuid.set(record.uuid, record);
}
remove(opaqueRef: Record["$ref"]) {
if (!this.state.recordsByOpaqueRef.has(opaqueRef)) {
return;
}
const record = this.state.recordsByOpaqueRef.get(opaqueRef)!;
this.state.recordsByOpaqueRef.delete(opaqueRef);
this.state.recordsByUuid.delete(record.uuid);
}
private async fetchAll(xenApiStore: ReturnType<typeof useXenApiStore>) {
try {
this.state.isFetching = true;
this.state.lastError = undefined;
const records = await xenApiStore
.getXapi()
.loadRecords<any, Record>(this.type);
this.state.recordsByOpaqueRef.clear();
this.state.recordsByUuid.clear();
records.forEach((record) => this.add(record));
this.state.isReady = true;
} catch {
this.state.lastError = `[${this.type}] Failed to fetch records`;
} finally {
this.state.isFetching = false;
}
}
}

View File

@@ -1,5 +1,4 @@
import { buildXoObject, parseDateTime } from "@/libs/utils";
import type { RawTypeToRecord } from "@/types/xen-api-collection";
import { JSONRPCClient } from "json-rpc-2.0";
import { castArray } from "lodash-es";
@@ -175,6 +174,45 @@ export interface XenApiMessage<T extends RawObjectType = RawObjectType>
timestamp: string;
}
export type XenApiAlarmType =
| "cpu_usage"
| "disk_usage"
| "fs_usage"
| "log_fs_usage"
| "mem_usage"
| "memory_free_kib"
| "network_usage"
| "physical_utilisation"
| "sr_io_throughput_total_per_host";
export interface XenApiAlarm extends XenApiMessage {
level: number;
triggerLevel: number;
type: XenApiAlarmType;
}
export type RawTypeToRecord<T extends RawObjectType> = T extends "SR"
? XenApiSr
: T extends "VM"
? XenApiVm
: T extends "VM_guest_metrics"
? XenApiVmGuestMetrics
: T extends "VM_metrics"
? XenApiVmMetrics
: T extends "console"
? XenApiConsole
: T extends "host"
? XenApiHost
: T extends "host_metrics"
? XenApiHostMetrics
: T extends "message"
? XenApiMessage
: T extends "pool"
? XenApiPool
: T extends "task"
? XenApiTask
: never;
type WatchCallbackResult = {
id: string;
class: ObjectType;
@@ -295,7 +333,7 @@ export default class XenApi {
async loadRecords<
T extends RawObjectType,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
>(type: T): Promise<R[]> {
const result = await this.#call<{ [key: string]: R }>(
`${type}.get_all_records`,

View File

@@ -1,21 +1,21 @@
import type { RouteRecordRaw } from "vue-router";
const componentLoaders = import.meta.glob("@/stories/**/*.story.vue");
const docLoaders = import.meta.glob("@/stories/**/*.story.md", { as: "raw" });
const componentLoaders = import.meta.glob("@/stories/*.story.vue");
const docLoaders = import.meta.glob("@/stories/*.story.md", { as: "raw" });
const children: RouteRecordRaw[] = Object.entries(componentLoaders).map(
([path, componentLoader]) => {
const basePath = path.replace(/^\/src\/stories\/(.*)\.story.vue$/, "$1");
const basename = path.replace(/^\/src\/stories\/(.*)\.story.vue$/, "$1");
const docPath = path.replace(/\.vue$/, ".md");
const routeName = `story-${basePath}`;
const routeName = `story-${basename}`;
return {
name: routeName,
path: basePath,
path: basename,
component: componentLoader,
meta: {
isStory: true,
storyTitle: basePathToStoryTitle(basePath),
storyTitle: basenameToStoryTitle(basename),
storyMdLoader: docLoaders[docPath],
},
};
@@ -46,10 +46,8 @@ export default {
* Basename: `my-component`
* Page title: `My Component`
*/
function basePathToStoryTitle(basePath: string) {
return basePath
.split("/")
.pop()!
function basenameToStoryTitle(basename: string) {
return basename
.split("-")
.map((s) => `${s.charAt(0).toUpperCase()}${s.substring(1)}`)
.join(" ");

View File

@@ -0,0 +1,36 @@
import type { XenApiAlarm } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import type {
DeferExtension,
Options,
Subscription,
XenApiRecordExtension,
} from "@/types/subscription";
import { defineStore } from "pinia";
import { computed } from "vue";
type Extensions = [XenApiRecordExtension<XenApiAlarm>, DeferExtension];
export const useAlarmStore = defineStore("alarm", () => {
const messageCollection = useXapiCollectionStore().get("message");
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const subscription = messageCollection.subscribe(options);
const extendedSubscription = {
records: computed(() =>
subscription.records.value.filter((record) => record.name === "alarm")
),
};
return {
...subscription,
...extendedSubscription,
} as Subscription<Extensions, O>;
};
return {
...messageCollection,
subscribe,
};
});

View File

@@ -0,0 +1,6 @@
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { defineStore } from "pinia";
export const useConsoleStore = defineStore("console", () =>
useXapiCollectionStore().get("console")
);

View File

@@ -0,0 +1,6 @@
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { defineStore } from "pinia";
export const useHostMetricsStore = defineStore("host-metrics", () =>
useXapiCollectionStore().get("host_metrics")
);

View File

@@ -0,0 +1,88 @@
import { isHostRunning } from "@/libs/utils";
import type {
GRANULARITY,
HostStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { XenApiHost, XenApiHostMetrics } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
Extension,
XenApiRecordExtension,
XenApiRecordSubscription,
} from "@/types/subscription";
import type { PartialSubscription } from "@/types/subscription";
import { computed } from "vue";
import type { ComputedRef } from "vue";
type GetStatsExtension = Extension<{
getStats: (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<HostStats> | undefined> | undefined;
}>;
type RunningHostsExtension = Extension<
{ runningHosts: ComputedRef<XenApiHost[]> },
{ hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics> }
>;
export type HostExtensions = [
XenApiRecordExtension<XenApiHost>,
GetStatsExtension,
RunningHostsExtension
];
export const getStatsSubscription = (
hostSubscription: XenApiRecordSubscription<XenApiHost>
): PartialSubscription<GetStatsExtension> => {
const xenApiStore = useXenApiStore();
return {
getStats: (
hostUuid,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const host = hostSubscription.getByUuid(hostUuid);
if (host === undefined) {
throw new Error(`Host ${hostUuid} could not be found.`);
}
const xapiStats = xenApiStore.isConnected
? xenApiStore.getXapiStats()
: undefined;
return xapiStats?._getAndUpdateStats<HostStats>({
abortSignal,
host,
ignoreExpired,
uuid: host.uuid,
granularity,
});
},
};
};
export const runningHostsSubscription = (
hostSubscription: XenApiRecordSubscription<XenApiHost>,
hostMetricsSubscription:
| XenApiRecordSubscription<XenApiHostMetrics>
| undefined
): PartialSubscription<RunningHostsExtension> | undefined => {
if (hostMetricsSubscription === undefined) {
return undefined;
}
return {
runningHosts: computed(() =>
hostSubscription.records.value.filter((host) =>
isHostRunning(host, hostMetricsSubscription)
)
),
};
};

View File

@@ -0,0 +1,31 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { HostExtensions } from "@/stores/host.extension";
import {
getStatsSubscription,
runningHostsSubscription,
} from "@/stores/host.extension";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import type { Options, Subscription } from "@/types/subscription";
import { defineStore } from "pinia";
export const useHostStore = defineStore("host", () => {
const hostCollection = useXapiCollectionStore().get("host");
hostCollection.setSort(sortRecordsByNameLabel);
const subscribe = <O extends Options<HostExtensions>>(options?: O) => {
const subscription = hostCollection.subscribe(options);
const { hostMetricsSubscription } = options ?? {};
return {
...subscription,
...getStatsSubscription(subscription),
...runningHostsSubscription(subscription, hostMetricsSubscription),
} as Subscription<HostExtensions, O>;
};
return {
...hostCollection,
subscribe,
};
});

View File

@@ -0,0 +1,39 @@
import { getFirst } from "@/libs/utils";
import type { XenApiPool } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import type {
Extension,
Options,
PartialSubscription,
Subscription,
XenApiRecordExtension,
} from "@/types/subscription";
import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type PoolExtension = Extension<{
pool: ComputedRef<XenApiPool | undefined>;
}>;
type Extensions = [XenApiRecordExtension<XenApiPool>, PoolExtension];
export const usePoolStore = defineStore("pool", () => {
const poolCollection = useXapiCollectionStore().get("pool");
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const subscription = poolCollection.subscribe(options);
const extendedSubscription: PartialSubscription<PoolExtension> = {
pool: computed(() => getFirst(subscription.records.value)),
};
return {
...subscription,
...extendedSubscription,
} as Subscription<Extensions, O>;
};
return {
...poolCollection,
subscribe,
};
});

View File

@@ -0,0 +1,6 @@
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { defineStore } from "pinia";
export const useSrStore = defineStore("SR", () =>
useXapiCollectionStore().get("SR")
);

View File

@@ -1,14 +1,30 @@
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
import useCollectionFilter from "@/composables/collection-filter.composable";
import useCollectionSorter from "@/composables/collection-sorter.composable";
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
import type {
Extension,
PartialSubscription,
XenApiRecordExtension,
XenApiRecordSubscription,
} from "@/types/subscription";
import type { ComputedRef, Ref } from "vue";
export const useTaskCollection = () => {
const collection = useXenApiCollection("task");
type AdditionalTasksExtension = Extension<{
pendingTasks: ComputedRef<XenApiTask[]>;
finishedTasks: Ref<XenApiTask[]>;
}>;
export type TaskExtensions = [
XenApiRecordExtension<XenApiTask>,
AdditionalTasksExtension
];
export const additionalTasksSubscription = (
taskSubscription: XenApiRecordSubscription<XenApiTask>
): PartialSubscription<AdditionalTasksExtension> => {
const { compareFn } = useCollectionSorter<XenApiTask>({
initialSorts: ["-created"],
});
@@ -20,10 +36,9 @@ export const useTaskCollection = () => {
],
});
const sortedTasks = useSortedCollection(collection.records, compareFn);
const sortedTasks = useSortedCollection(taskSubscription.records, compareFn);
return {
...collection,
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
finishedTasks: useArrayRemovedItemsHistory(
sortedTasks,

View File

@@ -0,0 +1,22 @@
import {
additionalTasksSubscription,
type TaskExtensions,
} from "@/stores/task.extension";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import type { Options, Subscription } from "@/types/subscription";
import { defineStore } from "pinia";
export const useTaskStore = defineStore("task", () => {
const tasksCollection = useXapiCollectionStore().get("task");
const subscribe = <O extends Options<TaskExtensions>>(options?: O) => {
const subscription = tasksCollection.subscribe(options);
return {
...subscription,
...additionalTasksSubscription(subscription),
} as Subscription<TaskExtensions, O>;
};
return { ...tasksCollection, subscribe };
});

View File

@@ -0,0 +1,6 @@
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { defineStore } from "pinia";
export const useVmGuestMetricsStore = defineStore("vm-guest-metrics", () =>
useXapiCollectionStore().get("VM_guest_metrics")
);

View File

@@ -0,0 +1,6 @@
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { defineStore } from "pinia";
export const useVmMetricsStore = defineStore("vm-metrics", () =>
useXapiCollectionStore().get("VM_metrics")
);

View File

@@ -0,0 +1,108 @@
import type {
GRANULARITY,
VmStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import { POWER_STATE, type XenApiHost, type XenApiVm } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
Extension,
PartialSubscription,
XenApiRecordExtension,
XenApiRecordSubscription,
} from "@/types/subscription";
import { computed, type ComputedRef } from "vue";
type RecordsByHostRefExtension = Extension<{
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
}>;
type RunningVmsExtension = Extension<{
runningVms: ComputedRef<XenApiVm[]>;
}>;
type GetStatsExtension = Extension<
{
getStats: (
id: XenApiVm["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<VmStats> | undefined> | undefined;
},
{ hostSubscription: XenApiRecordSubscription<XenApiHost> }
>;
export type VmExtensions = [
XenApiRecordExtension<XenApiVm>,
RecordsByHostRefExtension,
RunningVmsExtension,
GetStatsExtension
];
export const recordsByHostRefSubscription = (
vmSubscription: XenApiRecordSubscription<XenApiVm>
): PartialSubscription<RecordsByHostRefExtension> => ({
recordsByHostRef: computed(() => {
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
vmSubscription.records.value.forEach((vm) => {
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
vmsByHostOpaqueRef.set(vm.resident_on, []);
}
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
});
return vmsByHostOpaqueRef;
}),
});
export const runningVmsSubscription = (
vmSubscription: XenApiRecordSubscription<XenApiVm>
): PartialSubscription<RunningVmsExtension> => ({
runningVms: computed(() =>
vmSubscription.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
});
export const getStatsSubscription = (
vmSubscription: XenApiRecordSubscription<XenApiVm>,
hostSubscription: XenApiRecordSubscription<XenApiHost> | undefined
): PartialSubscription<GetStatsExtension> | undefined => {
if (hostSubscription === undefined) {
return;
}
return {
getStats: (id, granularity, ignoreExpired = false, { abortSignal }) => {
const xenApiStore = useXenApiStore();
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = vmSubscription.getByUuid(id);
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
}
const host = hostSubscription.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return xenApiStore.getXapiStats()._getAndUpdateStats<VmStats>({
abortSignal,
host,
ignoreExpired,
uuid: vm.uuid,
granularity,
});
},
};
};

View File

@@ -0,0 +1,36 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import {
getStatsSubscription,
recordsByHostRefSubscription,
runningVmsSubscription,
type VmExtensions,
} from "@/stores/vm.extension";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import type { Options, Subscription } from "@/types/subscription";
import { defineStore } from "pinia";
export const useVmStore = defineStore("vm", () => {
const vmCollection = useXapiCollectionStore().get("VM");
vmCollection.setFilter(
(vm) => !vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
);
vmCollection.setSort(sortRecordsByNameLabel);
const subscribe = <O extends Options<VmExtensions>>(options?: O) => {
const subscription = vmCollection.subscribe(options);
return {
...subscription,
...recordsByHostRefSubscription(subscription),
...runningVmsSubscription(subscription),
...getStatsSubscription(subscription, options?.hostSubscription),
} as Subscription<VmExtensions, O>;
};
return {
...vmCollection,
subscribe,
};
});

View File

@@ -0,0 +1,159 @@
import type { RawObjectType, RawTypeToRecord } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
DeferExtension,
Options,
Subscription,
XenApiRecordExtension,
} from "@/types/subscription";
import { tryOnUnmounted, whenever } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, readonly, ref } from "vue";
export const useXapiCollectionStore = defineStore("xapiCollection", () => {
const collections = ref(new Map());
function get<T extends RawObjectType>(
type: T
): ReturnType<typeof createXapiCollection<T>> {
if (!collections.value.has(type)) {
collections.value.set(type, createXapiCollection(type));
}
return collections.value.get(type)!;
}
return { get };
});
const createXapiCollection = <
T extends RawObjectType,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
>(
type: T
) => {
const isReady = ref(false);
const isFetching = ref(false);
const isReloading = computed(() => isReady.value && isFetching.value);
const lastError = ref<string>();
const hasError = computed(() => lastError.value !== undefined);
const subscriptions = ref(new Set<symbol>());
const recordsByOpaqueRef = ref(new Map<R["$ref"], R>());
const recordsByUuid = ref(new Map<R["uuid"], R>());
const filter = ref<(record: R) => boolean>();
const sort = ref<(record1: R, record2: R) => 1 | 0 | -1>();
const xenApiStore = useXenApiStore();
const setFilter = (newFilter: (record: R) => boolean) =>
(filter.value = newFilter);
const setSort = (newSort: (record1: R, record2: R) => 1 | 0 | -1) =>
(sort.value = newSort);
const records = computed<R[]>(() => {
const records = Array.from(recordsByOpaqueRef.value.values()).sort(
sort.value
);
return filter.value !== undefined ? records.filter(filter.value) : records;
});
const getByOpaqueRef = (opaqueRef: R["$ref"]) =>
recordsByOpaqueRef.value.get(opaqueRef);
const getByUuid = (uuid: R["uuid"]) => recordsByUuid.value.get(uuid);
const hasUuid = (uuid: R["uuid"]) => recordsByUuid.value.has(uuid);
const hasSubscriptions = computed(() => subscriptions.value.size > 0);
const fetchAll = async () => {
try {
isFetching.value = true;
lastError.value = undefined;
const records = await xenApiStore.getXapi().loadRecords<T, R>(type);
recordsByOpaqueRef.value.clear();
recordsByUuid.value.clear();
records.forEach(add);
isReady.value = true;
} catch (e) {
lastError.value = `[${type}] Failed to fetch records`;
} finally {
isFetching.value = false;
}
};
const add = (record: R) => {
recordsByOpaqueRef.value.set(record.$ref, record);
recordsByUuid.value.set(record.uuid, record);
};
const update = (record: R) => {
recordsByOpaqueRef.value.set(record.$ref, record);
recordsByUuid.value.set(record.uuid, record);
};
const remove = (opaqueRef: R["$ref"]) => {
if (!recordsByOpaqueRef.value.has(opaqueRef)) {
return;
}
const record = recordsByOpaqueRef.value.get(opaqueRef)!;
recordsByOpaqueRef.value.delete(opaqueRef);
recordsByUuid.value.delete(record.uuid);
};
whenever(
() => xenApiStore.isConnected && hasSubscriptions.value,
() => fetchAll()
);
type Extensions = [XenApiRecordExtension<R>, DeferExtension];
function subscribe<O extends Options<Extensions>>(
options?: O
): Subscription<Extensions, O> {
const id = Symbol();
tryOnUnmounted(() => {
unsubscribe(id);
});
const subscription = {
records,
getByOpaqueRef,
getByUuid,
hasUuid,
isReady: readonly(isReady),
isFetching: readonly(isFetching),
isReloading: isReloading,
hasError,
lastError: readonly(lastError),
};
const start = () => subscriptions.value.add(id);
if (options?.immediate !== false) {
start();
return subscription as Subscription<Extensions, O>;
}
return {
...subscription,
start,
isStarted: computed(() => subscriptions.value.has(id)),
} as Subscription<Extensions, O>;
}
const unsubscribe = (id: symbol) => subscriptions.value.delete(id);
return {
hasSubscriptions,
subscribe,
unsubscribe,
add,
update,
remove,
setFilter,
setSort,
};
};

View File

@@ -1,7 +1,7 @@
import { useXenApiCollectionManager } from "@/composables/xen-api-collection.composable";
import { buildXoObject } from "@/libs/utils";
import XapiStats from "@/libs/xapi-stats";
import XenApi, { getRawObjectType } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
@@ -31,11 +31,11 @@ export const useXenApiStore = defineStore("xen-api", () => {
xenApi.registerWatchCallBack((results) => {
results.forEach((result) => {
const collectionManager = useXenApiCollectionManager(
const collection = useXapiCollectionStore().get(
getRawObjectType(result.class)
);
if (!collectionManager.hasSubscriptions.value) {
if (!collection.hasSubscriptions) {
return;
}
@@ -44,11 +44,11 @@ export const useXenApiStore = defineStore("xen-api", () => {
switch (result.operation) {
case "add":
return collectionManager.add(buildObject());
return collection.add(buildObject());
case "mod":
return collectionManager.update(buildObject());
return collection.update(buildObject());
case "del":
return collectionManager.remove(result.ref as any);
return collection.remove(result.ref as any);
}
});
});

View File

@@ -18,8 +18,8 @@
import RouterTab from "@/components/RouterTab.vue";
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { prop, setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
import { prop, setting, slot } from "@/libs/story/story-param.js";
import { text } from "@/libs/story/story-widget.js";
</script>
<style lang="postcss" scoped></style>

View File

@@ -18,7 +18,7 @@
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiTab from "@/components/ui/UiTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { prop, slot } from "@/libs/story/story-param";
import { prop, slot } from "@/libs/story/story-param.js";
</script>
<style lang="postcss" scoped></style>

View File

@@ -19,8 +19,8 @@
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiTab from "@/components/ui/UiTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { prop, setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
import { prop, setting, slot } from "@/libs/story/story-param.js";
import { text } from "@/libs/story/story-widget.js";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,74 @@
import type { XenApiRecord } from "@/libs/xen-api";
import type { ComputedRef, Ref } from "vue";
type SimpleExtension<Value extends object> = { type: "simple"; value: Value };
type ConditionalExtension<Value extends object, Condition extends object> = {
type: "conditional";
value: Value;
condition: Condition;
};
type UnpackExtension<E, Options> = E extends SimpleExtension<infer Value>
? Value
: E extends ConditionalExtension<infer Value, infer Condition>
? Options extends Condition
? Value
: object
: object;
export type Extension<
Value extends object,
Condition extends object | undefined = undefined
> = Condition extends object
? ConditionalExtension<Value, Condition>
: SimpleExtension<Value>;
export type Options<Extensions extends any[]> = Extensions extends [
infer First,
...infer Rest
]
? First extends ConditionalExtension<any, infer Condition>
? Rest extends any[]
? Partial<Condition> & Options<Rest>
: Partial<Condition>
: Rest extends any[]
? Options<Rest>
: object
: object;
export type Subscription<
Extensions extends any[],
Options extends object
> = Extensions extends [infer First, ...infer Rest]
? UnpackExtension<First, Options> & Subscription<Rest, Options>
: object;
export type PartialSubscription<E> = E extends SimpleExtension<infer Value>
? Value
: E extends ConditionalExtension<infer Value, any>
? Value
: never;
export type XenApiRecordExtension<T extends XenApiRecord<any>> = Extension<{
records: ComputedRef<T[]>;
getByOpaqueRef: (opaqueRef: T["$ref"]) => T | undefined;
getByUuid: (uuid: T["uuid"]) => T | undefined;
hasUuid: (uuid: T["uuid"]) => boolean;
isReady: Readonly<Ref<boolean>>;
isFetching: Readonly<Ref<boolean>>;
isReloading: ComputedRef<boolean>;
hasError: ComputedRef<boolean>;
lastError: Readonly<Ref<string | undefined>>;
}>;
export type DeferExtension = Extension<
{
start: () => void;
isStarted: ComputedRef<boolean>;
},
{ immediate: false }
>;
export type XenApiRecordSubscription<T extends XenApiRecord<any>> =
PartialSubscription<XenApiRecordExtension<T>>;

View File

@@ -1,68 +0,0 @@
import type {
RawObjectType,
XenApiConsole,
XenApiHost,
XenApiHostMetrics,
XenApiMessage,
XenApiPool,
XenApiRecord,
XenApiSr,
XenApiTask,
XenApiVm,
XenApiVmGuestMetrics,
XenApiVmMetrics,
} from "@/libs/xen-api";
import type { XenApiCollection } from "@/libs/xen-api-collection";
import type { ComputedRef } from "vue";
export type RawTypeToRecord<ObjectType extends RawObjectType> =
ObjectType extends "SR"
? XenApiSr
: ObjectType extends "VM"
? XenApiVm
: ObjectType extends "VM_guest_metrics"
? XenApiVmGuestMetrics
: ObjectType extends "VM_metrics"
? XenApiVmMetrics
: ObjectType extends "console"
? XenApiConsole
: ObjectType extends "host"
? XenApiHost
: ObjectType extends "host_metrics"
? XenApiHostMetrics
: ObjectType extends "message"
? XenApiMessage
: ObjectType extends "pool"
? XenApiPool
: ObjectType extends "task"
? XenApiTask
: never;
type XenApiBaseCollectionProps =
| "isFetching"
| "isReloading"
| "hasError"
| "hasUuid"
| "isReady"
| "getByUuid"
| "getByOpaqueRef"
| "records";
type XenApiCollectionManagerProps =
| "add"
| "remove"
| "update"
| "hasSubscriptions";
export type XenApiBaseCollection<
Record extends XenApiRecord<any>,
Immediate extends boolean,
> = Pick<XenApiCollection<Record>, XenApiBaseCollectionProps> &
(Immediate extends false
? { start: () => void; isStarted: ComputedRef<boolean> }
: object);
export type XenApiCollectionManager<Record extends XenApiRecord<any>> = Pick<
XenApiCollection<Record>,
XenApiCollectionManagerProps
>;

View File

@@ -1,13 +1,12 @@
<template>Chargement en cours...</template>
<script lang="ts" setup>
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
import { whenever } from "@vueuse/core";
import { useRouter } from "vue-router";
const router = useRouter();
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
whenever(
() => pool.value?.uuid,

View File

@@ -6,14 +6,14 @@
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { computed, watchEffect } from "vue";
import { useRoute } from "vue-router";
const { hasUuid, isReady, getByUuid } = useHostCollection();
const { hasUuid, isReady, getByUuid } = useHostStore().subscribe();
const route = useRoute();
const uiStore = useUiStore();

View File

@@ -42,13 +42,13 @@ import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboard
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 "@/composables/xen-api-collection/host-collection.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import useFetchStats from "@/composables/fetch-stats.composable";
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useVmStore } from "@/stores/vm.store";
import {
IK_HOST_LAST_WEEK_STATS,
IK_HOST_STATS,
@@ -60,8 +60,15 @@ import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("dashboard"));
const { getStats: getHostStats, runningHosts } = useHostCollection();
const { getStats: getVmStats, runningVms } = useVmCollection();
const hostMetricsSubscription = useHostMetricsStore().subscribe();
const hostSubscription = useHostStore().subscribe({ hostMetricsSubscription });
const { runningHosts, getStats: getHostStats } = hostSubscription;
const { runningVms, getStats: getVmStats } = useVmStore().subscribe({
hostSubscription,
});
const {
register: hostRegister,

View File

@@ -10,11 +10,10 @@
<script lang="ts" setup>
import PoolHeader from "@/components/pool/PoolHeader.vue";
import PoolTabBar from "@/components/pool/PoolTabBar.vue";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
import { usePageTitleStore } from "@/stores/page-title.store";
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
usePageTitleStore().setObject(pool);
</script>

View File

@@ -15,11 +15,12 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { useTaskCollection } from "@/composables/xen-api-collection/task-collection.composable";
import { useTaskStore } from "@/stores/task.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
const { pendingTasks, finishedTasks, isReady, hasError } = useTaskCollection();
const { pendingTasks, finishedTasks, isReady, hasError } =
useTaskStore().subscribe();
const { t } = useI18n();

View File

@@ -37,10 +37,10 @@ import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { POWER_STATE } from "@/libs/xen-api";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import type { Filters } from "@/types/filter";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
@@ -52,7 +52,7 @@ const { t } = useI18n();
const titleStore = usePageTitleStore();
titleStore.setTitle(t("vms"));
const { records: vms } = useVmCollection();
const { records: vms } = useVmStore().subscribe();
const { isMobile, isDesktop } = storeToRefs(useUiStore());
const filters: Filters = {

View File

@@ -157,8 +157,6 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePageTitleStore } from "@/stores/page-title.store";
import { computed } from "vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
@@ -168,6 +166,8 @@ import { useUiStore } from "@/stores/ui.store";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { useI18n } from "vue-i18n";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { locales } from "@/i18n";
import {
faEarthAmericas,
@@ -186,9 +186,8 @@ const { t, locale } = useI18n();
usePageTitleStore().setTitle(() => t("settings"));
const { pool } = usePoolCollection();
const { getByOpaqueRef: getHost } = useHostCollection();
const { pool } = usePoolStore().subscribe();
const { getByOpaqueRef: getHost } = useHostStore().subscribe();
const poolMaster = computed(() =>
pool.value ? getHost(pool.value.master) : undefined

View File

@@ -31,11 +31,12 @@
import RemoteConsole from "@/components/RemoteConsole.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useConsoleCollection } from "@/composables/xen-api-collection/console-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { isOperationsPending } from "@/libs/utils";
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { useConsoleStore } from "@/stores/console.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
@@ -60,14 +61,13 @@ const {
isReady: isVmReady,
getByUuid: getVmByUuid,
hasError: hasVmError,
isOperationPending,
} = useVmCollection();
} = useVmStore().subscribe();
const {
isReady: isConsoleReady,
getByOpaqueRef: getConsoleByOpaqueRef,
hasError: hasConsoleError,
} = useConsoleCollection();
} = useConsoleStore().subscribe();
const isReady = computed(() => isVmReady.value && isConsoleReady.value);
@@ -89,8 +89,9 @@ const vmConsole = computed(() => {
return getConsoleByOpaqueRef(consoleOpaqueRef);
});
const isConsoleAvailable = computed(() =>
vm.value !== undefined ? isOperationPending(vm.value, STOP_OPERATIONS) : false
const isConsoleAvailable = computed(
() =>
vm.value !== undefined && !isOperationsPending(vm.value, STOP_OPERATIONS)
);
</script>

View File

@@ -12,16 +12,16 @@
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import VmHeader from "@/components/vm/VmHeader.vue";
import VmTabBar from "@/components/vm/VmTabBar.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiVm } from "@/libs/xen-api";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import { whenever } from "@vueuse/core";
import { computed } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const { getByUuid, hasUuid, isReady } = useVmCollection();
const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
const uiStore = useUiStore();
const vm = computed(() => getByUuid(route.params.uuid as XenApiVm["uuid"]));
whenever(vm, (vm) => (uiStore.currentHostOpaqueRef = vm.resident_on));

View File

@@ -56,13 +56,9 @@ export default class Tasks extends EventEmitter {
},
})
#app
constructor(app) {
super()
this.#app = app
app.hooks
.on('clean', () => this.#gc(app.config.getOptional('tasks.gc.keep') ?? 1e3))
.on('start', async () => {
@@ -135,10 +131,10 @@ export default class Tasks extends EventEmitter {
*
* @returns {Task}
*/
create({ name, objectId, userId = this.#app.apiContext?.user?.id, type }) {
create({ name, objectId, type }) {
const tasks = this.#tasks
const task = new Task({ properties: { name, objectId, userId, type }, onProgress: this.#onProgress })
const task = new Task({ properties: { name, objectId, type }, onProgress: this.#onProgress })
// Use a compact, sortable, string representation of the creation date
//

View File

@@ -43,7 +43,7 @@
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",
"fs-extra": "^11.1.0",
"get-stream": "^7.0.1",
"get-stream": "^6.0.0",
"getopts": "^2.2.3",
"golike-defer": "^0.5.1",
"http-server-plus": "^1.0.0",

View File

@@ -32,14 +32,9 @@ class Host {
* @param {string} ref - Opaque reference of the host
*/
async smartReboot($defer, ref) {
const suspendedVms = []
if (await this.getField('host', ref, 'enabled')) {
await this.callAsync('host.disable', ref)
$defer(async () => {
await this.callAsync('host.enable', ref)
// Resuming VMs should occur after host enabling to avoid triggering a 'NO_HOSTS_AVAILABLE' error
return asyncEach(suspendedVms, vmRef => this.callAsync('VM.resume', vmRef, false, false))
})
$defer(() => this.callAsync('host.enable', ref))
}
let currentVmRef
@@ -56,7 +51,7 @@ class Host {
try {
await this.callAsync('VM.suspend', vmRef)
suspendedVms.push(vmRef)
$defer(() => this.callAsync('VM.resume', vmRef, false, false))
} catch (error) {
const { code } = error

View File

@@ -28,7 +28,7 @@
"@vates/nbd-client": "^2.0.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"d3-time-format": "^4.1.0",
"d3-time-format": "^3.0.0",
"golike-defer": "^0.5.1",
"http-request-plus": "^1.0.0",
"json-rpc-protocol": "^0.13.2",

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