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",
"version": "1.8.0-dev",
"version": "1.8.0-rc1",
"private": false,
"engines": {
"node": ">=20.0.0"

View File

@ -61,7 +61,8 @@ const FEATURE_FLAGS = {
'v1.8.0': [
'hotplugCdRom',
'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_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',

View File

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

View File

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

View File

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

View File

@ -355,7 +355,10 @@ harvester:
available: Available Devices
compatibleNodes: Compatible Nodes
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.'
oldFormatDevices:
help: |-
@ -425,7 +428,7 @@ harvester:
volume:
upperType: 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:
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.'
@ -1832,7 +1835,8 @@ harvester:
numVFs: Number Of Virtual Functions
vfAddresses: Virtual Functions Addresses
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:
label: SR-IOV GPU Devices

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.8.0-dev",
"version": "1.8.0-rc1",
"private": false,
"rancher": {
"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);
});
let requiredVolume = false;
let hasBootableVolume = false;
_volumes.forEach((V, idx) => {
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;
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) {
@ -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) {
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting'));
if (!hasBootableVolume && !value.links) {
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needAtLeastOneBootable'));
}
return errors;