Compare commits

..
Author SHA1 Message Date
renovate[bot] 9233fb26f3 deps: update dependency @types/node to v25.9.8 2026-09-27 06:02:09 +00:00
19 changed files with 30 additions and 953 deletions
+1 -1
View File
@@ -27,7 +27,7 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "25.9.6",
"@types/node": "25.9.8",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
-1
View File
@@ -76,7 +76,6 @@ const FEATURE_FLAGS = {
'longhornV2HugepageSettings',
'staticIPForVM',
'fsFreezeDeadline',
'highPerformanceStorage',
],
'v1.9.1': [
'lvmDmThinOnly',
-15
View File
@@ -39,21 +39,6 @@ export const VOLUME_TYPE = [{
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 = {
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
@@ -33,11 +33,6 @@ export const HCI = {
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
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',
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
@@ -214,14 +214,6 @@ export default {
this['diskRows'] = diskRows;
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
}
@@ -262,9 +254,6 @@ export default {
:namespace="value.metadata.namespace"
:vm="value"
:resource-type="value.type"
:block-multi-queue="blockMultiQueue"
:io-threads-policy="ioThreadsPolicy"
:io-thread-count="ioThreadCount"
/>
</Tab>
@@ -308,9 +308,6 @@ export default {
>
<Volume
v-model:value="diskRows"
v-model:block-multi-queue="blockMultiQueue"
v-model:io-threads-policy="ioThreadsPolicy"
v-model:io-thread-count="ioThreadCount"
:mode="mode"
:namespace="value.metadata.namespace"
:existing-volume-disabled="true"
+2 -40
View File
@@ -8,7 +8,6 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Conditions from '@shell/components/form/Conditions';
import DiskPerformanceOptions from './kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import jsyaml from 'js-yaml';
@@ -48,8 +47,7 @@ export default {
LabeledSelect,
LabeledInput,
NameNsDescription,
Conditions,
DiskPerformanceOptions
Conditions
},
mixins: [CreateEditView],
@@ -102,15 +100,7 @@ export default {
createWithDataVolume: false,
snapshots: [],
images: [],
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,
},
GIBIBYTE
};
},
@@ -342,7 +332,6 @@ export default {
}
},
'value.spec.volumeMode'(neu) {
this.perf.volumeMode = neu;
if (neu === VOLUME_MODE.FILE_SYSTEM) {
this.setVolumeForVmAnnotation();
} else if (neu === VOLUME_MODE.BLOCK ) {
@@ -457,23 +446,6 @@ export default {
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 = {
...this.value.spec,
resources: { requests: { storage: this.storage } },
@@ -638,16 +610,6 @@ export default {
class="mb-20"
@update:value="update"
/>
<Banner
color="info"
:label="t('harvester.volume.performance.pvcTip')"
/>
<DiskPerformanceOptions
:value="perf"
:mode="mode"
@update="update"
/>
</Tab>
<Tab
v-if="!isCreate"
@@ -1,321 +0,0 @@
<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>
@@ -1,191 +0,0 @@
<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>
@@ -1,125 +0,0 @@
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,14 +7,13 @@ import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
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 { clone } from '@shell/utils/object';
import { ucFirst, randomStr } from '@shell/utils/string';
import { removeObject } from '@shell/utils/array';
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
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 { HCI } from '../../../types';
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
@@ -24,10 +23,10 @@ import { EMPTY_IMAGE } from '../../../utils/vm';
const { READ_WRITE_MANY } = ACCESS_MODE;
export default {
emits: ['update:value', 'update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
emits: ['update:value'],
components: {
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard, VMPerformanceOptions
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard
},
props: {
@@ -78,22 +77,6 @@ export default {
resourceType: {
type: String,
default: ''
},
// KubeVirt high-performance VM-level features
blockMultiQueue: {
type: Boolean,
default: false
},
ioThreadsPolicy: {
type: String,
default: ''
},
ioThreadCount: {
type: Number,
default: 2
}
},
@@ -164,14 +147,6 @@ export default {
isLHV2VolExpansionFeatureEnabled() {
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: {
@@ -533,18 +508,6 @@ export default {
</button>
</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
v-if="isOpen"
name="deleteTip"
@@ -4,7 +4,6 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { Banner } from '@components/Banner';
import DiskPerformanceOptions from '../DiskPerformanceOptions';
export default {
name: 'HarvesterEditContainer',
@@ -12,7 +11,7 @@ export default {
emits: ['update'],
components: {
LabeledInput, LabeledSelect, InputOrDisplay, Banner, DiskPerformanceOptions
LabeledInput, LabeledSelect, InputOrDisplay, Banner
},
props: {
@@ -154,11 +153,5 @@ export default {
class="mb-20"
:label="value.volumeBackups.error.message"
/>
<DiskPerformanceOptions
:value="value"
:mode="mode"
@update="update"
/>
</div>
</template>
@@ -14,7 +14,6 @@ import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { HCI } from '../../../../types';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { GIBIBYTE } from '../../../../utils/unit';
import DiskPerformanceOptions from '../DiskPerformanceOptions';
export default {
name: 'HarvesterEditExisting',
@@ -22,7 +21,7 @@ export default {
emits: ['update'],
components: {
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, Checkbox, DiskPerformanceOptions
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, Checkbox
},
props: {
@@ -173,14 +172,6 @@ export default {
this.value.storageClassName = pvcResource.spec.storageClassName;
this.value.volumeMode = pvcResource.spec.volumeMode;
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();
},
@@ -388,11 +379,5 @@ export default {
class="mb-20"
:label="value.volumeBackups.error.message"
/>
<DiskPerformanceOptions
:value="value"
:mode="mode"
@update="update"
/>
</div>
</template>
@@ -15,7 +15,6 @@ import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string';
import { GIBIBYTE } from '../../../../utils/unit';
import { EMPTY_IMAGE } from '../../../../utils/vm';
import DiskPerformanceOptions from '../DiskPerformanceOptions';
export default {
name: 'HarvesterEditVMImage',
@@ -23,7 +22,7 @@ export default {
emits: ['update'],
components: {
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, DiskPerformanceOptions
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
},
props: {
@@ -483,11 +482,5 @@ export default {
color="error"
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
/>
<DiskPerformanceOptions
:value="value"
:mode="mode"
@update="update"
/>
</div>
</template>
@@ -9,7 +9,6 @@ import { formatSi, parseSi } from '@shell/utils/units';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { _VIEW } from '@shell/config/query-params';
import LabelValue from '@shell/components/LabelValue';
import DiskPerformanceOptions from '../DiskPerformanceOptions';
import { ucFirst } from '@shell/utils/string';
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
@@ -25,7 +24,7 @@ export default {
emits: ['update'],
components: {
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, DiskPerformanceOptions
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
},
props: {
@@ -363,11 +362,6 @@ export default {
</InputOrDisplay>
</div>
</div>
<DiskPerformanceOptions
:value="value"
:mode="mode"
@update="update"
/>
<div class="row mb-20">
<div
v-if="value.volumeEncryptionFeatureEnabled && isView"
@@ -772,9 +772,6 @@ export default {
>
<Volume
v-model:value="diskRows"
v-model:block-multi-queue="blockMultiQueue"
v-model:io-threads-policy="ioThreadsPolicy"
v-model:io-thread-count="ioThreadCount"
:mode="mode"
:custom-volume-mode="customVolumeMode"
:namespace="value.metadata.namespace"
-52
View File
@@ -800,56 +800,6 @@ harvester:
setFirst: Set as root volume
saveVolume: Update Volume
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:
label: Shareable
tip: Allow multiple virtual machines to attach and write to this volume simultaneously.
@@ -1047,8 +997,6 @@ harvester:
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
showAdvanced: Show 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
kind: Kind
sourceOptions:
+14 -90
View File
@@ -195,10 +195,6 @@ export default {
cpuPinning: false,
cpuModel: '',
sysprep: { secretName: '', xmlContent: '' },
// KubeVirt high-performance VM-level features
blockMultiQueue: false,
ioThreadsPolicy: '',
ioThreadCount: 2,
};
},
@@ -413,10 +409,6 @@ export default {
const cpuPinning = this.isCpuPinning(spec);
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 accessCredentials = this.getAccessCredentials(spec);
@@ -455,10 +447,6 @@ export default {
this['cpuPinning'] = cpuPinning;
this['cpuModel'] = cpuModel;
this['blockMultiQueue'] = blockMultiQueue;
this['ioThreadsPolicy'] = ioThreadsPolicy;
this['ioThreadCount'] = ioThreadCount;
this['hasCreateVolumes'] = hasCreateVolumes;
this['networkRows'] = networkRows;
this['imageId'] = imageId;
@@ -515,20 +503,17 @@ export default {
}
out.push({
id: randomStr(5),
source: SOURCE_TYPE.IMAGE,
name: 'disk-0',
accessMode: READ_WRITE_MANY, // root disk only support LHv1 volume, should be RWX
id: randomStr(5),
source: SOURCE_TYPE.IMAGE,
name: 'disk-0',
accessMode: READ_WRITE_MANY, // root disk only support LHv1 volume, should be RWX
bus,
volumeName: '',
volumeName: '',
size,
type,
storageClassName: '',
image: this.imageId,
volumeMode: VOLUME_MODE.BLOCK,
cache: '',
io: '',
dedicatedIOThread: false,
storageClassName: '',
image: this.imageId,
volumeMode: VOLUME_MODE.BLOCK,
isEncrypted,
volumeBackups,
});
@@ -618,25 +603,22 @@ export default {
const volumeBackups = volBackups?.find((vBackup) => vBackup.volumeName === DISK.name) || null;
return {
id: randomStr(5),
id: randomStr(5),
bootOrder,
source,
name: DISK.name,
name: DISK.name,
realName,
bus,
volumeName,
container,
accessMode,
size: `${ formatSize }${ GIBIBYTE }`,
volumeMode: volumeMode || this.customVolumeMode,
size: `${ formatSize }${ GIBIBYTE }`,
volumeMode: volumeMode || this.customVolumeMode,
image,
type,
storageClassName,
hotpluggable,
cache: DISK?.cache || '',
io: DISK?.io || '',
dedicatedIOThread: DISK?.dedicatedIOThread || false,
shareable: DISK.shareable || false,
shareable: DISK.shareable || false,
volumeStatus,
dataSource,
namespace,
@@ -991,25 +973,9 @@ export default {
// `shareable` is an attachment-level opt-in, remove the field
// inherited from the old spec when the disk row no longer requests it
mergedDisks.forEach((disk) => {
const parsed = disks.find((D) => D.name === disk.name);
if (disk.shareable && !parsed?.shareable) {
if (disk.shareable && !disks.find((D) => D.name === disk.name)?.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 = {
@@ -1046,35 +1012,6 @@ export default {
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.isSingle) {
spec = this.multiVMScheduler(spec);
@@ -1322,19 +1259,6 @@ export default {
if (R.type === HARD_DISK) {
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) {
out.cdrom = { bus: R.bus };
}
+6 -16
View File
@@ -2935,10 +2935,10 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.6":
version "25.9.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.6.tgz#47865018f9a5635f161ec69c041385edb15d78fe"
integrity sha512-JR6Q/PV5DKFvjrGFVqQJdeG0qvsqQQLDa3TzFrqVwhqRXqwNaxPo2KYCtCQXpOdIulCouKwT7a6in9nFthBAzw==
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.8":
version "25.9.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.8.tgz#a460865b194c2c416a8f11841613207848b92583"
integrity sha512-VfMrScDmMhUJQmd5hArdQnFvK0OIeD36uN2Va1FcpYaLB/BgkgM8Ulc50XtYISPMz7APJ30+N0T5EM0jlcdfRw==
dependencies:
undici-types ">=7.24.0 <7.24.7"
@@ -3099,16 +3099,11 @@
"@typescript-eslint/types" "8.62.0"
"@typescript-eslint/visitor-keys" "8.62.0"
"@typescript-eslint/tsconfig-utils@8.62.0":
"@typescript-eslint/tsconfig-utils@8.62.0", "@typescript-eslint/tsconfig-utils@^8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
"@typescript-eslint/tsconfig-utils@^8.62.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz#594d7a3c5952055b3c431fc563ca7fd1defcce18"
integrity sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==
"@typescript-eslint/type-utils@8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz#6f64d813ed9f340d796baed40cdab86b8e9a491a"
@@ -3120,16 +3115,11 @@
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.62.0":
"@typescript-eslint/types@8.62.0", "@typescript-eslint/types@^8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
"@typescript-eslint/types@^8.62.0":
version "8.68.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.68.0.tgz#3f9d4e62fbe5728f09403cdc7b4d58af842ac1af"
integrity sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==
"@typescript-eslint/typescript-estree@8.62.0":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz#b96b55d02e26aa09434421c3fa678e525ca09a4c"