Compare commits

...

5 Commits

Author SHA1 Message Date
mergify[bot]
b8111f0ad7
fix: reword the error message to focus on bootable volume (#736) (#738)
(cherry picked from commit a9c392c13feb3cfe4100843eb41e0501e6708aaf)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:46:08 +08:00
mergify[bot]
6d627f82e9
fix: container disks don't need volumeClaimTemplates but volumes (#735) (#737)
(cherry picked from commit 888ec7a50fdc2e8ffc7d5210ae0e39d30c3e43cc)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:45:40 +08:00
mergify[bot]
cfc7a76fe7
feat: add vGPU filter button and hide the enable/disable passthrough in PCIDevice page (#729) (#732)
* feat: add another filter button



* feat: add feature falg to hide vGPU enable/disable actions in PCIDevices page



* refactor: update with conditionally rendering



---------


(cherry picked from commit 23344e0c0759e970bbdc88bc463774053448e0a1)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:39:41 +08:00
mergify[bot]
71d3067354
feat: ensure the state is pending when perform cloning the efi (#730) (#734)
* feat: ensure the state is pending when perform cloning the efi



* feat: define harvesterhci.io/clone-backend-storage-status in labels-annotations.js



---------


(cherry picked from commit a2486a7d389ff86760f8456a73ef2202ed06ca02)

Signed-off-by: pohanhuang <pohan.huang@suse.com>
Co-authored-by: Po Han Huang <hhcs9527@gmail.com>
2026-03-11 15:39:01 +08:00
mergify[bot]
9ecc372009
chore: bump to v1.8.0-rc1 (#731) (#733)
(cherry picked from commit df3d249923a51ea10d9080fecea6c5b942f47f8a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:34:55 +08:00
12 changed files with 107 additions and 39 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.8.0-dev", "version": "1.8.0-rc1",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

View File

@ -61,7 +61,8 @@ const FEATURE_FLAGS = {
'v1.8.0': [ 'v1.8.0': [
'hotplugCdRom', 'hotplugCdRom',
'supportBundleFileNameSetting', 'supportBundleFileNameSetting',
'clusterRegistrationTLSVerify' 'clusterRegistrationTLSVerify',
'vGPUAsPCIDevice',
], ],
}; };

View File

@ -28,6 +28,7 @@ export const HCI = {
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane', NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness', NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status', PROMOTE_STATUS: 'harvesterhci.io/promote-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',
IMAGE_NAME: 'harvesterhci.io/image-name', IMAGE_NAME: 'harvesterhci.io/image-name',

View File

@ -31,6 +31,7 @@ export default {
const _hash = { const _hash = {
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }), pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }), sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
}; };
await allHash(_hash); await allHash(_hash);
@ -106,19 +107,32 @@ export default {
}, },
computed: { computed: {
parentSriovOptions() { allSriovs() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
return allSriovs.map((sriov) => { return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
return sriov.id; },
}); allSriovGPUs() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
},
parentSriovOptions() {
return this.allSriovs.map((sriov) => sriov.id);
},
parentSriovGPUOptions() {
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
}, },
parentSriovLabel() { parentSriovLabel() {
return HCI_ANNOTATIONS.PARENT_SRIOV; return HCI_ANNOTATIONS.PARENT_SRIOV;
}
}, },
parentSriovGPULabel() {
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
},
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
},
methods: { methods: {
enableGroup(rows = []) { enableGroup(rows = []) {
const row = rows[0]; const row = rows[0];
@ -206,6 +220,15 @@ export default {
:rows="rows" :rows="rows"
@change-rows="changeRows" @change-rows="changeRows"
/> />
<FilterBySriov
v-if="vGPUAsPCIDeviceEnabled"
ref="filterByParentSRIOVGPU"
:parent-sriov-options="parentSriovGPUOptions"
:parent-sriov-label="parentSriovGPULabel"
:label="t('harvester.sriov.parentSriovGPU')"
:rows="rows"
@change-rows="changeRows"
/>
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>

View File

@ -8,6 +8,7 @@ import { set } from '@shell/utils/object';
import { HCI } from '../../../types'; import { HCI } from '../../../types';
import DeviceList from './DeviceList'; import DeviceList from './DeviceList';
import CompatibilityMatrix from '../CompatibilityMatrix'; import CompatibilityMatrix from '../CompatibilityMatrix';
import MessageLink from '@shell/components/MessageLink';
export default { export default {
name: 'VirtualMachinePCIDevices', name: 'VirtualMachinePCIDevices',
@ -15,7 +16,8 @@ export default {
LabeledSelect, LabeledSelect,
DeviceList, DeviceList,
CompatibilityMatrix, CompatibilityMatrix,
Banner Banner,
MessageLink
}, },
props: { props: {
mode: { mode: {
@ -138,6 +140,13 @@ export default {
return inUse; return inUse;
}, },
toVGpuDevicesPage() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
};
},
devicesByNode() { devicesByNode() {
return this.enabledDevices?.reduce((acc, device) => { return this.enabledDevices?.reduce((acc, device) => {
const nodeName = device.status?.nodeName; const nodeName = device.status?.nodeName;
@ -232,7 +241,12 @@ export default {
<div class="row"> <div class="row">
<div class="col span-12"> <div class="col span-12">
<Banner color="info"> <Banner color="info">
<t k="harvester.pci.howToUseDevice" /> <MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDevice.prefix"
middle-label="harvester.pci.howToUseDevice.middle"
suffix-label="harvester.pci.howToUseDevice.suffix"
/>
</Banner> </Banner>
<Banner <Banner
v-if="selectedDevices.length > 0" v-if="selectedDevices.length > 0"

View File

@ -211,6 +211,10 @@ export default {
return false; return false;
}, },
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
usbPassthroughEnabled() { usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough'); return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
}, },
@ -740,7 +744,7 @@ export default {
</Tab> </Tab>
<Tab <Tab
v-if="enabledSriovgpu" v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
:label="t('harvester.tab.vGpuDevices')" :label="t('harvester.tab.vGpuDevices')"
name="vGpuDevices" name="vGpuDevices"
:weight="-6" :weight="-6"

View File

@ -355,7 +355,10 @@ harvester:
available: Available Devices available: Available Devices
compatibleNodes: Compatible Nodes compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.' impossibleSelection: 'There are no hosts with all of the selected devices.'
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.' howToUseDevice:
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.<br>For vGPU devices, please enable them on the'
middle: vGPU Devices
suffix: page first.
deviceInTheSameHost: 'You can only select devices on the same host.' deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices: oldFormatDevices:
help: |- help: |-
@ -425,7 +428,7 @@ harvester:
volume: volume:
upperType: Volume name upperType: Volume name
lowerType: volume name lowerType: volume name
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!' needAtLeastOneBootable: 'At least one bootable volume is required!'
image: image:
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.' ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.' ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
@ -1832,7 +1835,8 @@ harvester:
numVFs: Number Of Virtual Functions numVFs: Number Of Virtual Functions
vfAddresses: Virtual Functions Addresses vfAddresses: Virtual Functions Addresses
showMore: Show More showMore: Show More
parentSriov: Filter By Parent SR-IOV parentSriov: Filter By Parent SR-IOV Netork Device
parentSriovGPU: Filter By Parent SR-IOV GPU Device
sriovgpu: sriovgpu:
label: SR-IOV GPU Devices label: SR-IOV GPU Devices

View File

@ -707,18 +707,22 @@ export default {
} }
}, },
needVolume(R) { needVolumeRelatedInfo(R) {
if (R.image === EMPTY_IMAGE) { // return [needVolume, needVolumeClaimTemplate]
return false; if (R.source === SOURCE_TYPE.CONTAINER) {
return [true, false];
} }
return true; if (R.source === SOURCE_TYPE.IMAGE && R.image === EMPTY_IMAGE) {
return [false, false];
}
return [true, true];
}, },
parseDiskRows(disk) { parseDiskRows(disk) {
const disks = []; const disks = [];
const volumes = []; const volumes = [];
const diskNameLabels = [];
const volumeClaimTemplates = []; const volumeClaimTemplates = [];
disk.forEach( (R, index) => { disk.forEach( (R, index) => {
@ -726,14 +730,18 @@ export default {
disks.push(_disk); disks.push(_disk);
if (this.needVolume(R)) {
const prefixName = this.value.metadata?.name || ''; const prefixName = this.value.metadata?.name || '';
const dataVolumeName = this.parseDataVolumeName(R, prefixName); const dataVolumeName = this.parseDataVolumeName(R, prefixName);
const [needVolume, needVolumeClaimTemplate] = this.needVolumeRelatedInfo(R);
if (needVolume) {
const _volume = this.parseVolume(R, dataVolumeName); const _volume = this.parseVolume(R, dataVolumeName);
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
volumes.push(_volume); volumes.push(_volume);
diskNameLabels.push(dataVolumeName); }
if (needVolumeClaimTemplate) {
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
volumeClaimTemplates.push(_dataVolumeTemplate); volumeClaimTemplates.push(_dataVolumeTemplate);
} }
}); });

View File

@ -1,6 +1,7 @@
import SteveModel from '@shell/plugins/steve/steve-class'; import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types'; import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
const STATUS_DISPLAY = { const STATUS_DISPLAY = {
enabled: { enabled: {
@ -32,7 +33,7 @@ export default class PCIDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.isEnabling, enabled: !this.isEnabling && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@ -41,7 +42,7 @@ export default class PCIDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe, enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@ -52,6 +53,14 @@ export default class PCIDevice extends SteveModel {
return out; return out;
} }
get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false;
}
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU] || this.status?.resourceName.includes('nvidia.com');
}
get canYaml() { get canYaml() {
return false; return false;
} }
@ -176,6 +185,10 @@ export default class PCIDevice extends SteveModel {
return this.status?.description; return this.status?.description;
} }
get vGPUAsPCIDeviceFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
}
showDetachWarning() { showDetachWarning() {
this.$dispatch('growl/warning', { this.$dispatch('growl/warning', {
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'), title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),

View File

@ -770,11 +770,11 @@ export default class VirtVm extends HarvesterResource {
} }
get isPending() { get isPending() {
if (this && if ((this &&
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
this.isVMCreated && this.isVMCreated &&
this.vmi?.status?.phase === VMIPhase.Pending this.vmi?.status?.phase === VMIPhase.Pending
) { ) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
return { status: VMIPhase.Pending }; return { status: VMIPhase.Pending };
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.8.0-dev", "version": "1.8.0-rc1",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {

View File

@ -69,7 +69,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType); validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
}); });
let requiredVolume = false; let hasBootableVolume = false;
_volumes.forEach((V, idx) => { _volumes.forEach((V, idx) => {
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value); const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
@ -77,7 +77,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
const prefix = V.name || idx + 1; const prefix = V.name || idx + 1;
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) { if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
requiredVolume = true; hasBootableVolume = true;
} }
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) { if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
@ -137,10 +137,10 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
}); });
/** /**
* At least one volume must be create. (Verify only when create.) * At least one bootable volume must be provided. (Verify only when create.)
*/ */
if ((!requiredVolume || _volumes.length === 0) && !value.links) { if (!hasBootableVolume && !value.links) {
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting')); errors.push(getters['i18n/t']('harvester.validation.vm.volume.needAtLeastOneBootable'));
} }
return errors; return errors;