mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-13 01:26:06 +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>
(cherry picked from commit 528255dea4)
# Conflicts:
# pkg/harvester/mixins/harvester-vm/index.js
This commit is contained in:
committed by
Mergify
parent
fb190996b6
commit
d0bda99f00
@@ -193,6 +193,10 @@ export default {
|
||||
cpuPinning: false,
|
||||
cpuModel: '',
|
||||
sysprep: { secretName: '', xmlContent: '' },
|
||||
// KubeVirt high-performance VM-level features
|
||||
blockMultiQueue: false,
|
||||
ioThreadsPolicy: '',
|
||||
ioThreadCount: 2,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -407,6 +411,10 @@ 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);
|
||||
|
||||
@@ -445,6 +453,10 @@ 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;
|
||||
@@ -501,17 +513,27 @@ export default {
|
||||
}
|
||||
|
||||
out.push({
|
||||
<<<<<<< HEAD
|
||||
id: randomStr(5),
|
||||
source: SOURCE_TYPE.IMAGE,
|
||||
name: 'disk-0',
|
||||
accessMode: 'ReadWriteMany', // 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
|
||||
>>>>>>> 528255d (feat: expose KubeVirt high-performance storage features in VM and Volume UI (#1133))
|
||||
bus,
|
||||
volumeName: '',
|
||||
volumeName: '',
|
||||
size,
|
||||
type,
|
||||
storageClassName: '',
|
||||
image: this.imageId,
|
||||
volumeMode: VOLUME_MODE.BLOCK,
|
||||
storageClassName: '',
|
||||
image: this.imageId,
|
||||
volumeMode: VOLUME_MODE.BLOCK,
|
||||
cache: '',
|
||||
io: '',
|
||||
dedicatedIOThread: false,
|
||||
isEncrypted,
|
||||
volumeBackups,
|
||||
});
|
||||
@@ -601,22 +623,25 @@ 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,
|
||||
shareable: DISK.shareable || false,
|
||||
cache: DISK?.cache || '',
|
||||
io: DISK?.io || '',
|
||||
dedicatedIOThread: DISK?.dedicatedIOThread || false,
|
||||
shareable: DISK.shareable || false,
|
||||
volumeStatus,
|
||||
dataSource,
|
||||
namespace,
|
||||
@@ -971,9 +996,25 @@ 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) => {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
@@ -1010,6 +1051,35 @@ 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);
|
||||
@@ -1257,6 +1327,19 @@ 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user