mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 21:26:08 +08:00
Compare commits
73
Commits
v1.9.1-rc2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528255dea4 | ||
|
|
3c92a06268 | ||
|
|
6256cdc97a | ||
|
|
4323132565 | ||
|
|
d05ec68e16 | ||
|
|
cb2cef5e73 | ||
|
|
45116b568a | ||
|
|
f42d798d10 | ||
|
|
50b4cf5248 | ||
|
|
cba37194b6 | ||
|
|
bbe0c4643f | ||
|
|
00584f1d19 | ||
|
|
4fa6470343 | ||
|
|
d550046189 | ||
|
|
5f32a9ade5 | ||
|
|
3f39d561be | ||
|
|
7c84269ed6 | ||
|
|
f63be73b11 | ||
|
|
fabc1cec21 | ||
|
|
ed2b6f2551 | ||
|
|
72282ae50e | ||
|
|
8fba4e7050 | ||
|
|
fbd4a1108f | ||
|
|
823a88c85b | ||
|
|
3a2c6e3a3d | ||
|
|
be6e9b5f06 | ||
|
|
a88bed9eb4 | ||
|
|
f97f14c811 | ||
|
|
f8a29a1c54 | ||
|
|
9a935434c2 | ||
|
|
afe632b723 | ||
|
|
d050f21f9a | ||
|
|
b87f367583 | ||
|
|
92aa8ec603 | ||
|
|
e013534373 | ||
|
|
d23c3d3b72 | ||
|
|
719c33fa12 | ||
|
|
a6bd7ec00e | ||
|
|
ac26fe2f0e | ||
|
|
790cade04d | ||
|
|
2903e86c02 | ||
|
|
2d747e435a | ||
|
|
eadda2a18b | ||
|
|
a178302f73 | ||
|
|
d376bfb3a5 | ||
|
|
7a84e08f99 | ||
|
|
3d39626b93 | ||
|
|
1c3b40b631 | ||
|
|
87baf75301 | ||
|
|
a10d651827 | ||
|
|
11ff7c8a7c | ||
|
|
f6dffe9816 | ||
|
|
b813bc578e | ||
|
|
7c3b2730ab | ||
|
|
429d9e18ad | ||
|
|
dc166d8fc9 | ||
|
|
3ff4534294 | ||
|
|
d4223ad2ae | ||
|
|
44297ada16 | ||
|
|
aea53fdd65 | ||
|
|
9640c87947 | ||
|
|
2cefd7cb78 | ||
|
|
fb6ffa3c0f | ||
|
|
e0436bff28 | ||
|
|
219f39ae52 | ||
|
|
41cf44d231 | ||
|
|
7d09cb7142 | ||
|
|
560643d299 | ||
|
|
c3bb0ea4be | ||
|
|
3c37f04ef6 | ||
|
|
d98db6ba49 | ||
|
|
531ed03796 | ||
|
|
59f260eab8 |
@@ -8,12 +8,16 @@
|
|||||||
- [ ] Yes, the backend owner is @
|
- [ ] Yes, the backend owner is @
|
||||||
- [ ] No, frontend-only.
|
- [ ] No, frontend-only.
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue
|
||||||
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
||||||
harvester/harvester#<issue_number>
|
harvester/harvester#<issue_number>
|
||||||
|
|
||||||
|
|
||||||
### Test screenshot or video (Required)
|
### Test Steps
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Provide the test steps for reviewer to verify the issue -->
|
||||||
|
1. Go to ... page
|
||||||
|
|
||||||
|
### Test screenshot or video
|
||||||
|
<!-- Attach screenshot or video of the changes and eventual comparison -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Build Standalone on PR Merge
|
name: Build Standalone on PR Merge
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -29,3 +30,35 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
with:
|
with:
|
||||||
CI_BRANCH: ${{github.ref_name}}
|
CI_BRANCH: ${{github.ref_name}}
|
||||||
|
comment-on-pr:
|
||||||
|
name: Comment build result on PR
|
||||||
|
# Only for manual runs so the existing push / pull_request flows are untouched.
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Add build artifact comment to the PR opened from this branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Match scripts/version: sanitize the branch and map main -> latest.
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
DIR="latest"
|
||||||
|
else
|
||||||
|
DIR=$(printf '%s' "$BRANCH" | sed -E 's/[^a-zA-Z0-9.-]+/-/g')
|
||||||
|
fi
|
||||||
|
URL="https://releases.rancher.com/harvester-ui/dashboard/${DIR}/index.html"
|
||||||
|
|
||||||
|
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$BRANCH" --json number --jq '.[0].number // empty')
|
||||||
|
if [ -z "$PR" ]; then
|
||||||
|
echo "No open PR found for branch $BRANCH, skipping comment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY=$(printf '🚀 **UI Artifact Build Status:** `SUCCESS` ✅\n> ui-index: %s\n> ui-source: external' "$URL")
|
||||||
|
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$BODY"
|
||||||
|
|||||||
+8
-6
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.9.1-rc2",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
"@rancher/shell": "3.0.12-rc.7",
|
"@rancher/shell": "3.0.13",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
"@vue-flow/controls": "^1.1.1",
|
"@vue-flow/controls": "^1.1.1",
|
||||||
"@vue-flow/core": "^1.33.5",
|
"@vue-flow/core": "^1.33.5",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "25.9.5",
|
"@types/node": "25.9.6",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
@@ -48,15 +48,17 @@
|
|||||||
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
|
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
|
||||||
"build": "./node_modules/.bin/vue-cli-service build",
|
"build": "./node_modules/.bin/vue-cli-service build",
|
||||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint": "./node_modules/@rancher/shell/scripts/lint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint:fix": "./node_modules/@rancher/shell/scripts/lint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||||
"commitlint": "commitlint --edit",
|
"commitlint": "commitlint --edit",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh",
|
||||||
|
"prelint": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error",
|
||||||
|
"prelint:fix": "npm --prefix node_modules/@rancher/shell/eslint-legacy install eslint-plugin-jest@^24 --no-save --no-audit --no-fund --legacy-peer-deps --loglevel=error"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/load": "^19.8.1",
|
"@commitlint/load": "^19.8.1",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ export const VOLUME_MODE = {
|
|||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACCESS_MODE = {
|
||||||
|
READ_WRITE_MANY: 'ReadWriteMany',
|
||||||
|
READ_WRITE_ONCE: 'ReadWriteOnce',
|
||||||
|
READ_ONLY_MANY: 'ReadOnlyMany',
|
||||||
|
READ_WRITE_ONCE_POD: 'ReadWriteOncePod',
|
||||||
|
};
|
||||||
|
|
||||||
export const NETWORK_PROTOCOL = {
|
export const NETWORK_PROTOCOL = {
|
||||||
IPv4: 'IPv4',
|
IPv4: 'IPv4',
|
||||||
IPv6: 'IPv6',
|
IPv6: 'IPv6',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
@@ -10,6 +10,10 @@ import { HCI_SETTING } from '../../config/settings';
|
|||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
const {
|
||||||
|
READ_WRITE_ONCE, READ_ONLY_MANY, READ_WRITE_MANY, READ_WRITE_ONCE_POD
|
||||||
|
} = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CDISettings',
|
name: 'CDISettings',
|
||||||
|
|
||||||
@@ -90,10 +94,10 @@ export default {
|
|||||||
|
|
||||||
accessModeOptions() {
|
accessModeOptions() {
|
||||||
return [
|
return [
|
||||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
{ label: READ_WRITE_ONCE, value: READ_WRITE_ONCE },
|
||||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
{ label: READ_ONLY_MANY, value: READ_ONLY_MANY },
|
||||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
{ label: READ_WRITE_MANY, value: READ_WRITE_MANY },
|
||||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
{ label: READ_WRITE_ONCE_POD, value: READ_WRITE_ONCE_POD },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -26,9 +27,11 @@ import { HCI, VOLUME_SNAPSHOT } from '../types';
|
|||||||
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';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY, READ_WRITE_ONCE, READ_ONLY_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
|
|
||||||
@@ -45,7 +48,8 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
Conditions
|
Conditions,
|
||||||
|
DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -83,7 +87,7 @@ export default {
|
|||||||
if (this.mode === _CREATE) {
|
if (this.mode === _CREATE) {
|
||||||
// default volumeMode to Block
|
// default volumeMode to Block
|
||||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
||||||
this.value.spec.accessModes = ['ReadWriteMany'];
|
this.value.spec.accessModes = [READ_WRITE_MANY];
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = this.value?.spec?.resources?.requests?.storage || null;
|
const storage = this.value?.spec?.resources?.requests?.storage || null;
|
||||||
@@ -98,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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -160,7 +172,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
accessModeOptions() {
|
accessModeOptions() {
|
||||||
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
|
return [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY];
|
||||||
},
|
},
|
||||||
|
|
||||||
imageOption() {
|
imageOption() {
|
||||||
@@ -330,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 ) {
|
||||||
@@ -355,7 +368,7 @@ export default {
|
|||||||
|
|
||||||
getAccessMode() {
|
getAccessMode() {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return ['ReadWriteMany'];
|
return [READ_WRITE_MANY];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||||
@@ -371,7 +384,7 @@ export default {
|
|||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
return readWriteOnce ? [READ_WRITE_ONCE] : [READ_WRITE_MANY];
|
||||||
},
|
},
|
||||||
buildDataVolumeObj() {
|
buildDataVolumeObj() {
|
||||||
const storage = {
|
const storage = {
|
||||||
@@ -444,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 } },
|
||||||
@@ -608,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,24 +7,27 @@ 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 } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
import { EMPTY_IMAGE } from '../../../utils/vm';
|
import { EMPTY_IMAGE } from '../../../utils/vm';
|
||||||
|
|
||||||
|
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: {
|
||||||
@@ -69,12 +72,28 @@ export default {
|
|||||||
|
|
||||||
customAccessMode: {
|
customAccessMode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'ReadWriteMany'
|
default: READ_WRITE_MANY
|
||||||
},
|
},
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -145,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: {
|
||||||
@@ -506,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,12 +9,15 @@ 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';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY, READ_WRITE_ONCE } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVolume',
|
name: 'HarvesterEditVolume',
|
||||||
@@ -22,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: {
|
||||||
@@ -205,7 +208,7 @@ export default {
|
|||||||
|
|
||||||
getAccessMode(storageClassName) {
|
getAccessMode(storageClassName) {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return 'ReadWriteMany';
|
return READ_WRITE_MANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||||
@@ -216,7 +219,7 @@ export default {
|
|||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
|
return readWriteOnce ? READ_WRITE_ONCE : READ_WRITE_MANY;
|
||||||
},
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -360,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:
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import { HCI } from '../../types';
|
|||||||
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
@@ -193,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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -276,7 +282,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
customAccessMode() {
|
customAccessMode() {
|
||||||
return this.storageClassSetting.accessModes || 'ReadWriteMany';
|
return this.storageClassSetting.accessModes || READ_WRITE_MANY;
|
||||||
},
|
},
|
||||||
|
|
||||||
isWindows() {
|
isWindows() {
|
||||||
@@ -407,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);
|
||||||
|
|
||||||
@@ -445,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;
|
||||||
@@ -501,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: 'ReadWriteMany', // 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,
|
||||||
});
|
});
|
||||||
@@ -570,7 +587,7 @@ export default {
|
|||||||
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
|
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
|
||||||
|
|
||||||
source = SOURCE_TYPE.ATTACH_VOLUME;
|
source = SOURCE_TYPE.ATTACH_VOLUME;
|
||||||
accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany';
|
accessMode = pvcResource?.spec?.accessModes?.[0] || READ_WRITE_MANY;
|
||||||
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
||||||
storageClassName = pvcResource?.spec?.storageClassName;
|
storageClassName = pvcResource?.spec?.storageClassName;
|
||||||
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
||||||
@@ -601,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,
|
||||||
@@ -971,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 = {
|
||||||
@@ -1010,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);
|
||||||
@@ -1257,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 };
|
||||||
}
|
}
|
||||||
@@ -1315,8 +1393,9 @@ export default {
|
|||||||
case SOURCE_TYPE.IMAGE: {
|
case SOURCE_TYPE.IMAGE: {
|
||||||
const image = this.images.find( (I) => R.image === I.id);
|
const image = this.images.find( (I) => R.image === I.id);
|
||||||
|
|
||||||
|
out.spec.storageClassName = R.realName ? R.storageClassName : (R.storageClassName || image?.storageClassName);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
out.spec.storageClassName = image.storageClassName;
|
|
||||||
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: image.id };
|
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: image.id };
|
||||||
} else {
|
} else {
|
||||||
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: '' };
|
out.metadata.annotations = { [HCI_ANNOTATIONS.IMAGE_ID]: '' };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.9.1-rc2",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
Reference in New Issue
Block a user