mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 21:26:08 +08:00
feat: expose KubeVirt high-performance storage features in VM and Volume UI (#1133)
* feat: expose KubeVirt high-performance storage features in VM and Volume UI Adds UI to configure KubeVirt high-performance disk/storage settings that were previously only reachable via VMBuilder/Terraform: - Per-volume "Storage Performance Options": a performance profile (Default / High Performance / Custom) exposing per-disk cache, io and dedicatedIOThread on all disk types (VM image/root, new, existing, container) and on the standalone Volume form. - VM-wide "High Performance (I/O Threads and Multi-Queue)": blockMultiQueue and ioThreadsPolicy (+ supplemental-pool thread count), shown only in the VM section since these are domain-level and cannot be set per-PVC. - Compatibility guardrails: io=native forces cache=none; cache=none on Filesystem volumeMode warns; blockMultiQueue requires a virtio disk. Ref: harvester/harvester#11550 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alejandro Bonilla <abonilla@suse.com> * fix: address review feedback on high-performance storage UI Resolves the Copilot review comments on #1133: - Gate the whole feature behind a new `highPerformanceStorage` release feature flag (v1.9.0) so older supported clusters never render the controls or receive unsupported KubeVirt fields. - Force Cache Mode to "none" whenever Native I/O is selected, including from the "Default" ('') cache value, and disable every non-"none" cache option (Default included) while Native is active. Previously the empty cache value slipped past the guardrail and produced an invalid combo. - Clear an already-enabled blockMultiQueue when the last virtio disk is removed, so a disabled checkbox can no longer persist an invalid setting. - Strip stale cache/io/dedicatedIOThread fields from the merged disk spec when a regenerated disk no longer requests them, so switching a disk back to "Default" (or unchecking Dedicated I/O Thread) while editing no longer silently preserves the previous values. - Expose the disclosure toggle's state to assistive tech via aria-expanded and an expand/collapse aria-label. - Add unit tests for the profile transitions and the Native/cache guardrails. Ref: harvester/harvester#11550 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alejandro Bonilla <abonilla@suse.com> * fix: address second-round review on high-performance storage UI Resolves the two Copilot findings on #1133 and votdev's UX feedback on harvester/harvester#11550: - Restore the disk's original bus when the performance profile goes back to "Default". The "High Performance" preset switches the bus to virtio, but leaving the preset only cleared cache/io/dedicatedIOThread, so a SATA/SCSI disk was silently left on virtio — a bus change that can make an existing guest unbootable. The pre-preset bus is now remembered and put back, unless the user picked a different bus themselves in the meantime. - Pass the VM-wide performance values from the VM detail page to the shared Volume component. The mixin already parsed them via getInitConfig(), but the detail caller never forwarded them, so VMPerformanceOptions fell back to its prop defaults and hid itself in view mode — configured blockMultiQueue / ioThreadsPolicy settings were invisible. They are also refreshed in the value watcher so the panel does not go stale. - Make the VM-wide "High Performance" panel expandable/collapsible, matching the per-volume "Storage Performance Options" disclosure. It stays collapsed unless the VM already has something configured, so the average user is not faced with specialist tuning controls by default. - Fix the unit tests to use Vue Test Utils v2 mount options. They used the v1 top-level `mocks` key, which VTU 2.x ignores, so every case failed on mount. The repo has no jest config or test script, so this was not caught. Add coverage for the bus save/restore behaviour. Ref: harvester/harvester#11550 Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Alejandro Bonilla <abonilla@suse.com> --------- Signed-off-by: Alejandro Bonilla <abonilla@suse.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
3c92a06268
commit
528255dea4
@@ -76,6 +76,7 @@ const FEATURE_FLAGS = {
|
|||||||
'longhornV2HugepageSettings',
|
'longhornV2HugepageSettings',
|
||||||
'staticIPForVM',
|
'staticIPForVM',
|
||||||
'fsFreezeDeadline',
|
'fsFreezeDeadline',
|
||||||
|
'highPerformanceStorage',
|
||||||
],
|
],
|
||||||
'v1.9.1': [
|
'v1.9.1': [
|
||||||
'lvmDmThinOnly',
|
'lvmDmThinOnly',
|
||||||
|
|||||||
@@ -39,6 +39,21 @@ export const VOLUME_TYPE = [{
|
|||||||
value: 'cd-rom'
|
value: 'cd-rom'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// KubeVirt high-performance disk features
|
||||||
|
// https://kubevirt.io/user-guide/storage/disks_and_volumes/#high-performance-features
|
||||||
|
export const DISK_CACHE_MODE = ['', 'none', 'writeback', 'writethrough'];
|
||||||
|
|
||||||
|
export const DISK_IO_MODE = ['', 'native', 'threads'];
|
||||||
|
|
||||||
|
export const IO_THREADS_POLICY = ['', 'shared', 'auto', 'supplementalPool'];
|
||||||
|
|
||||||
|
// "Easy-to-consume" presets layered on top of the raw KubeVirt fields.
|
||||||
|
export const DISK_PERFORMANCE_PROFILE = {
|
||||||
|
DEFAULT: 'default',
|
||||||
|
HIGH: 'highPerformance',
|
||||||
|
CUSTOM: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
export const VOLUME_HOTPLUG_ACTION = {
|
export const VOLUME_HOTPLUG_ACTION = {
|
||||||
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
||||||
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export const HCI = {
|
|||||||
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
|
// Default KubeVirt high-performance disk settings, applied when the volume is
|
||||||
|
// attached to a VM as a disk. See disks_and_volumes.md#high-performance-features.
|
||||||
|
DISK_CACHE_MODE: 'harvesterhci.io/disk-cache-mode',
|
||||||
|
DISK_IO_MODE: 'harvesterhci.io/disk-io-mode',
|
||||||
|
DISK_DEDICATED_IOTHREAD: 'harvesterhci.io/disk-dedicated-iothread',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
||||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||||
|
|||||||
@@ -214,6 +214,14 @@ export default {
|
|||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||||
|
|
||||||
|
// getInitConfig() seeds these on created(); refresh them here too so the
|
||||||
|
// VM-wide performance panel does not go stale when the spec changes.
|
||||||
|
const domain = neu?.spec?.template?.spec?.domain;
|
||||||
|
|
||||||
|
this['blockMultiQueue'] = domain?.devices?.blockMultiQueue || false;
|
||||||
|
this['ioThreadsPolicy'] = domain?.ioThreadsPolicy || '';
|
||||||
|
this['ioThreadCount'] = domain?.ioThreads?.supplementalPoolThreadCount || 2;
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
@@ -254,6 +262,9 @@ export default {
|
|||||||
:namespace="value.metadata.namespace"
|
:namespace="value.metadata.namespace"
|
||||||
:vm="value"
|
:vm="value"
|
||||||
:resource-type="value.type"
|
:resource-type="value.type"
|
||||||
|
:block-multi-queue="blockMultiQueue"
|
||||||
|
:io-threads-policy="ioThreadsPolicy"
|
||||||
|
:io-thread-count="ioThreadCount"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -308,6 +308,9 @@ export default {
|
|||||||
>
|
>
|
||||||
<Volume
|
<Volume
|
||||||
v-model:value="diskRows"
|
v-model:value="diskRows"
|
||||||
|
v-model:block-multi-queue="blockMultiQueue"
|
||||||
|
v-model:io-threads-policy="ioThreadsPolicy"
|
||||||
|
v-model:io-thread-count="ioThreadCount"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:namespace="value.metadata.namespace"
|
:namespace="value.metadata.namespace"
|
||||||
:existing-volume-disabled="true"
|
:existing-volume-disabled="true"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import Conditions from '@shell/components/form/Conditions';
|
import Conditions from '@shell/components/form/Conditions';
|
||||||
|
import DiskPerformanceOptions from './kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import jsyaml from 'js-yaml';
|
import jsyaml from 'js-yaml';
|
||||||
@@ -47,7 +48,8 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
Conditions
|
Conditions,
|
||||||
|
DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -100,7 +102,15 @@ export default {
|
|||||||
createWithDataVolume: false,
|
createWithDataVolume: false,
|
||||||
snapshots: [],
|
snapshots: [],
|
||||||
images: [],
|
images: [],
|
||||||
GIBIBYTE
|
GIBIBYTE,
|
||||||
|
// Default KubeVirt high-performance disk profile, stored as annotations and
|
||||||
|
// applied when this volume is later attached to a VM as a disk.
|
||||||
|
perf: {
|
||||||
|
cache: get(this.value, `metadata.annotations."${ HCI_ANNOTATIONS.DISK_CACHE_MODE }"`) || '',
|
||||||
|
io: get(this.value, `metadata.annotations."${ HCI_ANNOTATIONS.DISK_IO_MODE }"`) || '',
|
||||||
|
dedicatedIOThread: get(this.value, `metadata.annotations."${ HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD }"`) === 'true',
|
||||||
|
volumeMode: this.value?.spec?.volumeMode,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -332,6 +342,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'value.spec.volumeMode'(neu) {
|
'value.spec.volumeMode'(neu) {
|
||||||
|
this.perf.volumeMode = neu;
|
||||||
if (neu === VOLUME_MODE.FILE_SYSTEM) {
|
if (neu === VOLUME_MODE.FILE_SYSTEM) {
|
||||||
this.setVolumeForVmAnnotation();
|
this.setVolumeForVmAnnotation();
|
||||||
} else if (neu === VOLUME_MODE.BLOCK ) {
|
} else if (neu === VOLUME_MODE.BLOCK ) {
|
||||||
@@ -446,6 +457,23 @@ export default {
|
|||||||
imageAnnotations = { ...this.value.metadata.annotations };
|
imageAnnotations = { ...this.value.metadata.annotations };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the default high-performance disk profile as annotations.
|
||||||
|
if (this.perf.cache) {
|
||||||
|
imageAnnotations[HCI_ANNOTATIONS.DISK_CACHE_MODE] = this.perf.cache;
|
||||||
|
} else {
|
||||||
|
delete imageAnnotations[HCI_ANNOTATIONS.DISK_CACHE_MODE];
|
||||||
|
}
|
||||||
|
if (this.perf.io) {
|
||||||
|
imageAnnotations[HCI_ANNOTATIONS.DISK_IO_MODE] = this.perf.io;
|
||||||
|
} else {
|
||||||
|
delete imageAnnotations[HCI_ANNOTATIONS.DISK_IO_MODE];
|
||||||
|
}
|
||||||
|
if (this.perf.dedicatedIOThread) {
|
||||||
|
imageAnnotations[HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD] = 'true';
|
||||||
|
} else {
|
||||||
|
delete imageAnnotations[HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD];
|
||||||
|
}
|
||||||
|
|
||||||
const spec = {
|
const spec = {
|
||||||
...this.value.spec,
|
...this.value.spec,
|
||||||
resources: { requests: { storage: this.storage } },
|
resources: { requests: { storage: this.storage } },
|
||||||
@@ -610,6 +638,16 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.volume.performance.pvcTip')"
|
||||||
|
/>
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="perf"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="!isCreate"
|
v-if="!isCreate"
|
||||||
|
|||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { DISK_CACHE_MODE, DISK_IO_MODE, DISK_PERFORMANCE_PROFILE } from '../../../config/harvester-map';
|
||||||
|
|
||||||
|
const { DEFAULT, HIGH, CUSTOM } = DISK_PERFORMANCE_PROFILE;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DiskPerformanceOptions',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledSelect, Checkbox, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
// The disk row. This component reads/writes value.cache, value.io,
|
||||||
|
// value.dedicatedIOThread and (for the "High Performance" preset) value.bus.
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const hasPerf = !!(this.value.cache || this.value.io || this.value.dedicatedIOThread);
|
||||||
|
|
||||||
|
return {
|
||||||
|
DISK_PERFORMANCE_PROFILE,
|
||||||
|
expanded: hasPerf,
|
||||||
|
profile: this.detectProfile(),
|
||||||
|
// Bus the disk used before the "High Performance" preset switched it to
|
||||||
|
// virtio, so returning to "Default" can put it back.
|
||||||
|
busBeforeHighProfile: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
storagePerformanceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('highPerformanceStorage');
|
||||||
|
},
|
||||||
|
|
||||||
|
isCdRom() {
|
||||||
|
return this.value.type === 'cd-rom';
|
||||||
|
},
|
||||||
|
|
||||||
|
isCustom() {
|
||||||
|
return this.profile === CUSTOM;
|
||||||
|
},
|
||||||
|
|
||||||
|
isNativeIo() {
|
||||||
|
return this.value.io === 'native';
|
||||||
|
},
|
||||||
|
|
||||||
|
profileOptions() {
|
||||||
|
return [DEFAULT, HIGH, CUSTOM].map((value) => ({
|
||||||
|
label: this.t(`harvester.virtualMachine.volume.performance.profile.${ value }`),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheOptions() {
|
||||||
|
return DISK_CACHE_MODE.map((value) => ({
|
||||||
|
label: this.cacheLabel(value),
|
||||||
|
value,
|
||||||
|
// Native AIO only works with an uncached (O_DIRECT) disk, so while it is
|
||||||
|
// selected the only valid cache option is "none" — disable everything
|
||||||
|
// else, including "Default", to prevent an invalid combination.
|
||||||
|
disabled: this.isNativeIo && value !== 'none',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
ioOptions() {
|
||||||
|
return DISK_IO_MODE.map((value) => ({
|
||||||
|
label: value === '' ? this.t('harvester.virtualMachine.volume.performance.ioMode.default') : this.t(`harvester.virtualMachine.volume.performance.ioMode.${ value }`),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
showFilesystemCacheWarning() {
|
||||||
|
return this.value.cache === 'none' && this.value.volumeMode === 'Filesystem';
|
||||||
|
},
|
||||||
|
|
||||||
|
showBusTip() {
|
||||||
|
const hasPerf = !!(this.value.cache || this.value.io || this.value.dedicatedIOThread);
|
||||||
|
|
||||||
|
return hasPerf && this.value.bus && this.value.bus !== 'virtio';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// Keep the profile selector in sync if the row is repopulated (e.g. editing an existing VM).
|
||||||
|
'value.cache'() {
|
||||||
|
this.profile = this.detectProfile();
|
||||||
|
},
|
||||||
|
'value.io'() {
|
||||||
|
this.profile = this.detectProfile();
|
||||||
|
},
|
||||||
|
'value.dedicatedIOThread'() {
|
||||||
|
this.profile = this.detectProfile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
detectProfile() {
|
||||||
|
const { cache, io, dedicatedIOThread } = this.value;
|
||||||
|
|
||||||
|
if (!cache && !io && !dedicatedIOThread) {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache === 'none' && io === 'native' && dedicatedIOThread) {
|
||||||
|
return HIGH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CUSTOM;
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheLabel(value) {
|
||||||
|
if (value === '') {
|
||||||
|
return this.t('harvester.virtualMachine.volume.performance.cacheMode.default');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.t(`harvester.virtualMachine.volume.performance.cacheMode.${ value }`);
|
||||||
|
},
|
||||||
|
|
||||||
|
onProfileChange(profile) {
|
||||||
|
const previous = this.profile;
|
||||||
|
|
||||||
|
this.profile = profile;
|
||||||
|
|
||||||
|
if (profile === DEFAULT) {
|
||||||
|
this.value.cache = '';
|
||||||
|
this.value.io = '';
|
||||||
|
this.value.dedicatedIOThread = false;
|
||||||
|
|
||||||
|
// "Default" means no overrides, so also undo the virtio bus that the
|
||||||
|
// "High Performance" preset applied. Skip it if the user has since
|
||||||
|
// picked a different bus themselves — that choice is theirs to keep.
|
||||||
|
if (this.busBeforeHighProfile !== null) {
|
||||||
|
if (this.value.bus === 'virtio') {
|
||||||
|
this.value.bus = this.busBeforeHighProfile;
|
||||||
|
}
|
||||||
|
this.busBeforeHighProfile = null;
|
||||||
|
}
|
||||||
|
} else if (profile === HIGH) {
|
||||||
|
this.value.cache = 'none';
|
||||||
|
this.value.io = 'native';
|
||||||
|
this.value.dedicatedIOThread = true;
|
||||||
|
|
||||||
|
if (previous !== HIGH) {
|
||||||
|
this.busBeforeHighProfile = this.value.bus || '';
|
||||||
|
}
|
||||||
|
this.value.bus = 'virtio';
|
||||||
|
}
|
||||||
|
// CUSTOM keeps whatever is currently set, including the bus.
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onIoChange(io) {
|
||||||
|
this.value.io = io;
|
||||||
|
// Native AIO requires an uncached disk; force cache to "none" to keep the
|
||||||
|
// combo valid — including when cache is still on its "Default" ('') value,
|
||||||
|
// which libvirt would otherwise reject at domain start.
|
||||||
|
if (io === 'native' && this.value.cache !== 'none') {
|
||||||
|
this.value.cache = 'none';
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.$emit('update');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="!isCdRom && storagePerformanceEnabled"
|
||||||
|
class="disk-performance"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm role-link expand-toggle"
|
||||||
|
:aria-expanded="expanded"
|
||||||
|
:aria-label="expanded ? t('harvester.virtualMachine.volume.performance.collapse') : t('harvester.virtualMachine.volume.performance.expand')"
|
||||||
|
@click.prevent="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon"
|
||||||
|
:class="expanded ? 'icon-chevron-down' : 'icon-chevron-right'"
|
||||||
|
/>
|
||||||
|
{{ t('harvester.virtualMachine.volume.performance.title') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="expanded || isView"
|
||||||
|
class="perf-body mt-10"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-disk-perf-profile"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="profile"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.profile.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.profile.tip')"
|
||||||
|
:options="profileOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="onProfileChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="profile === DISK_PERFORMANCE_PROFILE.HIGH"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.highProfileTip')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isCustom || isView"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-disk-cache"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.cache"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.cacheMode.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.cacheMode.tip')"
|
||||||
|
:options="cacheOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-disk-io"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="value.io"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.ioMode.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.ioMode.tip')"
|
||||||
|
:options="ioOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="onIoChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isCustom || isView"
|
||||||
|
class="row mb-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col span-12"
|
||||||
|
data-testid="input-disk-dedicated-iothread"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="value.dedicatedIOThread"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.dedicatedIOThread.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.dedicatedIOThread.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="isNativeIo"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.ioMode.nativeRequiresNoCacheTip')"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="showFilesystemCacheWarning"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.cacheMode.filesystemWarning')"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="showBusTip"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.busTip')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.disk-performance {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
.expand-toggle {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
<script>
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { IO_THREADS_POLICY } from '../../../config/harvester-map';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMPerformanceOptions',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
InfoBox, LabeledSelect, LabeledInput, Checkbox, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
blockMultiQueue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadsPolicy: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Whether the current disk set contains a virtio disk (VM context only).
|
||||||
|
hasVirtioDisk: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Whether any disk requests a dedicated I/O thread (drives the auto-enable hint).
|
||||||
|
hasDedicatedIothread: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
// These are specialist tuning options, so keep the panel collapsed unless the
|
||||||
|
// VM already has something configured.
|
||||||
|
return { expanded: !!(this.blockMultiQueue || this.ioThreadsPolicy) };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
storagePerformanceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('highPerformanceStorage');
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadsPolicyOptions() {
|
||||||
|
return IO_THREADS_POLICY.map((value) => ({
|
||||||
|
label: value === '' ? this.t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.default') : this.t(`harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.${ value }`),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// Block Multi-Queue only applies to virtio disks. If the last virtio disk is
|
||||||
|
// removed while it is enabled, clear it so the VM is not left with an invalid
|
||||||
|
// setting the disabled checkbox can no longer surface.
|
||||||
|
hasVirtioDisk(neu) {
|
||||||
|
if (!neu && this.blockMultiQueue) {
|
||||||
|
this.$emit('update:blockMultiQueue', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<InfoBox
|
||||||
|
v-if="storagePerformanceEnabled && (!isView || blockMultiQueue || !!ioThreadsPolicy)"
|
||||||
|
class="vm-perf mt-10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm role-link expand-toggle"
|
||||||
|
:aria-expanded="expanded"
|
||||||
|
:aria-label="expanded ? t('harvester.virtualMachine.volume.diskPerformance.collapse') : t('harvester.virtualMachine.volume.diskPerformance.expand')"
|
||||||
|
@click.prevent="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon"
|
||||||
|
:class="expanded ? 'icon-chevron-down' : 'icon-chevron-right'"
|
||||||
|
/>
|
||||||
|
{{ t('harvester.virtualMachine.volume.diskPerformance.title') }}
|
||||||
|
</button>
|
||||||
|
<h3
|
||||||
|
v-else
|
||||||
|
class="mb-5"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.volume.diskPerformance.title') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<template v-if="expanded || isView">
|
||||||
|
<p class="text-muted mb-15 mt-10">
|
||||||
|
{{ t('harvester.virtualMachine.volume.diskPerformance.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div
|
||||||
|
class="col span-12"
|
||||||
|
data-testid="input-vm-block-multi-queue"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:value="blockMultiQueue"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="!hasVirtioDisk"
|
||||||
|
@update:value="$emit('update:blockMultiQueue', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-if="!hasVirtioDisk && !isView"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-vm-iothreads-policy"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="ioThreadsPolicy"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.tip')"
|
||||||
|
:options="ioThreadsPolicyOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="$emit('update:ioThreadsPolicy', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="ioThreadsPolicy === 'supplementalPool'"
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-vm-iothread-count"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:value="ioThreadCount"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="$emit('update:ioThreadCount', Number($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-if="hasDedicatedIothread && !ioThreadsPolicy"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.autoEnabledTip')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</InfoBox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vm-perf {
|
||||||
|
.expand-toggle {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions.vue';
|
||||||
|
|
||||||
|
const mountOptions = (value, { featureEnabled = true } = {}) => ({
|
||||||
|
props: { value, mode: _EDIT },
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$store: {
|
||||||
|
getters: {
|
||||||
|
'harvester-common/getFeatureEnabled': () => featureEnabled,
|
||||||
|
'i18n/t': (key) => key,
|
||||||
|
'i18n/exists': () => true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t: (key) => key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hardDisk = (overrides = {}) => ({
|
||||||
|
type: 'disk', bus: 'virtio', cache: '', io: '', dedicatedIOThread: false, ...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component: DiskPerformanceOptions', () => {
|
||||||
|
it('is hidden when the highPerformanceStorage feature is disabled', () => {
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk(), { featureEnabled: false }));
|
||||||
|
|
||||||
|
expect(wrapper.find('.disk-performance').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders for a hard disk when the feature is enabled', () => {
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk()));
|
||||||
|
|
||||||
|
expect(wrapper.find('.disk-performance').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the High Performance preset and emits update', () => {
|
||||||
|
const value = hardDisk();
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('highPerformance');
|
||||||
|
|
||||||
|
expect(value.cache).toBe('none');
|
||||||
|
expect(value.io).toBe('native');
|
||||||
|
expect(value.dedicatedIOThread).toBe(true);
|
||||||
|
expect(value.bus).toBe('virtio');
|
||||||
|
expect(wrapper.emitted('update')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears all overrides when switching back to Default', () => {
|
||||||
|
const value = hardDisk({
|
||||||
|
cache: 'none', io: 'native', dedicatedIOThread: true
|
||||||
|
});
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
|
||||||
|
expect(value.cache).toBe('');
|
||||||
|
expect(value.io).toBe('');
|
||||||
|
expect(value.dedicatedIOThread).toBe(false);
|
||||||
|
expect(wrapper.emitted('update')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores the original bus when leaving High Performance for Default', () => {
|
||||||
|
const value = hardDisk({ bus: 'sata' });
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('highPerformance');
|
||||||
|
expect(value.bus).toBe('virtio');
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
expect(value.bus).toBe('sata');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a bus the user picked themselves after High Performance', () => {
|
||||||
|
const value = hardDisk({ bus: 'sata' });
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('highPerformance');
|
||||||
|
value.bus = 'scsi';
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
expect(value.bus).toBe('scsi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the bus alone for a disk that never used the High Performance preset', () => {
|
||||||
|
const value = hardDisk({
|
||||||
|
bus: 'sata', cache: 'writeback', io: 'threads'
|
||||||
|
});
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
expect(value.bus).toBe('sata');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['a cached state', 'writeback'],
|
||||||
|
['the Default cache', ''],
|
||||||
|
])('forces cache to none when Native I/O is selected from %s', (_label, cache) => {
|
||||||
|
const value = hardDisk({ cache });
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onIoChange('native');
|
||||||
|
|
||||||
|
expect(value.io).toBe('native');
|
||||||
|
expect(value.cache).toBe('none');
|
||||||
|
expect(wrapper.emitted('update')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables every cache option except none while Native I/O is active', () => {
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ io: 'native' })));
|
||||||
|
|
||||||
|
const disabledByValue = wrapper.vm.cacheOptions.reduce((acc, opt) => {
|
||||||
|
acc[opt.value] = opt.disabled;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
expect(disabledByValue['none']).toBe(false);
|
||||||
|
expect(disabledByValue['']).toBe(true);
|
||||||
|
expect(disabledByValue['writeback']).toBe(true);
|
||||||
|
expect(disabledByValue['writethrough']).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,13 +7,14 @@ import UnitInput from '@shell/components/form/UnitInput';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
import VMPerformanceOptions from './VMPerformanceOptions';
|
||||||
|
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { ucFirst, randomStr } from '@shell/utils/string';
|
import { ucFirst, randomStr } from '@shell/utils/string';
|
||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||||
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
@@ -23,10 +24,10 @@ import { EMPTY_IMAGE } from '../../../utils/vm';
|
|||||||
const { READ_WRITE_MANY } = ACCESS_MODE;
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value', 'update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard
|
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard, VMPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -77,6 +78,22 @@ export default {
|
|||||||
resourceType: {
|
resourceType: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// KubeVirt high-performance VM-level features
|
||||||
|
blockMultiQueue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadsPolicy: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,6 +164,14 @@ export default {
|
|||||||
isLHV2VolExpansionFeatureEnabled() {
|
isLHV2VolExpansionFeatureEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasVirtioDisk() {
|
||||||
|
return this.rows.some((R) => R.type === 'disk' && R.bus === 'virtio');
|
||||||
|
},
|
||||||
|
|
||||||
|
hasDedicatedIOThread() {
|
||||||
|
return this.rows.some((R) => R.dedicatedIOThread);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -508,6 +533,18 @@ export default {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VMPerformanceOptions
|
||||||
|
:block-multi-queue="blockMultiQueue"
|
||||||
|
:io-threads-policy="ioThreadsPolicy"
|
||||||
|
:io-thread-count="ioThreadCount"
|
||||||
|
:has-virtio-disk="hasVirtioDisk"
|
||||||
|
:has-dedicated-iothread="hasDedicatedIOThread"
|
||||||
|
:mode="mode"
|
||||||
|
@update:block-multi-queue="$emit('update:blockMultiQueue', $event)"
|
||||||
|
@update:io-threads-policy="$emit('update:ioThreadsPolicy', $event)"
|
||||||
|
@update:io-thread-count="$emit('update:ioThreadCount', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<ModalWithCard
|
<ModalWithCard
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
name="deleteTip"
|
name="deleteTip"
|
||||||
|
|||||||
+8
-1
@@ -4,6 +4,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditContainer',
|
name: 'HarvesterEditContainer',
|
||||||
@@ -11,7 +12,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
LabeledInput, LabeledSelect, InputOrDisplay, Banner
|
LabeledInput, LabeledSelect, InputOrDisplay, Banner, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -153,5 +154,11 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
:label="value.volumeBackups.error.message"
|
:label="value.volumeBackups.error.message"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+16
-1
@@ -14,6 +14,7 @@ import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
|||||||
import { HCI } from '../../../../types';
|
import { HCI } from '../../../../types';
|
||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditExisting',
|
name: 'HarvesterEditExisting',
|
||||||
@@ -21,7 +22,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, Checkbox
|
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, Checkbox, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -172,6 +173,14 @@ export default {
|
|||||||
this.value.storageClassName = pvcResource.spec.storageClassName;
|
this.value.storageClassName = pvcResource.spec.storageClassName;
|
||||||
this.value.volumeMode = pvcResource.spec.volumeMode;
|
this.value.volumeMode = pvcResource.spec.volumeMode;
|
||||||
this.value.shareable = false;
|
this.value.shareable = false;
|
||||||
|
|
||||||
|
// Adopt the volume's default high-performance profile (if any) as a starting point.
|
||||||
|
const ann = pvcResource.metadata?.annotations || {};
|
||||||
|
|
||||||
|
this.value.cache = ann[HCI_ANNOTATIONS.DISK_CACHE_MODE] || '';
|
||||||
|
this.value.io = ann[HCI_ANNOTATIONS.DISK_IO_MODE] || '';
|
||||||
|
this.value.dedicatedIOThread = ann[HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD] === 'true';
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -379,5 +388,11 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
:label="value.volumeBackups.error.message"
|
:label="value.volumeBackups.error.message"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import LabelValue from '@shell/components/LabelValue';
|
|||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMImage',
|
name: 'HarvesterEditVMImage',
|
||||||
@@ -22,7 +23,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
|
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -482,5 +483,11 @@ export default {
|
|||||||
color="error"
|
color="error"
|
||||||
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
|
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { formatSi, parseSi } from '@shell/utils/units';
|
|||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { _VIEW } from '@shell/config/query-params';
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||||
@@ -24,7 +25,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
|
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -362,6 +363,11 @@ export default {
|
|||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div
|
<div
|
||||||
v-if="value.volumeEncryptionFeatureEnabled && isView"
|
v-if="value.volumeEncryptionFeatureEnabled && isView"
|
||||||
|
|||||||
@@ -772,6 +772,9 @@ export default {
|
|||||||
>
|
>
|
||||||
<Volume
|
<Volume
|
||||||
v-model:value="diskRows"
|
v-model:value="diskRows"
|
||||||
|
v-model:block-multi-queue="blockMultiQueue"
|
||||||
|
v-model:io-threads-policy="ioThreadsPolicy"
|
||||||
|
v-model:io-thread-count="ioThreadCount"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:custom-volume-mode="customVolumeMode"
|
:custom-volume-mode="customVolumeMode"
|
||||||
:namespace="value.metadata.namespace"
|
:namespace="value.metadata.namespace"
|
||||||
|
|||||||
@@ -800,6 +800,56 @@ harvester:
|
|||||||
setFirst: Set as root volume
|
setFirst: Set as root volume
|
||||||
saveVolume: Update Volume
|
saveVolume: Update Volume
|
||||||
encryption: Encryption
|
encryption: Encryption
|
||||||
|
performance:
|
||||||
|
title: Storage Performance Options
|
||||||
|
expand: Show high-performance options
|
||||||
|
collapse: Hide high-performance options
|
||||||
|
profile:
|
||||||
|
label: Performance Profile
|
||||||
|
tip: "\"High Performance\" applies a tuned preset (no host cache, native AIO, a dedicated I/O thread on the virtio bus). Choose \"Custom\" to set each option yourself."
|
||||||
|
default: Default (Balanced)
|
||||||
|
highPerformance: High Performance
|
||||||
|
custom: Custom
|
||||||
|
highProfileTip: This disk will use the virtio bus with host cache disabled, native AIO and a dedicated I/O thread.
|
||||||
|
cacheMode:
|
||||||
|
label: Cache Mode
|
||||||
|
tip: "How the host caches disk I/O. \"None\" (host cache disabled) is best for high-throughput guests on block storage; \"WriteBack\" favours performance; \"WriteThrough\" favours durability."
|
||||||
|
default: Default (hypervisor)
|
||||||
|
none: None (no host cache)
|
||||||
|
writeback: WriteBack
|
||||||
|
writethrough: WriteThrough
|
||||||
|
filesystemWarning: "\"None\" (O_DIRECT) requires storage that supports direct I/O. It is recommended only for Block volume mode; on Filesystem volumes the VM may fail to start."
|
||||||
|
ioMode:
|
||||||
|
label: I/O Mode
|
||||||
|
tip: "How QEMU submits I/O. \"Native\" (Linux AIO) offers the lowest overhead but requires host cache set to \"None\"; \"Threads\" uses a host thread pool."
|
||||||
|
default: Default (hypervisor)
|
||||||
|
native: Native (AIO)
|
||||||
|
threads: Threads
|
||||||
|
nativeRequiresNoCacheTip: Native I/O mode requires cache mode "None"; it has been set for you.
|
||||||
|
dedicatedIOThread:
|
||||||
|
label: Dedicated I/O Thread
|
||||||
|
tip: Allocate an exclusive libvirt I/O thread to this disk instead of sharing one. Useful for disks with heavy I/O. Enabling this on any disk turns on the VM's I/O threads policy.
|
||||||
|
busTip: High-performance features work best on the virtio bus.
|
||||||
|
diskPerformance:
|
||||||
|
title: High Performance (I/O Threads and Multi-Queue)
|
||||||
|
description: These settings apply to all volumes in the Virtual Machine and complement the per-volume options above.
|
||||||
|
expand: Show VM-wide high-performance options
|
||||||
|
collapse: Hide VM-wide high-performance options
|
||||||
|
blockMultiQueue:
|
||||||
|
label: Virtio Block Multi-Queue
|
||||||
|
tip: Map disk I/O to multiple queues so it is processed across multiple CPUs. Applies to virtio disks and requires a fixed CPU allocation (Harvester always sets one).
|
||||||
|
noVirtioTip: Add at least one disk on the virtio bus to enable Block Multi-Queue.
|
||||||
|
ioThreadsPolicy:
|
||||||
|
label: I/O Threads Policy
|
||||||
|
tip: "How libvirt allocates I/O threads. \"Shared\" uses one thread for all disks; \"Auto\" pools threads round-robin (up to ~2x vCPUs); \"Supplemental Pool\" lets you pick a fixed thread count and adds matching CPUs to the pod."
|
||||||
|
default: "Disabled (default)"
|
||||||
|
shared: Shared
|
||||||
|
auto: Auto
|
||||||
|
supplementalPool: Supplemental Pool
|
||||||
|
autoEnabledTip: A disk requests a dedicated I/O thread, so the policy is enabled (defaults to "Shared").
|
||||||
|
ioThreadCount:
|
||||||
|
label: I/O Thread Count
|
||||||
|
tip: Number of I/O threads for the supplemental pool. 4-8 is a common range depending on workload.
|
||||||
shareable:
|
shareable:
|
||||||
label: Shareable
|
label: Shareable
|
||||||
tip: Allow multiple virtual machines to attach and write to this volume simultaneously.
|
tip: Allow multiple virtual machines to attach and write to this volume simultaneously.
|
||||||
@@ -997,6 +1047,8 @@ harvester:
|
|||||||
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
|
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
|
||||||
showAdvanced: Show Advanced Options
|
showAdvanced: Show Advanced Options
|
||||||
hideAdvanced: Hide Advanced Options
|
hideAdvanced: Hide Advanced Options
|
||||||
|
performance:
|
||||||
|
pvcTip: These high-performance options are saved as defaults on the volume and applied when it is attached to a virtual machine as a disk.
|
||||||
source: Source
|
source: Source
|
||||||
kind: Kind
|
kind: Kind
|
||||||
sourceOptions:
|
sourceOptions:
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ export default {
|
|||||||
cpuPinning: false,
|
cpuPinning: false,
|
||||||
cpuModel: '',
|
cpuModel: '',
|
||||||
sysprep: { secretName: '', xmlContent: '' },
|
sysprep: { secretName: '', xmlContent: '' },
|
||||||
|
// KubeVirt high-performance VM-level features
|
||||||
|
blockMultiQueue: false,
|
||||||
|
ioThreadsPolicy: '',
|
||||||
|
ioThreadCount: 2,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -409,6 +413,10 @@ export default {
|
|||||||
const cpuPinning = this.isCpuPinning(spec);
|
const cpuPinning = this.isCpuPinning(spec);
|
||||||
const cpuModel = spec.template.spec.domain.cpu?.model || '';
|
const cpuModel = spec.template.spec.domain.cpu?.model || '';
|
||||||
|
|
||||||
|
const blockMultiQueue = spec.template.spec.domain?.devices?.blockMultiQueue || false;
|
||||||
|
const ioThreadsPolicy = spec.template.spec.domain?.ioThreadsPolicy || '';
|
||||||
|
const ioThreadCount = spec.template.spec.domain?.ioThreads?.supplementalPoolThreadCount || 2;
|
||||||
|
|
||||||
const secretRef = this.getSecret(spec);
|
const secretRef = this.getSecret(spec);
|
||||||
const accessCredentials = this.getAccessCredentials(spec);
|
const accessCredentials = this.getAccessCredentials(spec);
|
||||||
|
|
||||||
@@ -447,6 +455,10 @@ export default {
|
|||||||
this['cpuPinning'] = cpuPinning;
|
this['cpuPinning'] = cpuPinning;
|
||||||
this['cpuModel'] = cpuModel;
|
this['cpuModel'] = cpuModel;
|
||||||
|
|
||||||
|
this['blockMultiQueue'] = blockMultiQueue;
|
||||||
|
this['ioThreadsPolicy'] = ioThreadsPolicy;
|
||||||
|
this['ioThreadCount'] = ioThreadCount;
|
||||||
|
|
||||||
this['hasCreateVolumes'] = hasCreateVolumes;
|
this['hasCreateVolumes'] = hasCreateVolumes;
|
||||||
this['networkRows'] = networkRows;
|
this['networkRows'] = networkRows;
|
||||||
this['imageId'] = imageId;
|
this['imageId'] = imageId;
|
||||||
@@ -503,17 +515,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
id: randomStr(5),
|
id: randomStr(5),
|
||||||
source: SOURCE_TYPE.IMAGE,
|
source: SOURCE_TYPE.IMAGE,
|
||||||
name: 'disk-0',
|
name: 'disk-0',
|
||||||
accessMode: READ_WRITE_MANY, // root disk only support LHv1 volume, should be RWX
|
accessMode: READ_WRITE_MANY, // root disk only support LHv1 volume, should be RWX
|
||||||
bus,
|
bus,
|
||||||
volumeName: '',
|
volumeName: '',
|
||||||
size,
|
size,
|
||||||
type,
|
type,
|
||||||
storageClassName: '',
|
storageClassName: '',
|
||||||
image: this.imageId,
|
image: this.imageId,
|
||||||
volumeMode: VOLUME_MODE.BLOCK,
|
volumeMode: VOLUME_MODE.BLOCK,
|
||||||
|
cache: '',
|
||||||
|
io: '',
|
||||||
|
dedicatedIOThread: false,
|
||||||
isEncrypted,
|
isEncrypted,
|
||||||
volumeBackups,
|
volumeBackups,
|
||||||
});
|
});
|
||||||
@@ -603,22 +618,25 @@ export default {
|
|||||||
const volumeBackups = volBackups?.find((vBackup) => vBackup.volumeName === DISK.name) || null;
|
const volumeBackups = volBackups?.find((vBackup) => vBackup.volumeName === DISK.name) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: randomStr(5),
|
id: randomStr(5),
|
||||||
bootOrder,
|
bootOrder,
|
||||||
source,
|
source,
|
||||||
name: DISK.name,
|
name: DISK.name,
|
||||||
realName,
|
realName,
|
||||||
bus,
|
bus,
|
||||||
volumeName,
|
volumeName,
|
||||||
container,
|
container,
|
||||||
accessMode,
|
accessMode,
|
||||||
size: `${ formatSize }${ GIBIBYTE }`,
|
size: `${ formatSize }${ GIBIBYTE }`,
|
||||||
volumeMode: volumeMode || this.customVolumeMode,
|
volumeMode: volumeMode || this.customVolumeMode,
|
||||||
image,
|
image,
|
||||||
type,
|
type,
|
||||||
storageClassName,
|
storageClassName,
|
||||||
hotpluggable,
|
hotpluggable,
|
||||||
shareable: DISK.shareable || false,
|
cache: DISK?.cache || '',
|
||||||
|
io: DISK?.io || '',
|
||||||
|
dedicatedIOThread: DISK?.dedicatedIOThread || false,
|
||||||
|
shareable: DISK.shareable || false,
|
||||||
volumeStatus,
|
volumeStatus,
|
||||||
dataSource,
|
dataSource,
|
||||||
namespace,
|
namespace,
|
||||||
@@ -973,9 +991,25 @@ export default {
|
|||||||
// `shareable` is an attachment-level opt-in, remove the field
|
// `shareable` is an attachment-level opt-in, remove the field
|
||||||
// inherited from the old spec when the disk row no longer requests it
|
// inherited from the old spec when the disk row no longer requests it
|
||||||
mergedDisks.forEach((disk) => {
|
mergedDisks.forEach((disk) => {
|
||||||
if (disk.shareable && !disks.find((D) => D.name === disk.name)?.shareable) {
|
const parsed = disks.find((D) => D.name === disk.name);
|
||||||
|
|
||||||
|
if (disk.shareable && !parsed?.shareable) {
|
||||||
delete disk.shareable;
|
delete disk.shareable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// High-performance fields are only serialized when requested (see
|
||||||
|
// parseDisk). When editing, mergeDeviceList would otherwise keep the
|
||||||
|
// previous spec's cache/io/dedicatedIOThread even after the row is
|
||||||
|
// switched back to "Default" or the dedicated I/O thread is unchecked,
|
||||||
|
// silently preserving stale values — strip them so the disk matches
|
||||||
|
// what the row actually requests.
|
||||||
|
if (parsed) {
|
||||||
|
['cache', 'io', 'dedicatedIOThread'].forEach((field) => {
|
||||||
|
if (!parsed[field] && field in disk) {
|
||||||
|
delete disk[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let spec = {
|
let spec = {
|
||||||
@@ -1012,6 +1046,35 @@ export default {
|
|||||||
delete spec.template.spec.volumes;
|
delete spec.template.spec.volumes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KubeVirt high-performance VM-level features (I/O threads & block multi-queue).
|
||||||
|
const domainRef = spec.template.spec.domain;
|
||||||
|
const hasDedicatedIOThread = mergedDisks.some((D) => D.dedicatedIOThread);
|
||||||
|
// If any disk requests a dedicated I/O thread, KubeVirt requires a policy;
|
||||||
|
// default it to "shared" so the dedicated thread is honored.
|
||||||
|
let ioThreadsPolicy = this.ioThreadsPolicy;
|
||||||
|
|
||||||
|
if (hasDedicatedIOThread && !ioThreadsPolicy) {
|
||||||
|
ioThreadsPolicy = 'shared';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ioThreadsPolicy) {
|
||||||
|
domainRef.ioThreadsPolicy = ioThreadsPolicy;
|
||||||
|
} else {
|
||||||
|
delete domainRef.ioThreadsPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ioThreadsPolicy === 'supplementalPool') {
|
||||||
|
domainRef.ioThreads = { supplementalPoolThreadCount: Number(this.ioThreadCount) || 1 };
|
||||||
|
} else {
|
||||||
|
delete domainRef.ioThreads;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.blockMultiQueue) {
|
||||||
|
domainRef.devices.blockMultiQueue = true;
|
||||||
|
} else {
|
||||||
|
delete domainRef.devices.blockMultiQueue;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.resourceType === HCI.VM) {
|
if (this.resourceType === HCI.VM) {
|
||||||
if (!this.isSingle) {
|
if (!this.isSingle) {
|
||||||
spec = this.multiVMScheduler(spec);
|
spec = this.multiVMScheduler(spec);
|
||||||
@@ -1259,6 +1322,19 @@ export default {
|
|||||||
|
|
||||||
if (R.type === HARD_DISK) {
|
if (R.type === HARD_DISK) {
|
||||||
out.disk = { bus: R.bus };
|
out.disk = { bus: R.bus };
|
||||||
|
|
||||||
|
// KubeVirt high-performance per-disk features. `cache`, `io` and
|
||||||
|
// `dedicatedIOThread` live at the Disk level (siblings of `disk`), not
|
||||||
|
// inside the DiskTarget. See disks_and_volumes.md#high-performance-features.
|
||||||
|
if (R.cache) {
|
||||||
|
out.cache = R.cache;
|
||||||
|
}
|
||||||
|
if (R.io) {
|
||||||
|
out.io = R.io;
|
||||||
|
}
|
||||||
|
if (R.dedicatedIOThread) {
|
||||||
|
out.dedicatedIOThread = true;
|
||||||
|
}
|
||||||
} else if (R.type === CD_ROM) {
|
} else if (R.type === CD_ROM) {
|
||||||
out.cdrom = { bus: R.bus };
|
out.cdrom = { bus: R.bus };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user