Compare commits
1 Commits
lite/rewor
...
lite/fix-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a39bc460f |
@@ -4,53 +4,6 @@ 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.
|
||||
@@ -87,102 +40,71 @@ export const useConsoleStore = defineStore("console", () =>
|
||||
|
||||
To extend the base Subscription, you'll need to override the `subscribe` method.
|
||||
|
||||
For that, you can use the `createSubscribe<XenApiRecord, Extensions>((options) => { /* ... */})` helper.
|
||||
|
||||
### Define the extensions
|
||||
|
||||
Subscription extensions are defined as a simple extension (`Extension<object>`) or as a conditional
|
||||
extension (`Extension<object, object>`).
|
||||
Subscription extensions are defined as `(object | [object, RequiredOptions])[]`.
|
||||
|
||||
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`.
|
||||
When using a tuple (`[object, RequiredOptions]`), the corresponding `object` type will be added to the subscription if
|
||||
the `RequiredOptions` for that tuple are present in the options passed to `subscribe`.
|
||||
|
||||
```typescript
|
||||
// Always present extension
|
||||
type PropABExtension = Extension<{
|
||||
type DefaultExtension = {
|
||||
propA: string;
|
||||
propB: ComputedRef<number>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Conditional extension 1
|
||||
type PropCExtension = Extension<
|
||||
type FirstConditionalExtension = [
|
||||
{ propC: ComputedRef<string> }, // <- This signature will be added
|
||||
{ optC: string } // <- if this condition is met
|
||||
>;
|
||||
];
|
||||
|
||||
// Conditional extension 2
|
||||
type PropDExtension = Extension<
|
||||
type SecondConditionalExtension = [
|
||||
{ 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
|
||||
DefaultExtension,
|
||||
FirstConditionalExtension,
|
||||
SecondConditionalExtension
|
||||
];
|
||||
```
|
||||
|
||||
### 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.
|
||||
### Define the subscription
|
||||
|
||||
```typescript
|
||||
export const useConsoleStore = defineStore("console", () => {
|
||||
const consoleCollection = useXapiCollectionStore().get("console");
|
||||
|
||||
const subscribe = <O extends Options<Extensions>>(options?: O) => {
|
||||
const subscribe = createSubscribe<XenApiConsole, Extensions>((options) => {
|
||||
const originalSubscription = consoleCollection.subscribe(options);
|
||||
|
||||
const propABSubscription: PartialSubscription<PropABExtension> = {
|
||||
const extendedSubscription = {
|
||||
propA: "Some string",
|
||||
propB: computed(() => 42),
|
||||
};
|
||||
|
||||
const propCSubscription: PartialSubscription<PropCExtension> | undefined =
|
||||
options?.optC !== undefined
|
||||
? {
|
||||
propC: computed(() => "Some other string"),
|
||||
}
|
||||
: undefined;
|
||||
const propCSubscription = options?.optC !== undefined && {
|
||||
propC: computed(() => "Some other string"),
|
||||
};
|
||||
|
||||
const propDSubscription: PartialSubscription<PropDExtension> | undefined =
|
||||
options?.optD !== undefined
|
||||
? {
|
||||
propD: () => console.log("Hello"),
|
||||
}
|
||||
: undefined;
|
||||
const propDSubscription = options?.optD !== undefined && {
|
||||
propD: () => console.log("Hello"),
|
||||
};
|
||||
|
||||
return {
|
||||
...originalSubscription,
|
||||
...propABSubscription,
|
||||
...extendedSubscription,
|
||||
...propCSubscription,
|
||||
...propDSubscription,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...consoleCollection,
|
||||
@@ -203,18 +125,20 @@ type Options = {
|
||||
|
||||
### Use the subscription
|
||||
|
||||
In each case, all the default properties (`records`, `getByUuid`, etc.) will be present.
|
||||
|
||||
```typescript
|
||||
const store = useConsoleStore();
|
||||
|
||||
// No options (Contains common properties: `propA`, `propB`, `records`, `getByUuid`, etc.)
|
||||
const subscription1 = store.subscribe();
|
||||
// No options (propA and propB will be present)
|
||||
const subscription = store.subscribe();
|
||||
|
||||
// optC option (Contains common properties + `propC`)
|
||||
const subscription2 = store.subscribe({ optC: "Hello" });
|
||||
// optC option (propA, propB and propC will be present)
|
||||
const subscription = store.subscribe({ optC: "Hello" });
|
||||
|
||||
// optD option (Contains common properties + `propD`)
|
||||
const subscription3 = store.subscribe({ optD: 12 });
|
||||
// optD option (propA, propB and propD will be present)
|
||||
const subscription = store.subscribe({ optD: 12 });
|
||||
|
||||
// optC and optD options (Contains common properties + `propC` + `propD`)
|
||||
const subscription4 = store.subscribe({ optC: "Hello", optD: 12 });
|
||||
// optC and optD options (propA, propB, propC and propD will be present)
|
||||
const subscription = store.subscribe({ optC: "Hello", optD: 12 });
|
||||
```
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "GIT_HEAD=$(git rev-parse HEAD) vite build",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"test": "yarn run type-check",
|
||||
"lint": "eslint src",
|
||||
"test": "run-p type-check lint",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
</template>
|
||||
|
||||
<script
|
||||
generic="T extends XenApiRecord<RawObjectType>, I extends T['uuid']"
|
||||
generic="T extends XenApiRecord<string>, I extends T['uuid']"
|
||||
lang="ts"
|
||||
setup
|
||||
>
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
|
||||
import type { XenApiRecord } from "@/libs/xen-api";
|
||||
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</AppMenu>
|
||||
</UiTabBar>
|
||||
|
||||
<div :class="{ 'full-width': fullWidthComponent }" class="tabs">
|
||||
<div class="tabs">
|
||||
<UiCard v-if="selectedTab === TAB.NONE" class="tab-content">
|
||||
<i>No configuration defined</i>
|
||||
</UiCard>
|
||||
@@ -102,11 +102,11 @@ import StorySettingParams from "@/components/component-story/StorySettingParams.
|
||||
import StorySlotParams from "@/components/component-story/StorySlotParams.vue";
|
||||
import AppMenu from "@/components/menu/AppMenu.vue";
|
||||
import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
|
||||
import UiCounter from "@/components/ui/UiCounter.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiTab from "@/components/ui/UiTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import {
|
||||
@@ -140,7 +140,6 @@ const props = defineProps<{
|
||||
settings?: Record<string, any>;
|
||||
}
|
||||
>;
|
||||
fullWidthComponent?: boolean;
|
||||
}>();
|
||||
|
||||
enum TAB {
|
||||
@@ -330,10 +329,6 @@ const applyPreset = (preset: {
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
&.full-width {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<li class="ui-resource">
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<div class="separator" />
|
||||
<div class="label">{{ label }}</div>
|
||||
<div class="count">{{ count }}</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
icon: IconDefinition;
|
||||
label: string;
|
||||
count: string | number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-resource {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-extra-blue-base);
|
||||
font-size: 3.2rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 4.5rem;
|
||||
width: 0;
|
||||
border-left: 0.1rem solid var(--color-extra-blue-base);
|
||||
background-color: var(--color-extra-blue-base);
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<ul class="ui-resources">
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-resources {
|
||||
display: flex;
|
||||
gap: 1rem 5.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,13 @@
|
||||
import type {
|
||||
RawXenApiRecord,
|
||||
XenApiHost,
|
||||
XenApiHostMetrics,
|
||||
XenApiRecord,
|
||||
XenApiVm,
|
||||
VM_OPERATION,
|
||||
RawObjectType,
|
||||
XenApiHostMetrics,
|
||||
} from "@/libs/xen-api";
|
||||
import type { Filter } from "@/types/filter";
|
||||
import type { XenApiRecordSubscription } from "@/types/subscription";
|
||||
import type { Subscription } from "@/types/xapi-collection";
|
||||
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons";
|
||||
import { utcParse } from "d3-time-format";
|
||||
@@ -117,14 +116,14 @@ export function getStatsLength(stats?: object | any[]) {
|
||||
|
||||
export function isHostRunning(
|
||||
host: XenApiHost,
|
||||
hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics>
|
||||
hostMetricsSubscription: Subscription<XenApiHostMetrics, object>
|
||||
) {
|
||||
return hostMetricsSubscription.getByOpaqueRef(host.metrics)?.live === true;
|
||||
}
|
||||
|
||||
export function getHostMemory(
|
||||
host: XenApiHost,
|
||||
hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics>
|
||||
hostMetricsSubscription: Subscription<XenApiHostMetrics, object>
|
||||
) {
|
||||
const hostMetrics = hostMetricsSubscription.getByOpaqueRef(host.metrics);
|
||||
|
||||
@@ -137,7 +136,7 @@ export function getHostMemory(
|
||||
}
|
||||
}
|
||||
|
||||
export const buildXoObject = <T extends XenApiRecord<RawObjectType>>(
|
||||
export const buildXoObject = <T extends XenApiRecord<string>>(
|
||||
record: RawXenApiRecord<T>,
|
||||
params: { opaqueRef: T["$ref"] }
|
||||
) => {
|
||||
|
||||
@@ -90,17 +90,14 @@ export enum VM_OPERATION {
|
||||
|
||||
declare const __brand: unique symbol;
|
||||
|
||||
export interface XenApiRecord<Name extends RawObjectType> {
|
||||
export interface XenApiRecord<Name extends string> {
|
||||
$ref: string & { [__brand]: `${Name}Ref` };
|
||||
uuid: string & { [__brand]: `${Name}Uuid` };
|
||||
}
|
||||
|
||||
export type RawXenApiRecord<T extends XenApiRecord<RawObjectType>> = Omit<
|
||||
T,
|
||||
"$ref"
|
||||
>;
|
||||
export type RawXenApiRecord<T extends XenApiRecord<string>> = Omit<T, "$ref">;
|
||||
|
||||
export interface XenApiPool extends XenApiRecord<"pool"> {
|
||||
export interface XenApiPool extends XenApiRecord<"Pool"> {
|
||||
cpu_info: {
|
||||
cpu_count: string;
|
||||
};
|
||||
@@ -108,7 +105,7 @@ export interface XenApiPool extends XenApiRecord<"pool"> {
|
||||
name_label: string;
|
||||
}
|
||||
|
||||
export interface XenApiHost extends XenApiRecord<"host"> {
|
||||
export interface XenApiHost extends XenApiRecord<"Host"> {
|
||||
address: string;
|
||||
name_label: string;
|
||||
metrics: XenApiHostMetrics["$ref"];
|
||||
@@ -117,13 +114,13 @@ export interface XenApiHost extends XenApiRecord<"host"> {
|
||||
software_version: { product_version: string };
|
||||
}
|
||||
|
||||
export interface XenApiSr extends XenApiRecord<"SR"> {
|
||||
export interface XenApiSr extends XenApiRecord<"Sr"> {
|
||||
name_label: string;
|
||||
physical_size: number;
|
||||
physical_utilisation: number;
|
||||
}
|
||||
|
||||
export interface XenApiVm extends XenApiRecord<"VM"> {
|
||||
export interface XenApiVm extends XenApiRecord<"Vm"> {
|
||||
current_operations: Record<string, VM_OPERATION>;
|
||||
guest_metrics: string;
|
||||
metrics: XenApiVmMetrics["$ref"];
|
||||
@@ -138,24 +135,24 @@ export interface XenApiVm extends XenApiRecord<"VM"> {
|
||||
VCPUs_at_startup: number;
|
||||
}
|
||||
|
||||
export interface XenApiConsole extends XenApiRecord<"console"> {
|
||||
export interface XenApiConsole extends XenApiRecord<"Console"> {
|
||||
protocol: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
export interface XenApiHostMetrics extends XenApiRecord<"host_metrics"> {
|
||||
export interface XenApiHostMetrics extends XenApiRecord<"HostMetrics"> {
|
||||
live: boolean;
|
||||
memory_free: number;
|
||||
memory_total: number;
|
||||
}
|
||||
|
||||
export interface XenApiVmMetrics extends XenApiRecord<"VM_metrics"> {
|
||||
export interface XenApiVmMetrics extends XenApiRecord<"VmMetrics"> {
|
||||
VCPUs_number: number;
|
||||
}
|
||||
|
||||
export type XenApiVmGuestMetrics = XenApiRecord<"VM_guest_metrics">;
|
||||
export type XenApiVmGuestMetrics = XenApiRecord<"VmGuestMetrics">;
|
||||
|
||||
export interface XenApiTask extends XenApiRecord<"task"> {
|
||||
export interface XenApiTask extends XenApiRecord<"Task"> {
|
||||
name_label: string;
|
||||
resident_on: XenApiHost["$ref"];
|
||||
created: string;
|
||||
@@ -164,61 +161,17 @@ export interface XenApiTask extends XenApiRecord<"task"> {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface XenApiMessage<T extends RawObjectType = RawObjectType>
|
||||
extends XenApiRecord<"message"> {
|
||||
body: string;
|
||||
cls: T;
|
||||
export interface XenApiMessage extends XenApiRecord<"Message"> {
|
||||
name: string;
|
||||
obj_uuid: RawTypeToRecord<T>["uuid"];
|
||||
priority: number;
|
||||
timestamp: string;
|
||||
cls: RawObjectType;
|
||||
}
|
||||
|
||||
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;
|
||||
operation: "add" | "mod" | "del";
|
||||
ref: XenApiRecord<RawObjectType>["$ref"];
|
||||
snapshot: RawXenApiRecord<XenApiRecord<RawObjectType>>;
|
||||
ref: XenApiRecord<string>["$ref"];
|
||||
snapshot: RawXenApiRecord<XenApiRecord<string>>;
|
||||
};
|
||||
|
||||
type WatchCallback = (results: WatchCallbackResult[]) => void;
|
||||
@@ -331,17 +284,16 @@ export default class XenApi {
|
||||
return fetch(url, { signal: abortSignal });
|
||||
}
|
||||
|
||||
async loadRecords<
|
||||
T extends RawObjectType,
|
||||
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
|
||||
>(type: T): Promise<R[]> {
|
||||
const result = await this.#call<{ [key: string]: R }>(
|
||||
async loadRecords<T extends XenApiRecord<string>>(
|
||||
type: RawObjectType
|
||||
): Promise<T[]> {
|
||||
const result = await this.#call<{ [key: string]: RawXenApiRecord<T> }>(
|
||||
`${type}.get_all_records`,
|
||||
[this.sessionId]
|
||||
);
|
||||
|
||||
return Object.entries(result).map(([opaqueRef, record]) =>
|
||||
buildXoObject(record, { opaqueRef: opaqueRef as R["$ref"] })
|
||||
buildXoObject(record, { opaqueRef: opaqueRef as T["$ref"] })
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import type { XenApiAlarm } from "@/libs/xen-api";
|
||||
import type { XenApiMessage } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import type {
|
||||
DeferExtension,
|
||||
Options,
|
||||
Subscription,
|
||||
XenApiRecordExtension,
|
||||
} from "@/types/subscription";
|
||||
import { createSubscribe } from "@/types/xapi-collection";
|
||||
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 subscribe = createSubscribe<XenApiMessage, []>((options) => {
|
||||
const originalSubscription = messageCollection.subscribe(options);
|
||||
|
||||
const extendedSubscription = {
|
||||
records: computed(() =>
|
||||
subscription.records.value.filter((record) => record.name === "alarm")
|
||||
originalSubscription.records.value.filter(
|
||||
(record) => record.name === "alarm"
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
} as Subscription<Extensions, O>;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...messageCollection,
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
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)
|
||||
)
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,28 +1,88 @@
|
||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import type { HostExtensions } from "@/stores/host.extension";
|
||||
import {
|
||||
getStatsSubscription,
|
||||
runningHostsSubscription,
|
||||
} from "@/stores/host.extension";
|
||||
import { isHostRunning, sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import type {
|
||||
GRANULARITY,
|
||||
HostStats,
|
||||
XapiStatsResponse,
|
||||
} from "@/libs/xapi-stats";
|
||||
import type { XenApiHost, XenApiHostMetrics } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import type { Options, Subscription } from "@/types/subscription";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import type { Subscription } from "@/types/xapi-collection";
|
||||
import { createSubscribe } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
|
||||
type GetStats = (
|
||||
hostUuid: XenApiHost["uuid"],
|
||||
granularity: GRANULARITY,
|
||||
ignoreExpired: boolean,
|
||||
opts: { abortSignal?: AbortSignal }
|
||||
) => Promise<XapiStatsResponse<HostStats> | undefined> | undefined;
|
||||
|
||||
type GetStatsExtension = {
|
||||
getStats: GetStats;
|
||||
};
|
||||
|
||||
type RunningHostsExtension = [
|
||||
{ runningHosts: ComputedRef<XenApiHost[]> },
|
||||
{ hostMetricsSubscription: Subscription<XenApiHostMetrics, any> }
|
||||
];
|
||||
|
||||
type Extensions = [GetStatsExtension, RunningHostsExtension];
|
||||
|
||||
export const useHostStore = defineStore("host", () => {
|
||||
const xenApiStore = useXenApiStore();
|
||||
const hostCollection = useXapiCollectionStore().get("host");
|
||||
|
||||
hostCollection.setSort(sortRecordsByNameLabel);
|
||||
|
||||
const subscribe = <O extends Options<HostExtensions>>(options?: O) => {
|
||||
const subscription = hostCollection.subscribe(options);
|
||||
const { hostMetricsSubscription } = options ?? {};
|
||||
const subscribe = createSubscribe<XenApiHost, Extensions>((options) => {
|
||||
const originalSubscription = hostCollection.subscribe(options);
|
||||
|
||||
const getStats: GetStats = (
|
||||
hostUuid,
|
||||
granularity,
|
||||
ignoreExpired = false,
|
||||
{ abortSignal }
|
||||
) => {
|
||||
const host = originalSubscription.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,
|
||||
});
|
||||
};
|
||||
|
||||
const extendedSubscription = {
|
||||
getStats,
|
||||
};
|
||||
|
||||
const hostMetricsSubscription = options?.hostMetricsSubscription;
|
||||
|
||||
const runningHostsSubscription = hostMetricsSubscription !== undefined && {
|
||||
runningHosts: computed(() =>
|
||||
originalSubscription.records.value.filter((host) =>
|
||||
isHostRunning(host, hostMetricsSubscription)
|
||||
)
|
||||
),
|
||||
};
|
||||
return {
|
||||
...subscription,
|
||||
...getStatsSubscription(subscription),
|
||||
...runningHostsSubscription(subscription, hostMetricsSubscription),
|
||||
} as Subscription<HostExtensions, O>;
|
||||
};
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
...runningHostsSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...hostCollection,
|
||||
|
||||
@@ -1,36 +1,31 @@
|
||||
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 { createSubscribe } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
|
||||
type PoolExtension = Extension<{
|
||||
type PoolExtension = {
|
||||
pool: ComputedRef<XenApiPool | undefined>;
|
||||
}>;
|
||||
};
|
||||
|
||||
type Extensions = [XenApiRecordExtension<XenApiPool>, PoolExtension];
|
||||
type Extensions = [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)),
|
||||
const subscribe = createSubscribe<XenApiPool, Extensions>((options) => {
|
||||
const originalSubscription = poolCollection.subscribe(options);
|
||||
|
||||
const extendedSubscription = {
|
||||
pool: computed(() => getFirst(originalSubscription.records.value)),
|
||||
};
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
} as Subscription<Extensions, O>;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...poolCollection,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import useCollectionSorter from "@/composables/collection-sorter.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";
|
||||
|
||||
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"],
|
||||
});
|
||||
|
||||
const { predicate } = useCollectionFilter({
|
||||
initialFilters: [
|
||||
"!name_label:|(SR.scan host.call_plugin)",
|
||||
"status:pending",
|
||||
],
|
||||
});
|
||||
|
||||
const sortedTasks = useSortedCollection(taskSubscription.records, compareFn);
|
||||
|
||||
return {
|
||||
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
|
||||
finishedTasks: useArrayRemovedItemsHistory(
|
||||
sortedTasks,
|
||||
(task) => task.uuid,
|
||||
{
|
||||
limit: 50,
|
||||
onRemove: (tasks) =>
|
||||
tasks.map((task) => ({
|
||||
...task,
|
||||
finished: new Date().toISOString(),
|
||||
})),
|
||||
}
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,22 +1,64 @@
|
||||
import {
|
||||
additionalTasksSubscription,
|
||||
type TaskExtensions,
|
||||
} from "@/stores/task.extension";
|
||||
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import useCollectionSorter from "@/composables/collection-sorter.composable";
|
||||
import useFilteredCollection from "@/composables/filtered-collection.composable";
|
||||
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||
import type { XenApiTask } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import type { Options, Subscription } from "@/types/subscription";
|
||||
import { createSubscribe } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
|
||||
type PendingTasksExtension = {
|
||||
pendingTasks: ComputedRef<XenApiTask[]>;
|
||||
};
|
||||
|
||||
type FinishedTasksExtension = {
|
||||
finishedTasks: Ref<XenApiTask[]>;
|
||||
};
|
||||
|
||||
type Extensions = [PendingTasksExtension, FinishedTasksExtension];
|
||||
|
||||
export const useTaskStore = defineStore("task", () => {
|
||||
const tasksCollection = useXapiCollectionStore().get("task");
|
||||
|
||||
const subscribe = <O extends Options<TaskExtensions>>(options?: O) => {
|
||||
const subscription = tasksCollection.subscribe(options);
|
||||
const subscribe = createSubscribe<XenApiTask, Extensions>(() => {
|
||||
const subscription = tasksCollection.subscribe();
|
||||
|
||||
const { compareFn } = useCollectionSorter<XenApiTask>({
|
||||
initialSorts: ["-created"],
|
||||
});
|
||||
|
||||
const sortedTasks = useSortedCollection(subscription.records, compareFn);
|
||||
|
||||
const { predicate } = useCollectionFilter({
|
||||
initialFilters: [
|
||||
"!name_label:|(SR.scan host.call_plugin)",
|
||||
"status:pending",
|
||||
],
|
||||
});
|
||||
|
||||
const extendedSubscription = {
|
||||
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
|
||||
finishedTasks: useArrayRemovedItemsHistory(
|
||||
sortedTasks,
|
||||
(task) => task.uuid,
|
||||
{
|
||||
limit: 50,
|
||||
onRemove: (tasks) =>
|
||||
tasks.map((task) => ({
|
||||
...task,
|
||||
finished: new Date().toISOString(),
|
||||
})),
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
...additionalTasksSubscription(subscription),
|
||||
} as Subscription<TaskExtensions, O>;
|
||||
};
|
||||
...extendedSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return { ...tasksCollection, subscribe };
|
||||
});
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,36 @@
|
||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import {
|
||||
getStatsSubscription,
|
||||
recordsByHostRefSubscription,
|
||||
runningVmsSubscription,
|
||||
type VmExtensions,
|
||||
} from "@/stores/vm.extension";
|
||||
import type {
|
||||
GRANULARITY,
|
||||
VmStats,
|
||||
XapiStatsResponse,
|
||||
} from "@/libs/xapi-stats";
|
||||
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
|
||||
import { POWER_STATE } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import type { Options, Subscription } from "@/types/subscription";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { createSubscribe, type Subscription } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
|
||||
type GetStats = (
|
||||
id: XenApiVm["uuid"],
|
||||
granularity: GRANULARITY,
|
||||
ignoreExpired: boolean,
|
||||
opts: { abortSignal?: AbortSignal }
|
||||
) => Promise<XapiStatsResponse<VmStats> | undefined> | undefined;
|
||||
type DefaultExtension = {
|
||||
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
|
||||
runningVms: ComputedRef<XenApiVm[]>;
|
||||
};
|
||||
|
||||
type GetStatsExtension = [
|
||||
{
|
||||
getStats: GetStats;
|
||||
},
|
||||
{ hostSubscription: Subscription<XenApiHost, object> }
|
||||
];
|
||||
|
||||
type Extensions = [DefaultExtension, GetStatsExtension];
|
||||
|
||||
export const useVmStore = defineStore("vm", () => {
|
||||
const vmCollection = useXapiCollectionStore().get("VM");
|
||||
@@ -18,16 +41,82 @@ export const useVmStore = defineStore("vm", () => {
|
||||
|
||||
vmCollection.setSort(sortRecordsByNameLabel);
|
||||
|
||||
const subscribe = <O extends Options<VmExtensions>>(options?: O) => {
|
||||
const subscription = vmCollection.subscribe(options);
|
||||
const subscribe = createSubscribe<XenApiVm, Extensions>((options) => {
|
||||
const originalSubscription = vmCollection.subscribe(options);
|
||||
|
||||
const extendedSubscription = {
|
||||
recordsByHostRef: computed(() => {
|
||||
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
|
||||
|
||||
originalSubscription.records.value.forEach((vm) => {
|
||||
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
|
||||
vmsByHostOpaqueRef.set(vm.resident_on, []);
|
||||
}
|
||||
|
||||
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
|
||||
});
|
||||
|
||||
return vmsByHostOpaqueRef;
|
||||
}),
|
||||
runningVms: computed(() =>
|
||||
originalSubscription.records.value.filter(
|
||||
(vm) => vm.power_state === POWER_STATE.RUNNING
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
const hostSubscription = options?.hostSubscription;
|
||||
|
||||
const getStatsSubscription:
|
||||
| {
|
||||
getStats: GetStats;
|
||||
}
|
||||
| undefined =
|
||||
hostSubscription !== undefined
|
||||
? {
|
||||
getStats: (
|
||||
id,
|
||||
granularity,
|
||||
ignoreExpired = false,
|
||||
{ abortSignal }
|
||||
) => {
|
||||
const xenApiStore = useXenApiStore();
|
||||
|
||||
if (!xenApiStore.isConnected) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const vm = originalSubscription.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,
|
||||
});
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
...recordsByHostRefSubscription(subscription),
|
||||
...runningVmsSubscription(subscription),
|
||||
...getStatsSubscription(subscription, options?.hostSubscription),
|
||||
} as Subscription<VmExtensions, O>;
|
||||
};
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
...getStatsSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...vmCollection,
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import type { RawObjectType, RawTypeToRecord } from "@/libs/xen-api";
|
||||
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import type {
|
||||
DeferExtension,
|
||||
Options,
|
||||
RawTypeToObject,
|
||||
SubscribeOptions,
|
||||
Subscription,
|
||||
XenApiRecordExtension,
|
||||
} from "@/types/subscription";
|
||||
} from "@/types/xapi-collection";
|
||||
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());
|
||||
const collections = ref(
|
||||
new Map<RawObjectType, ReturnType<typeof createXapiCollection<any>>>()
|
||||
);
|
||||
|
||||
function get<T extends RawObjectType>(
|
||||
type: T
|
||||
): ReturnType<typeof createXapiCollection<T>> {
|
||||
function get<
|
||||
T extends RawObjectType,
|
||||
S extends XenApiRecord<string> = RawTypeToObject[T]
|
||||
>(type: T): ReturnType<typeof createXapiCollection<S>> {
|
||||
if (!collections.value.has(type)) {
|
||||
collections.value.set(type, createXapiCollection(type));
|
||||
collections.value.set(type, createXapiCollection<S>(type));
|
||||
}
|
||||
|
||||
return collections.value.get(type)!;
|
||||
@@ -26,11 +28,8 @@ export const useXapiCollectionStore = defineStore("xapiCollection", () => {
|
||||
return { get };
|
||||
});
|
||||
|
||||
const createXapiCollection = <
|
||||
T extends RawObjectType,
|
||||
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
|
||||
>(
|
||||
type: T
|
||||
const createXapiCollection = <T extends XenApiRecord<string>>(
|
||||
type: RawObjectType
|
||||
) => {
|
||||
const isReady = ref(false);
|
||||
const isFetching = ref(false);
|
||||
@@ -38,31 +37,31 @@ const createXapiCollection = <
|
||||
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 recordsByOpaqueRef = ref(new Map<T["$ref"], T>());
|
||||
const recordsByUuid = ref(new Map<T["uuid"], T>());
|
||||
const filter = ref<(record: T) => boolean>();
|
||||
const sort = ref<(record1: T, record2: T) => 1 | 0 | -1>();
|
||||
const xenApiStore = useXenApiStore();
|
||||
|
||||
const setFilter = (newFilter: (record: R) => boolean) =>
|
||||
const setFilter = (newFilter: (record: T) => boolean) =>
|
||||
(filter.value = newFilter);
|
||||
|
||||
const setSort = (newSort: (record1: R, record2: R) => 1 | 0 | -1) =>
|
||||
const setSort = (newSort: (record1: T, record2: T) => 1 | 0 | -1) =>
|
||||
(sort.value = newSort);
|
||||
|
||||
const records = computed<R[]>(() => {
|
||||
const records = computed<T[]>(() => {
|
||||
const records = Array.from(recordsByOpaqueRef.value.values()).sort(
|
||||
sort.value
|
||||
);
|
||||
return filter.value !== undefined ? records.filter(filter.value) : records;
|
||||
});
|
||||
|
||||
const getByOpaqueRef = (opaqueRef: R["$ref"]) =>
|
||||
const getByOpaqueRef = (opaqueRef: T["$ref"]) =>
|
||||
recordsByOpaqueRef.value.get(opaqueRef);
|
||||
|
||||
const getByUuid = (uuid: R["uuid"]) => recordsByUuid.value.get(uuid);
|
||||
const getByUuid = (uuid: T["uuid"]) => recordsByUuid.value.get(uuid);
|
||||
|
||||
const hasUuid = (uuid: R["uuid"]) => recordsByUuid.value.has(uuid);
|
||||
const hasUuid = (uuid: T["uuid"]) => recordsByUuid.value.has(uuid);
|
||||
|
||||
const hasSubscriptions = computed(() => subscriptions.value.size > 0);
|
||||
|
||||
@@ -70,7 +69,7 @@ const createXapiCollection = <
|
||||
try {
|
||||
isFetching.value = true;
|
||||
lastError.value = undefined;
|
||||
const records = await xenApiStore.getXapi().loadRecords<T, R>(type);
|
||||
const records = await xenApiStore.getXapi().loadRecords<T>(type);
|
||||
recordsByOpaqueRef.value.clear();
|
||||
recordsByUuid.value.clear();
|
||||
records.forEach(add);
|
||||
@@ -82,17 +81,17 @@ const createXapiCollection = <
|
||||
}
|
||||
};
|
||||
|
||||
const add = (record: R) => {
|
||||
const add = (record: T) => {
|
||||
recordsByOpaqueRef.value.set(record.$ref, record);
|
||||
recordsByUuid.value.set(record.uuid, record);
|
||||
};
|
||||
|
||||
const update = (record: R) => {
|
||||
const update = (record: T) => {
|
||||
recordsByOpaqueRef.value.set(record.$ref, record);
|
||||
recordsByUuid.value.set(record.uuid, record);
|
||||
};
|
||||
|
||||
const remove = (opaqueRef: R["$ref"]) => {
|
||||
const remove = (opaqueRef: T["$ref"]) => {
|
||||
if (!recordsByOpaqueRef.value.has(opaqueRef)) {
|
||||
return;
|
||||
}
|
||||
@@ -107,11 +106,9 @@ const createXapiCollection = <
|
||||
() => fetchAll()
|
||||
);
|
||||
|
||||
type Extensions = [XenApiRecordExtension<R>, DeferExtension];
|
||||
|
||||
function subscribe<O extends Options<Extensions>>(
|
||||
function subscribe<O extends SubscribeOptions<any>>(
|
||||
options?: O
|
||||
): Subscription<Extensions, O> {
|
||||
): Subscription<T, O> {
|
||||
const id = Symbol();
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
@@ -134,14 +131,14 @@ const createXapiCollection = <
|
||||
|
||||
if (options?.immediate !== false) {
|
||||
start();
|
||||
return subscription as Subscription<Extensions, O>;
|
||||
return subscription as unknown as Subscription<T, O>;
|
||||
}
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
start,
|
||||
isStarted: computed(() => subscriptions.value.has(id)),
|
||||
} as Subscription<Extensions, O>;
|
||||
} as unknown as Subscription<T, O>;
|
||||
}
|
||||
|
||||
const unsubscribe = (id: symbol) => subscriptions.value.delete(id);
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
iconProp().preset(faRocket),
|
||||
prop('label').required().str().widget().preset('Rockets'),
|
||||
prop('count')
|
||||
.required()
|
||||
.type('string | number')
|
||||
.widget(text())
|
||||
.preset('175'),
|
||||
]"
|
||||
:presets="presets"
|
||||
>
|
||||
<UiResource v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiResource from "@/components/ui/resources/UiResource.vue";
|
||||
import { iconProp, prop } from "@/libs/story/story-param";
|
||||
import { text } from "@/libs/story/story-widget";
|
||||
import {
|
||||
faDatabase,
|
||||
faDisplay,
|
||||
faMemory,
|
||||
faMicrochip,
|
||||
faNetworkWired,
|
||||
faRocket,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const presets = {
|
||||
VMs: {
|
||||
props: {
|
||||
icon: faDisplay,
|
||||
count: 1,
|
||||
label: "VMs",
|
||||
},
|
||||
},
|
||||
vCPUs: {
|
||||
props: {
|
||||
icon: faMicrochip,
|
||||
count: 4,
|
||||
label: "vCPUs",
|
||||
},
|
||||
},
|
||||
RAM: {
|
||||
props: {
|
||||
icon: faMemory,
|
||||
count: 2,
|
||||
label: "RAM",
|
||||
},
|
||||
},
|
||||
SR: {
|
||||
props: {
|
||||
icon: faDatabase,
|
||||
count: 1,
|
||||
label: "SR",
|
||||
},
|
||||
},
|
||||
Interfaces: {
|
||||
props: {
|
||||
icon: faNetworkWired,
|
||||
count: 2,
|
||||
label: "Interfaces",
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
# Example
|
||||
|
||||
```vue-template
|
||||
<UiResources>
|
||||
<UiResource :icon="faDisplay" count="1" label="VMs" />
|
||||
<UiResource :icon="faMicrochip" count="4" label="vCPUs" />
|
||||
<UiResource :icon="faMemory" count="2" label="RAM" />
|
||||
<UiResource :icon="faDatabase" count="1" label="SR" />
|
||||
<UiResource :icon="faNetworkWired" count="2" label="Interfaces" />
|
||||
</UiResources>
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
:params="[slot().help('One or multiple `UiResource`')]"
|
||||
full-width-component
|
||||
>
|
||||
<UiResources>
|
||||
<UiResource :icon="faDisplay" count="1" label="VMs" />
|
||||
<UiResource :icon="faMicrochip" count="4" label="vCPUs" />
|
||||
<UiResource :icon="faMemory" count="2" label="RAM" />
|
||||
<UiResource :icon="faDatabase" count="1" label="SR" />
|
||||
<UiResource :icon="faNetworkWired" count="2" label="Interfaces" />
|
||||
</UiResources>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiResource from "@/components/ui/resources/UiResource.vue";
|
||||
import UiResources from "@/components/ui/resources/UiResources.vue";
|
||||
import { slot } from "@/libs/story/story-param";
|
||||
import {
|
||||
faDatabase,
|
||||
faDisplay,
|
||||
faMemory,
|
||||
faMicrochip,
|
||||
faNetworkWired,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
</script>
|
||||
@@ -1,74 +0,0 @@
|
||||
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>>;
|
||||
140
@xen-orchestra/lite/src/types/xapi-collection.ts
Normal file
140
@xen-orchestra/lite/src/types/xapi-collection.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type {
|
||||
XenApiConsole,
|
||||
XenApiHost,
|
||||
XenApiHostMetrics,
|
||||
XenApiMessage,
|
||||
XenApiPool,
|
||||
XenApiRecord,
|
||||
XenApiSr,
|
||||
XenApiTask,
|
||||
XenApiVm,
|
||||
XenApiVmGuestMetrics,
|
||||
XenApiVmMetrics,
|
||||
} from "@/libs/xen-api";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
|
||||
type DefaultExtension<T extends XenApiRecord<string>> = {
|
||||
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>>;
|
||||
};
|
||||
|
||||
type DeferExtension = [
|
||||
{
|
||||
start: () => void;
|
||||
isStarted: ComputedRef<boolean>;
|
||||
},
|
||||
{ immediate: false }
|
||||
];
|
||||
|
||||
type DefaultExtensions<T extends XenApiRecord<string>> = [
|
||||
DefaultExtension<T>,
|
||||
DeferExtension
|
||||
];
|
||||
|
||||
type GenerateSubscribeOptions<Extensions extends any[]> = Extensions extends [
|
||||
infer FirstExtension,
|
||||
...infer RestExtension
|
||||
]
|
||||
? FirstExtension extends [object, infer FirstCondition]
|
||||
? FirstCondition & GenerateSubscribeOptions<RestExtension>
|
||||
: GenerateSubscribeOptions<RestExtension>
|
||||
: object;
|
||||
|
||||
export type SubscribeOptions<Extensions extends any[]> = Partial<
|
||||
GenerateSubscribeOptions<Extensions> &
|
||||
GenerateSubscribeOptions<DefaultExtensions<any>>
|
||||
>;
|
||||
|
||||
type GenerateSubscription<
|
||||
Options extends object,
|
||||
Extensions extends any[]
|
||||
> = Extensions extends [infer FirstExtension, ...infer RestExtension]
|
||||
? FirstExtension extends [infer FirstObject, infer FirstCondition]
|
||||
? Options extends FirstCondition
|
||||
? FirstObject & GenerateSubscription<Options, RestExtension>
|
||||
: GenerateSubscription<Options, RestExtension>
|
||||
: FirstExtension & GenerateSubscription<Options, RestExtension>
|
||||
: object;
|
||||
|
||||
export type Subscription<
|
||||
T extends XenApiRecord<string>,
|
||||
Options extends object,
|
||||
Extensions extends any[] = []
|
||||
> = GenerateSubscription<Options, Extensions> &
|
||||
GenerateSubscription<Options, DefaultExtensions<T>>;
|
||||
|
||||
export function createSubscribe<
|
||||
T extends XenApiRecord<string>,
|
||||
Extensions extends any[],
|
||||
Options extends object = SubscribeOptions<Extensions>
|
||||
>(builder: (options?: Options) => Subscription<T, Options, Extensions>) {
|
||||
return function subscribe<O extends Options>(
|
||||
options?: O
|
||||
): Subscription<T, O, Extensions> {
|
||||
return builder(options);
|
||||
};
|
||||
}
|
||||
|
||||
export type RawTypeToObject = {
|
||||
Bond: never;
|
||||
Certificate: never;
|
||||
Cluster: never;
|
||||
Cluster_host: never;
|
||||
DR_task: never;
|
||||
Feature: never;
|
||||
GPU_group: never;
|
||||
PBD: never;
|
||||
PCI: never;
|
||||
PGPU: never;
|
||||
PIF: never;
|
||||
PIF_metrics: never;
|
||||
PUSB: never;
|
||||
PVS_cache_storage: never;
|
||||
PVS_proxy: never;
|
||||
PVS_server: never;
|
||||
PVS_site: never;
|
||||
SDN_controller: never;
|
||||
SM: never;
|
||||
SR: XenApiSr;
|
||||
USB_group: never;
|
||||
VBD: never;
|
||||
VBD_metrics: never;
|
||||
VDI: never;
|
||||
VGPU: never;
|
||||
VGPU_type: never;
|
||||
VIF: never;
|
||||
VIF_metrics: never;
|
||||
VLAN: never;
|
||||
VM: XenApiVm;
|
||||
VMPP: never;
|
||||
VMSS: never;
|
||||
VM_guest_metrics: XenApiVmGuestMetrics;
|
||||
VM_metrics: XenApiVmMetrics;
|
||||
VUSB: never;
|
||||
blob: never;
|
||||
console: XenApiConsole;
|
||||
crashdump: never;
|
||||
host: XenApiHost;
|
||||
host_cpu: never;
|
||||
host_crashdump: never;
|
||||
host_metrics: XenApiHostMetrics;
|
||||
host_patch: never;
|
||||
message: XenApiMessage;
|
||||
network: never;
|
||||
network_sriov: never;
|
||||
pool: XenApiPool;
|
||||
pool_patch: never;
|
||||
pool_update: never;
|
||||
role: never;
|
||||
secret: never;
|
||||
subject: never;
|
||||
task: XenApiTask;
|
||||
tunnel: never;
|
||||
};
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,11 +1,10 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.85.0** (2023-07-31)
|
||||
## **next**
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Highlights
|
||||
### Enhancements
|
||||
|
||||
- [Backup/Restore] Button to open the raw log in the REST API (PR [#6936](https://github.com/vatesfr/xen-orchestra/pull/6936))
|
||||
- [Import/From VMWare] Support ESXi 6.5+ with snapshot (PR [#6909](https://github.com/vatesfr/xen-orchestra/pull/6909))
|
||||
- [Netbox] New major version. BREAKING: in order for this new version to work, you need to assign the type `virtualization > vminterface` to the custom field `UUID` in your Netbox instance. [See documentation](https://xen-orchestra.com/docs/advanced.html#netbox). [#6038](https://github.com/vatesfr/xen-orchestra/issues/6038) [#6135](https://github.com/vatesfr/xen-orchestra/issues/6135) [#6024](https://github.com/vatesfr/xen-orchestra/issues/6024) [#6036](https://github.com/vatesfr/xen-orchestra/issues/6036) [Forum#6070](https://xcp-ng.org/forum/topic/6070) [Forum#6149](https://xcp-ng.org/forum/topic/6149) [Forum#6332](https://xcp-ng.org/forum/topic/6332) [Forum#6902](https://xcp-ng.org/forum/topic/6902) (PR [#6950](https://github.com/vatesfr/xen-orchestra/pull/6950))
|
||||
- Synchronize VM description
|
||||
@@ -15,16 +14,10 @@
|
||||
- Fix largest IP prefix being picked instead of smallest
|
||||
- Fix synchronization not working if some pools are unavailable
|
||||
- Better error messages
|
||||
- [RPU] Avoid migration of VMs on hosts without missing patches (PR [#6943](https://github.com/vatesfr/xen-orchestra/pull/6943))
|
||||
- [Backup/File restore] Faster and more robust ZIP export
|
||||
- [Backup/File restore] Add faster tar+gzip (`.tgz`) export
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Backup/Restore] Button to open the raw log in the REST API (PR [#6936](https://github.com/vatesfr/xen-orchestra/pull/6936))
|
||||
- [RPU] Avoid migration of VMs on hosts without missing patches (PR [#6943](https://github.com/vatesfr/xen-orchestra/pull/6943))
|
||||
- [Settings/Users] Show users authentication methods (PR [#6962](https://github.com/vatesfr/xen-orchestra/pull/6962))
|
||||
- [Settings/Users] User external authentication methods can be manually removed (PR [#6962](https://github.com/vatesfr/xen-orchestra/pull/6962))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Incremental Backup & Replication] Attempt to work around HVM multiplier issues when creating VMs on older XAPIs (PR [#6866](https://github.com/vatesfr/xen-orchestra/pull/6866))
|
||||
@@ -34,8 +27,6 @@
|
||||
- [New SR] Send provided NFS version to XAPI when probing a share
|
||||
- [Backup/exports] Show more information on error ` stream has ended with not enough data (actual: xxx, expected: 512)` (PR [#6940](https://github.com/vatesfr/xen-orchestra/pull/6940))
|
||||
- [Backup] Fix incremental replication with multiple SRs (PR [#6811](https://github.com/vatesfr/xen-orchestra/pull/6811))
|
||||
- [New VM] Order interfaces by device as done on a VM Network tab (PR [#6944](https://github.com/vatesfr/xen-orchestra/pull/6944))
|
||||
- Users can no longer sign in using their XO password if they are using other authentication providers (PR [#6962](https://github.com/vatesfr/xen-orchestra/pull/6962))
|
||||
|
||||
### Released packages
|
||||
|
||||
@@ -51,17 +42,15 @@
|
||||
- @xen-orchestra/mixins 0.11.0
|
||||
- @xen-orchestra/proxy 0.26.30
|
||||
- @xen-orchestra/vmware-explorer 0.3.0
|
||||
- xo-server 5.119.0
|
||||
- xo-server-audit 0.10.4
|
||||
- xo-server-netbox 1.0.0
|
||||
- xo-server-transport-xmpp 0.1.2
|
||||
- xo-server-auth-github 0.3.0
|
||||
- xo-server-auth-google 0.3.0
|
||||
- xo-web 5.122.2
|
||||
- xo-server 5.120.2
|
||||
- xo-web 5.122.0
|
||||
|
||||
## **5.84.0** (2023-06-30)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -110,6 +99,8 @@
|
||||
|
||||
## **5.83.3** (2023-06-23)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Settings/Servers] Fix connecting using an explicit IPv6 address
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [LDAP] Mark the _Id attribute_ setting as required
|
||||
- [New VM] Order interfaces by device as done on a VM Network tab (PR [#6944](https://github.com/vatesfr/xen-orchestra/pull/6944))
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- xo-server patch
|
||||
- xo-server-auth-ldap patch
|
||||
- xo-web patch
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-auth-github",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "GitHub authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -38,7 +38,7 @@ class AuthGitHubXoPlugin {
|
||||
this._unregisterPassportStrategy = xo.registerPassportStrategy(
|
||||
new Strategy(this._conf, async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
done(null, await xo.registerUser2('github', { id: profile.id, name: profile.username }))
|
||||
done(null, await xo.registerUser('github', profile.username))
|
||||
} catch (error) {
|
||||
done(error.message)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-auth-google",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Google authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -52,10 +52,7 @@ class AuthGoogleXoPlugin {
|
||||
try {
|
||||
done(
|
||||
null,
|
||||
await xo.registerUser2('google', {
|
||||
id: profile.id,
|
||||
name: conf.scope === 'email' ? profile.emails[0].value : profile.displayName,
|
||||
})
|
||||
await xo.registerUser('google', conf.scope === 'email' ? profile.emails[0].value : profile.displayName)
|
||||
)
|
||||
} catch (error) {
|
||||
done(error.message)
|
||||
|
||||
@@ -11,6 +11,11 @@ const logger = createLogger('xo:xo-server-auth-ldap')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULTS = {
|
||||
checkCertificate: true,
|
||||
filter: '(uid={{name}})',
|
||||
}
|
||||
|
||||
const { escape } = Filter.prototype
|
||||
|
||||
const VAR_RE = /\{\{([^}]+)\}\}/g
|
||||
@@ -50,7 +55,7 @@ If not specified, it will use a default set of well-known CAs.
|
||||
description:
|
||||
"Enforce the validity of the server's certificates. You can disable it when connecting to servers that use a self-signed certificate.",
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
default: DEFAULTS.checkCertificate,
|
||||
},
|
||||
startTls: {
|
||||
title: 'Use StartTLS',
|
||||
@@ -105,7 +110,7 @@ Or something like this if you also want to filter by group:
|
||||
- \`(&(sAMAccountName={{name}})(memberOf=<group DN>))\`
|
||||
`.trim(),
|
||||
type: 'string',
|
||||
default: '(uid={{name}})',
|
||||
default: DEFAULTS.filter,
|
||||
},
|
||||
userIdAttribute: {
|
||||
title: 'ID attribute',
|
||||
@@ -159,7 +164,7 @@ Or something like this if you also want to filter by group:
|
||||
required: ['base', 'filter', 'idAttribute', 'displayNameAttribute', 'membersMapping'],
|
||||
},
|
||||
},
|
||||
required: ['uri', 'base', 'userIdAttribute'],
|
||||
required: ['uri', 'base'],
|
||||
}
|
||||
|
||||
export const testSchema = {
|
||||
@@ -193,7 +198,7 @@ class AuthLdap {
|
||||
})
|
||||
|
||||
{
|
||||
const { checkCertificate, certificateAuthorities } = conf
|
||||
const { checkCertificate = DEFAULTS.checkCertificate, certificateAuthorities } = conf
|
||||
|
||||
const tlsOptions = (this._tlsOptions = {})
|
||||
|
||||
@@ -207,7 +212,15 @@ class AuthLdap {
|
||||
}
|
||||
}
|
||||
|
||||
const { bind: credentials, base: searchBase, filter: searchFilter, startTls, groups, uri, userIdAttribute } = conf
|
||||
const {
|
||||
bind: credentials,
|
||||
base: searchBase,
|
||||
filter: searchFilter = DEFAULTS.filter,
|
||||
startTls = false,
|
||||
groups,
|
||||
uri,
|
||||
userIdAttribute,
|
||||
} = conf
|
||||
|
||||
this._credentials = credentials
|
||||
this._serverUri = uri
|
||||
@@ -290,17 +303,23 @@ class AuthLdap {
|
||||
return
|
||||
}
|
||||
|
||||
const ldapId = entry[this._userIdAttribute]
|
||||
const user = await this._xo.registerUser2('ldap', {
|
||||
user: { id: ldapId, name: username },
|
||||
})
|
||||
let user
|
||||
if (this._userIdAttribute === undefined) {
|
||||
// Support legacy config
|
||||
user = await this._xo.registerUser(undefined, username)
|
||||
} else {
|
||||
const ldapId = entry[this._userIdAttribute]
|
||||
user = await this._xo.registerUser2('ldap', {
|
||||
user: { id: ldapId, name: username },
|
||||
})
|
||||
|
||||
const groupsConfig = this._groupsConfig
|
||||
if (groupsConfig !== undefined) {
|
||||
try {
|
||||
await this._synchronizeGroups(user, entry[groupsConfig.membersMapping.userAttribute])
|
||||
} catch (error) {
|
||||
logger.error(`failed to synchronize groups: ${error.message}`)
|
||||
const groupsConfig = this._groupsConfig
|
||||
if (groupsConfig !== undefined) {
|
||||
try {
|
||||
await this._synchronizeGroups(user, entry[groupsConfig.membersMapping.userAttribute])
|
||||
} catch (error) {
|
||||
logger.error(`failed to synchronize groups: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,27 +13,12 @@ exports.configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {
|
||||
// name of the setting to display in the UI instead of the raw name of property (here `foo`).
|
||||
title: 'Foo',
|
||||
|
||||
// Markdown description for this setting
|
||||
description: 'Value to use when doing foo',
|
||||
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['foo'],
|
||||
}
|
||||
|
||||
// This (optional) dictionary provides example configurations that can be used to help
|
||||
// configuring this plugin.
|
||||
//
|
||||
// The keys are the preset names, and the values are subset of the configuration.
|
||||
exports.configurationPresets = {
|
||||
'preset 1': { foo: 'foo value 1' },
|
||||
'preset 2': { foo: 'foo value 2' },
|
||||
}
|
||||
|
||||
// This (optional) schema is used to test the configuration
|
||||
// of the plugin.
|
||||
exports.testSchema = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.120.2",
|
||||
"version": "5.119.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -112,16 +112,3 @@ changePassword.params = {
|
||||
oldPassword: { type: 'string' },
|
||||
newPassword: { type: 'string' },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function removeAuthProvider({ id, authProvider }) {
|
||||
await this.updateUser(id, { authProviders: { [authProvider]: null } })
|
||||
}
|
||||
|
||||
removeAuthProvider.permission = 'admin'
|
||||
|
||||
removeAuthProvider.params = {
|
||||
authProvider: { type: 'string' },
|
||||
id: { type: 'string' },
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ xo-server-recover-account <user name or email>
|
||||
const user = await xo.getUserByName(name, true)
|
||||
if (user !== null) {
|
||||
await xo.updateUser(user.id, {
|
||||
authProviders: null,
|
||||
password,
|
||||
permission: 'admin',
|
||||
preferences: { otp: null },
|
||||
|
||||
@@ -113,6 +113,8 @@ export default class {
|
||||
// - `userId`
|
||||
// - optionally `expiration` to indicate when the session is no longer
|
||||
// valid
|
||||
// - an object with a property `username` containing the name
|
||||
// of the authenticated user
|
||||
const result = await provider(credentials, userData)
|
||||
|
||||
// No match.
|
||||
@@ -120,10 +122,10 @@ export default class {
|
||||
continue
|
||||
}
|
||||
|
||||
const { userId, expiration } = result
|
||||
const { userId, username, expiration } = result
|
||||
|
||||
return {
|
||||
user: await this._app.getUser(userId),
|
||||
user: await (userId !== undefined ? this._app.getUser(userId) : this._app.registerUser(undefined, username)),
|
||||
expiration,
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -150,8 +150,8 @@ export default class {
|
||||
if (permission) {
|
||||
user.permission = permission
|
||||
}
|
||||
if (password !== undefined) {
|
||||
user.pw_hash = password === null ? undefined : await hash(password)
|
||||
if (password) {
|
||||
user.pw_hash = await hash(password)
|
||||
}
|
||||
|
||||
const newPreferences = { ...user.preferences }
|
||||
@@ -164,33 +164,15 @@ export default class {
|
||||
})
|
||||
user.preferences = isEmpty(newPreferences) ? undefined : newPreferences
|
||||
|
||||
if (authProviders !== undefined) {
|
||||
let newAuthProviders
|
||||
if (authProviders !== null) {
|
||||
newAuthProviders = { ...user.authProviders }
|
||||
forEach(authProviders, (value, name) => {
|
||||
if (value == null) {
|
||||
delete newAuthProviders[name]
|
||||
} else {
|
||||
newAuthProviders[name] = value
|
||||
}
|
||||
})
|
||||
const newAuthProviders = { ...user.authProviders }
|
||||
forEach(authProviders, (value, name) => {
|
||||
if (value == null) {
|
||||
delete newAuthProviders[name]
|
||||
} else {
|
||||
newAuthProviders[name] = value
|
||||
}
|
||||
user.authProviders = isEmpty(newAuthProviders) ? undefined : newAuthProviders
|
||||
}
|
||||
|
||||
// if updating either authProviders or password, check consistency
|
||||
if (
|
||||
(authProviders !== undefined || password !== undefined) &&
|
||||
user.pw_hash !== undefined &&
|
||||
!isEmpty(user.authProviders)
|
||||
) {
|
||||
throw new Error('user cannot have both password and auth providers')
|
||||
}
|
||||
|
||||
if (user.pw_hash === undefined && isEmpty(user.authProviders) && id === this._app.apiContext?.user.id) {
|
||||
throw new Error('current user cannot be without password and auth providers')
|
||||
}
|
||||
})
|
||||
user.authProviders = isEmpty(newAuthProviders) ? undefined : newAuthProviders
|
||||
|
||||
// TODO: remove
|
||||
user.email = user.name
|
||||
@@ -239,8 +221,26 @@ export default class {
|
||||
throw noSuchObject(username, 'user')
|
||||
}
|
||||
|
||||
async registerUser() {
|
||||
throw new Error('use registerUser2 instead')
|
||||
// Deprecated: use registerUser2 instead
|
||||
// Get or create a user associated with an auth provider.
|
||||
async registerUser(provider, name) {
|
||||
const user = await this.getUserByName(name, true)
|
||||
if (user) {
|
||||
if (user._provider !== provider) {
|
||||
throw new Error(`the name ${name} is already taken`)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
if (!this._app.config.get('createUserOnFirstSignin')) {
|
||||
throw new Error(`registering ${name} user is forbidden`)
|
||||
}
|
||||
|
||||
return /* await */ this.createUser({
|
||||
name,
|
||||
_provider: provider,
|
||||
})
|
||||
}
|
||||
|
||||
// New implementation of registerUser that:
|
||||
@@ -306,7 +306,6 @@ export default class {
|
||||
data: data !== undefined ? data : user.authProviders?.[providerId]?.data,
|
||||
},
|
||||
},
|
||||
password: null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -322,8 +321,8 @@ export default class {
|
||||
}
|
||||
|
||||
async checkUserPassword(userId, password, updateIfNecessary = true) {
|
||||
const { authProviders, pw_hash: hash } = await this.getUser(userId)
|
||||
if (!(hash !== undefined && isEmpty(authProviders) && (await verify(password, hash)))) {
|
||||
const { pw_hash: hash } = await this.getUser(userId)
|
||||
if (!(hash && (await verify(password, hash)))) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.122.2",
|
||||
"version": "5.122.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -733,7 +733,7 @@ const messages = {
|
||||
userGroupsColumn: 'Member of',
|
||||
userCountGroups: '{nGroups, number} group{nGroups, plural, one {} other {s}}',
|
||||
userPermissionColumn: 'Permissions',
|
||||
userAuthColumn: 'Password / Authentication methods',
|
||||
userPasswordColumn: 'Password',
|
||||
userName: 'Username',
|
||||
userPassword: 'Password',
|
||||
createUserButton: 'Create',
|
||||
|
||||
@@ -2964,14 +2964,6 @@ export const deleteUsers = users =>
|
||||
export const editUser = (user, { email, password, permission }) =>
|
||||
_call('user.set', { id: resolveId(user), email, password, permission })::tap(subscribeUsers.forceRefresh)
|
||||
|
||||
export const removeUserAuthProvider = ({ userId, authProviderId }) => {
|
||||
_call('user.removeAuthProvider', { id: userId, authProvider: authProviderId })
|
||||
::tap(subscribeUsers.forceRefresh)
|
||||
.catch(e => {
|
||||
error('user.removeAuthProvider', e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const _signOutFromEverywhereElse = () =>
|
||||
_call('token.delete', {
|
||||
pattern: {
|
||||
|
||||
@@ -546,16 +546,15 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
let VIFs = []
|
||||
const defaultNetworkIds = this._getDefaultNetworkIds(template)
|
||||
forEach(
|
||||
// iterate template VIFs in device order
|
||||
template.VIFs.map(id => getObject(storeState, id, resourceSet)).sort((a, b) => a.device - b.device),
|
||||
|
||||
vif => {
|
||||
VIFs.push({
|
||||
network: pool || isInResourceSet(vif.$network) ? vif.$network : defaultNetworkIds[0],
|
||||
})
|
||||
}
|
||||
)
|
||||
forEach(template.VIFs, vifId => {
|
||||
const vif = getObject(storeState, vifId, resourceSet)
|
||||
VIFs.push({
|
||||
device: vif.device,
|
||||
network: pool || isInResourceSet(vif.$network) ? vif.$network : defaultNetworkIds[0],
|
||||
})
|
||||
})
|
||||
// sort the array so it corresponds to the order chosen by the user when creating the VM
|
||||
VIFs.sort((a, b) => Number(a.device) - Number(b.device))
|
||||
if (VIFs.length === 0) {
|
||||
VIFs = defaultNetworkIds.map(id => ({ network: id }))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import merge from 'lodash/merge'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import pFinally from 'promise-toolbox/finally'
|
||||
import React from 'react'
|
||||
@@ -106,7 +105,7 @@ class Plugin extends Component {
|
||||
_applyPredefinedConfiguration = () => {
|
||||
const configName = this.refs.selectPredefinedConfiguration.value
|
||||
this.setState({
|
||||
editedConfig: merge(undefined, this.state.editedConfig, this.props.configurationPresets[configName]),
|
||||
editedConfig: this.props.configurationPresets[configName],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -168,7 +167,7 @@ class Plugin extends Component {
|
||||
<p>{_('pluginConfigurationChoosePreset')}</p>
|
||||
</span>
|
||||
<div className='input-group'>
|
||||
<select className='form-control' ref='selectPredefinedConfiguration'>
|
||||
<select className='form-control' disabled={!editedConfig} ref='selectPredefinedConfiguration'>
|
||||
{map(configurationPresets, (_, name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
@@ -176,7 +175,7 @@ class Plugin extends Component {
|
||||
))}
|
||||
</select>
|
||||
<span className='input-group-btn'>
|
||||
<Button btnStyle='primary' onClick={this._applyPredefinedConfiguration}>
|
||||
<Button btnStyle='primary' disabled={!editedConfig} onClick={this._applyPredefinedConfiguration}>
|
||||
{_('applyPluginPreset')}
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@@ -6,7 +6,6 @@ import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
@@ -17,16 +16,7 @@ import { get } from '@xen-orchestra/defined'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Password, Select } from 'form'
|
||||
|
||||
import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
deleteUsers,
|
||||
editUser,
|
||||
removeOtp,
|
||||
removeUserAuthProvider,
|
||||
subscribeGroups,
|
||||
subscribeUsers,
|
||||
} from 'xo'
|
||||
import { createUser, deleteUser, deleteUsers, editUser, removeOtp, subscribeGroups, subscribeUsers } from 'xo'
|
||||
|
||||
const permissions = {
|
||||
none: {
|
||||
@@ -86,36 +76,9 @@ const USER_COLUMNS = [
|
||||
sortCriteria: user => user.permission,
|
||||
},
|
||||
{
|
||||
name: _('userAuthColumn'),
|
||||
itemRenderer: user => {
|
||||
const { authProviders } = user
|
||||
return isEmpty(authProviders) ? (
|
||||
<Editable.Password onChange={password => editUser(user, { password })} value='' />
|
||||
) : (
|
||||
<ul className='list-group'>
|
||||
{Object.keys(authProviders)
|
||||
.sort()
|
||||
.map(id => {
|
||||
const shortId = id.split(':')[0]
|
||||
const plugin = 'auth-' + shortId
|
||||
return (
|
||||
<li key={id} className='list-group-item'>
|
||||
<Link to={`/settings/plugins/?s=${encodeURIComponent(`name=^${plugin}$`)}`}>{shortId}</Link>
|
||||
<ActionButton
|
||||
className='pull-right'
|
||||
btnStyle='warning'
|
||||
size='small'
|
||||
icon='remove'
|
||||
handler={removeUserAuthProvider}
|
||||
data-userId={user.id}
|
||||
data-authProviderId={id}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
name: _('userPasswordColumn'),
|
||||
itemRenderer: user =>
|
||||
isEmpty(user.authProviders) && <Editable.Password onChange={password => editUser(user, { password })} value='' />,
|
||||
},
|
||||
{
|
||||
name: 'OTP',
|
||||
|
||||
Reference in New Issue
Block a user