mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-07 22:56:07 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f13aadeed | ||
|
|
da36db143f | ||
|
|
5af181f5b6 | ||
|
|
528255dea4 | ||
|
|
3c92a06268 | ||
|
|
6256cdc97a | ||
|
|
4323132565 | ||
|
|
d05ec68e16 | ||
|
|
cb2cef5e73 | ||
|
|
45116b568a | ||
|
|
f42d798d10 |
@@ -125,6 +125,13 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
|
# Tag builds (RC, formal and sprint releases) ship the tag as the package
|
||||||
|
# version. RC/formal tags already match package.json; sprint tags
|
||||||
|
# (vX.Y.Z-dev-YYYYMMDD) only share its prefix, so pin it here for the build.
|
||||||
|
- name: Pin package version to tag
|
||||||
|
if: ${{ inputs.CI_BUILD_TAG != '' }}
|
||||||
|
run: ./scripts/bump_version.sh "${{ inputs.CI_BUILD_TAG }}"
|
||||||
|
|
||||||
- id: ci-build-pkg
|
- id: ci-build-pkg
|
||||||
name: Build pkg
|
name: Build pkg
|
||||||
run: ./scripts/ci-build-pkg.sh harvester
|
run: ./scripts/ci-build-pkg.sh harvester
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ jobs:
|
|||||||
- name: Setup Nodejs with yarn install
|
- name: Setup Nodejs with yarn install
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
|
|
||||||
|
# The catalog image is tagged with the package.json version. Sprint tags
|
||||||
|
# (vX.Y.Z-dev-YYYYMMDD) only share its prefix, so pin it to the tag here.
|
||||||
|
- name: Pin package version to tag
|
||||||
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
run: ./scripts/bump_version.sh "${{ github.ref_name }}"
|
||||||
|
|
||||||
- name: Build and push UI image
|
- name: Build and push UI image
|
||||||
run: |
|
run: |
|
||||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||||
|
|||||||
+1
-1
@@ -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",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.8 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.8 KiB |
@@ -10,6 +10,7 @@ import { docLink } from '../utils/feature-flags';
|
|||||||
const CATEGORY = {
|
const CATEGORY = {
|
||||||
ui: [
|
ui: [
|
||||||
'branding',
|
'branding',
|
||||||
|
HCI_SETTING.SERVER_FLAVOR,
|
||||||
'ui-source',
|
'ui-source',
|
||||||
'ui-index',
|
'ui-index',
|
||||||
]
|
]
|
||||||
@@ -53,7 +54,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
computed: { ...mapGetters({ t: 'i18n/t', isHarvesterPrime: 'harvester-common/isHarvesterPrime' }) },
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -269,7 +270,8 @@ export default {
|
|||||||
v-if="!setting.hide"
|
v-if="!setting.hide"
|
||||||
class="settings-value"
|
class="settings-value"
|
||||||
>
|
>
|
||||||
<pre v-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
<pre v-if="setting.id === 'branding' && isHarvesterPrime">{{ $store.getters['harvester-common/privateLabel'] }}</pre>
|
||||||
|
<pre v-else-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
||||||
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
|
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
|
||||||
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
|
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
|
||||||
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
||||||
|
|||||||
@@ -77,6 +77,14 @@ const FEATURE_FLAGS = {
|
|||||||
'staticIPForVM',
|
'staticIPForVM',
|
||||||
'fsFreezeDeadline',
|
'fsFreezeDeadline',
|
||||||
],
|
],
|
||||||
|
'v1.9.1': [
|
||||||
|
'lvmDmThinOnly',
|
||||||
|
'highPerformanceStorage',
|
||||||
|
],
|
||||||
|
'v1.10.0': [
|
||||||
|
'maintenanceModeConditions',
|
||||||
|
'maintenanceModeDrainTimeoutSetting',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import {
|
|||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
import { ADD_ONS } from './harvester-map';
|
import { ADD_ONS } from './harvester-map';
|
||||||
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
||||||
|
import { setVendor } from '@shell/config/private-label';
|
||||||
|
import { createCssVars } from '@shell/utils/color';
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
@@ -109,15 +111,51 @@ export function init($plugin, store) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
store.watch(
|
||||||
|
(_state, getters) => JSON.stringify([
|
||||||
|
!!getters['harvester-common/isHarvesterPrime'],
|
||||||
|
getters['prefs/theme'] === 'dark',
|
||||||
|
getters['harvester-common/privateLabel']
|
||||||
|
]),
|
||||||
|
(value) => {
|
||||||
|
const [isHarvesterPrime, isDark, privateLabel] = JSON.parse(value);
|
||||||
|
const primeLogo = isDark ? require('../assets/images/dark/suse-virtualization.svg') : require('../assets/images/suse-virtualization.svg');
|
||||||
|
|
||||||
|
setVendor(privateLabel || '');
|
||||||
|
|
||||||
|
let primaryColorStyle = document.getElementById('harvester-prime-primary-color');
|
||||||
|
|
||||||
|
// set harvester prime primary color style to #4dd192 (Cyan Green)
|
||||||
|
if (isHarvesterPrime) {
|
||||||
|
if (!primaryColorStyle) {
|
||||||
|
primaryColorStyle = document.createElement('style');
|
||||||
|
primaryColorStyle.id = 'harvester-prime-primary-color';
|
||||||
|
document.head.appendChild(primaryColorStyle);
|
||||||
|
primaryColorStyle.sheet.insertRule('body.theme-light, body.theme-dark {}', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = createCssVars('#4dd192', isDark ? 'dark' : 'light');
|
||||||
|
const rule = primaryColorStyle.sheet.cssRules[0];
|
||||||
|
|
||||||
|
Object.entries(colors).forEach(([property, color]) => rule.style.setProperty(property.trim(), color));
|
||||||
|
} else {
|
||||||
|
primaryColorStyle?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const primeLabelKey = privateLabel ? 'harvester.branding.primeLabel' : 'harvester.branding.primeEmptyLabel';
|
||||||
|
|
||||||
store.dispatch('setIsSingleProduct', {
|
store.dispatch('setIsSingleProduct', {
|
||||||
productName: PRODUCT_NAME,
|
productName: PRODUCT_NAME,
|
||||||
logo: require(`@shell/assets/images/providers/harvester.svg`),
|
logo: isHarvesterPrime ? primeLogo : require(`@shell/assets/images/providers/harvester.svg`),
|
||||||
productNameKey: 'harvester.productLabel',
|
productNameKey: isHarvesterPrime ? primeLabelKey : 'harvester.productLabel',
|
||||||
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
|
getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown',
|
||||||
afterLoginRoute: home,
|
afterLoginRoute: home,
|
||||||
logoRoute: home,
|
logoRoute: home,
|
||||||
supportCustomLogo: true
|
supportCustomLogo: !isHarvesterPrime
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
product({
|
product({
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const HCI = {
|
|||||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||||
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
|
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
|
||||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status', // Deprecated >= v1.10.0
|
||||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||||
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
||||||
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const HCI_SETTING = {
|
|||||||
CONTAINERD_REGISTRY: 'containerd-registry',
|
CONTAINERD_REGISTRY: 'containerd-registry',
|
||||||
LOG_LEVEL: 'log-level',
|
LOG_LEVEL: 'log-level',
|
||||||
SERVER_VERSION: 'server-version',
|
SERVER_VERSION: 'server-version',
|
||||||
|
SERVER_FLAVOR: 'server-flavor',
|
||||||
UI_INDEX: 'ui-index',
|
UI_INDEX: 'ui-index',
|
||||||
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
||||||
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
||||||
@@ -44,7 +45,8 @@ export const HCI_SETTING = {
|
|||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
||||||
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard',
|
||||||
|
MAINTENANCE_MODE_DRAIN_TIMEOUT: 'maintenance-mode-drain-timeout'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -68,6 +70,7 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||||
|
[HCI_SETTING.SERVER_FLAVOR]: { readOnly: true },
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||||
@@ -146,6 +149,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.MAINTENANCE_MODE_DRAIN_TIMEOUT]: {
|
||||||
|
kind: 'number', canReset: true, featureFlag: 'maintenanceModeDrainTimeoutSetting'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/V
|
|||||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
import { getBootDevices } from '../../utils/vm';
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -74,18 +75,8 @@ export default {
|
|||||||
return image?.spec?.displayName || '-';
|
return image?.spec?.displayName || '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
disks() {
|
bootDevices() {
|
||||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
return getBootDevices(this?.spec?.template?.spec?.domain?.devices);
|
||||||
|
|
||||||
return disks.filter((disk) => {
|
|
||||||
return !!disk.bootOrder;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
if (a.bootOrder < b.bootOrder) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cdroms() {
|
cdroms() {
|
||||||
@@ -101,8 +92,10 @@ export default {
|
|||||||
getDeviceType(o) {
|
getDeviceType(o) {
|
||||||
if (o.disk) {
|
if (o.disk) {
|
||||||
return 'Disk';
|
return 'Disk';
|
||||||
} else {
|
} else if (o.cdrom) {
|
||||||
return 'CD-ROM';
|
return 'CD-ROM';
|
||||||
|
} else {
|
||||||
|
return 'Network';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
@@ -184,10 +177,10 @@ export default {
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(disk, i) in disks"
|
v-for="(device, i) in bootDevices"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
{{ device.bootOrder }}. {{ device.name }} ({{ getDeviceType(device) }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,6 +226,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/V
|
|||||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
import { getBootDevices } from '../../utils/vm';
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -74,18 +75,8 @@ export default {
|
|||||||
return image?.spec?.displayName || '-';
|
return image?.spec?.displayName || '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
disks() {
|
bootDevices() {
|
||||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
return getBootDevices(this?.spec?.template?.spec?.domain?.devices);
|
||||||
|
|
||||||
return disks.filter((disk) => {
|
|
||||||
return !!disk.bootOrder;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
if (a.bootOrder < b.bootOrder) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cdroms() {
|
cdroms() {
|
||||||
@@ -101,8 +92,10 @@ export default {
|
|||||||
getDeviceType(o) {
|
getDeviceType(o) {
|
||||||
if (o.disk) {
|
if (o.disk) {
|
||||||
return 'Disk';
|
return 'Disk';
|
||||||
} else {
|
} else if (o.cdrom) {
|
||||||
return 'CD-ROM';
|
return 'CD-ROM';
|
||||||
|
} else {
|
||||||
|
return 'Network';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
@@ -184,10 +177,10 @@ export default {
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(disk, i) in disks"
|
v-for="(device, i) in bootDevices"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
{{ device.bootOrder }}. {{ device.name }} ({{ getDeviceType(device) }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,6 +227,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
+9
-16
@@ -6,6 +6,7 @@ import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
|||||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||||
|
import { getBootDevices } from '../../../utils/vm';
|
||||||
|
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
@@ -69,18 +70,8 @@ export default {
|
|||||||
return image?.spec?.displayName || 'N/A';
|
return image?.spec?.displayName || 'N/A';
|
||||||
},
|
},
|
||||||
|
|
||||||
disks() {
|
bootDevices() {
|
||||||
const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
|
return getBootDevices(this.value?.spec?.template?.spec?.domain?.devices);
|
||||||
|
|
||||||
return disks.filter((disk) => {
|
|
||||||
return !!disk.bootOrder;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
if (a.bootOrder < b.bootOrder) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cdroms() {
|
cdroms() {
|
||||||
@@ -122,8 +113,10 @@ export default {
|
|||||||
getDeviceType(o) {
|
getDeviceType(o) {
|
||||||
if (o.disk) {
|
if (o.disk) {
|
||||||
return 'Disk';
|
return 'Disk';
|
||||||
} else {
|
} else if (o.cdrom) {
|
||||||
return 'CD-ROM';
|
return 'CD-ROM';
|
||||||
|
} else {
|
||||||
|
return 'Network';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
@@ -219,16 +212,16 @@ export default {
|
|||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<InputOrDisplay
|
<InputOrDisplay
|
||||||
:name="t('harvester.virtualMachine.detail.details.bootOrder')"
|
:name="t('harvester.virtualMachine.detail.details.bootOrder')"
|
||||||
:value="disks"
|
:value="bootDevices"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
>
|
>
|
||||||
<template #value>
|
<template #value>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(disk, i) in disks"
|
v-for="(device, i) in bootDevices"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
{{ device.bootOrder }}. {{ device.name }} ({{ getDeviceType(device) }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -214,6 +214,15 @@ 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 });
|
||||||
|
this['networkBootFirst'] = this.isNetworkBootFirst(neu.spec);
|
||||||
|
|
||||||
|
// 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 +263,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>
|
||||||
|
|
||||||
@@ -265,6 +277,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
:network-boot-first="networkBootFirst"
|
||||||
mode="view"
|
mode="view"
|
||||||
:vm="value"
|
:vm="value"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|||||||
import YamlEditor from '@shell/components/YamlEditor';
|
import YamlEditor from '@shell/components/YamlEditor';
|
||||||
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FormValidation from '@shell/mixins/form-validation';
|
||||||
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -23,7 +24,7 @@ export default {
|
|||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView, FormValidation],
|
||||||
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
|
||||||
@@ -31,6 +32,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
config: this.value.data?.cloudInit || '',
|
config: this.value.data?.cloudInit || '',
|
||||||
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
|
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
|
||||||
|
fvFormRuleSets: [{
|
||||||
|
path: 'metadata.name',
|
||||||
|
rules: ['required'],
|
||||||
|
translationKey: 'nameNsDescription.name.label',
|
||||||
|
}],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -78,6 +84,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
|
:validation-passed="fvFormIsValid"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors=e"
|
||||||
@@ -86,6 +93,7 @@ export default {
|
|||||||
:value="value"
|
:value="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:namespaced="true"
|
:namespaced="true"
|
||||||
|
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
||||||
@update:value="$emit('update:value', $event)"
|
@update:value="$emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -7,8 +7,12 @@ import { allHash } from '@shell/utils/promise';
|
|||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { _CREATE, _CLONE } from '@shell/config/query-params';
|
||||||
import { LVM_TOPOLOGY_LABEL } from '../index.vue';
|
import { LVM_TOPOLOGY_LABEL } from '../index.vue';
|
||||||
|
|
||||||
|
const STRIPED_TYPE = 'striped';
|
||||||
|
const DM_THIN_TYPE = 'dm-thin';
|
||||||
|
|
||||||
const DEFAULT_PARAMETERS = [
|
const DEFAULT_PARAMETERS = [
|
||||||
'type',
|
'type',
|
||||||
'vgName'
|
'vgName'
|
||||||
@@ -54,8 +58,17 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
const node = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).find((t) => t.key === LVM_TOPOLOGY_LABEL)?.values[0];
|
const node = (this.value.allowedTopologies?.[0]?.matchLabelExpressions || []).find((t) => t.key === LVM_TOPOLOGY_LABEL)?.values[0];
|
||||||
|
|
||||||
|
// striped is dropped once the LVM add-on reaches GA (v1.9.1); dm-thin is the only supported type.
|
||||||
|
const dmThinOnly = this.$store.getters['harvester-common/getFeatureEnabled']('lvmDmThinOnly');
|
||||||
|
const volumeGroupTypes = dmThinOnly ? [DM_THIN_TYPE] : [STRIPED_TYPE, DM_THIN_TYPE];
|
||||||
|
|
||||||
|
// Default new storage classes to dm-thin, and reset a cloned striped type that is no longer selectable.
|
||||||
|
if (dmThinOnly && [_CREATE, _CLONE].includes(this.realMode) && !volumeGroupTypes.includes(this.value.parameters.type)) {
|
||||||
|
this.value.parameters.type = DM_THIN_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
volumeGroupTypes: ['striped', 'dm-thin'],
|
volumeGroupTypes,
|
||||||
node,
|
node,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -321,6 +324,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
v-model:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import Conditions from '@shell/components/form/Conditions';
|
import Conditions from '@shell/components/form/Conditions';
|
||||||
|
import DiskPerformanceOptions from './kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import jsyaml from 'js-yaml';
|
import jsyaml from 'js-yaml';
|
||||||
@@ -47,7 +48,8 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
Conditions
|
Conditions,
|
||||||
|
DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -100,7 +102,15 @@ export default {
|
|||||||
createWithDataVolume: false,
|
createWithDataVolume: false,
|
||||||
snapshots: [],
|
snapshots: [],
|
||||||
images: [],
|
images: [],
|
||||||
GIBIBYTE
|
GIBIBYTE,
|
||||||
|
// Default KubeVirt high-performance disk profile, stored as annotations and
|
||||||
|
// applied when this volume is later attached to a VM as a disk.
|
||||||
|
perf: {
|
||||||
|
cache: get(this.value, `metadata.annotations."${ HCI_ANNOTATIONS.DISK_CACHE_MODE }"`) || '',
|
||||||
|
io: get(this.value, `metadata.annotations."${ HCI_ANNOTATIONS.DISK_IO_MODE }"`) || '',
|
||||||
|
dedicatedIOThread: get(this.value, `metadata.annotations."${ HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD }"`) === 'true',
|
||||||
|
volumeMode: this.value?.spec?.volumeMode,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -332,6 +342,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'value.spec.volumeMode'(neu) {
|
'value.spec.volumeMode'(neu) {
|
||||||
|
this.perf.volumeMode = neu;
|
||||||
if (neu === VOLUME_MODE.FILE_SYSTEM) {
|
if (neu === VOLUME_MODE.FILE_SYSTEM) {
|
||||||
this.setVolumeForVmAnnotation();
|
this.setVolumeForVmAnnotation();
|
||||||
} else if (neu === VOLUME_MODE.BLOCK ) {
|
} else if (neu === VOLUME_MODE.BLOCK ) {
|
||||||
@@ -446,6 +457,23 @@ export default {
|
|||||||
imageAnnotations = { ...this.value.metadata.annotations };
|
imageAnnotations = { ...this.value.metadata.annotations };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the default high-performance disk profile as annotations.
|
||||||
|
if (this.perf.cache) {
|
||||||
|
imageAnnotations[HCI_ANNOTATIONS.DISK_CACHE_MODE] = this.perf.cache;
|
||||||
|
} else {
|
||||||
|
delete imageAnnotations[HCI_ANNOTATIONS.DISK_CACHE_MODE];
|
||||||
|
}
|
||||||
|
if (this.perf.io) {
|
||||||
|
imageAnnotations[HCI_ANNOTATIONS.DISK_IO_MODE] = this.perf.io;
|
||||||
|
} else {
|
||||||
|
delete imageAnnotations[HCI_ANNOTATIONS.DISK_IO_MODE];
|
||||||
|
}
|
||||||
|
if (this.perf.dedicatedIOThread) {
|
||||||
|
imageAnnotations[HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD] = 'true';
|
||||||
|
} else {
|
||||||
|
delete imageAnnotations[HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD];
|
||||||
|
}
|
||||||
|
|
||||||
const spec = {
|
const spec = {
|
||||||
...this.value.spec,
|
...this.value.spec,
|
||||||
resources: { requests: { storage: this.storage } },
|
resources: { requests: { storage: this.storage } },
|
||||||
@@ -610,6 +638,16 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.volume.performance.pvcTip')"
|
||||||
|
/>
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="perf"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="!isCreate"
|
v-if="!isCreate"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
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 { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
@@ -33,7 +34,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
LabeledInput, LabeledSelect, InputOrDisplay
|
LabeledInput, LabeledSelect, InputOrDisplay, Checkbox
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -169,6 +170,7 @@ export default {
|
|||||||
this.value.isPod = true;
|
this.value.isPod = true;
|
||||||
this.value.macAddress = '';
|
this.value.macAddress = '';
|
||||||
this.value.staticIp = '';
|
this.value.staticIp = '';
|
||||||
|
this.value.networkBoot = false;
|
||||||
} else {
|
} else {
|
||||||
this.value.isPod = false;
|
this.value.isPod = false;
|
||||||
}
|
}
|
||||||
@@ -285,6 +287,22 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isMasquerade"
|
||||||
|
data-testid="input-hen-networkBoot"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="value.networkBoot"
|
||||||
|
:label="t('harvester.virtualMachine.network.networkBoot.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.network.networkBoot.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!isMasquerade && isSingle">
|
<div v-if="!isMasquerade && isSingle">
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
@@ -10,7 +11,11 @@ import { HCI as HCI_ANNOTATIONS } from '../../../config/labels-annotations';
|
|||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { InfoBox, Base },
|
components: {
|
||||||
|
InfoBox, Base, RadioGroup
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:value', 'update:networkBootFirst'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
vm: {
|
vm: {
|
||||||
@@ -35,6 +40,11 @@ export default {
|
|||||||
isSingle: {
|
isSingle: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
networkBootFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -82,6 +92,20 @@ export default {
|
|||||||
|
|
||||||
vmState() {
|
vmState() {
|
||||||
return this.vm?.stateDisplay;
|
return this.vm?.stateDisplay;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasNetworkBoot() {
|
||||||
|
return this.rows.some((R) => R.networkBoot);
|
||||||
|
},
|
||||||
|
|
||||||
|
networkBootOrderOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('harvester.virtualMachine.network.networkBoot.first'),
|
||||||
|
value: true
|
||||||
|
}, {
|
||||||
|
label: this.t('harvester.virtualMachine.network.networkBoot.last'),
|
||||||
|
value: false
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -218,6 +242,22 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="hasNetworkBoot"
|
||||||
|
data-testid="input-hen-networkBootFirst"
|
||||||
|
class="mb-20"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
:value="networkBootFirst"
|
||||||
|
name="networkBootFirst"
|
||||||
|
:label="t('harvester.virtualMachine.network.networkBoot.order')"
|
||||||
|
:options="networkBootOrderOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:row="true"
|
||||||
|
@update:value="$emit('update:networkBootFirst', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="!isView"
|
v-if="!isView"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { DISK_CACHE_MODE, DISK_IO_MODE, DISK_PERFORMANCE_PROFILE } from '../../../config/harvester-map';
|
||||||
|
|
||||||
|
const { DEFAULT, HIGH, CUSTOM } = DISK_PERFORMANCE_PROFILE;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DiskPerformanceOptions',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledSelect, Checkbox, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
// The disk row. This component reads/writes value.cache, value.io,
|
||||||
|
// value.dedicatedIOThread and (for the "High Performance" preset) value.bus.
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const hasPerf = !!(this.value.cache || this.value.io || this.value.dedicatedIOThread);
|
||||||
|
|
||||||
|
return {
|
||||||
|
DISK_PERFORMANCE_PROFILE,
|
||||||
|
expanded: hasPerf,
|
||||||
|
profile: this.detectProfile(),
|
||||||
|
// Bus the disk used before the "High Performance" preset switched it to
|
||||||
|
// virtio, so returning to "Default" can put it back.
|
||||||
|
busBeforeHighProfile: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
storagePerformanceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('highPerformanceStorage');
|
||||||
|
},
|
||||||
|
|
||||||
|
isCdRom() {
|
||||||
|
return this.value.type === 'cd-rom';
|
||||||
|
},
|
||||||
|
|
||||||
|
isCustom() {
|
||||||
|
return this.profile === CUSTOM;
|
||||||
|
},
|
||||||
|
|
||||||
|
isNativeIo() {
|
||||||
|
return this.value.io === 'native';
|
||||||
|
},
|
||||||
|
|
||||||
|
profileOptions() {
|
||||||
|
return [DEFAULT, HIGH, CUSTOM].map((value) => ({
|
||||||
|
label: this.t(`harvester.virtualMachine.volume.performance.profile.${ value }`),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheOptions() {
|
||||||
|
return DISK_CACHE_MODE.map((value) => ({
|
||||||
|
label: this.cacheLabel(value),
|
||||||
|
value,
|
||||||
|
// Native AIO only works with an uncached (O_DIRECT) disk, so while it is
|
||||||
|
// selected the only valid cache option is "none" — disable everything
|
||||||
|
// else, including "Default", to prevent an invalid combination.
|
||||||
|
disabled: this.isNativeIo && value !== 'none',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
ioOptions() {
|
||||||
|
return DISK_IO_MODE.map((value) => ({
|
||||||
|
label: value === '' ? this.t('harvester.virtualMachine.volume.performance.ioMode.default') : this.t(`harvester.virtualMachine.volume.performance.ioMode.${ value }`),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
showFilesystemCacheWarning() {
|
||||||
|
return this.value.cache === 'none' && this.value.volumeMode === 'Filesystem';
|
||||||
|
},
|
||||||
|
|
||||||
|
showBusTip() {
|
||||||
|
const hasPerf = !!(this.value.cache || this.value.io || this.value.dedicatedIOThread);
|
||||||
|
|
||||||
|
return hasPerf && this.value.bus && this.value.bus !== 'virtio';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// Keep the profile selector in sync if the row is repopulated (e.g. editing an existing VM).
|
||||||
|
'value.cache'() {
|
||||||
|
this.profile = this.detectProfile();
|
||||||
|
},
|
||||||
|
'value.io'() {
|
||||||
|
this.profile = this.detectProfile();
|
||||||
|
},
|
||||||
|
'value.dedicatedIOThread'() {
|
||||||
|
this.profile = this.detectProfile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
detectProfile() {
|
||||||
|
const { cache, io, dedicatedIOThread } = this.value;
|
||||||
|
|
||||||
|
if (!cache && !io && !dedicatedIOThread) {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache === 'none' && io === 'native' && dedicatedIOThread) {
|
||||||
|
return HIGH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CUSTOM;
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheLabel(value) {
|
||||||
|
if (value === '') {
|
||||||
|
return this.t('harvester.virtualMachine.volume.performance.cacheMode.default');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.t(`harvester.virtualMachine.volume.performance.cacheMode.${ value }`);
|
||||||
|
},
|
||||||
|
|
||||||
|
onProfileChange(profile) {
|
||||||
|
const previous = this.profile;
|
||||||
|
|
||||||
|
this.profile = profile;
|
||||||
|
|
||||||
|
if (profile === DEFAULT) {
|
||||||
|
this.value.cache = '';
|
||||||
|
this.value.io = '';
|
||||||
|
this.value.dedicatedIOThread = false;
|
||||||
|
|
||||||
|
// "Default" means no overrides, so also undo the virtio bus that the
|
||||||
|
// "High Performance" preset applied. Skip it if the user has since
|
||||||
|
// picked a different bus themselves — that choice is theirs to keep.
|
||||||
|
if (this.busBeforeHighProfile !== null) {
|
||||||
|
if (this.value.bus === 'virtio') {
|
||||||
|
this.value.bus = this.busBeforeHighProfile;
|
||||||
|
}
|
||||||
|
this.busBeforeHighProfile = null;
|
||||||
|
}
|
||||||
|
} else if (profile === HIGH) {
|
||||||
|
this.value.cache = 'none';
|
||||||
|
this.value.io = 'native';
|
||||||
|
this.value.dedicatedIOThread = true;
|
||||||
|
|
||||||
|
if (previous !== HIGH) {
|
||||||
|
this.busBeforeHighProfile = this.value.bus || '';
|
||||||
|
}
|
||||||
|
this.value.bus = 'virtio';
|
||||||
|
}
|
||||||
|
// CUSTOM keeps whatever is currently set, including the bus.
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onIoChange(io) {
|
||||||
|
this.value.io = io;
|
||||||
|
// Native AIO requires an uncached disk; force cache to "none" to keep the
|
||||||
|
// combo valid — including when cache is still on its "Default" ('') value,
|
||||||
|
// which libvirt would otherwise reject at domain start.
|
||||||
|
if (io === 'native' && this.value.cache !== 'none') {
|
||||||
|
this.value.cache = 'none';
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.$emit('update');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="!isCdRom && storagePerformanceEnabled"
|
||||||
|
class="disk-performance"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm role-link expand-toggle"
|
||||||
|
:aria-expanded="expanded"
|
||||||
|
:aria-label="expanded ? t('harvester.virtualMachine.volume.performance.collapse') : t('harvester.virtualMachine.volume.performance.expand')"
|
||||||
|
@click.prevent="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon"
|
||||||
|
:class="expanded ? 'icon-chevron-down' : 'icon-chevron-right'"
|
||||||
|
/>
|
||||||
|
{{ t('harvester.virtualMachine.volume.performance.title') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="expanded || isView"
|
||||||
|
class="perf-body mt-10"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-disk-perf-profile"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="profile"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.profile.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.profile.tip')"
|
||||||
|
:options="profileOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="onProfileChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="profile === DISK_PERFORMANCE_PROFILE.HIGH"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.highProfileTip')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isCustom || isView"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-disk-cache"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.cache"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.cacheMode.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.cacheMode.tip')"
|
||||||
|
:options="cacheOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-disk-io"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="value.io"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.ioMode.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.ioMode.tip')"
|
||||||
|
:options="ioOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="onIoChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isCustom || isView"
|
||||||
|
class="row mb-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col span-12"
|
||||||
|
data-testid="input-disk-dedicated-iothread"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="value.dedicatedIOThread"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.dedicatedIOThread.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.performance.dedicatedIOThread.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="isNativeIo"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.ioMode.nativeRequiresNoCacheTip')"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="showFilesystemCacheWarning"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.cacheMode.filesystemWarning')"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="showBusTip"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.virtualMachine.volume.performance.busTip')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.disk-performance {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
.expand-toggle {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
<script>
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { IO_THREADS_POLICY } from '../../../config/harvester-map';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMPerformanceOptions',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
InfoBox, LabeledSelect, LabeledInput, Checkbox, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
blockMultiQueue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadsPolicy: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Whether the current disk set contains a virtio disk (VM context only).
|
||||||
|
hasVirtioDisk: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Whether any disk requests a dedicated I/O thread (drives the auto-enable hint).
|
||||||
|
hasDedicatedIothread: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
// These are specialist tuning options, so keep the panel collapsed unless the
|
||||||
|
// VM already has something configured.
|
||||||
|
return { expanded: !!(this.blockMultiQueue || this.ioThreadsPolicy) };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
storagePerformanceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('highPerformanceStorage');
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadsPolicyOptions() {
|
||||||
|
return IO_THREADS_POLICY.map((value) => ({
|
||||||
|
label: value === '' ? this.t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.default') : this.t(`harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.${ value }`),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// Block Multi-Queue only applies to virtio disks. If the last virtio disk is
|
||||||
|
// removed while it is enabled, clear it so the VM is not left with an invalid
|
||||||
|
// setting the disabled checkbox can no longer surface.
|
||||||
|
hasVirtioDisk(neu) {
|
||||||
|
if (!neu && this.blockMultiQueue) {
|
||||||
|
this.$emit('update:blockMultiQueue', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<InfoBox
|
||||||
|
v-if="storagePerformanceEnabled && (!isView || blockMultiQueue || !!ioThreadsPolicy)"
|
||||||
|
class="vm-perf mt-10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm role-link expand-toggle"
|
||||||
|
:aria-expanded="expanded"
|
||||||
|
:aria-label="expanded ? t('harvester.virtualMachine.volume.diskPerformance.collapse') : t('harvester.virtualMachine.volume.diskPerformance.expand')"
|
||||||
|
@click.prevent="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon"
|
||||||
|
:class="expanded ? 'icon-chevron-down' : 'icon-chevron-right'"
|
||||||
|
/>
|
||||||
|
{{ t('harvester.virtualMachine.volume.diskPerformance.title') }}
|
||||||
|
</button>
|
||||||
|
<h3
|
||||||
|
v-else
|
||||||
|
class="mb-5"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.volume.diskPerformance.title') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<template v-if="expanded || isView">
|
||||||
|
<p class="text-muted mb-15 mt-10">
|
||||||
|
{{ t('harvester.virtualMachine.volume.diskPerformance.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div
|
||||||
|
class="col span-12"
|
||||||
|
data-testid="input-vm-block-multi-queue"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:value="blockMultiQueue"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="!hasVirtioDisk"
|
||||||
|
@update:value="$emit('update:blockMultiQueue', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-if="!hasVirtioDisk && !isView"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-vm-iothreads-policy"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="ioThreadsPolicy"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.tip')"
|
||||||
|
:options="ioThreadsPolicyOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="$emit('update:ioThreadsPolicy', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="ioThreadsPolicy === 'supplementalPool'"
|
||||||
|
class="col span-6"
|
||||||
|
data-testid="input-vm-iothread-count"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:value="ioThreadCount"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="$emit('update:ioThreadCount', Number($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-if="hasDedicatedIothread && !ioThreadsPolicy"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.autoEnabledTip')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</InfoBox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vm-perf {
|
||||||
|
.expand-toggle {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions.vue';
|
||||||
|
|
||||||
|
const mountOptions = (value, { featureEnabled = true } = {}) => ({
|
||||||
|
props: { value, mode: _EDIT },
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$store: {
|
||||||
|
getters: {
|
||||||
|
'harvester-common/getFeatureEnabled': () => featureEnabled,
|
||||||
|
'i18n/t': (key) => key,
|
||||||
|
'i18n/exists': () => true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t: (key) => key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hardDisk = (overrides = {}) => ({
|
||||||
|
type: 'disk', bus: 'virtio', cache: '', io: '', dedicatedIOThread: false, ...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component: DiskPerformanceOptions', () => {
|
||||||
|
it('is hidden when the highPerformanceStorage feature is disabled', () => {
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk(), { featureEnabled: false }));
|
||||||
|
|
||||||
|
expect(wrapper.find('.disk-performance').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders for a hard disk when the feature is enabled', () => {
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk()));
|
||||||
|
|
||||||
|
expect(wrapper.find('.disk-performance').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the High Performance preset and emits update', () => {
|
||||||
|
const value = hardDisk();
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('highPerformance');
|
||||||
|
|
||||||
|
expect(value.cache).toBe('none');
|
||||||
|
expect(value.io).toBe('native');
|
||||||
|
expect(value.dedicatedIOThread).toBe(true);
|
||||||
|
expect(value.bus).toBe('virtio');
|
||||||
|
expect(wrapper.emitted('update')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears all overrides when switching back to Default', () => {
|
||||||
|
const value = hardDisk({
|
||||||
|
cache: 'none', io: 'native', dedicatedIOThread: true
|
||||||
|
});
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
|
||||||
|
expect(value.cache).toBe('');
|
||||||
|
expect(value.io).toBe('');
|
||||||
|
expect(value.dedicatedIOThread).toBe(false);
|
||||||
|
expect(wrapper.emitted('update')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores the original bus when leaving High Performance for Default', () => {
|
||||||
|
const value = hardDisk({ bus: 'sata' });
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('highPerformance');
|
||||||
|
expect(value.bus).toBe('virtio');
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
expect(value.bus).toBe('sata');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a bus the user picked themselves after High Performance', () => {
|
||||||
|
const value = hardDisk({ bus: 'sata' });
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('highPerformance');
|
||||||
|
value.bus = 'scsi';
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
expect(value.bus).toBe('scsi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the bus alone for a disk that never used the High Performance preset', () => {
|
||||||
|
const value = hardDisk({
|
||||||
|
bus: 'sata', cache: 'writeback', io: 'threads'
|
||||||
|
});
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onProfileChange('default');
|
||||||
|
expect(value.bus).toBe('sata');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['a cached state', 'writeback'],
|
||||||
|
['the Default cache', ''],
|
||||||
|
])('forces cache to none when Native I/O is selected from %s', (_label, cache) => {
|
||||||
|
const value = hardDisk({ cache });
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
|
||||||
|
|
||||||
|
wrapper.vm.onIoChange('native');
|
||||||
|
|
||||||
|
expect(value.io).toBe('native');
|
||||||
|
expect(value.cache).toBe('none');
|
||||||
|
expect(wrapper.emitted('update')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables every cache option except none while Native I/O is active', () => {
|
||||||
|
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ io: 'native' })));
|
||||||
|
|
||||||
|
const disabledByValue = wrapper.vm.cacheOptions.reduce((acc, opt) => {
|
||||||
|
acc[opt.value] = opt.disabled;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
expect(disabledByValue['none']).toBe(false);
|
||||||
|
expect(disabledByValue['']).toBe(true);
|
||||||
|
expect(disabledByValue['writeback']).toBe(true);
|
||||||
|
expect(disabledByValue['writethrough']).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,13 +7,14 @@ import UnitInput from '@shell/components/form/UnitInput';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
import VMPerformanceOptions from './VMPerformanceOptions';
|
||||||
|
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { ucFirst, randomStr } from '@shell/utils/string';
|
import { ucFirst, randomStr } from '@shell/utils/string';
|
||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||||
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
@@ -23,10 +24,10 @@ import { EMPTY_IMAGE } from '../../../utils/vm';
|
|||||||
const { READ_WRITE_MANY } = ACCESS_MODE;
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value', 'update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard
|
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard, VMPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -77,6 +78,22 @@ export default {
|
|||||||
resourceType: {
|
resourceType: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// KubeVirt high-performance VM-level features
|
||||||
|
blockMultiQueue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadsPolicy: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
ioThreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,6 +164,14 @@ export default {
|
|||||||
isLHV2VolExpansionFeatureEnabled() {
|
isLHV2VolExpansionFeatureEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasVirtioDisk() {
|
||||||
|
return this.rows.some((R) => R.type === 'disk' && R.bus === 'virtio');
|
||||||
|
},
|
||||||
|
|
||||||
|
hasDedicatedIOThread() {
|
||||||
|
return this.rows.some((R) => R.dedicatedIOThread);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -508,6 +533,18 @@ export default {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VMPerformanceOptions
|
||||||
|
:block-multi-queue="blockMultiQueue"
|
||||||
|
:io-threads-policy="ioThreadsPolicy"
|
||||||
|
:io-thread-count="ioThreadCount"
|
||||||
|
:has-virtio-disk="hasVirtioDisk"
|
||||||
|
:has-dedicated-iothread="hasDedicatedIOThread"
|
||||||
|
:mode="mode"
|
||||||
|
@update:block-multi-queue="$emit('update:blockMultiQueue', $event)"
|
||||||
|
@update:io-threads-policy="$emit('update:ioThreadsPolicy', $event)"
|
||||||
|
@update:io-thread-count="$emit('update:ioThreadCount', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<ModalWithCard
|
<ModalWithCard
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
name="deleteTip"
|
name="deleteTip"
|
||||||
|
|||||||
+8
-1
@@ -4,6 +4,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditContainer',
|
name: 'HarvesterEditContainer',
|
||||||
@@ -11,7 +12,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
LabeledInput, LabeledSelect, InputOrDisplay, Banner
|
LabeledInput, LabeledSelect, InputOrDisplay, Banner, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -153,5 +154,11 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
:label="value.volumeBackups.error.message"
|
:label="value.volumeBackups.error.message"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+16
-1
@@ -14,6 +14,7 @@ import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
|||||||
import { HCI } from '../../../../types';
|
import { HCI } from '../../../../types';
|
||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditExisting',
|
name: 'HarvesterEditExisting',
|
||||||
@@ -21,7 +22,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, Checkbox
|
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, Checkbox, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -172,6 +173,14 @@ export default {
|
|||||||
this.value.storageClassName = pvcResource.spec.storageClassName;
|
this.value.storageClassName = pvcResource.spec.storageClassName;
|
||||||
this.value.volumeMode = pvcResource.spec.volumeMode;
|
this.value.volumeMode = pvcResource.spec.volumeMode;
|
||||||
this.value.shareable = false;
|
this.value.shareable = false;
|
||||||
|
|
||||||
|
// Adopt the volume's default high-performance profile (if any) as a starting point.
|
||||||
|
const ann = pvcResource.metadata?.annotations || {};
|
||||||
|
|
||||||
|
this.value.cache = ann[HCI_ANNOTATIONS.DISK_CACHE_MODE] || '';
|
||||||
|
this.value.io = ann[HCI_ANNOTATIONS.DISK_IO_MODE] || '';
|
||||||
|
this.value.dedicatedIOThread = ann[HCI_ANNOTATIONS.DISK_DEDICATED_IOTHREAD] === 'true';
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -379,5 +388,11 @@ export default {
|
|||||||
class="mb-20"
|
class="mb-20"
|
||||||
:label="value.volumeBackups.error.message"
|
:label="value.volumeBackups.error.message"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import LabelValue from '@shell/components/LabelValue';
|
|||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMImage',
|
name: 'HarvesterEditVMImage',
|
||||||
@@ -22,7 +23,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
|
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -482,5 +483,11 @@ export default {
|
|||||||
color="error"
|
color="error"
|
||||||
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
|
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { formatSi, parseSi } from '@shell/utils/units';
|
|||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { _VIEW } from '@shell/config/query-params';
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import DiskPerformanceOptions from '../DiskPerformanceOptions';
|
||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||||
@@ -24,7 +25,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
|
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, DiskPerformanceOptions
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -362,6 +363,11 @@ export default {
|
|||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DiskPerformanceOptions
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
@update="update"
|
||||||
|
/>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div
|
<div
|
||||||
v-if="value.volumeEncryptionFeatureEnabled && isView"
|
v-if="value.volumeEncryptionFeatureEnabled && isView"
|
||||||
|
|||||||
@@ -772,6 +772,9 @@ export default {
|
|||||||
>
|
>
|
||||||
<Volume
|
<Volume
|
||||||
v-model:value="diskRows"
|
v-model:value="diskRows"
|
||||||
|
v-model:block-multi-queue="blockMultiQueue"
|
||||||
|
v-model:io-threads-policy="ioThreadsPolicy"
|
||||||
|
v-model:io-thread-count="ioThreadCount"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:custom-volume-mode="customVolumeMode"
|
:custom-volume-mode="customVolumeMode"
|
||||||
:namespace="value.metadata.namespace"
|
:namespace="value.metadata.namespace"
|
||||||
@@ -788,6 +791,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
v-model:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:is-single="isSingle"
|
:is-single="isSingle"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ dialog:
|
|||||||
|
|
||||||
harvester:
|
harvester:
|
||||||
branding:
|
branding:
|
||||||
|
primeLabel: '{vendor}'
|
||||||
|
primeEmptyLabel: ''
|
||||||
logos:
|
logos:
|
||||||
tip: 'Upload a logo to replace the Harvester logo in the top-level navigation header. Image height should be 21 pixels with a max width of 200 pixels. Max file size is 20KB. Accepted formats: JPEG, PNG, SVG.'
|
tip: 'Upload a logo to replace the Harvester logo in the top-level navigation header. Image height should be 21 pixels with a max width of 200 pixels. Max file size is 20KB. Accepted formats: JPEG, PNG, SVG.'
|
||||||
favicon:
|
favicon:
|
||||||
@@ -284,6 +286,7 @@ harvester:
|
|||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
disableMaintenance: Disable Maintenance Mode
|
disableMaintenance: Disable Maintenance Mode
|
||||||
|
clearMaintenance: Clear Maintenance Mode Error
|
||||||
enableCPUManager: Enable CPU Manager
|
enableCPUManager: Enable CPU Manager
|
||||||
disableCPUManager: Disable CPU Manager
|
disableCPUManager: Disable CPU Manager
|
||||||
cordon: Cordon
|
cordon: Cordon
|
||||||
@@ -798,6 +801,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.
|
||||||
@@ -819,6 +872,12 @@ harvester:
|
|||||||
title: Network
|
title: Network
|
||||||
addNetwork: Add Network
|
addNetwork: Add Network
|
||||||
addPort: Add Port
|
addPort: Add Port
|
||||||
|
networkBoot:
|
||||||
|
label: Enable network boot (PXE)
|
||||||
|
tip: Adds this interface to the VM boot order so the guest firmware can boot from the network. Requires a DHCP/TFTP (PXE) server reachable on the selected network; the management network does not provide one.
|
||||||
|
order: Network boot order
|
||||||
|
first: Before volumes
|
||||||
|
last: After volumes
|
||||||
cloudConfig:
|
cloudConfig:
|
||||||
title: Cloud Configuration
|
title: Cloud Configuration
|
||||||
createTemplateTitle: 'Create {name}'
|
createTemplateTitle: 'Create {name}'
|
||||||
@@ -995,6 +1054,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:
|
||||||
@@ -2445,6 +2506,7 @@ advancedSettings:
|
|||||||
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
||||||
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
||||||
'harv-server-version': Harvester server version.
|
'harv-server-version': Harvester server version.
|
||||||
|
'harv-server-flavor': "Indicates the Harvester edition: Prime or Community."
|
||||||
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
|
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
|
||||||
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
|
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
|
||||||
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
|
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
|
||||||
@@ -2480,6 +2542,7 @@ advancedSettings:
|
|||||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||||
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
||||||
|
'harv-maintenance-mode-drain-timeout': 'Maintenance mode drain timeout in minutes. Use 0 to disable the timeout.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export default {
|
|||||||
imageId: '',
|
imageId: '',
|
||||||
diskRows: [],
|
diskRows: [],
|
||||||
networkRows: [],
|
networkRows: [],
|
||||||
|
networkBootFirst: true,
|
||||||
filesystemRows: [],
|
filesystemRows: [],
|
||||||
machineType: '',
|
machineType: '',
|
||||||
machineTypes: [],
|
machineTypes: [],
|
||||||
@@ -195,6 +196,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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -380,6 +385,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(vm, volumeBackups);
|
const diskRows = this.getDiskRows(vm, volumeBackups);
|
||||||
|
|
||||||
const networkRows = this.getNetworkRows(vm, { fromTemplate, init });
|
const networkRows = this.getNetworkRows(vm, { fromTemplate, init });
|
||||||
|
const networkBootFirst = this.isNetworkBootFirst(spec);
|
||||||
const hasCreateVolumes = this.getHasCreatedVolumes(spec) || [];
|
const hasCreateVolumes = this.getHasCreatedVolumes(spec) || [];
|
||||||
|
|
||||||
let { userData = undefined, networkData = undefined } = this.getCloudInitNoCloud(spec);
|
let { userData = undefined, networkData = undefined } = this.getCloudInitNoCloud(spec);
|
||||||
@@ -409,6 +415,10 @@ export default {
|
|||||||
const cpuPinning = this.isCpuPinning(spec);
|
const cpuPinning = this.isCpuPinning(spec);
|
||||||
const cpuModel = spec.template.spec.domain.cpu?.model || '';
|
const cpuModel = spec.template.spec.domain.cpu?.model || '';
|
||||||
|
|
||||||
|
const blockMultiQueue = spec.template.spec.domain?.devices?.blockMultiQueue || false;
|
||||||
|
const ioThreadsPolicy = spec.template.spec.domain?.ioThreadsPolicy || '';
|
||||||
|
const ioThreadCount = spec.template.spec.domain?.ioThreads?.supplementalPoolThreadCount || 2;
|
||||||
|
|
||||||
const secretRef = this.getSecret(spec);
|
const secretRef = this.getSecret(spec);
|
||||||
const accessCredentials = this.getAccessCredentials(spec);
|
const accessCredentials = this.getAccessCredentials(spec);
|
||||||
|
|
||||||
@@ -447,8 +457,13 @@ 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['networkBootFirst'] = networkBootFirst;
|
||||||
this['imageId'] = imageId;
|
this['imageId'] = imageId;
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
@@ -514,6 +529,9 @@ export default {
|
|||||||
storageClassName: '',
|
storageClassName: '',
|
||||||
image: this.imageId,
|
image: this.imageId,
|
||||||
volumeMode: VOLUME_MODE.BLOCK,
|
volumeMode: VOLUME_MODE.BLOCK,
|
||||||
|
cache: '',
|
||||||
|
io: '',
|
||||||
|
dedicatedIOThread: false,
|
||||||
isEncrypted,
|
isEncrypted,
|
||||||
volumeBackups,
|
volumeBackups,
|
||||||
});
|
});
|
||||||
@@ -618,6 +636,9 @@ export default {
|
|||||||
type,
|
type,
|
||||||
storageClassName,
|
storageClassName,
|
||||||
hotpluggable,
|
hotpluggable,
|
||||||
|
cache: DISK?.cache || '',
|
||||||
|
io: DISK?.io || '',
|
||||||
|
dedicatedIOThread: DISK?.dedicatedIOThread || false,
|
||||||
shareable: DISK.shareable || false,
|
shareable: DISK.shareable || false,
|
||||||
volumeStatus,
|
volumeStatus,
|
||||||
dataSource,
|
dataSource,
|
||||||
@@ -658,12 +679,29 @@ export default {
|
|||||||
model: I.model,
|
model: I.model,
|
||||||
networkName: isPod ? MANAGEMENT_NETWORK : network?.multus?.networkName,
|
networkName: isPod ? MANAGEMENT_NETWORK : network?.multus?.networkName,
|
||||||
staticIp: annotations[`${ staticIpPrefix }${ I.name }`] || '',
|
staticIp: annotations[`${ staticIpPrefix }${ I.name }`] || '',
|
||||||
|
networkBoot: !!I.bootOrder,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network boot devices are placed either before or after all volumes in the boot sequence.
|
||||||
|
* Derive which one from an existing spec, so a round trip through the form keeps it.
|
||||||
|
*/
|
||||||
|
isNetworkBootFirst(spec) {
|
||||||
|
const devices = spec?.template?.spec?.domain?.devices || {};
|
||||||
|
const nicOrders = (devices.interfaces || []).map((I) => I.bootOrder).filter(Boolean);
|
||||||
|
const diskOrders = (devices.disks || []).map((D) => D.bootOrder).filter(Boolean);
|
||||||
|
|
||||||
|
if (nicOrders.length === 0 || diskOrders.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(...nicOrders) < Math.min(...diskOrders);
|
||||||
|
},
|
||||||
|
|
||||||
parseVM() {
|
parseVM() {
|
||||||
this.userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
|
this.userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
|
||||||
this.parseOther();
|
this.parseOther();
|
||||||
@@ -876,9 +914,10 @@ export default {
|
|||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
const bootOrderOffset = this.networkBootFirst ? this.networkRows.filter((R) => R.networkBoot).length : 0;
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
const _disk = this.parseDisk(R, index);
|
const _disk = this.parseDisk(R, index + bootOrderOffset);
|
||||||
|
|
||||||
disks.push(_disk);
|
disks.push(_disk);
|
||||||
|
|
||||||
@@ -973,9 +1012,25 @@ export default {
|
|||||||
// `shareable` is an attachment-level opt-in, remove the field
|
// `shareable` is an attachment-level opt-in, remove the field
|
||||||
// inherited from the old spec when the disk row no longer requests it
|
// inherited from the old spec when the disk row no longer requests it
|
||||||
mergedDisks.forEach((disk) => {
|
mergedDisks.forEach((disk) => {
|
||||||
if (disk.shareable && !disks.find((D) => D.name === disk.name)?.shareable) {
|
const parsed = disks.find((D) => D.name === disk.name);
|
||||||
|
|
||||||
|
if (disk.shareable && !parsed?.shareable) {
|
||||||
delete disk.shareable;
|
delete disk.shareable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// High-performance fields are only serialized when requested (see
|
||||||
|
// parseDisk). When editing, mergeDeviceList would otherwise keep the
|
||||||
|
// previous spec's cache/io/dedicatedIOThread even after the row is
|
||||||
|
// switched back to "Default" or the dedicated I/O thread is unchecked,
|
||||||
|
// silently preserving stale values — strip them so the disk matches
|
||||||
|
// what the row actually requests.
|
||||||
|
if (parsed) {
|
||||||
|
['cache', 'io', 'dedicatedIOThread'].forEach((field) => {
|
||||||
|
if (!parsed[field] && field in disk) {
|
||||||
|
delete disk[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let spec = {
|
let spec = {
|
||||||
@@ -1012,6 +1067,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);
|
||||||
@@ -1091,9 +1175,11 @@ export default {
|
|||||||
const networks = [];
|
const networks = [];
|
||||||
const interfaces = [];
|
const interfaces = [];
|
||||||
|
|
||||||
|
let bootOrder = this.networkBootFirst ? 1 : this.diskRows.length + 1;
|
||||||
|
|
||||||
networkRow.forEach( (R) => {
|
networkRow.forEach( (R) => {
|
||||||
const _network = this.parseNetwork(R);
|
const _network = this.parseNetwork(R);
|
||||||
const _interface = this.parseInterface(R);
|
const _interface = this.parseInterface(R, R.networkBoot ? bootOrder++ : undefined);
|
||||||
|
|
||||||
networks.push(_network);
|
networks.push(_network);
|
||||||
interfaces.push(_interface);
|
interfaces.push(_interface);
|
||||||
@@ -1259,6 +1345,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 };
|
||||||
}
|
}
|
||||||
@@ -1336,7 +1435,7 @@ export default {
|
|||||||
return arr.map( (id) => this.getSSHValue(id)).filter( (O) => O !== undefined);
|
return arr.map( (id) => this.getSSHValue(id)).filter( (O) => O !== undefined);
|
||||||
},
|
},
|
||||||
|
|
||||||
parseInterface(R) {
|
parseInterface(R, bootOrder) {
|
||||||
const _interface = {};
|
const _interface = {};
|
||||||
const type = R.type;
|
const type = R.type;
|
||||||
|
|
||||||
@@ -1346,6 +1445,10 @@ export default {
|
|||||||
_interface.macAddress = R.macAddress;
|
_interface.macAddress = R.macAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bootOrder) {
|
||||||
|
_interface.bootOrder = bootOrder;
|
||||||
|
}
|
||||||
|
|
||||||
_interface.model = R.model;
|
_interface.model = R.model;
|
||||||
_interface.name = R.name;
|
_interface.name = R.name;
|
||||||
|
|
||||||
@@ -1853,6 +1956,11 @@ export default {
|
|||||||
delete merged['bridge'];
|
delete merged['bridge'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// boot order is owned by the form, don't inherit a stale one from the old spec
|
||||||
|
if (!iface.bootOrder) {
|
||||||
|
delete merged.bootOrder;
|
||||||
|
}
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ const ALLOW_SYSTEM_LABEL_KEYS = [
|
|||||||
|
|
||||||
const HEALTHY = 'healthy';
|
const HEALTHY = 'healthy';
|
||||||
const WARNING = 'warning';
|
const WARNING = 'warning';
|
||||||
|
const MAINTENANCE_MODE = 'MaintenanceMode';
|
||||||
|
const MAINTENANCE_ERROR = 'Error';
|
||||||
|
const MAINTENANCE_ENTERING_REASONS = ['Validating', 'Draining', 'Evacuating'];
|
||||||
|
const MAINTENANCE_COMPLETED = 'Completed';
|
||||||
|
// Legacy `harvesterhci.io/maintain-status` annotation values (clusters < v1.10)
|
||||||
|
const LEGACY_MAINTENANCE_RUNNING = 'running';
|
||||||
|
const LEGACY_MAINTENANCE_COMPLETED = 'completed';
|
||||||
|
|
||||||
export default class HciNode extends HarvesterResource {
|
export default class HciNode extends HarvesterResource {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
@@ -58,6 +65,14 @@ export default class HciNode extends HarvesterResource {
|
|||||||
total: 1
|
total: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearMaintenance = {
|
||||||
|
action: 'clearMaintenanceMode',
|
||||||
|
enabled: this.hasAction('clearMaintenanceMode'),
|
||||||
|
icon: 'icon icon-fw icon-close',
|
||||||
|
label: this.t('harvester.action.clearMaintenance'),
|
||||||
|
total: 1
|
||||||
|
};
|
||||||
|
|
||||||
const enableCPUManager = {
|
const enableCPUManager = {
|
||||||
action: 'enableCPUManager',
|
action: 'enableCPUManager',
|
||||||
enabled: this.cpuPinningFeatureEnabled && this.hasAction('enableCPUManager') && !this.isCPUManagerEnableInProgress && !this.isCPUManagerEnabled && !this.isEtcd, // witness node doesn't have CPU manager
|
enabled: this.cpuPinningFeatureEnabled && this.hasAction('enableCPUManager') && !this.isCPUManagerEnableInProgress && !this.isCPUManagerEnabled && !this.isEtcd, // witness node doesn't have CPU manager
|
||||||
@@ -103,6 +118,7 @@ export default class HciNode extends HarvesterResource {
|
|||||||
uncordon,
|
uncordon,
|
||||||
enableMaintenance,
|
enableMaintenance,
|
||||||
disableMaintenance,
|
disableMaintenance,
|
||||||
|
clearMaintenance,
|
||||||
enableCPUManager,
|
enableCPUManager,
|
||||||
disableCPUManager,
|
disableCPUManager,
|
||||||
shutDown,
|
shutDown,
|
||||||
@@ -198,6 +214,10 @@ export default class HciNode extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get stateDisplay() {
|
get stateDisplay() {
|
||||||
|
if (this.isMaintenanceModeError) {
|
||||||
|
return super.stateDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isEnteringMaintenance) {
|
if (this.isEnteringMaintenance) {
|
||||||
return 'Entering maintenance mode';
|
return 'Entering maintenance mode';
|
||||||
}
|
}
|
||||||
@@ -246,6 +266,10 @@ export default class HciNode extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get stateDescription() {
|
get stateDescription() {
|
||||||
|
if (this.isMaintenanceModeError) {
|
||||||
|
return super.stateDescription;
|
||||||
|
}
|
||||||
|
|
||||||
const currentIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.CURRENT_IP];
|
const currentIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.CURRENT_IP];
|
||||||
const initIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.INIT_IP];
|
const initIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.INIT_IP];
|
||||||
|
|
||||||
@@ -325,7 +349,11 @@ export default class HciNode extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disableMaintenanceMode() {
|
disableMaintenanceMode() {
|
||||||
this.doAction('disableMaintenanceMode', {});
|
this.doActionGrowl('disableMaintenanceMode', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMaintenanceMode() {
|
||||||
|
this.doActionGrowl('clearMaintenanceMode', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
enableCPUManager() {
|
enableCPUManager() {
|
||||||
@@ -343,11 +371,32 @@ export default class HciNode extends HarvesterResource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get maintenanceModeCondition() {
|
||||||
|
return (this.status?.conditions || []).find(
|
||||||
|
(c) => c.type === MAINTENANCE_MODE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get maintenanceModeConditionsEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('maintenanceModeConditions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clusters < v1.10 only expose the `harvesterhci.io/maintain-status` annotation
|
||||||
|
get legacyMaintenanceStatus() {
|
||||||
|
return this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS];
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMaintenanceModeError() {
|
||||||
|
return this.maintenanceModeConditionsEnabled && this.maintenanceModeCondition?.reason === MAINTENANCE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
get isMigratable() {
|
get isMigratable() {
|
||||||
const states = ['in-progress', 'unavailable'];
|
const states = ['in-progress', 'unavailable'];
|
||||||
|
|
||||||
|
const engaged = this.maintenanceModeConditionsEnabled ? this.maintenanceModeCondition?.status === 'True' : !!this.legacyMaintenanceStatus;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS] &&
|
!engaged &&
|
||||||
!this.isUnSchedulable &&
|
!this.isUnSchedulable &&
|
||||||
!states.includes(this.state)
|
!states.includes(this.state)
|
||||||
);
|
);
|
||||||
@@ -362,17 +411,26 @@ export default class HciNode extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isEnteringMaintenance() {
|
get isEnteringMaintenance() {
|
||||||
|
if (!this.maintenanceModeConditionsEnabled) {
|
||||||
|
return this.legacyMaintenanceStatus === LEGACY_MAINTENANCE_RUNNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cond = this.maintenanceModeCondition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS] ===
|
cond?.status === 'True' &&
|
||||||
'running'
|
MAINTENANCE_ENTERING_REASONS.includes(cond?.reason)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMaintenance() {
|
get isMaintenance() {
|
||||||
return (
|
if (!this.maintenanceModeConditionsEnabled) {
|
||||||
this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS] ===
|
return this.legacyMaintenanceStatus === LEGACY_MAINTENANCE_COMPLETED;
|
||||||
'completed'
|
}
|
||||||
);
|
|
||||||
|
const cond = this.maintenanceModeCondition;
|
||||||
|
|
||||||
|
return cond?.status === 'True' && cond?.reason === MAINTENANCE_COMPLETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
get cpuPinningFeatureEnabled() {
|
get cpuPinningFeatureEnabled() {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import ManagementSetting from '@shell/models/management.cattle.io.setting';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
|
||||||
|
export default class HarvesterManagementSetting extends ManagementSetting {
|
||||||
|
get _availableActions() {
|
||||||
|
const actions = super._availableActions;
|
||||||
|
|
||||||
|
if (this.$rootGetters['isStandaloneHarvester'] && this.id === HCI_SETTING.UI_PL) {
|
||||||
|
return actions.filter((action) => !['goToClone', 'promptRemove'].includes(action.action));
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
vendor: getVendor(),
|
vendor: getVendor(),
|
||||||
uiPLSetting: {},
|
uiPLSetting: {},
|
||||||
|
privateLabelDraft: null,
|
||||||
uiLogoDarkSetting: {},
|
uiLogoDarkSetting: {},
|
||||||
uiLogoDark: '',
|
uiLogoDark: '',
|
||||||
uiLogoLightSetting: {},
|
uiLogoLightSetting: {},
|
||||||
@@ -81,6 +82,17 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isHarvesterPrime() {
|
||||||
|
return !!this.$store.getters['harvester-common/isHarvesterPrime'];
|
||||||
|
},
|
||||||
|
privateLabel: {
|
||||||
|
get() {
|
||||||
|
return this.privateLabelDraft ?? (this.isHarvesterPrime ? this.$store.getters['harvester-common/privateLabel'] : this.uiPLSetting.value);
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.privateLabelDraft = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
mode() {
|
mode() {
|
||||||
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
|
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
|
||||||
|
|
||||||
@@ -112,7 +124,7 @@ export default {
|
|||||||
this.errors.push(e);
|
this.errors.push(e);
|
||||||
},
|
},
|
||||||
async save(btnCB) {
|
async save(btnCB) {
|
||||||
this.uiPLSetting.value = this.uiPLSetting.value.replaceAll(/[\<>&=#()"]/gm, '');
|
this.uiPLSetting.value = (this.privateLabel || '').replaceAll(/[\<>&=#()"]/gm, '');
|
||||||
if (this.customizeLogo) {
|
if (this.customizeLogo) {
|
||||||
this.uiLogoLightSetting.value = this.uiLogoLight;
|
this.uiLogoLightSetting.value = this.uiLogoLight;
|
||||||
this.uiLogoDarkSetting.value = this.uiLogoDark;
|
this.uiLogoDarkSetting.value = this.uiLogoDark;
|
||||||
@@ -169,7 +181,7 @@ export default {
|
|||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="uiPLSetting.value"
|
v-model:value="privateLabel"
|
||||||
:label="t('branding.uiPL.label')"
|
:label="t('branding.uiPL.label')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:maxlength="100"
|
:maxlength="100"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import Parse from 'url-parse';
|
import Parse from 'url-parse';
|
||||||
|
import { MANAGEMENT } from '@shell/config/types';
|
||||||
|
import { SETTING } from '@shell/config/settings';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { PRODUCT_NAME } from '../config/harvester';
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
import { featureEnabled, getVersion } from '../utils/feature-flags';
|
import { featureEnabled, getVersion } from '../utils/feature-flags';
|
||||||
@@ -64,6 +66,23 @@ const mutations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
|
isHarvesterPrime: (_state, _getters, _rootState, rootGetters) => {
|
||||||
|
const setting = rootGetters['harvester/byId']?.(HCI.SETTING, 'server-flavor');
|
||||||
|
|
||||||
|
return (setting?.value || setting?.default) === 'prime';
|
||||||
|
},
|
||||||
|
|
||||||
|
privateLabel: (_state, getters, _rootState, rootGetters) => {
|
||||||
|
const setting = rootGetters['management/byId']?.(MANAGEMENT.SETTING, SETTING.PL);
|
||||||
|
const value = setting?.value || '';
|
||||||
|
|
||||||
|
if (getters.isHarvesterPrime && value === (setting?.default || 'Harvester')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
getBundleId(state) {
|
getBundleId(state) {
|
||||||
return state.latestBundleId;
|
return state.latestBundleId;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { FAVORITE_TYPES } from '@shell/store/prefs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically toggles SideNav entries based on the enabled status of a specific Addon.
|
* Dynamically toggles SideNav entries based on the enabled status of a specific Addon.
|
||||||
*
|
*
|
||||||
@@ -26,10 +28,13 @@ export function registerAddonSideNav(store, productName, {
|
|||||||
|
|
||||||
// Toggle the trigger a few times so an early kick (fired before the SideNav
|
// Toggle the trigger a few times so an early kick (fired before the SideNav
|
||||||
// has mounted on first login) is retried once the component is listening.
|
// has mounted on first login) is retried once the component is listening.
|
||||||
|
// Use the prefs/load mutation (in-memory only)
|
||||||
[0, 600, 1500].forEach((delay) => {
|
[0, 600, 1500].forEach((delay) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
store.dispatch('type-map/addFavorite', TRIGGER);
|
const base = (store.getters['prefs/get'](FAVORITE_TYPES) || []).filter((t) => t !== TRIGGER);
|
||||||
setTimeout(() => store.dispatch('type-map/removeFavorite', TRIGGER), 300);
|
|
||||||
|
store.commit('prefs/load', { key: FAVORITE_TYPES, value: [...base, TRIGGER] });
|
||||||
|
setTimeout(() => store.commit('prefs/load', { key: FAVORITE_TYPES, value: [...base] }), 300);
|
||||||
}, delay);
|
}, delay);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,3 +11,12 @@ export function parseVolumeClaimTemplates(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_IMAGE = 'EMPTY_IMAGE';
|
export const EMPTY_IMAGE = 'EMPTY_IMAGE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disks and interfaces that take part in the boot sequence, sorted by boot order.
|
||||||
|
*/
|
||||||
|
export function getBootDevices(devices = {}) {
|
||||||
|
return [...(devices.disks || []), ...(devices.interfaces || [])]
|
||||||
|
.filter((device) => !!device.bootOrder)
|
||||||
|
.sort((a, b) => a.bootOrder - b.bootOrder);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2935,10 +2935,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.5":
|
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.6":
|
||||||
version "25.9.5"
|
version "25.9.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.5.tgz#0fefc09e6e82e94cde291bacf43522e989eb01a4"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.6.tgz#47865018f9a5635f161ec69c041385edb15d78fe"
|
||||||
integrity sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==
|
integrity sha512-JR6Q/PV5DKFvjrGFVqQJdeG0qvsqQQLDa3TzFrqVwhqRXqwNaxPo2KYCtCQXpOdIulCouKwT7a6in9nFthBAzw==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types ">=7.24.0 <7.24.7"
|
undici-types ">=7.24.0 <7.24.7"
|
||||||
|
|
||||||
@@ -3091,15 +3091,6 @@
|
|||||||
"@typescript-eslint/types" "^8.62.0"
|
"@typescript-eslint/types" "^8.62.0"
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
|
|
||||||
"@typescript-eslint/project-service@8.68.0":
|
|
||||||
version "8.68.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.68.0.tgz#ea4b2869f59165c420cd7a4bbebc38039794e8cc"
|
|
||||||
integrity sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==
|
|
||||||
dependencies:
|
|
||||||
"@typescript-eslint/tsconfig-utils" "^8.68.0"
|
|
||||||
"@typescript-eslint/types" "^8.68.0"
|
|
||||||
debug "^4.4.3"
|
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@8.62.0":
|
"@typescript-eslint/scope-manager@8.62.0":
|
||||||
version "8.62.0"
|
version "8.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz#a7a7b428d32444bc9a4fe16f24a78fc124283fd4"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz#a7a7b428d32444bc9a4fe16f24a78fc124283fd4"
|
||||||
@@ -3108,20 +3099,12 @@
|
|||||||
"@typescript-eslint/types" "8.62.0"
|
"@typescript-eslint/types" "8.62.0"
|
||||||
"@typescript-eslint/visitor-keys" "8.62.0"
|
"@typescript-eslint/visitor-keys" "8.62.0"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@8.68.0":
|
|
||||||
version "8.68.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz#e5a13a1159497faeab4e48279bf07576045b1499"
|
|
||||||
integrity sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==
|
|
||||||
dependencies:
|
|
||||||
"@typescript-eslint/types" "8.68.0"
|
|
||||||
"@typescript-eslint/visitor-keys" "8.68.0"
|
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@8.62.0":
|
"@typescript-eslint/tsconfig-utils@8.62.0":
|
||||||
version "8.62.0"
|
version "8.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
|
||||||
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
|
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@8.68.0", "@typescript-eslint/tsconfig-utils@^8.62.0", "@typescript-eslint/tsconfig-utils@^8.68.0":
|
"@typescript-eslint/tsconfig-utils@^8.62.0":
|
||||||
version "8.68.0"
|
version "8.68.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz#594d7a3c5952055b3c431fc563ca7fd1defcce18"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz#594d7a3c5952055b3c431fc563ca7fd1defcce18"
|
||||||
integrity sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==
|
integrity sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==
|
||||||
@@ -3142,7 +3125,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
|
||||||
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
|
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
|
||||||
|
|
||||||
"@typescript-eslint/types@8.68.0", "@typescript-eslint/types@^8.62.0", "@typescript-eslint/types@^8.68.0":
|
"@typescript-eslint/types@^8.62.0":
|
||||||
version "8.68.0"
|
version "8.68.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.68.0.tgz#3f9d4e62fbe5728f09403cdc7b4d58af842ac1af"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.68.0.tgz#3f9d4e62fbe5728f09403cdc7b4d58af842ac1af"
|
||||||
integrity sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==
|
integrity sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==
|
||||||
@@ -3162,22 +3145,7 @@
|
|||||||
tinyglobby "^0.2.15"
|
tinyglobby "^0.2.15"
|
||||||
ts-api-utils "^2.5.0"
|
ts-api-utils "^2.5.0"
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@8.68.0":
|
"@typescript-eslint/utils@8.62.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.13.0":
|
||||||
version "8.68.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz#bf4165029825138ac27231a3ff02923ecd977f38"
|
|
||||||
integrity sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==
|
|
||||||
dependencies:
|
|
||||||
"@typescript-eslint/project-service" "8.68.0"
|
|
||||||
"@typescript-eslint/tsconfig-utils" "8.68.0"
|
|
||||||
"@typescript-eslint/types" "8.68.0"
|
|
||||||
"@typescript-eslint/visitor-keys" "8.68.0"
|
|
||||||
debug "^4.4.3"
|
|
||||||
minimatch "^10.2.2"
|
|
||||||
semver "^7.7.3"
|
|
||||||
tinyglobby "^0.2.15"
|
|
||||||
ts-api-utils "^2.5.0"
|
|
||||||
|
|
||||||
"@typescript-eslint/utils@8.62.0":
|
|
||||||
version "8.62.0"
|
version "8.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.0.tgz#b5228524ca1ee51af40e156c82d425dec3e01cfe"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.0.tgz#b5228524ca1ee51af40e156c82d425dec3e01cfe"
|
||||||
integrity sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==
|
integrity sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==
|
||||||
@@ -3187,16 +3155,6 @@
|
|||||||
"@typescript-eslint/types" "8.62.0"
|
"@typescript-eslint/types" "8.62.0"
|
||||||
"@typescript-eslint/typescript-estree" "8.62.0"
|
"@typescript-eslint/typescript-estree" "8.62.0"
|
||||||
|
|
||||||
"@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.13.0":
|
|
||||||
version "8.68.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.68.0.tgz#00547f2c8de8aca2a3c21752a9711f73206fd36d"
|
|
||||||
integrity sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==
|
|
||||||
dependencies:
|
|
||||||
"@eslint-community/eslint-utils" "^4.9.1"
|
|
||||||
"@typescript-eslint/scope-manager" "8.68.0"
|
|
||||||
"@typescript-eslint/types" "8.68.0"
|
|
||||||
"@typescript-eslint/typescript-estree" "8.68.0"
|
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@8.62.0":
|
"@typescript-eslint/visitor-keys@8.62.0":
|
||||||
version "8.62.0"
|
version "8.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz#b6daab190bf8f18612f5b86323469a12288c6b31"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz#b6daab190bf8f18612f5b86323469a12288c6b31"
|
||||||
@@ -3205,14 +3163,6 @@
|
|||||||
"@typescript-eslint/types" "8.62.0"
|
"@typescript-eslint/types" "8.62.0"
|
||||||
eslint-visitor-keys "^5.0.0"
|
eslint-visitor-keys "^5.0.0"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@8.68.0":
|
|
||||||
version "8.68.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz#78db3c9bb258a0309d9e2b1b617127c3a8fb1f54"
|
|
||||||
integrity sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==
|
|
||||||
dependencies:
|
|
||||||
"@typescript-eslint/types" "8.68.0"
|
|
||||||
eslint-visitor-keys "^5.0.0"
|
|
||||||
|
|
||||||
"@vee-validate/zod@4.15.0":
|
"@vee-validate/zod@4.15.0":
|
||||||
version "4.15.0"
|
version "4.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vee-validate/zod/-/zod-4.15.0.tgz#2de193da114eda9f873b5a677f13c23bcecb58aa"
|
resolved "https://registry.yarnpkg.com/@vee-validate/zod/-/zod-4.15.0.tgz#2de193da114eda9f873b5a677f13c23bcecb58aa"
|
||||||
@@ -12712,9 +12662,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
|
|||||||
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
||||||
|
|
||||||
yaml@^2.5.1:
|
yaml@^2.5.1:
|
||||||
version "2.9.0"
|
version "2.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.1.tgz#c16233fb31944e705cfefaff38795587f57588ce"
|
||||||
integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==
|
integrity sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==
|
||||||
|
|
||||||
yargs-parser@^18.1.2:
|
yargs-parser@^18.1.2:
|
||||||
version "18.1.3"
|
version "18.1.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user