+ {{ t('harvester.virtualMachine.volume.diskPerformance.description') }}
+
+<<<<<<< HEAD
{{ t('harvester.virtualMachine.volume.diskPerformance.description') }}
@@ -136,38 +116,42 @@ export default {
v-if="!hasVirtioDisk && !isView"
color="info"
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip')"
+=======
+
+ >>>>>> 12c28c0 (feat: align the storage performance options with the Rancher machine pool form (#1212))
/>
+
+
-
-
-
-
-
-
-
+
+
+
+<<<<<<< HEAD
+=======
+
+
+
+
+
+
+>>>>>>> 12c28c0 (feat: align the storage performance options with the Rancher machine pool form (#1212))
-
-
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts
index 59f20006..712b72a0 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts
@@ -1,3 +1,6 @@
+import fs from 'fs';
+import path from 'path';
+import { load } from 'js-yaml';
import { mount } from '@vue/test-utils';
import { _EDIT } from '@shell/config/query-params';
import DiskPerformanceOptions from '../DiskPerformanceOptions.vue';
@@ -22,6 +25,12 @@ const hardDisk = (overrides = {}) => ({
type: 'disk', bus: 'virtio', cache: '', io: '', dedicatedIOThread: false, ...overrides
});
+const nativeTip = (wrapper) => wrapper.findAllComponents(Banner)
+ .find((b) => b.props('labelKey') === 'harvester.virtualMachine.volume.performance.ioMode.nativeRequiresNoCacheTip');
+
+const filesystemWarning = (wrapper) => wrapper.findAllComponents(Banner)
+ .find((b) => b.props('labelKey') === 'harvester.virtualMachine.volume.performance.cacheMode.filesystemWarning');
+
describe('component: DiskPerformanceOptions', () => {
it('is hidden when the highPerformanceStorage feature is disabled', () => {
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk(), { featureEnabled: false }));
@@ -35,6 +44,102 @@ describe('component: DiskPerformanceOptions', () => {
expect(wrapper.find('.disk-performance').exists()).toBe(true);
});
+ it('shows the profile without a toggle, and the individual fields only for Custom', () => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk()));
+
+ expect(wrapper.find('.expand-toggle').exists()).toBe(false);
+ expect(wrapper.find('[data-testid="input-disk-perf-profile"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="input-disk-cache"]').exists()).toBe(false);
+ expect(wrapper.find('[data-testid="input-disk-dedicated-iothread"]').exists()).toBe(false);
+ });
+
+ it('shows the individual fields for a disk with custom options', () => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ cache: 'writeback', io: 'threads' })));
+
+ expect(wrapper.find('[data-testid="input-disk-cache"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="input-disk-io"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="input-disk-dedicated-iothread"]').exists()).toBe(true);
+ });
+
+ it('is hidden for a CD-ROM', () => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ type: 'cd-rom' })));
+
+ expect(wrapper.find('.disk-performance').exists()).toBe(false);
+ });
+
+ it('names the bus in the bus tip once the disk uses an option', () => {
+ const plain = mount(DiskPerformanceOptions, mountOptions(hardDisk({ bus: 'sata' })));
+ const tuned = mount(DiskPerformanceOptions, mountOptions(hardDisk({ bus: 'sata', cache: 'writeback' })));
+
+ expect(plain.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(false);
+ expect(tuned.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(true);
+ });
+
+ it('says in the bus tip that a dedicated I/O thread needs the virtio bus', () => {
+ const translations = load(fs.readFileSync(path.join(__dirname, '../../../../l10n/en-us.yaml'), 'utf8'));
+ const { busTip } = translations.harvester.virtualMachine.volume.performance;
+
+ expect(busTip).toContain('{bus}');
+ expect(busTip).toContain('A dedicated I/O thread needs the virtio bus');
+ });
+
+ it('shows the bus tip and the Filesystem warning inside a live region that is there before they appear', async() => {
+ const disk = { bus: 'sata', volumeMode: 'Filesystem' };
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk(disk)));
+ const status = wrapper.find('[data-testid="disk-perf-warning-status"]');
+
+ expect(status.attributes('role')).toBe('status');
+ expect(status.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(false);
+ expect(filesystemWarning(wrapper)).toBeUndefined();
+
+ await wrapper.setProps({ value: hardDisk({ ...disk, cache: 'none' }) });
+
+ expect(wrapper.find('[data-testid="disk-perf-warning-status"]').element).toBe(status.element);
+ expect(status.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(true);
+ expect(status.element.contains(filesystemWarning(wrapper)?.element)).toBe(true);
+ });
+
+ it('shows the Native I/O tip inside a live region that is there before Native I/O is picked', async() => {
+ const value = hardDisk({ cache: 'writeback', io: 'threads' });
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
+ const status = wrapper.find('[data-testid="disk-perf-native-status"]');
+
+ expect(status.attributes('role')).toBe('status');
+ expect(nativeTip(wrapper)).toBeUndefined();
+
+ wrapper.vm.onIoChange('native');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('[data-testid="disk-perf-native-status"]').element).toBe(status.element);
+ expect(status.element.contains(nativeTip(wrapper)?.element)).toBe(true);
+ });
+
+ it('keeps the live region and the Native I/O tip when picking Native I/O switches Custom to High Performance', async() => {
+ const value = hardDisk({
+ cache: 'writeback', io: 'threads', dedicatedIOThread: true
+ });
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
+ const status = wrapper.find('[data-testid="disk-perf-native-status"]');
+
+ expect(wrapper.vm.profile).toBe('custom');
+
+ wrapper.vm.onIoChange('native');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.profile).toBe('highPerformance');
+ expect(wrapper.find('[data-testid="input-disk-cache"]').exists()).toBe(false);
+ expect(wrapper.find('[data-testid="disk-perf-native-status"]').element).toBe(status.element);
+ expect(status.element.contains(nativeTip(wrapper)?.element)).toBe(true);
+ });
+
+ it('passes the Filesystem warning as a label key so its quotes are not double-escaped', () => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ cache: 'none', volumeMode: 'Filesystem' })));
+ const banner = filesystemWarning(wrapper);
+
+ expect(banner).toBeDefined();
+ expect(banner?.props('label')).toBeFalsy();
+ });
+
it('applies the High Performance preset and emits update', () => {
const value = hardDisk();
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
@@ -122,4 +227,61 @@ describe('component: DiskPerformanceOptions', () => {
expect(disabledByValue['writeback']).toBe(true);
expect(disabledByValue['writethrough']).toBe(true);
});
+<<<<<<< HEAD
+=======
+
+ it.each([
+ ['Default', {
+ cache: '', io: '', dedicatedIOThread: false
+ }],
+ ['High Performance', {
+ cache: 'none', io: 'native', dedicatedIOThread: true
+ }],
+ ])('stays on Custom when the options match the %s preset', async(_label, opts) => {
+ const value = hardDisk({ cache: 'writeback' });
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
+
+ wrapper.vm.onProfileChange('custom');
+ await wrapper.setProps({ value: { ...value, ...opts } });
+ await nextTick();
+
+ expect(wrapper.vm.profile).toBe('custom');
+ });
+
+ it('detects the profile from an existing disk', async() => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk()));
+
+ expect(wrapper.vm.profile).toBe('default');
+
+ await wrapper.setProps({
+ value: hardDisk({
+ cache: 'none', io: 'native', dedicatedIOThread: true
+ })
+ });
+
+ expect(wrapper.vm.profile).toBe('highPerformance');
+ });
+
+ it('updates an automatically detected Custom profile when preset values are loaded', async() => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ cache: 'writeback' })));
+
+ expect(wrapper.vm.profile).toBe('custom');
+
+ await wrapper.setProps({
+ value: hardDisk({
+ cache: 'none', io: 'native', dedicatedIOThread: true
+ })
+ });
+
+ expect(wrapper.vm.profile).toBe('highPerformance');
+ });
+
+ it('passes the Native I/O hint as a label key so its quotes are not double-escaped', () => {
+ const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ io: 'native' })));
+ const banner = nativeTip(wrapper);
+
+ expect(banner).toBeDefined();
+ expect(banner?.props('label')).toBeFalsy();
+ });
+>>>>>>> 12c28c0 (feat: align the storage performance options with the Rancher machine pool form (#1212))
});
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/VMPerformanceOptions.test.js b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/VMPerformanceOptions.test.js
new file mode 100644
index 00000000..261b3acd
--- /dev/null
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/VMPerformanceOptions.test.js
@@ -0,0 +1,105 @@
+import { mount } from '@vue/test-utils';
+import { _CREATE, _VIEW } from '@shell/config/query-params';
+import { Banner } from '@components/Banner';
+import VMPerformanceOptions from '../VMPerformanceOptions.vue';
+
+const AUTO_ENABLED_TIP = 'harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.autoEnabledTip';
+const NO_VIRTIO_TIP = 'harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip';
+
+function mountOptions(props = {}, { featureEnabled = true } = {}) {
+ return mount(VMPerformanceOptions, {
+ props: { mode: _CREATE, ...props },
+ global: {
+ mocks: {
+ $store: {
+ getters: {
+ 'harvester-common/getFeatureEnabled': () => featureEnabled,
+ 'i18n/t': (key) => key,
+ 'i18n/exists': () => true,
+ }
+ },
+ t: (key) => key,
+ },
+ },
+ });
+}
+
+function bannerFor(wrapper, labelKey) {
+ return wrapper.findAllComponents(Banner).find((b) => b.props('labelKey') === labelKey);
+}
+
+describe('component: VMPerformanceOptions', () => {
+ it('should be hidden when the highPerformanceStorage feature is disabled', () => {
+ const wrapper = mountOptions({}, { featureEnabled: false });
+
+ expect(wrapper.find('.vm-perf').exists()).toBe(false);
+ });
+
+ it('should show the section with its title and fields, without a toggle', () => {
+ const wrapper = mountOptions();
+
+ expect(wrapper.find('h3').text()).toBe('harvester.virtualMachine.volume.diskPerformance.title');
+ expect(wrapper.find('.expand-toggle').exists()).toBe(false);
+ expect(wrapper.find('[data-testid="input-vm-block-multi-queue"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="input-vm-iothreads-policy"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="input-vm-iothread-count"]').exists()).toBe(false);
+ });
+
+ it('should only accept whole numbers as the supplemental pool thread count', () => {
+ const wrapper = mountOptions({ ioThreadsPolicy: 'supplementalPool', ioThreadCount: 4 });
+ const input = wrapper.find('[data-testid="input-vm-iothread-count"] input');
+
+ expect(input.attributes('inputmode')).toBe('numeric');
+ });
+
+ it.each([
+ [_CREATE, true],
+ [_VIEW, false],
+ ])('in %s mode, should mark the supplemental pool thread count as required: %p', (mode, required) => {
+ const wrapper = mountOptions({
+ mode, ioThreadsPolicy: 'supplementalPool', ioThreadCount: 4
+ });
+
+ expect(wrapper.find('[data-testid="input-vm-iothread-count"] .required').exists()).toBe(required);
+ });
+
+ it.each([
+ [{}, false],
+ [{ blockMultiQueue: true }, true],
+ [{ ioThreadsPolicy: 'auto' }, true],
+ ])('in view mode with %p, should show the section: %p', (props, shown) => {
+ const wrapper = mountOptions({ ...props, mode: _VIEW });
+
+ expect(wrapper.find('.vm-perf').exists()).toBe(shown);
+ });
+
+ it('should clear Block Multi-Queue when the last virtio disk goes away', async() => {
+ const wrapper = mountOptions({ blockMultiQueue: true, hasVirtioDisk: true });
+
+ await wrapper.setProps({ hasVirtioDisk: false });
+
+ expect(wrapper.emitted('update:blockMultiQueue')).toStrictEqual([[false]]);
+ });
+
+ it('should pass the I/O threads policy hint as a label key so its quotes are not double-escaped', () => {
+ const wrapper = mountOptions({ hasDedicatedIothread: true });
+ const banner = bannerFor(wrapper, AUTO_ENABLED_TIP);
+
+ expect(banner).toBeDefined();
+ expect(banner.props('label')).toBeFalsy();
+ });
+
+ it('should not show the I/O threads policy hint when a policy is set', () => {
+ const wrapper = mountOptions({ hasDedicatedIothread: true, ioThreadsPolicy: 'shared' });
+
+ expect(bannerFor(wrapper, AUTO_ENABLED_TIP)).toBeUndefined();
+ });
+
+ it('should pass the no virtio disk hint as a label key', () => {
+ const wrapper = mountOptions({ hasVirtioDisk: false });
+ const banner = bannerFor(wrapper, NO_VIRTIO_TIP);
+
+ expect(banner).toBeDefined();
+ expect(banner.props('label')).toBeFalsy();
+ });
+});
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue
index 8190f906..4124c0fb 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue
@@ -7,7 +7,6 @@ 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';
@@ -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 {
-