Compare commits

...

1 Commits

Author SHA1 Message Date
Thierry
82196ddd0e feat(lite): vm migration full 2023-10-10 11:08:54 +02:00
20 changed files with 830 additions and 109 deletions

View File

@@ -46,7 +46,7 @@ watchEffect(() => {
color: var(--color-blue-scale-500); color: var(--color-blue-scale-500);
border-radius: 0.5em; border-radius: 0.5em;
background-color: var(--color-blue-scale-100); background-color: var(--color-blue-scale-100);
z-index: 2; z-index: 5;
} }
.triangle { .triangle {

View File

@@ -1,10 +1,10 @@
<template> <template>
<MenuItem <MenuItem
v-tooltip=" v-tooltip="
!areAllVmsMigratable && $t('some-selected-vms-can-not-be-migrated') !areAllVmsAllowedToMigrate && $t('some-selected-vms-can-not-be-migrated')
" "
:busy="isMigrating" :busy="isMigrating"
:disabled="isParentDisabled || !areAllVmsMigratable" :disabled="isParentDisabled || !areAllVmsAllowedToMigrate"
:icon="faRoute" :icon="faRoute"
@click="openModal" @click="openModal"
> >
@@ -12,33 +12,69 @@
</MenuItem> </MenuItem>
<UiModal v-model="isModalOpen"> <UiModal v-model="isModalOpen">
<FormModalLayout :disabled="isMigrating" @submit.prevent="handleMigrate"> <FormModalLayout
:disabled="!isReady || isMigrating"
@submit.prevent="handleMigrate"
>
<template #title> <template #title>
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }} {{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</template> </template>
<div> <div>
<FormInputWrapper :label="$t('select-destination-host')" light> <FormInputWrapper :label="$t('select-destination-host')" light>
<FormSelect v-model="selectedHost"> <FormSelect v-model="selectedHostRef">
<option :value="undefined"> <option :value="undefined">{{ $t("please-select") }}</option>
{{ $t("select-destination-host") }}
</option>
<option <option
v-for="host in availableHosts" v-for="host in availableHosts"
:key="host.$ref" :key="host.$ref"
:value="host" :value="host.$ref"
> >
{{ host.name_label }} {{ host.name_label }}
</option> </option>
</FormSelect> </FormSelect>
</FormInputWrapper> </FormInputWrapper>
<FormInputWrapper
v-if="selectedHostRef !== undefined"
:label="$t('select-optional-migration-network')"
light
>
<FormSelect v-model="selectedMigrationNetworkRef">
<option :value="undefined">{{ $t("please-select") }}</option>
<option
v-for="network in availableNetworks"
:key="network.$ref"
:value="network.$ref"
>
{{ network.name_label }}
</option>
</FormSelect>
</FormInputWrapper>
<FormInputWrapper
v-if="selectedMigrationNetworkRef !== undefined"
:label="$t('select-destination-sr')"
light
>
<FormSelect v-model="selectedSrRef">
<option :value="undefined">{{ $t("please-select") }}</option>
<option v-for="sr in availableSrs" :key="sr.$ref" :value="sr.$ref">
{{ sr.name_label }}
</option>
</FormSelect>
</FormInputWrapper>
</div> </div>
<template #buttons> <template #buttons>
<UiButton outlined @click="closeModal"> <UiButton outlined @click="closeModal" :disabled="false">
{{ isMigrating ? $t("close") : $t("cancel") }} {{ isMigrating ? $t("close") : $t("cancel") }}
</UiButton> </UiButton>
<UiButton :busy="isMigrating" :disabled="!isValid" type="submit"> <UiButton
:busy="isMigrating"
:disabled="!canExecuteMigration"
v-tooltip="notMigratableReason ?? false"
type="submit"
>
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }} {{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</UiButton> </UiButton>
</template> </template>
@@ -60,6 +96,7 @@ import { DisabledContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive"; import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types"; import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { faRoute } from "@fortawesome/free-solid-svg-icons"; import { faRoute } from "@fortawesome/free-solid-svg-icons";
import { useI18n } from "vue-i18n";
const props = defineProps<{ const props = defineProps<{
selectedRefs: XenApiVm["$ref"][]; selectedRefs: XenApiVm["$ref"][];
@@ -67,29 +104,40 @@ const props = defineProps<{
const isParentDisabled = useContext(DisabledContext); const isParentDisabled = useContext(DisabledContext);
const { t } = useI18n();
const { const {
open: openModal, open: openModal,
isOpen: isModalOpen, isOpen: isModalOpen,
close: closeModal, close: closeModal,
} = useModal({ } = useModal({
onClose: () => (selectedHost.value = undefined), confirmClose: () => {
if (!isMigrating.value) {
return true;
}
return confirm(t("migration-close-warning"));
},
onClose: () => (selectedHostRef.value = undefined),
}); });
const { const {
selectedHost, isReady,
selectedHostRef,
selectedMigrationNetworkRef,
selectedSrRef,
availableHosts, availableHosts,
isValid, availableNetworks,
availableSrs,
migrate, migrate,
isMigrating, isMigrating,
areAllVmsMigratable, canExecuteMigration,
notMigratableReason,
areAllVmsAllowedToMigrate,
} = useVmMigration(() => props.selectedRefs); } = useVmMigration(() => props.selectedRefs);
const handleMigrate = async () => { const handleMigrate = async () => {
try { await migrate();
await migrate(); closeModal();
closeModal();
} catch (e) {
console.error("Error while migrating", e);
}
}; };
</script> </script>

View File

@@ -95,13 +95,14 @@
import MenuItem from "@/components/menu/MenuItem.vue"; import MenuItem from "@/components/menu/MenuItem.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue"; import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue"; import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useHostCollection } from "@/stores/xen-api/host.store"; import { VM_OPERATION, VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store"; import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store"; import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { useVmCollection } from "@/stores/xen-api/vm.store"; import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types"; import type { MaybeArray } from "@/types";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import { import {
faCirclePlay, faCirclePlay,
faMoon, faMoon,
@@ -148,7 +149,7 @@ const areVmsPaused = computed(() =>
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.PAUSED) vms.value.every((vm) => vm.power_state === VM_POWER_STATE.PAUSED)
); );
const areOperationsPending = (operation: VM_OPERATION | VM_OPERATION[]) => const areOperationsPending = (operation: MaybeArray<VM_OPERATION>) =>
vms.value.some((vm) => isOperationPending(vm, operation)); vms.value.some((vm) => isOperationPending(vm, operation));
const areVmsBusyToStart = computed(() => const areVmsBusyToStart = computed(() =>

View File

@@ -1,82 +1,329 @@
import { sortRecordsByNameLabel } from "@/libs/utils"; import { areCollectionsReady, sortRecordsByNameLabel } from "@/libs/utils";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums"; import { VBD_TYPE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types"; import type {
XenApiHost,
XenApiNetwork,
XenApiSr,
XenApiVdi,
XenApiVm,
} from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store"; import { useXenApiStore } from "@/stores/xen-api.store";
import { useHostCollection } from "@/stores/xen-api/host.store"; import { useHostCollection } from "@/stores/xen-api/host.store";
import { useNetworkCollection } from "@/stores/xen-api/network.store";
import { usePbdCollection } from "@/stores/xen-api/pbd.store";
import { usePifCollection } from "@/stores/xen-api/pif.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { useSrCollection } from "@/stores/xen-api/sr.store";
import { useVbdCollection } from "@/stores/xen-api/vbd.store";
import { useVdiCollection } from "@/stores/xen-api/vdi.store";
import { useVmCollection } from "@/stores/xen-api/vm.store"; import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { MaybeArray } from "@/types";
import type { VmMigrationData } from "@/types/xen-api";
import { useMemoize } from "@vueuse/core";
import { castArray } from "lodash-es"; import { castArray } from "lodash-es";
import type { MaybeRefOrGetter } from "vue"; import type { MaybeRefOrGetter } from "vue";
import { computed, ref, toValue } from "vue"; import { computed, ref, toValue, watch } from "vue";
import { useI18n } from "vue-i18n";
export const useVmMigration = ( export const useVmMigration = (
vmRefs: MaybeRefOrGetter<XenApiVm["$ref"] | XenApiVm["$ref"][]> vmRefsToMigrate: MaybeRefOrGetter<MaybeArray<XenApiVm["$ref"]>>
) => { ) => {
const $isMigrating = ref(false); const xapi = useXenApiStore().getXapi();
const selectedHost = ref<XenApiHost>();
const { getByOpaqueRef: getVm } = useVmCollection();
const { records: hosts } = useHostCollection();
const vms = computed( const poolCollection = usePoolCollection();
() => const hostCollection = useHostCollection();
castArray(toValue(vmRefs)) const vmCollection = useVmCollection();
.map((vmRef) => getVm(vmRef)) const vbdCollection = useVbdCollection();
.filter((vm) => vm !== undefined) as XenApiVm[] const vdiCollection = useVdiCollection();
const srCollection = useSrCollection();
const networkCollection = useNetworkCollection();
const pbdCollection = usePbdCollection();
const pifCollection = usePifCollection();
const isReady = areCollectionsReady(
poolCollection,
hostCollection,
vmCollection,
vbdCollection,
vdiCollection,
srCollection,
networkCollection,
pbdCollection,
pifCollection
); );
const { pool } = poolCollection;
const { getByOpaqueRef: getHost, records: hosts } = hostCollection;
const {
getByOpaqueRefs: getVms,
isOperationPending,
isOperationAllowed,
} = vmCollection;
const { getByOpaqueRefs: getVbds } = vbdCollection;
const { getByOpaqueRef: getVdi } = vdiCollection;
const { getByOpaqueRef: getSr } = srCollection;
const {
getByOpaqueRef: getNetwork,
getByOpaqueRefs: getNetworks,
getByUuid: getNetworkByUuid,
} = networkCollection;
const { getByOpaqueRefs: getPbds } = pbdCollection;
const { getByOpaqueRefs: getPifs } = pifCollection;
const selectedHostRef = ref<XenApiHost["$ref"]>();
const selectedHost = computed(() => getHost(selectedHostRef.value));
const selectedMigrationNetworkRef = ref<XenApiNetwork["$ref"]>();
const selectedMigrationNetwork = computed(() =>
getNetwork(selectedMigrationNetworkRef.value)
);
const selectedSrRef = ref<XenApiSr["$ref"]>();
const selectedSr = computed(() => getSr(selectedSrRef.value));
const isSimpleMigration = computed(
() => selectedMigrationNetworkRef.value === undefined
);
const availableHosts = computed(() =>
hosts.value
.filter((host) =>
vmsToMigrate.value.some((vm) => vm.resident_on !== host.$ref)
)
.sort(sortRecordsByNameLabel)
);
const getPifsForSelectedHost = () =>
getPifs(selectedHost.value!.PIFs).filter((pif) => pif.IP);
const availableNetworks = computed(() => {
if (!selectedHost.value) {
return [];
}
return getNetworks(getPifsForSelectedHost().map((pif) => pif.network));
});
const availableSrs = computed(() => {
if (!selectedHost.value) {
return [];
}
const srs = new Set<XenApiSr>();
getPbds(selectedHost.value!.PBDs).forEach((pbd) => {
const sr = getSr(pbd.SR);
if (
sr !== undefined &&
sr.content_type !== "iso" &&
sr.physical_size > 0
) {
srs.add(sr);
}
});
return Array.from(srs);
});
const $isMigrating = ref(false);
const vmsToMigrate = computed(() =>
getVms(castArray(toValue(vmRefsToMigrate)))
);
const getVmVbds = (vm: XenApiVm) =>
getVms(vm.snapshots).reduce(
(acc, vm) => acc.concat(getVbds(vm.VBDs)),
getVbds(vm.VBDs)
);
const getVmVdis = (
vmToMigrate: XenApiVm,
destinationHost: XenApiHost,
forcedSr?: XenApiSr
) =>
getVmVbds(vmToMigrate).reduce(
(acc, vbd) => {
if (vbd.type !== VBD_TYPE.DISK) {
return acc;
}
const vdi = getVdi(vbd.VDI);
if (vdi === undefined || vdi.snapshot_of !== "OpaqueRef:NULL") {
return acc;
}
acc[vdi.$ref] = isSrConnected(vdi.SR, destinationHost)
? vdi.SR
: forcedSr !== undefined
? forcedSr.$ref
: getDefaultSr().$ref;
return acc;
},
{} as Record<XenApiVdi["$ref"], XenApiSr["$ref"]>
);
const isSrConnected = useMemoize(
(srRef: XenApiSr["$ref"], destinationHost: XenApiHost) =>
getSr(srRef)?.PBDs.some((pbdRef) =>
destinationHost.PBDs.includes(pbdRef)
) ?? false
);
const getDefaultMigrationNetwork = () => {
if (selectedHost.value === undefined) {
return undefined;
}
const migrationNetworkUuid = pool.value!.other_config[
"xo:migrationNetwork"
] as XenApiNetwork["uuid"];
const migrationNetwork = getNetworkByUuid(migrationNetworkUuid);
if (migrationNetwork === undefined) {
return undefined;
}
if (
getPifsForSelectedHost().some(
(pif) => pif.network === migrationNetwork.$ref
)
) {
return migrationNetwork;
}
return undefined;
};
const getDefaultSr = () => {
const defaultSr = getSr(pool.value?.default_SR);
if (defaultSr === undefined) {
throw new Error(
`This operation requires a default SR to be set on the pool ${
pool.value!.name_label
}`
);
}
return defaultSr;
};
watch(selectedHost, (host) => {
if (host === undefined) {
selectedMigrationNetworkRef.value = undefined;
return;
}
selectedMigrationNetworkRef.value = getDefaultMigrationNetwork()?.$ref;
});
watch(selectedMigrationNetworkRef, (networkRef) => {
if (networkRef === undefined) {
selectedSrRef.value = undefined;
return;
}
selectedSrRef.value = getDefaultSr().$ref;
});
const isMigrating = computed( const isMigrating = computed(
() => () =>
$isMigrating.value || $isMigrating.value ||
vms.value.some((vm) => isOperationPending(vmsToMigrate.value, [
Object.values(vm.current_operations).some( VM_OPERATION.MIGRATE_SEND,
(operation) => operation === VM_OPERATION.POOL_MIGRATE VM_OPERATION.POOL_MIGRATE,
) ])
)
); );
const availableHosts = computed(() => { const areAllVmsAllowedToMigrate = computed(() =>
return hosts.value isOperationAllowed(
.filter((host) => vms.value.some((vm) => vm.resident_on !== host.$ref)) vmsToMigrate.value,
.sort(sortRecordsByNameLabel); isSimpleMigration.value
}); ? VM_OPERATION.POOL_MIGRATE
: VM_OPERATION.MIGRATE_SEND
const areAllVmsMigratable = computed(() =>
vms.value.every((vm) =>
vm.allowed_operations.includes(VM_OPERATION.POOL_MIGRATE)
) )
); );
const isValid = computed( const { t } = useI18n();
() => const notMigratableReason = computed(() => {
!isMigrating.value && if (isMigrating.value) {
vms.value.length > 0 && return t("vms-migration-error.already-being-migrated");
selectedHost.value !== undefined }
if (!areAllVmsAllowedToMigrate.value) {
return t("vms-migration-error.not-allowed");
}
if (selectedHost.value === undefined) {
return t("vms-migration-error.no-destination-host");
}
if (isSimpleMigration.value) {
return undefined;
}
if (selectedMigrationNetwork.value === undefined) {
return t("vms-migration-error.no-migration-network");
}
if (selectedSr.value === undefined) {
return t("vms-migration-error.no-destination-sr");
}
return undefined;
});
const canExecuteMigration = computed(
() => notMigratableReason.value === undefined
); );
const migrateSimple = () =>
xapi.vm.migrate(
vmsToMigrate.value.map((vm) => vm.$ref),
selectedHostRef.value!
);
const migrateComplex = () => {
const vmsMigrationMap: Record<XenApiVm["$ref"], VmMigrationData> = {};
vmsToMigrate.value.forEach((vm) => {
vmsMigrationMap[vm.$ref] = {
destinationHost: selectedHostRef.value!,
destinationSr: selectedSrRef.value!,
migrationNetwork: selectedMigrationNetworkRef.value!,
vdisMap: getVmVdis(vm, selectedHost.value!, selectedSr.value!),
};
});
return xapi.vm.migrateComplex(vmsMigrationMap);
};
const migrate = async () => { const migrate = async () => {
if (!isValid.value) { if (!canExecuteMigration.value) {
return; return;
} }
try { try {
$isMigrating.value = true; $isMigrating.value = true;
const hostRef = selectedHost.value!.$ref; isSimpleMigration.value ? await migrateSimple() : await migrateComplex();
const xapi = useXenApiStore().getXapi();
await xapi.vm.migrate(
vms.value.map((vm) => vm.$ref),
hostRef
);
} finally { } finally {
$isMigrating.value = false; $isMigrating.value = false;
} }
}; };
return { return {
isReady,
isMigrating, isMigrating,
areAllVmsAllowedToMigrate,
canExecuteMigration,
notMigratableReason,
availableHosts, availableHosts,
selectedHost, availableNetworks,
areAllVmsMigratable, availableSrs,
isValid, selectedHostRef,
selectedMigrationNetworkRef,
selectedSrRef,
migrate, migrate,
}; };
}; };

View File

@@ -14,10 +14,20 @@ export const useXenApiStoreBaseContext = <
const lastError = ref<string>(); const lastError = ref<string>();
const hasError = computed(() => lastError.value !== undefined); const hasError = computed(() => lastError.value !== undefined);
const getByOpaqueRef = (opaqueRef: XRecord["$ref"]) => { const getByOpaqueRef = (opaqueRef: XRecord["$ref"] | undefined) => {
if (opaqueRef === undefined) {
return undefined;
}
return recordsByOpaqueRef.get(opaqueRef); return recordsByOpaqueRef.get(opaqueRef);
}; };
const getByOpaqueRefs = (opaqueRefs: XRecord["$ref"][]) => {
return opaqueRefs
.map((opaqueRef) => recordsByOpaqueRef.get(opaqueRef))
.filter((record) => record !== undefined) as XRecord[];
};
const getByUuid = (uuid: XRecord["uuid"]) => { const getByUuid = (uuid: XRecord["uuid"]) => {
return recordsByUuid.get(uuid); return recordsByUuid.get(uuid);
}; };
@@ -49,6 +59,7 @@ export const useXenApiStoreBaseContext = <
lastError, lastError,
records, records,
getByOpaqueRef, getByOpaqueRef,
getByOpaqueRefs,
getByUuid, getByUuid,
hasUuid, hasUuid,
add, add,

View File

@@ -1,9 +1,11 @@
import type { MaybeArray } from "@/types";
import type { Filter } from "@/types/filter"; import type { Filter } from "@/types/filter";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons"; import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons"; import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons";
import { utcParse } from "d3-time-format"; import { utcParse } from "d3-time-format";
import humanFormat from "human-format"; import humanFormat from "human-format";
import { find, forEach, round, size, sum } from "lodash-es"; import { find, forEach, round, size, sum } from "lodash-es";
import { computed, type Ref } from "vue";
export function sortRecordsByNameLabel( export function sortRecordsByNameLabel(
record1: { name_label: string }, record1: { name_label: string },
@@ -140,5 +142,9 @@ export function parseRamUsage(
}; };
} }
export const getFirst = <T>(value: T | T[]): T | undefined => export const getFirst = <T>(value: MaybeArray<T>): T | undefined =>
Array.isArray(value) ? value[0] : value; Array.isArray(value) ? value[0] : value;
export const areCollectionsReady = (
...collections: { isReady: Ref<boolean> }[]
) => computed(() => collections.every(({ isReady }) => isReady.value));

View File

@@ -16,6 +16,13 @@ import type {
XenApiVm, XenApiVm,
} from "@/libs/xen-api/xen-api.types"; } from "@/libs/xen-api/xen-api.types";
import { buildXoObject, typeToRawType } from "@/libs/xen-api/xen-api.utils"; import { buildXoObject, typeToRawType } from "@/libs/xen-api/xen-api.utils";
import type { MaybeArray } from "@/types";
import type {
VmRefsWithMigration,
VmRefsWithNameLabel,
VmRefsWithPowerState,
XenApiMigrationParams,
} from "@/types/xen-api";
import { JSONRPCClient } from "json-rpc-2.0"; import { JSONRPCClient } from "json-rpc-2.0";
import { castArray } from "lodash-es"; import { castArray } from "lodash-es";
@@ -267,8 +274,6 @@ export default class XenApi {
return; return;
} }
await new Promise((resolve) => setTimeout(resolve, 2000));
if (this.listenedTypes.length === 0) { if (this.listenedTypes.length === 0) {
void this.watch(); void this.watch();
return; return;
@@ -277,11 +282,7 @@ export default class XenApi {
const result: { const result: {
token: string; token: string;
events: XenApiEvent<ObjectType, XenApiRecord<any>>[]; events: XenApiEvent<ObjectType, XenApiRecord<any>>[];
} = await this.call("event.from", [ } = await this.call("event.from", [this.listenedTypes, this.fromToken, 60]);
this.listenedTypes,
this.fromToken,
5.001,
]);
this.fromToken = result.token; this.fromToken = result.token;
@@ -291,35 +292,31 @@ export default class XenApi {
} }
get vm() { get vm() {
type VmRefs = XenApiVm["$ref"] | XenApiVm["$ref"][];
type VmRefsWithPowerState = Record<
XenApiVm["$ref"],
XenApiVm["power_state"]
>;
type VmRefsWithNameLabel = Record<XenApiVm["$ref"], string>;
return { return {
delete: (vmRefs: VmRefs) => delete: (vmRefs: MaybeArray<XenApiVm["$ref"]>) =>
Promise.all( Promise.all(
castArray(vmRefs).map((vmRef) => this.call("VM.destroy", [vmRef])) castArray(vmRefs).map((vmRef) => this.call("VM.destroy", [vmRef]))
), ),
start: (vmRefs: VmRefs) => start: (vmRefs: MaybeArray<XenApiVm["$ref"]>) =>
Promise.all( Promise.all(
castArray(vmRefs).map((vmRef) => castArray(vmRefs).map((vmRef) =>
this.call("VM.start", [vmRef, false, false]) this.call("VM.start", [vmRef, false, false])
) )
), ),
startOn: (vmRefs: VmRefs, hostRef: XenApiHost["$ref"]) => startOn: (
vmRefs: MaybeArray<XenApiVm["$ref"]>,
hostRef: XenApiHost["$ref"]
) =>
Promise.all( Promise.all(
castArray(vmRefs).map((vmRef) => castArray(vmRefs).map((vmRef) =>
this.call("VM.start_on", [vmRef, hostRef, false, false]) this.call("VM.start_on", [vmRef, hostRef, false, false])
) )
), ),
pause: (vmRefs: VmRefs) => pause: (vmRefs: MaybeArray<XenApiVm["$ref"]>) =>
Promise.all( Promise.all(
castArray(vmRefs).map((vmRef) => this.call("VM.pause", [vmRef])) castArray(vmRefs).map((vmRef) => this.call("VM.pause", [vmRef]))
), ),
suspend: (vmRefs: VmRefs) => { suspend: (vmRefs: MaybeArray<XenApiVm["$ref"]>) => {
return Promise.all( return Promise.all(
castArray(vmRefs).map((vmRef) => this.call("VM.suspend", [vmRef])) castArray(vmRefs).map((vmRef) => this.call("VM.suspend", [vmRef]))
); );
@@ -337,14 +334,14 @@ export default class XenApi {
}) })
); );
}, },
reboot: (vmRefs: VmRefs, force = false) => { reboot: (vmRefs: MaybeArray<XenApiVm["$ref"]>, force = false) => {
return Promise.all( return Promise.all(
castArray(vmRefs).map((vmRef) => castArray(vmRefs).map((vmRef) =>
this.call(`VM.${force ? "hard" : "clean"}_reboot`, [vmRef]) this.call(`VM.${force ? "hard" : "clean"}_reboot`, [vmRef])
) )
); );
}, },
shutdown: (vmRefs: VmRefs, force = false) => { shutdown: (vmRefs: MaybeArray<XenApiVm["$ref"]>, force = false) => {
return Promise.all( return Promise.all(
castArray(vmRefs).map((vmRef) => castArray(vmRefs).map((vmRef) =>
this.call(`VM.${force ? "hard" : "clean"}_shutdown`, [vmRef]) this.call(`VM.${force ? "hard" : "clean"}_shutdown`, [vmRef])
@@ -360,7 +357,10 @@ export default class XenApi {
) )
); );
}, },
migrate: (vmRefs: VmRefs, destinationHostRef: XenApiHost["$ref"]) => { migrate: (
vmRefs: MaybeArray<XenApiVm["$ref"]>,
destinationHostRef: XenApiHost["$ref"]
) => {
return Promise.all( return Promise.all(
castArray(vmRefs).map((vmRef) => castArray(vmRefs).map((vmRef) =>
this.call("VM.pool_migrate", [ this.call("VM.pool_migrate", [
@@ -371,6 +371,49 @@ export default class XenApi {
) )
); );
}, },
migrateComplex: (vmRefsToMigrate: VmRefsWithMigration) => {
const vmRefs = Object.keys(vmRefsToMigrate) as XenApiVm["$ref"][];
return Promise.all(
vmRefs.map(async (vmRef) => {
const migrateData = vmRefsToMigrate[vmRef];
const params: XenApiMigrationParams = [
vmRef,
await this.call("host.migrate_receive", [
migrateData.destinationHost,
migrateData.migrationNetwork,
{},
]),
true, // Live migration
migrateData.vdisMap,
{}, // vifsMap,
{
force: migrateData.force ? "true" : "false",
},
];
if (!migrateData.bypassAssert) {
await this.call("VM.assert_can_migrate", params);
}
const doMigration = async () => {
try {
await this.call("VM.migrate_send", params);
} catch (error: any) {
if (error?.code === "TOO_MANY_STORAGE_MIGRATES") {
await new Promise((resolve) => setTimeout(resolve, 1000));
await doMigration();
} else {
throw error;
}
}
};
await doMigration();
})
);
},
snapshot: (vmRefsToSnapshot: VmRefsWithNameLabel) => { snapshot: (vmRefsToSnapshot: VmRefsWithNameLabel) => {
const vmRefs = Object.keys(vmRefsToSnapshot) as XenApiVm["$ref"][]; const vmRefs = Object.keys(vmRefsToSnapshot) as XenApiVm["$ref"][];

View File

@@ -1,9 +1,14 @@
import type { import type {
AFTER_APPLY_GUIDANCE,
ALLOCATION_ALGORITHM, ALLOCATION_ALGORITHM,
BOND_MODE, BOND_MODE,
CERTIFICATE_TYPE,
DOMAIN_TYPE, DOMAIN_TYPE,
HOST_ALLOWED_OPERATION,
HOST_DISPLAY,
IP_CONFIGURATION_MODE, IP_CONFIGURATION_MODE,
IPV6_CONFIGURATION_MODE, IPV6_CONFIGURATION_MODE,
LATEST_SYNCED_UPDATES_APPLIED_STATE,
NETWORK_DEFAULT_LOCKING_MODE, NETWORK_DEFAULT_LOCKING_MODE,
NETWORK_OPERATION, NETWORK_OPERATION,
NETWORK_PURPOSE, NETWORK_PURPOSE,
@@ -14,10 +19,15 @@ import type {
PERSISTENCE_BACKEND, PERSISTENCE_BACKEND,
PGPU_DOM0_ACCESS, PGPU_DOM0_ACCESS,
PIF_IGMP_STATUS, PIF_IGMP_STATUS,
POOL_ALLOWED_OPERATION,
PRIMARY_ADDRESS_TYPE, PRIMARY_ADDRESS_TYPE,
SRIOV_CONFIGURATION_MODE, SRIOV_CONFIGURATION_MODE,
STORAGE_OPERATION,
TELEMETRY_FREQUENCY,
TUNNEL_PROTOCOL, TUNNEL_PROTOCOL,
UPDATE_AFTER_APPLY_GUIDANCE,
UPDATE_GUIDANCE, UPDATE_GUIDANCE,
UPDATE_SYNC_FREQUENCY,
VBD_MODE, VBD_MODE,
VBD_OPERATION, VBD_OPERATION,
VBD_TYPE, VBD_TYPE,
@@ -59,6 +69,12 @@ type ObjectTypeToRecordMapping = {
vm: XenApiVm; vm: XenApiVm;
vm_guest_metrics: XenApiVmGuestMetrics; vm_guest_metrics: XenApiVmGuestMetrics;
vm_metrics: XenApiVmMetrics; vm_metrics: XenApiVmMetrics;
vbd: XenApiVbd;
vdi: XenApiVdi;
vif: XenApiVif;
pif: XenApiPif;
network: XenApiNetwork;
pbd: XenApiPbd;
}; };
export type ObjectTypeToRecord<Type extends ObjectType> = export type ObjectTypeToRecord<Type extends ObjectType> =
@@ -96,26 +112,255 @@ export type RawXenApiRecord<T extends XenApiRecord<ObjectType>> = Omit<
>; >;
export interface XenApiPool extends XenApiRecord<"pool"> { export interface XenApiPool extends XenApiRecord<"pool"> {
cpu_info: { allowed_operations: POOL_ALLOWED_OPERATION[];
cpu_count: string; blobs: Record<string, XenApiBlob["$ref"]>;
}; client_certificate_auth_enabled: boolean;
client_certificate_auth_name: string;
coordinator_bias: boolean;
cpu_info: Record<string, string> & { cpu_count: string };
crash_dump_SR: XenApiSr["$ref"];
current_operations: Record<string, POOL_ALLOWED_OPERATION>;
default_SR: XenApiSr["$ref"];
guest_agent_config: Record<string, string>;
gui_config: Record<string, string>;
ha_allow_overcommit: boolean;
ha_cluster_stack: string;
ha_configuration: Record<string, string>;
ha_enabled: boolean;
ha_host_failures_to_tolerate: number;
ha_overcommitted: boolean;
ha_plan_exists_for: number;
ha_statefiles: string[];
health_check_config: Record<string, string>;
igmp_snooping_enabled: boolean;
is_psr_pending: boolean;
last_update_sync: string;
live_patching_disabled: boolean;
master: XenApiHost["$ref"]; master: XenApiHost["$ref"];
metadata_VDIs: XenApiVdi["$ref"][];
migration_compression: boolean;
name_description: string;
name_label: string; name_label: string;
other_config: Record<string, string>;
policy_no_vendor_device: boolean;
redo_log_enabled: boolean;
redo_log_vdi: XenApiVdi["$ref"];
repositories: XenApiRepository["$ref"][];
repository_proxy_password: XenApiSecret["$ref"];
repository_proxy_url: string;
repository_proxy_username: string;
restrictions: Record<string, string>;
suspend_image_SR: XenApiSr["$ref"];
tags: string[];
telemetry_frequency: TELEMETRY_FREQUENCY;
telemetry_next_collection: string;
telemetry_uuid: XenApiSecret["$ref"];
tls_verification_enabled: boolean;
uefi_certificates: string;
update_sync_day: number;
update_sync_enabled: boolean;
update_sync_frequency: UPDATE_SYNC_FREQUENCY;
vswitch_controller: string;
wlb_enabled: boolean;
wlb_url: string;
wlb_username: string;
wlb_verify_cert: boolean;
}
export interface XenApiSecret extends XenApiRecord<"secret"> {
other_config: Record<string, string>;
value: string;
}
export interface XenApiRepository extends XenApiRecord<"repository"> {
binary_url: string;
gpgkey_path: string;
hash: string;
name_description: string;
name_label: string;
source_url: string;
up_to_date: boolean;
update: boolean;
} }
export interface XenApiHost extends XenApiRecord<"host"> { export interface XenApiHost extends XenApiRecord<"host"> {
API_version_major: number;
API_version_minor: number;
API_version_vendor: string;
API_version_vendor_implementation: Record<string, string>;
PBDs: XenApiPbd["$ref"][];
PCIs: XenApiPci["$ref"][];
PGPUs: XenApiPgpu["$ref"][];
PIFs: XenApiPif["$ref"][];
PUSBs: XenApiPusb["$ref"][];
address: string; address: string;
name_label: string; allowed_operations: HOST_ALLOWED_OPERATION[];
bios_strings: Record<string, string>;
blobs: Record<string, XenApiBlob["$ref"]>;
capabilities: string[];
certificates: XenApiCertificate["$ref"][];
chipset_info: Record<string, string>;
control_domain: XenApiVm["$ref"];
cpu_configuration: Record<string, string>;
cpu_info: Record<string, string> & { cpu_count: string };
crash_dump_sr: XenApiSr["$ref"];
crashdumps: XenApiHostCrashdump["$ref"][];
current_operations: Record<string, HOST_ALLOWED_OPERATION>;
display: HOST_DISPLAY;
edition: string;
editions: string[];
enabled: boolean;
external_auth_configuration: Record<string, string>;
external_auth_service_name: string;
external_auth_type: string;
features: XenApiFeature["$ref"][];
guest_VCPUs_params: Record<string, string>;
ha_network_peers: string[];
ha_statefiles: string[];
host_CPUs: XenApiHostCpu["$ref"][];
hostname: string;
https_only: boolean;
iscsi_iqn: string;
last_software_update: string;
latest_synced_updates_applied: LATEST_SYNCED_UPDATES_APPLIED_STATE;
license_params: Record<string, string>;
license_server: Record<string, string>;
local_cache_sr: XenApiSr["$ref"];
logging: Record<string, string>;
memory_overhead: number;
metrics: XenApiHostMetrics["$ref"]; metrics: XenApiHostMetrics["$ref"];
multipathing: boolean;
name_description: string;
name_label: string;
other_config: Record<string, string>;
patches: XenApiHostPatch["$ref"][];
pending_guidances: UPDATE_GUIDANCE[];
power_on_config: Record<string, string>;
power_on_mode: string;
resident_VMs: XenApiVm["$ref"][]; resident_VMs: XenApiVm["$ref"][];
cpu_info: { cpu_count: string }; sched_policy: string;
software_version: { product_version: string }; software_version: Record<string, string> & { product_version: string };
ssl_legacy: boolean;
supported_bootloaders: string[];
suspend_image_sr: XenApiSr["$ref"];
tags: string[];
tls_verification_enabled: boolean;
uefi_certificates: string;
updates: XenApiPoolUpdate["$ref"][];
updates_requiring_reboot: XenApiPoolUpdate["$ref"][];
virtual_hardware_platform_versions: number[];
}
export interface XenApiCertificate extends XenApiRecord<"certificate"> {
fingerprint: string;
host: XenApiHost["$ref"];
name: string;
not_after: string;
not_before: string;
type: CERTIFICATE_TYPE;
}
export interface XenApiHostCrashdump extends XenApiRecord<"host_crashdump"> {
host: XenApiHost["$ref"];
other_config: Record<string, string>;
size: number;
timestamp: string;
}
export interface XenApiFeature extends XenApiRecord<"feature"> {
enabled: boolean;
experimental: boolean;
host: XenApiHost["$ref"];
name_description: string;
name_label: string;
version: string;
}
export interface XenApiHostCpu extends XenApiRecord<"host_cpu"> {
family: number;
features: string;
flags: string;
host: XenApiHost["$ref"];
model: number;
modelname: string;
number: number;
other_config: Record<string, string>;
speed: number;
stepping: string;
utilisation: number;
vendor: string;
}
export interface XenApiPbd extends XenApiRecord<"pbd"> {
SR: XenApiSr["$ref"];
currently_attached: boolean;
device_config: Record<string, string>;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
}
export interface XenApiPoolUpdate extends XenApiRecord<"pool_update"> {
after_apply_guidance: UPDATE_AFTER_APPLY_GUIDANCE[];
enforce_homogeneity: boolean;
hosts: XenApiHost["$ref"][];
installation_size: number;
key: string;
name_description: string;
name_label: string;
other_config: Record<string, string>;
vdi: XenApiVdi["$ref"];
version: string;
}
export interface XenApiHostPatch extends XenApiRecord<"host_patch"> {
applied: boolean;
host: XenApiHost["$ref"];
name_description: string;
name_label: string;
other_config: Record<string, string>;
pool_patch: XenApiPoolPatch["$ref"];
size: number;
timestamp_applied: string;
version: string;
}
export interface XenApiPoolPatch extends XenApiRecord<"pool_patch"> {
after_apply_guidance: AFTER_APPLY_GUIDANCE[];
host_patches: XenApiHostPatch["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
pool_applied: boolean;
pool_update: XenApiPoolUpdate["$ref"];
size: number;
version: string;
} }
export interface XenApiSr extends XenApiRecord<"sr"> { export interface XenApiSr extends XenApiRecord<"sr"> {
PBDs: XenApiPbd["$ref"][];
VDIs: XenApiVdi["$ref"][];
allowed_operations: STORAGE_OPERATION[];
blobs: Record<string, XenApiBlob["$ref"]>;
clustered: boolean;
content_type: string;
current_operations: Record<string, STORAGE_OPERATION>;
introduced_by: XenApiDrTask["$ref"];
is_tools_sr: boolean;
local_cache_enabled: boolean;
name_description: string;
name_label: string; name_label: string;
other_config: Record<string, string>;
physical_size: number; physical_size: number;
physical_utilisation: number; physical_utilisation: number;
shared: boolean;
sm_config: Record<string, string>;
tags: string[];
type: string;
virtual_allocation: number;
}
export interface XenApiDrTask extends XenApiRecord<"dr_task"> {
introduced_SRs: XenApiSr["$ref"][];
} }
export interface XenApiVm extends XenApiRecord<"vm"> { export interface XenApiVm extends XenApiRecord<"vm"> {

View File

@@ -25,6 +25,7 @@ export const XEN_API_OBJECT_TYPES = {
pvs_proxy: "PVS_proxy", pvs_proxy: "PVS_proxy",
pvs_server: "PVS_server", pvs_server: "PVS_server",
pvs_site: "PVS_site", pvs_site: "PVS_site",
repository: "repository",
sdn_controller: "SDN_controller", sdn_controller: "SDN_controller",
sm: "SM", sm: "SM",
sr: "SR", sr: "SR",

View File

@@ -87,6 +87,7 @@
"login": "Login", "login": "Login",
"migrate": "Migrate", "migrate": "Migrate",
"migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs", "migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
"migration-close-warning": "Warning: If you close this window, failed migration attempts will not be retried.",
"n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch", "n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch",
"n-missing": "{n} missing", "n-missing": "{n} missing",
"n-vms": "1 VM | {n} VMs", "n-vms": "1 VM | {n} VMs",
@@ -112,6 +113,7 @@
"patches": "Patches", "patches": "Patches",
"pause": "Pause", "pause": "Pause",
"please-confirm": "Please confirm", "please-confirm": "Please confirm",
"please-select": "Please select",
"pool-cpu-usage": "Pool CPU Usage", "pool-cpu-usage": "Pool CPU Usage",
"pool-ram-usage": "Pool RAM Usage", "pool-ram-usage": "Pool RAM Usage",
"power-on-for-console": "Power on your VM to access its console", "power-on-for-console": "Power on your VM to access its console",
@@ -134,6 +136,8 @@
"resume": "Resume", "resume": "Resume",
"save": "Save", "save": "Save",
"select-destination-host": "Select a destination host", "select-destination-host": "Select a destination host",
"select-destination-sr": "Select a destination SR",
"select-optional-migration-network": "Select a migration network (optional)",
"selected-vms-in-execution": "Some selected VMs are running", "selected-vms-in-execution": "Some selected VMs are running",
"send-us-feedback": "Send us feedback", "send-us-feedback": "Send us feedback",
"settings": "Settings", "settings": "Settings",
@@ -173,5 +177,12 @@
"vcpus-used": "vCPUs used", "vcpus-used": "vCPUs used",
"version": "Version", "version": "Version",
"vms": "VMs", "vms": "VMs",
"vms-migration-error": {
"already-being-migrated": "At least one selected VM is already being migrated",
"not-allowed": "Some VMs are not allowed to be migrated",
"no-destination-host": "No destination host has been selected",
"no-migration-network": "No migration network has been selected",
"no-destination-sr": "No destination SR has been selected"
},
"xo-lite-under-construction": "XOLite is under construction" "xo-lite-under-construction": "XOLite is under construction"
} }

View File

@@ -87,6 +87,7 @@
"login": "Connexion", "login": "Connexion",
"migrate": "Migrer", "migrate": "Migrer",
"migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs", "migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
"migration-close-warning": "Attention : Si vous fermez cette fenêtre, les tentatives de migration échouées ne seront pas réessayées.",
"n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch", "n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch",
"n-missing": "{n} manquant | {n} manquants", "n-missing": "{n} manquant | {n} manquants",
"n-vms": "1 VM | {n} VMs", "n-vms": "1 VM | {n} VMs",
@@ -112,6 +113,7 @@
"patches": "Patches", "patches": "Patches",
"pause": "Pause", "pause": "Pause",
"please-confirm": "Veuillez confirmer", "please-confirm": "Veuillez confirmer",
"please-select": "Veuillez sélectionner",
"pool-cpu-usage": "Utilisation CPU du Pool", "pool-cpu-usage": "Utilisation CPU du Pool",
"pool-ram-usage": "Utilisation RAM du Pool", "pool-ram-usage": "Utilisation RAM du Pool",
"power-on-for-console": "Allumez votre VM pour accéder à sa console", "power-on-for-console": "Allumez votre VM pour accéder à sa console",
@@ -173,5 +175,12 @@
"vcpus-used": "vCPUs utilisés", "vcpus-used": "vCPUs utilisés",
"version": "Version", "version": "Version",
"vms": "VMs", "vms": "VMs",
"vms-migration-error": {
"already-being-migrated": "Au moins une VM sélectionnée est déjà en cours de migration",
"not-allowed": "Certaines VM ne sont pas autorisées à être migrées",
"no-destination-host": "Aucun hôte de destination n'a été sélectionné",
"no-migration-network": "Aucun réseau de migration n'a été sélectionné",
"no-destination-sr": "Aucun SR de destination n'a été sélectionné"
},
"xo-lite-under-construction": "XOLite est en construction" "xo-lite-under-construction": "XOLite est en construction"
} }

View File

@@ -0,0 +1,9 @@
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { defineStore } from "pinia";
export const useNetworkStore = defineStore("xen-api-network", () => {
return useXenApiStoreSubscribableContext("network");
});
export const useNetworkCollection = createUseCollection(useNetworkStore);

View File

@@ -0,0 +1,9 @@
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { defineStore } from "pinia";
export const usePbdStore = defineStore("xen-api-pbd", () => {
return useXenApiStoreSubscribableContext("pbd");
});
export const usePbdCollection = createUseCollection(usePbdStore);

View File

@@ -0,0 +1,9 @@
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { defineStore } from "pinia";
export const usePifStore = defineStore("xen-api-pif", () => {
return useXenApiStoreSubscribableContext("pif");
});
export const usePifCollection = createUseCollection(usePifStore);

View File

@@ -0,0 +1,9 @@
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { defineStore } from "pinia";
export const useVbdStore = defineStore("xen-api-vbd", () => {
return useXenApiStoreSubscribableContext("vbd");
});
export const useVbdCollection = createUseCollection(useVbdStore);

View File

@@ -0,0 +1,9 @@
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { defineStore } from "pinia";
export const useVdiStore = defineStore("xen-api-vdi", () => {
return useXenApiStoreSubscribableContext("vdi");
});
export const useVdiCollection = createUseCollection(useVdiStore);

View File

@@ -2,14 +2,15 @@ import type { GetStats } from "@/composables/fetch-stats.composable";
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable"; import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { sortRecordsByNameLabel } from "@/libs/utils"; import { sortRecordsByNameLabel } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats"; import type { VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { import {
type VM_OPERATION, type VM_OPERATION,
VM_POWER_STATE, VM_POWER_STATE,
} from "@/libs/xen-api/xen-api.enums"; } from "@/libs/xen-api/xen-api.enums";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store"; import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection"; import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostStore } from "@/stores/xen-api/host.store"; import { useHostStore } from "@/stores/xen-api/host.store";
import type { MaybeArray } from "@/types";
import { castArray } from "lodash-es"; import { castArray } from "lodash-es";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed } from "vue"; import { computed } from "vue";
@@ -25,17 +26,30 @@ export const useVmStore = defineStore("xen-api-vm", () => {
.sort(sortRecordsByNameLabel) .sort(sortRecordsByNameLabel)
); );
const isOperationPending = ( const hasOperation = (
vm: XenApiVm, vms: MaybeArray<XenApiVm>,
operations: VM_OPERATION[] | VM_OPERATION operations: MaybeArray<VM_OPERATION>,
operationType: "current_operations" | "allowed_operations"
) => { ) => {
const currentOperations = Object.values(vm.current_operations); return castArray(vms).some((vm) => {
const currentOperations = Object.values(vm[operationType]);
return castArray(operations).some((operation) => return castArray(operations).some((operation) =>
currentOperations.includes(operation) currentOperations.includes(operation)
); );
});
}; };
const isOperationPending = (
vms: XenApiVm | XenApiVm[],
operations: MaybeArray<VM_OPERATION>
) => hasOperation(vms, operations, "current_operations");
const isOperationAllowed = (
vms: MaybeArray<XenApiVm>,
operations: MaybeArray<VM_OPERATION>
) => hasOperation(vms, operations, "allowed_operations");
const runningVms = computed(() => const runningVms = computed(() =>
records.value.filter((vm) => vm.power_state === VM_POWER_STATE.RUNNING) records.value.filter((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
); );
@@ -92,6 +106,7 @@ export const useVmStore = defineStore("xen-api-vm", () => {
...context, ...context,
records, records,
isOperationPending, isOperationPending,
isOperationAllowed,
runningVms, runningVms,
recordsByHostRef, recordsByHostRef,
getStats, getStats,

View File

@@ -1 +1,3 @@
export type Color = "info" | "error" | "warning" | "success"; export type Color = "info" | "error" | "warning" | "success";
export type MaybeArray<T> = T | T[];

View File

@@ -1,6 +1,11 @@
import type { import type {
RawObjectType, RawObjectType,
XenApiHost,
XenApiMessage, XenApiMessage,
XenApiNetwork,
XenApiSr,
XenApiVdi,
XenApiVm,
} from "@/libs/xen-api/xen-api.types"; } from "@/libs/xen-api/xen-api.types";
export type XenApiAlarmType = export type XenApiAlarmType =
@@ -37,3 +42,32 @@ export type XenApiPatch = {
author: string; author: string;
}; };
}; };
export type XenApiMigrationToken = Record<string, string>;
export type XenApiMigrationParams = [
XenApiVm["$ref"],
XenApiMigrationToken,
boolean,
Record<XenApiVdi["$ref"], XenApiSr["$ref"]>,
Record<any, never>,
{ force: "true" | "false" },
];
export type VmRefsWithPowerState = Record<
XenApiVm["$ref"],
XenApiVm["power_state"]
>;
export type VmRefsWithNameLabel = Record<XenApiVm["$ref"], string>;
export type VmMigrationData = {
destinationHost: XenApiHost["$ref"];
migrationNetwork: XenApiNetwork["$ref"];
destinationSr: XenApiSr["$ref"];
vdisMap: Record<XenApiVdi["$ref"], XenApiSr["$ref"]>;
force?: boolean;
bypassAssert?: boolean;
};
export type VmRefsWithMigration = Record<XenApiVm["$ref"], VmMigrationData>;

View File

@@ -81,7 +81,10 @@ const selectedVmsRefs = ref([]);
titleStore.setCount(() => selectedVmsRefs.value.length); titleStore.setCount(() => selectedVmsRefs.value.length);
const isMigrating = (vm: XenApiVm) => const isMigrating = (vm: XenApiVm) =>
isOperationPending(vm, VM_OPERATION.POOL_MIGRATE); isOperationPending(vm, [
VM_OPERATION.POOL_MIGRATE,
VM_OPERATION.MIGRATE_SEND,
]);
</script> </script>
<style lang="postcss" scoped> <style lang="postcss" scoped>